@minniexcode/codex-switch 0.0.3 → 0.0.4

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.
@@ -1,29 +1,28 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.COMMON_TAG_CHOICES = void 0;
3
4
  exports.collectAddInput = collectAddInput;
4
5
  exports.createNonInteractiveAddError = createNonInteractiveAddError;
5
- const config_repo_1 = require("../infra/config-repo");
6
+ exports.promptTags = promptTags;
7
+ exports.parseTags = parseTags;
6
8
  const errors_1 = require("../domain/errors");
9
+ exports.COMMON_TAG_CHOICES = ["free", "paid", "daily", "backup"];
7
10
  /**
8
11
  * Collects add command inputs interactively when required values are missing.
9
12
  */
10
- async function collectAddInput(runtime, defaults, providerExists, configPath) {
13
+ async function collectAddInput(runtime, defaults, providerExists) {
11
14
  runtime.writeLine("Interactive add mode");
12
15
  runtime.writeLine("Provide the missing required fields. Press Enter to skip optional fields.");
13
16
  const providerName = defaults.providerName
14
17
  ? normalizeRequiredValue(defaults.providerName)
15
18
  : await promptProviderName(runtime, providerExists);
16
- const profile = defaults.profile
17
- ? normalizeRequiredValue(defaults.profile)
18
- : await promptProfile(runtime, configPath);
19
+ const profile = defaults.profile ? normalizeRequiredValue(defaults.profile) : await promptRequiredValue(runtime, "Profile");
19
20
  const apiKey = defaults.apiKey
20
21
  ? normalizeRequiredValue(defaults.apiKey)
21
22
  : await promptConfirmedSecret(runtime, "API key", "Confirm API key");
22
23
  const baseUrl = defaults.baseUrl ?? normalizeOptionalValue(await runtime.inputText("Base URL (optional)"));
23
24
  const note = defaults.note ?? normalizeOptionalValue(await runtime.inputText("Note (optional)"));
24
- const tags = defaults.tags.length > 0
25
- ? defaults.tags
26
- : parseTags(await runtime.inputText("Tags (optional, comma-separated)"));
25
+ const tags = defaults.tags.length > 0 ? defaults.tags : await promptTags(runtime);
27
26
  return {
28
27
  providerName,
29
28
  profile,
@@ -37,7 +36,7 @@ async function collectAddInput(runtime, defaults, providerExists, configPath) {
37
36
  * Throws a consistent error when interactive add is unavailable.
38
37
  */
39
38
  function createNonInteractiveAddError() {
40
- return (0, errors_1.cliError)("INVALID_IMPORT_FILE", "add requires <provider>, --profile, and --api-key when running without an interactive TTY.", {
39
+ return (0, errors_1.cliError)("INVALID_ARGUMENT", "add requires <provider>, --profile, and --api-key when running without an interactive TTY.", {
41
40
  suggestion: "Run in a terminal TTY or pass all required values explicitly.",
42
41
  });
43
42
  }
@@ -51,26 +50,6 @@ async function promptProviderName(runtime, providerExists) {
51
50
  return providerName;
52
51
  }
53
52
  }
54
- async function promptProfile(runtime, configPath) {
55
- const profileChoices = loadProfileChoices(configPath);
56
- if (profileChoices.length > 0) {
57
- return runtime.selectOne("Profile", profileChoices);
58
- }
59
- return promptRequiredValue(runtime, "Profile");
60
- }
61
- function loadProfileChoices(configPath) {
62
- try {
63
- return Array.from((0, config_repo_1.listConfigProfiles)(configPath))
64
- .sort()
65
- .map((profileName) => ({
66
- value: profileName,
67
- label: profileName,
68
- }));
69
- }
70
- catch {
71
- return [];
72
- }
73
- }
74
53
  async function promptRequiredValue(runtime, label) {
75
54
  while (true) {
76
55
  const value = normalizeRequiredValue(await runtime.inputText(label));
@@ -106,9 +85,24 @@ function normalizeOptionalValue(value) {
106
85
  const normalized = value.trim();
107
86
  return normalized === "" ? null : normalized;
108
87
  }
88
+ async function promptTags(runtime, defaults = []) {
89
+ const defaultPresetTags = defaults.filter(isCommonTag);
90
+ const defaultCustomTags = defaults.filter((tag) => !isCommonTag(tag));
91
+ const presetTags = await runtime.selectMany("Select tags (optional)", exports.COMMON_TAG_CHOICES.map((tag) => ({ value: tag, label: tag })), { defaultValues: defaultPresetTags });
92
+ const customTags = parseTags(await runtime.inputText("Custom tags (optional, comma-separated)", {
93
+ defaultValue: defaultCustomTags.join(", "),
94
+ }));
95
+ return dedupeTags([...presetTags, ...customTags]);
96
+ }
109
97
  function parseTags(value) {
110
- return value
98
+ return dedupeTags(value
111
99
  .split(",")
112
100
  .map((tag) => tag.trim())
113
- .filter((tag) => tag.length > 0);
101
+ .filter((tag) => tag.length > 0));
102
+ }
103
+ function isCommonTag(tag) {
104
+ return exports.COMMON_TAG_CHOICES.includes(tag);
105
+ }
106
+ function dedupeTags(tags) {
107
+ return Array.from(new Set(tags));
114
108
  }
package/dist/cli/args.js CHANGED
@@ -21,7 +21,7 @@ function parseArgs(argv) {
21
21
  if (value === "--codex-dir") {
22
22
  const next = argv[index + 1];
23
23
  if (!next) {
24
- throw (0, errors_1.cliError)("INVALID_IMPORT_FILE", "--codex-dir requires a path value.");
24
+ throw (0, errors_1.cliError)("INVALID_ARGUMENT", "--codex-dir requires a path value.");
25
25
  }
26
26
  codexDir = (0, codex_paths_1.resolveCodexDir)(next);
27
27
  index += 1;
@@ -30,6 +30,7 @@ function parseArgs(argv) {
30
30
  remaining.push(value);
31
31
  }
32
32
  if (remaining[0] === "help") {
33
+ const helpTarget = remaining[1] === "backups" && remaining[2] === "list" ? "backups" : remaining[1] ?? null;
33
34
  return {
34
35
  command: null,
35
36
  positionals: [],
@@ -39,7 +40,7 @@ function parseArgs(argv) {
39
40
  },
40
41
  commandOptions: new Map(),
41
42
  helpRequested: true,
42
- helpTarget: remaining[1] ?? null,
43
+ helpTarget,
43
44
  versionRequested: false,
44
45
  };
45
46
  }
@@ -51,11 +52,13 @@ function parseArgs(argv) {
51
52
  versionRequested: true,
52
53
  });
53
54
  }
54
- const command = remaining[0] ?? null;
55
+ const commandToken = remaining[0] ?? null;
56
+ const command = commandToken === "backups" && remaining[1] === "list" ? "backups-list" : commandToken;
55
57
  const positionals = [];
56
58
  const commandOptions = new Map();
57
59
  let helpRequested = false;
58
- for (let index = 1; index < remaining.length; index += 1) {
60
+ const startIndex = command === "backups-list" ? 2 : 1;
61
+ for (let index = startIndex; index < remaining.length; index += 1) {
59
62
  const value = remaining[index];
60
63
  if (value === "--help" || value === "-h") {
61
64
  helpRequested = true;
@@ -86,7 +89,7 @@ function parseArgs(argv) {
86
89
  },
87
90
  commandOptions,
88
91
  helpRequested,
89
- helpTarget: helpRequested ? command : null,
92
+ helpTarget: helpRequested ? (command === "backups-list" ? "backups" : command) : null,
90
93
  versionRequested: false,
91
94
  };
92
95
  }
package/dist/cli/help.js CHANGED
@@ -9,6 +9,18 @@ const GROUP_TITLES = {
9
9
  recovery: "Diagnostics And Recovery",
10
10
  };
11
11
  const COMMANDS = [
12
+ {
13
+ name: "setup",
14
+ group: "write",
15
+ summary: "Initialize providers.json from an existing Codex directory.",
16
+ usage: ["codexs setup [--json] [--codex-dir <path>] [--merge|--overwrite]"],
17
+ details: [
18
+ "Reads config.toml profiles, collects complete provider records, then writes providers.json under managed backup flow.",
19
+ "TTY mode can collect missing provider details and choose merge or overwrite when providers.json already exists.",
20
+ "Non-TTY and --json runs stay non-interactive and require explicit strategy when providers.json already exists.",
21
+ ],
22
+ examples: ["codexs setup", "codexs setup --overwrite --json --codex-dir ~/.codex"],
23
+ },
12
24
  {
13
25
  name: "list",
14
26
  group: "read",
@@ -20,6 +32,18 @@ const COMMANDS = [
20
32
  ],
21
33
  examples: ["codexs list", "codexs list --json"],
22
34
  },
35
+ {
36
+ name: "show",
37
+ group: "read",
38
+ summary: "Show one provider record from providers.json.",
39
+ usage: ["codexs show <provider> [--json] [--codex-dir <path>]"],
40
+ details: [
41
+ "Human-readable output masks apiKey by default.",
42
+ "TTY mode can select a missing provider interactively before showing its record.",
43
+ "JSON mode returns the full provider payload for local automation.",
44
+ ],
45
+ examples: ["codexs show packycode", "codexs show packycode --json"],
46
+ },
23
47
  {
24
48
  name: "current",
25
49
  group: "read",
@@ -42,6 +66,21 @@ const COMMANDS = [
42
66
  ],
43
67
  examples: ["codexs status", "codexs status --json --codex-dir ./.tmp-codex"],
44
68
  },
69
+ {
70
+ name: "edit",
71
+ group: "write",
72
+ summary: "Update fields on a single provider record.",
73
+ usage: [
74
+ "codexs edit <provider> [--profile <name>] [--api-key <key>] [--base-url <url>] [--note <text>] [--tag <tag> ...] [--json] [--codex-dir <path>]",
75
+ ],
76
+ details: [
77
+ "Passed flags replace only the selected fields and keep the rest unchanged.",
78
+ "TTY mode can first select a provider, then prompt for fields when no editable options were provided.",
79
+ "Interactive tags use preset multi-select plus optional custom comma-separated input.",
80
+ "Backs up providers.json before writing.",
81
+ ],
82
+ examples: ["codexs edit packycode --note primary", "codexs edit packycode --tag daily --tag paid --json"],
83
+ },
45
84
  {
46
85
  name: "add",
47
86
  group: "write",
@@ -52,8 +91,9 @@ const COMMANDS = [
52
91
  ],
53
92
  details: [
54
93
  "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.",
94
+ "Interactive add collects provider name, profile, and apiKey progressively as plain text inputs.",
56
95
  "Confirm API key when prompted interactively because the hidden prompt asks twice before writing.",
96
+ "Interactive tags use preset multi-select plus optional custom comma-separated input.",
57
97
  "Automation and non-TTY environments must pass all required values explicitly.",
58
98
  ],
59
99
  examples: [
@@ -95,10 +135,10 @@ const COMMANDS = [
95
135
  usage: ["codexs import <file> [--json] [--codex-dir <path>]"],
96
136
  details: [
97
137
  "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.",
138
+ "TTY mode asks for confirmation before replacing or merging into the current providers registry.",
99
139
  "Non-TTY and --json runs stay non-interactive and validate the file before writing.",
100
140
  ],
101
- examples: ["codexs import ./providers.json", "codexs import ./providers.json --json"],
141
+ examples: ["codexs import ./providers.json", "codexs import ./providers.json --merge --json"],
102
142
  },
103
143
  {
104
144
  name: "export",
@@ -112,6 +152,17 @@ const COMMANDS = [
112
152
  ],
113
153
  examples: ["codexs export ./providers-backup.json", "codexs export ./providers-backup.json --force"],
114
154
  },
155
+ {
156
+ name: "backups",
157
+ group: "recovery",
158
+ summary: "List historical backup entries.",
159
+ usage: ["codexs backups list [--json] [--codex-dir <path>]"],
160
+ details: [
161
+ "Enumerates backups/ manifests and returns them newest first.",
162
+ "Corrupt backup manifests are skipped with warnings instead of failing the whole command.",
163
+ ],
164
+ examples: ["codexs backups list", "codexs backups list --json"],
165
+ },
115
166
  {
116
167
  name: "doctor",
117
168
  group: "recovery",
@@ -126,14 +177,14 @@ const COMMANDS = [
126
177
  {
127
178
  name: "rollback",
128
179
  group: "recovery",
129
- summary: "Restore the latest managed backup.",
130
- usage: ["codexs rollback [--json] [--codex-dir <path>]"],
180
+ summary: "Restore the latest managed backup or a specific backup id.",
181
+ usage: ["codexs rollback [<backup-id>] [--json] [--codex-dir <path>]"],
131
182
  details: [
132
- "TTY mode previews the latest backup path and affected files, then asks for confirmation.",
183
+ "TTY mode previews the target backup path and affected files, then asks for confirmation.",
133
184
  "Non-TTY and --json runs stay non-interactive and execute immediately.",
134
185
  "Use after a failed or undesired managed mutation.",
135
186
  ],
136
- examples: ["codexs rollback", "codexs rollback --json"],
187
+ examples: ["codexs rollback", "codexs rollback 20260511-221457-switch --json"],
137
188
  },
138
189
  ];
139
190
  const COMMAND_NAME_SET = new Set(COMMANDS.map((command) => command.name));
@@ -141,9 +192,10 @@ function getKnownCommandNames() {
141
192
  return COMMANDS.map((command) => command.name);
142
193
  }
143
194
  function isKnownCommandName(commandName) {
144
- return COMMAND_NAME_SET.has(commandName);
195
+ return COMMAND_NAME_SET.has(commandName) || commandName === "backups-list";
145
196
  }
146
197
  function buildHelpText(commandName) {
198
+ const normalizedCommandName = commandName === "backups-list" ? "backups" : commandName;
147
199
  if (!commandName) {
148
200
  return [
149
201
  "codex-switch",
@@ -162,6 +214,10 @@ function buildHelpText(commandName) {
162
214
  " --help Show top-level or command-specific help.",
163
215
  " --version Print the current CLI version.",
164
216
  "",
217
+ "Environment:",
218
+ " CODEXS_CODEX_DIR Default Codex directory when --codex-dir is not passed.",
219
+ " NODE_ENV=development defaults to ./test-fixtures/sample-codex when no override is set.",
220
+ "",
165
221
  "Interactive rules:",
166
222
  " Progressive prompts only run in a real TTY and never run under --json.",
167
223
  " Human write commands may guide missing inputs or ask for dangerous-action confirmation.",
@@ -169,23 +225,25 @@ function buildHelpText(commandName) {
169
225
  "",
170
226
  "Dangerous commands:",
171
227
  " remove deletes provider records.",
172
- " import replaces providers.json.",
228
+ " import replaces or merges providers.json.",
173
229
  " export may overwrite a target file.",
174
- " rollback restores files from the latest backup.",
230
+ " rollback restores files from a managed backup.",
175
231
  "",
176
232
  "Examples:",
233
+ " codexs setup",
177
234
  " codexs list",
178
235
  " codexs switch",
179
236
  " codexs add packycode --profile packycode --api-key sk-xxx",
180
237
  " codexs remove freemodel",
238
+ " codexs backups list",
181
239
  " codexs rollback",
182
240
  " codexs help add",
183
241
  ].join("\n");
184
242
  }
185
- const command = COMMANDS.find((candidate) => candidate.name === commandName);
243
+ const command = COMMANDS.find((candidate) => candidate.name === normalizedCommandName);
186
244
  if (!command) {
187
245
  return [
188
- `Unknown help topic: ${commandName}`,
246
+ `Unknown help topic: ${normalizedCommandName}`,
189
247
  "",
190
248
  "Available commands:",
191
249
  ...getKnownCommandNames().map((name) => ` ${name}`),
@@ -40,12 +40,18 @@ 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.collectSetupProviderDetails = collectSetupProviderDetails;
47
+ exports.collectEditInput = collectEditInput;
44
48
  const fs = __importStar(require("node:fs"));
45
49
  const path = __importStar(require("node:path"));
46
50
  const errors_1 = require("../domain/errors");
51
+ const backups_1 = require("../domain/backups");
47
52
  const providers_repo_1 = require("../infra/providers-repo");
48
53
  const backup_repo_1 = require("../infra/backup-repo");
54
+ const add_interactive_1 = require("./add-interactive");
49
55
  /**
50
56
  * Keeps CLI-side interactivity rules in one place so automation paths remain explicit.
51
57
  */
@@ -71,13 +77,15 @@ async function confirmProviderRemoval(runtime, providerName) {
71
77
  defaultValue: false,
72
78
  });
73
79
  if (!confirmed) {
74
- throw (0, errors_1.cliError)("INVALID_IMPORT_FILE", `Removal cancelled for provider "${providerName}".`);
80
+ throw (0, errors_1.cliError)("PROMPT_CANCELLED", `Removal cancelled for provider "${providerName}".`);
75
81
  }
76
82
  }
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 });
83
+ async function confirmImport(runtime, sourceFile, merge = false) {
84
+ const confirmed = await runtime.confirmAction(merge
85
+ ? `Import providers from ${path.resolve(sourceFile)} and merge into the current registry?`
86
+ : `Import providers from ${path.resolve(sourceFile)} and replace the current registry?`, { defaultValue: false });
79
87
  if (!confirmed) {
80
- throw (0, errors_1.cliError)("INVALID_IMPORT_FILE", "Import cancelled.");
88
+ throw (0, errors_1.cliError)("PROMPT_CANCELLED", "Import cancelled.");
81
89
  }
82
90
  }
83
91
  async function confirmExportOverwrite(runtime, targetFile) {
@@ -90,8 +98,16 @@ function exportTargetExists(targetFile) {
90
98
  }
91
99
  function getRollbackSummary(latestBackupPath) {
92
100
  const manifest = (0, backup_repo_1.loadLatestManifest)(latestBackupPath);
101
+ return buildRollbackSummary(manifest);
102
+ }
103
+ function getRollbackSummaryById(backupsDir, backupId) {
104
+ const manifest = (0, backup_repo_1.loadManifestById)(backupsDir, backupId);
105
+ return buildRollbackSummary(manifest);
106
+ }
107
+ function buildRollbackSummary(manifest) {
93
108
  const previewLines = [
94
109
  "Rollback preview",
110
+ `Backup ID: ${(0, backups_1.getBackupId)(manifest.backupDir)}`,
95
111
  `Backup: ${manifest.backupDir}`,
96
112
  ...manifest.files.map((file) => {
97
113
  const suffix = file.existed ? "restore" : "remove";
@@ -100,15 +116,58 @@ function getRollbackSummary(latestBackupPath) {
100
116
  ];
101
117
  return { manifest, previewLines };
102
118
  }
103
- async function confirmRollback(runtime, latestBackupPath) {
104
- const { previewLines } = getRollbackSummary(latestBackupPath);
119
+ async function confirmRollback(runtime, latestBackupPath, backupsDir, backupId) {
120
+ const { previewLines } = backupId && backupsDir
121
+ ? getRollbackSummaryById(backupsDir, backupId)
122
+ : getRollbackSummary(latestBackupPath);
105
123
  for (const line of previewLines) {
106
124
  runtime.writeLine(line);
107
125
  }
108
- const confirmed = await runtime.confirmAction("Restore files from the latest backup?", {
126
+ const confirmed = await runtime.confirmAction(backupId ? `Restore files from backup "${backupId}"?` : "Restore files from the latest backup?", {
109
127
  defaultValue: false,
110
128
  });
111
129
  if (!confirmed) {
112
- throw (0, errors_1.cliError)("INVALID_IMPORT_FILE", "Rollback cancelled.");
130
+ throw (0, errors_1.cliError)("PROMPT_CANCELLED", "Rollback cancelled.");
131
+ }
132
+ }
133
+ async function chooseSetupStrategy(runtime) {
134
+ return runtime.selectOne("providers.json already exists. Choose a setup strategy.", [
135
+ { value: "merge", label: "merge", hint: "keep existing providers and override by imported names" },
136
+ { value: "overwrite", label: "overwrite", hint: "replace the existing registry" },
137
+ { value: "cancel", label: "cancel", hint: "abort setup without writing" },
138
+ ]);
139
+ }
140
+ async function collectSetupProviderDetails(runtime, profiles) {
141
+ const result = {};
142
+ for (const profile of profiles) {
143
+ const providerName = (await runtime.inputText(`Provider name for profile "${profile}"`, {
144
+ defaultValue: profile,
145
+ })).trim();
146
+ const apiKey = (await runtime.inputSecret(`API key for profile "${profile}"`)).trim();
147
+ const baseUrl = (await runtime.inputText(`Base URL for profile "${profile}" (optional)`)).trim();
148
+ const note = (await runtime.inputText(`Note for profile "${profile}" (optional)`)).trim();
149
+ const tags = await (0, add_interactive_1.promptTags)(runtime);
150
+ result[profile] = {
151
+ providerName: providerName || profile,
152
+ apiKey,
153
+ baseUrl: baseUrl || undefined,
154
+ note: note || undefined,
155
+ tags: tags.length > 0 ? tags : undefined,
156
+ };
113
157
  }
158
+ return result;
159
+ }
160
+ async function collectEditInput(runtime, current) {
161
+ const profile = (await runtime.inputText("Profile", { defaultValue: current.profile })).trim();
162
+ const apiKey = (await runtime.inputSecret("API key")).trim() || current.apiKey;
163
+ const baseUrl = (await runtime.inputText("Base URL (optional)", { defaultValue: current.baseUrl ?? "" })).trim();
164
+ const note = (await runtime.inputText("Note (optional)", { defaultValue: current.note ?? "" })).trim();
165
+ const tags = await (0, add_interactive_1.promptTags)(runtime, current.tags ?? []);
166
+ return {
167
+ profile,
168
+ apiKey,
169
+ baseUrl: baseUrl || undefined,
170
+ note: note || undefined,
171
+ tags,
172
+ };
114
173
  }
@@ -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;
@@ -111,11 +127,21 @@ function renderHumanSuccess(command, data, warnings) {
111
127
  lines.push(`Login performed: ${String(data?.loginPerformed ?? false)}`);
112
128
  break;
113
129
  case "import":
114
- lines.push(`Imported providers from file. Backup: ${String(data?.backupPath ?? "")}`);
130
+ lines.push(`Imported providers from file using mode ${String(data?.mode ?? "replace")}. Backup: ${String(data?.backupPath ?? "")}`);
115
131
  break;
116
132
  case "export":
117
133
  lines.push(`Exported providers to ${String(data?.exportedTo ?? "")}.`);
118
134
  break;
135
+ case "setup":
136
+ lines.push(`Initialized providers in ${String(data?.codexDir ?? "")} using ${String(data?.strategy ?? "")}.`);
137
+ lines.push(`Providers initialized: ${String(data?.providersInitialized ?? 0)}`);
138
+ lines.push(`Doctor healthy: ${String(data?.doctor?.healthy ?? false)}`);
139
+ lines.push(`Backup: ${String(data?.backupPath ?? "")}`);
140
+ break;
141
+ case "edit":
142
+ lines.push(`Updated provider ${String(data?.provider ?? "")}. Backup: ${String(data?.backupPath ?? "")}`);
143
+ lines.push(`Updated fields: ${Array.isArray(data?.updatedFields) ? (data?.updatedFields).join(", ") : ""}`);
144
+ break;
119
145
  case "add":
120
146
  lines.push(`Added provider ${String(data?.provider ?? "")}. Backup: ${String(data?.backupPath ?? "")}`);
121
147
  break;
@@ -131,6 +157,13 @@ function renderHumanSuccess(command, data, warnings) {
131
157
  }
132
158
  break;
133
159
  }
160
+ case "backups-list": {
161
+ const backups = data?.backups ?? [];
162
+ for (const backup of backups) {
163
+ lines.push(`${backup.backupId} ${backup.reason} ${backup.createdAt}`);
164
+ }
165
+ break;
166
+ }
134
167
  case "rollback":
135
168
  lines.push(`Rollback restored files from ${String(data?.backupPath ?? "")}.`);
136
169
  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
  }