@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/README.AI.md +8 -3
- package/README.md +160 -91
- package/dist/app/edit-provider.js +64 -0
- package/dist/app/import-providers.js +10 -1
- package/dist/app/list-backups.js +17 -0
- package/dist/app/rollback-backup.js +30 -0
- package/dist/app/setup-codex.js +138 -0
- package/dist/app/show-provider.js +22 -0
- package/dist/cli/add-interactive.js +25 -31
- package/dist/cli/args.js +8 -5
- package/dist/cli/help.js +70 -12
- package/dist/cli/interactive.js +67 -8
- package/dist/cli/output.js +34 -1
- package/dist/cli/prompt.js +19 -2
- package/dist/cli.js +160 -12
- package/dist/domain/backups.js +103 -0
- package/dist/domain/errors.js +3 -3
- package/dist/domain/providers.js +10 -0
- package/dist/domain/setup.js +30 -0
- package/dist/infra/backup-repo.js +65 -6
- package/dist/infra/codex-cli.js +62 -0
- package/dist/infra/codex-discovery.js +48 -0
- package/dist/infra/codex-paths.js +14 -1
- package/dist/infra/providers-repo.js +29 -0
- package/docs/PRD/codex-switch-prd-v0.1.0.md +393 -0
- package/docs/{codex-switch-prd.md → PRD/codex-switch-prd.md} +9 -5
- package/docs/cli-usage.md +580 -0
- package/docs/codex-switch-command-design.md +1 -1
- package/docs/codex-switch-product-overview.md +1 -1
- package/docs/codex-switch-product-research.md +2 -2
- package/docs/codex-switch-technical-architecture.md +1 -1
- package/docs/codex-switch-v0.0.4-design.md +874 -0
- package/package.json +1 -1
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
|
|
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.
|
|
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)("
|
|
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)("
|
|
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)("
|
|
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)("
|
|
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])
|
|
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)("
|
|
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,
|
|
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)("
|
|
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
|
+
}
|
package/dist/domain/errors.js
CHANGED
|
@@ -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 ?? "
|
|
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: "
|
|
28
|
+
code: "INVALID_ARGUMENT",
|
|
29
29
|
message: error.message,
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
32
|
return {
|
|
33
|
-
code: "
|
|
33
|
+
code: "INVALID_ARGUMENT",
|
|
34
34
|
message: String(error),
|
|
35
35
|
};
|
|
36
36
|
}
|
package/dist/domain/providers.js
CHANGED
|
@@ -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)("
|
|
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
|
-
|
|
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
|
*/
|
package/dist/infra/codex-cli.js
CHANGED
|
@@ -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
|
+
}
|