@minniexcode/codex-switch 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.AI.md +105 -0
  2. package/README.CN.md +160 -0
  3. package/README.md +102 -111
  4. package/dist/app/add-provider.js +45 -0
  5. package/dist/app/export-providers.js +62 -0
  6. package/dist/app/get-current-profile.js +14 -0
  7. package/dist/app/get-status.js +75 -0
  8. package/dist/app/import-providers.js +74 -0
  9. package/dist/app/list-providers.js +23 -0
  10. package/dist/app/remove-provider.js +31 -0
  11. package/dist/app/rollback-latest.js +26 -0
  12. package/dist/app/run-doctor.js +130 -0
  13. package/dist/app/run-mutation.js +63 -0
  14. package/dist/app/switch-provider.js +43 -0
  15. package/dist/app/types.js +2 -0
  16. package/dist/cli/add-interactive.js +114 -0
  17. package/dist/cli/args.js +125 -0
  18. package/dist/cli/help.js +220 -0
  19. package/dist/cli/interactive.js +114 -0
  20. package/dist/cli/output.js +156 -0
  21. package/dist/cli/prompt.js +93 -0
  22. package/dist/cli.js +215 -26
  23. package/dist/domain/backup.js +2 -0
  24. package/dist/domain/config.js +106 -0
  25. package/dist/domain/errors.js +36 -0
  26. package/dist/domain/providers.js +92 -0
  27. package/dist/domain/runtime-state.js +56 -0
  28. package/dist/infra/backup-repo.js +151 -0
  29. package/dist/infra/codex-cli.js +53 -0
  30. package/dist/infra/codex-paths.js +58 -0
  31. package/dist/infra/config-repo.js +56 -0
  32. package/dist/infra/fs-utils.js +97 -0
  33. package/dist/infra/lock-repo.js +99 -0
  34. package/dist/infra/providers-repo.js +69 -0
  35. package/docs/codex-switch-command-design.md +646 -0
  36. package/docs/codex-switch-prd.md +24 -3
  37. package/docs/codex-switch-product-overview.md +2 -0
  38. package/docs/codex-switch-technical-architecture.md +1042 -0
  39. package/package.json +7 -4
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createPromptRuntime = createPromptRuntime;
7
+ const inquirer_1 = __importDefault(require("inquirer"));
8
+ const errors_1 = require("../domain/errors");
9
+ /**
10
+ * Creates the default prompt runtime backed by inquirer on the current process TTY.
11
+ */
12
+ function createPromptRuntime() {
13
+ return {
14
+ isInteractive: () => Boolean(process.stdin.isTTY && process.stdout.isTTY),
15
+ inputText: async (message, options) => {
16
+ return handlePromptCancellation(async () => {
17
+ const answer = await inquirer_1.default.prompt([
18
+ {
19
+ type: "input",
20
+ name: "value",
21
+ message,
22
+ default: options?.defaultValue ?? undefined,
23
+ },
24
+ ]);
25
+ return String(answer.value ?? "");
26
+ });
27
+ },
28
+ inputSecret: async (message) => {
29
+ return handlePromptCancellation(async () => {
30
+ const answer = await inquirer_1.default.prompt([
31
+ {
32
+ type: "password",
33
+ name: "value",
34
+ message,
35
+ mask: "*",
36
+ },
37
+ ]);
38
+ return String(answer.value ?? "");
39
+ });
40
+ },
41
+ selectOne: async (message, choices) => {
42
+ return handlePromptCancellation(async () => {
43
+ const answer = await inquirer_1.default.prompt([
44
+ {
45
+ type: "select",
46
+ name: "value",
47
+ message,
48
+ choices: choices.map((choice) => ({
49
+ value: choice.value,
50
+ name: choice.hint ? `${choice.label} (${choice.hint})` : choice.label,
51
+ })),
52
+ },
53
+ ]);
54
+ return answer.value;
55
+ });
56
+ },
57
+ confirmAction: async (message, options) => {
58
+ return handlePromptCancellation(async () => {
59
+ const answer = await inquirer_1.default.prompt([
60
+ {
61
+ type: "confirm",
62
+ name: "value",
63
+ message,
64
+ default: options?.defaultValue ?? false,
65
+ },
66
+ ]);
67
+ return Boolean(answer.value);
68
+ });
69
+ },
70
+ writeLine: (message) => {
71
+ process.stdout.write(`${message}\n`);
72
+ },
73
+ };
74
+ }
75
+ async function handlePromptCancellation(run) {
76
+ try {
77
+ return await run();
78
+ }
79
+ catch (error) {
80
+ if (isPromptCancellation(error)) {
81
+ throw (0, errors_1.cliError)("INVALID_IMPORT_FILE", "Interactive prompt was cancelled.");
82
+ }
83
+ throw (0, errors_1.cliError)("INVALID_IMPORT_FILE", "Interactive prompt failed.", {
84
+ cause: error instanceof Error ? error.message : String(error),
85
+ });
86
+ }
87
+ }
88
+ function isPromptCancellation(error) {
89
+ if (!(error instanceof Error)) {
90
+ return false;
91
+ }
92
+ return error.name === "ExitPromptError" || error.message.includes("force closed");
93
+ }
package/dist/cli.js CHANGED
@@ -1,30 +1,219 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
- const args = process.argv.slice(2);
4
- const version = "0.0.1";
5
- const helpText = `codex-switch
6
-
7
- Bootstrap release for the future Codex provider/profile switching CLI.
8
-
9
- Usage:
10
- codexs
11
- codexs --help
12
- codexs --version
13
-
14
- Status:
15
- This package currently reserves the npm scope and exposes the planned CLI entrypoint.
16
- The full switching workflow is not implemented yet.
17
-
18
- Docs:
19
- https://github.com/minniexcode/codex-switch
20
- `;
21
- if (args.includes("--version") || args.includes("-v")) {
22
- console.log(version);
23
- process.exit(0);
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.printHelp = printHelp;
5
+ exports.printVersion = printVersion;
6
+ exports.executeCommand = executeCommand;
7
+ const add_provider_1 = require("./app/add-provider");
8
+ const export_providers_1 = require("./app/export-providers");
9
+ const get_current_profile_1 = require("./app/get-current-profile");
10
+ const get_status_1 = require("./app/get-status");
11
+ const import_providers_1 = require("./app/import-providers");
12
+ const list_providers_1 = require("./app/list-providers");
13
+ const remove_provider_1 = require("./app/remove-provider");
14
+ const rollback_latest_1 = require("./app/rollback-latest");
15
+ const run_doctor_1 = require("./app/run-doctor");
16
+ const switch_provider_1 = require("./app/switch-provider");
17
+ const errors_1 = require("./domain/errors");
18
+ const providers_repo_1 = require("./infra/providers-repo");
19
+ const codex_paths_1 = require("./infra/codex-paths");
20
+ const args_1 = require("./cli/args");
21
+ const add_interactive_1 = require("./cli/add-interactive");
22
+ const help_1 = require("./cli/help");
23
+ const interactive_1 = require("./cli/interactive");
24
+ const output_1 = require("./cli/output");
25
+ const prompt_1 = require("./cli/prompt");
26
+ const VERSION = "0.0.3";
27
+ /**
28
+ * Prints the command help text to stdout.
29
+ */
30
+ function printHelp(commandName) {
31
+ process.stdout.write(`${(0, help_1.buildHelpText)(commandName)}\n`);
24
32
  }
25
- if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
26
- console.log(helpText);
27
- process.exit(0);
33
+ /**
34
+ * Prints the current CLI version to stdout.
35
+ */
36
+ function printVersion() {
37
+ process.stdout.write(`${VERSION}\n`);
38
+ }
39
+ /**
40
+ * Parses arguments, dispatches the selected command, and renders the final output.
41
+ */
42
+ function main() {
43
+ const parsed = (0, args_1.parseArgs)(process.argv.slice(2));
44
+ if (parsed.versionRequested) {
45
+ printVersion();
46
+ process.exit(0);
47
+ }
48
+ if (parsed.helpRequested) {
49
+ if (parsed.helpTarget && !(0, help_1.isKnownCommandName)(parsed.helpTarget)) {
50
+ (0, output_1.outputFailure)({ command: "help", options: parsed.globalOptions }, (0, errors_1.cliError)("INVALID_IMPORT_FILE", `Unknown help topic: ${parsed.helpTarget}`, {
51
+ availableCommands: (0, help_1.buildHelpText)(parsed.helpTarget).split("\n").slice(2),
52
+ }));
53
+ return;
54
+ }
55
+ printHelp(parsed.helpTarget);
56
+ process.exit(0);
57
+ }
58
+ if (!parsed.command) {
59
+ printHelp();
60
+ process.exit(0);
61
+ }
62
+ const ctx = {
63
+ command: parsed.command,
64
+ options: parsed.globalOptions,
65
+ };
66
+ executeCommand(ctx, parsed)
67
+ .then((result) => {
68
+ (0, output_1.outputSuccess)(ctx, result);
69
+ })
70
+ .catch((error) => {
71
+ (0, output_1.outputFailure)(ctx, (0, errors_1.normalizeError)(error));
72
+ });
73
+ }
74
+ /**
75
+ * Dispatches a parsed CLI command into the application layer.
76
+ */
77
+ async function executeCommand(ctx, parsed, runtime = (0, prompt_1.createPromptRuntime)()) {
78
+ const paths = (0, codex_paths_1.createCodexPaths)(ctx.options.codexDir);
79
+ switch (ctx.command) {
80
+ case "list":
81
+ return (0, list_providers_1.listProviders)(paths.providersPath);
82
+ case "current":
83
+ return (0, get_current_profile_1.getCurrentProfile)(paths.configPath);
84
+ case "status":
85
+ return (0, get_status_1.getStatus)(paths.codexDir, paths.configPath, paths.providersPath);
86
+ case "switch": {
87
+ let providerName = parsed.positionals[0] ?? null;
88
+ if (!providerName && (0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
89
+ providerName = await (0, interactive_1.promptForProviderSelection)(runtime, paths.providersPath, "Choose a provider to switch to");
90
+ }
91
+ if (!providerName) {
92
+ throw (0, errors_1.cliError)("PROVIDER_NOT_FOUND", "Missing provider name for switch command.");
93
+ }
94
+ return (0, switch_provider_1.switchProvider)({
95
+ codexDir: paths.codexDir,
96
+ backupsDir: paths.backupsDir,
97
+ latestBackupPath: paths.latestBackupPath,
98
+ configPath: paths.configPath,
99
+ providersPath: paths.providersPath,
100
+ authPath: paths.authPath,
101
+ providerName,
102
+ noLogin: (0, args_1.hasFlag)(parsed.commandOptions, "--no-login"),
103
+ });
104
+ }
105
+ case "import": {
106
+ const sourceFile = parsed.positionals[0];
107
+ if (!sourceFile) {
108
+ throw (0, errors_1.cliError)("INVALID_IMPORT_FILE", "Missing import file path.");
109
+ }
110
+ if ((0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
111
+ await (0, interactive_1.confirmImport)(runtime, sourceFile);
112
+ }
113
+ return (0, import_providers_1.importProviders)({
114
+ codexDir: paths.codexDir,
115
+ backupsDir: paths.backupsDir,
116
+ latestBackupPath: paths.latestBackupPath,
117
+ providersPath: paths.providersPath,
118
+ sourceFile,
119
+ });
120
+ }
121
+ case "export": {
122
+ const targetFile = parsed.positionals[0];
123
+ if (!targetFile) {
124
+ throw (0, errors_1.cliError)("INVALID_IMPORT_FILE", "Missing export file path.");
125
+ }
126
+ let force = (0, args_1.hasFlag)(parsed.commandOptions, "--force");
127
+ if (!force && (0, interactive_1.canPrompt)(runtime, ctx.options.json) && (0, interactive_1.exportTargetExists)(targetFile)) {
128
+ const confirmed = await (0, interactive_1.confirmExportOverwrite)(runtime, targetFile);
129
+ if (!confirmed) {
130
+ throw (0, errors_1.cliError)("INVALID_IMPORT_FILE", "Export cancelled.");
131
+ }
132
+ force = true;
133
+ }
134
+ return (0, export_providers_1.exportProviders)({
135
+ providersPath: paths.providersPath,
136
+ targetFile,
137
+ force,
138
+ });
139
+ }
140
+ case "add": {
141
+ let providerName = parsed.positionals[0] ?? null;
142
+ let profile = (0, args_1.getSingleOption)(parsed.commandOptions, "--profile");
143
+ let apiKey = (0, args_1.getSingleOption)(parsed.commandOptions, "--api-key");
144
+ let baseUrl = (0, args_1.getSingleOption)(parsed.commandOptions, "--base-url", false);
145
+ let note = (0, args_1.getSingleOption)(parsed.commandOptions, "--note", false);
146
+ let tags = parsed.commandOptions.get("--tag") ?? [];
147
+ if (!providerName || !profile || !apiKey) {
148
+ if (ctx.options.json || !runtime.isInteractive()) {
149
+ throw (0, add_interactive_1.createNonInteractiveAddError)();
150
+ }
151
+ const prompted = await (0, add_interactive_1.collectAddInput)(runtime, {
152
+ providerName,
153
+ profile,
154
+ apiKey,
155
+ baseUrl,
156
+ note,
157
+ tags,
158
+ }, (candidate) => Boolean((0, providers_repo_1.readProvidersFileIfExists)(paths.providersPath).providers[candidate]), paths.configPath);
159
+ providerName = prompted.providerName;
160
+ profile = prompted.profile;
161
+ apiKey = prompted.apiKey;
162
+ baseUrl = prompted.baseUrl ?? null;
163
+ note = prompted.note ?? null;
164
+ tags = prompted.tags;
165
+ }
166
+ return (0, add_provider_1.addProvider)({
167
+ codexDir: paths.codexDir,
168
+ backupsDir: paths.backupsDir,
169
+ latestBackupPath: paths.latestBackupPath,
170
+ providersPath: paths.providersPath,
171
+ providerName,
172
+ profile,
173
+ apiKey,
174
+ baseUrl,
175
+ note,
176
+ tags,
177
+ });
178
+ }
179
+ case "remove": {
180
+ let providerName = parsed.positionals[0] ?? null;
181
+ const force = (0, args_1.hasFlag)(parsed.commandOptions, "--force");
182
+ if (!providerName && (0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
183
+ providerName = await (0, interactive_1.promptForProviderSelection)(runtime, paths.providersPath, "Choose a provider to remove");
184
+ }
185
+ if (!providerName) {
186
+ throw (0, errors_1.cliError)("PROVIDER_NOT_FOUND", "Missing provider name for remove command.");
187
+ }
188
+ if (!force && !(0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
189
+ throw (0, errors_1.cliError)("INVALID_IMPORT_FILE", "remove requires --force.");
190
+ }
191
+ if ((0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
192
+ await (0, interactive_1.confirmProviderRemoval)(runtime, providerName);
193
+ }
194
+ return (0, remove_provider_1.removeProvider)({
195
+ codexDir: paths.codexDir,
196
+ backupsDir: paths.backupsDir,
197
+ latestBackupPath: paths.latestBackupPath,
198
+ providersPath: paths.providersPath,
199
+ providerName,
200
+ });
201
+ }
202
+ case "doctor":
203
+ return (0, run_doctor_1.runDoctor)({
204
+ codexDir: paths.codexDir,
205
+ configPath: paths.configPath,
206
+ providersPath: paths.providersPath,
207
+ });
208
+ case "rollback":
209
+ if ((0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
210
+ await (0, interactive_1.confirmRollback)(runtime, paths.latestBackupPath);
211
+ }
212
+ return (0, rollback_latest_1.rollbackLatest)(paths.latestBackupPath);
213
+ default:
214
+ throw (0, errors_1.cliError)("INVALID_IMPORT_FILE", `Unknown command: ${ctx.command}`);
215
+ }
216
+ }
217
+ if (require.main === module) {
218
+ main();
28
219
  }
29
- console.error(`Command not implemented yet: ${args.join(" ")}\nRun "codexs --help" for the current bootstrap status.`);
30
- process.exit(1);
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.parseTopLevelProfile = parseTopLevelProfile;
37
+ exports.parseProfileNames = parseProfileNames;
38
+ exports.replaceTopLevelProfile = replaceTopLevelProfile;
39
+ const os = __importStar(require("node:os"));
40
+ /**
41
+ * Reads the active top-level profile from config.toml content.
42
+ */
43
+ function parseTopLevelProfile(configContent) {
44
+ let inRoot = true;
45
+ for (const line of configContent.split(/\r?\n/)) {
46
+ const trimmed = line.trim();
47
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
48
+ inRoot = false;
49
+ continue;
50
+ }
51
+ if (!inRoot || trimmed === "" || trimmed.startsWith("#")) {
52
+ continue;
53
+ }
54
+ const match = trimmed.match(/^profile\s*=\s*["']([^"']+)["']/);
55
+ if (match) {
56
+ return match[1];
57
+ }
58
+ }
59
+ return null;
60
+ }
61
+ /**
62
+ * Collects all named profile sections declared in config.toml content.
63
+ */
64
+ function parseProfileNames(configContent) {
65
+ const result = new Set();
66
+ for (const line of configContent.split(/\r?\n/)) {
67
+ const trimmed = line.trim();
68
+ const match = trimmed.match(/^\[profiles\.([^\]]+)\]$/);
69
+ if (match) {
70
+ result.add(match[1]);
71
+ }
72
+ }
73
+ return result;
74
+ }
75
+ /**
76
+ * Replaces or inserts the top-level profile assignment while preserving the rest of the file.
77
+ */
78
+ function replaceTopLevelProfile(configContent, profile) {
79
+ const lines = configContent.split(/\r?\n/);
80
+ let inRoot = true;
81
+ let replaced = false;
82
+ const nextLines = lines.map((line) => {
83
+ const trimmed = line.trim();
84
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
85
+ // Only the root section may contain the active `profile = ...` switch.
86
+ inRoot = false;
87
+ return line;
88
+ }
89
+ if (!replaced && inRoot && /^profile\s*=/.test(trimmed)) {
90
+ replaced = true;
91
+ return `profile = "${profile}"`;
92
+ }
93
+ return line;
94
+ });
95
+ if (!replaced) {
96
+ // When no root-level profile exists yet, insert it before the first section header.
97
+ const insertAt = nextLines.findIndex((line) => line.trim().startsWith("["));
98
+ if (insertAt === -1) {
99
+ nextLines.push(`profile = "${profile}"`);
100
+ }
101
+ else {
102
+ nextLines.splice(insertAt, 0, `profile = "${profile}"`);
103
+ }
104
+ }
105
+ return nextLines.join(os.EOL);
106
+ }
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.cliError = cliError;
4
+ exports.normalizeError = normalizeError;
5
+ /**
6
+ * Creates an Error instance enriched with a stable CLI error code and optional details.
7
+ */
8
+ function cliError(code, message, details) {
9
+ const error = new Error(message);
10
+ error.code = code;
11
+ error.details = details;
12
+ return error;
13
+ }
14
+ /**
15
+ * Normalizes unknown thrown values into the shared CLI error shape.
16
+ */
17
+ function normalizeError(error) {
18
+ if (error && typeof error === "object" && "code" in error && "message" in error) {
19
+ const candidate = error;
20
+ return {
21
+ code: candidate.code ?? "INVALID_IMPORT_FILE",
22
+ message: candidate.message ?? "Unknown error.",
23
+ details: candidate.details,
24
+ };
25
+ }
26
+ if (error instanceof Error) {
27
+ return {
28
+ code: "INVALID_IMPORT_FILE",
29
+ message: error.message,
30
+ };
31
+ }
32
+ return {
33
+ code: "INVALID_IMPORT_FILE",
34
+ message: String(error),
35
+ };
36
+ }
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateProvidersShape = validateProvidersShape;
4
+ exports.cleanProviderRecord = cleanProviderRecord;
5
+ exports.sortProviders = sortProviders;
6
+ exports.findProviderByProfile = findProviderByProfile;
7
+ /**
8
+ * Validates and normalizes unknown JSON into the providers.json domain model.
9
+ */
10
+ function validateProvidersShape(input) {
11
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
12
+ throw new Error("Root value must be an object.");
13
+ }
14
+ const providersValue = input.providers;
15
+ if (!providersValue || typeof providersValue !== "object" || Array.isArray(providersValue)) {
16
+ throw new Error('Missing or invalid "providers" object.');
17
+ }
18
+ const providers = {};
19
+ for (const [name, value] of Object.entries(providersValue)) {
20
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
21
+ throw new Error(`Provider "${name}" must be an object.`);
22
+ }
23
+ const provider = value;
24
+ if (typeof provider.profile !== "string" || provider.profile.trim() === "") {
25
+ throw new Error(`Provider "${name}" is missing a valid profile.`);
26
+ }
27
+ if (typeof provider.apiKey !== "string" || provider.apiKey.trim() === "") {
28
+ throw new Error(`Provider "${name}" is missing a valid apiKey.`);
29
+ }
30
+ if (provider.baseUrl !== undefined && typeof provider.baseUrl !== "string") {
31
+ throw new Error(`Provider "${name}" has an invalid baseUrl.`);
32
+ }
33
+ if (provider.note !== undefined && typeof provider.note !== "string") {
34
+ throw new Error(`Provider "${name}" has an invalid note.`);
35
+ }
36
+ if (provider.tags !== undefined &&
37
+ (!Array.isArray(provider.tags) || provider.tags.some((tag) => typeof tag !== "string"))) {
38
+ throw new Error(`Provider "${name}" has invalid tags.`);
39
+ }
40
+ // Normalize provider fields during validation so the persisted format stays clean.
41
+ providers[name] = cleanProviderRecord({
42
+ profile: provider.profile,
43
+ apiKey: provider.apiKey,
44
+ baseUrl: provider.baseUrl,
45
+ note: provider.note,
46
+ tags: provider.tags,
47
+ });
48
+ }
49
+ return { providers };
50
+ }
51
+ /**
52
+ * Trims optional fields and removes empty values from a provider record.
53
+ */
54
+ function cleanProviderRecord(record) {
55
+ const next = {
56
+ profile: record.profile.trim(),
57
+ apiKey: record.apiKey.trim(),
58
+ };
59
+ if (record.baseUrl && record.baseUrl.trim() !== "") {
60
+ next.baseUrl = record.baseUrl.trim();
61
+ }
62
+ if (record.note && record.note.trim() !== "") {
63
+ next.note = record.note.trim();
64
+ }
65
+ if (record.tags && record.tags.length > 0) {
66
+ next.tags = record.tags.map((tag) => tag.trim()).filter((tag) => tag.length > 0);
67
+ }
68
+ return next;
69
+ }
70
+ /**
71
+ * Returns a copy of the providers file with provider names sorted deterministically.
72
+ */
73
+ function sortProviders(providers) {
74
+ const orderedProviders = Object.keys(providers.providers)
75
+ .sort()
76
+ .reduce((accumulator, key) => {
77
+ accumulator[key] = providers.providers[key];
78
+ return accumulator;
79
+ }, {});
80
+ return { providers: orderedProviders };
81
+ }
82
+ /**
83
+ * Finds the provider name associated with a given Codex profile.
84
+ */
85
+ function findProviderByProfile(providers, profile) {
86
+ for (const [name, provider] of Object.entries(providers.providers)) {
87
+ if (provider.profile === profile) {
88
+ return name;
89
+ }
90
+ }
91
+ return null;
92
+ }
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getStorageRoles = getStorageRoles;
4
+ exports.inspectLiveStateDrift = inspectLiveStateDrift;
5
+ /**
6
+ * Returns the stable storage contract used by the CLI.
7
+ */
8
+ function getStorageRoles() {
9
+ return {
10
+ managementSSOT: "providers.json",
11
+ runtimeMirrors: ["config.toml", "auth.json"],
12
+ rollbackState: "backups/latest.json",
13
+ };
14
+ }
15
+ /**
16
+ * Compares the live active profile against managed providers to detect drift.
17
+ */
18
+ function inspectLiveStateDrift(currentProfile, providers) {
19
+ if (currentProfile === null) {
20
+ return {
21
+ currentProfile,
22
+ mappedProvider: null,
23
+ profileMapped: false,
24
+ canBackfillActiveProvider: false,
25
+ reason: providers ? "profile-missing" : "config-missing",
26
+ };
27
+ }
28
+ if (!providers) {
29
+ return {
30
+ currentProfile,
31
+ mappedProvider: null,
32
+ profileMapped: false,
33
+ canBackfillActiveProvider: false,
34
+ reason: "providers-missing",
35
+ };
36
+ }
37
+ for (const [name, provider] of Object.entries(providers.providers)) {
38
+ // A direct profile match means the runtime state is still managed.
39
+ if (provider.profile === currentProfile) {
40
+ return {
41
+ currentProfile,
42
+ mappedProvider: name,
43
+ profileMapped: true,
44
+ canBackfillActiveProvider: false,
45
+ reason: "ok",
46
+ };
47
+ }
48
+ }
49
+ return {
50
+ currentProfile,
51
+ mappedProvider: null,
52
+ profileMapped: false,
53
+ canBackfillActiveProvider: true,
54
+ reason: "provider-unmapped",
55
+ };
56
+ }