Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion utils/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
21 changes: 12 additions & 9 deletions utils/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 6 additions & 4 deletions utils/src/copilot/copilot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import type { CopilotClient, CopilotSession } from "@github/copilot-sdk";
import type * as vscode from "vscode";
import { InvalidCopilotResponseError } from "../errors";
import { ensureCopilotCliInstalled, getCopilotCliPath } from "./installCopilotCli";

let client: CopilotClient | undefined;
let session: CopilotSession | undefined;
Expand All @@ -14,10 +15,6 @@ async function loadCopilotSdk(): Promise<typeof import("@github/copilot-sdk")> {
return await import("@github/copilot-sdk");
}

function getCopilotCliPath(): string {
return require.resolve(`@github/copilot-${process.platform}-${process.arch}`);
}

export function createPrimaryPromptToGetSingleQuickPickInput(picks: string[], placeholder?: string): string {
return `
Task: choose one pick.
Expand Down Expand Up @@ -83,6 +80,11 @@ export async function getCopilotSession(relevantContext?: string): Promise<Copil
return session;
}

const installed = await ensureCopilotCliInstalled();
if (!installed) {
throw new InvalidCopilotResponseError();
}

const { CopilotClient } = await loadCopilotSdk();
client = new CopilotClient({ cliPath: getCopilotCliPath() });
session = await client.createSession({
Expand Down
59 changes: 58 additions & 1 deletion utils/src/copilot/installCopilotCli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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<boolean> {
try {
await spawnStreamAsync('copilot', composeArgs(withArg('--version'))(), {});
Expand Down
2 changes: 1 addition & 1 deletion utils/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.'));
}
}

Expand Down
Loading