@minniexcode/codex-switch 0.0.8 → 0.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.AI.md +5 -3
  2. package/README.CN.md +25 -3
  3. package/README.md +3 -2
  4. package/dist/app/add-provider.js +1 -12
  5. package/dist/app/bridge.js +295 -0
  6. package/dist/app/edit-provider.js +1 -17
  7. package/dist/app/get-status.js +32 -2
  8. package/dist/app/list-providers.js +0 -1
  9. package/dist/app/run-doctor.js +45 -38
  10. package/dist/app/setup-codex.js +27 -17
  11. package/dist/app/show-config.js +1 -5
  12. package/dist/app/switch-provider.js +33 -20
  13. package/dist/cli/output.js +4 -6
  14. package/dist/cli.js +1 -1
  15. package/dist/commands/handlers.js +223 -39
  16. package/dist/commands/help.js +1 -0
  17. package/dist/commands/registry.js +48 -4
  18. package/dist/domain/config.js +4 -68
  19. package/dist/domain/providers.js +0 -5
  20. package/dist/domain/runtime-state.js +2 -1
  21. package/dist/domain/setup.js +58 -3
  22. package/dist/interaction/add-interactive.js +55 -1
  23. package/dist/interaction/interactive.js +1 -5
  24. package/dist/runtime/copilot-adapter.js +44 -1
  25. package/dist/runtime/copilot-bridge-worker.js +1 -1
  26. package/dist/runtime/copilot-bridge.js +60 -19
  27. package/dist/runtime/copilot-cli.js +70 -0
  28. package/dist/runtime/copilot-installer.js +49 -2
  29. package/dist/storage/auth-repo.js +28 -77
  30. package/dist/storage/config-repo.js +1 -36
  31. package/dist/storage/runtime-state-repo.js +32 -0
  32. package/docs/Design/codex-switch-copilot-integration-design.md +517 -0
  33. package/docs/Design/codex-switch-v0.0.10-design.md +669 -0
  34. package/docs/Design/codex-switch-v0.0.9-design.md +182 -0
  35. package/docs/PRD/codex-switch-prd-v0.0.10.md +406 -0
  36. package/docs/PRD/codex-switch-prd-v0.0.9.md +166 -0
  37. package/docs/Tests/testing-bridge-v0.0.9.md +367 -0
  38. package/docs/cli-usage.md +38 -14
  39. package/docs/codex-switch-product-overview.md +2 -2
  40. package/docs/codex-switch-technical-architecture.md +6 -5
  41. package/package.json +1 -1
  42. /package/docs/{test-report-0.0.5.md → Tests/test-report-0.0.5.md} +0 -0
  43. /package/docs/{test-report-0.0.7.md → Tests/test-report-0.0.7.md} +0 -0
  44. /package/docs/{testing.md → Tests/testing.md} +0 -0
@@ -2,21 +2,23 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildSetupDrafts = buildSetupDrafts;
4
4
  exports.findIncompleteSetupProfiles = findIncompleteSetupProfiles;
5
+ exports.collectMigrateAdoptability = collectMigrateAdoptability;
6
+ const config_1 = require("./config");
5
7
  const providers_1 = require("./providers");
6
8
  /**
7
9
  * Creates initial provider drafts from config profile names.
8
10
  */
9
- function buildSetupDrafts(profiles, detailsByProfile) {
11
+ function buildSetupDrafts(profiles, detailsByProfile, runtimeByProfile) {
10
12
  return profiles.map((profile) => {
11
13
  const detail = detailsByProfile[profile] ?? {};
14
+ const runtime = runtimeByProfile[profile];
12
15
  const providerName = (detail.providerName ?? profile).trim();
13
16
  return {
14
17
  providerName,
15
18
  record: (0, providers_1.cleanProviderRecord)({
16
19
  profile,
17
20
  apiKey: detail.apiKey ?? "",
18
- envKey: detail.envKey ?? "",
19
- baseUrl: detail.baseUrl,
21
+ baseUrl: detail.baseUrl ?? runtime?.baseUrl,
20
22
  note: detail.note,
21
23
  tags: detail.tags,
22
24
  }),
@@ -29,3 +31,56 @@ function buildSetupDrafts(profiles, detailsByProfile) {
29
31
  function findIncompleteSetupProfiles(drafts) {
30
32
  return drafts.filter((draft) => draft.record.apiKey.trim() === "").map((draft) => draft.record.profile);
31
33
  }
34
+ /**
35
+ * Collects the unmanaged profiles that can be safely adopted by migrate.
36
+ */
37
+ function collectMigrateAdoptability(document, providers) {
38
+ const views = (0, config_1.buildManagedProfileViews)(document, providers)
39
+ .filter((view) => view.source !== "orphaned-reference")
40
+ .sort((left, right) => left.name.localeCompare(right.name));
41
+ const modelProvidersByName = new Map(document.modelProviders.map((provider) => [provider.name, provider]));
42
+ const availableProfiles = views.map((view) => view.name);
43
+ const adoptableProfileDetails = [];
44
+ const blockingReasonsByProfile = {};
45
+ for (const view of views) {
46
+ const reasons = [];
47
+ if (!view.model) {
48
+ reasons.push("model is missing.");
49
+ }
50
+ if (!view.modelProvider) {
51
+ reasons.push("model_provider is missing.");
52
+ }
53
+ else {
54
+ if (view.modelProvider !== view.name) {
55
+ reasons.push(`model_provider must match the profile name "${view.name}".`);
56
+ }
57
+ const modelProviderSection = modelProvidersByName.get(view.modelProvider);
58
+ if (!modelProviderSection) {
59
+ reasons.push(`model_providers.${view.modelProvider} section is missing.`);
60
+ }
61
+ else {
62
+ if (!modelProviderSection.baseUrl) {
63
+ reasons.push(`model_providers.${view.modelProvider}.base_url is missing.`);
64
+ }
65
+ }
66
+ }
67
+ if (view.source !== "unmanaged") {
68
+ reasons.push("profile is already managed by providers.json.");
69
+ }
70
+ if (reasons.length === 0) {
71
+ adoptableProfileDetails.push({
72
+ name: view.name,
73
+ model: view.model,
74
+ baseUrl: view.baseUrl,
75
+ });
76
+ continue;
77
+ }
78
+ blockingReasonsByProfile[view.name] = reasons;
79
+ }
80
+ return {
81
+ availableProfiles,
82
+ adoptableProfiles: adoptableProfileDetails.map((profile) => profile.name),
83
+ blockingReasonsByProfile,
84
+ adoptableProfileDetails,
85
+ };
86
+ }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.COMMON_TAG_CHOICES = void 0;
4
4
  exports.collectAddInput = collectAddInput;
5
+ exports.collectCopilotAddInput = collectCopilotAddInput;
5
6
  exports.createNonInteractiveAddError = createNonInteractiveAddError;
6
7
  exports.promptTags = promptTags;
7
8
  const errors_1 = require("../domain/errors");
@@ -37,10 +38,51 @@ async function collectAddInput(runtime, defaults, providerExists, profileExists)
37
38
  tags,
38
39
  };
39
40
  }
41
+ /**
42
+ * Collects Copilot add command inputs interactively when required values are missing.
43
+ */
44
+ async function collectCopilotAddInput(runtime, defaults, providerExists, profileExists, options) {
45
+ runtime.writeLine("Interactive add mode");
46
+ runtime.writeLine("Provide the missing Copilot provider fields. Press Enter to keep optional values empty.");
47
+ const providerName = defaults.providerName
48
+ ? normalizeRequiredValue(defaults.providerName)
49
+ : await promptProviderName(runtime, providerExists);
50
+ const profile = defaults.profile ? normalizeRequiredValue(defaults.profile) : await promptRequiredValue(runtime, "Profile");
51
+ const createProfile = !profileExists(profile);
52
+ const model = createProfile
53
+ ? defaults.model
54
+ ? normalizeRequiredValue(defaults.model)
55
+ : await promptRequiredValue(runtime, `Model for new profile "${profile}"`)
56
+ : defaults.model ?? null;
57
+ const note = defaults.note ?? normalizeOptionalValue(await runtime.inputText("Note (optional)"));
58
+ const tags = defaults.tags.length > 0 ? defaults.tags : await promptTags(runtime);
59
+ const bridgeHost = options?.bridgeHost ?? normalizeOptionalValue(await runtime.inputText("Bridge host (optional)", { defaultValue: "127.0.0.1" }));
60
+ const bridgePortText = options?.bridgePort !== undefined && options.bridgePort !== null
61
+ ? String(options.bridgePort)
62
+ : await runtime.inputText("Bridge port (optional)", { defaultValue: "41415" });
63
+ const bridgePort = normalizeBridgePort(runtime, bridgePortText);
64
+ const bridgeApiKey = options?.bridgeApiKey ?? normalizeOptionalValue(await runtime.inputSecret("Bridge API key (optional)"));
65
+ return {
66
+ providerName,
67
+ profile,
68
+ createProfile,
69
+ model,
70
+ note,
71
+ tags,
72
+ bridgeApiKey,
73
+ bridgeHost,
74
+ bridgePort,
75
+ };
76
+ }
40
77
  /**
41
78
  * Throws a consistent error when interactive add is unavailable.
42
79
  */
43
- function createNonInteractiveAddError() {
80
+ function createNonInteractiveAddError(options) {
81
+ if (options?.copilot) {
82
+ return (0, errors_1.cliError)("INVALID_ARGUMENT", "add --copilot requires <provider> and --profile when running without an interactive TTY.", {
83
+ suggestion: "Run in a terminal TTY or pass <provider>, --profile, and any optional Copilot bridge flags explicitly.",
84
+ });
85
+ }
44
86
  return (0, errors_1.cliError)("INVALID_ARGUMENT", "add requires <provider>, --profile, and --api-key when running without an interactive TTY.", {
45
87
  suggestion: "Run in a terminal TTY or pass all required values explicitly.",
46
88
  });
@@ -90,6 +132,18 @@ function normalizeOptionalValue(value) {
90
132
  const normalized = value.trim();
91
133
  return normalized === "" ? null : normalized;
92
134
  }
135
+ function normalizeBridgePort(runtime, value) {
136
+ const normalized = value.trim();
137
+ if (normalized === "") {
138
+ return null;
139
+ }
140
+ const parsed = Number(normalized);
141
+ if (Number.isInteger(parsed) && parsed > 0) {
142
+ return parsed;
143
+ }
144
+ runtime.writeLine("Bridge port must be a positive integer. Falling back to the default port.");
145
+ return null;
146
+ }
93
147
  async function promptTags(runtime, defaults = []) {
94
148
  const defaultPresetTags = defaults.filter(isCommonTag);
95
149
  return runtime.selectMany("Select tags (optional)", exports.COMMON_TAG_CHOICES.map((tag) => ({ value: tag, label: tag })), { defaultValues: defaultPresetTags });
@@ -222,7 +222,7 @@ async function chooseSetupProfiles(runtime, profiles) {
222
222
  return runtime.selectMany("Choose unmanaged config profiles to adopt into providers.json.", profiles.map((profile) => ({
223
223
  value: profile.name,
224
224
  label: profile.name,
225
- hint: `${profile.model} | ${profile.baseUrl} | ${profile.envKey}`,
225
+ hint: `${profile.model} | ${profile.baseUrl}`,
226
226
  })));
227
227
  }
228
228
  /**
@@ -235,9 +235,6 @@ async function collectSetupProviderDetails(runtime, profiles, defaultsByProfile
235
235
  const providerName = (await runtime.inputText(`Provider name for profile "${profile}"`, {
236
236
  defaultValue: defaults.providerName ?? profile,
237
237
  })).trim();
238
- if (defaults.envKey) {
239
- runtime.writeLine(`Runtime env key for "${profile}": ${defaults.envKey}`);
240
- }
241
238
  const apiKey = await promptRequiredSecret(runtime, `API key for profile "${profile}"`, defaults.apiKey?.trim() || undefined);
242
239
  const baseUrl = (await runtime.inputText(`Base URL note for profile "${profile}" (optional)`, {
243
240
  defaultValue: defaults.baseUrl ?? "",
@@ -249,7 +246,6 @@ async function collectSetupProviderDetails(runtime, profiles, defaultsByProfile
249
246
  result[profile] = {
250
247
  providerName: providerName || defaults.providerName || profile,
251
248
  apiKey,
252
- envKey: defaults.envKey,
253
249
  baseUrl: baseUrl || defaults.baseUrl || undefined,
254
250
  note: note || defaults.note || undefined,
255
251
  // Empty selections are omitted so downstream setup validation can distinguish unset from explicit data.
@@ -114,7 +114,7 @@ async function createCopilotSession() {
114
114
  throw (0, errors_1.cliError)("COPILOT_SDK_UNSUPPORTED", "The installed Copilot SDK does not expose a supported createSession API.", {});
115
115
  }
116
116
  try {
117
- const session = (await Promise.resolve(createSession({})));
117
+ const session = (await Promise.resolve(createSession(createSessionOptions(sdk))));
118
118
  return {
119
119
  sdk,
120
120
  client,
@@ -122,11 +122,30 @@ async function createCopilotSession() {
122
122
  };
123
123
  }
124
124
  catch (error) {
125
+ if (classifyCopilotSessionError(error) === "unsupported") {
126
+ throw (0, errors_1.cliError)("COPILOT_SDK_UNSUPPORTED", "The installed Copilot SDK does not expose a compatible permission-handling session API.", {
127
+ cause: error instanceof Error ? error.message : String(error),
128
+ });
129
+ }
125
130
  throw (0, errors_1.cliError)("COPILOT_AUTH_REQUIRED", "Copilot authentication is required before the local bridge can be used.", {
126
131
  cause: error instanceof Error ? error.message : String(error),
127
132
  });
128
133
  }
129
134
  }
135
+ /**
136
+ * Builds the session options used consistently across auth probes and request execution.
137
+ */
138
+ function createSessionOptions(sdk) {
139
+ const approveAll = resolveApproveAll(sdk);
140
+ if (approveAll) {
141
+ return {
142
+ onPermissionRequest: (request) => approveAll(request),
143
+ };
144
+ }
145
+ return {
146
+ onPermissionRequest: () => true,
147
+ };
148
+ }
130
149
  function createCopilotClient(sdk) {
131
150
  const ClientCtor = resolveConstructor(sdk, "CopilotClient");
132
151
  if (!ClientCtor) {
@@ -146,6 +165,16 @@ async function stopCopilotClient(client) {
146
165
  await Promise.resolve(client.stop());
147
166
  }
148
167
  }
168
+ /**
169
+ * Distinguishes true auth failures from SDK API-shape mismatches.
170
+ */
171
+ function classifyCopilotSessionError(error) {
172
+ const message = error instanceof Error ? error.message : String(error);
173
+ if (/onPermissionRequest/i.test(message) || /permission/i.test(message)) {
174
+ return "unsupported";
175
+ }
176
+ return "auth";
177
+ }
149
178
  function resolveCallable(target, name) {
150
179
  if (!target) {
151
180
  return null;
@@ -171,3 +200,17 @@ function resolveConstructor(target, name) {
171
200
  }
172
201
  return null;
173
202
  }
203
+ /**
204
+ * Resolves the SDK-provided permission helper when available.
205
+ */
206
+ function resolveApproveAll(target) {
207
+ const direct = target.approveAll;
208
+ if (typeof direct === "function") {
209
+ return direct;
210
+ }
211
+ const nestedDefault = target.default;
212
+ if (nestedDefault && typeof nestedDefault.approveAll === "function") {
213
+ return nestedDefault.approveAll;
214
+ }
215
+ return null;
216
+ }
@@ -5,7 +5,7 @@ const copilot_adapter_1 = require("./copilot-adapter");
5
5
  async function main() {
6
6
  const provider = process.env.CODEX_SWITCH_BRIDGE_PROVIDER ?? "copilot";
7
7
  const host = process.env.CODEX_SWITCH_BRIDGE_HOST ?? "127.0.0.1";
8
- const port = Number(process.env.CODEX_SWITCH_BRIDGE_PORT ?? "4141");
8
+ const port = Number(process.env.CODEX_SWITCH_BRIDGE_PORT ?? "41415");
9
9
  const apiKey = process.env.CODEX_SWITCH_BRIDGE_API_KEY ?? "";
10
10
  await (0, copilot_bridge_1.startCopilotBridgeServer)({
11
11
  host,
@@ -37,6 +37,7 @@ exports.setCopilotBridgeSpawnImplementation = setCopilotBridgeSpawnImplementatio
37
37
  exports.resetCopilotBridgeSpawnImplementation = resetCopilotBridgeSpawnImplementation;
38
38
  exports.probeCopilotBridgeRuntime = probeCopilotBridgeRuntime;
39
39
  exports.ensureCopilotBridge = ensureCopilotBridge;
40
+ exports.startOrReuseCopilotBridge = startOrReuseCopilotBridge;
40
41
  exports.createCopilotBridgeRequestHandler = createCopilotBridgeRequestHandler;
41
42
  exports.startCopilotBridgeServer = startCopilotBridgeServer;
42
43
  exports.waitForCopilotBridgeHealth = waitForCopilotBridgeHealth;
@@ -64,8 +65,17 @@ function resetCopilotBridgeSpawnImplementation() {
64
65
  /**
65
66
  * Returns the last known Copilot bridge runtime status.
66
67
  */
67
- async function probeCopilotBridgeRuntime(provider) {
68
- const state = (0, runtime_state_repo_1.readCopilotBridgeState)();
68
+ async function probeCopilotBridgeRuntime(provider, persistedState) {
69
+ const state = persistedState === undefined ? (0, runtime_state_repo_1.readCopilotBridgeState)() : persistedState;
70
+ if (state && (!provider || !(0, providers_1.isCopilotBridgeProvider)(provider))) {
71
+ return {
72
+ ok: false,
73
+ runtime: "copilot-bridge",
74
+ reason: "failed",
75
+ cause: "Copilot bridge runtime state exists but no active Copilot bridge provider is selected.",
76
+ details: state,
77
+ };
78
+ }
69
79
  if (!provider || !(0, providers_1.isCopilotBridgeProvider)(provider)) {
70
80
  return {
71
81
  ok: false,
@@ -127,6 +137,12 @@ async function probeCopilotBridgeRuntime(provider) {
127
137
  * Starts or reuses a Copilot bridge worker, then verifies its health before returning.
128
138
  */
129
139
  async function ensureCopilotBridge(providerName, provider) {
140
+ return startOrReuseCopilotBridge(providerName, provider);
141
+ }
142
+ /**
143
+ * Starts or reuses a Copilot bridge worker and reports the chosen port.
144
+ */
145
+ async function startOrReuseCopilotBridge(providerName, provider) {
130
146
  if (!(0, providers_1.isCopilotBridgeProvider)(provider)) {
131
147
  throw (0, errors_1.cliError)("RUNTIME_PROVIDER_INVALID", "Provider is not backed by a Copilot bridge runtime.", {
132
148
  provider: providerName,
@@ -140,6 +156,7 @@ async function ensureCopilotBridge(providerName, provider) {
140
156
  }
141
157
  const expectedBaseUrl = (0, providers_1.buildCopilotBridgeBaseUrl)(runtime);
142
158
  const current = (0, runtime_state_repo_1.readCopilotBridgeState)();
159
+ let replaced = false;
143
160
  if (current && current.provider === providerName && current.baseUrl === expectedBaseUrl) {
144
161
  const healthy = await healthcheckCopilotBridge(current.host, current.port);
145
162
  if (healthy.ok) {
@@ -149,19 +166,20 @@ async function ensureCopilotBridge(providerName, provider) {
149
166
  });
150
167
  return {
151
168
  baseUrl: expectedBaseUrl,
169
+ host: current.host,
170
+ port: current.port,
152
171
  reused: true,
172
+ portChanged: false,
173
+ replaced: false,
153
174
  };
154
175
  }
155
176
  }
156
- const portCheck = await checkPortAvailability(runtime.bridgeHost, runtime.bridgePort);
157
- if (!portCheck.ok) {
158
- throw (0, errors_1.cliError)("BRIDGE_PORT_CONFLICT", "Copilot bridge port is already in use.", {
159
- provider: providerName,
160
- host: runtime.bridgeHost,
161
- port: runtime.bridgePort,
162
- cause: portCheck.cause,
163
- });
177
+ if (current && current.provider !== providerName) {
178
+ stopCopilotBridge();
179
+ replaced = true;
164
180
  }
181
+ const selectedPort = await selectBridgePort(runtime.bridgeHost, runtime.bridgePort);
182
+ const selectedBaseUrl = `http://${runtime.bridgeHost}:${selectedPort}${runtime.bridgePath}`;
165
183
  const workerPath = path.join(__dirname, "copilot-bridge-worker.js");
166
184
  let child;
167
185
  try {
@@ -172,9 +190,9 @@ async function ensureCopilotBridge(providerName, provider) {
172
190
  ...process.env,
173
191
  CODEX_SWITCH_BRIDGE_PROVIDER: providerName,
174
192
  CODEX_SWITCH_BRIDGE_HOST: runtime.bridgeHost,
175
- CODEX_SWITCH_BRIDGE_PORT: String(runtime.bridgePort),
193
+ CODEX_SWITCH_BRIDGE_PORT: String(selectedPort),
176
194
  CODEX_SWITCH_BRIDGE_API_KEY: provider.apiKey,
177
- CODEX_SWITCH_BRIDGE_BASE_URL: expectedBaseUrl,
195
+ CODEX_SWITCH_BRIDGE_BASE_URL: selectedBaseUrl,
178
196
  },
179
197
  });
180
198
  }
@@ -182,27 +200,27 @@ async function ensureCopilotBridge(providerName, provider) {
182
200
  throw (0, errors_1.cliError)("BRIDGE_START_FAILED", "Failed to start the Copilot bridge worker.", {
183
201
  provider: providerName,
184
202
  host: runtime.bridgeHost,
185
- port: runtime.bridgePort,
203
+ port: selectedPort,
186
204
  cause: error instanceof Error ? error.message : String(error),
187
205
  });
188
206
  }
189
207
  child.unref();
190
208
  const startedAt = new Date().toISOString();
191
- const healthy = await waitForCopilotBridgeStartup(child, runtime.bridgeHost, runtime.bridgePort, 15, 200);
209
+ const healthy = await waitForCopilotBridgeStartup(child, runtime.bridgeHost, selectedPort, 15, 200);
192
210
  if (!healthy.ok) {
193
211
  (0, runtime_state_repo_1.clearCopilotBridgeState)();
194
212
  if (healthy.reason === "start-failed") {
195
213
  throw (0, errors_1.cliError)("BRIDGE_START_FAILED", "Copilot bridge worker exited before becoming healthy.", {
196
214
  provider: providerName,
197
215
  host: runtime.bridgeHost,
198
- port: runtime.bridgePort,
216
+ port: selectedPort,
199
217
  cause: healthy.cause,
200
218
  });
201
219
  }
202
220
  throw (0, errors_1.cliError)("BRIDGE_HEALTHCHECK_FAILED", "Copilot bridge did not become healthy after startup.", {
203
221
  provider: providerName,
204
222
  host: runtime.bridgeHost,
205
- port: runtime.bridgePort,
223
+ port: selectedPort,
206
224
  cause: healthy.cause,
207
225
  });
208
226
  }
@@ -210,15 +228,19 @@ async function ensureCopilotBridge(providerName, provider) {
210
228
  provider: providerName,
211
229
  pid: child.pid ?? null,
212
230
  host: runtime.bridgeHost,
213
- port: runtime.bridgePort,
214
- baseUrl: expectedBaseUrl,
231
+ port: selectedPort,
232
+ baseUrl: selectedBaseUrl,
215
233
  startedAt,
216
234
  lastHealthcheckAt: new Date().toISOString(),
217
235
  };
218
236
  (0, runtime_state_repo_1.writeCopilotBridgeState)(state);
219
237
  return {
220
- baseUrl: expectedBaseUrl,
238
+ baseUrl: selectedBaseUrl,
239
+ host: runtime.bridgeHost,
240
+ port: selectedPort,
221
241
  reused: false,
242
+ portChanged: selectedPort !== runtime.bridgePort,
243
+ replaced,
222
244
  };
223
245
  }
224
246
  /**
@@ -342,6 +364,25 @@ async function checkPortAvailability(host, port) {
342
364
  });
343
365
  });
344
366
  }
367
+ async function selectBridgePort(host, preferredPort) {
368
+ const preferred = await checkPortAvailability(host, preferredPort);
369
+ if (preferred.ok) {
370
+ return preferredPort;
371
+ }
372
+ for (let port = 10000; port <= 99999; port += 1) {
373
+ if (port === preferredPort) {
374
+ continue;
375
+ }
376
+ const available = await checkPortAvailability(host, port);
377
+ if (available.ok) {
378
+ return port;
379
+ }
380
+ }
381
+ throw (0, errors_1.cliError)("BRIDGE_PORT_CONFLICT", "Unable to find a free 5-digit bridge port.", {
382
+ host,
383
+ port: preferredPort,
384
+ });
385
+ }
345
386
  async function waitForCopilotBridgeStartup(child, host, port, attempts, delayMs) {
346
387
  let startupFailure = null;
347
388
  const onError = (error) => {
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setCopilotCliSpawnImplementation = setCopilotCliSpawnImplementation;
4
+ exports.resetCopilotCliSpawnImplementation = resetCopilotCliSpawnImplementation;
5
+ exports.checkCopilotCliAvailable = checkCopilotCliAvailable;
6
+ exports.runCopilotLogin = runCopilotLogin;
7
+ const node_child_process_1 = require("node:child_process");
8
+ let spawnImplementation = node_child_process_1.spawnSync;
9
+ /**
10
+ * Overrides the spawn implementation for Copilot CLI tests.
11
+ */
12
+ function setCopilotCliSpawnImplementation(spawnLike) {
13
+ spawnImplementation = spawnLike;
14
+ }
15
+ /**
16
+ * Restores the default spawn implementation after tests.
17
+ */
18
+ function resetCopilotCliSpawnImplementation() {
19
+ spawnImplementation = node_child_process_1.spawnSync;
20
+ }
21
+ /**
22
+ * Checks whether the GitHub Copilot CLI is available on PATH.
23
+ */
24
+ function checkCopilotCliAvailable() {
25
+ const invocation = getCopilotInvocation(["--help"]);
26
+ const result = spawnImplementation(invocation.command, invocation.args, {
27
+ stdio: "pipe",
28
+ encoding: "utf8",
29
+ shell: false,
30
+ });
31
+ if (result.error || result.status !== 0) {
32
+ return {
33
+ ok: false,
34
+ cause: result.error?.message ?? (result.stderr.trim() || "Unknown failure"),
35
+ };
36
+ }
37
+ return { ok: true };
38
+ }
39
+ /**
40
+ * Launches the official `copilot login` flow in the current terminal.
41
+ */
42
+ function runCopilotLogin(options) {
43
+ const args = ["login"];
44
+ if (options?.host) {
45
+ args.push("--hostname", options.host);
46
+ }
47
+ const invocation = getCopilotInvocation(args);
48
+ const result = spawnImplementation(invocation.command, invocation.args, {
49
+ stdio: "inherit",
50
+ shell: false,
51
+ });
52
+ if (result.error || result.status !== 0) {
53
+ throw new Error(result.error?.message ?? `copilot login exited with status ${String(result.status)}`);
54
+ }
55
+ }
56
+ /**
57
+ * Resolves a cross-platform invocation for the Copilot CLI.
58
+ */
59
+ function getCopilotInvocation(args) {
60
+ if (process.platform === "win32") {
61
+ return {
62
+ command: process.env.ComSpec || "cmd.exe",
63
+ args: ["/d", "/s", "/c", ["copilot", ...args].join(" ")],
64
+ };
65
+ }
66
+ return {
67
+ command: "copilot",
68
+ args,
69
+ };
70
+ }
@@ -107,19 +107,66 @@ function installCopilotSdk() {
107
107
  if (!fs.existsSync(packageJsonPath)) {
108
108
  fs.writeFileSync(packageJsonPath, `${JSON.stringify({ name: "codex-switch-copilot-runtime", private: true, version: "0.0.0" }, null, 2)}\n`, "utf8");
109
109
  }
110
- const command = process.platform === "win32" ? "npm.cmd" : "npm";
111
- const result = spawnImplementation(command, ["install", "--no-save", `${COPILOT_SDK_PACKAGE}@${COPILOT_SDK_VERSION}`], {
110
+ const installCommand = resolveNpmInstallCommand();
111
+ const result = spawnImplementation(installCommand.command, installCommand.args, {
112
112
  cwd: installDir,
113
113
  stdio: "pipe",
114
114
  encoding: "utf8",
115
115
  shell: false,
116
116
  });
117
+ if (result.error) {
118
+ throw (0, errors_1.cliError)("COPILOT_SDK_INSTALL_FAILED", "Failed to install the optional Copilot SDK runtime.", {
119
+ installDir,
120
+ packageName: COPILOT_SDK_PACKAGE,
121
+ cause: result.error.message,
122
+ errorCode: result.error.code ?? null,
123
+ command: installCommand.command,
124
+ args: installCommand.args,
125
+ });
126
+ }
117
127
  if (result.status !== 0) {
118
128
  throw (0, errors_1.cliError)("COPILOT_SDK_INSTALL_FAILED", "Failed to install the optional Copilot SDK runtime.", {
119
129
  installDir,
120
130
  packageName: COPILOT_SDK_PACKAGE,
121
131
  cause: result.stderr || result.stdout || `npm exited with status ${String(result.status)}`,
132
+ command: installCommand.command,
133
+ args: installCommand.args,
122
134
  });
123
135
  }
124
136
  return probeCopilotSdkInstall();
125
137
  }
138
+ /**
139
+ * Resolves a stable npm install invocation for the optional Copilot SDK runtime.
140
+ */
141
+ function resolveNpmInstallCommand() {
142
+ const installArgs = ["install", "--no-save", `${COPILOT_SDK_PACKAGE}@${COPILOT_SDK_VERSION}`];
143
+ const npmCliPath = resolveNpmCliPath();
144
+ if (npmCliPath) {
145
+ return {
146
+ command: process.execPath,
147
+ args: [npmCliPath, ...installArgs],
148
+ };
149
+ }
150
+ return {
151
+ command: process.platform === "win32" ? "npm.cmd" : "npm",
152
+ args: installArgs,
153
+ };
154
+ }
155
+ /**
156
+ * Finds a locally available npm CLI script near the active Node runtime.
157
+ */
158
+ function resolveNpmCliPath() {
159
+ const execDir = path.dirname(process.execPath);
160
+ const candidates = [
161
+ process.env.npm_execpath,
162
+ path.join(execDir, "node_modules", "npm", "bin", "npm-cli.js"),
163
+ path.join(execDir, "..", "node_modules", "npm", "bin", "npm-cli.js"),
164
+ path.join(execDir, "..", "..", "node_modules", "npm", "bin", "npm-cli.js"),
165
+ ];
166
+ for (const candidate of candidates) {
167
+ if (candidate && fs.existsSync(candidate)) {
168
+ return path.resolve(candidate);
169
+ }
170
+ }
171
+ return null;
172
+ }