@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.
- package/README.AI.md +5 -3
- package/README.CN.md +25 -3
- package/README.md +3 -2
- package/dist/app/add-provider.js +1 -12
- package/dist/app/bridge.js +295 -0
- package/dist/app/edit-provider.js +1 -17
- package/dist/app/get-status.js +32 -2
- package/dist/app/list-providers.js +0 -1
- package/dist/app/run-doctor.js +45 -38
- package/dist/app/setup-codex.js +27 -17
- package/dist/app/show-config.js +1 -5
- package/dist/app/switch-provider.js +33 -20
- package/dist/cli/output.js +4 -6
- package/dist/cli.js +1 -1
- package/dist/commands/handlers.js +223 -39
- package/dist/commands/help.js +1 -0
- package/dist/commands/registry.js +48 -4
- package/dist/domain/config.js +4 -68
- package/dist/domain/providers.js +0 -5
- package/dist/domain/runtime-state.js +2 -1
- package/dist/domain/setup.js +58 -3
- package/dist/interaction/add-interactive.js +55 -1
- package/dist/interaction/interactive.js +1 -5
- package/dist/runtime/copilot-adapter.js +44 -1
- package/dist/runtime/copilot-bridge-worker.js +1 -1
- package/dist/runtime/copilot-bridge.js +60 -19
- package/dist/runtime/copilot-cli.js +70 -0
- package/dist/runtime/copilot-installer.js +49 -2
- package/dist/storage/auth-repo.js +28 -77
- package/dist/storage/config-repo.js +1 -36
- package/dist/storage/runtime-state-repo.js +32 -0
- package/docs/Design/codex-switch-copilot-integration-design.md +517 -0
- package/docs/Design/codex-switch-v0.0.10-design.md +669 -0
- package/docs/Design/codex-switch-v0.0.9-design.md +182 -0
- package/docs/PRD/codex-switch-prd-v0.0.10.md +406 -0
- package/docs/PRD/codex-switch-prd-v0.0.9.md +166 -0
- package/docs/Tests/testing-bridge-v0.0.9.md +367 -0
- package/docs/cli-usage.md +38 -14
- package/docs/codex-switch-product-overview.md +2 -2
- package/docs/codex-switch-technical-architecture.md +6 -5
- package/package.json +1 -1
- /package/docs/{test-report-0.0.5.md → Tests/test-report-0.0.5.md} +0 -0
- /package/docs/{test-report-0.0.7.md → Tests/test-report-0.0.7.md} +0 -0
- /package/docs/{testing.md → Tests/testing.md} +0 -0
package/dist/domain/setup.js
CHANGED
|
@@ -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
|
-
|
|
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}
|
|
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 ?? "
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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(
|
|
193
|
+
CODEX_SWITCH_BRIDGE_PORT: String(selectedPort),
|
|
176
194
|
CODEX_SWITCH_BRIDGE_API_KEY: provider.apiKey,
|
|
177
|
-
CODEX_SWITCH_BRIDGE_BASE_URL:
|
|
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:
|
|
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,
|
|
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:
|
|
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:
|
|
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:
|
|
214
|
-
baseUrl:
|
|
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:
|
|
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
|
|
111
|
-
const result = spawnImplementation(command,
|
|
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
|
+
}
|