diff --git a/agent/CHANGELOG.md b/agent/CHANGELOG.md index ff184a344b4e..319215c54169 100644 --- a/agent/CHANGELOG.md +++ b/agent/CHANGELOG.md @@ -10,10 +10,21 @@ This is a log of all notable changes to the Cody command-line tool. [Unreleased] ### Changed -## 0.1.1 +## 0.1.2 + +### Changed + +- The "service account name" for storing secrets is now formatted as "Cody: + $SERVER_ENDPOINT ($USERNAME)" instead of "Cody" making it easier to + understand what account/endpoint is stored there. ### Fixed +- Running `cody-agent help` should work now. It was previously crashing about a missing keytar dependencies. + +## 0.1.1 +### Fixed + - Running `npm install -g @sourcegraph/cody-agent` should work now. It was previously crashing about a missing keytar dependency. ## 0.1.0 diff --git a/agent/package.json b/agent/package.json index ad19885773d0..d41cd19ad61c 100644 --- a/agent/package.json +++ b/agent/package.json @@ -1,6 +1,6 @@ { "name": "@sourcegraph/cody-agent", - "version": "0.1.1", + "version": "0.1.2", "description": "Cody JSON-RPC agent for consistent cross-editor support", "license": "Apache-2.0", "repository": { @@ -24,13 +24,7 @@ "prepublishOnly": "pnpm run build" }, "bin": "dist/index.js", - "files": [ - "dist/index.js", - "dist/index.js.map", - "dist/*.wasm", - "dist/win-ca-roots.exe", - "dist/keytar-*.node" - ], + "files": ["dist/index.js", "dist/index.js.map", "dist/*.wasm", "dist/win-ca-roots.exe"], "peerDependencies": { "@inquirer/prompts": "^5.0.7", "@pollyjs/core": "^6.0.6", @@ -48,7 +42,6 @@ "fast-myers-diff": "^3.2.0", "glob": "^7.2.3", "js-levenshtein": "^1.1.6", - "keytar": "^7.9.0", "lodash": "^4.17.21", "mac-ca": "^2.0.3", "minimatch": "^9.0.3", diff --git a/agent/src/cli/auth/AuthenticatedAccount.ts b/agent/src/cli/auth/AuthenticatedAccount.ts index 441cde19210a..1299519bd450 100644 --- a/agent/src/cli/auth/AuthenticatedAccount.ts +++ b/agent/src/cli/auth/AuthenticatedAccount.ts @@ -39,23 +39,24 @@ export class AuthenticatedAccount { return this.account.serverEndpoint } - public static async fromUserSettings(spinner?: Ora): Promise { + public static async fromUserSettings(spinner: Ora): Promise { const settings = loadUserSettings() if (!settings.activeAccountID) { return undefined } const account = settings.accounts?.find(({ id }) => id === settings.activeAccountID) if (!account) { - spinner?.fail(`Failed to find active account ${settings.activeAccountID}`) + spinner.fail(`Failed to find active account ${settings.activeAccountID}`) return undefined } - return AuthenticatedAccount.fromUnauthenticated(account) + return AuthenticatedAccount.fromUnauthenticated(spinner, account) } public static async fromUnauthenticated( + spinner: Ora, account: Account ): Promise { - const accessToken = await readCodySecret(account) + const accessToken = await readCodySecret(spinner, account) if (!accessToken) { return undefined } diff --git a/agent/src/cli/auth/command-accounts.ts b/agent/src/cli/auth/command-accounts.ts index 7a8367540c9f..87eee70c2054 100644 --- a/agent/src/cli/auth/command-accounts.ts +++ b/agent/src/cli/auth/command-accounts.ts @@ -20,7 +20,7 @@ export const accountsCommand = new Command('accounts') } const t = new Table() for (const account of settings.accounts ?? []) { - const authenticated = await AuthenticatedAccount.fromUnauthenticated(account) + const authenticated = await AuthenticatedAccount.fromUnauthenticated(spinner, account) t.cell(chalk.bold('Name'), account.id) t.cell(chalk.bold('Instance'), account.serverEndpoint) const isActiveAccount = account.id === settings.activeAccountID diff --git a/agent/src/cli/auth/command-login.ts b/agent/src/cli/auth/command-login.ts index 29ee2e250658..01fa526880ef 100644 --- a/agent/src/cli/auth/command-login.ts +++ b/agent/src/cli/auth/command-login.ts @@ -32,6 +32,9 @@ export const loginCommand = new Command('login') .action(async (options: LoginOptions) => { const spinner = ora('Logging in...').start() const account = await AuthenticatedAccount.fromUserSettings(spinner) + if (!spinner.isSpinning) { + process.exit(1) + } const userInfo = await account?.getCurrentUserInfo() if (!isError(userInfo) && userInfo?.username) { spinner.succeed('You are already logged in as ' + userInfo.username) @@ -119,16 +122,16 @@ export async function loginAction( }) const userInfo = await client.getCurrentUserInfo() if (isError(userInfo)) { - spinner.fail('Failed to get username from GraphQL') + spinner.fail('Failed to get username from GraphQL. Error: ' + String(userInfo)) return undefined } const oldSettings = loadUserSettings() const id = uniqueID(userInfo.username, oldSettings) - const account: Account = { id, serverEndpoint } + const account: Account = { id, username: userInfo.username, serverEndpoint } const oldAccounts = oldSettings?.accounts ? oldSettings.accounts.filter(({ id }) => id !== account.id) : [] - await writeCodySecret(account, token) + await writeCodySecret(spinner, account, token) const newAccounts = [account, ...oldAccounts] const newSettings: UserSettings = { accounts: newAccounts, activeAccountID: account.id } writeUserSettings(newSettings) diff --git a/agent/src/cli/auth/command-logout.ts b/agent/src/cli/auth/command-logout.ts index 58f57855692c..334f9b1c4308 100644 --- a/agent/src/cli/auth/command-logout.ts +++ b/agent/src/cli/auth/command-logout.ts @@ -13,7 +13,6 @@ export const logoutCommand = new Command('logout') spinner.fail('You are already logged out') process.exit(1) } - await removeCodySecret(settings.activeAccountID) const account = settings.accounts.find(account => account.id === settings.activeAccountID) if (!account) { spinner.fail( @@ -23,11 +22,12 @@ export const logoutCommand = new Command('logout') ) process.exit(1) } + await removeCodySecret(spinner, account) const newAccounts = settings.accounts.filter( account => account.id !== settings.activeAccountID ) writeUserSettings({ accounts: newAccounts }) - spinner.succeed(`Logged out of account ${account.id} on ${account.serverEndpoint}`) + spinner.succeed(`Logged out of account ${account.username} on ${account.serverEndpoint}`) process.exit(0) } catch (error) { if (error instanceof Error) { diff --git a/agent/src/cli/auth/messages.ts b/agent/src/cli/auth/messages.ts index 880659008766..5fd2493888e4 100644 --- a/agent/src/cli/auth/messages.ts +++ b/agent/src/cli/auth/messages.ts @@ -1,5 +1,8 @@ import type { Ora } from 'ora' export function notLoggedIn(spinner: Ora): void { + if (!spinner.isSpinning) { + return + } spinner.fail('Not logged in. To fix this problem, run:\n\tcody auth login --web') } diff --git a/agent/src/cli/auth/secrets.ts b/agent/src/cli/auth/secrets.ts index d384699efc29..c7a7224a556f 100644 --- a/agent/src/cli/auth/secrets.ts +++ b/agent/src/cli/auth/secrets.ts @@ -1,42 +1,282 @@ -import keytar from 'keytar' +import { execSync, spawn } from 'node:child_process' +import type { Ora } from 'ora' import { logDebug } from '../../../../vscode/src/log' import type { Account } from './settings' // This file deals with reading/writing/removing Cody access tokens from the // operating system's secret storage (Keychain on macOS, Credential Value on -// Windows, etc.). +// Windows, etc.). Originally, we used the `keytar` npm dependency to interact +// with the OS secret storage. However, Keytar is unmaintained and it was +// complicated to distribute anyways because you had to distribute native +// modules for each supported OS. The current implementation shells out to the +// `security` command on macOS, `CredentialManager` on Windows, and `secret-tool` on +// Linux. This is an o)ptional feature. Users can always avoid using this functionality +// by setting the `CODY_ACCESS_TOKEN` environment variable or passing the `--access-token` +// command line argument. +// +// One problem with this custom solution is that users will most likely +// select "Always allow" on macOS when prompted if the "system" tool can access the Cody +// secrets. This means that any other tool on the computer can shell out to +// `system` to read the same secret. However, we chose to go with this approach on macOS +// regardless of this risk based on the following observations: +// - The user can chose not to let Cody manage its secrets. This is an optional feature. +// - Storing the secret as a global environment variable also isn't secure +// because it means any process on the computer can read the same +// SRC_ACCESS_TOKEN environment variable without custom work. With the secret manager, +// a malicious user needs to know at least the Sourcegraph server endpoint URL and the +// user's username (both easy to access information, but still more inconvenient than +// reading an environment variable). +// - The `gh` cli tool uses the same approach (shelling out to `security` on macOS), +// meaning that any tool on my computer can read my GitHub access token by shelling out +// to `security` without me knowing. I have not reviewed what `gh` does on +// Windows or Linux. +// - The ideal solution would be to do something similar to IntelliJ does, which is to +// build a native module that is a signed Sourcegraph application and is very inconvenient +// to run outside of the the `cody` cli tool (not obvious how to do this). Keytar delivers +// some of these benefits with a native Node.js module. -const codyServiceName = 'Cody' - -function keytarServiceName(account: Account): string { - const host = new URL(account.serverEndpoint).host - return `${account.id} on ${host}` -} -export async function writeCodySecret(account: Account, secret: string): Promise { +export async function writeCodySecret(spinner: Ora, account: Account, secret: string): Promise { + const keychain = getKeychainOperations(spinner, account) try { - await keytar.setPassword(codyServiceName, keytarServiceName(account), secret) + await keychain.writeSecret(secret) } catch (error) { - logDebug('keytar-storage', 'Error storing secret:', error) + logDebug('keychain-storage', 'Error storing secret:', error) } } -export async function readCodySecret(account: Account) { +export async function readCodySecret(spinner: Ora, account: Account) { + const keychain = getKeychainOperations(spinner, account) try { - const secret = await keytar.getPassword(codyServiceName, keytarServiceName(account)) + const secret = await keychain.readSecret() if (secret) { return secret } return null } catch (error) { - logDebug('keytar-storage', 'Error retrieving secret:', error) + logDebug('keychain-storage', 'Error retrieving secret:', error) return null } } -export async function removeCodySecret(account: string) { +export async function removeCodySecret(spinner: Ora, account: Account) { + const keychain = getKeychainOperations(spinner, account) + try { + await keychain.deleteSecret() + } catch (error) { + logDebug('keychain-storage', 'Error deleting secret:', error) + } +} + +function getKeychainOperations(spinner: Ora, account: Account): KeychainOperations { + switch (process.platform) { + case 'darwin': + return new MacOSKeychain(spinner, account) + case 'win32': + return new WindowsCredentialManager(spinner, account) + case 'linux': + return new LinuxSecretService(spinner, account) + default: + throw new Error(`Unsupported platform: ${process.platform}`) + } +} + +abstract class KeychainOperations { + constructor( + public spinner: Ora, + public account: Account + ) {} + abstract readSecret(): Promise + abstract writeSecret(secret: string): Promise + abstract deleteSecret(): Promise + protected service(): string { + const host = new URL(this.account.serverEndpoint).host + return `Cody: ${host} (${this.account.id})` + } + protected spawnAsync(command: string, args: string[], options?: { stdin: string }): Promise { + return new Promise((resolve, reject) => { + const child = spawn(command, args, { stdio: 'pipe', ...options }) + let stdout = '' + let stderr = '' + child.stdout.on('data', data => { + stdout += data + }) + + child.stderr.on('data', data => { + stderr += data + }) + + if (options?.stdin) { + child.stdin.write(options.stdin) + child.stdin.end() + } + + child.on('exit', code => { + if (code !== 0) { + reject( + new Error(`command failed: ${command} ${args.join(' ')}\n${stdout}\n${stderr}`) + ) + } else { + resolve(stdout.trim()) + } + }) + }) + } +} + +class MacOSKeychain extends KeychainOperations { + installationInstructions = '' + async readSecret(): Promise { + return await this.spawnAsync('security', [ + 'find-generic-password', + '-s', + this.service(), + '-a', + this.account.username, + '-w', + ]) + } + + async writeSecret(secret: string): Promise { + await this.spawnAsync('security', [ + 'add-generic-password', + '-s', + this.service(), + '-a', + this.account.username, + '-w', + secret, + ]) + } + + async deleteSecret(): Promise { + await this.spawnAsync('security', [ + 'delete-generic-password', + '-s', + this.service(), + '-a', + this.account.username, + ]) + } +} +const alternativelyMessage = + 'Alternatively, you can manually supply an access token with --access-token or the environment variable SRC_ACCESS_TOKEN' + +class WindowsCredentialManager extends KeychainOperations { + installationInstructions = `The 'CredentialManager' PowerShell module needs to be installed to let Cody manage your access token. +To fix this problem, run the command below to install the missing dependencies: + Install-Module -Name CredentialManager +${alternativelyMessage}` + private target(): string { + return `${this.service()}:${this.account.username}`.replaceAll('"', '_') + } + async readSecret(): Promise { + const powershellCommand = `(Get-StoredCredential -Target "${this.target()}").GetNetworkCredential().Password` + return await this.spawnAsync('powershell.exe', ['-Command', powershellCommand]) + } + + override async spawnAsync( + command: string, + args: string[], + options?: { stdin: string } | undefined + ): Promise { + try { + return await super.spawnAsync(command, args, options) + } catch (error) { + this.spinner.fail(this.installationInstructions) + throw error + } + } + + async writeSecret(secret: string): Promise { + const powershellCommand = `& {New-StoredCredential -Target '${this.target()}' -Password '${secret}' -Persist LocalMachine}` + await this.spawnAsync('powershell.exe', ['-Command', powershellCommand]) + } + + async deleteSecret(): Promise { + await this.spawnAsync('powershell.exe', [ + '-Command', + `& {Remove-StoredCredential -Target '${this.service()}:${this.account.username}'}`, + ]) + } +} + +class LinuxSecretService extends KeychainOperations { + private installationInstructions = `The command 'secret-tool' is not installed on this computer. +This tool is required to let Cody manage your access token securely. +To fix this problem, run the commands below and try again: + sudo apt install libsecret-tools + sudo apt install gnome-keyring +${alternativelyMessage}` + async readSecret(): Promise { + return await this.spawnAsync('secret-tool', [ + 'lookup', + 'service', + this.service(), + 'account', + this.account.username, + ]) + } + + override spawnAsync( + command: string, + args: string[], + options?: { stdin: string } | undefined + ): Promise { + if (!checkInstalled(this.spinner, command, this.installationInstructions)) { + return Promise.reject(`command not found: ${command}`) + } + return super.spawnAsync(command, args, options) + } + + async writeSecret(secret: string): Promise { + await this.spawnAsync( + 'secret-tool', + [ + 'store', + '--label', + this.service(), + 'service', + this.service(), + 'account', + this.account.username, + ], + { stdin: secret } + ) + } + + async deleteSecret(): Promise { + await this.spawnAsync('secret-tool', [ + 'clear', + 'service', + this.service(), + 'account', + this.account.username, + ]) + } +} + +const availableCommands = new Map() +function checkInstalled(spinner: Ora, command: string, installationInstructions: string): boolean { + if (process.platform === 'win32') { + // which doesn't work on Windows + return true + } + const fromCache = availableCommands.get(command) + if (fromCache !== undefined) { + return fromCache + } + const isInstalled = canSpawnCommand(command) + availableCommands.set(command, isInstalled) + if (!isInstalled) { + spinner.fail(installationInstructions) + } + return isInstalled +} +function canSpawnCommand(command: string) { try { - await keytar.deletePassword(codyServiceName, account) + execSync(`which ${command}`, { stdio: 'ignore' }) + return true } catch (error) { - logDebug('keytar-storage', 'Error deleting secret:', error) + return false } } diff --git a/agent/src/cli/auth/settings.ts b/agent/src/cli/auth/settings.ts index 405e95fd8c09..fc16002ab781 100644 --- a/agent/src/cli/auth/settings.ts +++ b/agent/src/cli/auth/settings.ts @@ -6,11 +6,14 @@ import { codyPaths } from '../../codyPaths' export interface Account { // In most cases, the ID will be the same as the username. It's only when // you have multiple accounts with the same username on different server - // endpoints when the ID will be different from the username. - id: string - serverEndpoint: string - preferredModel?: string - customHeaders?: any + // endpoints when the ID will be different from the username. Having `id` be + // separate from `username` avoids ugly workarounds like concatenating + // `username+serverEndpoint` all over the place. + readonly id: string + readonly username: string + readonly serverEndpoint: string + readonly preferredModel?: string + readonly customHeaders?: any } export interface UserSettings { @@ -43,6 +46,7 @@ export function loadUserSettings(): UserSettings { if (typeof json !== 'object') { throw new Error('Invalid user settings. Expected object. Got ' + JSON.stringify(json, null, 2)) } + if (json?.accounts && !Array.isArray(json.accounts)) { throw new Error( 'Invalid user settings. Expected accounts to be an array. Got ' + @@ -50,5 +54,18 @@ export function loadUserSettings(): UserSettings { ) } + for (const account of json.accounts || []) { + if (!account?.id) { + throw new Error( + `Invalid user settings. Missing required field 'id': ${JSON.stringify(account)} ` + ) + } + + // The `username` property was not included in the first version of the cli. + if (!account?.username) { + account.username = account?.id + } + } + return json } diff --git a/agent/src/cli/chat.ts b/agent/src/cli/chat.ts index 71f4ac4f2bf5..8729e5190062 100644 --- a/agent/src/cli/chat.ts +++ b/agent/src/cli/chat.ts @@ -58,7 +58,12 @@ export const chatCommand = () => .option('--debug', 'Enable debug logging', false) .action(async (options: ChatOptions) => { if (!options.accessToken) { - const account = await AuthenticatedAccount.fromUserSettings() + const spinner = ora().start('Loading access token') + const account = await AuthenticatedAccount.fromUserSettings(spinner) + if (!spinner.isSpinning) { + process.exit(1) + } + spinner.stop() if (account) { options.accessToken = account.accessToken options.endpoint = account.serverEndpoint diff --git a/agent/src/esbuild.mjs b/agent/src/esbuild.mjs index 728625bee62d..98bbb167a7aa 100644 --- a/agent/src/esbuild.mjs +++ b/agent/src/esbuild.mjs @@ -58,7 +58,6 @@ async function buildAgent(minify) { logLevel: 'error', external: ['typescript'], minify: minify, - loader: { '.node': 'copy' }, alias: { vscode: path.resolve(process.cwd(), 'src', 'vscode-shim.ts'), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b962fb292e5e..93f940facc05 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -204,9 +204,6 @@ importers: js-levenshtein: specifier: ^1.1.6 version: 1.1.6 - keytar: - specifier: ^7.9.0 - version: 7.9.0 lodash: specifier: ^4.17.21 version: 4.17.21 @@ -7113,6 +7110,7 @@ packages: buffer: 5.7.1 inherits: 2.0.4 readable-stream: 3.6.2 + dev: true /bluebird@3.4.7: resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==} @@ -7259,6 +7257,7 @@ packages: dependencies: base64-js: 1.5.1 ieee754: 1.2.1 + dev: true /buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} @@ -7515,6 +7514,7 @@ packages: /chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + dev: true /chownr@2.0.0: resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} @@ -8079,6 +8079,8 @@ packages: requiresBuild: true dependencies: mimic-response: 3.1.0 + dev: true + optional: true /dedent@0.7.0: resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} @@ -8130,6 +8132,8 @@ packages: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} requiresBuild: true + dev: true + optional: true /deep-freeze@0.0.1: resolution: {integrity: sha512-Z+z8HiAvsGwmjqlphnHW5oz6yWlOwu6EQfFTjmeTWlDeda3FS2yv3jhq35TX/ewmsnqB+RX2IdsIOyjJCQN5tg==} @@ -8265,6 +8269,8 @@ packages: resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} engines: {node: '>=8'} requiresBuild: true + dev: true + optional: true /detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} @@ -8766,6 +8772,8 @@ packages: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} requiresBuild: true + dev: true + optional: true /exponential-backoff@3.1.1: resolution: {integrity: sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==} @@ -9189,6 +9197,7 @@ packages: /fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + dev: true /fs-extra@10.1.0: resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} @@ -9377,6 +9386,8 @@ packages: /github-from-package@0.0.0: resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} requiresBuild: true + dev: true + optional: true /github-slugger@2.0.0: resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} @@ -10032,6 +10043,7 @@ packages: /ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + dev: true /ini@4.1.2: resolution: {integrity: sha512-AMB1mvwR1pyBFY/nSevUX6y8nJWS63/SzUKD3JyQn97s4xgIdgQPT75IRouIiBAN4yLQBUShNYVW0+UG25daCw==} @@ -10720,6 +10732,8 @@ packages: dependencies: node-addon-api: 4.3.0 prebuild-install: 7.1.2 + dev: true + optional: true /keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -11807,6 +11821,8 @@ packages: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} requiresBuild: true + dev: true + optional: true /min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} @@ -11926,6 +11942,7 @@ packages: /mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + dev: true /mkdirp@0.5.6: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} @@ -12070,6 +12087,8 @@ packages: /napi-build-utils@1.0.2: resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} requiresBuild: true + dev: true + optional: true /negotiator@0.6.3: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} @@ -12112,10 +12131,14 @@ packages: requiresBuild: true dependencies: semver: 7.6.0 + dev: true + optional: true /node-addon-api@4.3.0: resolution: {integrity: sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==} requiresBuild: true + dev: true + optional: true /node-dir@0.1.17: resolution: {integrity: sha512-tmPX422rYgofd4epzrNoOXiE8XFZYOcCq1vD7MAXCDO+O+zndlA2ztdKKMa+EeuBG5tHETpr4ml4RGgpqDCCAg==} @@ -12889,6 +12912,8 @@ packages: simple-get: 4.0.1 tar-fs: 2.1.1 tunnel-agent: 0.6.0 + dev: true + optional: true /prelude-ls@1.1.2: resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} @@ -13110,6 +13135,7 @@ packages: dependencies: end-of-stream: 1.4.4 once: 1.4.0 + dev: true /pumpify@1.5.1: resolution: {integrity: sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==} @@ -13192,6 +13218,8 @@ packages: ini: 1.3.8 minimist: 1.2.8 strip-json-comments: 2.0.1 + dev: true + optional: true /re2js@0.4.1: resolution: {integrity: sha512-Kxb+OKXrEPowP4bXAF07NDXtgYX07S8HeVGgadx5/D/R41LzWg1kgTD2szIv2iHJM3vrAPnDKaBzfUE/7QWX9w==} @@ -14036,6 +14064,8 @@ packages: /simple-concat@1.0.1: resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} requiresBuild: true + dev: true + optional: true /simple-get@4.0.1: resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} @@ -14044,6 +14074,8 @@ packages: decompress-response: 6.0.0 once: 1.4.0 simple-concat: 1.0.1 + dev: true + optional: true /sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -14416,6 +14448,8 @@ packages: resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} engines: {node: '>=0.10.0'} requiresBuild: true + dev: true + optional: true /strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} @@ -14724,6 +14758,7 @@ packages: mkdirp-classic: 0.5.3 pump: 3.0.0 tar-stream: 2.2.0 + dev: true /tar-fs@3.0.6: resolution: {integrity: sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==} @@ -14744,6 +14779,7 @@ packages: fs-constants: 1.0.0 inherits: 2.0.4 readable-stream: 3.6.2 + dev: true /tar-stream@3.1.7: resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} @@ -15042,6 +15078,8 @@ packages: requiresBuild: true dependencies: safe-buffer: 5.2.1 + dev: true + optional: true /tunnel@0.0.6: resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==}