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