@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
|
@@ -1,29 +1,28 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.COMMON_TAG_CHOICES = void 0;
|
|
3
4
|
exports.collectAddInput = collectAddInput;
|
|
4
5
|
exports.createNonInteractiveAddError = createNonInteractiveAddError;
|
|
5
|
-
|
|
6
|
+
exports.promptTags = promptTags;
|
|
7
|
+
exports.parseTags = parseTags;
|
|
6
8
|
const errors_1 = require("../domain/errors");
|
|
9
|
+
exports.COMMON_TAG_CHOICES = ["free", "paid", "daily", "backup"];
|
|
7
10
|
/**
|
|
8
11
|
* Collects add command inputs interactively when required values are missing.
|
|
9
12
|
*/
|
|
10
|
-
async function collectAddInput(runtime, defaults, providerExists
|
|
13
|
+
async function collectAddInput(runtime, defaults, providerExists) {
|
|
11
14
|
runtime.writeLine("Interactive add mode");
|
|
12
15
|
runtime.writeLine("Provide the missing required fields. Press Enter to skip optional fields.");
|
|
13
16
|
const providerName = defaults.providerName
|
|
14
17
|
? normalizeRequiredValue(defaults.providerName)
|
|
15
18
|
: await promptProviderName(runtime, providerExists);
|
|
16
|
-
const profile = defaults.profile
|
|
17
|
-
? normalizeRequiredValue(defaults.profile)
|
|
18
|
-
: await promptProfile(runtime, configPath);
|
|
19
|
+
const profile = defaults.profile ? normalizeRequiredValue(defaults.profile) : await promptRequiredValue(runtime, "Profile");
|
|
19
20
|
const apiKey = defaults.apiKey
|
|
20
21
|
? normalizeRequiredValue(defaults.apiKey)
|
|
21
22
|
: await promptConfirmedSecret(runtime, "API key", "Confirm API key");
|
|
22
23
|
const baseUrl = defaults.baseUrl ?? normalizeOptionalValue(await runtime.inputText("Base URL (optional)"));
|
|
23
24
|
const note = defaults.note ?? normalizeOptionalValue(await runtime.inputText("Note (optional)"));
|
|
24
|
-
const tags = defaults.tags.length > 0
|
|
25
|
-
? defaults.tags
|
|
26
|
-
: parseTags(await runtime.inputText("Tags (optional, comma-separated)"));
|
|
25
|
+
const tags = defaults.tags.length > 0 ? defaults.tags : await promptTags(runtime);
|
|
27
26
|
return {
|
|
28
27
|
providerName,
|
|
29
28
|
profile,
|
|
@@ -37,7 +36,7 @@ async function collectAddInput(runtime, defaults, providerExists, configPath) {
|
|
|
37
36
|
* Throws a consistent error when interactive add is unavailable.
|
|
38
37
|
*/
|
|
39
38
|
function createNonInteractiveAddError() {
|
|
40
|
-
return (0, errors_1.cliError)("
|
|
39
|
+
return (0, errors_1.cliError)("INVALID_ARGUMENT", "add requires <provider>, --profile, and --api-key when running without an interactive TTY.", {
|
|
41
40
|
suggestion: "Run in a terminal TTY or pass all required values explicitly.",
|
|
42
41
|
});
|
|
43
42
|
}
|
|
@@ -51,26 +50,6 @@ async function promptProviderName(runtime, providerExists) {
|
|
|
51
50
|
return providerName;
|
|
52
51
|
}
|
|
53
52
|
}
|
|
54
|
-
async function promptProfile(runtime, configPath) {
|
|
55
|
-
const profileChoices = loadProfileChoices(configPath);
|
|
56
|
-
if (profileChoices.length > 0) {
|
|
57
|
-
return runtime.selectOne("Profile", profileChoices);
|
|
58
|
-
}
|
|
59
|
-
return promptRequiredValue(runtime, "Profile");
|
|
60
|
-
}
|
|
61
|
-
function loadProfileChoices(configPath) {
|
|
62
|
-
try {
|
|
63
|
-
return Array.from((0, config_repo_1.listConfigProfiles)(configPath))
|
|
64
|
-
.sort()
|
|
65
|
-
.map((profileName) => ({
|
|
66
|
-
value: profileName,
|
|
67
|
-
label: profileName,
|
|
68
|
-
}));
|
|
69
|
-
}
|
|
70
|
-
catch {
|
|
71
|
-
return [];
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
53
|
async function promptRequiredValue(runtime, label) {
|
|
75
54
|
while (true) {
|
|
76
55
|
const value = normalizeRequiredValue(await runtime.inputText(label));
|
|
@@ -106,9 +85,24 @@ function normalizeOptionalValue(value) {
|
|
|
106
85
|
const normalized = value.trim();
|
|
107
86
|
return normalized === "" ? null : normalized;
|
|
108
87
|
}
|
|
88
|
+
async function promptTags(runtime, defaults = []) {
|
|
89
|
+
const defaultPresetTags = defaults.filter(isCommonTag);
|
|
90
|
+
const defaultCustomTags = defaults.filter((tag) => !isCommonTag(tag));
|
|
91
|
+
const presetTags = await runtime.selectMany("Select tags (optional)", exports.COMMON_TAG_CHOICES.map((tag) => ({ value: tag, label: tag })), { defaultValues: defaultPresetTags });
|
|
92
|
+
const customTags = parseTags(await runtime.inputText("Custom tags (optional, comma-separated)", {
|
|
93
|
+
defaultValue: defaultCustomTags.join(", "),
|
|
94
|
+
}));
|
|
95
|
+
return dedupeTags([...presetTags, ...customTags]);
|
|
96
|
+
}
|
|
109
97
|
function parseTags(value) {
|
|
110
|
-
return value
|
|
98
|
+
return dedupeTags(value
|
|
111
99
|
.split(",")
|
|
112
100
|
.map((tag) => tag.trim())
|
|
113
|
-
.filter((tag) => tag.length > 0);
|
|
101
|
+
.filter((tag) => tag.length > 0));
|
|
102
|
+
}
|
|
103
|
+
function isCommonTag(tag) {
|
|
104
|
+
return exports.COMMON_TAG_CHOICES.includes(tag);
|
|
105
|
+
}
|
|
106
|
+
function dedupeTags(tags) {
|
|
107
|
+
return Array.from(new Set(tags));
|
|
114
108
|
}
|
package/dist/cli/args.js
CHANGED
|
@@ -21,7 +21,7 @@ function parseArgs(argv) {
|
|
|
21
21
|
if (value === "--codex-dir") {
|
|
22
22
|
const next = argv[index + 1];
|
|
23
23
|
if (!next) {
|
|
24
|
-
throw (0, errors_1.cliError)("
|
|
24
|
+
throw (0, errors_1.cliError)("INVALID_ARGUMENT", "--codex-dir requires a path value.");
|
|
25
25
|
}
|
|
26
26
|
codexDir = (0, codex_paths_1.resolveCodexDir)(next);
|
|
27
27
|
index += 1;
|
|
@@ -30,6 +30,7 @@ function parseArgs(argv) {
|
|
|
30
30
|
remaining.push(value);
|
|
31
31
|
}
|
|
32
32
|
if (remaining[0] === "help") {
|
|
33
|
+
const helpTarget = remaining[1] === "backups" && remaining[2] === "list" ? "backups" : remaining[1] ?? null;
|
|
33
34
|
return {
|
|
34
35
|
command: null,
|
|
35
36
|
positionals: [],
|
|
@@ -39,7 +40,7 @@ function parseArgs(argv) {
|
|
|
39
40
|
},
|
|
40
41
|
commandOptions: new Map(),
|
|
41
42
|
helpRequested: true,
|
|
42
|
-
helpTarget
|
|
43
|
+
helpTarget,
|
|
43
44
|
versionRequested: false,
|
|
44
45
|
};
|
|
45
46
|
}
|
|
@@ -51,11 +52,13 @@ function parseArgs(argv) {
|
|
|
51
52
|
versionRequested: true,
|
|
52
53
|
});
|
|
53
54
|
}
|
|
54
|
-
const
|
|
55
|
+
const commandToken = remaining[0] ?? null;
|
|
56
|
+
const command = commandToken === "backups" && remaining[1] === "list" ? "backups-list" : commandToken;
|
|
55
57
|
const positionals = [];
|
|
56
58
|
const commandOptions = new Map();
|
|
57
59
|
let helpRequested = false;
|
|
58
|
-
|
|
60
|
+
const startIndex = command === "backups-list" ? 2 : 1;
|
|
61
|
+
for (let index = startIndex; index < remaining.length; index += 1) {
|
|
59
62
|
const value = remaining[index];
|
|
60
63
|
if (value === "--help" || value === "-h") {
|
|
61
64
|
helpRequested = true;
|
|
@@ -86,7 +89,7 @@ function parseArgs(argv) {
|
|
|
86
89
|
},
|
|
87
90
|
commandOptions,
|
|
88
91
|
helpRequested,
|
|
89
|
-
helpTarget: helpRequested ? command : null,
|
|
92
|
+
helpTarget: helpRequested ? (command === "backups-list" ? "backups" : command) : null,
|
|
90
93
|
versionRequested: false,
|
|
91
94
|
};
|
|
92
95
|
}
|
package/dist/cli/help.js
CHANGED
|
@@ -9,6 +9,18 @@ const GROUP_TITLES = {
|
|
|
9
9
|
recovery: "Diagnostics And Recovery",
|
|
10
10
|
};
|
|
11
11
|
const COMMANDS = [
|
|
12
|
+
{
|
|
13
|
+
name: "setup",
|
|
14
|
+
group: "write",
|
|
15
|
+
summary: "Initialize providers.json from an existing Codex directory.",
|
|
16
|
+
usage: ["codexs setup [--json] [--codex-dir <path>] [--merge|--overwrite]"],
|
|
17
|
+
details: [
|
|
18
|
+
"Reads config.toml profiles, collects complete provider records, then writes providers.json under managed backup flow.",
|
|
19
|
+
"TTY mode can collect missing provider details and choose merge or overwrite when providers.json already exists.",
|
|
20
|
+
"Non-TTY and --json runs stay non-interactive and require explicit strategy when providers.json already exists.",
|
|
21
|
+
],
|
|
22
|
+
examples: ["codexs setup", "codexs setup --overwrite --json --codex-dir ~/.codex"],
|
|
23
|
+
},
|
|
12
24
|
{
|
|
13
25
|
name: "list",
|
|
14
26
|
group: "read",
|
|
@@ -20,6 +32,18 @@ const COMMANDS = [
|
|
|
20
32
|
],
|
|
21
33
|
examples: ["codexs list", "codexs list --json"],
|
|
22
34
|
},
|
|
35
|
+
{
|
|
36
|
+
name: "show",
|
|
37
|
+
group: "read",
|
|
38
|
+
summary: "Show one provider record from providers.json.",
|
|
39
|
+
usage: ["codexs show <provider> [--json] [--codex-dir <path>]"],
|
|
40
|
+
details: [
|
|
41
|
+
"Human-readable output masks apiKey by default.",
|
|
42
|
+
"TTY mode can select a missing provider interactively before showing its record.",
|
|
43
|
+
"JSON mode returns the full provider payload for local automation.",
|
|
44
|
+
],
|
|
45
|
+
examples: ["codexs show packycode", "codexs show packycode --json"],
|
|
46
|
+
},
|
|
23
47
|
{
|
|
24
48
|
name: "current",
|
|
25
49
|
group: "read",
|
|
@@ -42,6 +66,21 @@ const COMMANDS = [
|
|
|
42
66
|
],
|
|
43
67
|
examples: ["codexs status", "codexs status --json --codex-dir ./.tmp-codex"],
|
|
44
68
|
},
|
|
69
|
+
{
|
|
70
|
+
name: "edit",
|
|
71
|
+
group: "write",
|
|
72
|
+
summary: "Update fields on a single provider record.",
|
|
73
|
+
usage: [
|
|
74
|
+
"codexs edit <provider> [--profile <name>] [--api-key <key>] [--base-url <url>] [--note <text>] [--tag <tag> ...] [--json] [--codex-dir <path>]",
|
|
75
|
+
],
|
|
76
|
+
details: [
|
|
77
|
+
"Passed flags replace only the selected fields and keep the rest unchanged.",
|
|
78
|
+
"TTY mode can first select a provider, then prompt for fields when no editable options were provided.",
|
|
79
|
+
"Interactive tags use preset multi-select plus optional custom comma-separated input.",
|
|
80
|
+
"Backs up providers.json before writing.",
|
|
81
|
+
],
|
|
82
|
+
examples: ["codexs edit packycode --note primary", "codexs edit packycode --tag daily --tag paid --json"],
|
|
83
|
+
},
|
|
45
84
|
{
|
|
46
85
|
name: "add",
|
|
47
86
|
group: "write",
|
|
@@ -52,8 +91,9 @@ const COMMANDS = [
|
|
|
52
91
|
],
|
|
53
92
|
details: [
|
|
54
93
|
"Prompts only for missing required values when stdin/stdout are TTYs and --json is not set.",
|
|
55
|
-
"
|
|
94
|
+
"Interactive add collects provider name, profile, and apiKey progressively as plain text inputs.",
|
|
56
95
|
"Confirm API key when prompted interactively because the hidden prompt asks twice before writing.",
|
|
96
|
+
"Interactive tags use preset multi-select plus optional custom comma-separated input.",
|
|
57
97
|
"Automation and non-TTY environments must pass all required values explicitly.",
|
|
58
98
|
],
|
|
59
99
|
examples: [
|
|
@@ -95,10 +135,10 @@ const COMMANDS = [
|
|
|
95
135
|
usage: ["codexs import <file> [--json] [--codex-dir <path>]"],
|
|
96
136
|
details: [
|
|
97
137
|
"The file path is always explicit; there is no path wizard in this release.",
|
|
98
|
-
"TTY mode asks for confirmation before replacing the current providers registry.",
|
|
138
|
+
"TTY mode asks for confirmation before replacing or merging into the current providers registry.",
|
|
99
139
|
"Non-TTY and --json runs stay non-interactive and validate the file before writing.",
|
|
100
140
|
],
|
|
101
|
-
examples: ["codexs import ./providers.json", "codexs import ./providers.json --json"],
|
|
141
|
+
examples: ["codexs import ./providers.json", "codexs import ./providers.json --merge --json"],
|
|
102
142
|
},
|
|
103
143
|
{
|
|
104
144
|
name: "export",
|
|
@@ -112,6 +152,17 @@ const COMMANDS = [
|
|
|
112
152
|
],
|
|
113
153
|
examples: ["codexs export ./providers-backup.json", "codexs export ./providers-backup.json --force"],
|
|
114
154
|
},
|
|
155
|
+
{
|
|
156
|
+
name: "backups",
|
|
157
|
+
group: "recovery",
|
|
158
|
+
summary: "List historical backup entries.",
|
|
159
|
+
usage: ["codexs backups list [--json] [--codex-dir <path>]"],
|
|
160
|
+
details: [
|
|
161
|
+
"Enumerates backups/ manifests and returns them newest first.",
|
|
162
|
+
"Corrupt backup manifests are skipped with warnings instead of failing the whole command.",
|
|
163
|
+
],
|
|
164
|
+
examples: ["codexs backups list", "codexs backups list --json"],
|
|
165
|
+
},
|
|
115
166
|
{
|
|
116
167
|
name: "doctor",
|
|
117
168
|
group: "recovery",
|
|
@@ -126,14 +177,14 @@ const COMMANDS = [
|
|
|
126
177
|
{
|
|
127
178
|
name: "rollback",
|
|
128
179
|
group: "recovery",
|
|
129
|
-
summary: "Restore the latest managed backup.",
|
|
130
|
-
usage: ["codexs rollback [--json] [--codex-dir <path>]"],
|
|
180
|
+
summary: "Restore the latest managed backup or a specific backup id.",
|
|
181
|
+
usage: ["codexs rollback [<backup-id>] [--json] [--codex-dir <path>]"],
|
|
131
182
|
details: [
|
|
132
|
-
"TTY mode previews the
|
|
183
|
+
"TTY mode previews the target backup path and affected files, then asks for confirmation.",
|
|
133
184
|
"Non-TTY and --json runs stay non-interactive and execute immediately.",
|
|
134
185
|
"Use after a failed or undesired managed mutation.",
|
|
135
186
|
],
|
|
136
|
-
examples: ["codexs rollback", "codexs rollback --json"],
|
|
187
|
+
examples: ["codexs rollback", "codexs rollback 20260511-221457-switch --json"],
|
|
137
188
|
},
|
|
138
189
|
];
|
|
139
190
|
const COMMAND_NAME_SET = new Set(COMMANDS.map((command) => command.name));
|
|
@@ -141,9 +192,10 @@ function getKnownCommandNames() {
|
|
|
141
192
|
return COMMANDS.map((command) => command.name);
|
|
142
193
|
}
|
|
143
194
|
function isKnownCommandName(commandName) {
|
|
144
|
-
return COMMAND_NAME_SET.has(commandName);
|
|
195
|
+
return COMMAND_NAME_SET.has(commandName) || commandName === "backups-list";
|
|
145
196
|
}
|
|
146
197
|
function buildHelpText(commandName) {
|
|
198
|
+
const normalizedCommandName = commandName === "backups-list" ? "backups" : commandName;
|
|
147
199
|
if (!commandName) {
|
|
148
200
|
return [
|
|
149
201
|
"codex-switch",
|
|
@@ -162,6 +214,10 @@ function buildHelpText(commandName) {
|
|
|
162
214
|
" --help Show top-level or command-specific help.",
|
|
163
215
|
" --version Print the current CLI version.",
|
|
164
216
|
"",
|
|
217
|
+
"Environment:",
|
|
218
|
+
" CODEXS_CODEX_DIR Default Codex directory when --codex-dir is not passed.",
|
|
219
|
+
" NODE_ENV=development defaults to ./test-fixtures/sample-codex when no override is set.",
|
|
220
|
+
"",
|
|
165
221
|
"Interactive rules:",
|
|
166
222
|
" Progressive prompts only run in a real TTY and never run under --json.",
|
|
167
223
|
" Human write commands may guide missing inputs or ask for dangerous-action confirmation.",
|
|
@@ -169,23 +225,25 @@ function buildHelpText(commandName) {
|
|
|
169
225
|
"",
|
|
170
226
|
"Dangerous commands:",
|
|
171
227
|
" remove deletes provider records.",
|
|
172
|
-
" import replaces providers.json.",
|
|
228
|
+
" import replaces or merges providers.json.",
|
|
173
229
|
" export may overwrite a target file.",
|
|
174
|
-
" rollback restores files from
|
|
230
|
+
" rollback restores files from a managed backup.",
|
|
175
231
|
"",
|
|
176
232
|
"Examples:",
|
|
233
|
+
" codexs setup",
|
|
177
234
|
" codexs list",
|
|
178
235
|
" codexs switch",
|
|
179
236
|
" codexs add packycode --profile packycode --api-key sk-xxx",
|
|
180
237
|
" codexs remove freemodel",
|
|
238
|
+
" codexs backups list",
|
|
181
239
|
" codexs rollback",
|
|
182
240
|
" codexs help add",
|
|
183
241
|
].join("\n");
|
|
184
242
|
}
|
|
185
|
-
const command = COMMANDS.find((candidate) => candidate.name ===
|
|
243
|
+
const command = COMMANDS.find((candidate) => candidate.name === normalizedCommandName);
|
|
186
244
|
if (!command) {
|
|
187
245
|
return [
|
|
188
|
-
`Unknown help topic: ${
|
|
246
|
+
`Unknown help topic: ${normalizedCommandName}`,
|
|
189
247
|
"",
|
|
190
248
|
"Available commands:",
|
|
191
249
|
...getKnownCommandNames().map((name) => ` ${name}`),
|
package/dist/cli/interactive.js
CHANGED
|
@@ -40,12 +40,18 @@ exports.confirmImport = confirmImport;
|
|
|
40
40
|
exports.confirmExportOverwrite = confirmExportOverwrite;
|
|
41
41
|
exports.exportTargetExists = exportTargetExists;
|
|
42
42
|
exports.getRollbackSummary = getRollbackSummary;
|
|
43
|
+
exports.getRollbackSummaryById = getRollbackSummaryById;
|
|
43
44
|
exports.confirmRollback = confirmRollback;
|
|
45
|
+
exports.chooseSetupStrategy = chooseSetupStrategy;
|
|
46
|
+
exports.collectSetupProviderDetails = collectSetupProviderDetails;
|
|
47
|
+
exports.collectEditInput = collectEditInput;
|
|
44
48
|
const fs = __importStar(require("node:fs"));
|
|
45
49
|
const path = __importStar(require("node:path"));
|
|
46
50
|
const errors_1 = require("../domain/errors");
|
|
51
|
+
const backups_1 = require("../domain/backups");
|
|
47
52
|
const providers_repo_1 = require("../infra/providers-repo");
|
|
48
53
|
const backup_repo_1 = require("../infra/backup-repo");
|
|
54
|
+
const add_interactive_1 = require("./add-interactive");
|
|
49
55
|
/**
|
|
50
56
|
* Keeps CLI-side interactivity rules in one place so automation paths remain explicit.
|
|
51
57
|
*/
|
|
@@ -71,13 +77,15 @@ async function confirmProviderRemoval(runtime, providerName) {
|
|
|
71
77
|
defaultValue: false,
|
|
72
78
|
});
|
|
73
79
|
if (!confirmed) {
|
|
74
|
-
throw (0, errors_1.cliError)("
|
|
80
|
+
throw (0, errors_1.cliError)("PROMPT_CANCELLED", `Removal cancelled for provider "${providerName}".`);
|
|
75
81
|
}
|
|
76
82
|
}
|
|
77
|
-
async function confirmImport(runtime, sourceFile) {
|
|
78
|
-
const confirmed = await runtime.confirmAction(
|
|
83
|
+
async function confirmImport(runtime, sourceFile, merge = false) {
|
|
84
|
+
const confirmed = await runtime.confirmAction(merge
|
|
85
|
+
? `Import providers from ${path.resolve(sourceFile)} and merge into the current registry?`
|
|
86
|
+
: `Import providers from ${path.resolve(sourceFile)} and replace the current registry?`, { defaultValue: false });
|
|
79
87
|
if (!confirmed) {
|
|
80
|
-
throw (0, errors_1.cliError)("
|
|
88
|
+
throw (0, errors_1.cliError)("PROMPT_CANCELLED", "Import cancelled.");
|
|
81
89
|
}
|
|
82
90
|
}
|
|
83
91
|
async function confirmExportOverwrite(runtime, targetFile) {
|
|
@@ -90,8 +98,16 @@ function exportTargetExists(targetFile) {
|
|
|
90
98
|
}
|
|
91
99
|
function getRollbackSummary(latestBackupPath) {
|
|
92
100
|
const manifest = (0, backup_repo_1.loadLatestManifest)(latestBackupPath);
|
|
101
|
+
return buildRollbackSummary(manifest);
|
|
102
|
+
}
|
|
103
|
+
function getRollbackSummaryById(backupsDir, backupId) {
|
|
104
|
+
const manifest = (0, backup_repo_1.loadManifestById)(backupsDir, backupId);
|
|
105
|
+
return buildRollbackSummary(manifest);
|
|
106
|
+
}
|
|
107
|
+
function buildRollbackSummary(manifest) {
|
|
93
108
|
const previewLines = [
|
|
94
109
|
"Rollback preview",
|
|
110
|
+
`Backup ID: ${(0, backups_1.getBackupId)(manifest.backupDir)}`,
|
|
95
111
|
`Backup: ${manifest.backupDir}`,
|
|
96
112
|
...manifest.files.map((file) => {
|
|
97
113
|
const suffix = file.existed ? "restore" : "remove";
|
|
@@ -100,15 +116,58 @@ function getRollbackSummary(latestBackupPath) {
|
|
|
100
116
|
];
|
|
101
117
|
return { manifest, previewLines };
|
|
102
118
|
}
|
|
103
|
-
async function confirmRollback(runtime, latestBackupPath) {
|
|
104
|
-
const { previewLines } =
|
|
119
|
+
async function confirmRollback(runtime, latestBackupPath, backupsDir, backupId) {
|
|
120
|
+
const { previewLines } = backupId && backupsDir
|
|
121
|
+
? getRollbackSummaryById(backupsDir, backupId)
|
|
122
|
+
: getRollbackSummary(latestBackupPath);
|
|
105
123
|
for (const line of previewLines) {
|
|
106
124
|
runtime.writeLine(line);
|
|
107
125
|
}
|
|
108
|
-
const confirmed = await runtime.confirmAction("Restore files from the latest backup?", {
|
|
126
|
+
const confirmed = await runtime.confirmAction(backupId ? `Restore files from backup "${backupId}"?` : "Restore files from the latest backup?", {
|
|
109
127
|
defaultValue: false,
|
|
110
128
|
});
|
|
111
129
|
if (!confirmed) {
|
|
112
|
-
throw (0, errors_1.cliError)("
|
|
130
|
+
throw (0, errors_1.cliError)("PROMPT_CANCELLED", "Rollback cancelled.");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function chooseSetupStrategy(runtime) {
|
|
134
|
+
return runtime.selectOne("providers.json already exists. Choose a setup strategy.", [
|
|
135
|
+
{ value: "merge", label: "merge", hint: "keep existing providers and override by imported names" },
|
|
136
|
+
{ value: "overwrite", label: "overwrite", hint: "replace the existing registry" },
|
|
137
|
+
{ value: "cancel", label: "cancel", hint: "abort setup without writing" },
|
|
138
|
+
]);
|
|
139
|
+
}
|
|
140
|
+
async function collectSetupProviderDetails(runtime, profiles) {
|
|
141
|
+
const result = {};
|
|
142
|
+
for (const profile of profiles) {
|
|
143
|
+
const providerName = (await runtime.inputText(`Provider name for profile "${profile}"`, {
|
|
144
|
+
defaultValue: profile,
|
|
145
|
+
})).trim();
|
|
146
|
+
const apiKey = (await runtime.inputSecret(`API key for profile "${profile}"`)).trim();
|
|
147
|
+
const baseUrl = (await runtime.inputText(`Base URL for profile "${profile}" (optional)`)).trim();
|
|
148
|
+
const note = (await runtime.inputText(`Note for profile "${profile}" (optional)`)).trim();
|
|
149
|
+
const tags = await (0, add_interactive_1.promptTags)(runtime);
|
|
150
|
+
result[profile] = {
|
|
151
|
+
providerName: providerName || profile,
|
|
152
|
+
apiKey,
|
|
153
|
+
baseUrl: baseUrl || undefined,
|
|
154
|
+
note: note || undefined,
|
|
155
|
+
tags: tags.length > 0 ? tags : undefined,
|
|
156
|
+
};
|
|
113
157
|
}
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
async function collectEditInput(runtime, current) {
|
|
161
|
+
const profile = (await runtime.inputText("Profile", { defaultValue: current.profile })).trim();
|
|
162
|
+
const apiKey = (await runtime.inputSecret("API key")).trim() || current.apiKey;
|
|
163
|
+
const baseUrl = (await runtime.inputText("Base URL (optional)", { defaultValue: current.baseUrl ?? "" })).trim();
|
|
164
|
+
const note = (await runtime.inputText("Note (optional)", { defaultValue: current.note ?? "" })).trim();
|
|
165
|
+
const tags = await (0, add_interactive_1.promptTags)(runtime, current.tags ?? []);
|
|
166
|
+
return {
|
|
167
|
+
profile,
|
|
168
|
+
apiKey,
|
|
169
|
+
baseUrl: baseUrl || undefined,
|
|
170
|
+
note: note || undefined,
|
|
171
|
+
tags,
|
|
172
|
+
};
|
|
114
173
|
}
|
package/dist/cli/output.js
CHANGED
|
@@ -95,6 +95,22 @@ function renderHumanSuccess(command, data, warnings) {
|
|
|
95
95
|
}
|
|
96
96
|
break;
|
|
97
97
|
}
|
|
98
|
+
case "show": {
|
|
99
|
+
const provider = data?.provider ?? {};
|
|
100
|
+
lines.push(`Provider: ${String(data?.providerName ?? "")}`);
|
|
101
|
+
lines.push(`profile: ${String(provider.profile ?? "")}`);
|
|
102
|
+
lines.push(`apiKey: ${String(provider.apiKey ?? "")}`);
|
|
103
|
+
if (provider.baseUrl) {
|
|
104
|
+
lines.push(`baseUrl: ${String(provider.baseUrl)}`);
|
|
105
|
+
}
|
|
106
|
+
if (provider.note) {
|
|
107
|
+
lines.push(`note: ${String(provider.note)}`);
|
|
108
|
+
}
|
|
109
|
+
if (Array.isArray(provider.tags) && provider.tags.length > 0) {
|
|
110
|
+
lines.push(`tags: ${provider.tags.join(", ")}`);
|
|
111
|
+
}
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
98
114
|
case "current":
|
|
99
115
|
lines.push(`Current profile: ${String(data?.profile ?? "")}`);
|
|
100
116
|
break;
|
|
@@ -111,11 +127,21 @@ function renderHumanSuccess(command, data, warnings) {
|
|
|
111
127
|
lines.push(`Login performed: ${String(data?.loginPerformed ?? false)}`);
|
|
112
128
|
break;
|
|
113
129
|
case "import":
|
|
114
|
-
lines.push(`Imported providers from file. Backup: ${String(data?.backupPath ?? "")}`);
|
|
130
|
+
lines.push(`Imported providers from file using mode ${String(data?.mode ?? "replace")}. Backup: ${String(data?.backupPath ?? "")}`);
|
|
115
131
|
break;
|
|
116
132
|
case "export":
|
|
117
133
|
lines.push(`Exported providers to ${String(data?.exportedTo ?? "")}.`);
|
|
118
134
|
break;
|
|
135
|
+
case "setup":
|
|
136
|
+
lines.push(`Initialized providers in ${String(data?.codexDir ?? "")} using ${String(data?.strategy ?? "")}.`);
|
|
137
|
+
lines.push(`Providers initialized: ${String(data?.providersInitialized ?? 0)}`);
|
|
138
|
+
lines.push(`Doctor healthy: ${String(data?.doctor?.healthy ?? false)}`);
|
|
139
|
+
lines.push(`Backup: ${String(data?.backupPath ?? "")}`);
|
|
140
|
+
break;
|
|
141
|
+
case "edit":
|
|
142
|
+
lines.push(`Updated provider ${String(data?.provider ?? "")}. Backup: ${String(data?.backupPath ?? "")}`);
|
|
143
|
+
lines.push(`Updated fields: ${Array.isArray(data?.updatedFields) ? (data?.updatedFields).join(", ") : ""}`);
|
|
144
|
+
break;
|
|
119
145
|
case "add":
|
|
120
146
|
lines.push(`Added provider ${String(data?.provider ?? "")}. Backup: ${String(data?.backupPath ?? "")}`);
|
|
121
147
|
break;
|
|
@@ -131,6 +157,13 @@ function renderHumanSuccess(command, data, warnings) {
|
|
|
131
157
|
}
|
|
132
158
|
break;
|
|
133
159
|
}
|
|
160
|
+
case "backups-list": {
|
|
161
|
+
const backups = data?.backups ?? [];
|
|
162
|
+
for (const backup of backups) {
|
|
163
|
+
lines.push(`${backup.backupId} ${backup.reason} ${backup.createdAt}`);
|
|
164
|
+
}
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
134
167
|
case "rollback":
|
|
135
168
|
lines.push(`Rollback restored files from ${String(data?.backupPath ?? "")}.`);
|
|
136
169
|
break;
|
package/dist/cli/prompt.js
CHANGED
|
@@ -54,6 +54,23 @@ function createPromptRuntime() {
|
|
|
54
54
|
return answer.value;
|
|
55
55
|
});
|
|
56
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
|
+
},
|
|
57
74
|
confirmAction: async (message, options) => {
|
|
58
75
|
return handlePromptCancellation(async () => {
|
|
59
76
|
const answer = await inquirer_1.default.prompt([
|
|
@@ -78,9 +95,9 @@ async function handlePromptCancellation(run) {
|
|
|
78
95
|
}
|
|
79
96
|
catch (error) {
|
|
80
97
|
if (isPromptCancellation(error)) {
|
|
81
|
-
throw (0, errors_1.cliError)("
|
|
98
|
+
throw (0, errors_1.cliError)("PROMPT_CANCELLED", "Interactive prompt was cancelled.");
|
|
82
99
|
}
|
|
83
|
-
throw (0, errors_1.cliError)("
|
|
100
|
+
throw (0, errors_1.cliError)("INVALID_ARGUMENT", "Interactive prompt failed.", {
|
|
84
101
|
cause: error instanceof Error ? error.message : String(error),
|
|
85
102
|
});
|
|
86
103
|
}
|