@minniexcode/codex-switch 0.0.2 → 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.
package/dist/cli.js CHANGED
@@ -1,51 +1,73 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
3
36
  Object.defineProperty(exports, "__esModule", { value: true });
4
37
  exports.printHelp = printHelp;
5
38
  exports.printVersion = printVersion;
6
39
  exports.executeCommand = executeCommand;
40
+ const fs = __importStar(require("node:fs"));
7
41
  const add_provider_1 = require("./app/add-provider");
42
+ const edit_provider_1 = require("./app/edit-provider");
8
43
  const export_providers_1 = require("./app/export-providers");
9
44
  const get_current_profile_1 = require("./app/get-current-profile");
10
45
  const get_status_1 = require("./app/get-status");
11
46
  const import_providers_1 = require("./app/import-providers");
47
+ const list_backups_1 = require("./app/list-backups");
12
48
  const list_providers_1 = require("./app/list-providers");
13
49
  const remove_provider_1 = require("./app/remove-provider");
14
- const rollback_latest_1 = require("./app/rollback-latest");
50
+ const rollback_backup_1 = require("./app/rollback-backup");
15
51
  const run_doctor_1 = require("./app/run-doctor");
52
+ const setup_codex_1 = require("./app/setup-codex");
53
+ const show_provider_1 = require("./app/show-provider");
16
54
  const switch_provider_1 = require("./app/switch-provider");
17
55
  const errors_1 = require("./domain/errors");
56
+ const config_repo_1 = require("./infra/config-repo");
57
+ const providers_repo_1 = require("./infra/providers-repo");
18
58
  const codex_paths_1 = require("./infra/codex-paths");
19
59
  const args_1 = require("./cli/args");
60
+ const add_interactive_1 = require("./cli/add-interactive");
61
+ const help_1 = require("./cli/help");
62
+ const interactive_1 = require("./cli/interactive");
20
63
  const output_1 = require("./cli/output");
21
- const VERSION = "0.0.2";
22
- const HELP_TEXT = `codex-switch
23
-
24
- Usage:
25
- codexs <command> [options]
26
-
27
- Commands:
28
- codexs list
29
- codexs current
30
- codexs switch <provider> [--no-login]
31
- codexs status
32
- codexs import <file>
33
- codexs export <file> [--force]
34
- codexs add <provider> --profile <name> --api-key <key> [--base-url <url>] [--note <text>] [--tag <tag> ...]
35
- codexs remove <provider> --force
36
- codexs doctor
37
- codexs rollback
38
-
39
- Global options:
40
- --json
41
- --codex-dir <path>
42
- --help
43
- --version`;
64
+ const prompt_1 = require("./cli/prompt");
65
+ const VERSION = "0.0.4";
44
66
  /**
45
67
  * Prints the command help text to stdout.
46
68
  */
47
- function printHelp() {
48
- process.stdout.write(`${HELP_TEXT}\n`);
69
+ function printHelp(commandName) {
70
+ process.stdout.write(`${(0, help_1.buildHelpText)(commandName)}\n`);
49
71
  }
50
72
  /**
51
73
  * Prints the current CLI version to stdout.
@@ -58,16 +80,22 @@ function printVersion() {
58
80
  */
59
81
  function main() {
60
82
  const parsed = (0, args_1.parseArgs)(process.argv.slice(2));
61
- if (!parsed.command) {
62
- printHelp();
83
+ if (parsed.versionRequested) {
84
+ printVersion();
63
85
  process.exit(0);
64
86
  }
65
- if (parsed.command === "help") {
66
- printHelp();
87
+ if (parsed.helpRequested) {
88
+ if (parsed.helpTarget && !(0, help_1.isKnownCommandName)(parsed.helpTarget)) {
89
+ (0, output_1.outputFailure)({ command: "help", options: parsed.globalOptions }, (0, errors_1.cliError)("INVALID_ARGUMENT", `Unknown help topic: ${parsed.helpTarget}`, {
90
+ availableCommands: (0, help_1.buildHelpText)(parsed.helpTarget).split("\n").slice(2),
91
+ }));
92
+ return;
93
+ }
94
+ printHelp(parsed.helpTarget);
67
95
  process.exit(0);
68
96
  }
69
- if (parsed.command === "version") {
70
- printVersion();
97
+ if (!parsed.command) {
98
+ printHelp();
71
99
  process.exit(0);
72
100
  }
73
101
  const ctx = {
@@ -85,17 +113,34 @@ function main() {
85
113
  /**
86
114
  * Dispatches a parsed CLI command into the application layer.
87
115
  */
88
- async function executeCommand(ctx, parsed) {
116
+ async function executeCommand(ctx, parsed, runtime = (0, prompt_1.createPromptRuntime)()) {
89
117
  const paths = (0, codex_paths_1.createCodexPaths)(ctx.options.codexDir);
90
118
  switch (ctx.command) {
91
119
  case "list":
92
120
  return (0, list_providers_1.listProviders)(paths.providersPath);
121
+ case "show": {
122
+ let providerName = parsed.positionals[0] ?? null;
123
+ if (!providerName && (0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
124
+ providerName = await (0, interactive_1.promptForProviderSelection)(runtime, paths.providersPath, "Choose a provider to show");
125
+ }
126
+ if (!providerName) {
127
+ throw (0, errors_1.cliError)("INVALID_ARGUMENT", "Missing provider name for show command.");
128
+ }
129
+ return (0, show_provider_1.showProvider)({
130
+ providersPath: paths.providersPath,
131
+ providerName,
132
+ includeSecret: ctx.options.json,
133
+ });
134
+ }
93
135
  case "current":
94
136
  return (0, get_current_profile_1.getCurrentProfile)(paths.configPath);
95
137
  case "status":
96
138
  return (0, get_status_1.getStatus)(paths.codexDir, paths.configPath, paths.providersPath);
97
139
  case "switch": {
98
- const providerName = parsed.positionals[0];
140
+ let providerName = parsed.positionals[0] ?? null;
141
+ if (!providerName && (0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
142
+ providerName = await (0, interactive_1.promptForProviderSelection)(runtime, paths.providersPath, "Choose a provider to switch to");
143
+ }
99
144
  if (!providerName) {
100
145
  throw (0, errors_1.cliError)("PROVIDER_NOT_FOUND", "Missing provider name for switch command.");
101
146
  }
@@ -113,7 +158,11 @@ async function executeCommand(ctx, parsed) {
113
158
  case "import": {
114
159
  const sourceFile = parsed.positionals[0];
115
160
  if (!sourceFile) {
116
- throw (0, errors_1.cliError)("INVALID_IMPORT_FILE", "Missing import file path.");
161
+ throw (0, errors_1.cliError)("INVALID_ARGUMENT", "Missing import file path.");
162
+ }
163
+ const merge = (0, args_1.hasFlag)(parsed.commandOptions, "--merge");
164
+ if ((0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
165
+ await (0, interactive_1.confirmImport)(runtime, sourceFile, merge);
117
166
  }
118
167
  return (0, import_providers_1.importProviders)({
119
168
  codexDir: paths.codexDir,
@@ -121,30 +170,103 @@ async function executeCommand(ctx, parsed) {
121
170
  latestBackupPath: paths.latestBackupPath,
122
171
  providersPath: paths.providersPath,
123
172
  sourceFile,
173
+ merge,
124
174
  });
125
175
  }
126
176
  case "export": {
127
177
  const targetFile = parsed.positionals[0];
128
178
  if (!targetFile) {
129
- throw (0, errors_1.cliError)("INVALID_IMPORT_FILE", "Missing export file path.");
179
+ throw (0, errors_1.cliError)("INVALID_ARGUMENT", "Missing export file path.");
180
+ }
181
+ let force = (0, args_1.hasFlag)(parsed.commandOptions, "--force");
182
+ if (!force && (0, interactive_1.canPrompt)(runtime, ctx.options.json) && (0, interactive_1.exportTargetExists)(targetFile)) {
183
+ const confirmed = await (0, interactive_1.confirmExportOverwrite)(runtime, targetFile);
184
+ if (!confirmed) {
185
+ throw (0, errors_1.cliError)("PROMPT_CANCELLED", "Export cancelled.");
186
+ }
187
+ force = true;
130
188
  }
131
189
  return (0, export_providers_1.exportProviders)({
132
190
  providersPath: paths.providersPath,
133
191
  targetFile,
134
- force: (0, args_1.hasFlag)(parsed.commandOptions, "--force"),
192
+ force,
135
193
  });
136
194
  }
137
195
  case "add": {
138
- const providerName = parsed.positionals[0];
196
+ let providerName = parsed.positionals[0] ?? null;
197
+ let profile = (0, args_1.getSingleOption)(parsed.commandOptions, "--profile");
198
+ let apiKey = (0, args_1.getSingleOption)(parsed.commandOptions, "--api-key");
199
+ let baseUrl = (0, args_1.getSingleOption)(parsed.commandOptions, "--base-url", false);
200
+ let note = (0, args_1.getSingleOption)(parsed.commandOptions, "--note", false);
201
+ let tags = parsed.commandOptions.get("--tag") ?? [];
202
+ if (!providerName || !profile || !apiKey) {
203
+ if (ctx.options.json || !runtime.isInteractive()) {
204
+ throw (0, add_interactive_1.createNonInteractiveAddError)();
205
+ }
206
+ const prompted = await (0, add_interactive_1.collectAddInput)(runtime, {
207
+ providerName,
208
+ profile,
209
+ apiKey,
210
+ baseUrl,
211
+ note,
212
+ tags,
213
+ }, (candidate) => Boolean((0, providers_repo_1.readProvidersFileIfExists)(paths.providersPath).providers[candidate]));
214
+ providerName = prompted.providerName;
215
+ profile = prompted.profile;
216
+ apiKey = prompted.apiKey;
217
+ baseUrl = prompted.baseUrl ?? null;
218
+ note = prompted.note ?? null;
219
+ tags = prompted.tags;
220
+ }
221
+ return (0, add_provider_1.addProvider)({
222
+ codexDir: paths.codexDir,
223
+ backupsDir: paths.backupsDir,
224
+ latestBackupPath: paths.latestBackupPath,
225
+ providersPath: paths.providersPath,
226
+ providerName,
227
+ profile,
228
+ apiKey,
229
+ baseUrl,
230
+ note,
231
+ tags,
232
+ });
233
+ }
234
+ case "edit": {
235
+ let providerName = parsed.positionals[0] ?? null;
236
+ if (!providerName && (0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
237
+ providerName = await (0, interactive_1.promptForProviderSelection)(runtime, paths.providersPath, "Choose a provider to edit");
238
+ }
139
239
  if (!providerName) {
140
- throw (0, errors_1.cliError)("INVALID_IMPORT_FILE", "Missing provider name for add command.");
240
+ throw (0, errors_1.cliError)("INVALID_ARGUMENT", "Missing provider name for edit command.");
141
241
  }
142
- const profile = (0, args_1.getSingleOption)(parsed.commandOptions, "--profile");
143
- const apiKey = (0, args_1.getSingleOption)(parsed.commandOptions, "--api-key");
144
- if (!profile || !apiKey) {
145
- throw (0, errors_1.cliError)("INVALID_IMPORT_FILE", "add requires --profile and --api-key.");
242
+ let profile = (0, args_1.getSingleOption)(parsed.commandOptions, "--profile", false) ?? undefined;
243
+ let apiKey = (0, args_1.getSingleOption)(parsed.commandOptions, "--api-key", false) ?? undefined;
244
+ let baseUrl = (0, args_1.getSingleOption)(parsed.commandOptions, "--base-url", false) ?? undefined;
245
+ let note = (0, args_1.getSingleOption)(parsed.commandOptions, "--note", false) ?? undefined;
246
+ let tags = parsed.commandOptions.has("--tag")
247
+ ? parsed.commandOptions.get("--tag") ?? []
248
+ : undefined;
249
+ if (profile === undefined &&
250
+ apiKey === undefined &&
251
+ baseUrl === undefined &&
252
+ note === undefined &&
253
+ tags === undefined &&
254
+ (0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
255
+ const provider = (0, providers_repo_1.readProvidersFileIfExists)(paths.providersPath).providers[providerName];
256
+ if (!provider) {
257
+ throw (0, errors_1.cliError)("PROVIDER_NOT_FOUND", `Provider "${providerName}" was not found.`);
258
+ }
259
+ const prompted = await (0, interactive_1.collectEditInput)(runtime, provider);
260
+ profile = prompted.profile;
261
+ apiKey = prompted.apiKey;
262
+ baseUrl = prompted.baseUrl;
263
+ note = prompted.note;
264
+ tags = prompted.tags;
146
265
  }
147
- return (0, add_provider_1.addProvider)({
266
+ if (profile === undefined && apiKey === undefined && baseUrl === undefined && note === undefined && tags === undefined) {
267
+ throw (0, errors_1.cliError)("INVALID_ARGUMENT", "edit requires at least one field to update.");
268
+ }
269
+ return (0, edit_provider_1.editProvider)({
148
270
  codexDir: paths.codexDir,
149
271
  backupsDir: paths.backupsDir,
150
272
  latestBackupPath: paths.latestBackupPath,
@@ -152,18 +274,25 @@ async function executeCommand(ctx, parsed) {
152
274
  providerName,
153
275
  profile,
154
276
  apiKey,
155
- baseUrl: (0, args_1.getSingleOption)(parsed.commandOptions, "--base-url", false),
156
- note: (0, args_1.getSingleOption)(parsed.commandOptions, "--note", false),
157
- tags: parsed.commandOptions.get("--tag") ?? [],
277
+ baseUrl,
278
+ note,
279
+ tags,
158
280
  });
159
281
  }
160
282
  case "remove": {
161
- const providerName = parsed.positionals[0];
283
+ let providerName = parsed.positionals[0] ?? null;
284
+ const force = (0, args_1.hasFlag)(parsed.commandOptions, "--force");
285
+ if (!providerName && (0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
286
+ providerName = await (0, interactive_1.promptForProviderSelection)(runtime, paths.providersPath, "Choose a provider to remove");
287
+ }
162
288
  if (!providerName) {
163
289
  throw (0, errors_1.cliError)("PROVIDER_NOT_FOUND", "Missing provider name for remove command.");
164
290
  }
165
- if (!(0, args_1.hasFlag)(parsed.commandOptions, "--force")) {
166
- throw (0, errors_1.cliError)("INVALID_IMPORT_FILE", "remove requires --force.");
291
+ if (!force && !(0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
292
+ throw (0, errors_1.cliError)("INVALID_ARGUMENT", "remove requires --force.");
293
+ }
294
+ if ((0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
295
+ await (0, interactive_1.confirmProviderRemoval)(runtime, providerName);
167
296
  }
168
297
  return (0, remove_provider_1.removeProvider)({
169
298
  codexDir: paths.codexDir,
@@ -179,10 +308,58 @@ async function executeCommand(ctx, parsed) {
179
308
  configPath: paths.configPath,
180
309
  providersPath: paths.providersPath,
181
310
  });
311
+ case "setup": {
312
+ const overwrite = (0, args_1.hasFlag)(parsed.commandOptions, "--overwrite");
313
+ const merge = (0, args_1.hasFlag)(parsed.commandOptions, "--merge");
314
+ if (overwrite && merge) {
315
+ throw (0, errors_1.cliError)("INVALID_ARGUMENT", "setup does not allow both --merge and --overwrite.");
316
+ }
317
+ let strategy = overwrite ? "overwrite" : merge ? "merge" : null;
318
+ const providersExists = fs.existsSync(paths.providersPath);
319
+ if (providersExists && strategy === null) {
320
+ if (!(0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
321
+ throw (0, errors_1.cliError)("PROVIDERS_ALREADY_EXISTS", "providers.json already exists. Pass --merge or --overwrite.", {
322
+ file: paths.providersPath,
323
+ });
324
+ }
325
+ const selected = await (0, interactive_1.chooseSetupStrategy)(runtime);
326
+ if (selected === "cancel") {
327
+ throw (0, errors_1.cliError)("PROMPT_CANCELLED", "Setup cancelled.");
328
+ }
329
+ strategy = selected;
330
+ }
331
+ const profiles = Array.from((0, config_repo_1.listConfigProfiles)(paths.configPath)).sort();
332
+ let providerDetailsByProfile = {};
333
+ if ((0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
334
+ providerDetailsByProfile = await (0, interactive_1.collectSetupProviderDetails)(runtime, profiles);
335
+ }
336
+ return (0, setup_codex_1.setupCodex)({
337
+ codexDirOption: ctx.options.codexDir,
338
+ codexDir: paths.codexDir,
339
+ configPath: paths.configPath,
340
+ providersPath: paths.providersPath,
341
+ backupsDir: paths.backupsDir,
342
+ latestBackupPath: paths.latestBackupPath,
343
+ strategy: strategy ?? "overwrite",
344
+ providerDetailsByProfile,
345
+ });
346
+ }
347
+ case "backups-list":
348
+ return (0, list_backups_1.listBackupEntries)(paths.backupsDir);
182
349
  case "rollback":
183
- return (0, rollback_latest_1.rollbackLatest)(paths.latestBackupPath);
350
+ if (parsed.positionals.length > 1) {
351
+ throw (0, errors_1.cliError)("INVALID_ARGUMENT", "rollback accepts at most one backup id.");
352
+ }
353
+ if ((0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
354
+ await (0, interactive_1.confirmRollback)(runtime, paths.latestBackupPath, paths.backupsDir, parsed.positionals[0] ?? null);
355
+ }
356
+ return (0, rollback_backup_1.rollbackBackup)({
357
+ latestBackupPath: paths.latestBackupPath,
358
+ backupsDir: paths.backupsDir,
359
+ backupId: parsed.positionals[0] ?? null,
360
+ });
184
361
  default:
185
- throw (0, errors_1.cliError)("INVALID_IMPORT_FILE", `Unknown command: ${ctx.command}`);
362
+ throw (0, errors_1.cliError)("UNKNOWN_COMMAND", `Unknown command: ${ctx.command}`);
186
363
  }
187
364
  }
188
365
  if (require.main === module) {
@@ -0,0 +1,103 @@
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.getBackupId = getBackupId;
37
+ exports.validateBackupManifest = validateBackupManifest;
38
+ exports.sortBackupList = sortBackupList;
39
+ exports.toBackupListItem = toBackupListItem;
40
+ const path = __importStar(require("node:path"));
41
+ /**
42
+ * Returns the explicit backup identifier derived from the backup directory name.
43
+ */
44
+ function getBackupId(backupDir) {
45
+ return path.basename(backupDir);
46
+ }
47
+ /**
48
+ * Validates the minimal manifest shape needed for listing and restoring backups.
49
+ */
50
+ function validateBackupManifest(input) {
51
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
52
+ throw new Error("Backup manifest must be an object.");
53
+ }
54
+ const manifest = input;
55
+ if (manifest.version !== 1) {
56
+ throw new Error("Unsupported backup manifest version.");
57
+ }
58
+ if (typeof manifest.createdAt !== "string" || manifest.createdAt.trim() === "") {
59
+ throw new Error("Backup manifest is missing createdAt.");
60
+ }
61
+ if (typeof manifest.reason !== "string" || manifest.reason.trim() === "") {
62
+ throw new Error("Backup manifest is missing reason.");
63
+ }
64
+ if (typeof manifest.rootDir !== "string" || manifest.rootDir.trim() === "") {
65
+ throw new Error("Backup manifest is missing rootDir.");
66
+ }
67
+ if (typeof manifest.backupDir !== "string" || manifest.backupDir.trim() === "") {
68
+ throw new Error("Backup manifest is missing backupDir.");
69
+ }
70
+ if (!Array.isArray(manifest.files)) {
71
+ throw new Error("Backup manifest is missing files.");
72
+ }
73
+ for (const entry of manifest.files) {
74
+ if (!entry || typeof entry !== "object") {
75
+ throw new Error("Backup manifest contains an invalid file entry.");
76
+ }
77
+ if (typeof entry.relativePath !== "string" || typeof entry.existed !== "boolean") {
78
+ throw new Error("Backup manifest contains an invalid file entry.");
79
+ }
80
+ if (entry.backupFileName !== null && typeof entry.backupFileName !== "string") {
81
+ throw new Error("Backup manifest contains an invalid backup file name.");
82
+ }
83
+ }
84
+ return manifest;
85
+ }
86
+ /**
87
+ * Sorts backup list items from newest to oldest based on createdAt.
88
+ */
89
+ function sortBackupList(items) {
90
+ return [...items].sort((left, right) => right.createdAt.localeCompare(left.createdAt));
91
+ }
92
+ /**
93
+ * Converts a manifest into the stable list payload returned by `backups list`.
94
+ */
95
+ function toBackupListItem(manifest) {
96
+ return {
97
+ backupId: getBackupId(manifest.backupDir),
98
+ createdAt: manifest.createdAt,
99
+ reason: manifest.reason,
100
+ files: manifest.files.map((file) => file.relativePath),
101
+ backupPath: manifest.backupDir,
102
+ };
103
+ }
@@ -18,19 +18,19 @@ function normalizeError(error) {
18
18
  if (error && typeof error === "object" && "code" in error && "message" in error) {
19
19
  const candidate = error;
20
20
  return {
21
- code: candidate.code ?? "INVALID_IMPORT_FILE",
21
+ code: candidate.code ?? "INVALID_ARGUMENT",
22
22
  message: candidate.message ?? "Unknown error.",
23
23
  details: candidate.details,
24
24
  };
25
25
  }
26
26
  if (error instanceof Error) {
27
27
  return {
28
- code: "INVALID_IMPORT_FILE",
28
+ code: "INVALID_ARGUMENT",
29
29
  message: error.message,
30
30
  };
31
31
  }
32
32
  return {
33
- code: "INVALID_IMPORT_FILE",
33
+ code: "INVALID_ARGUMENT",
34
34
  message: String(error),
35
35
  };
36
36
  }
@@ -4,6 +4,7 @@ exports.validateProvidersShape = validateProvidersShape;
4
4
  exports.cleanProviderRecord = cleanProviderRecord;
5
5
  exports.sortProviders = sortProviders;
6
6
  exports.findProviderByProfile = findProviderByProfile;
7
+ exports.maskSecret = maskSecret;
7
8
  /**
8
9
  * Validates and normalizes unknown JSON into the providers.json domain model.
9
10
  */
@@ -90,3 +91,12 @@ function findProviderByProfile(providers, profile) {
90
91
  }
91
92
  return null;
92
93
  }
94
+ /**
95
+ * Masks a secret for human-readable output while preserving a short fingerprint.
96
+ */
97
+ function maskSecret(value) {
98
+ if (value.length <= 5) {
99
+ return "*".repeat(Math.max(value.length, 1));
100
+ }
101
+ return `${value.slice(0, 3)}***${value.slice(-2)}`;
102
+ }
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildSetupDrafts = buildSetupDrafts;
4
+ exports.findIncompleteSetupProfiles = findIncompleteSetupProfiles;
5
+ const providers_1 = require("./providers");
6
+ /**
7
+ * Creates initial provider drafts from config profile names.
8
+ */
9
+ function buildSetupDrafts(profiles, detailsByProfile) {
10
+ return profiles.map((profile) => {
11
+ const detail = detailsByProfile[profile] ?? {};
12
+ const providerName = (detail.providerName ?? profile).trim();
13
+ return {
14
+ providerName,
15
+ record: (0, providers_1.cleanProviderRecord)({
16
+ profile,
17
+ apiKey: detail.apiKey ?? "",
18
+ baseUrl: detail.baseUrl,
19
+ note: detail.note,
20
+ tags: detail.tags,
21
+ }),
22
+ };
23
+ });
24
+ }
25
+ /**
26
+ * Returns the profile names that still lack required provider fields.
27
+ */
28
+ function findIncompleteSetupProfiles(drafts) {
29
+ return drafts.filter((draft) => draft.record.apiKey.trim() === "").map((draft) => draft.record.profile);
30
+ }
@@ -37,8 +37,11 @@ exports.createBackup = createBackup;
37
37
  exports.restoreManifest = restoreManifest;
38
38
  exports.saveLatestManifest = saveLatestManifest;
39
39
  exports.loadLatestManifest = loadLatestManifest;
40
+ exports.loadManifestById = loadManifestById;
41
+ exports.listBackups = listBackups;
40
42
  const fs = __importStar(require("node:fs"));
41
43
  const path = __importStar(require("node:path"));
44
+ const backups_1 = require("../domain/backups");
42
45
  const errors_1 = require("../domain/errors");
43
46
  const fs_utils_1 = require("./fs-utils");
44
47
  /**
@@ -115,16 +118,12 @@ function saveLatestManifest(latestBackupPath, manifest) {
115
118
  */
116
119
  function loadLatestManifest(latestBackupPath) {
117
120
  if (!fs.existsSync(latestBackupPath)) {
118
- throw (0, errors_1.cliError)("ROLLBACK_FAILED", "No rollback backup is available.", {
121
+ throw (0, errors_1.cliError)("BACKUP_NOT_FOUND", "No rollback backup is available.", {
119
122
  file: latestBackupPath,
120
123
  });
121
124
  }
122
125
  try {
123
- const manifest = JSON.parse(fs.readFileSync(latestBackupPath, "utf8"));
124
- if (!manifest || typeof manifest !== "object" || !Array.isArray(manifest.files)) {
125
- throw new Error("Invalid latest backup manifest.");
126
- }
127
- return manifest;
126
+ return (0, backups_1.validateBackupManifest)(JSON.parse(fs.readFileSync(latestBackupPath, "utf8")));
128
127
  }
129
128
  catch (error) {
130
129
  throw (0, errors_1.cliError)("ROLLBACK_FAILED", "Failed to read latest backup manifest.", {
@@ -133,6 +132,66 @@ function loadLatestManifest(latestBackupPath) {
133
132
  });
134
133
  }
135
134
  }
135
+ /**
136
+ * Loads a backup manifest by its explicit backup id.
137
+ */
138
+ function loadManifestById(backupsDir, backupId) {
139
+ const manifestPath = path.join(backupsDir, backupId, "manifest.json");
140
+ if (!fs.existsSync(manifestPath)) {
141
+ throw (0, errors_1.cliError)("BACKUP_NOT_FOUND", `Backup "${backupId}" was not found.`, {
142
+ backupId,
143
+ file: manifestPath,
144
+ });
145
+ }
146
+ try {
147
+ return (0, backups_1.validateBackupManifest)(JSON.parse(fs.readFileSync(manifestPath, "utf8")));
148
+ }
149
+ catch (error) {
150
+ throw (0, errors_1.cliError)("ROLLBACK_FAILED", `Failed to read backup manifest "${backupId}".`, {
151
+ backupId,
152
+ file: manifestPath,
153
+ cause: (0, errors_1.normalizeError)(error).message,
154
+ });
155
+ }
156
+ }
157
+ /**
158
+ * Lists valid backup manifests under backups/, newest first, while skipping corrupt entries with warnings.
159
+ */
160
+ function listBackups(backupsDir) {
161
+ if (!fs.existsSync(backupsDir)) {
162
+ throw (0, errors_1.cliError)("BACKUP_NOT_FOUND", "No backups directory exists.", {
163
+ directory: backupsDir,
164
+ });
165
+ }
166
+ const entries = fs.readdirSync(backupsDir, { withFileTypes: true });
167
+ const backups = [];
168
+ const warnings = [];
169
+ for (const entry of entries) {
170
+ if (!entry.isDirectory() || entry.name === "latest.json") {
171
+ continue;
172
+ }
173
+ const manifestPath = path.join(backupsDir, entry.name, "manifest.json");
174
+ if (!fs.existsSync(manifestPath)) {
175
+ warnings.push(`Skipped backup "${entry.name}" because manifest.json is missing.`);
176
+ continue;
177
+ }
178
+ try {
179
+ backups.push((0, backups_1.toBackupListItem)((0, backups_1.validateBackupManifest)(JSON.parse(fs.readFileSync(manifestPath, "utf8")))));
180
+ }
181
+ catch (error) {
182
+ warnings.push(`Skipped backup "${entry.name}" because manifest.json is invalid: ${(0, errors_1.normalizeError)(error).message}`);
183
+ }
184
+ }
185
+ if (backups.length === 0) {
186
+ throw (0, errors_1.cliError)("BACKUP_NOT_FOUND", "No valid backups were found.", {
187
+ directory: backupsDir,
188
+ });
189
+ }
190
+ return {
191
+ backups: (0, backups_1.sortBackupList)(backups),
192
+ warnings,
193
+ };
194
+ }
136
195
  /**
137
196
  * Formats a filesystem-safe timestamp for backup directory names.
138
197
  */