Skip to content

Commit 0d3f6f0

Browse files
Optionally download CLI at runtime in Node SDK
1 parent 02f0d72 commit 0d3f6f0

13 files changed

Lines changed: 799 additions & 14 deletions

‎nodejs/.gitignore‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,5 +133,8 @@ dist
133133
dist/
134134
build/
135135

136+
# Generated version constants (regenerated at build time)
137+
src/generated/
138+
136139
# macOS
137140
.DS_Store

‎nodejs/README.md‎

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,10 @@ new CopilotClient(options?: CopilotClientOptions)
5555

5656
**Options:**
5757

58-
- `cliPath?: string` - Path to CLI executable (default: "copilot" from PATH)
58+
- `cliPath?: string` - Path to CLI executable (default: "copilot" from PATH). Mutually exclusive with `acquisition`.
5959
- `cliArgs?: string[]` - Extra arguments prepended before SDK-managed flags (e.g. `["./dist-cli/index.js"]` when using `node`)
60-
- `cliUrl?: string` - URL of existing CLI server to connect to (e.g., `"localhost:8080"`, `"http://127.0.0.1:9000"`, or just `"8080"`). When provided, the client will not spawn a CLI process.
60+
- `cliUrl?: string` - URL of existing CLI server to connect to (e.g., `"localhost:8080"`, `"http://127.0.0.1:9000"`, or just `"8080"`). When provided, the client will not spawn a CLI process. Mutually exclusive with `acquisition`.
61+
- `acquisition?: AcquisitionOptions` - Auto-download CLI if not present. See [CLI Acquisition](#cli-acquisition). Mutually exclusive with `cliPath` and `cliUrl`.
6162
- `port?: number` - Server port (default: 0 for random)
6263
- `useStdio?: boolean` - Use stdio transport instead of TCP (default: true)
6364
- `logLevel?: string` - Log level (default: "info")
@@ -645,7 +646,63 @@ try {
645646
## Requirements
646647

647648
- Node.js >= 18.0.0
648-
- GitHub Copilot CLI installed and in PATH (or provide custom `cliPath`)
649+
- GitHub Copilot CLI installed and in PATH, or:
650+
- Provide a custom `cliPath`, or
651+
- Use `acquisition` to auto-download the CLI (see below)
652+
653+
## CLI Acquisition
654+
655+
The SDK can automatically download and manage the Copilot CLI for you. This is useful when you can't rely on the CLI being pre-installed.
656+
657+
```typescript
658+
const client = new CopilotClient({
659+
acquisition: {
660+
downloadDir: "~/.myapp/copilot-cli", // Where to store CLI versions
661+
},
662+
});
663+
664+
await client.start(); // Downloads CLI if needed, then starts
665+
```
666+
667+
### How it works
668+
669+
1. **Check existing downloads**: Looks for any CLI version in `downloadDir` that is both >= your `minVersion` (if specified) and protocol-compatible with this SDK.
670+
671+
2. **If a suitable version exists**: Uses the highest compatible version. No download needed.
672+
673+
3. **If no suitable version exists**: Downloads the CLI version this SDK was built for.
674+
675+
This means SDK upgrades don't force re-downloads — your existing CLI is reused as long as it still works.
676+
677+
### Options
678+
679+
```typescript
680+
interface AcquisitionOptions {
681+
// Required: Directory for CLI downloads. Should be app-specific.
682+
downloadDir: string;
683+
684+
// Optional: Minimum CLI version required (e.g., "0.0.405").
685+
// If your existing version is lower, a new version will be downloaded.
686+
minVersion?: string;
687+
688+
// Optional: Progress callback for download updates.
689+
onProgress?: (progress: { bytesDownloaded: number; totalBytes: number }) => void;
690+
}
691+
```
692+
693+
### Example with progress reporting
694+
695+
```typescript
696+
const client = new CopilotClient({
697+
acquisition: {
698+
downloadDir: path.join(os.homedir(), ".myapp", "copilot-cli"),
699+
onProgress: ({ bytesDownloaded, totalBytes }) => {
700+
const pct = totalBytes > 0 ? Math.round((bytesDownloaded / totalBytes) * 100) : 0;
701+
console.log(`Downloading CLI: ${pct}%`);
702+
},
703+
},
704+
});
705+
```
649706

650707
## License
651708

‎nodejs/examples/basic-example.ts‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
*--------------------------------------------------------------------------------------------*/
44

55
import { z } from "zod";
6+
import { join } from "node:path";
7+
import { homedir } from "node:os";
68
import { CopilotClient, defineTool } from "../src/index.js";
79

810
console.log("🚀 Starting Copilot SDK Example\n");
@@ -20,10 +22,22 @@ const lookupFactTool = defineTool("lookup_fact", {
2022
handler: ({ topic }) => facts[topic.toLowerCase()] ?? `No fact stored for ${topic}.`,
2123
});
2224

23-
// Create client - will auto-start CLI server (searches PATH for "copilot")
24-
const client = new CopilotClient({ logLevel: "info" });
25+
// Create client with automatic CLI acquisition
26+
const client = new CopilotClient({
27+
logLevel: "info",
28+
acquisition: {
29+
downloadDir: join(homedir(), ".copilot-sdk-example", "cli"),
30+
onProgress: ({ bytesDownloaded, totalBytes }) => {
31+
if (totalBytes > 0) {
32+
const pct = Math.round((bytesDownloaded / totalBytes) * 100);
33+
process.stdout.write(`\r⬇️ Downloading CLI: ${pct}%`);
34+
}
35+
},
36+
},
37+
});
38+
2539
const session = await client.createSession({ tools: [lookupFactTool] });
26-
console.log(`✅ Session created: ${session.sessionId}\n`);
40+
console.log(`\n✅ Session created: ${session.sessionId}\n`);
2741

2842
// Listen to events
2943
session.on((event) => {

‎nodejs/package-lock.json‎

Lines changed: 73 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎nodejs/package.json‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
},
1717
"type": "module",
1818
"scripts": {
19-
"clean": "rimraf --glob dist *.tgz",
19+
"clean": "rimraf --glob dist *.tgz src/generated",
20+
"prebuild": "tsx scripts/generate-versions.ts",
2021
"build": "tsx esbuild-copilotsdk-nodejs.ts",
2122
"test": "vitest run",
2223
"test:watch": "vitest",
@@ -41,11 +42,13 @@
4142
"license": "MIT",
4243
"dependencies": {
4344
"@github/copilot": "^0.0.402",
45+
"semver": "^7.7.3",
4446
"vscode-jsonrpc": "^8.2.1",
4547
"zod": "^4.3.5"
4648
},
4749
"devDependencies": {
4850
"@types/node": "^25.2.0",
51+
"@types/semver": "^7.7.1",
4952
"@typescript-eslint/eslint-plugin": "^8.54.0",
5053
"@typescript-eslint/parser": "^8.54.0",
5154
"esbuild": "^0.27.2",
@@ -56,7 +59,7 @@
5659
"prettier": "^3.4.0",
5760
"quicktype-core": "^23.2.6",
5861
"rimraf": "^6.1.2",
59-
"semver": "^7.7.3",
62+
"tar": "^7.5.7",
6063
"tsx": "^4.20.6",
6164
"typescript": "^5.0.0",
6265
"vitest": "^4.0.18"
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Generates src/generated/versions.ts from package-lock.json @github/copilot version.
2+
// Run automatically via prebuild hook.
3+
4+
import fs from "node:fs";
5+
import path from "node:path";
6+
import { fileURLToPath } from "node:url";
7+
8+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
9+
const packageLock = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "package-lock.json"), "utf-8"));
10+
const cliVersion = packageLock.packages["node_modules/@github/copilot"].version;
11+
12+
const code = `// Generated by scripts/generate-versions.ts - DO NOT EDIT
13+
export const PREFERRED_CLI_VERSION = "${cliVersion}";
14+
`;
15+
16+
fs.mkdirSync(path.join(__dirname, "..", "src", "generated"), { recursive: true });
17+
fs.writeFileSync(path.join(__dirname, "..", "src", "generated", "versions.ts"), code);
18+
console.log(`Generated src/generated/versions.ts with PREFERRED_CLI_VERSION="${cliVersion}"`);

0 commit comments

Comments
 (0)