From b67b474d4501c9b3d8a5462a08206a77e4939032 Mon Sep 17 00:00:00 2001 From: Megan Mott Date: Thu, 11 Jun 2026 09:52:44 -0700 Subject: [PATCH 1/2] initial --- utils/index.d.ts | 1 - utils/package-lock.json | 21 ++++++++++++--------- utils/package.json | 2 +- utils/src/copilot/copilot.ts | 36 ++++++++++++++++++++++++++++++++---- utils/src/errors.ts | 2 +- 5 files changed, 46 insertions(+), 16 deletions(-) diff --git a/utils/index.d.ts b/utils/index.d.ts index a96cc43b8c..408df43dc3 100644 --- a/utils/index.d.ts +++ b/utils/index.d.ts @@ -8,7 +8,6 @@ import type { Environment } from '@azure/ms-rest-azure-env'; import type { AzExtResourceType, AzureAuthentication, AzureResource, AzureSubscription, ResourceModelBase } from '@microsoft/vscode-azureresources-api'; import type * as duration from 'dayjs/plugin/duration'; -import type * as vscode from 'vscode'; import type * as vscodeTypes from 'vscode'; import { AuthenticationSession, AuthenticationWwwAuthenticateRequest, CancellationToken, CancellationTokenSource, Command, Disposable, Event, ExtensionContext, FileChangeEvent, FileChangeType, FileStat, FileSystemProvider, FileType, InputBoxOptions, LanguageModelToolInvocationOptions, LanguageModelToolInvocationPrepareOptions, LanguageModelToolResult, LogLevel, LogOutputChannel, MarkdownString, MessageItem, MessageOptions, OpenDialogOptions, OutputChannel, PreparedToolInvocation, Progress, ProviderResult, QuickPickItem, TelemetryTrustedValue, TextDocumentShowOptions, ThemeIcon, TreeDataProvider, TreeItem, TreeItemCollapsibleState, TreeView, Uri, QuickPickOptions as VSCodeQuickPickOptions, WorkspaceFolder, WorkspaceFolderPickOptions } from 'vscode'; import { TargetPopulation } from 'vscode-tas-client'; diff --git a/utils/package-lock.json b/utils/package-lock.json index b4b2d9f837..4b82d6315e 100644 --- a/utils/package-lock.json +++ b/utils/package-lock.json @@ -15,7 +15,7 @@ "dayjs": "^1.11.19", "html-to-text": "^9.0.5", "semver": "^7.7.4", - "vscode-tas-client": "^0.1.84" + "vscode-tas-client": "^0.2.1" }, "devDependencies": { "@github/copilot-sdk": "*", @@ -4278,10 +4278,13 @@ } }, "node_modules/tas-client": { - "version": "0.2.33", - "resolved": "https://registry.npmjs.org/tas-client/-/tas-client-0.2.33.tgz", - "integrity": "sha512-V+uqV66BOQnWxvI6HjDnE4VkInmYZUQ4dgB7gzaDyFyFSK1i1nF/j7DpS9UbQAgV9NaF1XpcyuavnM1qOeiEIg==", - "license": "MIT" + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/tas-client/-/tas-client-0.4.1.tgz", + "integrity": "sha512-cNwPitLm+VvWtUYyZu6YOfP1qwgwRzqTHC7StkhCcI4zHp3l/uRAG+29LV4SpRnagd+jAEKmJFOIV6KwZDiA6w==", + "license": "MIT", + "engines": { + "node": ">=22" + } }, "node_modules/test-exclude": { "version": "7.0.1", @@ -4808,12 +4811,12 @@ } }, "node_modules/vscode-tas-client": { - "version": "0.1.84", - "resolved": "https://registry.npmjs.org/vscode-tas-client/-/vscode-tas-client-0.1.84.tgz", - "integrity": "sha512-rUTrUopV+70hvx1hW5ebdw1nd6djxubkLvVxjGdyD/r5v/wcVF41LIfiAtbm5qLZDtQdsMH1IaCuDoluoIa88w==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/vscode-tas-client/-/vscode-tas-client-0.2.1.tgz", + "integrity": "sha512-htmjTFhOkM213OKv42cyCboc6D4vhZTdGHLb+GiNvFeK66eBdTbec2P41rgIg9YxnBgcDOlkvTElRorm3VitXw==", "license": "MIT", "dependencies": { - "tas-client": "0.2.33" + "tas-client": "^0.4.0" }, "engines": { "vscode": "^1.85.0" diff --git a/utils/package.json b/utils/package.json index 5890a7b958..72761e7f60 100644 --- a/utils/package.json +++ b/utils/package.json @@ -73,7 +73,7 @@ "dayjs": "^1.11.19", "html-to-text": "^9.0.5", "semver": "^7.7.4", - "vscode-tas-client": "^0.1.84" + "vscode-tas-client": "^0.2.1" }, "peerDependencies": { "@azure/ms-rest-azure-env": "^2.0.0", diff --git a/utils/src/copilot/copilot.ts b/utils/src/copilot/copilot.ts index 8091712338..5ea1d45c06 100644 --- a/utils/src/copilot/copilot.ts +++ b/utils/src/copilot/copilot.ts @@ -3,9 +3,11 @@ * Licensed under the MIT License. See License.md in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { execFileSync } from "child_process"; import type { CopilotClient, CopilotSession } from "@github/copilot-sdk"; import type * as vscode from "vscode"; import { InvalidCopilotResponseError } from "../errors"; +import { ensureCopilotCliInstalled } from "./installCopilotCli"; let client: CopilotClient | undefined; let session: CopilotSession | undefined; @@ -14,8 +16,29 @@ async function loadCopilotSdk(): Promise { return await import("@github/copilot-sdk"); } -function getCopilotCliPath(): string { - return require.resolve(`@github/copilot-${process.platform}-${process.arch}`); +/** @internal Exported for testing. */ +export function getCopilotCliPath(): string { + try { + return require.resolve(`@github/copilot-${process.platform}-${process.arch}`); + } catch { + // The platform-specific binary package is not present. Fall back to a globally + // installed `copilot` CLI on PATH. + // We must resolve the full path because CopilotClient uses existsSync() + // which does not search PATH for bare command names. + try { + const fullPath = execFileSync( + process.platform === 'win32' ? 'where' : 'which', + ['copilot'], + { encoding: 'utf-8' }, + ).trim().split('\n')[0]; + if (fullPath) { + return fullPath; + } + } catch { + // which/where failed, fall back to bare command name + } + return 'copilot'; + } } export function createPrimaryPromptToGetSingleQuickPickInput(picks: string[], placeholder?: string): string { @@ -71,7 +94,7 @@ export async function doGithubCopilotInteraction(primaryPrompt: string, relevant mode: "immediate" }); - if (!response || !response.data) { + if (!response?.data) { throw new InvalidCopilotResponseError(); } @@ -83,6 +106,11 @@ export async function getCopilotSession(relevantContext?: string): Promise { function extractSubscriptionIdFromContext(context: string): string | undefined { const regex = /https:\/\/management\.[^/]+\/subscriptions\/([0-9a-fA-F-]{36})/; - const match = context.match(regex); + const match = regex.exec(context); return match ? match[1] : undefined; } diff --git a/utils/src/errors.ts b/utils/src/errors.ts index d8b55499ce..d9bb236a7f 100644 --- a/utils/src/errors.ts +++ b/utils/src/errors.ts @@ -25,7 +25,7 @@ export function isUserCancelledError(error: unknown): error is UserCancelledErro export class InvalidCopilotResponseError extends Error { constructor() { - super(vscode.l10n.t('Invalid input.')); + super(vscode.l10n.t('Unable to get a valid response from the GitHub Copilot CLI.')); } } From ad3a491896dcc666721fc974bebefdbdfe71f277 Mon Sep 17 00:00:00 2001 From: Megan Mott Date: Wed, 17 Jun 2026 09:43:38 -0700 Subject: [PATCH 2/2] changes --- utils/src/copilot/copilot.ts | 28 +----------- utils/src/copilot/installCopilotCli.ts | 61 +++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 29 deletions(-) diff --git a/utils/src/copilot/copilot.ts b/utils/src/copilot/copilot.ts index 5ea1d45c06..c563131118 100644 --- a/utils/src/copilot/copilot.ts +++ b/utils/src/copilot/copilot.ts @@ -3,11 +3,10 @@ * Licensed under the MIT License. See License.md in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { execFileSync } from "child_process"; import type { CopilotClient, CopilotSession } from "@github/copilot-sdk"; import type * as vscode from "vscode"; import { InvalidCopilotResponseError } from "../errors"; -import { ensureCopilotCliInstalled } from "./installCopilotCli"; +import { ensureCopilotCliInstalled, getCopilotCliPath } from "./installCopilotCli"; let client: CopilotClient | undefined; let session: CopilotSession | undefined; @@ -16,31 +15,6 @@ async function loadCopilotSdk(): Promise { return await import("@github/copilot-sdk"); } -/** @internal Exported for testing. */ -export function getCopilotCliPath(): string { - try { - return require.resolve(`@github/copilot-${process.platform}-${process.arch}`); - } catch { - // The platform-specific binary package is not present. Fall back to a globally - // installed `copilot` CLI on PATH. - // We must resolve the full path because CopilotClient uses existsSync() - // which does not search PATH for bare command names. - try { - const fullPath = execFileSync( - process.platform === 'win32' ? 'where' : 'which', - ['copilot'], - { encoding: 'utf-8' }, - ).trim().split('\n')[0]; - if (fullPath) { - return fullPath; - } - } catch { - // which/where failed, fall back to bare command name - } - return 'copilot'; - } -} - export function createPrimaryPromptToGetSingleQuickPickInput(picks: string[], placeholder?: string): string { return ` Task: choose one pick. diff --git a/utils/src/copilot/installCopilotCli.ts b/utils/src/copilot/installCopilotCli.ts index e9d0167c2b..e5e23b150f 100644 --- a/utils/src/copilot/installCopilotCli.ts +++ b/utils/src/copilot/installCopilotCli.ts @@ -3,7 +3,9 @@ * Licensed under the MIT License. See License.md in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { type CommandLineArgs, composeArgs, spawnStreamAsync, withArg } from "@microsoft/vscode-processutils"; +import { type CommandLineArgs, composeArgs, getSafeExecPath, spawnStreamAsync, withArg } from "@microsoft/vscode-processutils"; +import { existsSync } from "fs"; +import * as path from "path"; import { Writable } from "stream"; import * as vscode from "vscode"; import { ext } from "../extensionVariables"; @@ -13,6 +15,61 @@ interface InstallCommand { args: CommandLineArgs; } +/** + * Resolves the full filesystem path to the platform-specific `@github/copilot` binary, + * falling back to a globally installed `copilot` CLI on PATH. + * + * We must resolve a real, absolute file path because `CopilotClient` validates `cliPath` + * with `existsSync()` and does not search PATH for bare command names. + * + * @internal Exported for testing. + */ +export function getCopilotCliPath(): string { + try { + return require.resolve(`@github/copilot-${process.platform}-${process.arch}`); + } catch { + // The platform-specific binary package is not present. Fall back to a globally installed `copilot` CLI on PATH. + return resolveCopilotCliFromPath() ?? 'copilot'; + } +} + +/** + * Searches PATH for the `copilot` executable and returns its absolute path, or `undefined` + * if it cannot be found. Uses {@link getSafeExecPath} to resolve against PATH and verifies the + * result with `existsSync()`, then falls back to a manual PATH scan (which also resolves a real + * file path on non-Windows platforms, where `getSafeExecPath` returns the bare command name). + */ +function resolveCopilotCliFromPath(): string | undefined { + try { + const execPath = getSafeExecPath('copilot'); + if (path.isAbsolute(execPath) && existsSync(execPath)) { + return execPath; + } + } catch { + // `copilot` was not found on PATH; fall through to a manual scan. + } + + return scanPathForCopilot(); +} + +function scanPathForCopilot(): string | undefined { + const pathDirs = (process.env.PATH || '').split(path.delimiter).filter(dir => dir.length > 0); + const exeNames = process.platform === 'win32' + ? (process.env.PATHEXT || '.EXE;.CMD;.BAT').split(';').map(pathExt => `copilot${pathExt.trim().toLowerCase()}`) + : ['copilot']; + + for (const dir of pathDirs) { + for (const exeName of exeNames) { + const candidate = path.join(dir, exeName); + if (existsSync(candidate)) { + return candidate; + } + } + } + + return undefined; +} + export async function isCopilotCliInstalled(): Promise { try { await spawnStreamAsync('copilot', composeArgs(withArg('--version'))(), {}); @@ -78,7 +135,7 @@ async function runInstallCommand(installCommand: InstallCommand): Promise ext.outputChannel.appendLog(vscode.l10n.t('Installing GitHub Copilot CLI via "{0}"...', installCommand.command)); const outputStream = new Writable({ - write(chunk, _encoding, callback) { + write(chunk: Buffer, _encoding, callback) { ext.outputChannel.appendLog(chunk.toString()); callback(); },