|
12 | 12 | */ |
13 | 13 |
|
14 | 14 | import { spawn, type ChildProcess } from "node:child_process"; |
| 15 | +import { existsSync } from "node:fs"; |
15 | 16 | import { Socket } from "node:net"; |
| 17 | +import { dirname, join } from "node:path"; |
| 18 | +import { fileURLToPath } from "node:url"; |
16 | 19 | import { |
17 | 20 | createMessageConnection, |
18 | 21 | MessageConnection, |
@@ -100,6 +103,20 @@ function toJsonSchema(parameters: Tool["parameters"]): Record<string, unknown> | |
100 | 103 | * await client.stop(); |
101 | 104 | * ``` |
102 | 105 | */ |
| 106 | + |
| 107 | +/** |
| 108 | + * Gets the path to the bundled CLI from the @github/copilot package. |
| 109 | + * Uses index.js directly rather than npm-loader.js (which spawns the native binary). |
| 110 | + */ |
| 111 | +function getBundledCliPath(): string { |
| 112 | + // Find the actual location of the @github/copilot package by resolving its sdk export |
| 113 | + const sdkUrl = import.meta.resolve("@github/copilot/sdk"); |
| 114 | + const sdkPath = fileURLToPath(sdkUrl); |
| 115 | + // sdkPath is like .../node_modules/@github/copilot/sdk/index.js |
| 116 | + // Go up two levels to get the package root, then append index.js |
| 117 | + return join(dirname(dirname(sdkPath)), "index.js"); |
| 118 | +} |
| 119 | + |
103 | 120 | export class CopilotClient { |
104 | 121 | private cliProcess: ChildProcess | null = null; |
105 | 122 | private connection: MessageConnection | null = null; |
@@ -168,7 +185,7 @@ export class CopilotClient { |
168 | 185 | } |
169 | 186 |
|
170 | 187 | this.options = { |
171 | | - cliPath: options.cliPath || "copilot", |
| 188 | + cliPath: options.cliPath || getBundledCliPath(), |
172 | 189 | cliArgs: options.cliArgs ?? [], |
173 | 190 | cwd: options.cwd ?? process.cwd(), |
174 | 191 | port: options.port || 0, |
@@ -991,35 +1008,34 @@ export class CopilotClient { |
991 | 1008 | envWithoutNodeDebug.COPILOT_SDK_AUTH_TOKEN = this.options.githubToken; |
992 | 1009 | } |
993 | 1010 |
|
994 | | - // If cliPath is a .js file, spawn it with node |
995 | | - // Note that we can't rely on the shebang as Windows doesn't support it |
996 | | - const isJsFile = this.options.cliPath.endsWith(".js"); |
997 | | - const isAbsolutePath = |
998 | | - this.options.cliPath.startsWith("/") || /^[a-zA-Z]:/.test(this.options.cliPath); |
| 1011 | + // Verify CLI exists before attempting to spawn |
| 1012 | + if (!existsSync(this.options.cliPath)) { |
| 1013 | + throw new Error( |
| 1014 | + `Copilot CLI not found at ${this.options.cliPath}. Ensure @github/copilot is installed.` |
| 1015 | + ); |
| 1016 | + } |
999 | 1017 |
|
1000 | | - let command: string; |
1001 | | - let spawnArgs: string[]; |
| 1018 | + const stdioConfig: ["pipe", "pipe", "pipe"] | ["ignore", "pipe", "pipe"] = this.options |
| 1019 | + .useStdio |
| 1020 | + ? ["pipe", "pipe", "pipe"] |
| 1021 | + : ["ignore", "pipe", "pipe"]; |
1002 | 1022 |
|
| 1023 | + // For .js files, spawn node explicitly; for executables, spawn directly |
| 1024 | + const isJsFile = this.options.cliPath.endsWith(".js"); |
1003 | 1025 | if (isJsFile) { |
1004 | | - command = "node"; |
1005 | | - spawnArgs = [this.options.cliPath, ...args]; |
1006 | | - } else if (process.platform === "win32" && !isAbsolutePath) { |
1007 | | - // On Windows, spawn doesn't search PATHEXT, so use cmd /c to resolve the executable. |
1008 | | - command = "cmd"; |
1009 | | - spawnArgs = ["/c", `${this.options.cliPath}`, ...args]; |
| 1026 | + this.cliProcess = spawn(process.execPath, [this.options.cliPath, ...args], { |
| 1027 | + stdio: stdioConfig, |
| 1028 | + cwd: this.options.cwd, |
| 1029 | + env: envWithoutNodeDebug, |
| 1030 | + }); |
1010 | 1031 | } else { |
1011 | | - command = this.options.cliPath; |
1012 | | - spawnArgs = args; |
| 1032 | + this.cliProcess = spawn(this.options.cliPath, args, { |
| 1033 | + stdio: stdioConfig, |
| 1034 | + cwd: this.options.cwd, |
| 1035 | + env: envWithoutNodeDebug, |
| 1036 | + }); |
1013 | 1037 | } |
1014 | 1038 |
|
1015 | | - this.cliProcess = spawn(command, spawnArgs, { |
1016 | | - stdio: this.options.useStdio |
1017 | | - ? ["pipe", "pipe", "pipe"] |
1018 | | - : ["ignore", "pipe", "pipe"], |
1019 | | - cwd: this.options.cwd, |
1020 | | - env: envWithoutNodeDebug, |
1021 | | - }); |
1022 | | - |
1023 | 1039 | let stdout = ""; |
1024 | 1040 | let resolved = false; |
1025 | 1041 |
|
|
0 commit comments