@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
|
@@ -48,16 +48,20 @@ const list_providers_1 = require("../app/list-providers");
|
|
|
48
48
|
const remove_provider_1 = require("../app/remove-provider");
|
|
49
49
|
const rollback_backup_1 = require("../app/rollback-backup");
|
|
50
50
|
const run_doctor_1 = require("../app/run-doctor");
|
|
51
|
+
const bridge_1 = require("../app/bridge");
|
|
51
52
|
const setup_codex_1 = require("../app/setup-codex");
|
|
52
53
|
const show_config_1 = require("../app/show-config");
|
|
53
54
|
const show_provider_1 = require("../app/show-provider");
|
|
54
55
|
const switch_provider_1 = require("../app/switch-provider");
|
|
55
56
|
const config_1 = require("../domain/config");
|
|
56
57
|
const errors_1 = require("../domain/errors");
|
|
58
|
+
const setup_1 = require("../domain/setup");
|
|
57
59
|
const providers_1 = require("../domain/providers");
|
|
58
60
|
const add_interactive_1 = require("../interaction/add-interactive");
|
|
59
61
|
const interactive_1 = require("../interaction/interactive");
|
|
60
62
|
const prompt_1 = require("../interaction/prompt");
|
|
63
|
+
const copilot_adapter_1 = require("../runtime/copilot-adapter");
|
|
64
|
+
const copilot_cli_1 = require("../runtime/copilot-cli");
|
|
61
65
|
const copilot_installer_1 = require("../runtime/copilot-installer");
|
|
62
66
|
const config_repo_1 = require("../storage/config-repo");
|
|
63
67
|
const codex_paths_1 = require("../storage/codex-paths");
|
|
@@ -90,6 +94,36 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
90
94
|
return (0, get_current_profile_1.getCurrentProfile)(paths.configPath);
|
|
91
95
|
case "status":
|
|
92
96
|
return (0, get_status_1.getStatus)(paths.codexDir, paths.configPath, paths.providersPath, paths.authPath);
|
|
97
|
+
case "bridge-start": {
|
|
98
|
+
const providerName = parsed.positionals[0] ?? null;
|
|
99
|
+
return (0, bridge_1.startBridge)({
|
|
100
|
+
providersPath: paths.providersPath,
|
|
101
|
+
configPath: paths.configPath,
|
|
102
|
+
providerName,
|
|
103
|
+
runtime,
|
|
104
|
+
json: ctx.options.json,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
case "bridge-stop": {
|
|
108
|
+
const providerName = parsed.positionals[0] ?? null;
|
|
109
|
+
return (0, bridge_1.stopBridge)({
|
|
110
|
+
providersPath: paths.providersPath,
|
|
111
|
+
configPath: paths.configPath,
|
|
112
|
+
providerName,
|
|
113
|
+
runtime,
|
|
114
|
+
json: ctx.options.json,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
case "bridge-status": {
|
|
118
|
+
const providerName = parsed.positionals[0] ?? null;
|
|
119
|
+
return (0, bridge_1.statusBridge)({
|
|
120
|
+
providersPath: paths.providersPath,
|
|
121
|
+
configPath: paths.configPath,
|
|
122
|
+
providerName,
|
|
123
|
+
runtime,
|
|
124
|
+
json: ctx.options.json,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
93
127
|
case "init": {
|
|
94
128
|
let codexDir = ctx.options.codexDir;
|
|
95
129
|
const candidates = (0, config_repo_1.findCodexDirCandidates)(ctx.options.codexDirExplicit ? ctx.options.codexDir : null);
|
|
@@ -226,40 +260,70 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
226
260
|
let tags = parsed.commandOptions.get("--tag") ?? [];
|
|
227
261
|
let createProfile = (0, args_1.hasFlag)(parsed.commandOptions, "--create-profile");
|
|
228
262
|
const copilot = (0, args_1.hasFlag)(parsed.commandOptions, "--copilot");
|
|
229
|
-
|
|
263
|
+
let bridgeHost = (0, args_1.getSingleOption)(parsed.commandOptions, "--bridge-host", false);
|
|
230
264
|
const bridgePortValue = (0, args_1.getSingleOption)(parsed.commandOptions, "--bridge-port", false);
|
|
231
|
-
|
|
265
|
+
let bridgeApiKey = (0, args_1.getSingleOption)(parsed.commandOptions, "--bridge-api-key", false);
|
|
232
266
|
let installCopilotSdk = (0, args_1.hasFlag)(parsed.commandOptions, "--install-copilot-sdk");
|
|
233
|
-
|
|
267
|
+
let bridgePort = bridgePortValue ? Number(bridgePortValue) : null;
|
|
234
268
|
if (copilot && apiKey) {
|
|
235
269
|
throw (0, errors_1.cliError)("INVALID_ARGUMENT", "--copilot does not allow --api-key. Use --bridge-api-key for the local bridge secret.");
|
|
236
270
|
}
|
|
237
271
|
if (bridgePortValue && (!Number.isInteger(bridgePort) || bridgePort === null || bridgePort <= 0)) {
|
|
238
272
|
throw (0, errors_1.cliError)("INVALID_ARGUMENT", "--bridge-port must be a positive integer.");
|
|
239
273
|
}
|
|
240
|
-
if (copilot
|
|
241
|
-
installCopilotSdk = await
|
|
274
|
+
if (copilot) {
|
|
275
|
+
installCopilotSdk = await ensureCopilotReadyForAdd({
|
|
276
|
+
runtime,
|
|
277
|
+
json: ctx.options.json,
|
|
278
|
+
installCopilotSdk,
|
|
279
|
+
});
|
|
242
280
|
}
|
|
243
281
|
if (!providerName || !profile || (!apiKey && !copilot)) {
|
|
244
282
|
if (ctx.options.json || !runtime.isInteractive()) {
|
|
245
|
-
throw (0, add_interactive_1.createNonInteractiveAddError)();
|
|
283
|
+
throw (0, add_interactive_1.createNonInteractiveAddError)({ copilot });
|
|
284
|
+
}
|
|
285
|
+
if (copilot) {
|
|
286
|
+
const prompted = await (0, add_interactive_1.collectCopilotAddInput)(runtime, {
|
|
287
|
+
providerName,
|
|
288
|
+
profile,
|
|
289
|
+
model,
|
|
290
|
+
note,
|
|
291
|
+
tags,
|
|
292
|
+
}, (candidate) => Boolean((0, providers_repo_1.readProvidersFileIfExists)(paths.providersPath).providers[candidate]), (candidate) => Boolean((0, config_repo_1.readStructuredConfig)(paths.configPath).profiles.find((profileView) => profileView.name === candidate)), {
|
|
293
|
+
bridgeHost,
|
|
294
|
+
bridgePort,
|
|
295
|
+
bridgeApiKey,
|
|
296
|
+
});
|
|
297
|
+
providerName = prompted.providerName;
|
|
298
|
+
profile = prompted.profile;
|
|
299
|
+
model = prompted.model ?? null;
|
|
300
|
+
note = prompted.note ?? null;
|
|
301
|
+
tags = prompted.tags;
|
|
302
|
+
createProfile = createProfile || prompted.createProfile;
|
|
303
|
+
baseUrl = null;
|
|
304
|
+
bridgeHost = prompted.bridgeHost ?? bridgeHost;
|
|
305
|
+
bridgePort = prompted.bridgePort ?? bridgePort;
|
|
306
|
+
bridgeApiKey = prompted.bridgeApiKey ?? bridgeApiKey;
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
const prompted = await (0, add_interactive_1.collectAddInput)(runtime, {
|
|
310
|
+
providerName,
|
|
311
|
+
profile,
|
|
312
|
+
apiKey,
|
|
313
|
+
model,
|
|
314
|
+
baseUrl,
|
|
315
|
+
note,
|
|
316
|
+
tags,
|
|
317
|
+
}, (candidate) => Boolean((0, providers_repo_1.readProvidersFileIfExists)(paths.providersPath).providers[candidate]), (candidate) => Boolean((0, config_repo_1.readStructuredConfig)(paths.configPath).profiles.find((profileView) => profileView.name === candidate)));
|
|
318
|
+
providerName = prompted.providerName;
|
|
319
|
+
profile = prompted.profile;
|
|
320
|
+
apiKey = prompted.apiKey;
|
|
321
|
+
model = prompted.model ?? null;
|
|
322
|
+
baseUrl = prompted.baseUrl ?? null;
|
|
323
|
+
note = prompted.note ?? null;
|
|
324
|
+
tags = prompted.tags;
|
|
325
|
+
createProfile = createProfile || prompted.createProfile;
|
|
246
326
|
}
|
|
247
|
-
const prompted = await (0, add_interactive_1.collectAddInput)(runtime, {
|
|
248
|
-
providerName,
|
|
249
|
-
profile,
|
|
250
|
-
apiKey,
|
|
251
|
-
baseUrl,
|
|
252
|
-
note,
|
|
253
|
-
tags,
|
|
254
|
-
}, (candidate) => Boolean((0, providers_repo_1.readProvidersFileIfExists)(paths.providersPath).providers[candidate]), (candidate) => Boolean((0, config_repo_1.readStructuredConfig)(paths.configPath).profiles.find((profileView) => profileView.name === candidate)));
|
|
255
|
-
providerName = prompted.providerName;
|
|
256
|
-
profile = prompted.profile;
|
|
257
|
-
apiKey = prompted.apiKey;
|
|
258
|
-
model = prompted.model ?? null;
|
|
259
|
-
baseUrl = prompted.baseUrl ?? null;
|
|
260
|
-
note = prompted.note ?? null;
|
|
261
|
-
tags = prompted.tags;
|
|
262
|
-
createProfile = createProfile || prompted.createProfile;
|
|
263
327
|
}
|
|
264
328
|
return (0, add_provider_1.addProvider)({
|
|
265
329
|
codexDir: paths.codexDir,
|
|
@@ -402,9 +466,25 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
402
466
|
if (overwrite && merge) {
|
|
403
467
|
throw (0, errors_1.cliError)("INVALID_ARGUMENT", "migrate does not allow both --merge and --overwrite.");
|
|
404
468
|
}
|
|
405
|
-
let strategy = overwrite ? "overwrite" : merge ? "merge" : null;
|
|
406
469
|
const providersExists = fs.existsSync(setupPaths.providersPath);
|
|
407
|
-
|
|
470
|
+
const document = (0, config_repo_1.readStructuredConfig)(setupPaths.configPath);
|
|
471
|
+
const currentProviders = providersExists ? (0, providers_1.validateProvidersShape)((0, providers_repo_1.readProvidersFileIfExists)(setupPaths.providersPath)) : null;
|
|
472
|
+
const adoptability = (0, setup_1.collectMigrateAdoptability)(document, currentProviders);
|
|
473
|
+
if (adoptability.availableProfiles.length === 0) {
|
|
474
|
+
throw (0, errors_1.cliError)("PROFILE_NOT_FOUND", "No profiles were found in config.toml.", {
|
|
475
|
+
file: setupPaths.configPath,
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
if (adoptability.adoptableProfiles.length === 0) {
|
|
479
|
+
throw (0, errors_1.cliError)("MIGRATE_NO_ADOPTABLE_PROFILES", "No adoptable profiles were found for migrate.", {
|
|
480
|
+
availableProfiles: adoptability.availableProfiles,
|
|
481
|
+
adoptableProfiles: adoptability.adoptableProfiles,
|
|
482
|
+
blockingReasonsByProfile: adoptability.blockingReasonsByProfile,
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
let strategy = overwrite ? "overwrite" : merge ? "merge" : null;
|
|
486
|
+
const registryIsEmpty = !currentProviders || Object.keys(currentProviders.providers).length === 0;
|
|
487
|
+
if (providersExists && strategy === null && !registryIsEmpty) {
|
|
408
488
|
if (!(0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
|
|
409
489
|
throw (0, errors_1.cliError)("PROVIDERS_ALREADY_EXISTS", "providers.json already exists. Pass --merge or --overwrite.", {
|
|
410
490
|
file: setupPaths.providersPath,
|
|
@@ -416,35 +496,35 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
416
496
|
}
|
|
417
497
|
strategy = selected;
|
|
418
498
|
}
|
|
419
|
-
const
|
|
420
|
-
const adoptableProfiles = (0, config_1.buildManagedProfileViews)(document, null)
|
|
421
|
-
.filter((view) => view.source === "unmanaged" && view.model && view.modelProvider === view.name && view.baseUrl && view.envKey)
|
|
422
|
-
.map((view) => ({
|
|
423
|
-
name: view.name,
|
|
424
|
-
model: view.model,
|
|
425
|
-
baseUrl: view.baseUrl,
|
|
426
|
-
envKey: view.envKey,
|
|
427
|
-
}))
|
|
428
|
-
.sort((left, right) => left.name.localeCompare(right.name));
|
|
429
|
-
const selectedProfiles = Array.from((0, config_repo_1.listConfigProfiles)(setupPaths.configPath)).sort();
|
|
499
|
+
const adoptableProfiles = adoptability.adoptableProfileDetails;
|
|
430
500
|
let adoptProfiles = [];
|
|
431
501
|
let providerDetailsByProfile = {};
|
|
432
502
|
if ((0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
|
|
433
503
|
adoptProfiles = await (0, interactive_1.chooseSetupProfiles)(runtime, adoptableProfiles);
|
|
434
504
|
// Defaults are derived from config.toml so interactive setup only asks for missing provider metadata.
|
|
435
|
-
|
|
505
|
+
const collectedDetails = await (0, interactive_1.collectSetupProviderDetails)(runtime, adoptProfiles, adoptableProfiles.reduce((accumulator, profile) => {
|
|
436
506
|
accumulator[profile.name] = {
|
|
437
507
|
providerName: profile.name,
|
|
438
|
-
envKey: profile.envKey,
|
|
439
508
|
baseUrl: profile.baseUrl,
|
|
440
509
|
};
|
|
441
510
|
return accumulator;
|
|
442
511
|
}, {}));
|
|
512
|
+
providerDetailsByProfile = Object.fromEntries(Object.entries(collectedDetails).map(([profile, detail]) => [
|
|
513
|
+
profile,
|
|
514
|
+
{
|
|
515
|
+
providerName: detail.providerName,
|
|
516
|
+
apiKey: detail.apiKey,
|
|
517
|
+
baseUrl: detail.baseUrl,
|
|
518
|
+
note: detail.note,
|
|
519
|
+
tags: detail.tags,
|
|
520
|
+
},
|
|
521
|
+
]));
|
|
443
522
|
}
|
|
444
523
|
else {
|
|
445
524
|
throw (0, errors_1.cliError)("INVALID_ARGUMENT", "migrate currently requires an interactive TTY to choose adoptable profiles and collect provider details.", {
|
|
446
|
-
|
|
447
|
-
|
|
525
|
+
availableProfiles: adoptability.availableProfiles,
|
|
526
|
+
adoptableProfiles: adoptability.adoptableProfiles,
|
|
527
|
+
blockingReasonsByProfile: adoptability.blockingReasonsByProfile,
|
|
448
528
|
suggestion: "Run `codexs migrate` in an interactive terminal. Non-interactive migrate flags for profile selection and provider secrets are not available in this release.",
|
|
449
529
|
});
|
|
450
530
|
}
|
|
@@ -483,3 +563,107 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
483
563
|
throw (0, errors_1.cliError)("UNKNOWN_COMMAND", `Unknown command: ${ctx.command}`);
|
|
484
564
|
}
|
|
485
565
|
}
|
|
566
|
+
/**
|
|
567
|
+
* Runs the deterministic Copilot onboarding preflight before any provider persistence.
|
|
568
|
+
*/
|
|
569
|
+
async function ensureCopilotReadyForAdd(args) {
|
|
570
|
+
let installCopilotSdk = args.installCopilotSdk;
|
|
571
|
+
const interactive = (0, interactive_1.canPrompt)(args.runtime, args.json);
|
|
572
|
+
if (interactive) {
|
|
573
|
+
args.runtime.writeLine("Checking Copilot SDK runtime...");
|
|
574
|
+
}
|
|
575
|
+
if (!(0, copilot_installer_1.probeCopilotSdkInstall)().installed) {
|
|
576
|
+
if (!interactive) {
|
|
577
|
+
if (!installCopilotSdk) {
|
|
578
|
+
const installStatus = (0, copilot_installer_1.probeCopilotSdkInstall)();
|
|
579
|
+
throw (0, errors_1.cliError)("COPILOT_SDK_INSTALL_REQUIRES_TTY", "The optional Copilot SDK runtime is not installed. Pass --install-copilot-sdk when running non-interactively.", {
|
|
580
|
+
installDir: installStatus.installDir,
|
|
581
|
+
packageName: installStatus.packageName,
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
(0, copilot_installer_1.installCopilotSdk)();
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
if (!installCopilotSdk) {
|
|
588
|
+
installCopilotSdk = await args.runtime.confirmAction("The optional Copilot SDK runtime is not installed. Install it now?");
|
|
589
|
+
}
|
|
590
|
+
if (!installCopilotSdk) {
|
|
591
|
+
const installStatus = (0, copilot_installer_1.probeCopilotSdkInstall)();
|
|
592
|
+
throw (0, errors_1.cliError)("COPILOT_SDK_MISSING", "The optional Copilot SDK runtime is not installed. Re-run with --install-copilot-sdk or confirm installation interactively.", {
|
|
593
|
+
installDir: installStatus.installDir,
|
|
594
|
+
packageName: installStatus.packageName,
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
if (interactive) {
|
|
598
|
+
args.runtime.writeLine("Installing Copilot SDK runtime...");
|
|
599
|
+
}
|
|
600
|
+
(0, copilot_installer_1.installCopilotSdk)();
|
|
601
|
+
if (interactive) {
|
|
602
|
+
args.runtime.writeLine("Copilot SDK runtime installed.");
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
if (interactive) {
|
|
607
|
+
args.runtime.writeLine("Checking GitHub Copilot login...");
|
|
608
|
+
}
|
|
609
|
+
try {
|
|
610
|
+
await (0, copilot_adapter_1.readCopilotAuthState)();
|
|
611
|
+
return installCopilotSdk;
|
|
612
|
+
}
|
|
613
|
+
catch (error) {
|
|
614
|
+
const normalized = (0, errors_1.normalizeError)(error);
|
|
615
|
+
if (normalized.code !== "COPILOT_AUTH_REQUIRED") {
|
|
616
|
+
throw error;
|
|
617
|
+
}
|
|
618
|
+
if (!interactive) {
|
|
619
|
+
throw (0, errors_1.cliError)("COPILOT_AUTH_REQUIRED", "Copilot authentication is required before the local bridge can be added.", {
|
|
620
|
+
...(normalized.details ?? {}),
|
|
621
|
+
manualLoginCommand: "copilot login",
|
|
622
|
+
suggestion: "Run `copilot login` manually or provide supported Copilot SDK credentials, then rerun add --copilot.",
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
args.runtime.writeLine("GitHub Copilot login is required. Starting official copilot login...");
|
|
626
|
+
let loginLaunchCause;
|
|
627
|
+
try {
|
|
628
|
+
const availability = (0, copilot_cli_1.checkCopilotCliAvailable)();
|
|
629
|
+
if (!availability.ok) {
|
|
630
|
+
throw new Error(availability.cause ?? "copilot CLI is unavailable");
|
|
631
|
+
}
|
|
632
|
+
(0, copilot_cli_1.runCopilotLogin)();
|
|
633
|
+
}
|
|
634
|
+
catch (launchError) {
|
|
635
|
+
loginLaunchCause = launchError instanceof Error ? launchError.message : String(launchError);
|
|
636
|
+
args.runtime.writeLine("Unable to launch the official Copilot login automatically.");
|
|
637
|
+
args.runtime.writeLine("Run this command in the current terminal: copilot login");
|
|
638
|
+
args.runtime.writeLine("GitHub's official device flow should open the browser or show the verification URL and code.");
|
|
639
|
+
}
|
|
640
|
+
args.runtime.writeLine("GitHub Copilot login completed. Rechecking session...");
|
|
641
|
+
const retry = await args.runtime.confirmAction("Recheck GitHub Copilot login now?", {
|
|
642
|
+
defaultValue: true,
|
|
643
|
+
});
|
|
644
|
+
if (!retry) {
|
|
645
|
+
throw (0, errors_1.cliError)("COPILOT_AUTH_REQUIRED", "Copilot authentication is required before the local bridge can be added.", {
|
|
646
|
+
...(normalized.details ?? {}),
|
|
647
|
+
manualLoginCommand: "copilot login",
|
|
648
|
+
loginLaunchCause,
|
|
649
|
+
suggestion: "Complete GitHub Copilot login with the official tooling, then rerun add --copilot.",
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
try {
|
|
653
|
+
await (0, copilot_adapter_1.readCopilotAuthState)();
|
|
654
|
+
return installCopilotSdk;
|
|
655
|
+
}
|
|
656
|
+
catch (recheckError) {
|
|
657
|
+
const rechecked = (0, errors_1.normalizeError)(recheckError);
|
|
658
|
+
if (rechecked.code !== "COPILOT_AUTH_REQUIRED") {
|
|
659
|
+
throw recheckError;
|
|
660
|
+
}
|
|
661
|
+
throw (0, errors_1.cliError)("COPILOT_AUTH_REQUIRED", "Copilot authentication is required before the local bridge can be added.", {
|
|
662
|
+
...(rechecked.details ?? {}),
|
|
663
|
+
manualLoginCommand: "copilot login",
|
|
664
|
+
loginLaunchCause,
|
|
665
|
+
suggestion: "Complete GitHub Copilot login with the official tooling, then rerun add --copilot.",
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
package/dist/commands/help.js
CHANGED
|
@@ -41,6 +41,48 @@ exports.COMMANDS = [
|
|
|
41
41
|
],
|
|
42
42
|
examples: ["codexs config list-profiles", "codexs config list-profiles --json"],
|
|
43
43
|
},
|
|
44
|
+
{
|
|
45
|
+
id: "bridge-start",
|
|
46
|
+
tokens: ["bridge", "start"],
|
|
47
|
+
handler: handlers_1.handleRegisteredCommand,
|
|
48
|
+
group: "write",
|
|
49
|
+
summary: "Start or reuse the managed Copilot bridge.",
|
|
50
|
+
usage: ["codexs bridge start [provider] [--json] [--codex-dir <path>]"],
|
|
51
|
+
details: [
|
|
52
|
+
"Resolves a Copilot bridge provider by explicit name, active provider, sole provider, or TTY selection.",
|
|
53
|
+
"Reuses a healthy bridge for the same provider and replaces a different managed provider when needed.",
|
|
54
|
+
"If the preferred port is occupied, automatically selects another free 5-digit port and persists it.",
|
|
55
|
+
],
|
|
56
|
+
examples: ["codexs bridge start", "codexs bridge start copilot-main"],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "bridge-stop",
|
|
60
|
+
tokens: ["bridge", "stop"],
|
|
61
|
+
handler: handlers_1.handleRegisteredCommand,
|
|
62
|
+
group: "recovery",
|
|
63
|
+
summary: "Stop the managed Copilot bridge.",
|
|
64
|
+
usage: ["codexs bridge stop [provider] [--json] [--codex-dir <path>]"],
|
|
65
|
+
details: [
|
|
66
|
+
"Prefers the runtime-state instance when present and uses an explicit provider as a guard.",
|
|
67
|
+
"Clears the runtime-state manifest without mutating providers.json or Codex auth state.",
|
|
68
|
+
"Is idempotent when no managed bridge is currently running.",
|
|
69
|
+
],
|
|
70
|
+
examples: ["codexs bridge stop", "codexs bridge stop copilot-main"],
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: "bridge-status",
|
|
74
|
+
tokens: ["bridge", "status"],
|
|
75
|
+
handler: handlers_1.handleRegisteredCommand,
|
|
76
|
+
group: "read",
|
|
77
|
+
summary: "Inspect the managed Copilot bridge.",
|
|
78
|
+
usage: ["codexs bridge status [provider] [--json] [--codex-dir <path>]"],
|
|
79
|
+
details: [
|
|
80
|
+
"Reports runtime-state, provider binding, and whether the live worker matches the expected provider.",
|
|
81
|
+
"Prefers the runtime-state instance when one is present.",
|
|
82
|
+
"Uses an explicit provider as a guard instead of silently switching targets.",
|
|
83
|
+
],
|
|
84
|
+
examples: ["codexs bridge status", "codexs bridge status copilot-main"],
|
|
85
|
+
},
|
|
44
86
|
{
|
|
45
87
|
id: "init",
|
|
46
88
|
tokens: ["init"],
|
|
@@ -66,7 +108,7 @@ exports.COMMANDS = [
|
|
|
66
108
|
details: [
|
|
67
109
|
"Reads config.toml profiles, collects complete provider records, then writes providers.json under managed backup flow.",
|
|
68
110
|
"TTY mode can collect missing provider details and choose merge or overwrite when providers.json already exists.",
|
|
69
|
-
"Migrate adopts only runtime profiles that already expose model, model_provider, matching base_url
|
|
111
|
+
"Migrate adopts only runtime profiles that already expose model, model_provider, and matching base_url.",
|
|
70
112
|
"Non-TTY and --json runs still fail fast because migrate profile selection and provider details remain interactive in this release.",
|
|
71
113
|
],
|
|
72
114
|
examples: ["codexs migrate", "codexs migrate --overwrite --json --codex-dir ~/.codex"],
|
|
@@ -173,6 +215,7 @@ exports.COMMANDS = [
|
|
|
173
215
|
"Automation and non-TTY environments must pass all required values explicitly.",
|
|
174
216
|
"Creating a missing profile section requires --create-profile together with --model and --base-url.",
|
|
175
217
|
"Use --copilot to create a GitHub Copilot bridge provider backed by the official SDK.",
|
|
218
|
+
"TTY add --copilot checks SDK install and GitHub Copilot login before it asks for Copilot provider fields.",
|
|
176
219
|
],
|
|
177
220
|
examples: [
|
|
178
221
|
"codexs add packycode --profile packycode --api-key sk-xxx",
|
|
@@ -185,14 +228,15 @@ exports.COMMANDS = [
|
|
|
185
228
|
tokens: ["switch"],
|
|
186
229
|
handler: handlers_1.handleRegisteredCommand,
|
|
187
230
|
group: "write",
|
|
188
|
-
summary: "Switch
|
|
231
|
+
summary: "Switch the active config profile to a provider.",
|
|
189
232
|
usage: ["codexs switch <provider> [--json] [--codex-dir <path>]"],
|
|
190
233
|
details: [
|
|
191
234
|
"When <provider> is omitted in a TTY, an interactive provider selector is shown.",
|
|
192
235
|
"When <provider> is passed explicitly, switch proceeds directly without extra confirmation.",
|
|
193
|
-
"
|
|
236
|
+
"Direct providers update the active config profile and rewrite auth.json with auth_mode=apikey plus OPENAI_API_KEY.",
|
|
237
|
+
"Copilot bridge providers still manage runtime routing and bridge state instead of rewriting OPENAI_API_KEY.",
|
|
194
238
|
"Copilot bridge providers probe the optional official SDK before switching and fail fast if it is missing.",
|
|
195
|
-
"Backs up config.toml and auth.json
|
|
239
|
+
"Backs up config.toml and auth.json and rolls back on failure.",
|
|
196
240
|
],
|
|
197
241
|
examples: ["codexs switch freemodel", "codexs switch packycode --json"],
|
|
198
242
|
},
|
package/dist/domain/config.js
CHANGED
|
@@ -41,7 +41,6 @@ exports.parseStructuredConfig = parseStructuredConfig;
|
|
|
41
41
|
exports.buildManagedProfileViews = buildManagedProfileViews;
|
|
42
42
|
exports.collectConfigConsistencyIssues = collectConfigConsistencyIssues;
|
|
43
43
|
exports.validateManagedProfileCreation = validateManagedProfileCreation;
|
|
44
|
-
exports.buildManagedProfileEnvKey = buildManagedProfileEnvKey;
|
|
45
44
|
exports.planProfileLifecycleOutcome = planProfileLifecycleOutcome;
|
|
46
45
|
exports.planConfigMutation = planConfigMutation;
|
|
47
46
|
exports.applyPatchOperations = applyPatchOperations;
|
|
@@ -120,8 +119,6 @@ function parseStructuredConfig(configContent) {
|
|
|
120
119
|
sectionEnd: configContent.length,
|
|
121
120
|
baseUrlValueRange: null,
|
|
122
121
|
baseUrl: null,
|
|
123
|
-
envKeyValueRange: null,
|
|
124
|
-
envKey: null,
|
|
125
122
|
};
|
|
126
123
|
modelProviders.push(currentModelProvider);
|
|
127
124
|
inRoot = false;
|
|
@@ -176,14 +173,6 @@ function parseStructuredConfig(configContent) {
|
|
|
176
173
|
end: line.start + baseUrlMatch.valueEnd,
|
|
177
174
|
};
|
|
178
175
|
}
|
|
179
|
-
const envKeyMatch = matchKeyValueLine(line.content, "env_key");
|
|
180
|
-
if (envKeyMatch) {
|
|
181
|
-
currentModelProvider.envKey = envKeyMatch.value;
|
|
182
|
-
currentModelProvider.envKeyValueRange = {
|
|
183
|
-
start: line.start + envKeyMatch.valueStart,
|
|
184
|
-
end: line.start + envKeyMatch.valueEnd,
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
176
|
}
|
|
188
177
|
}
|
|
189
178
|
return {
|
|
@@ -218,7 +207,6 @@ function buildManagedProfileViews(document, providers) {
|
|
|
218
207
|
model: section.model,
|
|
219
208
|
modelProvider: section.modelProvider,
|
|
220
209
|
baseUrl: modelProviderSection?.baseUrl ?? null,
|
|
221
|
-
envKey: modelProviderSection?.envKey ?? null,
|
|
222
210
|
managedFields: collectManagedFields(section.model, section.modelProvider),
|
|
223
211
|
source: linkInfo.managed ? "managed" : "unmanaged",
|
|
224
212
|
});
|
|
@@ -235,7 +223,6 @@ function buildManagedProfileViews(document, providers) {
|
|
|
235
223
|
model: null,
|
|
236
224
|
modelProvider: null,
|
|
237
225
|
baseUrl: null,
|
|
238
|
-
envKey: null,
|
|
239
226
|
managedFields: [],
|
|
240
227
|
source: "orphaned-reference",
|
|
241
228
|
});
|
|
@@ -298,28 +285,6 @@ function collectConfigConsistencyIssues(document, providers) {
|
|
|
298
285
|
modelProvider: view.modelProvider,
|
|
299
286
|
});
|
|
300
287
|
}
|
|
301
|
-
else if (!modelProviderSection.envKey) {
|
|
302
|
-
issues.push({
|
|
303
|
-
code: "MODEL_PROVIDER_ENV_KEY_MISSING",
|
|
304
|
-
profile: view.name,
|
|
305
|
-
modelProvider: view.modelProvider,
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
for (const providerName of view.linkedProviders) {
|
|
310
|
-
const provider = providers?.providers[providerName];
|
|
311
|
-
if (!provider) {
|
|
312
|
-
continue;
|
|
313
|
-
}
|
|
314
|
-
if (provider.envKey !== view.envKey) {
|
|
315
|
-
issues.push({
|
|
316
|
-
code: "PROVIDER_ENV_KEY_MISMATCH",
|
|
317
|
-
provider: providerName,
|
|
318
|
-
profile: view.name,
|
|
319
|
-
providerEnvKey: provider.envKey,
|
|
320
|
-
runtimeEnvKey: view.envKey,
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
288
|
}
|
|
324
289
|
}
|
|
325
290
|
}
|
|
@@ -366,17 +331,6 @@ function validateManagedProfileCreation(profile, fields) {
|
|
|
366
331
|
modelProvider,
|
|
367
332
|
};
|
|
368
333
|
}
|
|
369
|
-
/**
|
|
370
|
-
* Normalizes a profile name into the default env_key used for generated runtime sections.
|
|
371
|
-
*/
|
|
372
|
-
function buildManagedProfileEnvKey(profile) {
|
|
373
|
-
const normalized = profile
|
|
374
|
-
.trim()
|
|
375
|
-
.replace(/[^A-Za-z0-9]+/g, "_")
|
|
376
|
-
.replace(/^_+|_+$/g, "")
|
|
377
|
-
.toUpperCase();
|
|
378
|
-
return `${normalized || "PROVIDER"}_API_KEY`;
|
|
379
|
-
}
|
|
380
334
|
/**
|
|
381
335
|
* Computes keep/delete/switch outcomes when a provider leaves or changes profiles.
|
|
382
336
|
*/
|
|
@@ -491,14 +445,12 @@ function planConfigMutation(document, args) {
|
|
|
491
445
|
const section = modelProviderSectionMap.get(profileName);
|
|
492
446
|
if (!section) {
|
|
493
447
|
const baseUrl = fields.baseUrl?.trim() ?? "";
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
throw (0, errors_1.cliError)("MANAGED_PROFILE_FIELDS_MISSING", `Model provider "${profileName}" requires both base_url and env_key.`, {
|
|
448
|
+
if (!baseUrl) {
|
|
449
|
+
throw (0, errors_1.cliError)("MANAGED_PROFILE_FIELDS_MISSING", `Model provider "${profileName}" requires base_url.`, {
|
|
497
450
|
profile: profileName,
|
|
498
451
|
modelProvider: profileName,
|
|
499
452
|
missingFields: [
|
|
500
453
|
!baseUrl ? "base_url" : null,
|
|
501
|
-
!envKey ? "env_key" : null,
|
|
502
454
|
].filter((value) => Boolean(value)),
|
|
503
455
|
});
|
|
504
456
|
}
|
|
@@ -509,8 +461,7 @@ function planConfigMutation(document, args) {
|
|
|
509
461
|
kind: "insert-at",
|
|
510
462
|
index: document.rawText.length,
|
|
511
463
|
text: `${prefix}[model_providers.${profileName}]${document.lineEnding}` +
|
|
512
|
-
`base_url = ${JSON.stringify(baseUrl)}${document.lineEnding}
|
|
513
|
-
`env_key = ${JSON.stringify(envKey)}${document.lineEnding}`,
|
|
464
|
+
`base_url = ${JSON.stringify(baseUrl)}${document.lineEnding}`,
|
|
514
465
|
});
|
|
515
466
|
createdModelProviderSections.push(profileName);
|
|
516
467
|
continue;
|
|
@@ -594,12 +545,11 @@ function planSectionFieldMutation(document, section, fields, operations) {
|
|
|
594
545
|
return updated;
|
|
595
546
|
}
|
|
596
547
|
/**
|
|
597
|
-
* Plans base_url
|
|
548
|
+
* Plans base_url updates for one model_providers section.
|
|
598
549
|
*/
|
|
599
550
|
function planModelProviderFieldMutation(section, fields, operations) {
|
|
600
551
|
let updated = false;
|
|
601
552
|
const baseUrlText = fields.baseUrl !== undefined ? JSON.stringify(fields.baseUrl) : null;
|
|
602
|
-
const envKeyText = fields.envKey !== undefined ? JSON.stringify(fields.envKey) : null;
|
|
603
553
|
const inserts = [];
|
|
604
554
|
if (baseUrlText !== null && section.baseUrlValueRange) {
|
|
605
555
|
if (section.baseUrl !== fields.baseUrl) {
|
|
@@ -615,20 +565,6 @@ function planModelProviderFieldMutation(section, fields, operations) {
|
|
|
615
565
|
else if (baseUrlText !== null) {
|
|
616
566
|
inserts.push(`base_url = ${baseUrlText}`);
|
|
617
567
|
}
|
|
618
|
-
if (envKeyText !== null && section.envKeyValueRange) {
|
|
619
|
-
if (section.envKey !== fields.envKey) {
|
|
620
|
-
operations.push({
|
|
621
|
-
kind: "replace-range",
|
|
622
|
-
start: section.envKeyValueRange.start,
|
|
623
|
-
end: section.envKeyValueRange.end,
|
|
624
|
-
text: envKeyText,
|
|
625
|
-
});
|
|
626
|
-
updated = true;
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
else if (envKeyText !== null) {
|
|
630
|
-
inserts.push(`env_key = ${envKeyText}`);
|
|
631
|
-
}
|
|
632
568
|
if (inserts.length > 0) {
|
|
633
569
|
operations.push({
|
|
634
570
|
kind: "insert-at",
|
package/dist/domain/providers.js
CHANGED
|
@@ -32,9 +32,6 @@ function validateProvidersShape(input) {
|
|
|
32
32
|
if (typeof provider.apiKey !== "string" || provider.apiKey.trim() === "") {
|
|
33
33
|
throw new Error(`Provider "${name}" is missing a valid apiKey.`);
|
|
34
34
|
}
|
|
35
|
-
if (typeof provider.envKey !== "string" || provider.envKey.trim() === "") {
|
|
36
|
-
throw new Error(`Provider "${name}" is missing a valid envKey.`);
|
|
37
|
-
}
|
|
38
35
|
if (provider.baseUrl !== undefined && typeof provider.baseUrl !== "string") {
|
|
39
36
|
throw new Error(`Provider "${name}" has an invalid baseUrl.`);
|
|
40
37
|
}
|
|
@@ -56,7 +53,6 @@ function validateProvidersShape(input) {
|
|
|
56
53
|
providers[name] = cleanProviderRecord({
|
|
57
54
|
profile: provider.profile,
|
|
58
55
|
apiKey: provider.apiKey,
|
|
59
|
-
envKey: provider.envKey,
|
|
60
56
|
baseUrl: provider.baseUrl,
|
|
61
57
|
note: provider.note,
|
|
62
58
|
tags: provider.tags,
|
|
@@ -72,7 +68,6 @@ function cleanProviderRecord(record) {
|
|
|
72
68
|
const next = {
|
|
73
69
|
profile: record.profile.trim(),
|
|
74
70
|
apiKey: record.apiKey.trim(),
|
|
75
|
-
envKey: record.envKey.trim(),
|
|
76
71
|
};
|
|
77
72
|
if (record.baseUrl && record.baseUrl.trim() !== "") {
|
|
78
73
|
next.baseUrl = record.baseUrl.trim();
|
|
@@ -8,7 +8,8 @@ exports.inspectLiveStateDrift = inspectLiveStateDrift;
|
|
|
8
8
|
function getStorageRoles() {
|
|
9
9
|
return {
|
|
10
10
|
managementSSOT: "providers.json",
|
|
11
|
-
runtimeMirrors: ["config.toml"
|
|
11
|
+
runtimeMirrors: ["config.toml"],
|
|
12
|
+
authStateFile: "auth.json",
|
|
12
13
|
rollbackState: "backups/latest.json",
|
|
13
14
|
};
|
|
14
15
|
}
|