@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.js CHANGED
@@ -1,20 +1,64 @@
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_config_profiles_1 = require("./app/list-config-profiles");
48
+ const list_backups_1 = require("./app/list-backups");
12
49
  const list_providers_1 = require("./app/list-providers");
13
50
  const remove_provider_1 = require("./app/remove-provider");
14
- const rollback_latest_1 = require("./app/rollback-latest");
51
+ const rollback_backup_1 = require("./app/rollback-backup");
15
52
  const run_doctor_1 = require("./app/run-doctor");
53
+ const setup_codex_1 = require("./app/setup-codex");
54
+ const show_config_1 = require("./app/show-config");
55
+ const show_provider_1 = require("./app/show-provider");
16
56
  const switch_provider_1 = require("./app/switch-provider");
57
+ const config_1 = require("./domain/config");
17
58
  const errors_1 = require("./domain/errors");
59
+ const providers_1 = require("./domain/providers");
60
+ const config_repo_1 = require("./infra/config-repo");
61
+ const config_repo_2 = require("./infra/config-repo");
18
62
  const providers_repo_1 = require("./infra/providers-repo");
19
63
  const codex_paths_1 = require("./infra/codex-paths");
20
64
  const args_1 = require("./cli/args");
@@ -23,7 +67,7 @@ const help_1 = require("./cli/help");
23
67
  const interactive_1 = require("./cli/interactive");
24
68
  const output_1 = require("./cli/output");
25
69
  const prompt_1 = require("./cli/prompt");
26
- const VERSION = "0.0.3";
70
+ const VERSION = "0.0.5";
27
71
  /**
28
72
  * Prints the command help text to stdout.
29
73
  */
@@ -47,7 +91,7 @@ function main() {
47
91
  }
48
92
  if (parsed.helpRequested) {
49
93
  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}`, {
94
+ (0, output_1.outputFailure)({ command: "help", options: parsed.globalOptions }, (0, errors_1.cliError)("INVALID_ARGUMENT", `Unknown help topic: ${parsed.helpTarget}`, {
51
95
  availableCommands: (0, help_1.buildHelpText)(parsed.helpTarget).split("\n").slice(2),
52
96
  }));
53
97
  return;
@@ -75,14 +119,40 @@ function main() {
75
119
  * Dispatches a parsed CLI command into the application layer.
76
120
  */
77
121
  async function executeCommand(ctx, parsed, runtime = (0, prompt_1.createPromptRuntime)()) {
78
- const paths = (0, codex_paths_1.createCodexPaths)(ctx.options.codexDir);
122
+ let setupPaths = (0, codex_paths_1.createCodexPaths)(ctx.options.codexDir);
123
+ const paths = setupPaths;
79
124
  switch (ctx.command) {
80
125
  case "list":
81
126
  return (0, list_providers_1.listProviders)(paths.providersPath);
127
+ case "show": {
128
+ let providerName = parsed.positionals[0] ?? null;
129
+ if (!providerName && (0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
130
+ providerName = await (0, interactive_1.promptForProviderSelection)(runtime, paths.providersPath, "Choose a provider to show");
131
+ }
132
+ if (!providerName) {
133
+ throw (0, errors_1.cliError)("INVALID_ARGUMENT", "Missing provider name for show command.");
134
+ }
135
+ return (0, show_provider_1.showProvider)({
136
+ providersPath: paths.providersPath,
137
+ providerName,
138
+ includeSecret: ctx.options.json,
139
+ });
140
+ }
82
141
  case "current":
83
142
  return (0, get_current_profile_1.getCurrentProfile)(paths.configPath);
84
143
  case "status":
85
144
  return (0, get_status_1.getStatus)(paths.codexDir, paths.configPath, paths.providersPath);
145
+ case "config-show":
146
+ return (0, show_config_1.showConfig)({
147
+ configPath: paths.configPath,
148
+ providersPath: paths.providersPath,
149
+ profileName: parsed.positionals[0] ?? null,
150
+ });
151
+ case "config-list-profiles":
152
+ return (0, list_config_profiles_1.listConfigProfilesView)({
153
+ configPath: paths.configPath,
154
+ providersPath: paths.providersPath,
155
+ });
86
156
  case "switch": {
87
157
  let providerName = parsed.positionals[0] ?? null;
88
158
  if (!providerName && (0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
@@ -105,29 +175,45 @@ async function executeCommand(ctx, parsed, runtime = (0, prompt_1.createPromptRu
105
175
  case "import": {
106
176
  const sourceFile = parsed.positionals[0];
107
177
  if (!sourceFile) {
108
- throw (0, errors_1.cliError)("INVALID_IMPORT_FILE", "Missing import file path.");
178
+ throw (0, errors_1.cliError)("INVALID_ARGUMENT", "Missing import file path.");
109
179
  }
180
+ const merge = (0, args_1.hasFlag)(parsed.commandOptions, "--merge");
181
+ let repairProfiles;
110
182
  if ((0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
111
- await (0, interactive_1.confirmImport)(runtime, sourceFile);
183
+ await (0, interactive_1.confirmImport)(runtime, sourceFile, merge);
184
+ const document = (0, config_repo_2.readStructuredConfig)(paths.configPath);
185
+ const imported = (0, providers_1.validateProvidersShape)(JSON.parse(fs.readFileSync(sourceFile, "utf8")));
186
+ const current = (0, providers_repo_1.readProvidersFileIfExists)(paths.providersPath);
187
+ const next = merge ? (0, providers_repo_1.mergeProviders)(current, imported) : imported;
188
+ const missingProfiles = (0, config_1.buildManagedProfileViews)(document, next)
189
+ .filter((view) => view.source === "orphaned-reference")
190
+ .map((view) => view.name)
191
+ .sort();
192
+ if (missingProfiles.length > 0) {
193
+ repairProfiles = await (0, interactive_1.collectImportRepairDetails)(runtime, missingProfiles);
194
+ }
112
195
  }
113
196
  return (0, import_providers_1.importProviders)({
114
197
  codexDir: paths.codexDir,
115
198
  backupsDir: paths.backupsDir,
116
199
  latestBackupPath: paths.latestBackupPath,
117
200
  providersPath: paths.providersPath,
201
+ configPath: paths.configPath,
118
202
  sourceFile,
203
+ merge,
204
+ repairProfiles,
119
205
  });
120
206
  }
121
207
  case "export": {
122
208
  const targetFile = parsed.positionals[0];
123
209
  if (!targetFile) {
124
- throw (0, errors_1.cliError)("INVALID_IMPORT_FILE", "Missing export file path.");
210
+ throw (0, errors_1.cliError)("INVALID_ARGUMENT", "Missing export file path.");
125
211
  }
126
212
  let force = (0, args_1.hasFlag)(parsed.commandOptions, "--force");
127
213
  if (!force && (0, interactive_1.canPrompt)(runtime, ctx.options.json) && (0, interactive_1.exportTargetExists)(targetFile)) {
128
214
  const confirmed = await (0, interactive_1.confirmExportOverwrite)(runtime, targetFile);
129
215
  if (!confirmed) {
130
- throw (0, errors_1.cliError)("INVALID_IMPORT_FILE", "Export cancelled.");
216
+ throw (0, errors_1.cliError)("PROMPT_CANCELLED", "Export cancelled.");
131
217
  }
132
218
  force = true;
133
219
  }
@@ -142,8 +228,10 @@ async function executeCommand(ctx, parsed, runtime = (0, prompt_1.createPromptRu
142
228
  let profile = (0, args_1.getSingleOption)(parsed.commandOptions, "--profile");
143
229
  let apiKey = (0, args_1.getSingleOption)(parsed.commandOptions, "--api-key");
144
230
  let baseUrl = (0, args_1.getSingleOption)(parsed.commandOptions, "--base-url", false);
231
+ let model = (0, args_1.getSingleOption)(parsed.commandOptions, "--model", false);
145
232
  let note = (0, args_1.getSingleOption)(parsed.commandOptions, "--note", false);
146
233
  let tags = parsed.commandOptions.get("--tag") ?? [];
234
+ const createProfile = (0, args_1.hasFlag)(parsed.commandOptions, "--create-profile");
147
235
  if (!providerName || !profile || !apiKey) {
148
236
  if (ctx.options.json || !runtime.isInteractive()) {
149
237
  throw (0, add_interactive_1.createNonInteractiveAddError)();
@@ -155,7 +243,7 @@ async function executeCommand(ctx, parsed, runtime = (0, prompt_1.createPromptRu
155
243
  baseUrl,
156
244
  note,
157
245
  tags,
158
- }, (candidate) => Boolean((0, providers_repo_1.readProvidersFileIfExists)(paths.providersPath).providers[candidate]), paths.configPath);
246
+ }, (candidate) => Boolean((0, providers_repo_1.readProvidersFileIfExists)(paths.providersPath).providers[candidate]));
159
247
  providerName = prompted.providerName;
160
248
  profile = prompted.profile;
161
249
  apiKey = prompted.apiKey;
@@ -168,17 +256,82 @@ async function executeCommand(ctx, parsed, runtime = (0, prompt_1.createPromptRu
168
256
  backupsDir: paths.backupsDir,
169
257
  latestBackupPath: paths.latestBackupPath,
170
258
  providersPath: paths.providersPath,
259
+ configPath: paths.configPath,
171
260
  providerName,
172
261
  profile,
173
262
  apiKey,
174
263
  baseUrl,
264
+ model,
175
265
  note,
176
266
  tags,
267
+ createProfile,
268
+ });
269
+ }
270
+ case "edit": {
271
+ let providerName = parsed.positionals[0] ?? null;
272
+ if (!providerName && (0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
273
+ providerName = await (0, interactive_1.promptForProviderSelection)(runtime, paths.providersPath, "Choose a provider to edit");
274
+ }
275
+ if (!providerName) {
276
+ throw (0, errors_1.cliError)("INVALID_ARGUMENT", "Missing provider name for edit command.");
277
+ }
278
+ let profile = (0, args_1.getSingleOption)(parsed.commandOptions, "--profile", false) ?? undefined;
279
+ let apiKey = (0, args_1.getSingleOption)(parsed.commandOptions, "--api-key", false) ?? undefined;
280
+ let baseUrl = (0, args_1.getSingleOption)(parsed.commandOptions, "--base-url", false) ?? undefined;
281
+ let model = (0, args_1.getSingleOption)(parsed.commandOptions, "--model", false) ?? undefined;
282
+ let note = (0, args_1.getSingleOption)(parsed.commandOptions, "--note", false) ?? undefined;
283
+ let tags = parsed.commandOptions.has("--tag")
284
+ ? parsed.commandOptions.get("--tag") ?? []
285
+ : undefined;
286
+ const createProfile = (0, args_1.hasFlag)(parsed.commandOptions, "--create-profile");
287
+ const switchToProfile = (0, args_1.getSingleOption)(parsed.commandOptions, "--switch-to", false) ?? undefined;
288
+ if (profile === undefined &&
289
+ apiKey === undefined &&
290
+ baseUrl === undefined &&
291
+ model === undefined &&
292
+ note === undefined &&
293
+ tags === undefined &&
294
+ (0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
295
+ const provider = (0, providers_repo_1.readProvidersFileIfExists)(paths.providersPath).providers[providerName];
296
+ if (!provider) {
297
+ throw (0, errors_1.cliError)("PROVIDER_NOT_FOUND", `Provider "${providerName}" was not found.`);
298
+ }
299
+ const prompted = await (0, interactive_1.collectEditInput)(runtime, provider);
300
+ profile = prompted.profile;
301
+ apiKey = prompted.apiKey;
302
+ baseUrl = prompted.baseUrl;
303
+ note = prompted.note;
304
+ tags = prompted.tags;
305
+ }
306
+ if (profile === undefined &&
307
+ apiKey === undefined &&
308
+ baseUrl === undefined &&
309
+ model === undefined &&
310
+ note === undefined &&
311
+ tags === undefined) {
312
+ throw (0, errors_1.cliError)("INVALID_ARGUMENT", "edit requires at least one field to update.");
313
+ }
314
+ return (0, edit_provider_1.editProvider)({
315
+ codexDir: paths.codexDir,
316
+ backupsDir: paths.backupsDir,
317
+ latestBackupPath: paths.latestBackupPath,
318
+ providersPath: paths.providersPath,
319
+ configPath: paths.configPath,
320
+ providerName,
321
+ profile,
322
+ apiKey,
323
+ baseUrl,
324
+ model,
325
+ note,
326
+ tags,
327
+ createProfile,
328
+ switchToProfile,
177
329
  });
178
330
  }
179
331
  case "remove": {
180
332
  let providerName = parsed.positionals[0] ?? null;
181
333
  const force = (0, args_1.hasFlag)(parsed.commandOptions, "--force");
334
+ const switchToProfile = (0, args_1.getSingleOption)(parsed.commandOptions, "--switch-to", false) ?? undefined;
182
335
  if (!providerName && (0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
183
336
  providerName = await (0, interactive_1.promptForProviderSelection)(runtime, paths.providersPath, "Choose a provider to remove");
184
337
  }
@@ -186,7 +339,7 @@ async function executeCommand(ctx, parsed, runtime = (0, prompt_1.createPromptRu
186
339
  throw (0, errors_1.cliError)("PROVIDER_NOT_FOUND", "Missing provider name for remove command.");
187
340
  }
188
341
  if (!force && !(0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
189
- throw (0, errors_1.cliError)("INVALID_IMPORT_FILE", "remove requires --force.");
342
+ throw (0, errors_1.cliError)("INVALID_ARGUMENT", "remove requires --force.");
190
343
  }
191
344
  if ((0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
192
345
  await (0, interactive_1.confirmProviderRemoval)(runtime, providerName);
@@ -196,7 +349,9 @@ async function executeCommand(ctx, parsed, runtime = (0, prompt_1.createPromptRu
196
349
  backupsDir: paths.backupsDir,
197
350
  latestBackupPath: paths.latestBackupPath,
198
351
  providersPath: paths.providersPath,
352
+ configPath: paths.configPath,
199
353
  providerName,
354
+ switchToProfile,
200
355
  });
201
356
  }
202
357
  case "doctor":
@@ -205,13 +360,95 @@ async function executeCommand(ctx, parsed, runtime = (0, prompt_1.createPromptRu
205
360
  configPath: paths.configPath,
206
361
  providersPath: paths.providersPath,
207
362
  });
363
+ case "setup": {
364
+ let codexDir = ctx.options.codexDir;
365
+ const candidates = (0, config_repo_1.findCodexDirCandidates)(ctx.options.codexDirExplicit ? ctx.options.codexDir : null);
366
+ if (!ctx.options.codexDirExplicit) {
367
+ if (candidates.length > 1) {
368
+ if (!(0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
369
+ throw (0, errors_1.cliError)("CODEX_DIR_AMBIGUOUS", "Multiple Codex directories were found.", {
370
+ candidates,
371
+ });
372
+ }
373
+ codexDir = await (0, interactive_1.chooseCodexDir)(runtime, candidates);
374
+ }
375
+ else if (candidates.length === 0) {
376
+ if (!(0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
377
+ throw (0, errors_1.cliError)("CODEX_DIR_NOT_FOUND", "No Codex directory could be found.");
378
+ }
379
+ codexDir = await (0, interactive_1.chooseCodexDir)(runtime, candidates);
380
+ }
381
+ else {
382
+ codexDir = candidates[0];
383
+ }
384
+ }
385
+ setupPaths = (0, codex_paths_1.createCodexPaths)(codexDir);
386
+ const overwrite = (0, args_1.hasFlag)(parsed.commandOptions, "--overwrite");
387
+ const merge = (0, args_1.hasFlag)(parsed.commandOptions, "--merge");
388
+ if (overwrite && merge) {
389
+ throw (0, errors_1.cliError)("INVALID_ARGUMENT", "setup does not allow both --merge and --overwrite.");
390
+ }
391
+ let strategy = overwrite ? "overwrite" : merge ? "merge" : null;
392
+ const providersExists = fs.existsSync(setupPaths.providersPath);
393
+ if (providersExists && strategy === null) {
394
+ if (!(0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
395
+ throw (0, errors_1.cliError)("PROVIDERS_ALREADY_EXISTS", "providers.json already exists. Pass --merge or --overwrite.", {
396
+ file: setupPaths.providersPath,
397
+ });
398
+ }
399
+ const selected = await (0, interactive_1.chooseSetupStrategy)(runtime);
400
+ if (selected === "cancel") {
401
+ throw (0, errors_1.cliError)("PROMPT_CANCELLED", "Setup cancelled.");
402
+ }
403
+ strategy = selected;
404
+ }
405
+ const document = (0, config_repo_2.readStructuredConfig)(setupPaths.configPath);
406
+ const adoptableProfiles = (0, config_1.buildManagedProfileViews)(document, null)
407
+ .filter((view) => view.source === "unmanaged" && view.model && view.baseUrl)
408
+ .map((view) => ({
409
+ name: view.name,
410
+ model: view.model,
411
+ baseUrl: view.baseUrl,
412
+ }))
413
+ .sort((left, right) => left.name.localeCompare(right.name));
414
+ const selectedProfiles = Array.from((0, config_repo_1.listConfigProfiles)(setupPaths.configPath)).sort();
415
+ let adoptProfiles = [];
416
+ let providerDetailsByProfile = {};
417
+ if ((0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
418
+ adoptProfiles = await (0, interactive_1.chooseSetupProfiles)(runtime, adoptableProfiles);
419
+ providerDetailsByProfile = await (0, interactive_1.collectSetupProviderDetails)(runtime, adoptProfiles);
420
+ }
421
+ else {
422
+ adoptProfiles = selectedProfiles.filter((profile) => Object.prototype.hasOwnProperty.call(providerDetailsByProfile, profile));
423
+ }
424
+ return (0, setup_codex_1.setupCodex)({
425
+ codexDirOption: ctx.options.codexDir,
426
+ codexDir: setupPaths.codexDir,
427
+ configPath: setupPaths.configPath,
428
+ providersPath: setupPaths.providersPath,
429
+ backupsDir: setupPaths.backupsDir,
430
+ latestBackupPath: setupPaths.latestBackupPath,
431
+ strategy: strategy ?? "overwrite",
432
+ adoptProfiles,
433
+ providerDetailsByProfile,
434
+ });
435
+ }
436
+ case "backups-list":
437
+ return (0, list_backups_1.listBackupEntries)(paths.backupsDir);
208
438
  case "rollback":
439
+ if (parsed.positionals.length > 1) {
440
+ throw (0, errors_1.cliError)("INVALID_ARGUMENT", "rollback accepts at most one backup id.");
441
+ }
209
442
  if ((0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
210
- await (0, interactive_1.confirmRollback)(runtime, paths.latestBackupPath);
443
+ await (0, interactive_1.confirmRollback)(runtime, paths.latestBackupPath, paths.backupsDir, parsed.positionals[0] ?? null);
211
444
  }
212
- return (0, rollback_latest_1.rollbackLatest)(paths.latestBackupPath);
445
+ return (0, rollback_backup_1.rollbackBackup)({
446
+ latestBackupPath: paths.latestBackupPath,
447
+ backupsDir: paths.backupsDir,
448
+ backupId: parsed.positionals[0] ?? null,
449
+ });
213
450
  default:
214
- throw (0, errors_1.cliError)("INVALID_IMPORT_FILE", `Unknown command: ${ctx.command}`);
451
+ throw (0, errors_1.cliError)("UNKNOWN_COMMAND", `Unknown command: ${ctx.command}`);
215
452
  }
216
453
  }
217
454
  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
+ }