@minniexcode/codex-switch 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.AI.md +8 -3
  2. package/README.md +160 -91
  3. package/dist/app/add-provider.js +32 -1
  4. package/dist/app/edit-provider.js +137 -0
  5. package/dist/app/get-status.js +9 -2
  6. package/dist/app/import-providers.js +47 -2
  7. package/dist/app/list-backups.js +17 -0
  8. package/dist/app/list-config-profiles.js +29 -0
  9. package/dist/app/remove-provider.js +34 -2
  10. package/dist/app/rollback-backup.js +30 -0
  11. package/dist/app/run-doctor.js +22 -21
  12. package/dist/app/setup-codex.js +155 -0
  13. package/dist/app/show-config.js +34 -0
  14. package/dist/app/show-provider.js +22 -0
  15. package/dist/app/switch-provider.js +5 -2
  16. package/dist/cli/add-interactive.js +25 -31
  17. package/dist/cli/args.js +19 -5
  18. package/dist/cli/help.js +109 -14
  19. package/dist/cli/interactive.js +123 -8
  20. package/dist/cli/output.js +56 -1
  21. package/dist/cli/prompt.js +19 -2
  22. package/dist/cli.js +250 -13
  23. package/dist/domain/backups.js +103 -0
  24. package/dist/domain/config.js +471 -39
  25. package/dist/domain/errors.js +3 -3
  26. package/dist/domain/providers.js +10 -0
  27. package/dist/domain/setup.js +30 -0
  28. package/dist/infra/backup-repo.js +65 -6
  29. package/dist/infra/codex-cli.js +79 -2
  30. package/dist/infra/codex-discovery.js +10 -0
  31. package/dist/infra/codex-paths.js +14 -1
  32. package/dist/infra/config-repo.js +102 -9
  33. package/dist/infra/providers-repo.js +29 -0
  34. package/docs/Design/codex-switch-v0.0.4-design.md +874 -0
  35. package/docs/Design/codex-switch-v0.0.5-design.md +922 -0
  36. package/docs/PRD/codex-switch-prd-v0.0.5-to-v0.1.0.md +308 -0
  37. package/docs/PRD/codex-switch-prd-v0.1.0.md +343 -0
  38. package/docs/{codex-switch-prd.md → PRD/codex-switch-prd.md} +9 -5
  39. package/docs/cli-usage.md +580 -0
  40. package/docs/codex-switch-command-design.md +1 -1
  41. package/docs/codex-switch-product-overview.md +1 -1
  42. package/docs/codex-switch-product-research.md +2 -2
  43. package/docs/codex-switch-technical-architecture.md +1 -1
  44. package/package.json +1 -1
package/dist/cli/help.js CHANGED
@@ -9,6 +9,40 @@ const GROUP_TITLES = {
9
9
  recovery: "Diagnostics And Recovery",
10
10
  };
11
11
  const COMMANDS = [
12
+ {
13
+ name: "config-show",
14
+ group: "read",
15
+ summary: "Show the structured config profile view.",
16
+ usage: ["codexs config show [profile] [--json] [--codex-dir <path>]"],
17
+ details: [
18
+ "Returns all recognizable config profiles by default, including unmanaged and orphaned references.",
19
+ "Passing [profile] narrows the response to one profile while preserving the same shape.",
20
+ ],
21
+ examples: ["codexs config show", "codexs config show packycode --json"],
22
+ },
23
+ {
24
+ name: "config-list-profiles",
25
+ group: "read",
26
+ summary: "List recognizable config profiles with managed-state hints.",
27
+ usage: ["codexs config list-profiles [--json] [--codex-dir <path>]"],
28
+ details: [
29
+ "Lists managed, unmanaged, and orphaned config profiles in one stable view.",
30
+ "Use config show for richer single-profile details.",
31
+ ],
32
+ examples: ["codexs config list-profiles", "codexs config list-profiles --json"],
33
+ },
34
+ {
35
+ name: "setup",
36
+ group: "write",
37
+ summary: "Initialize providers.json from an existing Codex directory.",
38
+ usage: ["codexs setup [--json] [--codex-dir <path>] [--merge|--overwrite]"],
39
+ details: [
40
+ "Reads config.toml profiles, collects complete provider records, then writes providers.json under managed backup flow.",
41
+ "TTY mode can collect missing provider details and choose merge or overwrite when providers.json already exists.",
42
+ "Non-TTY and --json runs stay non-interactive and require explicit strategy when providers.json already exists.",
43
+ ],
44
+ examples: ["codexs setup", "codexs setup --overwrite --json --codex-dir ~/.codex"],
45
+ },
12
46
  {
13
47
  name: "list",
14
48
  group: "read",
@@ -20,6 +54,18 @@ const COMMANDS = [
20
54
  ],
21
55
  examples: ["codexs list", "codexs list --json"],
22
56
  },
57
+ {
58
+ name: "show",
59
+ group: "read",
60
+ summary: "Show one provider record from providers.json.",
61
+ usage: ["codexs show <provider> [--json] [--codex-dir <path>]"],
62
+ details: [
63
+ "Human-readable output masks apiKey by default.",
64
+ "TTY mode can select a missing provider interactively before showing its record.",
65
+ "JSON mode returns the full provider payload for local automation.",
66
+ ],
67
+ examples: ["codexs show packycode", "codexs show packycode --json"],
68
+ },
23
69
  {
24
70
  name: "current",
25
71
  group: "read",
@@ -38,23 +84,44 @@ const COMMANDS = [
38
84
  usage: ["codexs status [--json] [--codex-dir <path>]"],
39
85
  details: [
40
86
  "Reports file presence, current profile, and whether the live profile is mapped.",
87
+ "Surfaces config consistency signals without mutating any files.",
41
88
  "Use doctor for deeper diagnostics.",
42
89
  ],
43
90
  examples: ["codexs status", "codexs status --json --codex-dir ./.tmp-codex"],
44
91
  },
92
+ {
93
+ name: "edit",
94
+ group: "write",
95
+ summary: "Update fields on a single provider record.",
96
+ usage: [
97
+ "codexs edit <provider> [--profile <name>] [--api-key <key>] [--base-url <url>] [--note <text>] [--tag <tag> ...] [--json] [--codex-dir <path>]",
98
+ "codexs edit <provider> --profile <name> --create-profile --model <name> --base-url <url>",
99
+ ],
100
+ details: [
101
+ "Passed flags replace only the selected fields and keep the rest unchanged.",
102
+ "TTY mode can first select a provider, then prompt for fields when no editable options were provided.",
103
+ "Interactive tags use preset multi-select plus optional custom comma-separated input.",
104
+ "When rebinding to a missing profile, --create-profile requires both --model and --base-url.",
105
+ "Backs up providers.json and config.toml before writing.",
106
+ ],
107
+ examples: ["codexs edit packycode --note primary", "codexs edit packycode --tag daily --tag paid --json"],
108
+ },
45
109
  {
46
110
  name: "add",
47
111
  group: "write",
48
112
  summary: "Add a provider with explicit flags or progressive TTY prompts.",
49
113
  usage: [
50
114
  "codexs add <provider> --profile <name> --api-key <key> [--base-url <url>] [--note <text>] [--tag <tag> ...]",
115
+ "codexs add <provider> --profile <name> --api-key <key> --create-profile --model <name> --base-url <url>",
51
116
  "codexs add [--profile <name>] [--api-key <key>] [--base-url <url>] [--note <text>] [--tag <tag> ...]",
52
117
  ],
53
118
  details: [
54
119
  "Prompts only for missing required values when stdin/stdout are TTYs and --json is not set.",
55
- "Profile selection prefers existing config.toml profiles, then falls back to free-text input.",
120
+ "Interactive add collects provider name, profile, and apiKey progressively as plain text inputs.",
56
121
  "Confirm API key when prompted interactively because the hidden prompt asks twice before writing.",
122
+ "Interactive tags use preset multi-select plus optional custom comma-separated input.",
57
123
  "Automation and non-TTY environments must pass all required values explicitly.",
124
+ "Creating a missing profile section requires --create-profile together with --model and --base-url.",
58
125
  ],
59
126
  examples: [
60
127
  "codexs add packycode --profile packycode --api-key sk-xxx",
@@ -79,12 +146,13 @@ const COMMANDS = [
79
146
  name: "remove",
80
147
  group: "write",
81
148
  summary: "Remove a provider from providers.json.",
82
- usage: ["codexs remove <provider> [--force] [--json] [--codex-dir <path>]"],
149
+ usage: ["codexs remove <provider> [--force] [--switch-to <profile>] [--json] [--codex-dir <path>]"],
83
150
  details: [
84
151
  "TTY mode can select a missing provider interactively and always asks for deletion confirmation.",
85
152
  "Non-TTY and --json automation still require both <provider> and --force.",
86
153
  "The confirmation prompt includes the provider name and cancels without writing when declined.",
87
- "Backs up providers.json before removing the record.",
154
+ "When removing the last provider linked to the active profile, pass --switch-to first.",
155
+ "Backs up providers.json and config.toml before removing the record.",
88
156
  ],
89
157
  examples: ["codexs remove freemodel", "codexs remove freemodel --force --json"],
90
158
  },
@@ -95,10 +163,10 @@ const COMMANDS = [
95
163
  usage: ["codexs import <file> [--json] [--codex-dir <path>]"],
96
164
  details: [
97
165
  "The file path is always explicit; there is no path wizard in this release.",
98
- "TTY mode asks for confirmation before replacing the current providers registry.",
166
+ "TTY mode asks for confirmation before replacing or merging into the current providers registry.",
99
167
  "Non-TTY and --json runs stay non-interactive and validate the file before writing.",
100
168
  ],
101
- examples: ["codexs import ./providers.json", "codexs import ./providers.json --json"],
169
+ examples: ["codexs import ./providers.json", "codexs import ./providers.json --merge --json"],
102
170
  },
103
171
  {
104
172
  name: "export",
@@ -112,6 +180,17 @@ const COMMANDS = [
112
180
  ],
113
181
  examples: ["codexs export ./providers-backup.json", "codexs export ./providers-backup.json --force"],
114
182
  },
183
+ {
184
+ name: "backups",
185
+ group: "recovery",
186
+ summary: "List historical backup entries.",
187
+ usage: ["codexs backups list [--json] [--codex-dir <path>]"],
188
+ details: [
189
+ "Enumerates backups/ manifests and returns them newest first.",
190
+ "Corrupt backup manifests are skipped with warnings instead of failing the whole command.",
191
+ ],
192
+ examples: ["codexs backups list", "codexs backups list --json"],
193
+ },
115
194
  {
116
195
  name: "doctor",
117
196
  group: "recovery",
@@ -126,14 +205,14 @@ const COMMANDS = [
126
205
  {
127
206
  name: "rollback",
128
207
  group: "recovery",
129
- summary: "Restore the latest managed backup.",
130
- usage: ["codexs rollback [--json] [--codex-dir <path>]"],
208
+ summary: "Restore the latest managed backup or a specific backup id.",
209
+ usage: ["codexs rollback [<backup-id>] [--json] [--codex-dir <path>]"],
131
210
  details: [
132
- "TTY mode previews the latest backup path and affected files, then asks for confirmation.",
211
+ "TTY mode previews the target backup path and affected files, then asks for confirmation.",
133
212
  "Non-TTY and --json runs stay non-interactive and execute immediately.",
134
213
  "Use after a failed or undesired managed mutation.",
135
214
  ],
136
- examples: ["codexs rollback", "codexs rollback --json"],
215
+ examples: ["codexs rollback", "codexs rollback 20260511-221457-switch --json"],
137
216
  },
138
217
  ];
139
218
  const COMMAND_NAME_SET = new Set(COMMANDS.map((command) => command.name));
@@ -141,9 +220,18 @@ function getKnownCommandNames() {
141
220
  return COMMANDS.map((command) => command.name);
142
221
  }
143
222
  function isKnownCommandName(commandName) {
144
- return COMMAND_NAME_SET.has(commandName);
223
+ return COMMAND_NAME_SET.has(commandName) || commandName === "backups-list";
145
224
  }
146
225
  function buildHelpText(commandName) {
226
+ const normalizedCommandName = commandName === "backups-list"
227
+ ? "backups"
228
+ : commandName === "config-show"
229
+ ? "config-show"
230
+ : commandName === "config-list-profiles"
231
+ ? "config-list-profiles"
232
+ : commandName === "config"
233
+ ? "config-show"
234
+ : commandName;
147
235
  if (!commandName) {
148
236
  return [
149
237
  "codex-switch",
@@ -162,6 +250,10 @@ function buildHelpText(commandName) {
162
250
  " --help Show top-level or command-specific help.",
163
251
  " --version Print the current CLI version.",
164
252
  "",
253
+ "Environment:",
254
+ " CODEXS_CODEX_DIR Default Codex directory when --codex-dir is not passed.",
255
+ " NODE_ENV=development defaults to ./dev-codex/local-sandbox when no override is set.",
256
+ "",
165
257
  "Interactive rules:",
166
258
  " Progressive prompts only run in a real TTY and never run under --json.",
167
259
  " Human write commands may guide missing inputs or ask for dangerous-action confirmation.",
@@ -169,23 +261,26 @@ function buildHelpText(commandName) {
169
261
  "",
170
262
  "Dangerous commands:",
171
263
  " remove deletes provider records.",
172
- " import replaces providers.json.",
264
+ " import replaces or merges providers.json.",
173
265
  " export may overwrite a target file.",
174
- " rollback restores files from the latest backup.",
266
+ " rollback restores files from a managed backup.",
175
267
  "",
176
268
  "Examples:",
269
+ " codexs setup",
177
270
  " codexs list",
178
271
  " codexs switch",
179
272
  " codexs add packycode --profile packycode --api-key sk-xxx",
273
+ " codexs config show",
180
274
  " codexs remove freemodel",
275
+ " codexs backups list",
181
276
  " codexs rollback",
182
277
  " codexs help add",
183
278
  ].join("\n");
184
279
  }
185
- const command = COMMANDS.find((candidate) => candidate.name === commandName);
280
+ const command = COMMANDS.find((candidate) => candidate.name === normalizedCommandName);
186
281
  if (!command) {
187
282
  return [
188
- `Unknown help topic: ${commandName}`,
283
+ `Unknown help topic: ${normalizedCommandName}`,
189
284
  "",
190
285
  "Available commands:",
191
286
  ...getKnownCommandNames().map((name) => ` ${name}`),
@@ -40,12 +40,22 @@ exports.confirmImport = confirmImport;
40
40
  exports.confirmExportOverwrite = confirmExportOverwrite;
41
41
  exports.exportTargetExists = exportTargetExists;
42
42
  exports.getRollbackSummary = getRollbackSummary;
43
+ exports.getRollbackSummaryById = getRollbackSummaryById;
43
44
  exports.confirmRollback = confirmRollback;
45
+ exports.chooseSetupStrategy = chooseSetupStrategy;
46
+ exports.chooseCodexDir = chooseCodexDir;
47
+ exports.chooseSetupProfiles = chooseSetupProfiles;
48
+ exports.collectSetupProviderDetails = collectSetupProviderDetails;
49
+ exports.collectImportRepairDetails = collectImportRepairDetails;
50
+ exports.collectEditInput = collectEditInput;
44
51
  const fs = __importStar(require("node:fs"));
45
52
  const path = __importStar(require("node:path"));
46
53
  const errors_1 = require("../domain/errors");
54
+ const backups_1 = require("../domain/backups");
55
+ const codex_paths_1 = require("../infra/codex-paths");
47
56
  const providers_repo_1 = require("../infra/providers-repo");
48
57
  const backup_repo_1 = require("../infra/backup-repo");
58
+ const add_interactive_1 = require("./add-interactive");
49
59
  /**
50
60
  * Keeps CLI-side interactivity rules in one place so automation paths remain explicit.
51
61
  */
@@ -71,13 +81,15 @@ async function confirmProviderRemoval(runtime, providerName) {
71
81
  defaultValue: false,
72
82
  });
73
83
  if (!confirmed) {
74
- throw (0, errors_1.cliError)("INVALID_IMPORT_FILE", `Removal cancelled for provider "${providerName}".`);
84
+ throw (0, errors_1.cliError)("PROMPT_CANCELLED", `Removal cancelled for provider "${providerName}".`);
75
85
  }
76
86
  }
77
- async function confirmImport(runtime, sourceFile) {
78
- const confirmed = await runtime.confirmAction(`Import providers from ${path.resolve(sourceFile)} and replace the current registry?`, { defaultValue: false });
87
+ async function confirmImport(runtime, sourceFile, merge = false) {
88
+ const confirmed = await runtime.confirmAction(merge
89
+ ? `Import providers from ${path.resolve(sourceFile)} and merge into the current registry?`
90
+ : `Import providers from ${path.resolve(sourceFile)} and replace the current registry?`, { defaultValue: false });
79
91
  if (!confirmed) {
80
- throw (0, errors_1.cliError)("INVALID_IMPORT_FILE", "Import cancelled.");
92
+ throw (0, errors_1.cliError)("PROMPT_CANCELLED", "Import cancelled.");
81
93
  }
82
94
  }
83
95
  async function confirmExportOverwrite(runtime, targetFile) {
@@ -90,8 +102,16 @@ function exportTargetExists(targetFile) {
90
102
  }
91
103
  function getRollbackSummary(latestBackupPath) {
92
104
  const manifest = (0, backup_repo_1.loadLatestManifest)(latestBackupPath);
105
+ return buildRollbackSummary(manifest);
106
+ }
107
+ function getRollbackSummaryById(backupsDir, backupId) {
108
+ const manifest = (0, backup_repo_1.loadManifestById)(backupsDir, backupId);
109
+ return buildRollbackSummary(manifest);
110
+ }
111
+ function buildRollbackSummary(manifest) {
93
112
  const previewLines = [
94
113
  "Rollback preview",
114
+ `Backup ID: ${(0, backups_1.getBackupId)(manifest.backupDir)}`,
95
115
  `Backup: ${manifest.backupDir}`,
96
116
  ...manifest.files.map((file) => {
97
117
  const suffix = file.existed ? "restore" : "remove";
@@ -100,15 +120,110 @@ function getRollbackSummary(latestBackupPath) {
100
120
  ];
101
121
  return { manifest, previewLines };
102
122
  }
103
- async function confirmRollback(runtime, latestBackupPath) {
104
- const { previewLines } = getRollbackSummary(latestBackupPath);
123
+ async function confirmRollback(runtime, latestBackupPath, backupsDir, backupId) {
124
+ const { previewLines } = backupId && backupsDir
125
+ ? getRollbackSummaryById(backupsDir, backupId)
126
+ : getRollbackSummary(latestBackupPath);
105
127
  for (const line of previewLines) {
106
128
  runtime.writeLine(line);
107
129
  }
108
- const confirmed = await runtime.confirmAction("Restore files from the latest backup?", {
130
+ const confirmed = await runtime.confirmAction(backupId ? `Restore files from backup "${backupId}"?` : "Restore files from the latest backup?", {
109
131
  defaultValue: false,
110
132
  });
111
133
  if (!confirmed) {
112
- throw (0, errors_1.cliError)("INVALID_IMPORT_FILE", "Rollback cancelled.");
134
+ throw (0, errors_1.cliError)("PROMPT_CANCELLED", "Rollback cancelled.");
135
+ }
136
+ }
137
+ async function chooseSetupStrategy(runtime) {
138
+ return runtime.selectOne("providers.json already exists. Choose a setup strategy.", [
139
+ { value: "merge", label: "merge", hint: "keep existing providers and override by imported names" },
140
+ { value: "overwrite", label: "overwrite", hint: "replace the existing registry" },
141
+ { value: "cancel", label: "cancel", hint: "abort setup without writing" },
142
+ ]);
143
+ }
144
+ async function chooseCodexDir(runtime, candidates) {
145
+ if (candidates.length === 0) {
146
+ const manual = (await runtime.inputText("Codex directory path")).trim();
147
+ if (!manual) {
148
+ throw (0, errors_1.cliError)("CODEX_DIR_NOT_FOUND", "No Codex directory was provided.");
149
+ }
150
+ return (0, codex_paths_1.resolveCodexDir)(manual);
151
+ }
152
+ if (candidates.length === 1) {
153
+ return candidates[0];
154
+ }
155
+ const selected = await runtime.selectOne("Choose a Codex directory", [
156
+ ...candidates.map((candidate) => ({
157
+ value: candidate,
158
+ label: candidate,
159
+ })),
160
+ {
161
+ value: "__manual__",
162
+ label: "Enter manually",
163
+ },
164
+ ]);
165
+ if (selected !== "__manual__") {
166
+ return selected;
167
+ }
168
+ const manual = (await runtime.inputText("Codex directory path")).trim();
169
+ if (!manual) {
170
+ throw (0, errors_1.cliError)("CODEX_DIR_NOT_FOUND", "No Codex directory was provided.");
171
+ }
172
+ return (0, codex_paths_1.resolveCodexDir)(manual);
173
+ }
174
+ async function chooseSetupProfiles(runtime, profiles) {
175
+ if (profiles.length === 0) {
176
+ return [];
113
177
  }
178
+ return runtime.selectMany("Choose unmanaged config profiles to adopt into providers.json.", profiles.map((profile) => ({
179
+ value: profile.name,
180
+ label: profile.name,
181
+ hint: `${profile.model} | ${profile.baseUrl}`,
182
+ })));
183
+ }
184
+ async function collectSetupProviderDetails(runtime, profiles) {
185
+ const result = {};
186
+ for (const profile of profiles) {
187
+ const providerName = (await runtime.inputText(`Provider name for profile "${profile}"`, {
188
+ defaultValue: profile,
189
+ })).trim();
190
+ const apiKey = (await runtime.inputSecret(`API key for profile "${profile}"`)).trim();
191
+ const baseUrl = (await runtime.inputText(`Base URL for profile "${profile}" (optional)`)).trim();
192
+ const note = (await runtime.inputText(`Note for profile "${profile}" (optional)`)).trim();
193
+ const tags = await (0, add_interactive_1.promptTags)(runtime);
194
+ result[profile] = {
195
+ providerName: providerName || profile,
196
+ apiKey,
197
+ baseUrl: baseUrl || undefined,
198
+ note: note || undefined,
199
+ tags: tags.length > 0 ? tags : undefined,
200
+ };
201
+ }
202
+ return result;
203
+ }
204
+ async function collectImportRepairDetails(runtime, profiles) {
205
+ const repairs = {};
206
+ for (const profile of profiles) {
207
+ const model = (await runtime.inputText(`Model for missing profile "${profile}"`)).trim();
208
+ const baseUrl = (await runtime.inputText(`Base URL for missing profile "${profile}"`)).trim();
209
+ repairs[profile] = {
210
+ model: model || undefined,
211
+ baseUrl: baseUrl || undefined,
212
+ };
213
+ }
214
+ return repairs;
215
+ }
216
+ async function collectEditInput(runtime, current) {
217
+ const profile = (await runtime.inputText("Profile", { defaultValue: current.profile })).trim();
218
+ const apiKey = (await runtime.inputSecret("API key")).trim() || current.apiKey;
219
+ const baseUrl = (await runtime.inputText("Base URL (optional)", { defaultValue: current.baseUrl ?? "" })).trim();
220
+ const note = (await runtime.inputText("Note (optional)", { defaultValue: current.note ?? "" })).trim();
221
+ const tags = await (0, add_interactive_1.promptTags)(runtime, current.tags ?? []);
222
+ return {
223
+ profile,
224
+ apiKey,
225
+ baseUrl: baseUrl || undefined,
226
+ note: note || undefined,
227
+ tags,
228
+ };
114
229
  }
@@ -95,6 +95,22 @@ function renderHumanSuccess(command, data, warnings) {
95
95
  }
96
96
  break;
97
97
  }
98
+ case "show": {
99
+ const provider = data?.provider ?? {};
100
+ lines.push(`Provider: ${String(data?.providerName ?? "")}`);
101
+ lines.push(`profile: ${String(provider.profile ?? "")}`);
102
+ lines.push(`apiKey: ${String(provider.apiKey ?? "")}`);
103
+ if (provider.baseUrl) {
104
+ lines.push(`baseUrl: ${String(provider.baseUrl)}`);
105
+ }
106
+ if (provider.note) {
107
+ lines.push(`note: ${String(provider.note)}`);
108
+ }
109
+ if (Array.isArray(provider.tags) && provider.tags.length > 0) {
110
+ lines.push(`tags: ${provider.tags.join(", ")}`);
111
+ }
112
+ break;
113
+ }
98
114
  case "current":
99
115
  lines.push(`Current profile: ${String(data?.profile ?? "")}`);
100
116
  break;
@@ -104,23 +120,55 @@ function renderHumanSuccess(command, data, warnings) {
104
120
  lines.push(`providersExists: ${String(data?.providersExists ?? false)}`);
105
121
  lines.push(`currentProfile: ${String(data?.currentProfile ?? "")}`);
106
122
  lines.push(`mappedProvider: ${String(data?.provider ?? "")}`);
123
+ lines.push(`issues: ${Array.isArray(data?.issues) ? (data?.issues).length : 0}`);
124
+ break;
125
+ case "config-show": {
126
+ lines.push(`activeProfile: ${String(data?.activeProfile ?? "")}`);
127
+ const profiles = data?.profiles ?? [];
128
+ for (const profile of profiles) {
129
+ lines.push(`${String(profile.name)} managed=${String(profile.managed)} active=${String(profile.isActive)} source=${String(profile.source)} model=${String(profile.model ?? "")} baseUrl=${String(profile.baseUrl ?? "")}`);
130
+ }
131
+ break;
132
+ }
133
+ case "config-list-profiles": {
134
+ const profiles = data?.profiles ?? [];
135
+ for (const profile of profiles) {
136
+ lines.push(`${String(profile.name)} managed=${String(profile.managed)} active=${String(profile.isActive)} source=${String(profile.source)}`);
137
+ }
107
138
  break;
139
+ }
108
140
  case "switch":
109
141
  lines.push(`Switched to provider ${String(data?.provider ?? "")} using profile ${String(data?.profile ?? "")}.`);
110
142
  lines.push(`Backup: ${String(data?.backupPath ?? "")}`);
111
143
  lines.push(`Login performed: ${String(data?.loginPerformed ?? false)}`);
112
144
  break;
113
145
  case "import":
114
- lines.push(`Imported providers from file. Backup: ${String(data?.backupPath ?? "")}`);
146
+ lines.push(`Imported providers from file using mode ${String(data?.mode ?? "replace")}. Backup: ${String(data?.backupPath ?? "")}`);
115
147
  break;
116
148
  case "export":
117
149
  lines.push(`Exported providers to ${String(data?.exportedTo ?? "")}.`);
118
150
  break;
151
+ case "setup":
152
+ lines.push(`Initialized providers in ${String(data?.codexDir ?? "")} using ${String(data?.strategy ?? "")}.`);
153
+ lines.push(`Providers initialized: ${String(data?.providersInitialized ?? 0)}`);
154
+ lines.push(`Doctor healthy: ${String(data?.doctor?.healthy ?? false)}`);
155
+ lines.push(`Backup: ${String(data?.backupPath ?? "")}`);
156
+ break;
157
+ case "edit":
158
+ lines.push(`Updated provider ${String(data?.provider ?? "")}. Backup: ${String(data?.backupPath ?? "")}`);
159
+ lines.push(`Updated fields: ${Array.isArray(data?.updatedFields) ? (data?.updatedFields).join(", ") : ""}`);
160
+ break;
119
161
  case "add":
120
162
  lines.push(`Added provider ${String(data?.provider ?? "")}. Backup: ${String(data?.backupPath ?? "")}`);
163
+ if (Array.isArray(data?.createdProfileSections) && (data?.createdProfileSections).length > 0) {
164
+ lines.push(`Created profiles: ${(data?.createdProfileSections).join(", ")}`);
165
+ }
121
166
  break;
122
167
  case "remove":
123
168
  lines.push(`Removed provider ${String(data?.provider ?? "")}. Backup: ${String(data?.backupPath ?? "")}`);
169
+ if (Array.isArray(data?.deletedProfileSections) && (data?.deletedProfileSections).length > 0) {
170
+ lines.push(`Deleted profiles: ${(data?.deletedProfileSections).join(", ")}`);
171
+ }
124
172
  break;
125
173
  case "doctor": {
126
174
  const healthy = Boolean(data?.healthy);
@@ -131,6 +179,13 @@ function renderHumanSuccess(command, data, warnings) {
131
179
  }
132
180
  break;
133
181
  }
182
+ case "backups-list": {
183
+ const backups = data?.backups ?? [];
184
+ for (const backup of backups) {
185
+ lines.push(`${backup.backupId} ${backup.reason} ${backup.createdAt}`);
186
+ }
187
+ break;
188
+ }
134
189
  case "rollback":
135
190
  lines.push(`Rollback restored files from ${String(data?.backupPath ?? "")}.`);
136
191
  break;
@@ -54,6 +54,23 @@ function createPromptRuntime() {
54
54
  return answer.value;
55
55
  });
56
56
  },
57
+ selectMany: async (message, choices, options) => {
58
+ return handlePromptCancellation(async () => {
59
+ const answer = await inquirer_1.default.prompt([
60
+ {
61
+ type: "checkbox",
62
+ name: "value",
63
+ message,
64
+ choices: choices.map((choice) => ({
65
+ value: choice.value,
66
+ name: choice.hint ? `${choice.label} (${choice.hint})` : choice.label,
67
+ checked: Boolean(options?.defaultValues?.includes(choice.value)),
68
+ })),
69
+ },
70
+ ]);
71
+ return (Array.isArray(answer.value) ? answer.value : []);
72
+ });
73
+ },
57
74
  confirmAction: async (message, options) => {
58
75
  return handlePromptCancellation(async () => {
59
76
  const answer = await inquirer_1.default.prompt([
@@ -78,9 +95,9 @@ async function handlePromptCancellation(run) {
78
95
  }
79
96
  catch (error) {
80
97
  if (isPromptCancellation(error)) {
81
- throw (0, errors_1.cliError)("INVALID_IMPORT_FILE", "Interactive prompt was cancelled.");
98
+ throw (0, errors_1.cliError)("PROMPT_CANCELLED", "Interactive prompt was cancelled.");
82
99
  }
83
- throw (0, errors_1.cliError)("INVALID_IMPORT_FILE", "Interactive prompt failed.", {
100
+ throw (0, errors_1.cliError)("INVALID_ARGUMENT", "Interactive prompt failed.", {
84
101
  cause: error instanceof Error ? error.message : String(error),
85
102
  });
86
103
  }