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