Path: blob/main/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIShim.ts
13399 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { spawnSync } from 'child_process';6import * as readline from 'readline';7import * as path from '../../../util/vs/base/common/path';89// ⚠️⚠️⚠️10// This file is built into a standalone bundle, executed from the terminal.11// Avoid including unnecessary dependencies!12//13// This is used on macOS and Linux. On Windows, you'll need to make changes14// in copilotCLITerminalIntegration.ps1 instead. This is because Electron on Windows15// is not built with support for console stdin.16// ⚠️⚠️⚠️1718/*19* Universal GitHub Copilot CLI bootstrapper20*21* Works from any interactive shell (bash, zsh, sh, PowerShell Core (pwsh), Nushell, csh/tcsh) via shebang.22* Responsibilities:23* 1. Locate the real Copilot CLI binary (avoid recursion if this file shadows it).24* 2. Offer to install if missing (npm -g @github/copilot).25* 3. Enforce minimum version (>= REQUIRED_VERSION) with interactive update.26* 4. Execute the real binary with original arguments and exit with its status.27*28* NOTE: This file intentionally keeps logic self‑contained (no external deps) so it can be dropped into PATH directly.29*/3031const REQUIRED_VERSION = '0.0.394';32const PACKAGE_NAME = '@github/copilot';33const env = { ...process.env, PATH: (process.env.PATH || '').replaceAll(`${__dirname}${path.delimiter}`, '').replaceAll(`${path.delimiter}${__dirname}`, '') };3435const rl = readline.createInterface({36input: process.stdin,37output: process.stdout,38});3940function log(msg: string) { process.stdout.write(msg + '\n'); }4142function warn(msg: string) { process.stderr.write(msg + '\n'); }4344function promptYes(question: string): Promise<boolean> {45return new Promise((resolve) => {46rl.question(`${question} ['y/N'] `, (answer) => {47resolve(answer.toLowerCase()[0] === 'y');48});49});50}5152function semverParts(v: string) {53const cleaned = v.replace(/^v/, '').split('.');54return [0, 1, 2].map(i => parseInt((cleaned[i] || '0').replace(/[^0-9].*$/, ''), 10) || 0);55}5657function versionGte(versionA: string, versionB: string) {58const aa = semverParts(versionA), bb = semverParts(versionB);59for (let i = 0; i < 3; i++) {60if (aa[i] > bb[i]) { return true; }61if (aa[i] < bb[i]) { return false; }62}63return true;64}6566/**67* Returns the version of Copilot CLI installed.68* If not installed, then returns `undefined`, else returns an object with the version.69* Version can be undefined if it cannot be determined.70*/71function getCopilotInfo(): { installed: true; version?: string } | undefined {72const result = spawnSync('copilot --version', { env, shell: true, encoding: 'utf8' });73if (result.error || result.status !== 0) {74return undefined;75}76const m = result.stdout.match(/[0-9]+\.[0-9]+\.[0-9]+/);77return m ? { version: m[0], installed: true } : { installed: true };7879}8081function runNpm(args: string[], label: string) {82const result = spawnSync('npm', args, { stdio: 'inherit', env });83if (result.error) {84warn(`${label} failed: ${result.error.message}`);85return false;86}87if (result.status !== 0) {88warn(`${label} failed with exit code ${result.status}`);89return false;90}91return true;92}9394function runBrew(label: string) {95const result = spawnSync('brew', ['install', 'copilot-cli'], { stdio: 'inherit', env });96if (result.error) {97warn(`${label} via brew failed: ${result.error.message}`);98return false;99}100if (result.status !== 0) {101warn(`${label} via brew failed with exit code ${result.status}`);102return false;103}104return true;105}106107function runCurl(label: string) {108const result = spawnSync('bash', ['-c', 'curl -fsSL https://gh.io/copilot-install | bash'], { stdio: 'inherit', env });109if (result.error) {110warn(`${label} via curl failed: ${result.error.message}`);111return false;112}113if (result.status !== 0) {114warn(`${label} via curl failed with exit code ${result.status}`);115return false;116}117return true;118}119120function runWget(label: string) {121const result = spawnSync('bash', ['-c', 'wget -qO- https://gh.io/copilot-install | bash'], { stdio: 'inherit', env });122if (result.error) {123warn(`${label} via wget failed: ${result.error.message}`);124return false;125}126if (result.status !== 0) {127warn(`${label} via wget failed with exit code ${result.status}`);128return false;129}130return true;131}132133function hasCommand(cmd: string) {134const result = spawnSync('sh', ['-c', `command -v ${cmd}`], { env, encoding: 'utf8' });135return !result.error && result.status === 0;136}137138function installCopilotCLI(label: string, update = false): boolean {139// Try npm first140if (hasCommand('npm') && runNpm([update ? 'update' : 'install', '-g', PACKAGE_NAME], label)) {141return true;142}143// Try brew144if (hasCommand('brew')) {145log(`npm is not available or ${update ? 'update' : 'installation'} failed. Trying brew...`);146if (runBrew(label)) { return true; }147}148// Try curl149if (hasCommand('curl')) {150log('Trying install script via curl...');151if (runCurl(label)) { return true; }152}153// Try wget154if (hasCommand('wget')) {155log('Trying install script via wget...');156if (runWget(label)) { return true; }157}158return false;159}160161async function ensureInstalled() {162const version = getCopilotInfo();163if (!version) {164warn('Cannot find GitHub Copilot CLI (https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli)');165if (await promptYes('Install GitHub Copilot CLI?')) {166if (installCopilotCLI('Installing')) {167return ensureInstalled();168}169await pressKeyToExit();170} else {171process.exit(0);172}173}174return version;175}176177async function validateVersion(version: string) {178if (!versionGte(version, REQUIRED_VERSION)) {179warn(`GitHub Copilot CLI version ${version} is not compatible.`);180log(`Version ${REQUIRED_VERSION} or later is required.`);181if (await promptYes('Update GitHub Copilot CLI?')) {182if (installCopilotCLI('Update', true)) {183return true;184}185await pressKeyToExit();186} else {187process.exit(0);188}189}190}191192async function pressKeyToExit(message: string = 'Press Enter to exit...'): Promise<void> {193await new Promise<void>((resolve) => {194rl.question(`${message}`, () => {195resolve();196});197});198process.exit(0);199200}201202(async function main() {203const info = await ensureInstalled();204if (info?.version) {205await validateVersion(info.version);206}207if (!info) {208warn('Error: Could not locate Copilot CLI after update.');209await pressKeyToExit('Try manually reinstalling (https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli)');210}211const args = process.argv.slice(2);212213// In vscode we use `--clear` to indicate that the terminal should be cleared before running the command214// Used when launching terminal in editor view (for best possible UX, so it doesn't look like a terminal)215if (args[0] === '--clear') {216console.clear();217args.shift();218}219220spawnSync('copilot', args, { stdio: 'inherit', env });221process.exit(0);222})();223224225