@minniexcode/codex-switch 0.0.5 → 0.0.7

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 (71) hide show
  1. package/README.AI.md +5 -2
  2. package/README.md +44 -100
  3. package/dist/app/add-provider.js +28 -4
  4. package/dist/app/edit-provider.js +47 -19
  5. package/dist/app/export-providers.js +2 -2
  6. package/dist/app/get-current-profile.js +1 -1
  7. package/dist/app/get-status.js +10 -3
  8. package/dist/app/import-providers.js +15 -7
  9. package/dist/app/init-codex.js +68 -0
  10. package/dist/app/list-backups.js +1 -1
  11. package/dist/app/list-config-profiles.js +3 -2
  12. package/dist/app/list-providers.js +2 -1
  13. package/dist/app/remove-provider.js +2 -2
  14. package/dist/app/rollback-backup.js +1 -1
  15. package/dist/app/rollback-latest.js +1 -1
  16. package/dist/app/run-doctor.js +83 -6
  17. package/dist/app/run-mutation.js +2 -2
  18. package/dist/app/setup-codex.js +21 -12
  19. package/dist/app/show-config.js +11 -3
  20. package/dist/app/show-provider.js +1 -1
  21. package/dist/app/switch-provider.js +16 -9
  22. package/dist/cli/add-interactive.js +7 -104
  23. package/dist/cli/args.js +6 -135
  24. package/dist/cli/help.js +8 -313
  25. package/dist/cli/interactive.js +17 -225
  26. package/dist/cli/output.js +21 -6
  27. package/dist/cli/prompt.js +4 -106
  28. package/dist/cli.js +10 -404
  29. package/dist/commands/args.js +132 -0
  30. package/dist/commands/dispatch.js +16 -0
  31. package/dist/commands/handlers.js +460 -0
  32. package/dist/commands/help.js +120 -0
  33. package/dist/commands/registry.js +351 -0
  34. package/dist/commands/types.js +2 -0
  35. package/dist/domain/config.js +235 -21
  36. package/dist/domain/providers.js +16 -2
  37. package/dist/domain/setup.js +1 -0
  38. package/dist/infra/backup-repo.js +9 -206
  39. package/dist/infra/codex-cli.js +9 -126
  40. package/dist/infra/codex-paths.js +6 -67
  41. package/dist/infra/config-repo.js +59 -0
  42. package/dist/infra/fs-utils.js +8 -93
  43. package/dist/infra/lock-repo.js +4 -95
  44. package/dist/infra/providers-repo.js +8 -94
  45. package/dist/interaction/add-interactive.js +99 -0
  46. package/dist/interaction/interactive.js +289 -0
  47. package/dist/interaction/prompt.js +110 -0
  48. package/dist/runtime/codex-cli.js +130 -0
  49. package/dist/runtime/codex-probe.js +57 -0
  50. package/dist/runtime/types.js +2 -0
  51. package/dist/storage/auth-repo.js +160 -0
  52. package/dist/storage/backup-repo.js +210 -0
  53. package/dist/storage/codex-paths.js +71 -0
  54. package/dist/storage/config-repo.js +266 -0
  55. package/dist/storage/fs-utils.js +97 -0
  56. package/dist/storage/lock-repo.js +99 -0
  57. package/dist/storage/providers-repo.js +98 -0
  58. package/docs/Design/codex-switch-v0.0.5-design.md +32 -22
  59. package/docs/Design/codex-switch-v0.0.6-design.md +708 -0
  60. package/docs/Design/codex-switch-v0.0.7-design.md +862 -0
  61. package/docs/PRD/codex-switch-prd-v0.0.5-to-v0.1.0.md +227 -89
  62. package/docs/PRD/codex-switch-prd-v0.1.0.md +200 -226
  63. package/docs/PRD/codex-switch-prd.md +1 -1
  64. package/docs/Reference/codex-config-reference.md +604 -0
  65. package/docs/Reference/codex-config-reference.zh-CN.md +633 -0
  66. package/docs/cli-usage.md +78 -29
  67. package/docs/codex-switch-technical-architecture.md +73 -4
  68. package/docs/test-report-0.0.5.md +163 -0
  69. package/docs/test-report-0.0.7.md +118 -0
  70. package/docs/testing.md +151 -0
  71. package/package.json +1 -1
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createPromptRuntime = createPromptRuntime;
7
+ const inquirer_1 = __importDefault(require("inquirer"));
8
+ const errors_1 = require("../domain/errors");
9
+ /**
10
+ * Creates the default prompt runtime backed by inquirer on the current process TTY.
11
+ */
12
+ function createPromptRuntime() {
13
+ return {
14
+ isInteractive: () => Boolean(process.stdin.isTTY && process.stdout.isTTY),
15
+ inputText: async (message, options) => {
16
+ return handlePromptCancellation(async () => {
17
+ const answer = await inquirer_1.default.prompt([
18
+ {
19
+ type: "input",
20
+ name: "value",
21
+ message,
22
+ default: options?.defaultValue ?? undefined,
23
+ },
24
+ ]);
25
+ return String(answer.value ?? "");
26
+ });
27
+ },
28
+ inputSecret: async (message) => {
29
+ return handlePromptCancellation(async () => {
30
+ const answer = await inquirer_1.default.prompt([
31
+ {
32
+ type: "password",
33
+ name: "value",
34
+ message,
35
+ mask: "*",
36
+ },
37
+ ]);
38
+ return String(answer.value ?? "");
39
+ });
40
+ },
41
+ selectOne: async (message, choices) => {
42
+ return handlePromptCancellation(async () => {
43
+ const answer = await inquirer_1.default.prompt([
44
+ {
45
+ type: "select",
46
+ name: "value",
47
+ message,
48
+ choices: choices.map((choice) => ({
49
+ value: choice.value,
50
+ name: choice.hint ? `${choice.label} (${choice.hint})` : choice.label,
51
+ })),
52
+ },
53
+ ]);
54
+ return answer.value;
55
+ });
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
+ },
74
+ confirmAction: async (message, options) => {
75
+ return handlePromptCancellation(async () => {
76
+ const answer = await inquirer_1.default.prompt([
77
+ {
78
+ type: "confirm",
79
+ name: "value",
80
+ message,
81
+ default: options?.defaultValue ?? false,
82
+ },
83
+ ]);
84
+ return Boolean(answer.value);
85
+ });
86
+ },
87
+ writeLine: (message) => {
88
+ process.stdout.write(`${message}\n`);
89
+ },
90
+ };
91
+ }
92
+ async function handlePromptCancellation(run) {
93
+ try {
94
+ return await run();
95
+ }
96
+ catch (error) {
97
+ if (isPromptCancellation(error)) {
98
+ throw (0, errors_1.cliError)("PROMPT_CANCELLED", "Interactive prompt was cancelled.");
99
+ }
100
+ throw (0, errors_1.cliError)("INVALID_ARGUMENT", "Interactive prompt failed.", {
101
+ cause: error instanceof Error ? error.message : String(error),
102
+ });
103
+ }
104
+ }
105
+ function isPromptCancellation(error) {
106
+ if (!(error instanceof Error)) {
107
+ return false;
108
+ }
109
+ return error.name === "ExitPromptError" || error.message.includes("force closed");
110
+ }
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setCodexSpawnImplementation = setCodexSpawnImplementation;
4
+ exports.resetCodexSpawnImplementation = resetCodexSpawnImplementation;
5
+ exports.runCodexLogin = runCodexLogin;
6
+ exports.checkCodexAvailable = checkCodexAvailable;
7
+ exports.readCodexVersion = readCodexVersion;
8
+ exports.checkCodexVersion = checkCodexVersion;
9
+ const node_child_process_1 = require("node:child_process");
10
+ const errors_1 = require("../domain/errors");
11
+ let spawnImplementation = node_child_process_1.spawnSync;
12
+ function getCodexInvocation(args) {
13
+ if (process.platform === "win32") {
14
+ return {
15
+ command: process.env.ComSpec || "cmd.exe",
16
+ args: ["/d", "/s", "/c", ["codex", ...args].join(" ")],
17
+ };
18
+ }
19
+ return {
20
+ command: "codex",
21
+ args,
22
+ };
23
+ }
24
+ /**
25
+ * Overrides the spawn implementation for tests.
26
+ */
27
+ function setCodexSpawnImplementation(spawnLike) {
28
+ spawnImplementation = spawnLike;
29
+ }
30
+ /**
31
+ * Restores the default Node spawn implementation after tests.
32
+ */
33
+ function resetCodexSpawnImplementation() {
34
+ spawnImplementation = node_child_process_1.spawnSync;
35
+ }
36
+ /**
37
+ * Runs `codex login --with-api-key` in the target Codex directory.
38
+ */
39
+ function runCodexLogin(apiKey, workingDir) {
40
+ const invocation = getCodexInvocation(["login", "--with-api-key"]);
41
+ const result = spawnImplementation(invocation.command, invocation.args, {
42
+ cwd: workingDir,
43
+ input: `${apiKey}\n`,
44
+ stdio: "pipe",
45
+ encoding: "utf8",
46
+ });
47
+ if (result.error || result.status !== 0) {
48
+ throw (0, errors_1.cliError)("CODEX_LOGIN_FAILED", "codex login --with-api-key failed.", {
49
+ cause: result.error?.message ?? (result.stderr.trim() || "Unknown codex login failure"),
50
+ });
51
+ }
52
+ }
53
+ /**
54
+ * Checks whether the Codex CLI is available on PATH.
55
+ */
56
+ function checkCodexAvailable() {
57
+ const invocation = getCodexInvocation(["--version"]);
58
+ const result = spawnImplementation(invocation.command, invocation.args, {
59
+ stdio: "pipe",
60
+ encoding: "utf8",
61
+ });
62
+ if (result.error || result.status !== 0) {
63
+ return {
64
+ ok: false,
65
+ cause: result.error?.message ?? (result.stderr.trim() || "Unknown failure"),
66
+ };
67
+ }
68
+ return { ok: true };
69
+ }
70
+ /**
71
+ * Reads the installed codex CLI version string.
72
+ */
73
+ function readCodexVersion() {
74
+ const invocation = getCodexInvocation(["--version"]);
75
+ const result = spawnImplementation(invocation.command, invocation.args, {
76
+ stdio: "pipe",
77
+ encoding: "utf8",
78
+ });
79
+ if (result.error || result.status !== 0) {
80
+ return {
81
+ ok: false,
82
+ cause: result.error?.message ?? (result.stderr.trim() || "Unknown failure"),
83
+ };
84
+ }
85
+ const raw = `${result.stdout ?? ""} ${result.stderr ?? ""}`.trim();
86
+ const match = raw.match(/(\d+\.\d+\.\d+)/);
87
+ if (!match) {
88
+ return {
89
+ ok: false,
90
+ cause: `Unable to parse codex version from output: ${raw || "(empty output)"}`,
91
+ };
92
+ }
93
+ return { ok: true, version: match[1] };
94
+ }
95
+ /**
96
+ * Compares the installed codex version against a minimum required version.
97
+ */
98
+ function checkCodexVersion(minVersion) {
99
+ const current = readCodexVersion();
100
+ if (!current.ok) {
101
+ return {
102
+ ok: false,
103
+ cause: current.cause,
104
+ };
105
+ }
106
+ if (compareVersions(current.version, minVersion) < 0) {
107
+ return {
108
+ ok: false,
109
+ currentVersion: current.version,
110
+ cause: `codex ${current.version} is below required ${minVersion}`,
111
+ };
112
+ }
113
+ return {
114
+ ok: true,
115
+ currentVersion: current.version,
116
+ };
117
+ }
118
+ function compareVersions(left, right) {
119
+ const leftParts = left.split(".").map((value) => Number.parseInt(value, 10));
120
+ const rightParts = right.split(".").map((value) => Number.parseInt(value, 10));
121
+ const length = Math.max(leftParts.length, rightParts.length);
122
+ for (let index = 0; index < length; index += 1) {
123
+ const leftValue = leftParts[index] ?? 0;
124
+ const rightValue = rightParts[index] ?? 0;
125
+ if (leftValue !== rightValue) {
126
+ return leftValue - rightValue;
127
+ }
128
+ }
129
+ return 0;
130
+ }
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.codexRuntimeProbe = void 0;
4
+ exports.probeCodexRuntime = probeCodexRuntime;
5
+ const codex_cli_1 = require("./codex-cli");
6
+ /**
7
+ * Default dependency probe implementation for the local codex CLI runtime.
8
+ */
9
+ exports.codexRuntimeProbe = {
10
+ probe(options) {
11
+ if (options?.minVersion) {
12
+ return probeCodexRuntime(options.minVersion);
13
+ }
14
+ return probeCodexRuntime();
15
+ },
16
+ };
17
+ /**
18
+ * Checks whether the codex CLI is installed and, optionally, satisfies a minimum version.
19
+ */
20
+ function probeCodexRuntime(minVersion) {
21
+ const availability = (0, codex_cli_1.checkCodexAvailable)();
22
+ if (!availability.ok) {
23
+ return {
24
+ ok: false,
25
+ runtime: "codex",
26
+ reason: "missing",
27
+ cause: availability.cause ?? "Unknown codex availability failure",
28
+ };
29
+ }
30
+ const versionInfo = (0, codex_cli_1.readCodexVersion)();
31
+ if (!versionInfo.ok) {
32
+ return {
33
+ ok: false,
34
+ runtime: "codex",
35
+ reason: "failed",
36
+ cause: versionInfo.cause,
37
+ };
38
+ }
39
+ if (minVersion) {
40
+ // Reuse the dedicated semver check so doctor and setup report the same unsupported-version behavior.
41
+ const versionCheck = (0, codex_cli_1.checkCodexVersion)(minVersion);
42
+ if (!versionCheck.ok) {
43
+ return {
44
+ ok: false,
45
+ runtime: "codex",
46
+ reason: "unsupported",
47
+ cause: versionCheck.cause ?? `codex ${versionInfo.version} is below required ${minVersion}`,
48
+ version: versionCheck.currentVersion,
49
+ };
50
+ }
51
+ }
52
+ return {
53
+ ok: true,
54
+ runtime: "codex",
55
+ version: versionInfo.version,
56
+ };
57
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,160 @@
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.readAuthFileIfExists = readAuthFileIfExists;
37
+ exports.buildManagedAuthPayload = buildManagedAuthPayload;
38
+ exports.buildManagedAuthJson = buildManagedAuthJson;
39
+ exports.writeAuthFile = writeAuthFile;
40
+ exports.extractManagedAuthFingerprint = extractManagedAuthFingerprint;
41
+ exports.readManagedAuthState = readManagedAuthState;
42
+ const fs = __importStar(require("node:fs"));
43
+ const errors_1 = require("../domain/errors");
44
+ const fs_utils_1 = require("./fs-utils");
45
+ const LEGACY_MANAGED_SECRET_KEYS = new Set(["api_key"]);
46
+ /**
47
+ * Reads auth.json when it exists and returns null otherwise.
48
+ */
49
+ function readAuthFileIfExists(authPath) {
50
+ if (!fs.existsSync(authPath)) {
51
+ return null;
52
+ }
53
+ try {
54
+ return JSON.parse(fs.readFileSync(authPath, "utf8"));
55
+ }
56
+ catch (error) {
57
+ throw (0, errors_1.cliError)("AUTH_JSON_INVALID", "Failed to parse auth.json.", {
58
+ file: authPath,
59
+ cause: (0, errors_1.normalizeError)(error).message,
60
+ });
61
+ }
62
+ }
63
+ /**
64
+ * Builds the stable managed auth payload for one provider.
65
+ */
66
+ function buildManagedAuthPayload(provider) {
67
+ return {
68
+ auth_mode: "apikey",
69
+ [provider.envKey]: provider.apiKey,
70
+ };
71
+ }
72
+ /**
73
+ * Builds the next auth.json object while preserving unmanaged metadata.
74
+ */
75
+ function buildManagedAuthJson(provider, existingAuthJson) {
76
+ const nextManaged = buildManagedAuthPayload(provider);
77
+ const result = {};
78
+ if (existingAuthJson && typeof existingAuthJson === "object" && !Array.isArray(existingAuthJson)) {
79
+ for (const [key, value] of Object.entries(existingAuthJson)) {
80
+ if (key === "auth_mode" || LEGACY_MANAGED_SECRET_KEYS.has(key) || looksLikeManagedSecretKey(key)) {
81
+ continue;
82
+ }
83
+ result[key] = value;
84
+ }
85
+ }
86
+ result.auth_mode = nextManaged.auth_mode;
87
+ result[provider.envKey] = provider.apiKey;
88
+ return result;
89
+ }
90
+ /**
91
+ * Writes auth.json atomically using the managed mirror strategy.
92
+ */
93
+ function writeAuthFile(authPath, provider, existingAuthJson) {
94
+ (0, fs_utils_1.writeTextFileAtomic)(authPath, `${JSON.stringify(buildManagedAuthJson(provider, existingAuthJson), null, 2)}\n`);
95
+ }
96
+ /**
97
+ * Extracts a lightweight fingerprint used by doctor/status.
98
+ */
99
+ function extractManagedAuthFingerprint(input) {
100
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
101
+ return {
102
+ authMode: null,
103
+ managedSecretKeys: [],
104
+ payload: null,
105
+ };
106
+ }
107
+ const payload = input;
108
+ const authMode = typeof payload.auth_mode === "string" ? payload.auth_mode : null;
109
+ const managedSecretKeys = Object.keys(payload)
110
+ .filter((key) => key !== "auth_mode" && looksLikeManagedSecretKey(key))
111
+ .sort();
112
+ return {
113
+ authMode,
114
+ managedSecretKeys,
115
+ payload,
116
+ };
117
+ }
118
+ /**
119
+ * Reads auth.json into a doctor-friendly managed state summary.
120
+ */
121
+ function readManagedAuthState(authPath) {
122
+ if (!fs.existsSync(authPath)) {
123
+ return {
124
+ exists: false,
125
+ valid: false,
126
+ parseError: null,
127
+ authMode: null,
128
+ managedSecretKeys: [],
129
+ payload: null,
130
+ };
131
+ }
132
+ try {
133
+ const payload = readAuthFileIfExists(authPath);
134
+ const fingerprint = extractManagedAuthFingerprint(payload);
135
+ return {
136
+ exists: true,
137
+ valid: Boolean(fingerprint.payload),
138
+ parseError: null,
139
+ authMode: fingerprint.authMode,
140
+ managedSecretKeys: fingerprint.managedSecretKeys,
141
+ payload: fingerprint.payload,
142
+ };
143
+ }
144
+ catch (error) {
145
+ return {
146
+ exists: true,
147
+ valid: false,
148
+ parseError: (0, errors_1.normalizeError)(error).message,
149
+ authMode: null,
150
+ managedSecretKeys: [],
151
+ payload: null,
152
+ };
153
+ }
154
+ }
155
+ function looksLikeManagedSecretKey(key) {
156
+ if (LEGACY_MANAGED_SECRET_KEYS.has(key)) {
157
+ return true;
158
+ }
159
+ return /^[A-Z0-9_]+$/.test(key);
160
+ }
@@ -0,0 +1,210 @@
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.createBackup = createBackup;
37
+ exports.restoreManifest = restoreManifest;
38
+ exports.saveLatestManifest = saveLatestManifest;
39
+ exports.loadLatestManifest = loadLatestManifest;
40
+ exports.loadManifestById = loadManifestById;
41
+ exports.listBackups = listBackups;
42
+ const fs = __importStar(require("node:fs"));
43
+ const path = __importStar(require("node:path"));
44
+ const backups_1 = require("../domain/backups");
45
+ const errors_1 = require("../domain/errors");
46
+ const fs_utils_1 = require("./fs-utils");
47
+ /**
48
+ * Creates a point-in-time backup for the managed files involved in a mutation.
49
+ */
50
+ function createBackup(codexDir, backupsDir, reason, files) {
51
+ try {
52
+ const backupDir = path.join(backupsDir, `${createTimestamp()}-${reason}`);
53
+ (0, fs_utils_1.ensureDir)(backupsDir);
54
+ (0, fs_utils_1.ensureDir)(backupDir);
55
+ const entries = [];
56
+ for (const file of files) {
57
+ const exists = fs.existsSync(file.absolutePath);
58
+ const backupFileName = exists ? file.relativePath.replace(/[\\/]/g, "__") : null;
59
+ if (exists && backupFileName) {
60
+ // Flatten relative paths into a single filename inside the backup directory.
61
+ fs.copyFileSync(file.absolutePath, path.join(backupDir, backupFileName));
62
+ }
63
+ entries.push({
64
+ relativePath: file.relativePath,
65
+ existed: exists,
66
+ backupFileName,
67
+ });
68
+ }
69
+ const manifest = {
70
+ version: 1,
71
+ createdAt: new Date().toISOString(),
72
+ reason,
73
+ rootDir: codexDir,
74
+ backupDir,
75
+ files: entries,
76
+ };
77
+ (0, fs_utils_1.writeTextFileAtomic)(path.join(backupDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
78
+ return manifest;
79
+ }
80
+ catch (error) {
81
+ throw (0, errors_1.cliError)("BACKUP_FAILED", "Failed to create backup.", {
82
+ cause: (0, errors_1.normalizeError)(error).message,
83
+ });
84
+ }
85
+ }
86
+ /**
87
+ * Restores all files described by a backup manifest back into the Codex directory.
88
+ */
89
+ function restoreManifest(manifest) {
90
+ for (const entry of manifest.files) {
91
+ const targetPath = path.join(manifest.rootDir, entry.relativePath);
92
+ if (!entry.existed) {
93
+ if (fs.existsSync(targetPath)) {
94
+ // Remove files that were created by the failed mutation but were absent before it.
95
+ fs.rmSync(targetPath, { force: true });
96
+ }
97
+ continue;
98
+ }
99
+ if (!entry.backupFileName) {
100
+ throw new Error(`Backup file for ${entry.relativePath} is missing from manifest.`);
101
+ }
102
+ const sourcePath = path.join(manifest.backupDir, entry.backupFileName);
103
+ if (!fs.existsSync(sourcePath)) {
104
+ throw new Error(`Backup file not found: ${sourcePath}`);
105
+ }
106
+ (0, fs_utils_1.ensureDir)(path.dirname(targetPath));
107
+ fs.copyFileSync(sourcePath, targetPath);
108
+ }
109
+ }
110
+ /**
111
+ * Persists the latest successful backup manifest for manual rollback.
112
+ */
113
+ function saveLatestManifest(latestBackupPath, manifest) {
114
+ (0, fs_utils_1.writeTextFileAtomic)(latestBackupPath, `${JSON.stringify(manifest, null, 2)}\n`);
115
+ }
116
+ /**
117
+ * Loads and validates the latest rollback manifest file.
118
+ */
119
+ function loadLatestManifest(latestBackupPath) {
120
+ if (!fs.existsSync(latestBackupPath)) {
121
+ throw (0, errors_1.cliError)("BACKUP_NOT_FOUND", "No rollback backup is available.", {
122
+ file: latestBackupPath,
123
+ });
124
+ }
125
+ try {
126
+ return (0, backups_1.validateBackupManifest)(JSON.parse(fs.readFileSync(latestBackupPath, "utf8")));
127
+ }
128
+ catch (error) {
129
+ throw (0, errors_1.cliError)("ROLLBACK_FAILED", "Failed to read latest backup manifest.", {
130
+ file: latestBackupPath,
131
+ cause: (0, errors_1.normalizeError)(error).message,
132
+ });
133
+ }
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
+ }
195
+ /**
196
+ * Formats a filesystem-safe timestamp for backup directory names.
197
+ */
198
+ function createTimestamp() {
199
+ const now = new Date();
200
+ const pad = (value) => value.toString().padStart(2, "0");
201
+ return [
202
+ now.getFullYear().toString(),
203
+ pad(now.getMonth() + 1),
204
+ pad(now.getDate()),
205
+ "-",
206
+ pad(now.getHours()),
207
+ pad(now.getMinutes()),
208
+ pad(now.getSeconds()),
209
+ ].join("");
210
+ }