@minniexcode/codex-switch 0.0.1 → 0.0.3
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 +105 -0
- package/README.CN.md +160 -0
- package/README.md +102 -111
- package/dist/app/add-provider.js +45 -0
- package/dist/app/export-providers.js +62 -0
- package/dist/app/get-current-profile.js +14 -0
- package/dist/app/get-status.js +75 -0
- package/dist/app/import-providers.js +74 -0
- package/dist/app/list-providers.js +23 -0
- package/dist/app/remove-provider.js +31 -0
- package/dist/app/rollback-latest.js +26 -0
- package/dist/app/run-doctor.js +130 -0
- package/dist/app/run-mutation.js +63 -0
- package/dist/app/switch-provider.js +43 -0
- package/dist/app/types.js +2 -0
- package/dist/cli/add-interactive.js +114 -0
- package/dist/cli/args.js +125 -0
- package/dist/cli/help.js +220 -0
- package/dist/cli/interactive.js +114 -0
- package/dist/cli/output.js +156 -0
- package/dist/cli/prompt.js +93 -0
- package/dist/cli.js +215 -26
- package/dist/domain/backup.js +2 -0
- package/dist/domain/config.js +106 -0
- package/dist/domain/errors.js +36 -0
- package/dist/domain/providers.js +92 -0
- package/dist/domain/runtime-state.js +56 -0
- package/dist/infra/backup-repo.js +151 -0
- package/dist/infra/codex-cli.js +53 -0
- package/dist/infra/codex-paths.js +58 -0
- package/dist/infra/config-repo.js +56 -0
- package/dist/infra/fs-utils.js +97 -0
- package/dist/infra/lock-repo.js +99 -0
- package/dist/infra/providers-repo.js +69 -0
- package/docs/codex-switch-command-design.md +646 -0
- package/docs/codex-switch-prd.md +24 -3
- package/docs/codex-switch-product-overview.md +2 -0
- package/docs/codex-switch-technical-architecture.md +1042 -0
- package/package.json +7 -4
package/dist/cli/args.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseArgs = parseArgs;
|
|
4
|
+
exports.hasFlag = hasFlag;
|
|
5
|
+
exports.getSingleOption = getSingleOption;
|
|
6
|
+
const errors_1 = require("../domain/errors");
|
|
7
|
+
const codex_paths_1 = require("../infra/codex-paths");
|
|
8
|
+
/**
|
|
9
|
+
* Parses argv into command positionals, global flags, and command-scoped options.
|
|
10
|
+
*/
|
|
11
|
+
function parseArgs(argv) {
|
|
12
|
+
let json = false;
|
|
13
|
+
let codexDir = (0, codex_paths_1.resolveCodexDir)();
|
|
14
|
+
const remaining = [];
|
|
15
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
16
|
+
const value = argv[index];
|
|
17
|
+
if (value === "--json") {
|
|
18
|
+
json = true;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (value === "--codex-dir") {
|
|
22
|
+
const next = argv[index + 1];
|
|
23
|
+
if (!next) {
|
|
24
|
+
throw (0, errors_1.cliError)("INVALID_IMPORT_FILE", "--codex-dir requires a path value.");
|
|
25
|
+
}
|
|
26
|
+
codexDir = (0, codex_paths_1.resolveCodexDir)(next);
|
|
27
|
+
index += 1;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
remaining.push(value);
|
|
31
|
+
}
|
|
32
|
+
if (remaining[0] === "help") {
|
|
33
|
+
return {
|
|
34
|
+
command: null,
|
|
35
|
+
positionals: [],
|
|
36
|
+
globalOptions: {
|
|
37
|
+
json,
|
|
38
|
+
codexDir,
|
|
39
|
+
},
|
|
40
|
+
commandOptions: new Map(),
|
|
41
|
+
helpRequested: true,
|
|
42
|
+
helpTarget: remaining[1] ?? null,
|
|
43
|
+
versionRequested: false,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const versionRequested = remaining.includes("--version") || remaining.includes("-v");
|
|
47
|
+
if (versionRequested) {
|
|
48
|
+
return defaultParsed(null, {
|
|
49
|
+
json,
|
|
50
|
+
codexDir,
|
|
51
|
+
versionRequested: true,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
const command = remaining[0] ?? null;
|
|
55
|
+
const positionals = [];
|
|
56
|
+
const commandOptions = new Map();
|
|
57
|
+
let helpRequested = false;
|
|
58
|
+
for (let index = 1; index < remaining.length; index += 1) {
|
|
59
|
+
const value = remaining[index];
|
|
60
|
+
if (value === "--help" || value === "-h") {
|
|
61
|
+
helpRequested = true;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (value.startsWith("--")) {
|
|
65
|
+
const optionName = value;
|
|
66
|
+
const next = remaining[index + 1];
|
|
67
|
+
if (!next || next.startsWith("--")) {
|
|
68
|
+
// Boolean flags are stored as "true" so later access uses one uniform map shape.
|
|
69
|
+
commandOptions.set(optionName, ["true"]);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const existing = commandOptions.get(optionName) ?? [];
|
|
73
|
+
existing.push(next);
|
|
74
|
+
commandOptions.set(optionName, existing);
|
|
75
|
+
index += 1;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
positionals.push(value);
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
command,
|
|
82
|
+
positionals,
|
|
83
|
+
globalOptions: {
|
|
84
|
+
json,
|
|
85
|
+
codexDir,
|
|
86
|
+
},
|
|
87
|
+
commandOptions,
|
|
88
|
+
helpRequested,
|
|
89
|
+
helpTarget: helpRequested ? command : null,
|
|
90
|
+
versionRequested: false,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Creates a parsed result for built-in synthetic commands such as help/version.
|
|
95
|
+
*/
|
|
96
|
+
function defaultParsed(command, overrides) {
|
|
97
|
+
return {
|
|
98
|
+
command,
|
|
99
|
+
positionals: [],
|
|
100
|
+
globalOptions: {
|
|
101
|
+
json: overrides?.json ?? false,
|
|
102
|
+
codexDir: overrides?.codexDir ?? (0, codex_paths_1.resolveCodexDir)(),
|
|
103
|
+
},
|
|
104
|
+
commandOptions: new Map(),
|
|
105
|
+
helpRequested: overrides?.helpRequested ?? false,
|
|
106
|
+
helpTarget: overrides?.helpTarget ?? null,
|
|
107
|
+
versionRequested: overrides?.versionRequested ?? false,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Checks whether a boolean-style option was supplied.
|
|
112
|
+
*/
|
|
113
|
+
function hasFlag(options, name) {
|
|
114
|
+
return options.has(name);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Returns the last supplied value for a single-valued command option.
|
|
118
|
+
*/
|
|
119
|
+
function getSingleOption(options, name, required = true) {
|
|
120
|
+
const values = options.get(name) ?? [];
|
|
121
|
+
if (values.length === 0) {
|
|
122
|
+
return required ? null : null;
|
|
123
|
+
}
|
|
124
|
+
return values[values.length - 1];
|
|
125
|
+
}
|
package/dist/cli/help.js
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getKnownCommandNames = getKnownCommandNames;
|
|
4
|
+
exports.isKnownCommandName = isKnownCommandName;
|
|
5
|
+
exports.buildHelpText = buildHelpText;
|
|
6
|
+
const GROUP_TITLES = {
|
|
7
|
+
read: "Read Commands",
|
|
8
|
+
write: "Change Commands",
|
|
9
|
+
recovery: "Diagnostics And Recovery",
|
|
10
|
+
};
|
|
11
|
+
const COMMANDS = [
|
|
12
|
+
{
|
|
13
|
+
name: "list",
|
|
14
|
+
group: "read",
|
|
15
|
+
summary: "List configured providers from providers.json.",
|
|
16
|
+
usage: ["codexs list [--json] [--codex-dir <path>]"],
|
|
17
|
+
details: [
|
|
18
|
+
"Reads providers.json and prints provider-to-profile mappings.",
|
|
19
|
+
"Use --json for machine-readable automation output.",
|
|
20
|
+
],
|
|
21
|
+
examples: ["codexs list", "codexs list --json"],
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: "current",
|
|
25
|
+
group: "read",
|
|
26
|
+
summary: "Show the active top-level profile from config.toml.",
|
|
27
|
+
usage: ["codexs current [--json] [--codex-dir <path>]"],
|
|
28
|
+
details: [
|
|
29
|
+
"Reads the currently active top-level profile.",
|
|
30
|
+
"Fails when config.toml is missing or has no top-level profile.",
|
|
31
|
+
],
|
|
32
|
+
examples: ["codexs current", "codexs current --json"],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "status",
|
|
36
|
+
group: "read",
|
|
37
|
+
summary: "Show a quick status summary for the local Codex directory.",
|
|
38
|
+
usage: ["codexs status [--json] [--codex-dir <path>]"],
|
|
39
|
+
details: [
|
|
40
|
+
"Reports file presence, current profile, and whether the live profile is mapped.",
|
|
41
|
+
"Use doctor for deeper diagnostics.",
|
|
42
|
+
],
|
|
43
|
+
examples: ["codexs status", "codexs status --json --codex-dir ./.tmp-codex"],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: "add",
|
|
47
|
+
group: "write",
|
|
48
|
+
summary: "Add a provider with explicit flags or progressive TTY prompts.",
|
|
49
|
+
usage: [
|
|
50
|
+
"codexs add <provider> --profile <name> --api-key <key> [--base-url <url>] [--note <text>] [--tag <tag> ...]",
|
|
51
|
+
"codexs add [--profile <name>] [--api-key <key>] [--base-url <url>] [--note <text>] [--tag <tag> ...]",
|
|
52
|
+
],
|
|
53
|
+
details: [
|
|
54
|
+
"Prompts only for missing required values when stdin/stdout are TTYs and --json is not set.",
|
|
55
|
+
"Profile selection prefers existing config.toml profiles, then falls back to free-text input.",
|
|
56
|
+
"Confirm API key when prompted interactively because the hidden prompt asks twice before writing.",
|
|
57
|
+
"Automation and non-TTY environments must pass all required values explicitly.",
|
|
58
|
+
],
|
|
59
|
+
examples: [
|
|
60
|
+
"codexs add packycode --profile packycode --api-key sk-xxx",
|
|
61
|
+
"codexs add packycode --profile packycode",
|
|
62
|
+
"codexs add",
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: "switch",
|
|
67
|
+
group: "write",
|
|
68
|
+
summary: "Switch to a provider and optionally refresh Codex login.",
|
|
69
|
+
usage: ["codexs switch <provider> [--no-login] [--json] [--codex-dir <path>]"],
|
|
70
|
+
details: [
|
|
71
|
+
"When <provider> is omitted in a TTY, an interactive provider selector is shown.",
|
|
72
|
+
"When <provider> is passed explicitly, switch proceeds directly without extra confirmation.",
|
|
73
|
+
"--no-login remains explicit and is never prompted interactively.",
|
|
74
|
+
"Backs up config.toml and auth.json, then rolls back on failure.",
|
|
75
|
+
],
|
|
76
|
+
examples: ["codexs switch freemodel", "codexs switch --no-login", "codexs switch freemodel --no-login --json"],
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: "remove",
|
|
80
|
+
group: "write",
|
|
81
|
+
summary: "Remove a provider from providers.json.",
|
|
82
|
+
usage: ["codexs remove <provider> [--force] [--json] [--codex-dir <path>]"],
|
|
83
|
+
details: [
|
|
84
|
+
"TTY mode can select a missing provider interactively and always asks for deletion confirmation.",
|
|
85
|
+
"Non-TTY and --json automation still require both <provider> and --force.",
|
|
86
|
+
"The confirmation prompt includes the provider name and cancels without writing when declined.",
|
|
87
|
+
"Backs up providers.json before removing the record.",
|
|
88
|
+
],
|
|
89
|
+
examples: ["codexs remove freemodel", "codexs remove freemodel --force --json"],
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: "import",
|
|
93
|
+
group: "write",
|
|
94
|
+
summary: "Replace providers.json with an external JSON file.",
|
|
95
|
+
usage: ["codexs import <file> [--json] [--codex-dir <path>]"],
|
|
96
|
+
details: [
|
|
97
|
+
"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.",
|
|
99
|
+
"Non-TTY and --json runs stay non-interactive and validate the file before writing.",
|
|
100
|
+
],
|
|
101
|
+
examples: ["codexs import ./providers.json", "codexs import ./providers.json --json"],
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: "export",
|
|
105
|
+
group: "write",
|
|
106
|
+
summary: "Export the current providers.json to another file.",
|
|
107
|
+
usage: ["codexs export <file> [--force] [--json] [--codex-dir <path>]"],
|
|
108
|
+
details: [
|
|
109
|
+
"The file path is always explicit; there is no path wizard in this release.",
|
|
110
|
+
"TTY mode asks before overwriting an existing target when --force is not supplied.",
|
|
111
|
+
"Non-TTY and --json automation require --force to overwrite an existing file.",
|
|
112
|
+
],
|
|
113
|
+
examples: ["codexs export ./providers-backup.json", "codexs export ./providers-backup.json --force"],
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: "doctor",
|
|
117
|
+
group: "recovery",
|
|
118
|
+
summary: "Run configuration and environment diagnostics.",
|
|
119
|
+
usage: ["codexs doctor [--json] [--codex-dir <path>]"],
|
|
120
|
+
details: [
|
|
121
|
+
"Checks the expected config files, provider/profile consistency, and Codex CLI availability.",
|
|
122
|
+
"Returns structured issues so users and AI agents can act on them.",
|
|
123
|
+
],
|
|
124
|
+
examples: ["codexs doctor", "codexs doctor --json"],
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: "rollback",
|
|
128
|
+
group: "recovery",
|
|
129
|
+
summary: "Restore the latest managed backup.",
|
|
130
|
+
usage: ["codexs rollback [--json] [--codex-dir <path>]"],
|
|
131
|
+
details: [
|
|
132
|
+
"TTY mode previews the latest backup path and affected files, then asks for confirmation.",
|
|
133
|
+
"Non-TTY and --json runs stay non-interactive and execute immediately.",
|
|
134
|
+
"Use after a failed or undesired managed mutation.",
|
|
135
|
+
],
|
|
136
|
+
examples: ["codexs rollback", "codexs rollback --json"],
|
|
137
|
+
},
|
|
138
|
+
];
|
|
139
|
+
const COMMAND_NAME_SET = new Set(COMMANDS.map((command) => command.name));
|
|
140
|
+
function getKnownCommandNames() {
|
|
141
|
+
return COMMANDS.map((command) => command.name);
|
|
142
|
+
}
|
|
143
|
+
function isKnownCommandName(commandName) {
|
|
144
|
+
return COMMAND_NAME_SET.has(commandName);
|
|
145
|
+
}
|
|
146
|
+
function buildHelpText(commandName) {
|
|
147
|
+
if (!commandName) {
|
|
148
|
+
return [
|
|
149
|
+
"codex-switch",
|
|
150
|
+
"",
|
|
151
|
+
"Manage and switch local Codex provider/profile configuration safely.",
|
|
152
|
+
"",
|
|
153
|
+
"Usage:",
|
|
154
|
+
" codexs <command> [options]",
|
|
155
|
+
" codexs help <command>",
|
|
156
|
+
"",
|
|
157
|
+
...renderGroupedCommands(),
|
|
158
|
+
"",
|
|
159
|
+
"Global options:",
|
|
160
|
+
" --json Output the standard JSON envelope and disable all prompts.",
|
|
161
|
+
" --codex-dir <path> Target a specific Codex directory instead of ~/.codex.",
|
|
162
|
+
" --help Show top-level or command-specific help.",
|
|
163
|
+
" --version Print the current CLI version.",
|
|
164
|
+
"",
|
|
165
|
+
"Interactive rules:",
|
|
166
|
+
" Progressive prompts only run in a real TTY and never run under --json.",
|
|
167
|
+
" Human write commands may guide missing inputs or ask for dangerous-action confirmation.",
|
|
168
|
+
" Automation should pass explicit arguments and prefer --json for stable parsing.",
|
|
169
|
+
"",
|
|
170
|
+
"Dangerous commands:",
|
|
171
|
+
" remove deletes provider records.",
|
|
172
|
+
" import replaces providers.json.",
|
|
173
|
+
" export may overwrite a target file.",
|
|
174
|
+
" rollback restores files from the latest backup.",
|
|
175
|
+
"",
|
|
176
|
+
"Examples:",
|
|
177
|
+
" codexs list",
|
|
178
|
+
" codexs switch",
|
|
179
|
+
" codexs add packycode --profile packycode --api-key sk-xxx",
|
|
180
|
+
" codexs remove freemodel",
|
|
181
|
+
" codexs rollback",
|
|
182
|
+
" codexs help add",
|
|
183
|
+
].join("\n");
|
|
184
|
+
}
|
|
185
|
+
const command = COMMANDS.find((candidate) => candidate.name === commandName);
|
|
186
|
+
if (!command) {
|
|
187
|
+
return [
|
|
188
|
+
`Unknown help topic: ${commandName}`,
|
|
189
|
+
"",
|
|
190
|
+
"Available commands:",
|
|
191
|
+
...getKnownCommandNames().map((name) => ` ${name}`),
|
|
192
|
+
].join("\n");
|
|
193
|
+
}
|
|
194
|
+
return [
|
|
195
|
+
`codexs ${command.name}`,
|
|
196
|
+
"",
|
|
197
|
+
command.summary,
|
|
198
|
+
"",
|
|
199
|
+
"Usage:",
|
|
200
|
+
...command.usage.map((usage) => ` ${usage}`),
|
|
201
|
+
"",
|
|
202
|
+
"Details:",
|
|
203
|
+
...command.details.map((detail) => ` ${detail}`),
|
|
204
|
+
"",
|
|
205
|
+
"Examples:",
|
|
206
|
+
...command.examples.map((example) => ` ${example}`),
|
|
207
|
+
].join("\n");
|
|
208
|
+
}
|
|
209
|
+
function renderGroupedCommands() {
|
|
210
|
+
const lines = [];
|
|
211
|
+
for (const group of ["read", "write", "recovery"]) {
|
|
212
|
+
lines.push(`${GROUP_TITLES[group]}:`);
|
|
213
|
+
for (const command of COMMANDS.filter((candidate) => candidate.group === group)) {
|
|
214
|
+
lines.push(` ${command.name.padEnd(8, " ")} ${command.summary}`);
|
|
215
|
+
}
|
|
216
|
+
lines.push("");
|
|
217
|
+
}
|
|
218
|
+
lines.pop();
|
|
219
|
+
return lines;
|
|
220
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
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.canPrompt = canPrompt;
|
|
37
|
+
exports.promptForProviderSelection = promptForProviderSelection;
|
|
38
|
+
exports.confirmProviderRemoval = confirmProviderRemoval;
|
|
39
|
+
exports.confirmImport = confirmImport;
|
|
40
|
+
exports.confirmExportOverwrite = confirmExportOverwrite;
|
|
41
|
+
exports.exportTargetExists = exportTargetExists;
|
|
42
|
+
exports.getRollbackSummary = getRollbackSummary;
|
|
43
|
+
exports.confirmRollback = confirmRollback;
|
|
44
|
+
const fs = __importStar(require("node:fs"));
|
|
45
|
+
const path = __importStar(require("node:path"));
|
|
46
|
+
const errors_1 = require("../domain/errors");
|
|
47
|
+
const providers_repo_1 = require("../infra/providers-repo");
|
|
48
|
+
const backup_repo_1 = require("../infra/backup-repo");
|
|
49
|
+
/**
|
|
50
|
+
* Keeps CLI-side interactivity rules in one place so automation paths remain explicit.
|
|
51
|
+
*/
|
|
52
|
+
function canPrompt(runtime, jsonMode) {
|
|
53
|
+
return !jsonMode && runtime.isInteractive();
|
|
54
|
+
}
|
|
55
|
+
async function promptForProviderSelection(runtime, providersPath, message) {
|
|
56
|
+
const providers = (0, providers_repo_1.readProvidersFile)(providersPath);
|
|
57
|
+
const choices = Object.entries(providers.providers)
|
|
58
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
59
|
+
.map(([providerName, provider]) => ({
|
|
60
|
+
value: providerName,
|
|
61
|
+
label: providerName,
|
|
62
|
+
hint: provider.profile,
|
|
63
|
+
}));
|
|
64
|
+
if (choices.length === 0) {
|
|
65
|
+
throw (0, errors_1.cliError)("PROVIDER_NOT_FOUND", "No providers are configured.");
|
|
66
|
+
}
|
|
67
|
+
return runtime.selectOne(message, choices);
|
|
68
|
+
}
|
|
69
|
+
async function confirmProviderRemoval(runtime, providerName) {
|
|
70
|
+
const confirmed = await runtime.confirmAction(`Remove provider "${providerName}"?`, {
|
|
71
|
+
defaultValue: false,
|
|
72
|
+
});
|
|
73
|
+
if (!confirmed) {
|
|
74
|
+
throw (0, errors_1.cliError)("INVALID_IMPORT_FILE", `Removal cancelled for provider "${providerName}".`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function confirmImport(runtime, sourceFile) {
|
|
78
|
+
const confirmed = await runtime.confirmAction(`Import providers from ${path.resolve(sourceFile)} and replace the current registry?`, { defaultValue: false });
|
|
79
|
+
if (!confirmed) {
|
|
80
|
+
throw (0, errors_1.cliError)("INVALID_IMPORT_FILE", "Import cancelled.");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function confirmExportOverwrite(runtime, targetFile) {
|
|
84
|
+
return runtime.confirmAction(`Overwrite existing export target ${path.resolve(targetFile)}?`, {
|
|
85
|
+
defaultValue: false,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
function exportTargetExists(targetFile) {
|
|
89
|
+
return fs.existsSync(path.resolve(targetFile));
|
|
90
|
+
}
|
|
91
|
+
function getRollbackSummary(latestBackupPath) {
|
|
92
|
+
const manifest = (0, backup_repo_1.loadLatestManifest)(latestBackupPath);
|
|
93
|
+
const previewLines = [
|
|
94
|
+
"Rollback preview",
|
|
95
|
+
`Backup: ${manifest.backupDir}`,
|
|
96
|
+
...manifest.files.map((file) => {
|
|
97
|
+
const suffix = file.existed ? "restore" : "remove";
|
|
98
|
+
return `- ${file.relativePath} (${suffix})`;
|
|
99
|
+
}),
|
|
100
|
+
];
|
|
101
|
+
return { manifest, previewLines };
|
|
102
|
+
}
|
|
103
|
+
async function confirmRollback(runtime, latestBackupPath) {
|
|
104
|
+
const { previewLines } = getRollbackSummary(latestBackupPath);
|
|
105
|
+
for (const line of previewLines) {
|
|
106
|
+
runtime.writeLine(line);
|
|
107
|
+
}
|
|
108
|
+
const confirmed = await runtime.confirmAction("Restore files from the latest backup?", {
|
|
109
|
+
defaultValue: false,
|
|
110
|
+
});
|
|
111
|
+
if (!confirmed) {
|
|
112
|
+
throw (0, errors_1.cliError)("INVALID_IMPORT_FILE", "Rollback cancelled.");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderSuccess = renderSuccess;
|
|
4
|
+
exports.renderFailure = renderFailure;
|
|
5
|
+
exports.outputSuccess = outputSuccess;
|
|
6
|
+
exports.outputFailure = outputFailure;
|
|
7
|
+
const fs_utils_1 = require("../infra/fs-utils");
|
|
8
|
+
/**
|
|
9
|
+
* Renders a successful command result for either JSON or human-readable output.
|
|
10
|
+
*/
|
|
11
|
+
function renderSuccess(ctx, result) {
|
|
12
|
+
const warnings = result.warnings ?? [];
|
|
13
|
+
if (ctx.options.json) {
|
|
14
|
+
const payload = {
|
|
15
|
+
ok: true,
|
|
16
|
+
command: ctx.command,
|
|
17
|
+
data: result.data,
|
|
18
|
+
warnings,
|
|
19
|
+
error: null,
|
|
20
|
+
};
|
|
21
|
+
return {
|
|
22
|
+
stdout: [JSON.stringify(payload, null, 2)],
|
|
23
|
+
stderr: [],
|
|
24
|
+
exitCode: 0,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
stdout: renderHumanSuccess(ctx.command, result.data, warnings),
|
|
29
|
+
stderr: [],
|
|
30
|
+
exitCode: 0,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Renders a failed command result for either JSON or human-readable output.
|
|
35
|
+
*/
|
|
36
|
+
function renderFailure(ctx, error) {
|
|
37
|
+
if (ctx.options.json) {
|
|
38
|
+
const payload = {
|
|
39
|
+
ok: false,
|
|
40
|
+
command: ctx.command,
|
|
41
|
+
data: null,
|
|
42
|
+
warnings: [],
|
|
43
|
+
error,
|
|
44
|
+
};
|
|
45
|
+
return {
|
|
46
|
+
stdout: [],
|
|
47
|
+
stderr: [JSON.stringify(payload, null, 2)],
|
|
48
|
+
exitCode: 1,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
stdout: [],
|
|
53
|
+
stderr: [`${error.code}: ${error.message}`, ...(0, fs_utils_1.printErrorDetails)(error)],
|
|
54
|
+
exitCode: 1,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Writes successful command output to stdout.
|
|
59
|
+
*/
|
|
60
|
+
function outputSuccess(ctx, result) {
|
|
61
|
+
const rendered = renderSuccess(ctx, result);
|
|
62
|
+
for (const line of rendered.stdout) {
|
|
63
|
+
printText(line);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Writes failure output to stderr and exits with the rendered status code.
|
|
68
|
+
*/
|
|
69
|
+
function outputFailure(ctx, error) {
|
|
70
|
+
const rendered = renderFailure(ctx, error);
|
|
71
|
+
for (const line of rendered.stderr) {
|
|
72
|
+
printText(line, true);
|
|
73
|
+
}
|
|
74
|
+
process.exit(rendered.exitCode);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Builds the plain-text success view for interactive terminal usage.
|
|
78
|
+
*/
|
|
79
|
+
function renderHumanSuccess(command, data, warnings) {
|
|
80
|
+
const lines = [];
|
|
81
|
+
switch (command) {
|
|
82
|
+
case "list": {
|
|
83
|
+
const providers = data?.providers ?? [];
|
|
84
|
+
if (providers.length === 0) {
|
|
85
|
+
lines.push("No providers configured.");
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
for (const provider of providers) {
|
|
89
|
+
const tags = Array.isArray(provider.tags) && provider.tags.length > 0
|
|
90
|
+
? ` tags=${provider.tags.join(",")}`
|
|
91
|
+
: "";
|
|
92
|
+
const note = provider.note ? ` note=${provider.note}` : "";
|
|
93
|
+
lines.push(`${provider.name} -> ${provider.profile}${tags}${note}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
case "current":
|
|
99
|
+
lines.push(`Current profile: ${String(data?.profile ?? "")}`);
|
|
100
|
+
break;
|
|
101
|
+
case "status":
|
|
102
|
+
lines.push(`codexDir: ${String(data?.codexDir ?? "")}`);
|
|
103
|
+
lines.push(`configExists: ${String(data?.configExists ?? false)}`);
|
|
104
|
+
lines.push(`providersExists: ${String(data?.providersExists ?? false)}`);
|
|
105
|
+
lines.push(`currentProfile: ${String(data?.currentProfile ?? "")}`);
|
|
106
|
+
lines.push(`mappedProvider: ${String(data?.provider ?? "")}`);
|
|
107
|
+
break;
|
|
108
|
+
case "switch":
|
|
109
|
+
lines.push(`Switched to provider ${String(data?.provider ?? "")} using profile ${String(data?.profile ?? "")}.`);
|
|
110
|
+
lines.push(`Backup: ${String(data?.backupPath ?? "")}`);
|
|
111
|
+
lines.push(`Login performed: ${String(data?.loginPerformed ?? false)}`);
|
|
112
|
+
break;
|
|
113
|
+
case "import":
|
|
114
|
+
lines.push(`Imported providers from file. Backup: ${String(data?.backupPath ?? "")}`);
|
|
115
|
+
break;
|
|
116
|
+
case "export":
|
|
117
|
+
lines.push(`Exported providers to ${String(data?.exportedTo ?? "")}.`);
|
|
118
|
+
break;
|
|
119
|
+
case "add":
|
|
120
|
+
lines.push(`Added provider ${String(data?.provider ?? "")}. Backup: ${String(data?.backupPath ?? "")}`);
|
|
121
|
+
break;
|
|
122
|
+
case "remove":
|
|
123
|
+
lines.push(`Removed provider ${String(data?.provider ?? "")}. Backup: ${String(data?.backupPath ?? "")}`);
|
|
124
|
+
break;
|
|
125
|
+
case "doctor": {
|
|
126
|
+
const healthy = Boolean(data?.healthy);
|
|
127
|
+
lines.push(healthy ? "No issues found." : "Issues found:");
|
|
128
|
+
const issues = data?.issues ?? [];
|
|
129
|
+
for (const issue of issues) {
|
|
130
|
+
lines.push(`${issue.code}: ${issue.message}`);
|
|
131
|
+
}
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
case "rollback":
|
|
135
|
+
lines.push(`Rollback restored files from ${String(data?.backupPath ?? "")}.`);
|
|
136
|
+
break;
|
|
137
|
+
default:
|
|
138
|
+
lines.push(JSON.stringify(data, null, 2));
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
// Emit warnings after the primary payload so the main outcome remains easy to scan.
|
|
142
|
+
for (const warning of warnings) {
|
|
143
|
+
lines.push(`Warning: ${warning}`);
|
|
144
|
+
}
|
|
145
|
+
return lines;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Writes one rendered line to either stdout or stderr.
|
|
149
|
+
*/
|
|
150
|
+
function printText(message, toStderr = false) {
|
|
151
|
+
if (toStderr) {
|
|
152
|
+
process.stderr.write(`${message}\n`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
process.stdout.write(`${message}\n`);
|
|
156
|
+
}
|