@minniexcode/codex-switch 0.0.9 → 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 +0 -11
- package/dist/app/bridge.js +0 -1
- package/dist/app/edit-provider.js +1 -17
- package/dist/app/get-status.js +24 -9
- package/dist/app/list-providers.js +0 -1
- package/dist/app/run-doctor.js +11 -36
- package/dist/app/setup-codex.js +27 -17
- package/dist/app/show-config.js +1 -5
- package/dist/app/switch-provider.js +5 -20
- package/dist/cli/output.js +4 -6
- package/dist/cli.js +1 -1
- package/dist/commands/handlers.js +192 -39
- package/dist/commands/registry.js +7 -5
- 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.js +2 -2
- 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/PRD/codex-switch-prd-v0.0.10.md +406 -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
|
@@ -55,10 +55,13 @@ const show_provider_1 = require("../app/show-provider");
|
|
|
55
55
|
const switch_provider_1 = require("../app/switch-provider");
|
|
56
56
|
const config_1 = require("../domain/config");
|
|
57
57
|
const errors_1 = require("../domain/errors");
|
|
58
|
+
const setup_1 = require("../domain/setup");
|
|
58
59
|
const providers_1 = require("../domain/providers");
|
|
59
60
|
const add_interactive_1 = require("../interaction/add-interactive");
|
|
60
61
|
const interactive_1 = require("../interaction/interactive");
|
|
61
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");
|
|
62
65
|
const copilot_installer_1 = require("../runtime/copilot-installer");
|
|
63
66
|
const config_repo_1 = require("../storage/config-repo");
|
|
64
67
|
const codex_paths_1 = require("../storage/codex-paths");
|
|
@@ -257,40 +260,70 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
257
260
|
let tags = parsed.commandOptions.get("--tag") ?? [];
|
|
258
261
|
let createProfile = (0, args_1.hasFlag)(parsed.commandOptions, "--create-profile");
|
|
259
262
|
const copilot = (0, args_1.hasFlag)(parsed.commandOptions, "--copilot");
|
|
260
|
-
|
|
263
|
+
let bridgeHost = (0, args_1.getSingleOption)(parsed.commandOptions, "--bridge-host", false);
|
|
261
264
|
const bridgePortValue = (0, args_1.getSingleOption)(parsed.commandOptions, "--bridge-port", false);
|
|
262
|
-
|
|
265
|
+
let bridgeApiKey = (0, args_1.getSingleOption)(parsed.commandOptions, "--bridge-api-key", false);
|
|
263
266
|
let installCopilotSdk = (0, args_1.hasFlag)(parsed.commandOptions, "--install-copilot-sdk");
|
|
264
|
-
|
|
267
|
+
let bridgePort = bridgePortValue ? Number(bridgePortValue) : null;
|
|
265
268
|
if (copilot && apiKey) {
|
|
266
269
|
throw (0, errors_1.cliError)("INVALID_ARGUMENT", "--copilot does not allow --api-key. Use --bridge-api-key for the local bridge secret.");
|
|
267
270
|
}
|
|
268
271
|
if (bridgePortValue && (!Number.isInteger(bridgePort) || bridgePort === null || bridgePort <= 0)) {
|
|
269
272
|
throw (0, errors_1.cliError)("INVALID_ARGUMENT", "--bridge-port must be a positive integer.");
|
|
270
273
|
}
|
|
271
|
-
if (copilot
|
|
272
|
-
installCopilotSdk = await
|
|
274
|
+
if (copilot) {
|
|
275
|
+
installCopilotSdk = await ensureCopilotReadyForAdd({
|
|
276
|
+
runtime,
|
|
277
|
+
json: ctx.options.json,
|
|
278
|
+
installCopilotSdk,
|
|
279
|
+
});
|
|
273
280
|
}
|
|
274
281
|
if (!providerName || !profile || (!apiKey && !copilot)) {
|
|
275
282
|
if (ctx.options.json || !runtime.isInteractive()) {
|
|
276
|
-
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;
|
|
277
326
|
}
|
|
278
|
-
const prompted = await (0, add_interactive_1.collectAddInput)(runtime, {
|
|
279
|
-
providerName,
|
|
280
|
-
profile,
|
|
281
|
-
apiKey,
|
|
282
|
-
baseUrl,
|
|
283
|
-
note,
|
|
284
|
-
tags,
|
|
285
|
-
}, (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)));
|
|
286
|
-
providerName = prompted.providerName;
|
|
287
|
-
profile = prompted.profile;
|
|
288
|
-
apiKey = prompted.apiKey;
|
|
289
|
-
model = prompted.model ?? null;
|
|
290
|
-
baseUrl = prompted.baseUrl ?? null;
|
|
291
|
-
note = prompted.note ?? null;
|
|
292
|
-
tags = prompted.tags;
|
|
293
|
-
createProfile = createProfile || prompted.createProfile;
|
|
294
327
|
}
|
|
295
328
|
return (0, add_provider_1.addProvider)({
|
|
296
329
|
codexDir: paths.codexDir,
|
|
@@ -433,9 +466,25 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
433
466
|
if (overwrite && merge) {
|
|
434
467
|
throw (0, errors_1.cliError)("INVALID_ARGUMENT", "migrate does not allow both --merge and --overwrite.");
|
|
435
468
|
}
|
|
436
|
-
let strategy = overwrite ? "overwrite" : merge ? "merge" : null;
|
|
437
469
|
const providersExists = fs.existsSync(setupPaths.providersPath);
|
|
438
|
-
|
|
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) {
|
|
439
488
|
if (!(0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
|
|
440
489
|
throw (0, errors_1.cliError)("PROVIDERS_ALREADY_EXISTS", "providers.json already exists. Pass --merge or --overwrite.", {
|
|
441
490
|
file: setupPaths.providersPath,
|
|
@@ -447,35 +496,35 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
447
496
|
}
|
|
448
497
|
strategy = selected;
|
|
449
498
|
}
|
|
450
|
-
const
|
|
451
|
-
const adoptableProfiles = (0, config_1.buildManagedProfileViews)(document, null)
|
|
452
|
-
.filter((view) => view.source === "unmanaged" && view.model && view.modelProvider === view.name && view.baseUrl && view.envKey)
|
|
453
|
-
.map((view) => ({
|
|
454
|
-
name: view.name,
|
|
455
|
-
model: view.model,
|
|
456
|
-
baseUrl: view.baseUrl,
|
|
457
|
-
envKey: view.envKey,
|
|
458
|
-
}))
|
|
459
|
-
.sort((left, right) => left.name.localeCompare(right.name));
|
|
460
|
-
const selectedProfiles = Array.from((0, config_repo_1.listConfigProfiles)(setupPaths.configPath)).sort();
|
|
499
|
+
const adoptableProfiles = adoptability.adoptableProfileDetails;
|
|
461
500
|
let adoptProfiles = [];
|
|
462
501
|
let providerDetailsByProfile = {};
|
|
463
502
|
if ((0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
|
|
464
503
|
adoptProfiles = await (0, interactive_1.chooseSetupProfiles)(runtime, adoptableProfiles);
|
|
465
504
|
// Defaults are derived from config.toml so interactive setup only asks for missing provider metadata.
|
|
466
|
-
|
|
505
|
+
const collectedDetails = await (0, interactive_1.collectSetupProviderDetails)(runtime, adoptProfiles, adoptableProfiles.reduce((accumulator, profile) => {
|
|
467
506
|
accumulator[profile.name] = {
|
|
468
507
|
providerName: profile.name,
|
|
469
|
-
envKey: profile.envKey,
|
|
470
508
|
baseUrl: profile.baseUrl,
|
|
471
509
|
};
|
|
472
510
|
return accumulator;
|
|
473
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
|
+
]));
|
|
474
522
|
}
|
|
475
523
|
else {
|
|
476
524
|
throw (0, errors_1.cliError)("INVALID_ARGUMENT", "migrate currently requires an interactive TTY to choose adoptable profiles and collect provider details.", {
|
|
477
|
-
|
|
478
|
-
|
|
525
|
+
availableProfiles: adoptability.availableProfiles,
|
|
526
|
+
adoptableProfiles: adoptability.adoptableProfiles,
|
|
527
|
+
blockingReasonsByProfile: adoptability.blockingReasonsByProfile,
|
|
479
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.",
|
|
480
529
|
});
|
|
481
530
|
}
|
|
@@ -514,3 +563,107 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
514
563
|
throw (0, errors_1.cliError)("UNKNOWN_COMMAND", `Unknown command: ${ctx.command}`);
|
|
515
564
|
}
|
|
516
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
|
+
}
|
|
@@ -64,7 +64,7 @@ exports.COMMANDS = [
|
|
|
64
64
|
usage: ["codexs bridge stop [provider] [--json] [--codex-dir <path>]"],
|
|
65
65
|
details: [
|
|
66
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 auth.
|
|
67
|
+
"Clears the runtime-state manifest without mutating providers.json or Codex auth state.",
|
|
68
68
|
"Is idempotent when no managed bridge is currently running.",
|
|
69
69
|
],
|
|
70
70
|
examples: ["codexs bridge stop", "codexs bridge stop copilot-main"],
|
|
@@ -108,7 +108,7 @@ exports.COMMANDS = [
|
|
|
108
108
|
details: [
|
|
109
109
|
"Reads config.toml profiles, collects complete provider records, then writes providers.json under managed backup flow.",
|
|
110
110
|
"TTY mode can collect missing provider details and choose merge or overwrite when providers.json already exists.",
|
|
111
|
-
"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.",
|
|
112
112
|
"Non-TTY and --json runs still fail fast because migrate profile selection and provider details remain interactive in this release.",
|
|
113
113
|
],
|
|
114
114
|
examples: ["codexs migrate", "codexs migrate --overwrite --json --codex-dir ~/.codex"],
|
|
@@ -215,6 +215,7 @@ exports.COMMANDS = [
|
|
|
215
215
|
"Automation and non-TTY environments must pass all required values explicitly.",
|
|
216
216
|
"Creating a missing profile section requires --create-profile together with --model and --base-url.",
|
|
217
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.",
|
|
218
219
|
],
|
|
219
220
|
examples: [
|
|
220
221
|
"codexs add packycode --profile packycode --api-key sk-xxx",
|
|
@@ -227,14 +228,15 @@ exports.COMMANDS = [
|
|
|
227
228
|
tokens: ["switch"],
|
|
228
229
|
handler: handlers_1.handleRegisteredCommand,
|
|
229
230
|
group: "write",
|
|
230
|
-
summary: "Switch
|
|
231
|
+
summary: "Switch the active config profile to a provider.",
|
|
231
232
|
usage: ["codexs switch <provider> [--json] [--codex-dir <path>]"],
|
|
232
233
|
details: [
|
|
233
234
|
"When <provider> is omitted in a TTY, an interactive provider selector is shown.",
|
|
234
235
|
"When <provider> is passed explicitly, switch proceeds directly without extra confirmation.",
|
|
235
|
-
"
|
|
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.",
|
|
236
238
|
"Copilot bridge providers probe the optional official SDK before switching and fail fast if it is missing.",
|
|
237
|
-
"Backs up config.toml and auth.json
|
|
239
|
+
"Backs up config.toml and auth.json and rolls back on failure.",
|
|
238
240
|
],
|
|
239
241
|
examples: ["codexs switch freemodel", "codexs switch packycode --json"],
|
|
240
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
|
}
|
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
|
+
}
|