From 5aae5298033eec20a9f659ba0f8fe77ad286bf7b Mon Sep 17 00:00:00 2001 From: jamesmcnamara Date: Thu, 3 Oct 2024 11:27:47 -0700 Subject: [PATCH 01/16] fix uninstaller and drop credentials on reinstall --- agent/package.json | 1 - agent/src/agent.ts | 2 +- agent/src/cli/command-auth/settings.ts | 2 +- agent/src/cli/command-bench/command-bench.ts | 3 +- agent/src/esbuild.mjs | 25 +------- lib/shared/esbuild.utils.mjs | 23 +++++++ lib/shared/package.json | 7 ++- {agent => lib/shared}/src/codyPaths.ts | 0 lib/shared/src/configuration/resolver.ts | 40 +++++++++--- lib/shared/src/index.ts | 1 + pnpm-lock.yaml | 36 ++++++----- vscode/package.json | 4 +- vscode/src/configuration.ts | 1 + vscode/src/extension.node.ts | 16 ++++- vscode/src/main.ts | 8 ++- vscode/src/services/telemetry-v2.ts | 18 +++--- vscode/src/uninstall/serializeConfig.ts | 66 -------------------- vscode/tsconfig.json | 3 +- vscode/uninstall/esbuild.mjs | 39 ++++++++++++ vscode/{src => }/uninstall/post-uninstall.ts | 31 +++++++-- vscode/uninstall/reinstall.ts | 20 ++++++ vscode/uninstall/serializeConfig.ts | 66 ++++++++++++++++++++ 22 files changed, 273 insertions(+), 139 deletions(-) create mode 100644 lib/shared/esbuild.utils.mjs rename {agent => lib/shared}/src/codyPaths.ts (100%) delete mode 100644 vscode/src/uninstall/serializeConfig.ts create mode 100644 vscode/uninstall/esbuild.mjs rename vscode/{src => }/uninstall/post-uninstall.ts (51%) create mode 100644 vscode/uninstall/reinstall.ts create mode 100644 vscode/uninstall/serializeConfig.ts diff --git a/agent/package.json b/agent/package.json index 2d27c7f2213a..77852a7e2b15 100644 --- a/agent/package.json +++ b/agent/package.json @@ -45,7 +45,6 @@ "csv-writer": "^1.6.0", "dedent": "^0.7.0", "easy-table": "^1.2.0", - "env-paths": "^3.0.0", "fast-myers-diff": "^3.2.0", "glob": "^11.0.0", "js-levenshtein": "^1.1.6", diff --git a/agent/src/agent.ts b/agent/src/agent.ts index 8760fa3abeca..d471f5c23100 100644 --- a/agent/src/agent.ts +++ b/agent/src/agent.ts @@ -47,6 +47,7 @@ import type * as agent_protocol from '../../vscode/src/jsonrpc/agent-protocol' import { mkdirSync, statSync } from 'node:fs' import { PassThrough } from 'node:stream' import type { Har } from '@pollyjs/persister' +import { codyPaths } from '@sourcegraph/cody-shared' import { TESTING_TELEMETRY_EXPORTER } from '@sourcegraph/cody-shared/src/telemetry-v2/TelemetryRecorderProvider' import { type TelemetryEventParameters, TestTelemetryExporter } from '@sourcegraph/telemetry' import { copySync } from 'fs-extra' @@ -77,7 +78,6 @@ import { AgentWorkspaceConfiguration } from './AgentWorkspaceConfiguration' import { AgentWorkspaceDocuments } from './AgentWorkspaceDocuments' import { registerNativeWebviewHandlers, resolveWebviewView } from './NativeWebview' import type { PollyRequestError } from './cli/command-jsonrpc-stdio' -import { codyPaths } from './codyPaths' import { AgentGlobalState } from './global-state/AgentGlobalState' import { MessageHandler, diff --git a/agent/src/cli/command-auth/settings.ts b/agent/src/cli/command-auth/settings.ts index fc16002ab781..5e44d116b08c 100644 --- a/agent/src/cli/command-auth/settings.ts +++ b/agent/src/cli/command-auth/settings.ts @@ -1,7 +1,7 @@ import fs from 'node:fs' import path from 'node:path' -import { codyPaths } from '../../codyPaths' +import { codyPaths } from '@sourcegraph/cody-shared' export interface Account { // In most cases, the ID will be the same as the username. It's only when diff --git a/agent/src/cli/command-bench/command-bench.ts b/agent/src/cli/command-bench/command-bench.ts index 12528add34c0..5fecbf5ee5c7 100644 --- a/agent/src/cli/command-bench/command-bench.ts +++ b/agent/src/cli/command-bench/command-bench.ts @@ -10,7 +10,7 @@ import { newAgentClient } from '../../agent' import { exec } from 'node:child_process' import fs from 'node:fs' import { promisify } from 'node:util' -import { isDefined, modelsService } from '@sourcegraph/cody-shared' +import { codyPaths, isDefined, modelsService } from '@sourcegraph/cody-shared' import { sleep } from '../../../../vscode/src/completions/utils' import { setStaticResolvedConfigurationWithAuthCredentials } from '../../../../vscode/src/configuration' import { localStorage } from '../../../../vscode/src/services/LocalStorageProvider' @@ -18,7 +18,6 @@ import { createOrUpdateTelemetryRecorderProvider } from '../../../../vscode/src/ import { startPollyRecording } from '../../../../vscode/src/testutils/polly' import { dotcomCredentials } from '../../../../vscode/src/testutils/testing-credentials' import { allClientCapabilitiesEnabled } from '../../allClientCapabilitiesEnabled' -import { codyPaths } from '../../codyPaths' import { arrayOption, booleanOption, intOption } from './cli-parsers' import { matchesGlobPatterns } from './matchesGlobPatterns' import { evaluateAutocompleteStrategy } from './strategy-autocomplete' diff --git a/agent/src/esbuild.mjs b/agent/src/esbuild.mjs index 4821b4086332..d8af51599ed0 100644 --- a/agent/src/esbuild.mjs +++ b/agent/src/esbuild.mjs @@ -1,6 +1,8 @@ import fs from 'node:fs/promises' import path from 'node:path' import process from 'node:process' +// @ts-ignore this is not compiled by typescript so it can import files from outside the rootDir +import { detectForbiddenImportPlugin } from "../../lib/shared/esbuild.utils.mjs"; import { build } from 'esbuild' @@ -69,7 +71,7 @@ async function buildAgent(minify) { '@sourcegraph/cody-shared/src': '@sourcegraph/cody-shared/src', }, } - const res = await build(esbuildOptions) + await build(esbuildOptions) // Copy all .wasm files to the dist/ directory const distDir = path.join(process.cwd(), '..', 'vscode', 'dist') @@ -87,24 +89,3 @@ async function buildAgent(minify) { await fs.copyFile(src, dest) } } - -function detectForbiddenImportPlugin(allForbiddenModules) { - return { - name: 'detect-forbidden-import-plugin', - setup(build) { - build.onResolve({ filter: /.*/ }, args => { - for (const forbidden of allForbiddenModules) { - if (args.path === forbidden) { - throw new Error(`'${forbidden}' module is imported in file: ${args.importer}`) - } - } - args - }) - - build.onLoad({ filter: /.*/ }, async args => { - const contents = await fs.readFile(args.path, 'utf8') - return { contents, loader: 'default' } - }) - }, - } -} diff --git a/lib/shared/esbuild.utils.mjs b/lib/shared/esbuild.utils.mjs new file mode 100644 index 000000000000..cf13870dc007 --- /dev/null +++ b/lib/shared/esbuild.utils.mjs @@ -0,0 +1,23 @@ + +import fs from "node:fs/promises"; + +export function detectForbiddenImportPlugin (allForbiddenModules) { + return { + name: 'detect-forbidden-import-plugin', + setup (build) { + build.onResolve({ filter: /.*/ }, args => { + for (const forbidden of allForbiddenModules) { + if (args.path === forbidden) { + throw new Error(`'${forbidden}' module is imported in file: ${args.importer}`) + } + } + args + }) + + build.onLoad({ filter: /.*/ }, async args => { + const contents = await fs.readFile(args.path, 'utf8') + return { contents, loader: 'default' } + }) + }, + } +} diff --git a/lib/shared/package.json b/lib/shared/package.json index 3b0b7e7859dc..530cae340b94 100644 --- a/lib/shared/package.json +++ b/lib/shared/package.json @@ -10,7 +10,11 @@ }, "main": "dist/index.js", "types": "dist/index.d.ts", - "files": ["dist", "src", "!**/*.test.*"], + "files": [ + "dist", + "src", + "!**/*.test.*" + ], "sideEffects": false, "scripts": { "build": "tsc --build", @@ -25,6 +29,7 @@ "date-fns": "^2.30.0", "dedent": "^0.7.0", "diff": "^5.2.0", + "env-paths": "^3.0.0", "immer": "^10.1.1", "isomorphic-fetch": "^3.0.0", "js-tiktoken": "^1.0.14", diff --git a/agent/src/codyPaths.ts b/lib/shared/src/codyPaths.ts similarity index 100% rename from agent/src/codyPaths.ts rename to lib/shared/src/codyPaths.ts diff --git a/lib/shared/src/configuration/resolver.ts b/lib/shared/src/configuration/resolver.ts index 2dcec7dc25be..e200344ca1d5 100644 --- a/lib/shared/src/configuration/resolver.ts +++ b/lib/shared/src/configuration/resolver.ts @@ -19,10 +19,12 @@ export interface ConfigurationInput { clientConfiguration: ClientConfiguration clientSecrets: ClientSecrets clientState: ClientState + isReinstalling: boolean } export interface ClientSecrets { getToken(endpoint: string): Promise + deleteToken(endpoint: string): Promise } export interface ClientState { @@ -42,6 +44,7 @@ export type ResolvedConfiguration = ReadonlyDeep<{ configuration: ClientConfiguration auth: AuthCredentials clientState: ClientState + isReinstalling: boolean }> /** @@ -67,29 +70,50 @@ export type PickResolvedConfiguration = { : undefined } -async function resolveConfiguration(input: ConfigurationInput): Promise { +async function resolveConfiguration({ + clientConfiguration, + clientSecrets, + clientState, + isReinstalling, +}: ConfigurationInput): Promise { // we allow for overriding the server endpoint from config if we haven't // manually signed in somewhere else const serverEndpoint = normalizeServerEndpointURL( - input.clientConfiguration.overrideServerEndpoint || - (input.clientState.lastUsedEndpoint ?? DOTCOM_URL.toString()) + clientConfiguration.overrideServerEndpoint || + // If we are reinstalling, we ignore previous state + ((isReinstalling ? undefined : clientState.lastUsedEndpoint) ?? DOTCOM_URL.toString()) ) // We must not throw here, because that would result in the `resolvedConfig` observable // terminating and all callers receiving no further config updates. - const loadTokenFn = () => - input.clientSecrets.getToken(serverEndpoint).catch(error => { + const loadTokenFn = async () => { + // When the user is reinstalling, we want to clear the cached credentials + if (isReinstalling) { + await Promise.all([ + clientSecrets.deleteToken(serverEndpoint), + clientConfiguration.overrideServerEndpoint + ? clientSecrets.deleteToken(clientConfiguration.overrideServerEndpoint) + : Promise.resolve(undefined), + clientState.lastUsedEndpoint + ? clientSecrets.deleteToken(clientState.lastUsedEndpoint) + : Promise.resolve(undefined), + ]) + return null + } + return clientSecrets.getToken(serverEndpoint).catch(error => { logError( 'resolveConfiguration', `Failed to get access token for endpoint ${serverEndpoint}: ${error}` ) return null }) - const accessToken = input.clientConfiguration.overrideAuthToken || ((await loadTokenFn()) ?? null) + } + const accessToken = clientConfiguration.overrideAuthToken || ((await loadTokenFn()) ?? null) return { - configuration: input.clientConfiguration, - clientState: input.clientState, + configuration: clientConfiguration, + clientState, auth: { accessToken, serverEndpoint }, + isReinstalling, } } diff --git a/lib/shared/src/index.ts b/lib/shared/src/index.ts index ae764be00e9b..d34292c4272b 100644 --- a/lib/shared/src/index.ts +++ b/lib/shared/src/index.ts @@ -121,6 +121,7 @@ export { LARGE_FILE_WARNING_LABEL, NO_SYMBOL_MATCHES_HELP_LABEL, } from './codebase-context/messages' +export * from './codyPaths' export type { CodyCommand, CodyCommandContext, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ee05e66961bd..91642a8beacd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -356,6 +356,9 @@ importers: diff: specifier: ^5.2.0 version: 5.2.0 + env-paths: + specifier: ^3.0.0 + version: 3.0.0 immer: specifier: ^10.1.1 version: 10.1.1 @@ -3810,7 +3813,7 @@ packages: /@radix-ui/primitive@1.0.1: resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==} dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.6 dev: false /@radix-ui/primitive@1.1.0: @@ -3939,7 +3942,7 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.6 '@types/react': 18.2.79 react: 18.2.0 dev: false @@ -3992,7 +3995,7 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.6 '@types/react': 18.2.79 react: 18.2.0 dev: false @@ -4129,7 +4132,7 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.6 '@radix-ui/primitive': 1.0.1 '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.79)(react@18.2.0) '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) @@ -4164,7 +4167,7 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.6 '@types/react': 18.2.79 react: 18.2.0 dev: false @@ -4205,7 +4208,7 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.6 '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.79)(react@18.2.0) '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.79)(react@18.2.0) @@ -4264,7 +4267,7 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.6 '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.79)(react@18.2.0) '@types/react': 18.2.79 react: 18.2.0 @@ -4352,7 +4355,7 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.6 '@floating-ui/react-dom': 2.0.9(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-arrow': 1.0.3(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.79)(react@18.2.0) @@ -4403,7 +4406,7 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.6 '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) '@types/react': 18.2.79 '@types/react-dom': 18.2.25 @@ -4446,7 +4449,7 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.6 '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.79)(react@18.2.0) '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.79)(react@18.2.0) '@types/react': 18.2.79 @@ -4489,7 +4492,7 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.6 '@radix-ui/react-slot': 1.0.2(@types/react@18.2.37)(react@18.2.0) '@types/react': 18.2.37 '@types/react-dom': 18.2.15 @@ -4510,7 +4513,7 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.6 '@radix-ui/react-slot': 1.0.2(@types/react@18.2.79)(react@18.2.0) '@types/react': 18.2.79 '@types/react-dom': 18.2.25 @@ -4575,7 +4578,7 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.6 '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.37)(react@18.2.0) '@types/react': 18.2.37 react: 18.2.0 @@ -4590,7 +4593,7 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.6 '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.79)(react@18.2.0) '@types/react': 18.2.79 react: 18.2.0 @@ -4747,7 +4750,7 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.6 '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.79)(react@18.2.0) '@types/react': 18.2.79 react: 18.2.0 @@ -4881,7 +4884,7 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.6 '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) '@types/react': 18.2.79 '@types/react-dom': 18.2.25 @@ -9058,7 +9061,6 @@ packages: /env-paths@3.0.0: resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: true /envinfo@7.13.0: resolution: {integrity: sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q==} diff --git a/vscode/package.json b/vscode/package.json index a1eb3b5d1cf2..d653faab7e37 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -32,8 +32,8 @@ "watch:build:dev:desktop": "concurrently \"pnpm run -s _build:esbuild:desktop --watch\" \"pnpm run -s _build:webviews --mode development --watch\"", "_build:esbuild:desktop": "pnpm download-wasm && pnpm run -s _build:esbuild:uninstall && pnpm run -s _build:esbuild:node", "_build:esbuild:node": "esbuild ./src/extension.node.ts --bundle --outfile=dist/extension.node.js --external:vscode --external:typescript --alias:@sourcegraph/cody-shared=@sourcegraph/cody-shared/src/index --alias:@sourcegraph/cody-shared/src=@sourcegraph/cody-shared/src --alias:lexical=./build/lexical-package-fix --format=cjs --platform=node --sourcemap", - "_build:esbuild:web": "esbuild ./src/extension.web.ts --platform=browser --bundle --outfile=dist/extension.web.js --alias:@sourcegraph/cody-shared=@sourcegraph/cody-shared/src/index --alias:@sourcegraph/cody-shared/src=@sourcegraph/cody-shared/src --alias:path=path-browserify --external:typescript --alias:node:path=path-browserify --alias:node:os=os-browserify --external:vscode --external:node:child_process --external:node:util --external:node:fs --external:node:fs/promises --define:process='{\"env\":{}}' --define:window=self --format=cjs --sourcemap", - "_build:esbuild:uninstall": "esbuild ./src/uninstall/post-uninstall.ts --bundle --outfile=dist/post-uninstall.js --external:typescript --format=cjs --platform=node --sourcemap --alias:@sourcegraph/cody-shared=@sourcegraph/cody-shared/src/index --alias:@sourcegraph/cody-shared/src=@sourcegraph/cody-shared/src --alias:lexical=./build/lexical-package-fix", + "_build:esbuild:web": "esbuild ./src/extension.web.ts --platform=browser --bundle --outfile=dist/extension.web.js --alias:@sourcegraph/cody-shared=@sourcegraph/cody-shared/src/index --alias:@sourcegraph/cody-shared/src=@sourcegraph/cody-shared/src --alias:path=path-browserify --external:typescript --alias:node:path=path-browserify --alias:node:os=os-browserify --external:vscode --external:node:child_process --external:node:util --external:node:fs --external:node:fs/promises --external:node:process --define:process='{\"env\":{}}' --define:window=self --format=cjs --sourcemap", + "_build:esbuild:uninstall": "node ./uninstall/esbuild.mjs", "_build:webviews": "vite -c webviews/vite.config.mts build", "release": "ts-node-transpile-only ./scripts/release.ts", "download-wasm": "ts-node-transpile-only ./scripts/download-wasm-modules.ts", diff --git a/vscode/src/configuration.ts b/vscode/src/configuration.ts index 73b0a49f597e..8f7116a48718 100644 --- a/vscode/src/configuration.ts +++ b/vscode/src/configuration.ts @@ -173,5 +173,6 @@ export function setStaticResolvedConfigurationWithAuthCredentials({ configuration: { ...getConfiguration(), customHeaders: configuration.customHeaders }, auth, clientState: localStorage.getClientState(), + isReinstalling: false, }) } diff --git a/vscode/src/extension.node.ts b/vscode/src/extension.node.ts index 4c934b7c86ab..d4a31743f9a7 100644 --- a/vscode/src/extension.node.ts +++ b/vscode/src/extension.node.ts @@ -2,12 +2,15 @@ import { NodeSentryService } from './services/sentry/sentry.node' import { + type ClientCapabilities, currentAuthStatus, currentResolvedConfig, + clientCapabilities as getClientCapabilities, resolvedConfig, subscriptionDisposable, } from '@sourcegraph/cody-shared' import * as vscode from 'vscode' +import { serializeConfigSnapshot } from '../uninstall/serializeConfig' import { startTokenReceiver } from './auth/token-receiver' import { CommandsProvider } from './commands/services/provider' import { SourcegraphNodeCompletionsClient } from './completions/nodeClient' @@ -18,7 +21,7 @@ import { initializeNetworkAgent, setCustomAgent } from './fetch.node' import { SymfRunner } from './local-context/symf' import { localStorage } from './services/LocalStorageProvider' import { OpenTelemetryService } from './services/open-telemetry/OpenTelemetryService.node' -import { serializeConfigSnapshot } from './uninstall/serializeConfig' +import { version } from './version' /** * Activation entrypoint for the VS Code extension when running VS Code as a desktop app @@ -66,8 +69,17 @@ export function activate( export async function deactivate(): Promise { const config = localStorage.getConfig() ?? (await currentResolvedConfig()) const authStatus = currentAuthStatus() - serializeConfigSnapshot({ + let clientCapabilities: ClientCapabilities | undefined + try { + clientCapabilities = getClientCapabilities() + } catch { + // If client capabilities cannot be retrieved, we will just synthesize + // them from defaults in the post-uninstall script. + } + await serializeConfigSnapshot({ config, authStatus, + clientCapabilities, + version, }) } diff --git a/vscode/src/main.ts b/vscode/src/main.ts index 102574fe1a6c..16c2d795df02 100644 --- a/vscode/src/main.ts +++ b/vscode/src/main.ts @@ -23,6 +23,7 @@ import { graphqlClient, isDotCom, modelsService, + promiseFactoryToObservable, resolvedConfig, setClientCapabilitiesFromConfiguration, setClientNameVersion, @@ -37,6 +38,7 @@ import { } from '@sourcegraph/cody-shared' import { isEqual } from 'lodash' import { filter, map } from 'observable-fns' +import { isReinstalling } from '../uninstall/reinstall' import type { CommandResult } from './CommandResult' import { showAccountMenu } from './auth/account-menu' import { showSignInMenu, showSignOutMenu, tokenCallbackHandler } from './auth/auth' @@ -145,14 +147,16 @@ export async function start( startWith(undefined), map(() => secretStorage) ), - localStorage.clientStateChanges.pipe(distinctUntilChanged()) + localStorage.clientStateChanges.pipe(distinctUntilChanged()), + promiseFactoryToObservable(isReinstalling) ).pipe( map( - ([clientConfiguration, clientSecrets, clientState]) => + ([clientConfiguration, clientSecrets, clientState, isReinstalling]) => ({ clientConfiguration, clientSecrets, clientState, + isReinstalling, }) satisfies ConfigurationInput ) ) diff --git a/vscode/src/services/telemetry-v2.ts b/vscode/src/services/telemetry-v2.ts index 9df01689dffb..04d385af07a6 100644 --- a/vscode/src/services/telemetry-v2.ts +++ b/vscode/src/services/telemetry-v2.ts @@ -44,7 +44,7 @@ export function createOrUpdateTelemetryRecorderProvider( isExtensionModeDevOrTest: boolean ): Disposable { return subscriptionDisposable( - resolvedConfig.subscribe(({ configuration, auth, clientState }) => { + resolvedConfig.subscribe(({ configuration, auth, clientState, isReinstalling }) => { // Add timestamp processor for realistic data in output for dev or no-op scenarios const defaultNoOpProvider = new NoOpTelemetryRecorderProvider([ new TimestampTelemetryProcessor(), @@ -90,12 +90,16 @@ export function createOrUpdateTelemetryRecorderProvider( /** * New user */ - telemetryRecorder.recordEvent('cody.extension', 'installed', { - billingMetadata: { - product: 'cody', - category: 'billable', - }, - }) + telemetryRecorder.recordEvent( + 'cody.extension', + isReinstalling ? 'reinstalled' : 'installed', + { + billingMetadata: { + product: 'cody', + category: 'billable', + }, + } + ) } else if ( !configuration.isRunningInsideAgent || configuration.agentHasPersistentStorage diff --git a/vscode/src/uninstall/serializeConfig.ts b/vscode/src/uninstall/serializeConfig.ts deleted file mode 100644 index ddbba402cb7a..000000000000 --- a/vscode/src/uninstall/serializeConfig.ts +++ /dev/null @@ -1,66 +0,0 @@ -import * as fs from 'node:fs' -import * as os from 'node:os' -import * as path from 'node:path' -import type { AuthStatus, ResolvedConfiguration } from '@sourcegraph/cody-shared' - -import { Platform, getOSArch } from '../os' - -const CONFIG_FILE = 'config.json' - -function getPlatformSpecificDirectory(): string { - const { platform } = getOSArch() - const appName = 'cody-ai' - - switch (platform) { - case Platform.Windows: - return path.join(process.env.APPDATA || os.homedir(), appName) - case Platform.Mac: - case Platform.Linux: - return path.join(os.homedir(), '.config', appName) - default: - throw new Error(`Unsupported platform: ${platform}`) - } -} - -function ensureDirectoryExists(directory: string) { - if (!fs.existsSync(directory)) { - fs.mkdirSync(directory, { recursive: true }) - } -} - -// Used to cleanup the uninstaller directory after the last telemetry event is sent -export function deleteUninstallerDirectory() { - fs.rmdirSync(getPlatformSpecificDirectory(), { recursive: true }) -} - -function writeSnapshot(directory: string, filename: string, content: any) { - const filePath = path.join(directory, filename) - - fs.writeFileSync(filePath, JSON.stringify(content, null, 2)) -} - -interface UninstallerConfig { - config?: ResolvedConfiguration - authStatus: AuthStatus | undefined -} - -/** - * Serializes the current configuration and auth status to disk. This is used in the case - * of an uninstall event to log one last telemetry event. - */ -export function serializeConfigSnapshot(uninstall: UninstallerConfig) { - const directory = getPlatformSpecificDirectory() - ensureDirectoryExists(directory) - writeSnapshot(directory, CONFIG_FILE, uninstall) -} - -export function readConfig(): UninstallerConfig | null { - const file = path.join(getPlatformSpecificDirectory(), CONFIG_FILE) - - if (!fs.existsSync(file)) { - return null - } - - const obj = fs.readFileSync(file, 'utf-8') - return JSON.parse(obj) -} diff --git a/vscode/tsconfig.json b/vscode/tsconfig.json index f83c0a08f846..ea82d5d5fba9 100644 --- a/vscode/tsconfig.json +++ b/vscode/tsconfig.json @@ -22,7 +22,8 @@ "e2e", "webviews", "webviews/*.d.ts", - "package.json" + "package.json", + "uninstall" ], "exclude": [ "typehacks", diff --git a/vscode/uninstall/esbuild.mjs b/vscode/uninstall/esbuild.mjs new file mode 100644 index 000000000000..1c2cc6208b5b --- /dev/null +++ b/vscode/uninstall/esbuild.mjs @@ -0,0 +1,39 @@ +// @ts-ignore this is not compiled by typescript so it can import files from outside the rootDir +import { detectForbiddenImportPlugin } from '../../lib/shared/esbuild.utils.mjs' + +import { build as esbuild } from 'esbuild' + +async function build () { + const plugins = [detectForbiddenImportPlugin(['vscode'])] + /** @type {import('esbuild').BuildOptions} */ + const esbuildOptions = { + entryPoints: ['./uninstall/post-uninstall.ts'], + bundle: true, + platform: 'node', + sourcemap: true, + logLevel: 'silent', + write: true, + outfile: './dist/post-uninstall.js', + plugins, + external: ['typescript'], + format: 'cjs', + alias: { + // Build from TypeScript sources so we don't need to run `tsc -b` in the background + // during dev. + '@sourcegraph/cody-shared': '@sourcegraph/cody-shared/src/index', + '@sourcegraph/cody-shared/src': '@sourcegraph/cody-shared/src', + lexical: './build/lexical-package-fix', + }, + } + + return esbuild(esbuildOptions) +} + +build() + .then(() => { + console.log('Post-uninstall script built successfully.') + }) + .catch(err => { + console.error('Could not build the post-uninstall script.', err.message) + process.exit(1) + }) diff --git a/vscode/src/uninstall/post-uninstall.ts b/vscode/uninstall/post-uninstall.ts similarity index 51% rename from vscode/src/uninstall/post-uninstall.ts rename to vscode/uninstall/post-uninstall.ts index 71fb4b9e1d78..50852094945e 100644 --- a/vscode/src/uninstall/post-uninstall.ts +++ b/vscode/uninstall/post-uninstall.ts @@ -1,11 +1,14 @@ import { + CodyIDE, TelemetryRecorderProvider, + mockClientCapabilities, nextTick, setAuthStatusObservable, setStaticResolvedConfigurationValue, } from '@sourcegraph/cody-shared' import { Observable } from 'observable-fns' -import { deleteUninstallerDirectory, readConfig } from './serializeConfig' +import { createUninstallMarker } from './reinstall' +import { deleteUninstallerConfig, readConfig } from './serializeConfig' async function main() { // Do not record telemetry events during testing @@ -13,18 +16,33 @@ async function main() { return } - const uninstaller = readConfig() + const uninstaller = await readConfig() if (uninstaller) { - const { config, authStatus } = uninstaller + const { config, authStatus, version, clientCapabilities } = uninstaller if (config && authStatus) { try { setStaticResolvedConfigurationValue(config) - } catch {} + } catch (error) { + console.error('Failed to set config', error) + } try { setAuthStatusObservable(Observable.of(authStatus)) - } catch {} + } catch (error) { + console.error('Failed to set auth status', error) + } // Wait for `currentAuthStatusOrNotReadyYet` to have this value synchronously. await nextTick() + mockClientCapabilities( + clientCapabilities ?? { + agentIDE: CodyIDE.VSCode, + isVSCode: true, + isCodyWeb: false, + agentExtensionVersion: version, + // Unused by TelemetryRecorderProvider + agentIDEVersion: '', + telemetryClientName: `${CodyIDE.VSCode}.Cody`, + } + ) const provider = new TelemetryRecorderProvider(config, 'connected-instance-only') const recorder = provider.getRecorder() @@ -36,7 +54,8 @@ async function main() { }) // cleanup the uninstaller config - deleteUninstallerDirectory() + await deleteUninstallerConfig() + await createUninstallMarker() } } } diff --git a/vscode/uninstall/reinstall.ts b/vscode/uninstall/reinstall.ts new file mode 100644 index 000000000000..81cfff17a277 --- /dev/null +++ b/vscode/uninstall/reinstall.ts @@ -0,0 +1,20 @@ +import fs from 'node:fs/promises' +import { codyPaths } from '@sourcegraph/cody-shared' + +export const uninstallMarker = codyPaths().config + '/uninstall-marker' + +export const createUninstallMarker = async (): Promise => { + await fs.writeFile(uninstallMarker, '') +} + +// Checks if the user is reinstalling the extension by checking for the existence of a marker file +// If found, it deletes the marker file so that we only report reinstalling once +export const isReinstalling = async (): Promise => { + try { + await fs.stat(uninstallMarker) + await fs.unlink(uninstallMarker) + return true + } catch (error) { + return false + } +} diff --git a/vscode/uninstall/serializeConfig.ts b/vscode/uninstall/serializeConfig.ts new file mode 100644 index 000000000000..4fa26509ebd5 --- /dev/null +++ b/vscode/uninstall/serializeConfig.ts @@ -0,0 +1,66 @@ +import * as fs from 'node:fs/promises' +import * as path from 'node:path' +import { + type AuthStatus, + type ClientCapabilities, + type ResolvedConfiguration, + codyPaths, +} from '@sourcegraph/cody-shared' + +const CONFIG_FILE = 'config.json' + +const getConfigPath = () => path.join(codyPaths().config, CONFIG_FILE) + +async function exists(path: string): Promise { + try { + await fs.stat(path) + return true + } catch { + return false + } +} + +async function ensureDirectoryExists(directory: string) { + if (!(await exists(directory))) { + await fs.mkdir(directory, { recursive: true }) + } +} + +// Used to cleanup the uninstaller directory after the last telemetry event is sent +export async function deleteUninstallerConfig() { + return fs.rm(getConfigPath()) +} + +async function writeSnapshot(directory: string, filename: string, content: any): Promise { + const filePath = path.join(directory, filename) + + return fs.writeFile(filePath, JSON.stringify(content, null, 2)) +} + +interface UninstallerConfig { + config?: ResolvedConfiguration + authStatus: AuthStatus | undefined + clientCapabilities?: ClientCapabilities + version?: string +} + +/** + * Serializes the current configuration and auth status to disk. This is used in the case + * of an uninstall event to log one last telemetry event. + */ +export async function serializeConfigSnapshot(uninstall: UninstallerConfig) { + const directory = codyPaths().config + await ensureDirectoryExists(directory) + await writeSnapshot(directory, CONFIG_FILE, uninstall) +} + +export async function readConfig(): Promise { + const file = getConfigPath() + + if (!(await exists(file))) { + return null + } + + const obj = await fs.readFile(file, 'utf-8') + return JSON.parse(obj) +} From 2caf3ee40a7e933d2509dda0a8f0e50d44f00ad3 Mon Sep 17 00:00:00 2001 From: jamesmcnamara Date: Thu, 3 Oct 2024 14:54:13 -0700 Subject: [PATCH 02/16] delete endpoint history on reinstall --- lib/shared/src/configuration/resolver.ts | 39 ++++++++++----------- vscode/src/configuration.ts | 2 +- vscode/src/main.ts | 34 ++++++++++++++---- vscode/src/services/LocalStorageProvider.ts | 7 ++++ vscode/src/services/telemetry-v2.ts | 4 +-- 5 files changed, 55 insertions(+), 31 deletions(-) diff --git a/lib/shared/src/configuration/resolver.ts b/lib/shared/src/configuration/resolver.ts index e200344ca1d5..73654df4c0f8 100644 --- a/lib/shared/src/configuration/resolver.ts +++ b/lib/shared/src/configuration/resolver.ts @@ -1,11 +1,12 @@ import { Observable, map } from 'observable-fns' import type { AuthCredentials, ClientConfiguration } from '../configuration' -import { logError } from '../logger' +import { logDebug, logError } from '../logger' import { distinctUntilChanged, firstValueFrom, fromLateSetSource, promiseToObservable, + tap, } from '../misc/observable' import { skipPendingOperation, switchMapReplayOperation } from '../misc/observableOperation' import type { PerSitePreferences } from '../models/modelsService' @@ -19,12 +20,14 @@ export interface ConfigurationInput { clientConfiguration: ClientConfiguration clientSecrets: ClientSecrets clientState: ClientState - isReinstalling: boolean + reinstall: { + isReinstalling(): Promise + onReinstall(): Promise + } } export interface ClientSecrets { getToken(endpoint: string): Promise - deleteToken(endpoint: string): Promise } export interface ClientState { @@ -44,7 +47,7 @@ export type ResolvedConfiguration = ReadonlyDeep<{ configuration: ClientConfiguration auth: AuthCredentials clientState: ClientState - isReinstalling: boolean + isReinstall: boolean }> /** @@ -74,32 +77,23 @@ async function resolveConfiguration({ clientConfiguration, clientSecrets, clientState, - isReinstalling, + reinstall: { isReinstalling, onReinstall }, }: ConfigurationInput): Promise { + const isReinstall = await isReinstalling() + if (isReinstall) { + await onReinstall() + } // we allow for overriding the server endpoint from config if we haven't // manually signed in somewhere else const serverEndpoint = normalizeServerEndpointURL( clientConfiguration.overrideServerEndpoint || // If we are reinstalling, we ignore previous state - ((isReinstalling ? undefined : clientState.lastUsedEndpoint) ?? DOTCOM_URL.toString()) + (clientState.lastUsedEndpoint ?? DOTCOM_URL.toString()) ) // We must not throw here, because that would result in the `resolvedConfig` observable // terminating and all callers receiving no further config updates. const loadTokenFn = async () => { - // When the user is reinstalling, we want to clear the cached credentials - if (isReinstalling) { - await Promise.all([ - clientSecrets.deleteToken(serverEndpoint), - clientConfiguration.overrideServerEndpoint - ? clientSecrets.deleteToken(clientConfiguration.overrideServerEndpoint) - : Promise.resolve(undefined), - clientState.lastUsedEndpoint - ? clientSecrets.deleteToken(clientState.lastUsedEndpoint) - : Promise.resolve(undefined), - ]) - return null - } return clientSecrets.getToken(serverEndpoint).catch(error => { logError( 'resolveConfiguration', @@ -113,7 +107,7 @@ async function resolveConfiguration({ configuration: clientConfiguration, clientState, auth: { accessToken, serverEndpoint }, - isReinstalling, + isReinstall, } } @@ -138,7 +132,10 @@ export function setResolvedConfigurationObservable(input: Observable { + logDebug('resolvedConfig', JSON.stringify(value)) + }) ), false ) diff --git a/vscode/src/configuration.ts b/vscode/src/configuration.ts index 8f7116a48718..4d05362f2260 100644 --- a/vscode/src/configuration.ts +++ b/vscode/src/configuration.ts @@ -173,6 +173,6 @@ export function setStaticResolvedConfigurationWithAuthCredentials({ configuration: { ...getConfiguration(), customHeaders: configuration.customHeaders }, auth, clientState: localStorage.getClientState(), - isReinstalling: false, + isReinstall: false, }) } diff --git a/vscode/src/main.ts b/vscode/src/main.ts index 16c2d795df02..1dcb21e1126b 100644 --- a/vscode/src/main.ts +++ b/vscode/src/main.ts @@ -4,6 +4,7 @@ import { type ChatClient, ClientConfigSingleton, type ConfigurationInput, + DOTCOM_URL, type DefaultCodyCommands, FeatureFlag, type Guardrails, @@ -23,7 +24,6 @@ import { graphqlClient, isDotCom, modelsService, - promiseFactoryToObservable, resolvedConfig, setClientCapabilitiesFromConfiguration, setClientNameVersion, @@ -36,6 +36,7 @@ import { take, telemetryRecorder, } from '@sourcegraph/cody-shared' +import _ from 'lodash' import { isEqual } from 'lodash' import { filter, map } from 'observable-fns' import { isReinstalling } from '../uninstall/reinstall' @@ -140,23 +141,42 @@ export async function start( event => event.affectsConfiguration('cody') || event.affectsConfiguration('openctx') ), startWith(undefined), - map(() => getConfiguration()), + map(getConfiguration), distinctUntilChanged() ), fromVSCodeEvent(secretStorage.onDidChange.bind(secretStorage)).pipe( startWith(undefined), map(() => secretStorage) ), - localStorage.clientStateChanges.pipe(distinctUntilChanged()), - promiseFactoryToObservable(isReinstalling) + localStorage.clientStateChanges.pipe(distinctUntilChanged()) ).pipe( map( - ([clientConfiguration, clientSecrets, clientState, isReinstalling]) => + ([clientConfiguration, clientSecrets, clientState]) => ({ clientConfiguration, clientSecrets, clientState, - isReinstalling, + reinstall: { + isReinstalling, + onReinstall: async () => { + logDebug('start', 'Reinstalling Cody') + // VSCode does not provide a way to simply clear all secrets + // associated with the extension (https://github.com/microsoft/vscode/issues/123817) + // So we have to build a list of all endpoints we'd expect to have been populated + // and clear them individually. + const history = await localStorage.deleteEndpointHistory() + const additionalEndpointsToClear = [ + clientConfiguration.overrideServerEndpoint, + clientState.lastUsedEndpoint, + DOTCOM_URL.toString(), + ].filter(_.isString) + await Promise.all( + history + .concat(additionalEndpointsToClear) + .map(clientSecrets.deleteToken.bind(clientSecrets)) + ) + }, + }, }) satisfies ConfigurationInput ) ) @@ -707,7 +727,7 @@ function registerAutocomplete( catchError(error => { finishLoading() //TODO: We could show something in the statusbar - logError('registerAutocomplete', 'Error', error) + logError('registerAutocomplete', 'Error', JSON.stringify(error)) return NEVER }) ) diff --git a/vscode/src/services/LocalStorageProvider.ts b/vscode/src/services/LocalStorageProvider.ts index e6b366cda15d..2b816ae6fcb1 100644 --- a/vscode/src/services/LocalStorageProvider.ts +++ b/vscode/src/services/LocalStorageProvider.ts @@ -140,6 +140,13 @@ class LocalStorage implements LocalStorageForModelPreferences { await this.set(this.LAST_USED_ENDPOINT, null) } + // Deletes and returns the endpoint history + public async deleteEndpointHistory(): Promise { + const history = this.getEndpointHistory() + await Promise.all([this.deleteEndpoint(), this.set(this.CODY_ENDPOINT_HISTORY, null)]) + return history || [] + } + public getEndpointHistory(): string[] | null { return this.get(this.CODY_ENDPOINT_HISTORY) } diff --git a/vscode/src/services/telemetry-v2.ts b/vscode/src/services/telemetry-v2.ts index 04d385af07a6..4c4a335aed87 100644 --- a/vscode/src/services/telemetry-v2.ts +++ b/vscode/src/services/telemetry-v2.ts @@ -44,7 +44,7 @@ export function createOrUpdateTelemetryRecorderProvider( isExtensionModeDevOrTest: boolean ): Disposable { return subscriptionDisposable( - resolvedConfig.subscribe(({ configuration, auth, clientState, isReinstalling }) => { + resolvedConfig.subscribe(({ configuration, auth, clientState, isReinstall }) => { // Add timestamp processor for realistic data in output for dev or no-op scenarios const defaultNoOpProvider = new NoOpTelemetryRecorderProvider([ new TimestampTelemetryProcessor(), @@ -92,7 +92,7 @@ export function createOrUpdateTelemetryRecorderProvider( */ telemetryRecorder.recordEvent( 'cody.extension', - isReinstalling ? 'reinstalled' : 'installed', + isReinstall ? 'reinstalled' : 'installed', { billingMetadata: { product: 'cody', From 14ea6f69c6ad63444d2df86b8f510e98f7a96bb8 Mon Sep 17 00:00:00 2001 From: jamesmcnamara Date: Fri, 4 Oct 2024 18:05:42 -0700 Subject: [PATCH 03/16] added e2e test --- pnpm-lock.yaml | 84 +++++++++++++------ vscode/package.json | 5 +- vscode/src/main.ts | 4 + vscode/src/services/telemetry-v2.ts | 2 +- vscode/test/e2e/helpers.ts | 123 +++++++++++++++++++--------- vscode/test/e2e/install-deps.ts | 42 +--------- vscode/test/e2e/uninstall.test.ts | 61 ++++++++++++++ vscode/uninstall/post-uninstall.ts | 10 +-- vscode/uninstall/reinstall.ts | 12 ++- 9 files changed, 232 insertions(+), 111 deletions(-) create mode 100644 vscode/test/e2e/uninstall.test.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 91642a8beacd..f74a70d7b6c5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -746,8 +746,8 @@ importers: specifier: ^1.79.0 version: 1.80.0 '@vscode/test-electron': - specifier: ^2.3.8 - version: 2.3.8 + specifier: ^2.4.0 + version: 2.4.0 '@vscode/test-web': specifier: ^0.0.47 version: 0.0.47 @@ -6393,11 +6393,6 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: true - /@tootallnate/once@1.1.2: - resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==} - engines: {node: '>= 6'} - dev: true - /@tootallnate/once@2.0.0: resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} engines: {node: '>= 10'} @@ -7026,14 +7021,15 @@ packages: /@vscode/codicons@0.0.35: resolution: {integrity: sha512-7iiKdA5wHVYSbO7/Mm0hiHD3i4h+9hKUe1O4hISAe/nHhagMwb2ZbFC8jU6d7Cw+JNT2dWXN2j+WHbkhT5/l2w==} - /@vscode/test-electron@2.3.8: - resolution: {integrity: sha512-b4aZZsBKtMGdDljAsOPObnAi7+VWIaYl3ylCz1jTs+oV6BZ4TNHcVNC3xUn0azPeszBmwSBDQYfFESIaUQnrOg==} + /@vscode/test-electron@2.4.0: + resolution: {integrity: sha512-yojuDFEjohx6Jb+x949JRNtSn6Wk2FAh4MldLE3ck9cfvCqzwxF32QsNy1T9Oe4oT+ZfFcg0uPUCajJzOmPlTA==} engines: {node: '>=16'} dependencies: - http-proxy-agent: 4.0.1 - https-proxy-agent: 5.0.1 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.4 jszip: 3.10.1 - semver: 7.6.0 + ora: 7.0.1 + semver: 7.6.3 transitivePeerDependencies: - supports-color dev: true @@ -7672,6 +7668,14 @@ packages: readable-stream: 3.6.2 dev: true + /bl@5.1.0: + resolution: {integrity: sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==} + dependencies: + buffer: 6.0.3 + inherits: 2.0.4 + readable-stream: 3.6.2 + dev: true + /bluebird@3.4.7: resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==} dev: false @@ -10413,17 +10417,6 @@ packages: transitivePeerDependencies: - supports-color - /http-proxy-agent@4.0.1: - resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==} - engines: {node: '>= 6'} - dependencies: - '@tootallnate/once': 1.1.2 - agent-base: 6.0.2 - debug: 4.3.5 - transitivePeerDependencies: - - supports-color - dev: true - /http-proxy-agent@5.0.0: resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} engines: {node: '>= 6'} @@ -11550,6 +11543,14 @@ packages: is-unicode-supported: 0.1.0 dev: true + /log-symbols@5.1.0: + resolution: {integrity: sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==} + engines: {node: '>=12'} + dependencies: + chalk: 5.3.0 + is-unicode-supported: 1.3.0 + dev: true + /log-symbols@6.0.0: resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} engines: {node: '>=18'} @@ -12950,6 +12951,21 @@ packages: wcwidth: 1.0.1 dev: true + /ora@7.0.1: + resolution: {integrity: sha512-0TUxTiFJWv+JnjWm4o9yvuskpEJLXTcng8MJuKd+SzAzp2o+OP3HWqNhB4OdJRt1Vsd9/mR0oyaEYlOnL7XIRw==} + engines: {node: '>=16'} + dependencies: + chalk: 5.3.0 + cli-cursor: 4.0.0 + cli-spinners: 2.9.2 + is-interactive: 2.0.0 + is-unicode-supported: 1.3.0 + log-symbols: 5.1.0 + stdin-discarder: 0.1.0 + string-width: 6.1.0 + strip-ansi: 7.1.0 + dev: true + /ora@8.0.1: resolution: {integrity: sha512-ANIvzobt1rls2BDny5fWZ3ZVKyD6nscLvfFRpQgfWsythlcsVUC9kL0zq6j2Z5z9wwp1kd7wpsD/T9qNPVLCaQ==} engines: {node: '>=18'} @@ -14502,6 +14518,12 @@ packages: dependencies: lru-cache: 6.0.0 + /semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + dev: true + /send@0.18.0: resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} engines: {node: '>= 0.8.0'} @@ -14872,6 +14894,13 @@ packages: resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} dev: true + /stdin-discarder@0.1.0: + resolution: {integrity: sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + bl: 5.1.0 + dev: true + /stdin-discarder@0.2.2: resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} engines: {node: '>=18'} @@ -14951,6 +14980,15 @@ packages: emoji-regex: 9.2.2 strip-ansi: 7.1.0 + /string-width@6.1.0: + resolution: {integrity: sha512-k01swCJAgQmuADB0YIc+7TuatfNvTBVOoaUWJjTB9R4VJzR5vNWzf5t42ESVZFPS8xTySF7CAdV4t/aaIm3UnQ==} + engines: {node: '>=16'} + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 10.3.0 + strip-ansi: 7.1.0 + dev: true + /string-width@7.1.0: resolution: {integrity: sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==} engines: {node: '>=18'} diff --git a/vscode/package.json b/vscode/package.json index 87f04e261df1..7fce1a82942a 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -35,12 +35,13 @@ "_build:esbuild:web": "esbuild ./src/extension.web.ts --platform=browser --bundle --outfile=dist/extension.web.js --alias:@sourcegraph/cody-shared=@sourcegraph/cody-shared/src/index --alias:@sourcegraph/cody-shared/src=@sourcegraph/cody-shared/src --alias:path=path-browserify --external:typescript --alias:node:path=path-browserify --alias:node:os=os-browserify --external:vscode --external:node:child_process --external:node:util --external:node:fs --external:node:fs/promises --external:node:process --define:process='{\"env\":{}}' --define:window=self --format=cjs --sourcemap", "_build:esbuild:uninstall": "node ./uninstall/esbuild.mjs", "_build:webviews": "vite -c webviews/vite.config.mts build", + "_build:vsix_for_test": "vsce package --no-dependencies --out dist/cody.e2e.vsix", "release": "ts-node-transpile-only ./scripts/release.ts", "download-wasm": "ts-node-transpile-only ./scripts/download-wasm-modules.ts", "copy-win-ca-roots": "ts-node-transpile-only ./scripts/copy-win-ca-roots.ts", "release:dry-run": "pnpm run download-wasm && CODY_RELEASE_DRY_RUN=1 ts-node ./scripts/release.ts", "storybook": "storybook dev -p 6007 --no-open --no-version-updates", - "test:e2e": "playwright install && tsc --build && node dist/tsc/test/e2e/install-deps.js && pnpm run -s build:dev:desktop && pnpm run -s test:e2e:run", + "test:e2e": "playwright install && tsc --build && node dist/tsc/test/e2e/install-deps.js && pnpm run -s _build:vsix_for_test && pnpm run -s build:dev:desktop && pnpm run -s test:e2e:run", "test:e2e:run": "playwright test", "test:e2e2": "pnpm -s test:e2e2:deps && pnpm -s build:root && pnpm -s build:dev:desktop && pnpm -s test:e2e2:run", "test:e2e2:run": "playwright test -c playwright.v2.config.ts", @@ -1424,7 +1425,7 @@ "@types/unzipper": "^0.10.7", "@types/uuid": "^9.0.2", "@types/vscode": "^1.79.0", - "@vscode/test-electron": "^2.3.8", + "@vscode/test-electron": "^2.4.0", "@vscode/test-web": "^0.0.47", "@vscode/vsce": "^2.22.0", "ajv": "^8.14.0", diff --git a/vscode/src/main.ts b/vscode/src/main.ts index 1dcb21e1126b..a27d074d4d66 100644 --- a/vscode/src/main.ts +++ b/vscode/src/main.ts @@ -134,6 +134,8 @@ export async function start( setClientCapabilitiesFromConfiguration(getConfiguration()) + let hasReinstallCleanupRun = false + setResolvedConfigurationObservable( combineLatest( fromVSCodeEvent(vscode.workspace.onDidChangeConfiguration).pipe( @@ -159,6 +161,7 @@ export async function start( reinstall: { isReinstalling, onReinstall: async () => { + if (hasReinstallCleanupRun) return logDebug('start', 'Reinstalling Cody') // VSCode does not provide a way to simply clear all secrets // associated with the extension (https://github.com/microsoft/vscode/issues/123817) @@ -175,6 +178,7 @@ export async function start( .concat(additionalEndpointsToClear) .map(clientSecrets.deleteToken.bind(clientSecrets)) ) + hasReinstallCleanupRun = true }, }, }) satisfies ConfigurationInput diff --git a/vscode/src/services/telemetry-v2.ts b/vscode/src/services/telemetry-v2.ts index 4c4a335aed87..cebe0a9abedf 100644 --- a/vscode/src/services/telemetry-v2.ts +++ b/vscode/src/services/telemetry-v2.ts @@ -86,7 +86,7 @@ export function createOrUpdateTelemetryRecorderProvider( */ const newAnonymousUser = localStorage.checkIfCreatedAnonymousUserID() if (initialize && !clientCapabilities().isCodyWeb) { - if (newAnonymousUser) { + if (newAnonymousUser || isReinstall) { /** * New user */ diff --git a/vscode/test/e2e/helpers.ts b/vscode/test/e2e/helpers.ts index ab09b519272a..70c571a8286f 100644 --- a/vscode/test/e2e/helpers.ts +++ b/vscode/test/e2e/helpers.ts @@ -27,6 +27,7 @@ import { import type { RepoListResponse } from '@sourcegraph/cody-shared' import type { RepositoryIdResponse } from '@sourcegraph/cody-shared/src/sourcegraph-api/graphql/client' +import { resolveCliArgsFromVSCodeExecutablePath } from '@vscode/test-electron' import { closeSidebar, expectAuthenticated, focusSidebar } from './common' import { installVsCode } from './install-deps' import { buildCustomCommandConfigFile } from './utils/buildCustomCommands' @@ -73,6 +74,17 @@ export const getAssetsDir = (testName: string): string => export const testAssetsTmpDir = (testName: string, label: string): string => path.join(getAssetsDir(testName), `temp-${label}`) +export interface OpenVSCodeOptions { + // A list of extensions to install or uninstall before starting VSCode. These can + // be extensions that are already published to the VSCode Marketplace, or they can + // be local paths to a VSIX package. Useful if you want to behavior related to setup / teardown + installExtensions?: string[] + uninstallExtensions?: string[] + // Whether or not the code in the current git tree will be installed int the VSCode + // instance. Defaults to true. + skipLocalInstall?: boolean +} + export const test = base // By default, use ../../test/fixtures/workspace as the workspace. .extend({ @@ -137,9 +149,9 @@ export const test = base { auto: true }, ], }) - .extend<{ app: ElectronApplication }>({ + .extend<{ openVSCode: (opts?: OpenVSCodeOptions) => Promise }>({ // starts a new instance of vscode with the given workspace settings - app: async ( + openVSCode: async ( { workspaceDirectory, extraWorkspaceSettings, @@ -174,37 +186,70 @@ export const test = base TESTING_SECRET_STORAGE_TOKEN: JSON.stringify([SERVER_URL, VALID_TOKEN]), } } + const args = [ + // https://github.com/microsoft/vscode/issues/84238 + '--no-sandbox', + // https://github.com/microsoft/vscode-test/issues/120 + '--disable-updates', + '--skip-welcome', + '--skip-release-notes', + '--disable-workspace-trust', + `--user-data-dir=${userDataDirectory}`, + `--extensions-dir=${extensionsDirectory}`, + ] + + async function runVSCodeCommand(cmd: string): Promise { + const [cli, ...additionalArgs] = + resolveCliArgsFromVSCodeExecutablePath(vscodeExecutablePath) + + await spawn(cli, [...additionalArgs, ...args, cmd], { + stdio: 'ignore', + shell: process.platform === 'win32', + }) + } + // See: https://github.com/microsoft/vscode-test/blob/main/lib/runTest.ts - const app = await electron.launch({ - executablePath: vscodeExecutablePath, - env: { - ...process.env, - ...dotcomUrlOverride, - ...secretStorageState, - CODY_TESTING: 'true', - CODY_LOG_FILE: tmpLogFile, - }, - args: [ - // https://github.com/microsoft/vscode/issues/84238 - '--no-sandbox', - // https://github.com/microsoft/vscode-test/issues/120 - '--disable-updates', - '--skip-welcome', - '--skip-release-notes', - '--disable-workspace-trust', - `--extensionDevelopmentPath=${extensionDevelopmentPath}`, - `--user-data-dir=${userDataDirectory}`, - `--extensions-dir=${extensionsDirectory}`, - workspaceDirectory, - ], - recordVideo: { - // All running tests will be recorded to a temp video file. - // successful runs will be deleted, failures will be kept - dir: testAssetsTmpDir(testInfo.title, 'videos'), - }, - }) + use(async (opts: OpenVSCodeOptions = {}) => { + if (opts.installExtensions?.length) { + runVSCodeCommand( + opts.installExtensions.map(ext => `--install-extension=${ext}`).join(' ') + ) + } + if (opts.uninstallExtensions?.length) { + runVSCodeCommand( + opts.uninstallExtensions.map(ext => `--uninstall-extension=${ext}`).join(' ') + ) + } + if (!opts.skipLocalInstall) { + args.push(`--extensionDevelopmentPath=${extensionDevelopmentPath}`) + } + + args.push(workspaceDirectory) - await waitUntil(() => app.windows().length > 0) + const app = await electron.launch({ + executablePath: vscodeExecutablePath, + env: { + ...process.env, + ...dotcomUrlOverride, + ...secretStorageState, + CODY_TESTING: 'true', + CODY_LOG_FILE: tmpLogFile, + }, + args, + recordVideo: { + // All running tests will be recorded to a temp video file. + // successful runs will be deleted, failures will be kept + dir: testAssetsTmpDir(testInfo.title, 'videos'), + }, + }) + await waitUntil(() => app.windows().length > 0) + return app + }) + }, + }) + .extend<{ app: ElectronApplication }>({ + app: async ({ openVSCode, userDataDirectory, extensionsDirectory }, use) => { + const app = await openVSCode() await use(app) @@ -337,7 +382,7 @@ export async function rmSyncWithRetries(path: PathLike, options?: RmOptions): Pr } } -async function getCodySidebar(page: Page): Promise { +export async function getCodySidebar(page: Page): Promise { async function findCodySidebarFrame(): Promise { for (const frame of page.frames()) { try { @@ -347,9 +392,13 @@ async function getCodySidebar(page: Page): Promise { } } catch (error: any) { // Skip over frames that were detached in the meantime. - if (error.message.indexOf('Frame was detached') === -1) { - throw error + if ( + error.message.includes('Frame was detached') || + error.message.includes('because of a navigation') + ) { + continue } + throw error } } return null @@ -412,7 +461,7 @@ export async function executeCommandInPalette(page: Page, commandName: string): /** * Verifies that loggedEvents contain all of expectedEvents (in any order). */ -const expect = baseExpect.extend({ +export const expect = baseExpect.extend({ async toContainEvents( received: string[], expected: string[], @@ -424,11 +473,9 @@ const expect = baseExpect.extend({ try { await baseExpect - .poll(() => received, { timeout: 3000, ...options }) + .poll(() => received, { timeout: options?.timeout ?? 3000, ...options }) .toEqual(baseExpect.arrayContaining(expected)) } catch (e: any) { - // const missingEvents = new Set() - // const extraEvents = new Set() const receivedSet = new Set(received) for (const event of expected) { if (!receivedSet.has(event)) { diff --git a/vscode/test/e2e/install-deps.ts b/vscode/test/e2e/install-deps.ts index e6f7a08ada06..3f6980a05ed1 100644 --- a/vscode/test/e2e/install-deps.ts +++ b/vscode/test/e2e/install-deps.ts @@ -1,12 +1,6 @@ import { spawn } from 'node:child_process' -import { - ConsoleReporter, - type ProgressReport, - ProgressReportStage, - downloadAndUnzipVSCode as _downloadAndUnzipVSCode, -} from '@vscode/test-electron' -import type { DownloadOptions } from '@vscode/test-electron/out/download' +import { SilentReporter, downloadAndUnzipVSCode } from '@vscode/test-electron' // The VS Code version to use for e2e tests (there is also a version in ../integration/main.ts used for integration tests). // @@ -15,38 +9,8 @@ import type { DownloadOptions } from '@vscode/test-electron/out/download' // missed because we're running on an older version than users. const vscodeVersion = 'stable' -// A custom version of the VS Code download reporter that silences matching installation -// notifications as these otherwise are emitted on every test run -class CustomConsoleReporter extends ConsoleReporter { - public report(report: ProgressReport): void { - if (report.stage !== ProgressReportStage.FoundMatchingInstall) { - super.report(report) - } - } -} - -/** - * Patches the default logger but otherwise leaves all options available - * @param opts - */ -export function downloadAndUnzipVSCode(opts: Partial) { - return _downloadAndUnzipVSCode( - Object.assign( - { - version: vscodeVersion, - reporter: new CustomConsoleReporter(process.stdout.isTTY), - } satisfies Partial, - opts - ) - ) -} - -export function installVsCode(): Promise { - return _downloadAndUnzipVSCode( - vscodeVersion, - undefined, - new CustomConsoleReporter(process.stdout.isTTY) - ) +export async function installVsCode(): Promise { + return downloadAndUnzipVSCode(vscodeVersion, undefined, new SilentReporter()) } function installChromium(): Promise { diff --git a/vscode/test/e2e/uninstall.test.ts b/vscode/test/e2e/uninstall.test.ts new file mode 100644 index 000000000000..319638154fb9 --- /dev/null +++ b/vscode/test/e2e/uninstall.test.ts @@ -0,0 +1,61 @@ +import path from 'node:path' +import type { Page } from 'playwright' +import { loggedV2Events } from '../fixtures/mock-server' +import { focusSidebar, sidebarSignin } from './common' +import { expect, getCodySidebar, test } from './helpers' + +test('uninstall extension', async ({ openVSCode }) => { + test.setTimeout(600000) + // In order to trigger the uninstall event, we need to actually install the extension + // into the local vscode instance + const customExtensionVSIX = path.join(process.cwd(), 'dist', 'cody.e2e.vsix') + let app = await openVSCode({ + installExtensions: [customExtensionVSIX], + skipLocalInstall: true, + }) + let page = await app.firstWindow() + await signin(page) + await app.close() + + // Now we uninstall the extension, and re-open VSCode. This will trigger the + // vscode:uninstall event which will trigger telemetry events and set a marker + // that the app has been uninstalled + app = await openVSCode({ + uninstallExtensions: [customExtensionVSIX], + skipLocalInstall: true, + }) + // Allow the uninstaller to finish + try { + await expect(loggedV2Events).toContainEvents(['cody.extension:uninstalled'], { timeout: 5000 }) + } catch (error) { + await sleep(100000) + } + await app.close() + + // Finally, we re-install the extension, and re-open VSCode. This will trigger the + // the reinstall flow which will trigger telemetry events but will clear out secret storage + app = await openVSCode({ + installExtensions: [customExtensionVSIX], + skipLocalInstall: true, + }) + page = await app.firstWindow() + + // This will fail if the credentials are saved because the login screen will still be + // visible, thus it acts as an implicit test that credentials were cleared out + await signin(page) + try { + await expect(loggedV2Events).toContainEvents(['cody.extension:reinstalled'], { timeout: 5000 }) + } catch (error) { + await sleep(100000) + } +}) + +async function signin(page: Page): Promise { + await focusSidebar(page) + const sidebar = await getCodySidebar(page) + await sidebarSignin(page, sidebar) +} + +async function sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)) +} diff --git a/vscode/uninstall/post-uninstall.ts b/vscode/uninstall/post-uninstall.ts index 50852094945e..e94cfc30a329 100644 --- a/vscode/uninstall/post-uninstall.ts +++ b/vscode/uninstall/post-uninstall.ts @@ -1,5 +1,6 @@ import { CodyIDE, + MockServerTelemetryRecorderProvider, TelemetryRecorderProvider, mockClientCapabilities, nextTick, @@ -11,11 +12,6 @@ import { createUninstallMarker } from './reinstall' import { deleteUninstallerConfig, readConfig } from './serializeConfig' async function main() { - // Do not record telemetry events during testing - if (process.env.CODY_TESTING) { - return - } - const uninstaller = await readConfig() if (uninstaller) { const { config, authStatus, version, clientCapabilities } = uninstaller @@ -44,7 +40,9 @@ async function main() { } ) - const provider = new TelemetryRecorderProvider(config, 'connected-instance-only') + const provider = process.env.CODY_TESTING + ? new MockServerTelemetryRecorderProvider(config) + : new TelemetryRecorderProvider(config, 'connected-instance-only') const recorder = provider.getRecorder() recorder.recordEvent('cody.extension', 'uninstalled', { billingMetadata: { diff --git a/vscode/uninstall/reinstall.ts b/vscode/uninstall/reinstall.ts index 81cfff17a277..d3804ee4c48a 100644 --- a/vscode/uninstall/reinstall.ts +++ b/vscode/uninstall/reinstall.ts @@ -7,14 +7,22 @@ export const createUninstallMarker = async (): Promise => { await fs.writeFile(uninstallMarker, '') } +let isReinstall: boolean | undefined = undefined // Checks if the user is reinstalling the extension by checking for the existence of a marker file // If found, it deletes the marker file so that we only report reinstalling once +// Caches the value of isReinstall so that throughout the lifetime of the extension +// it still reports it as a re-install export const isReinstalling = async (): Promise => { + if (typeof isReinstall === 'boolean') { + return isReinstall + } try { await fs.stat(uninstallMarker) await fs.unlink(uninstallMarker) - return true + isReinstall = true } catch (error) { - return false + isReinstall = false } + + return isReinstall } From 30afc689f9613dde0c4a7640b69730f8e3a5e2eb Mon Sep 17 00:00:00 2001 From: jamesmcnamara Date: Fri, 4 Oct 2024 18:09:11 -0700 Subject: [PATCH 04/16] fixed e2e test --- vscode/test/e2e/uninstall.test.ts | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/vscode/test/e2e/uninstall.test.ts b/vscode/test/e2e/uninstall.test.ts index 319638154fb9..80de777a49d7 100644 --- a/vscode/test/e2e/uninstall.test.ts +++ b/vscode/test/e2e/uninstall.test.ts @@ -1,7 +1,7 @@ import path from 'node:path' import type { Page } from 'playwright' import { loggedV2Events } from '../fixtures/mock-server' -import { focusSidebar, sidebarSignin } from './common' +import { expectAuthenticated, focusSidebar, sidebarSignin } from './common' import { expect, getCodySidebar, test } from './helpers' test('uninstall extension', async ({ openVSCode }) => { @@ -25,14 +25,10 @@ test('uninstall extension', async ({ openVSCode }) => { skipLocalInstall: true, }) // Allow the uninstaller to finish - try { - await expect(loggedV2Events).toContainEvents(['cody.extension:uninstalled'], { timeout: 5000 }) - } catch (error) { - await sleep(100000) - } + await expect(loggedV2Events).toContainEvents(['cody.extension:uninstalled'], { timeout: 5000 }) await app.close() - // Finally, we re-install the extension, and re-open VSCode. This will trigger the + // we re-install the extension, and re-open VSCode. This will trigger the // the reinstall flow which will trigger telemetry events but will clear out secret storage app = await openVSCode({ installExtensions: [customExtensionVSIX], @@ -43,11 +39,15 @@ test('uninstall extension', async ({ openVSCode }) => { // This will fail if the credentials are saved because the login screen will still be // visible, thus it acts as an implicit test that credentials were cleared out await signin(page) - try { - await expect(loggedV2Events).toContainEvents(['cody.extension:reinstalled'], { timeout: 5000 }) - } catch (error) { - await sleep(100000) - } + await expect(loggedV2Events).toContainEvents(['cody.extension:reinstalled'], { timeout: 5000 }) + await app.close() + + // Finally, re-open the VSCode and ensure that we are still logged in + app = await openVSCode({ + skipLocalInstall: true, + }) + page = await app.firstWindow() + await expectAuthenticated(page) }) async function signin(page: Page): Promise { @@ -55,7 +55,3 @@ async function signin(page: Page): Promise { const sidebar = await getCodySidebar(page) await sidebarSignin(page, sidebar) } - -async function sleep(ms: number): Promise { - return new Promise(resolve => setTimeout(resolve, ms)) -} From ab492f11da1b084b7dec31f23fc93e4abc20b5a9 Mon Sep 17 00:00:00 2001 From: jamesmcnamara Date: Fri, 4 Oct 2024 18:18:11 -0700 Subject: [PATCH 05/16] cleanup --- lib/shared/src/configuration/resolver.ts | 14 ++++---------- vscode/src/main.ts | 3 ++- vscode/test/e2e/uninstall.test.ts | 1 - 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/lib/shared/src/configuration/resolver.ts b/lib/shared/src/configuration/resolver.ts index 73654df4c0f8..b26ce410c193 100644 --- a/lib/shared/src/configuration/resolver.ts +++ b/lib/shared/src/configuration/resolver.ts @@ -1,12 +1,11 @@ import { Observable, map } from 'observable-fns' import type { AuthCredentials, ClientConfiguration } from '../configuration' -import { logDebug, logError } from '../logger' +import { logError } from '../logger' import { distinctUntilChanged, firstValueFrom, fromLateSetSource, promiseToObservable, - tap, } from '../misc/observable' import { skipPendingOperation, switchMapReplayOperation } from '../misc/observableOperation' import type { PerSitePreferences } from '../models/modelsService' @@ -87,21 +86,19 @@ async function resolveConfiguration({ // manually signed in somewhere else const serverEndpoint = normalizeServerEndpointURL( clientConfiguration.overrideServerEndpoint || - // If we are reinstalling, we ignore previous state (clientState.lastUsedEndpoint ?? DOTCOM_URL.toString()) ) // We must not throw here, because that would result in the `resolvedConfig` observable // terminating and all callers receiving no further config updates. - const loadTokenFn = async () => { - return clientSecrets.getToken(serverEndpoint).catch(error => { + const loadTokenFn = () => + clientSecrets.getToken(serverEndpoint).catch(error => { logError( 'resolveConfiguration', `Failed to get access token for endpoint ${serverEndpoint}: ${error}` ) return null }) - } const accessToken = clientConfiguration.overrideAuthToken || ((await loadTokenFn()) ?? null) return { configuration: clientConfiguration, @@ -132,10 +129,7 @@ export function setResolvedConfigurationObservable(input: Observable { - logDebug('resolvedConfig', JSON.stringify(value)) - }) + distinctUntilChanged() ), false ) diff --git a/vscode/src/main.ts b/vscode/src/main.ts index a27d074d4d66..350ed4e928d9 100644 --- a/vscode/src/main.ts +++ b/vscode/src/main.ts @@ -161,6 +161,7 @@ export async function start( reinstall: { isReinstalling, onReinstall: async () => { + // short circuit so that we only run this cleanup once, not every time the config updates if (hasReinstallCleanupRun) return logDebug('start', 'Reinstalling Cody') // VSCode does not provide a way to simply clear all secrets @@ -731,7 +732,7 @@ function registerAutocomplete( catchError(error => { finishLoading() //TODO: We could show something in the statusbar - logError('registerAutocomplete', 'Error', JSON.stringify(error)) + logError('registerAutocomplete', 'Error', error) return NEVER }) ) diff --git a/vscode/test/e2e/uninstall.test.ts b/vscode/test/e2e/uninstall.test.ts index 80de777a49d7..b04fe37938b6 100644 --- a/vscode/test/e2e/uninstall.test.ts +++ b/vscode/test/e2e/uninstall.test.ts @@ -5,7 +5,6 @@ import { expectAuthenticated, focusSidebar, sidebarSignin } from './common' import { expect, getCodySidebar, test } from './helpers' test('uninstall extension', async ({ openVSCode }) => { - test.setTimeout(600000) // In order to trigger the uninstall event, we need to actually install the extension // into the local vscode instance const customExtensionVSIX = path.join(process.cwd(), 'dist', 'cody.e2e.vsix') From 916def8face18cc74da6fde5951ca3b74ffe7bae Mon Sep 17 00:00:00 2001 From: jamesmcnamara Date: Sun, 6 Oct 2024 15:48:28 -0700 Subject: [PATCH 06/16] made env-paths a dynamic import --- agent/src/agent.ts | 6 +++--- lib/shared/src/codyPaths.ts | 9 ++++++--- lib/shared/tsconfig.json | 17 ++++------------- vscode/uninstall/reinstall.ts | 9 +++++++-- vscode/uninstall/serializeConfig.ts | 8 ++++---- 5 files changed, 24 insertions(+), 25 deletions(-) diff --git a/agent/src/agent.ts b/agent/src/agent.ts index d471f5c23100..8eda727a710d 100644 --- a/agent/src/agent.ts +++ b/agent/src/agent.ts @@ -150,7 +150,7 @@ async function initializeVscodeExtension( globalState: AgentGlobalState, secrets: vscode.SecretStorage ): Promise { - const paths = codyPaths() + const paths = await codyPaths() const extensionPath = paths.config copyExtensionRelativeResources(extensionPath, extensionClient) @@ -466,7 +466,7 @@ export class Agent extends MessageHandler implements ExtensionClient { } registerNativeWebviewHandlers( this, - vscode.Uri.file(codyPaths().config + '/dist'), + vscode.Uri.file((await codyPaths()).config + '/dist'), nativeWebviewConfig ) } else { @@ -1424,7 +1424,7 @@ export class Agent extends MessageHandler implements ExtensionClient { case 'server-managed': return AgentGlobalState.initialize( clientInfo.name, - clientInfo.globalStateDir ?? codyPaths().data + clientInfo.globalStateDir ?? (await codyPaths()).data ) case 'client-managed': throw new Error('client-managed global state is not supported') diff --git a/lib/shared/src/codyPaths.ts b/lib/shared/src/codyPaths.ts index e2686b0bb5a7..1d776540bf41 100644 --- a/lib/shared/src/codyPaths.ts +++ b/lib/shared/src/codyPaths.ts @@ -1,3 +1,6 @@ -import envPaths from 'env-paths' - -export const codyPaths = () => envPaths('Cody') +// Because env-paths is distributed as an ESM module but we package everything as CommonJS, +// we have to wrap this into a dynamic import for the playwright tests +export const codyPaths = async () => { + const envPaths = await import('env-paths') + return envPaths.default('Cody') +} diff --git a/lib/shared/tsconfig.json b/lib/shared/tsconfig.json index 15c112b36e89..78a2666e733b 100644 --- a/lib/shared/tsconfig.json +++ b/lib/shared/tsconfig.json @@ -4,18 +4,9 @@ "module": "ESNext", "rootDir": "src", "outDir": "dist", - "lib": [ - "ESNext", - "DOM", - "DOM.Iterable" - ], - "moduleResolution": "Bundler", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "moduleResolution": "Bundler" }, - "include": [ - "src", - ], - "exclude": [ - "dist", - "typehacks" - ], + "include": ["src"], + "exclude": ["dist", "typehacks"] } diff --git a/vscode/uninstall/reinstall.ts b/vscode/uninstall/reinstall.ts index d3804ee4c48a..03a0d7f5f4e8 100644 --- a/vscode/uninstall/reinstall.ts +++ b/vscode/uninstall/reinstall.ts @@ -1,10 +1,14 @@ import fs from 'node:fs/promises' +import path from 'node:path' import { codyPaths } from '@sourcegraph/cody-shared' -export const uninstallMarker = codyPaths().config + '/uninstall-marker' +export async function getUninstallMarker() { + const paths = await codyPaths() + return path.join(paths.config, 'uninstall-marker') +} export const createUninstallMarker = async (): Promise => { - await fs.writeFile(uninstallMarker, '') + await fs.writeFile(await getUninstallMarker(), '') } let isReinstall: boolean | undefined = undefined @@ -17,6 +21,7 @@ export const isReinstalling = async (): Promise => { return isReinstall } try { + const uninstallMarker = await getUninstallMarker() await fs.stat(uninstallMarker) await fs.unlink(uninstallMarker) isReinstall = true diff --git a/vscode/uninstall/serializeConfig.ts b/vscode/uninstall/serializeConfig.ts index 4fa26509ebd5..02afc428e014 100644 --- a/vscode/uninstall/serializeConfig.ts +++ b/vscode/uninstall/serializeConfig.ts @@ -9,7 +9,7 @@ import { const CONFIG_FILE = 'config.json' -const getConfigPath = () => path.join(codyPaths().config, CONFIG_FILE) +const getConfigPath = async () => path.join((await codyPaths()).config, CONFIG_FILE) async function exists(path: string): Promise { try { @@ -28,7 +28,7 @@ async function ensureDirectoryExists(directory: string) { // Used to cleanup the uninstaller directory after the last telemetry event is sent export async function deleteUninstallerConfig() { - return fs.rm(getConfigPath()) + return fs.rm(await getConfigPath()) } async function writeSnapshot(directory: string, filename: string, content: any): Promise { @@ -49,13 +49,13 @@ interface UninstallerConfig { * of an uninstall event to log one last telemetry event. */ export async function serializeConfigSnapshot(uninstall: UninstallerConfig) { - const directory = codyPaths().config + const directory = (await codyPaths()).config await ensureDirectoryExists(directory) await writeSnapshot(directory, CONFIG_FILE, uninstall) } export async function readConfig(): Promise { - const file = getConfigPath() + const file = await getConfigPath() if (!(await exists(file))) { return null From 4434d013983b216ea3de4cff966f3bbb7371b1bc Mon Sep 17 00:00:00 2001 From: jamesmcnamara Date: Mon, 7 Oct 2024 10:11:31 -0700 Subject: [PATCH 07/16] ran biome --- agent/src/esbuild.mjs | 2 +- lib/shared/esbuild.utils.mjs | 7 +++---- lib/shared/package.json | 6 +----- vscode/uninstall/esbuild.mjs | 2 +- 4 files changed, 6 insertions(+), 11 deletions(-) diff --git a/agent/src/esbuild.mjs b/agent/src/esbuild.mjs index 22413ed76b54..777c6b4f96be 100644 --- a/agent/src/esbuild.mjs +++ b/agent/src/esbuild.mjs @@ -2,7 +2,7 @@ import fs from 'node:fs/promises' import path from 'node:path' import process from 'node:process' // @ts-ignore this is not compiled by typescript so it can import files from outside the rootDir -import { detectForbiddenImportPlugin } from "../../lib/shared/esbuild.utils.mjs"; +import { detectForbiddenImportPlugin } from '../../lib/shared/esbuild.utils.mjs' import { build } from 'esbuild' diff --git a/lib/shared/esbuild.utils.mjs b/lib/shared/esbuild.utils.mjs index cf13870dc007..f97bc888db93 100644 --- a/lib/shared/esbuild.utils.mjs +++ b/lib/shared/esbuild.utils.mjs @@ -1,10 +1,9 @@ +import fs from 'node:fs/promises' -import fs from "node:fs/promises"; - -export function detectForbiddenImportPlugin (allForbiddenModules) { +export function detectForbiddenImportPlugin(allForbiddenModules) { return { name: 'detect-forbidden-import-plugin', - setup (build) { + setup(build) { build.onResolve({ filter: /.*/ }, args => { for (const forbidden of allForbiddenModules) { if (args.path === forbidden) { diff --git a/lib/shared/package.json b/lib/shared/package.json index 530cae340b94..57abb3023d1f 100644 --- a/lib/shared/package.json +++ b/lib/shared/package.json @@ -10,11 +10,7 @@ }, "main": "dist/index.js", "types": "dist/index.d.ts", - "files": [ - "dist", - "src", - "!**/*.test.*" - ], + "files": ["dist", "src", "!**/*.test.*"], "sideEffects": false, "scripts": { "build": "tsc --build", diff --git a/vscode/uninstall/esbuild.mjs b/vscode/uninstall/esbuild.mjs index 1c2cc6208b5b..e2c3b2200378 100644 --- a/vscode/uninstall/esbuild.mjs +++ b/vscode/uninstall/esbuild.mjs @@ -3,7 +3,7 @@ import { detectForbiddenImportPlugin } from '../../lib/shared/esbuild.utils.mjs' import { build as esbuild } from 'esbuild' -async function build () { +async function build() { const plugins = [detectForbiddenImportPlugin(['vscode'])] /** @type {import('esbuild').BuildOptions} */ const esbuildOptions = { From 6793cd79a5f266356b0e3f6fa8c9fd010fec0843 Mon Sep 17 00:00:00 2001 From: jamesmcnamara Date: Mon, 7 Oct 2024 11:32:58 -0700 Subject: [PATCH 08/16] reverted to older version of env-paths to allow for CJS build --- agent/src/agent.ts | 6 +++--- agent/tsconfig.json | 20 ++++++++++++++++---- lib/shared/package.json | 2 +- lib/shared/src/codyPaths.ts | 9 +++------ lib/shared/tsconfig.json | 2 +- pnpm-lock.yaml | 5 +++-- vscode/package.json | 2 +- vscode/uninstall/reinstall.ts | 8 ++------ vscode/uninstall/serializeConfig.ts | 8 ++++---- 9 files changed, 34 insertions(+), 28 deletions(-) diff --git a/agent/src/agent.ts b/agent/src/agent.ts index 8eda727a710d..d471f5c23100 100644 --- a/agent/src/agent.ts +++ b/agent/src/agent.ts @@ -150,7 +150,7 @@ async function initializeVscodeExtension( globalState: AgentGlobalState, secrets: vscode.SecretStorage ): Promise { - const paths = await codyPaths() + const paths = codyPaths() const extensionPath = paths.config copyExtensionRelativeResources(extensionPath, extensionClient) @@ -466,7 +466,7 @@ export class Agent extends MessageHandler implements ExtensionClient { } registerNativeWebviewHandlers( this, - vscode.Uri.file((await codyPaths()).config + '/dist'), + vscode.Uri.file(codyPaths().config + '/dist'), nativeWebviewConfig ) } else { @@ -1424,7 +1424,7 @@ export class Agent extends MessageHandler implements ExtensionClient { case 'server-managed': return AgentGlobalState.initialize( clientInfo.name, - clientInfo.globalStateDir ?? (await codyPaths()).data + clientInfo.globalStateDir ?? codyPaths().data ) case 'client-managed': throw new Error('client-managed global state is not supported') diff --git a/agent/tsconfig.json b/agent/tsconfig.json index d1b66575797c..72287b695456 100644 --- a/agent/tsconfig.json +++ b/agent/tsconfig.json @@ -5,9 +5,21 @@ "rootDir": ".", "outDir": "dist", "target": "es2019", - "allowJs": true, + "allowJs": false }, - "include": ["**/*", ".*", "package.json", "src/language-file-extensions.json"], - "exclude": ["dist", "bindings", "src/__tests__", "vitest.config.ts", "typehacks"], - "references": [{ "path": "../lib/shared" }, { "path": "../vscode" }], + "include": [ + "**/*", + ".*", + "package.json", + "src/language-file-extensions.json" + ], + "exclude": [ + "dist", + "bindings", + "src/__tests__", + "vitest.config.ts", + "typehacks", + "*.mjs" + ], + "references": [{ "path": "../lib/shared" }, { "path": "../vscode" }] } diff --git a/lib/shared/package.json b/lib/shared/package.json index 57abb3023d1f..448409eb038e 100644 --- a/lib/shared/package.json +++ b/lib/shared/package.json @@ -25,7 +25,7 @@ "date-fns": "^2.30.0", "dedent": "^0.7.0", "diff": "^5.2.0", - "env-paths": "^3.0.0", + "env-paths": "^2.2.1", "immer": "^10.1.1", "isomorphic-fetch": "^3.0.0", "js-tiktoken": "^1.0.14", diff --git a/lib/shared/src/codyPaths.ts b/lib/shared/src/codyPaths.ts index 1d776540bf41..e2686b0bb5a7 100644 --- a/lib/shared/src/codyPaths.ts +++ b/lib/shared/src/codyPaths.ts @@ -1,6 +1,3 @@ -// Because env-paths is distributed as an ESM module but we package everything as CommonJS, -// we have to wrap this into a dynamic import for the playwright tests -export const codyPaths = async () => { - const envPaths = await import('env-paths') - return envPaths.default('Cody') -} +import envPaths from 'env-paths' + +export const codyPaths = () => envPaths('Cody') diff --git a/lib/shared/tsconfig.json b/lib/shared/tsconfig.json index 78a2666e733b..38557f8fae93 100644 --- a/lib/shared/tsconfig.json +++ b/lib/shared/tsconfig.json @@ -8,5 +8,5 @@ "moduleResolution": "Bundler" }, "include": ["src"], - "exclude": ["dist", "typehacks"] + "exclude": ["dist", "typehacks", "*.mjs"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f74a70d7b6c5..13af5bf85c68 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -357,8 +357,8 @@ importers: specifier: ^5.2.0 version: 5.2.0 env-paths: - specifier: ^3.0.0 - version: 3.0.0 + specifier: ^2.2.1 + version: 2.2.1 immer: specifier: ^10.1.1 version: 10.1.1 @@ -9065,6 +9065,7 @@ packages: /env-paths@3.0.0: resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true /envinfo@7.13.0: resolution: {integrity: sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q==} diff --git a/vscode/package.json b/vscode/package.json index 7fce1a82942a..90d6913f1580 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -32,7 +32,7 @@ "watch:build:dev:desktop": "concurrently \"pnpm run -s _build:esbuild:desktop --watch\" \"pnpm run -s _build:webviews --mode development --watch\"", "_build:esbuild:desktop": "pnpm download-wasm && pnpm run -s _build:esbuild:uninstall && pnpm run -s _build:esbuild:node", "_build:esbuild:node": "esbuild ./src/extension.node.ts --bundle --outfile=dist/extension.node.js --external:vscode --external:typescript --alias:@sourcegraph/cody-shared=@sourcegraph/cody-shared/src/index --alias:@sourcegraph/cody-shared/src=@sourcegraph/cody-shared/src --alias:lexical=./build/lexical-package-fix --format=cjs --platform=node --sourcemap", - "_build:esbuild:web": "esbuild ./src/extension.web.ts --platform=browser --bundle --outfile=dist/extension.web.js --alias:@sourcegraph/cody-shared=@sourcegraph/cody-shared/src/index --alias:@sourcegraph/cody-shared/src=@sourcegraph/cody-shared/src --alias:path=path-browserify --external:typescript --alias:node:path=path-browserify --alias:node:os=os-browserify --external:vscode --external:node:child_process --external:node:util --external:node:fs --external:node:fs/promises --external:node:process --define:process='{\"env\":{}}' --define:window=self --format=cjs --sourcemap", + "_build:esbuild:web": "esbuild ./src/extension.web.ts --platform=browser --bundle --outfile=dist/extension.web.js --alias:@sourcegraph/cody-shared=@sourcegraph/cody-shared/src/index --alias:@sourcegraph/cody-shared/src=@sourcegraph/cody-shared/src --alias:path=path-browserify --external:typescript --alias:node:path=path-browserify --alias:node:os=os-browserify --alias:os=os-browserify --external:vscode --external:node:child_process --external:node:util --external:node:fs --external:node:fs/promises --external:node:process --define:process='{\"env\":{}}' --define:window=self --format=cjs --sourcemap", "_build:esbuild:uninstall": "node ./uninstall/esbuild.mjs", "_build:webviews": "vite -c webviews/vite.config.mts build", "_build:vsix_for_test": "vsce package --no-dependencies --out dist/cody.e2e.vsix", diff --git a/vscode/uninstall/reinstall.ts b/vscode/uninstall/reinstall.ts index 03a0d7f5f4e8..c69c3d2425e6 100644 --- a/vscode/uninstall/reinstall.ts +++ b/vscode/uninstall/reinstall.ts @@ -2,13 +2,10 @@ import fs from 'node:fs/promises' import path from 'node:path' import { codyPaths } from '@sourcegraph/cody-shared' -export async function getUninstallMarker() { - const paths = await codyPaths() - return path.join(paths.config, 'uninstall-marker') -} +export const uninstallMarker = path.join(codyPaths().config, 'uninstall-marker') export const createUninstallMarker = async (): Promise => { - await fs.writeFile(await getUninstallMarker(), '') + await fs.writeFile(uninstallMarker, '') } let isReinstall: boolean | undefined = undefined @@ -21,7 +18,6 @@ export const isReinstalling = async (): Promise => { return isReinstall } try { - const uninstallMarker = await getUninstallMarker() await fs.stat(uninstallMarker) await fs.unlink(uninstallMarker) isReinstall = true diff --git a/vscode/uninstall/serializeConfig.ts b/vscode/uninstall/serializeConfig.ts index 02afc428e014..4fa26509ebd5 100644 --- a/vscode/uninstall/serializeConfig.ts +++ b/vscode/uninstall/serializeConfig.ts @@ -9,7 +9,7 @@ import { const CONFIG_FILE = 'config.json' -const getConfigPath = async () => path.join((await codyPaths()).config, CONFIG_FILE) +const getConfigPath = () => path.join(codyPaths().config, CONFIG_FILE) async function exists(path: string): Promise { try { @@ -28,7 +28,7 @@ async function ensureDirectoryExists(directory: string) { // Used to cleanup the uninstaller directory after the last telemetry event is sent export async function deleteUninstallerConfig() { - return fs.rm(await getConfigPath()) + return fs.rm(getConfigPath()) } async function writeSnapshot(directory: string, filename: string, content: any): Promise { @@ -49,13 +49,13 @@ interface UninstallerConfig { * of an uninstall event to log one last telemetry event. */ export async function serializeConfigSnapshot(uninstall: UninstallerConfig) { - const directory = (await codyPaths()).config + const directory = codyPaths().config await ensureDirectoryExists(directory) await writeSnapshot(directory, CONFIG_FILE, uninstall) } export async function readConfig(): Promise { - const file = await getConfigPath() + const file = getConfigPath() if (!(await exists(file))) { return null From d5f441eaa6e0ee5ae73af88f1a4b372b0573928b Mon Sep 17 00:00:00 2001 From: jamesmcnamara Date: Mon, 7 Oct 2024 12:07:45 -0700 Subject: [PATCH 09/16] fixed unit test --- vscode/src/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vscode/src/main.ts b/vscode/src/main.ts index 350ed4e928d9..0577e4dcdca2 100644 --- a/vscode/src/main.ts +++ b/vscode/src/main.ts @@ -143,7 +143,7 @@ export async function start( event => event.affectsConfiguration('cody') || event.affectsConfiguration('openctx') ), startWith(undefined), - map(getConfiguration), + map(() => getConfiguration()), distinctUntilChanged() ), fromVSCodeEvent(secretStorage.onDidChange.bind(secretStorage)).pipe( From d6d4dfae9fe112fa621da3bc1532474b65a19429 Mon Sep 17 00:00:00 2001 From: jamesmcnamara Date: Mon, 7 Oct 2024 17:34:41 -0700 Subject: [PATCH 10/16] moved serialization to auth status update --- .../agent/protocol_generated/AuthStatus.kt | 42 +++++++++++++++++-- .../protocol_generated/CodyAgentServer.kt | 4 +- .../agent/protocol_generated/Constants.kt | 4 +- .../agent/protocol_generated/ContextItem.kt | 11 ++--- .../protocol_generated/ContextItemSource.kt | 2 +- .../ExtensionConfiguration.kt | 2 +- .../cody/agent/protocol_generated/Model.kt | 2 +- .../cody/agent/protocol_generated/ModelTag.kt | 2 +- .../ProtocolTypeAdapters.kt | 1 + .../cody/agent/protocol_generated/Range.kt | 4 +- .../protocol_generated/SelectedReposParams.kt | 8 ---- agent/src/agent.ts | 5 +++ agent/src/global-state/AgentGlobalState.ts | 10 +++-- vscode/src/extension.node.ts | 33 +-------------- vscode/src/jsonrpc/agent-protocol.ts | 4 ++ vscode/src/services/AuthProvider.ts | 38 +++++++++++++++-- vscode/uninstall/serializeConfig.ts | 6 ++- 17 files changed, 113 insertions(+), 65 deletions(-) delete mode 100644 agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/SelectedReposParams.kt diff --git a/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/AuthStatus.kt b/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/AuthStatus.kt index 5bc23862be00..7fd1426e3827 100644 --- a/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/AuthStatus.kt +++ b/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/AuthStatus.kt @@ -1,11 +1,41 @@ @file:Suppress("FunctionName", "ClassName", "unused", "EnumEntryName", "UnusedImport") package com.sourcegraph.cody.agent.protocol_generated; -data class AuthStatus( - val endpoint: String, +import com.google.gson.annotations.SerializedName; +import com.google.gson.Gson; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import java.lang.reflect.Type; + +sealed class AuthStatus { + companion object { + val deserializer: JsonDeserializer = + JsonDeserializer { element: JsonElement, _: Type, context: JsonDeserializationContext -> + when (element.getAsJsonObject().get("endpoint").getAsString()) { + "https://example.com" -> context.deserialize(element, UnauthenticatedAuthStatus::class.java) + else -> throw Exception("Unknown discriminator ${element}") + } + } + } +} + +data class UnauthenticatedAuthStatus( + val endpoint: EndpointEnum, // Oneof: https://example.com val authenticated: Boolean, val showNetworkError: Boolean? = null, val showInvalidAccessTokenError: Boolean? = null, + val pendingValidation: Boolean, +) : AuthStatus() { + + enum class EndpointEnum { + @SerializedName("https://example.com") `Https-example-com`, + } +} + +data class AuthenticatedAuthStatus( + val endpoint: EndpointEnum, // Oneof: https://example.com + val authenticated: Boolean, val username: String, val isFireworksTracingEnabled: Boolean? = null, val hasVerifiedEmail: Boolean? = null, @@ -17,5 +47,11 @@ data class AuthStatus( val displayName: String? = null, val avatarURL: String? = null, val userCanUpgrade: Boolean? = null, -) + val pendingValidation: Boolean, +) : AuthStatus() { + + enum class EndpointEnum { + @SerializedName("https://example.com") `Https-example-com`, + } +} diff --git a/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/CodyAgentServer.kt b/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/CodyAgentServer.kt index cbd33385b847..1dd949be13a6 100644 --- a/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/CodyAgentServer.kt +++ b/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/CodyAgentServer.kt @@ -34,8 +34,6 @@ interface CodyAgentServer { fun chat_setModel(params: Chat_SetModelParams): CompletableFuture @JsonRequest("commands/explain") fun commands_explain(params: Null?): CompletableFuture - @JsonRequest("commands/test") - fun commands_test(params: Null?): CompletableFuture @JsonRequest("commands/smell") fun commands_smell(params: Null?): CompletableFuture @JsonRequest("commands/custom") @@ -134,6 +132,8 @@ interface CodyAgentServer { fun ignore_test(params: Ignore_TestParams): CompletableFuture @JsonRequest("testing/ignore/overridePolicy") fun testing_ignore_overridePolicy(params: ContextFilters?): CompletableFuture + @JsonRequest("extension/reset") + fun extension_reset(params: Null?): CompletableFuture // ============= // Notifications diff --git a/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/Constants.kt b/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/Constants.kt index 82ab79c03755..105da2e2c31e 100644 --- a/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/Constants.kt +++ b/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/Constants.kt @@ -32,7 +32,6 @@ object Constants { const val edit = "edit" const val `edit-file` = "edit-file" const val editor = "editor" - const val embeddings = "embeddings" const val enabled = "enabled" const val enterprise = "enterprise" const val error = "error" @@ -42,6 +41,7 @@ object Constants { const val function = "function" const val gateway = "gateway" const val history = "history" + const val `https-example-com` = "https://example.com" const val human = "human" const val ignore = "ignore" const val indentation = "indentation" @@ -49,6 +49,7 @@ object Constants { const val information = "information" const val initial = "initial" const val insert = "insert" + const val internal = "internal" const val isChatErrorGuard = "isChatErrorGuard" const val local = "local" const val method = "method" @@ -86,6 +87,7 @@ object Constants { const val unified = "unified" const val use = "use" const val user = "user" + const val vision = "vision" const val waitlist = "waitlist" const val warning = "warning" const val workspace = "workspace" diff --git a/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/ContextItem.kt b/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/ContextItem.kt index 1c458380aa16..9cb03cc6e4bc 100644 --- a/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/ContextItem.kt +++ b/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/ContextItem.kt @@ -32,7 +32,7 @@ data class ContextItemFile( val revision: String? = null, val title: String? = null, val description: String? = null, - val source: ContextItemSource? = null, // Oneof: embeddings, user, editor, search, initial, unified, selection, terminal, history + val source: ContextItemSource? = null, // Oneof: user, editor, search, initial, unified, selection, terminal, history val size: Long? = null, val isIgnored: Boolean? = null, val isTooLarge: Boolean? = null, @@ -42,6 +42,7 @@ data class ContextItemFile( val metadata: List? = null, val type: TypeEnum, // Oneof: file val remoteRepositoryName: String? = null, + val ranges: List? = null, ) : ContextItem() { enum class TypeEnum { @@ -57,7 +58,7 @@ data class ContextItemRepository( val revision: String? = null, val title: String? = null, val description: String? = null, - val source: ContextItemSource? = null, // Oneof: embeddings, user, editor, search, initial, unified, selection, terminal, history + val source: ContextItemSource? = null, // Oneof: user, editor, search, initial, unified, selection, terminal, history val size: Long? = null, val isIgnored: Boolean? = null, val isTooLarge: Boolean? = null, @@ -82,7 +83,7 @@ data class ContextItemTree( val revision: String? = null, val title: String? = null, val description: String? = null, - val source: ContextItemSource? = null, // Oneof: embeddings, user, editor, search, initial, unified, selection, terminal, history + val source: ContextItemSource? = null, // Oneof: user, editor, search, initial, unified, selection, terminal, history val size: Long? = null, val isIgnored: Boolean? = null, val isTooLarge: Boolean? = null, @@ -108,7 +109,7 @@ data class ContextItemSymbol( val revision: String? = null, val title: String? = null, val description: String? = null, - val source: ContextItemSource? = null, // Oneof: embeddings, user, editor, search, initial, unified, selection, terminal, history + val source: ContextItemSource? = null, // Oneof: user, editor, search, initial, unified, selection, terminal, history val size: Long? = null, val isIgnored: Boolean? = null, val isTooLarge: Boolean? = null, @@ -135,7 +136,7 @@ data class ContextItemOpenCtx( val revision: String? = null, val title: String? = null, val description: String? = null, - val source: ContextItemSource? = null, // Oneof: embeddings, user, editor, search, initial, unified, selection, terminal, history + val source: ContextItemSource? = null, // Oneof: user, editor, search, initial, unified, selection, terminal, history val size: Long? = null, val isIgnored: Boolean? = null, val isTooLarge: Boolean? = null, diff --git a/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/ContextItemSource.kt b/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/ContextItemSource.kt index 8afa081e7d4e..345158e55134 100644 --- a/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/ContextItemSource.kt +++ b/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/ContextItemSource.kt @@ -1,5 +1,5 @@ @file:Suppress("FunctionName", "ClassName", "unused", "EnumEntryName", "UnusedImport") package com.sourcegraph.cody.agent.protocol_generated; -typealias ContextItemSource = String // One of: embeddings, user, editor, search, initial, unified, selection, terminal, history +typealias ContextItemSource = String // One of: user, editor, search, initial, unified, selection, terminal, history diff --git a/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/ExtensionConfiguration.kt b/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/ExtensionConfiguration.kt index 9d171598d028..30796747763e 100644 --- a/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/ExtensionConfiguration.kt +++ b/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/ExtensionConfiguration.kt @@ -4,7 +4,7 @@ package com.sourcegraph.cody.agent.protocol_generated; data class ExtensionConfiguration( val serverEndpoint: String, val proxy: String? = null, - val accessToken: String, + val accessToken: String? = null, val customHeaders: Map, val anonymousUserID: String? = null, val autocompleteAdvancedProvider: String? = null, diff --git a/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/Model.kt b/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/Model.kt index 5b9b873c9878..f5ea6115fc70 100644 --- a/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/Model.kt +++ b/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/Model.kt @@ -8,7 +8,7 @@ data class Model( val clientSideConfig: ClientSideConfig? = null, val provider: String, val title: String, - val tags: List, + val tags: List? = null, val modelRef: ModelRef? = null, ) diff --git a/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/ModelTag.kt b/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/ModelTag.kt index 03faff0cc422..94b2350c88c6 100644 --- a/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/ModelTag.kt +++ b/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/ModelTag.kt @@ -1,5 +1,5 @@ @file:Suppress("FunctionName", "ClassName", "unused", "EnumEntryName", "UnusedImport") package com.sourcegraph.cody.agent.protocol_generated; -typealias ModelTag = String // One of: power, speed, balanced, recommended, deprecated, experimental, waitlist, on-waitlist, early-access, pro, free, enterprise, gateway, byok, local, ollama, dev, stream-disabled +typealias ModelTag = String // One of: power, speed, balanced, recommended, deprecated, experimental, waitlist, on-waitlist, early-access, internal, pro, free, enterprise, gateway, byok, local, ollama, dev, stream-disabled, vision diff --git a/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/ProtocolTypeAdapters.kt b/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/ProtocolTypeAdapters.kt index c6a98d220822..8dd4606f0666 100644 --- a/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/ProtocolTypeAdapters.kt +++ b/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/ProtocolTypeAdapters.kt @@ -3,6 +3,7 @@ package com.sourcegraph.cody.agent.protocol_generated; object ProtocolTypeAdapters { fun register(gson: com.google.gson.GsonBuilder) { + gson.registerTypeAdapter(AuthStatus::class.java, AuthStatus.deserializer) gson.registerTypeAdapter(ContextItem::class.java, ContextItem.deserializer) gson.registerTypeAdapter(CustomCommandResult::class.java, CustomCommandResult.deserializer) gson.registerTypeAdapter(TextEdit::class.java, TextEdit.deserializer) diff --git a/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/Range.kt b/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/Range.kt index fa8099c8617b..b475565f3168 100644 --- a/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/Range.kt +++ b/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/Range.kt @@ -2,7 +2,7 @@ package com.sourcegraph.cody.agent.protocol_generated; data class Range( - val start: Position, - val end: Position, + val start: Location, + val end: Location, ) diff --git a/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/SelectedReposParams.kt b/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/SelectedReposParams.kt deleted file mode 100644 index 24a966a1f103..000000000000 --- a/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/SelectedReposParams.kt +++ /dev/null @@ -1,8 +0,0 @@ -@file:Suppress("FunctionName", "ClassName", "unused", "EnumEntryName", "UnusedImport") -package com.sourcegraph.cody.agent.protocol_generated; - -data class SelectedReposParams( - val id: String, - val name: String, -) - diff --git a/agent/src/agent.ts b/agent/src/agent.ts index d471f5c23100..b0d35eb7fc93 100644 --- a/agent/src/agent.ts +++ b/agent/src/agent.ts @@ -966,6 +966,11 @@ export class Agent extends MessageHandler implements ExtensionClient { } }) + this.registerAuthenticatedRequest('extension/reset', async () => { + this.globalState?.reset() + return null + }) + this.registerNotification('autocomplete/completionAccepted', async ({ completionID }) => { const provider = await vscode_shim.completionProvider() await provider.handleDidAcceptCompletionItem(completionID as CompletionItemID) diff --git a/agent/src/global-state/AgentGlobalState.ts b/agent/src/global-state/AgentGlobalState.ts index 5bda88f2f4ca..ab471eb335a8 100644 --- a/agent/src/global-state/AgentGlobalState.ts +++ b/agent/src/global-state/AgentGlobalState.ts @@ -44,10 +44,8 @@ export class AgentGlobalState implements vscode.Memento { this.db.set(key, value) } - public reset(): void { - if (this.db instanceof InMemoryDB) { - this.db.clear() - } + public reset() { + this.db.clear() } public keys(): readonly string[] { @@ -88,6 +86,7 @@ interface DB { get(key: string): any set(key: string, value: any): void keys(): readonly string[] + clear(): void } class InMemoryDB implements DB { @@ -117,6 +116,9 @@ class LocalStorageDB implements DB { const quota = 1024 * 1024 * 256 // 256 MB this.storage = new LocalStorage(path.join(dir, `${ide}-globalState`), quota) } + clear() { + this.storage.clear() + } get(key: string): any { const item = this.storage.getItem(key) diff --git a/vscode/src/extension.node.ts b/vscode/src/extension.node.ts index d4a31743f9a7..80a863b002f4 100644 --- a/vscode/src/extension.node.ts +++ b/vscode/src/extension.node.ts @@ -1,16 +1,8 @@ // Sentry should be imported first import { NodeSentryService } from './services/sentry/sentry.node' -import { - type ClientCapabilities, - currentAuthStatus, - currentResolvedConfig, - clientCapabilities as getClientCapabilities, - resolvedConfig, - subscriptionDisposable, -} from '@sourcegraph/cody-shared' +import { resolvedConfig, subscriptionDisposable } from '@sourcegraph/cody-shared' import * as vscode from 'vscode' -import { serializeConfigSnapshot } from '../uninstall/serializeConfig' import { startTokenReceiver } from './auth/token-receiver' import { CommandsProvider } from './commands/services/provider' import { SourcegraphNodeCompletionsClient } from './completions/nodeClient' @@ -19,9 +11,7 @@ import { type ExtensionClient, defaultVSCodeExtensionClient } from './extension- import { activate as activateCommon } from './extension.common' import { initializeNetworkAgent, setCustomAgent } from './fetch.node' import { SymfRunner } from './local-context/symf' -import { localStorage } from './services/LocalStorageProvider' import { OpenTelemetryService } from './services/open-telemetry/OpenTelemetryService.node' -import { version } from './version' /** * Activation entrypoint for the VS Code extension when running VS Code as a desktop app @@ -62,24 +52,3 @@ export function activate( extensionClient, }) } - -// When Cody is deactivated, we serialize the current configuration to disk, -// so that it can be sent with Telemetry when the post-uninstall script runs. -// The vscode API is not available in the post-uninstall script. -export async function deactivate(): Promise { - const config = localStorage.getConfig() ?? (await currentResolvedConfig()) - const authStatus = currentAuthStatus() - let clientCapabilities: ClientCapabilities | undefined - try { - clientCapabilities = getClientCapabilities() - } catch { - // If client capabilities cannot be retrieved, we will just synthesize - // them from defaults in the post-uninstall script. - } - await serializeConfigSnapshot({ - config, - authStatus, - clientCapabilities, - version, - }) -} diff --git a/vscode/src/jsonrpc/agent-protocol.ts b/vscode/src/jsonrpc/agent-protocol.ts index 17e9cd103deb..8d1271895a23 100644 --- a/vscode/src/jsonrpc/agent-protocol.ts +++ b/vscode/src/jsonrpc/agent-protocol.ts @@ -285,6 +285,10 @@ export type ClientRequests = { // which match the specified regular expressions. Pass `undefined` to remove // the override. 'testing/ignore/overridePolicy': [ContextFilters | null, null] + + // Called after the extension has been uninstalled by a user action. + // Attempts to wipe out any state that the extension has stored. + 'extension/reset': [null, null] } // ================ diff --git a/vscode/src/services/AuthProvider.ts b/vscode/src/services/AuthProvider.ts index a750b2ac985a..8dbc16c5dd1e 100644 --- a/vscode/src/services/AuthProvider.ts +++ b/vscode/src/services/AuthProvider.ts @@ -3,16 +3,17 @@ import * as vscode from 'vscode' import { type AuthCredentials, type AuthStatus, + type ClientCapabilities, NEVER, type ResolvedConfiguration, type Unsubscribable, abortableOperation, authStatus, - clientCapabilities, combineLatest, currentResolvedConfig, disposableSubscription, distinctUntilChanged, + clientCapabilities as getClientCapabilities, normalizeServerEndpointURL, pluck, resolvedConfig as resolvedConfig_, @@ -24,9 +25,11 @@ import { } from '@sourcegraph/cody-shared' import isEqual from 'lodash/isEqual' import { Observable, Subject } from 'observable-fns' +import { serializeConfigSnapshot } from '../../uninstall/serializeConfig' import { type ResolvedConfigurationCredentialsOnly, validateCredentials } from '../auth/auth' import { logError } from '../log' import { maybeStartInteractiveTutorial } from '../tutorial/helpers' +import { version } from '../version' import { localStorage } from './LocalStorageProvider' const HAS_AUTHENTICATED_BEFORE_KEY = 'has-authenticated-before' @@ -69,7 +72,7 @@ class AuthProvider implements vscode.Disposable { ) .pipe( abortableOperation(async ([config], signal) => { - if (clientCapabilities().isCodyWeb) { + if (getClientCapabilities().isCodyWeb) { // Cody Web calls {@link AuthProvider.validateAndStoreCredentials} // explicitly. This early exit prevents duplicate authentications during // the initial load. @@ -169,7 +172,10 @@ class AuthProvider implements vscode.Disposable { const shouldStore = mode === 'always-store' || authStatus.authenticated if (shouldStore) { this.lastValidatedAndStoredCredentials.next(credentials) - await localStorage.saveEndpointAndToken(credentials.auth) + await Promise.all([ + localStorage.saveEndpointAndToken(credentials.auth), + this.serializeUninstallerInfo(authStatus), + ]) this.status.next(authStatus) signal?.throwIfAborted() } @@ -205,6 +211,32 @@ class AuthProvider implements vscode.Disposable { private setHasAuthenticatedBefore() { return localStorage.set(HAS_AUTHENTICATED_BEFORE_KEY, 'true') } + + // When the auth status is updated, we serialize the current configuration to disk, + // so that it can be sent with Telemetry when the post-uninstall script runs. + // we only write on auth change as that is the only significantly important factor + // and we don't want to write too frequently (so we don't react to config changes) + // The vscode API is not available in the post-uninstall script. + private async serializeUninstallerInfo(authStatus: AuthStatus): Promise { + let clientCapabilities: ClientCapabilities | undefined + try { + clientCapabilities = getClientCapabilities() + } catch { + // If client capabilities cannot be retrieved, we will just synthesize + // them from defaults in the post-uninstall script. + } + // TODO: put this behind a proper client capability if any other IDE's need to uninstall + // the same way as VSCode (most editors have a proper uninstall hook) + if (clientCapabilities?.isVSCode) { + const config = localStorage.getConfig() ?? (await currentResolvedConfig()) + await serializeConfigSnapshot({ + config, + authStatus, + clientCapabilities, + version, + }) + } + } } export const authProvider = new AuthProvider() diff --git a/vscode/uninstall/serializeConfig.ts b/vscode/uninstall/serializeConfig.ts index 4fa26509ebd5..e3869896b99e 100644 --- a/vscode/uninstall/serializeConfig.ts +++ b/vscode/uninstall/serializeConfig.ts @@ -31,7 +31,11 @@ export async function deleteUninstallerConfig() { return fs.rm(getConfigPath()) } -async function writeSnapshot(directory: string, filename: string, content: any): Promise { +async function writeSnapshot( + directory: string, + filename: string, + content: UninstallerConfig +): Promise { const filePath = path.join(directory, filename) return fs.writeFile(filePath, JSON.stringify(content, null, 2)) From d775eafe3a307c444cccd3e78d9e21a12b4806cf Mon Sep 17 00:00:00 2001 From: jamesmcnamara Date: Mon, 7 Oct 2024 17:35:06 -0700 Subject: [PATCH 11/16] added missing file --- .../sourcegraph/cody/agent/protocol_generated/Location.kt | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/Location.kt diff --git a/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/Location.kt b/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/Location.kt new file mode 100644 index 000000000000..d403f845c6cc --- /dev/null +++ b/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/Location.kt @@ -0,0 +1,8 @@ +@file:Suppress("FunctionName", "ClassName", "unused", "EnumEntryName", "UnusedImport") +package com.sourcegraph.cody.agent.protocol_generated; + +data class Location( + val line: Long, + val column: Long, +) + From 55eaa203e52ce6de732c4cec2975d016a8e78812 Mon Sep 17 00:00:00 2001 From: jamesmcnamara Date: Thu, 10 Oct 2024 17:15:29 -0700 Subject: [PATCH 12/16] regenerated --- .../cody/agent/protocol_generated/CompletionItemID.kt | 5 +++++ .../com/sourcegraph/cody/agent/protocol_generated/Range.kt | 4 ++-- agent/src/agent.ts | 2 +- lib/shared/src/auth/types.ts | 6 ++---- 4 files changed, 10 insertions(+), 7 deletions(-) create mode 100644 agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/CompletionItemID.kt diff --git a/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/CompletionItemID.kt b/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/CompletionItemID.kt new file mode 100644 index 000000000000..e94279a60dc8 --- /dev/null +++ b/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/CompletionItemID.kt @@ -0,0 +1,5 @@ +@file:Suppress("FunctionName", "ClassName", "unused", "EnumEntryName", "UnusedImport") +package com.sourcegraph.cody.agent.protocol_generated; + +typealias CompletionItemID = String // One of: + diff --git a/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/Range.kt b/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/Range.kt index b475565f3168..fa8099c8617b 100644 --- a/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/Range.kt +++ b/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/Range.kt @@ -2,7 +2,7 @@ package com.sourcegraph.cody.agent.protocol_generated; data class Range( - val start: Location, - val end: Location, + val start: Position, + val end: Position, ) diff --git a/agent/src/agent.ts b/agent/src/agent.ts index 048a79d8adc3..b0d8927e0872 100644 --- a/agent/src/agent.ts +++ b/agent/src/agent.ts @@ -969,7 +969,7 @@ export class Agent extends MessageHandler implements ExtensionClient { }) this.registerAuthenticatedRequest('extension/reset', async () => { - this.globalState?.reset() + await this.globalState?.reset() return null }) diff --git a/lib/shared/src/auth/types.ts b/lib/shared/src/auth/types.ts index b770f282021a..162881d6bae0 100644 --- a/lib/shared/src/auth/types.ts +++ b/lib/shared/src/auth/types.ts @@ -51,16 +51,14 @@ export interface UnauthenticatedAuthStatus { } export const AUTH_STATUS_FIXTURE_AUTHED: AuthenticatedAuthStatus = { - // this typecast is necessary to prevent codegen from becoming too specific - endpoint: 'https://example.com' as string, + endpoint: 'https://example.com', authenticated: true, username: 'alice', pendingValidation: false, } export const AUTH_STATUS_FIXTURE_UNAUTHED: AuthStatus & { authenticated: false } = { - // this typecast is necessary to prevent codegen from becoming too specific - endpoint: 'https://example.com' as string, + endpoint: 'https://example.com', authenticated: false, pendingValidation: false, } From 3e241a789eb741edadca6fe89c987795eae0c98e Mon Sep 17 00:00:00 2001 From: jamesmcnamara Date: Fri, 11 Oct 2024 14:09:01 -0700 Subject: [PATCH 13/16] fixed unit test --- vscode/src/services/AuthProvider.test.ts | 7 +++++++ vscode/src/services/AuthProvider.ts | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/vscode/src/services/AuthProvider.test.ts b/vscode/src/services/AuthProvider.test.ts index 7964d4341553..2ff7fd5a7ab0 100644 --- a/vscode/src/services/AuthProvider.test.ts +++ b/vscode/src/services/AuthProvider.test.ts @@ -149,6 +149,10 @@ describe('AuthProvider', () => { .mockReturnValue(asyncValue(authedAuthStatusAlice, 10)) const { authProvider, authStatus, resolvedConfig } = setup() + const mockSerializeUninstallerInfo = vi + .spyOn(authProvider, 'serializeUninstallerInfo') + .mockReturnValue(asyncValue(undefined, 10)) + const { values, clearValues } = readValuesFrom(authStatus) resolvedConfig.next({ configuration: {}, @@ -165,6 +169,7 @@ describe('AuthProvider', () => { clearValues() expect(validateCredentialsMock).toHaveBeenCalledTimes(1) expect(saveEndpointAndTokenMock).toHaveBeenCalledTimes(0) + expect(mockSerializeUninstallerInfo).toHaveBeenCalledTimes(0) // Call validateAndStoreCredentials. validateCredentialsMock.mockReturnValue(asyncValue(authedAuthStatusBob, 10)) @@ -180,12 +185,14 @@ describe('AuthProvider', () => { expect(values).toStrictEqual([]) expect(validateCredentialsMock).toHaveBeenCalledTimes(2) expect(saveEndpointAndTokenMock).toHaveBeenCalledTimes(0) + expect(mockSerializeUninstallerInfo).toHaveBeenCalledTimes(0) await vi.advanceTimersByTimeAsync(9) await promise expect(values).toStrictEqual([authedAuthStatusBob]) expect(validateCredentialsMock).toHaveBeenCalledTimes(2) expect(saveEndpointAndTokenMock).toHaveBeenCalledTimes(1) + expect(mockSerializeUninstallerInfo).toHaveBeenCalledTimes(1) }) test('refresh', async () => { diff --git a/vscode/src/services/AuthProvider.ts b/vscode/src/services/AuthProvider.ts index f6ea7a148679..5aa2b331af24 100644 --- a/vscode/src/services/AuthProvider.ts +++ b/vscode/src/services/AuthProvider.ts @@ -224,7 +224,8 @@ class AuthProvider implements vscode.Disposable { // we only write on auth change as that is the only significantly important factor // and we don't want to write too frequently (so we don't react to config changes) // The vscode API is not available in the post-uninstall script. - private async serializeUninstallerInfo(authStatus: AuthStatus): Promise { + // Public so that it can be mocked for testing + public async serializeUninstallerInfo(authStatus: AuthStatus): Promise { let clientCapabilities: ClientCapabilities | undefined try { clientCapabilities = getClientCapabilities() From 639c216f0fe403974a71d8dee79727518aa9bd46 Mon Sep 17 00:00:00 2001 From: jamesmcnamara Date: Tue, 15 Oct 2024 10:59:55 -0700 Subject: [PATCH 14/16] merge conflict --- vscode/src/services/AuthProvider.ts | 4 ++-- vscode/uninstall/serializeConfig.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/vscode/src/services/AuthProvider.ts b/vscode/src/services/AuthProvider.ts index 5aa2b331af24..b8f7b78db8bb 100644 --- a/vscode/src/services/AuthProvider.ts +++ b/vscode/src/services/AuthProvider.ts @@ -3,7 +3,7 @@ import * as vscode from 'vscode' import { type AuthCredentials, type AuthStatus, - type ClientCapabilities, + type ClientCapabilitiesWithLegacyFields, NEVER, type ResolvedConfiguration, type Unsubscribable, @@ -226,7 +226,7 @@ class AuthProvider implements vscode.Disposable { // The vscode API is not available in the post-uninstall script. // Public so that it can be mocked for testing public async serializeUninstallerInfo(authStatus: AuthStatus): Promise { - let clientCapabilities: ClientCapabilities | undefined + let clientCapabilities: ClientCapabilitiesWithLegacyFields | undefined try { clientCapabilities = getClientCapabilities() } catch { diff --git a/vscode/uninstall/serializeConfig.ts b/vscode/uninstall/serializeConfig.ts index e3869896b99e..03d4f8497529 100644 --- a/vscode/uninstall/serializeConfig.ts +++ b/vscode/uninstall/serializeConfig.ts @@ -2,7 +2,7 @@ import * as fs from 'node:fs/promises' import * as path from 'node:path' import { type AuthStatus, - type ClientCapabilities, + type ClientCapabilitiesWithLegacyFields, type ResolvedConfiguration, codyPaths, } from '@sourcegraph/cody-shared' @@ -44,7 +44,7 @@ async function writeSnapshot( interface UninstallerConfig { config?: ResolvedConfiguration authStatus: AuthStatus | undefined - clientCapabilities?: ClientCapabilities + clientCapabilities?: ClientCapabilitiesWithLegacyFields version?: string } From 9b1e9574e085dd9c66525b895e13df92fb7ad884 Mon Sep 17 00:00:00 2001 From: jamesmcnamara Date: Tue, 15 Oct 2024 11:29:01 -0700 Subject: [PATCH 15/16] ran biome --- agent/src/cli/command-bench/command-bench.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/agent/src/cli/command-bench/command-bench.ts b/agent/src/cli/command-bench/command-bench.ts index 4283747f307d..169530c8ed6e 100644 --- a/agent/src/cli/command-bench/command-bench.ts +++ b/agent/src/cli/command-bench/command-bench.ts @@ -10,12 +10,7 @@ import { newAgentClient } from '../../agent' import { exec } from 'node:child_process' import fs from 'node:fs' import { promisify } from 'node:util' -import { - codyPaths, - isDefined, - modelsService, - setClientCapabilities, -} from '@sourcegraph/cody-shared' +import { codyPaths, isDefined, modelsService, setClientCapabilities } from '@sourcegraph/cody-shared' import { sleep } from '../../../../vscode/src/completions/utils' import { getConfiguration, From b18c4a83e6c3d358c254940a937a1573448012ca Mon Sep 17 00:00:00 2001 From: James McNamara Date: Tue, 15 Oct 2024 17:19:15 -0700 Subject: [PATCH 16/16] Update vscode/src/services/AuthProvider.ts Co-authored-by: Beatrix <68532117+abeatrix@users.noreply.github.com> --- vscode/src/services/AuthProvider.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/vscode/src/services/AuthProvider.ts b/vscode/src/services/AuthProvider.ts index b8f7b78db8bb..766f988f6e65 100644 --- a/vscode/src/services/AuthProvider.ts +++ b/vscode/src/services/AuthProvider.ts @@ -226,6 +226,7 @@ class AuthProvider implements vscode.Disposable { // The vscode API is not available in the post-uninstall script. // Public so that it can be mocked for testing public async serializeUninstallerInfo(authStatus: AuthStatus): Promise { + if (!authStatus.authenticated) return let clientCapabilities: ClientCapabilitiesWithLegacyFields | undefined try { clientCapabilities = getClientCapabilities()