@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.
package/dist/cli.js CHANGED
@@ -1,20 +1,59 @@
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");
18
57
  const providers_repo_1 = require("./infra/providers-repo");
19
58
  const codex_paths_1 = require("./infra/codex-paths");
20
59
  const args_1 = require("./cli/args");
@@ -23,7 +62,7 @@ const help_1 = require("./cli/help");
23
62
  const interactive_1 = require("./cli/interactive");
24
63
  const output_1 = require("./cli/output");
25
64
  const prompt_1 = require("./cli/prompt");
26
- const VERSION = "0.0.3";
65
+ const VERSION = "0.0.4";
27
66
  /**
28
67
  * Prints the command help text to stdout.
29
68
  */
@@ -47,7 +86,7 @@ function main() {
47
86
  }
48
87
  if (parsed.helpRequested) {
49
88
  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}`, {
89
+ (0, output_1.outputFailure)({ command: "help", options: parsed.globalOptions }, (0, errors_1.cliError)("INVALID_ARGUMENT", `Unknown help topic: ${parsed.helpTarget}`, {
51
90
  availableCommands: (0, help_1.buildHelpText)(parsed.helpTarget).split("\n").slice(2),
52
91
  }));
53
92
  return;
@@ -79,6 +118,20 @@ async function executeCommand(ctx, parsed, runtime = (0, prompt_1.createPromptRu
79
118
  switch (ctx.command) {
80
119
  case "list":
81
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
+ }
82
135
  case "current":
83
136
  return (0, get_current_profile_1.getCurrentProfile)(paths.configPath);
84
137
  case "status":
@@ -105,10 +158,11 @@ async function executeCommand(ctx, parsed, runtime = (0, prompt_1.createPromptRu
105
158
  case "import": {
106
159
  const sourceFile = parsed.positionals[0];
107
160
  if (!sourceFile) {
108
- throw (0, errors_1.cliError)("INVALID_IMPORT_FILE", "Missing import file path.");
161
+ throw (0, errors_1.cliError)("INVALID_ARGUMENT", "Missing import file path.");
109
162
  }
163
+ const merge = (0, args_1.hasFlag)(parsed.commandOptions, "--merge");
110
164
  if ((0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
111
- await (0, interactive_1.confirmImport)(runtime, sourceFile);
165
+ await (0, interactive_1.confirmImport)(runtime, sourceFile, merge);
112
166
  }
113
167
  return (0, import_providers_1.importProviders)({
114
168
  codexDir: paths.codexDir,
@@ -116,18 +170,19 @@ async function executeCommand(ctx, parsed, runtime = (0, prompt_1.createPromptRu
116
170
  latestBackupPath: paths.latestBackupPath,
117
171
  providersPath: paths.providersPath,
118
172
  sourceFile,
173
+ merge,
119
174
  });
120
175
  }
121
176
  case "export": {
122
177
  const targetFile = parsed.positionals[0];
123
178
  if (!targetFile) {
124
- throw (0, errors_1.cliError)("INVALID_IMPORT_FILE", "Missing export file path.");
179
+ throw (0, errors_1.cliError)("INVALID_ARGUMENT", "Missing export file path.");
125
180
  }
126
181
  let force = (0, args_1.hasFlag)(parsed.commandOptions, "--force");
127
182
  if (!force && (0, interactive_1.canPrompt)(runtime, ctx.options.json) && (0, interactive_1.exportTargetExists)(targetFile)) {
128
183
  const confirmed = await (0, interactive_1.confirmExportOverwrite)(runtime, targetFile);
129
184
  if (!confirmed) {
130
- throw (0, errors_1.cliError)("INVALID_IMPORT_FILE", "Export cancelled.");
185
+ throw (0, errors_1.cliError)("PROMPT_CANCELLED", "Export cancelled.");
131
186
  }
132
187
  force = true;
133
188
  }
@@ -155,7 +210,7 @@ async function executeCommand(ctx, parsed, runtime = (0, prompt_1.createPromptRu
155
210
  baseUrl,
156
211
  note,
157
212
  tags,
158
- }, (candidate) => Boolean((0, providers_repo_1.readProvidersFileIfExists)(paths.providersPath).providers[candidate]), paths.configPath);
213
+ }, (candidate) => Boolean((0, providers_repo_1.readProvidersFileIfExists)(paths.providersPath).providers[candidate]));
159
214
  providerName = prompted.providerName;
160
215
  profile = prompted.profile;
161
216
  apiKey = prompted.apiKey;
@@ -176,6 +231,54 @@ async function executeCommand(ctx, parsed, runtime = (0, prompt_1.createPromptRu
176
231
  tags,
177
232
  });
178
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
+ }
239
+ if (!providerName) {
240
+ throw (0, errors_1.cliError)("INVALID_ARGUMENT", "Missing provider name for edit command.");
241
+ }
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;
265
+ }
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)({
270
+ codexDir: paths.codexDir,
271
+ backupsDir: paths.backupsDir,
272
+ latestBackupPath: paths.latestBackupPath,
273
+ providersPath: paths.providersPath,
274
+ providerName,
275
+ profile,
276
+ apiKey,
277
+ baseUrl,
278
+ note,
279
+ tags,
280
+ });
281
+ }
179
282
  case "remove": {
180
283
  let providerName = parsed.positionals[0] ?? null;
181
284
  const force = (0, args_1.hasFlag)(parsed.commandOptions, "--force");
@@ -186,7 +289,7 @@ async function executeCommand(ctx, parsed, runtime = (0, prompt_1.createPromptRu
186
289
  throw (0, errors_1.cliError)("PROVIDER_NOT_FOUND", "Missing provider name for remove command.");
187
290
  }
188
291
  if (!force && !(0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
189
- throw (0, errors_1.cliError)("INVALID_IMPORT_FILE", "remove requires --force.");
292
+ throw (0, errors_1.cliError)("INVALID_ARGUMENT", "remove requires --force.");
190
293
  }
191
294
  if ((0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
192
295
  await (0, interactive_1.confirmProviderRemoval)(runtime, providerName);
@@ -205,13 +308,58 @@ async function executeCommand(ctx, parsed, runtime = (0, prompt_1.createPromptRu
205
308
  configPath: paths.configPath,
206
309
  providersPath: paths.providersPath,
207
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);
208
349
  case "rollback":
350
+ if (parsed.positionals.length > 1) {
351
+ throw (0, errors_1.cliError)("INVALID_ARGUMENT", "rollback accepts at most one backup id.");
352
+ }
209
353
  if ((0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
210
- await (0, interactive_1.confirmRollback)(runtime, paths.latestBackupPath);
354
+ await (0, interactive_1.confirmRollback)(runtime, paths.latestBackupPath, paths.backupsDir, parsed.positionals[0] ?? null);
211
355
  }
212
- return (0, rollback_latest_1.rollbackLatest)(paths.latestBackupPath);
356
+ return (0, rollback_backup_1.rollbackBackup)({
357
+ latestBackupPath: paths.latestBackupPath,
358
+ backupsDir: paths.backupsDir,
359
+ backupId: parsed.positionals[0] ?? null,
360
+ });
213
361
  default:
214
- throw (0, errors_1.cliError)("INVALID_IMPORT_FILE", `Unknown command: ${ctx.command}`);
362
+ throw (0, errors_1.cliError)("UNKNOWN_COMMAND", `Unknown command: ${ctx.command}`);
215
363
  }
216
364
  }
217
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
  */
@@ -4,6 +4,8 @@ exports.setCodexSpawnImplementation = setCodexSpawnImplementation;
4
4
  exports.resetCodexSpawnImplementation = resetCodexSpawnImplementation;
5
5
  exports.runCodexLogin = runCodexLogin;
6
6
  exports.checkCodexAvailable = checkCodexAvailable;
7
+ exports.readCodexVersion = readCodexVersion;
8
+ exports.checkCodexVersion = checkCodexVersion;
7
9
  const node_child_process_1 = require("node:child_process");
8
10
  const errors_1 = require("../domain/errors");
9
11
  let spawnImplementation = node_child_process_1.spawnSync;
@@ -51,3 +53,63 @@ function checkCodexAvailable() {
51
53
  }
52
54
  return { ok: true };
53
55
  }
56
+ /**
57
+ * Reads the installed codex CLI version string.
58
+ */
59
+ function readCodexVersion() {
60
+ const result = spawnImplementation("codex", ["--version"], {
61
+ stdio: "pipe",
62
+ encoding: "utf8",
63
+ });
64
+ if (result.error || result.status !== 0) {
65
+ return {
66
+ ok: false,
67
+ cause: result.error?.message ?? (result.stderr.trim() || "Unknown failure"),
68
+ };
69
+ }
70
+ const raw = `${result.stdout ?? ""} ${result.stderr ?? ""}`.trim();
71
+ const match = raw.match(/(\d+\.\d+\.\d+)/);
72
+ if (!match) {
73
+ return {
74
+ ok: false,
75
+ cause: `Unable to parse codex version from output: ${raw || "(empty output)"}`,
76
+ };
77
+ }
78
+ return { ok: true, version: match[1] };
79
+ }
80
+ /**
81
+ * Compares the installed codex version against a minimum required version.
82
+ */
83
+ function checkCodexVersion(minVersion) {
84
+ const current = readCodexVersion();
85
+ if (!current.ok) {
86
+ return {
87
+ ok: false,
88
+ cause: current.cause,
89
+ };
90
+ }
91
+ if (compareVersions(current.version, minVersion) < 0) {
92
+ return {
93
+ ok: false,
94
+ currentVersion: current.version,
95
+ cause: `codex ${current.version} is below required ${minVersion}`,
96
+ };
97
+ }
98
+ return {
99
+ ok: true,
100
+ currentVersion: current.version,
101
+ };
102
+ }
103
+ function compareVersions(left, right) {
104
+ const leftParts = left.split(".").map((value) => Number.parseInt(value, 10));
105
+ const rightParts = right.split(".").map((value) => Number.parseInt(value, 10));
106
+ const length = Math.max(leftParts.length, rightParts.length);
107
+ for (let index = 0; index < length; index += 1) {
108
+ const leftValue = leftParts[index] ?? 0;
109
+ const rightValue = rightParts[index] ?? 0;
110
+ if (leftValue !== rightValue) {
111
+ return leftValue - rightValue;
112
+ }
113
+ }
114
+ return 0;
115
+ }
@@ -0,0 +1,48 @@
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.findCodexDirCandidates = findCodexDirCandidates;
37
+ const fs = __importStar(require("node:fs"));
38
+ const codex_paths_1 = require("./codex-paths");
39
+ /**
40
+ * Finds candidate Codex home directories. v0.0.4 only supports explicit and default locations.
41
+ */
42
+ function findCodexDirCandidates(explicitCodexDir) {
43
+ if (explicitCodexDir) {
44
+ return [(0, codex_paths_1.resolveCodexDir)(explicitCodexDir)];
45
+ }
46
+ const defaultDir = (0, codex_paths_1.resolveCodexDir)();
47
+ return fs.existsSync(defaultDir) ? [defaultDir] : [];
48
+ }