@minniexcode/codex-switch 0.0.5 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.AI.md +5 -2
  2. package/README.md +44 -100
  3. package/dist/app/add-provider.js +28 -4
  4. package/dist/app/edit-provider.js +47 -19
  5. package/dist/app/export-providers.js +2 -2
  6. package/dist/app/get-current-profile.js +1 -1
  7. package/dist/app/get-status.js +10 -3
  8. package/dist/app/import-providers.js +15 -7
  9. package/dist/app/init-codex.js +68 -0
  10. package/dist/app/list-backups.js +1 -1
  11. package/dist/app/list-config-profiles.js +3 -2
  12. package/dist/app/list-providers.js +2 -1
  13. package/dist/app/remove-provider.js +2 -2
  14. package/dist/app/rollback-backup.js +1 -1
  15. package/dist/app/rollback-latest.js +1 -1
  16. package/dist/app/run-doctor.js +83 -6
  17. package/dist/app/run-mutation.js +2 -2
  18. package/dist/app/setup-codex.js +21 -12
  19. package/dist/app/show-config.js +11 -3
  20. package/dist/app/show-provider.js +1 -1
  21. package/dist/app/switch-provider.js +16 -9
  22. package/dist/cli/add-interactive.js +7 -104
  23. package/dist/cli/args.js +6 -135
  24. package/dist/cli/help.js +8 -313
  25. package/dist/cli/interactive.js +17 -225
  26. package/dist/cli/output.js +21 -6
  27. package/dist/cli/prompt.js +4 -106
  28. package/dist/cli.js +10 -404
  29. package/dist/commands/args.js +132 -0
  30. package/dist/commands/dispatch.js +16 -0
  31. package/dist/commands/handlers.js +460 -0
  32. package/dist/commands/help.js +120 -0
  33. package/dist/commands/registry.js +351 -0
  34. package/dist/commands/types.js +2 -0
  35. package/dist/domain/config.js +235 -21
  36. package/dist/domain/providers.js +16 -2
  37. package/dist/domain/setup.js +1 -0
  38. package/dist/infra/backup-repo.js +9 -206
  39. package/dist/infra/codex-cli.js +9 -126
  40. package/dist/infra/codex-paths.js +6 -67
  41. package/dist/infra/config-repo.js +59 -0
  42. package/dist/infra/fs-utils.js +8 -93
  43. package/dist/infra/lock-repo.js +4 -95
  44. package/dist/infra/providers-repo.js +8 -94
  45. package/dist/interaction/add-interactive.js +99 -0
  46. package/dist/interaction/interactive.js +289 -0
  47. package/dist/interaction/prompt.js +110 -0
  48. package/dist/runtime/codex-cli.js +130 -0
  49. package/dist/runtime/codex-probe.js +57 -0
  50. package/dist/runtime/types.js +2 -0
  51. package/dist/storage/auth-repo.js +160 -0
  52. package/dist/storage/backup-repo.js +210 -0
  53. package/dist/storage/codex-paths.js +71 -0
  54. package/dist/storage/config-repo.js +266 -0
  55. package/dist/storage/fs-utils.js +97 -0
  56. package/dist/storage/lock-repo.js +99 -0
  57. package/dist/storage/providers-repo.js +98 -0
  58. package/docs/Design/codex-switch-v0.0.5-design.md +32 -22
  59. package/docs/Design/codex-switch-v0.0.6-design.md +708 -0
  60. package/docs/Design/codex-switch-v0.0.7-design.md +862 -0
  61. package/docs/PRD/codex-switch-prd-v0.0.5-to-v0.1.0.md +227 -89
  62. package/docs/PRD/codex-switch-prd-v0.1.0.md +200 -226
  63. package/docs/PRD/codex-switch-prd.md +1 -1
  64. package/docs/Reference/codex-config-reference.md +604 -0
  65. package/docs/Reference/codex-config-reference.zh-CN.md +633 -0
  66. package/docs/cli-usage.md +78 -29
  67. package/docs/codex-switch-technical-architecture.md +73 -4
  68. package/docs/test-report-0.0.5.md +163 -0
  69. package/docs/test-report-0.0.7.md +118 -0
  70. package/docs/testing.md +151 -0
  71. package/package.json +1 -1
@@ -1,99 +1,8 @@
1
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
2
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.withCodexLock = withCodexLock;
37
- const fs = __importStar(require("node:fs"));
38
- const path = __importStar(require("node:path"));
39
- const errors_1 = require("../domain/errors");
40
- const fs_utils_1 = require("./fs-utils");
3
+ exports.withCodexLock = void 0;
41
4
  /**
42
- * Executes a mutation while holding an exclusive codex-switch lock file.
5
+ * Compatibility facade that re-exports Codex lock helpers from storage.
43
6
  */
44
- function withCodexLock(codexDir, operation, run) {
45
- (0, fs_utils_1.ensureDir)(codexDir);
46
- const lockPath = path.join(codexDir, ".codex-switch.lock");
47
- acquireLock(lockPath, operation);
48
- try {
49
- return run();
50
- }
51
- finally {
52
- releaseLock(lockPath);
53
- }
54
- }
55
- /**
56
- * Acquires the lock file using exclusive create semantics.
57
- */
58
- function acquireLock(lockPath, operation) {
59
- const payload = {
60
- pid: process.pid,
61
- operation,
62
- createdAt: new Date().toISOString(),
63
- };
64
- try {
65
- fs.writeFileSync(lockPath, `${JSON.stringify(payload, null, 2)}\n`, { encoding: "utf8", flag: "wx" });
66
- }
67
- catch {
68
- const existing = readLockRecord(lockPath);
69
- throw (0, errors_1.cliError)("LOCK_CONFLICT", "Another codex-switch write operation is already running.", {
70
- file: lockPath,
71
- activeOperation: existing?.operation ?? "unknown",
72
- activePid: existing?.pid ?? null,
73
- activeSince: existing?.createdAt ?? null,
74
- requestedOperation: operation,
75
- });
76
- }
77
- }
78
- /**
79
- * Removes the lock file when the mutation completes.
80
- */
81
- function releaseLock(lockPath) {
82
- if (fs.existsSync(lockPath)) {
83
- fs.rmSync(lockPath, { force: true });
84
- }
85
- }
86
- /**
87
- * Reads the current lock metadata when a lock conflict occurs.
88
- */
89
- function readLockRecord(lockPath) {
90
- if (!fs.existsSync(lockPath)) {
91
- return null;
92
- }
93
- try {
94
- return JSON.parse(fs.readFileSync(lockPath, "utf8"));
95
- }
96
- catch {
97
- return null;
98
- }
99
- }
7
+ var lock_repo_1 = require("../storage/lock-repo");
8
+ Object.defineProperty(exports, "withCodexLock", { enumerable: true, get: function () { return lock_repo_1.withCodexLock; } });
@@ -1,98 +1,12 @@
1
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
2
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.readProvidersFile = readProvidersFile;
37
- exports.readProvidersFileIfExists = readProvidersFileIfExists;
38
- exports.writeProvidersFile = writeProvidersFile;
39
- exports.readProviderRecord = readProviderRecord;
40
- exports.mergeProviders = mergeProviders;
41
- const fs = __importStar(require("node:fs"));
42
- const errors_1 = require("../domain/errors");
43
- const providers_1 = require("../domain/providers");
44
- const fs_utils_1 = require("./fs-utils");
3
+ exports.writeProvidersFile = exports.readProvidersFileIfExists = exports.readProvidersFile = exports.readProviderRecord = exports.mergeProviders = void 0;
45
4
  /**
46
- * Reads and validates providers.json from disk.
5
+ * Compatibility facade that re-exports provider repository helpers from storage.
47
6
  */
48
- function readProvidersFile(providersPath) {
49
- const raw = (0, fs_utils_1.readRequiredFile)(providersPath, "PROVIDERS_NOT_FOUND", "providers.json");
50
- try {
51
- return (0, providers_1.validateProvidersShape)(JSON.parse(raw));
52
- }
53
- catch (error) {
54
- throw (0, errors_1.cliError)("PROVIDERS_PARSE_ERROR", "Failed to parse providers.json.", {
55
- file: providersPath,
56
- cause: (0, errors_1.normalizeError)(error).message,
57
- });
58
- }
59
- }
60
- /**
61
- * Reads providers.json when it exists, otherwise returns an empty registry.
62
- */
63
- function readProvidersFileIfExists(providersPath) {
64
- return fs.existsSync(providersPath) ? readProvidersFile(providersPath) : { providers: {} };
65
- }
66
- /**
67
- * Persists providers.json using deterministic key ordering.
68
- */
69
- function writeProvidersFile(providersPath, providers) {
70
- (0, fs_utils_1.writeTextFileAtomic)(providersPath, `${JSON.stringify((0, providers_1.sortProviders)(providers), null, 2)}\n`);
71
- }
72
- /**
73
- * Returns a single provider record or throws a typed not-found error.
74
- */
75
- function readProviderRecord(providersPath, providerName) {
76
- const providers = readProvidersFile(providersPath);
77
- const record = providers.providers[providerName];
78
- if (!record) {
79
- throw (0, errors_1.cliError)("PROVIDER_NOT_FOUND", `Provider "${providerName}" was not found.`, {
80
- provider: providerName,
81
- file: providersPath,
82
- });
83
- }
84
- return record;
85
- }
86
- /**
87
- * Merges imported providers into the current registry, preferring the imported side on conflicts.
88
- */
89
- function mergeProviders(current, imported) {
90
- const providers = {};
91
- for (const [name, record] of Object.entries(current.providers)) {
92
- providers[name] = (0, providers_1.cleanProviderRecord)(record);
93
- }
94
- for (const [name, record] of Object.entries(imported.providers)) {
95
- providers[name] = (0, providers_1.cleanProviderRecord)(record);
96
- }
97
- return (0, providers_1.sortProviders)({ providers });
98
- }
7
+ var providers_repo_1 = require("../storage/providers-repo");
8
+ Object.defineProperty(exports, "mergeProviders", { enumerable: true, get: function () { return providers_repo_1.mergeProviders; } });
9
+ Object.defineProperty(exports, "readProviderRecord", { enumerable: true, get: function () { return providers_repo_1.readProviderRecord; } });
10
+ Object.defineProperty(exports, "readProvidersFile", { enumerable: true, get: function () { return providers_repo_1.readProvidersFile; } });
11
+ Object.defineProperty(exports, "readProvidersFileIfExists", { enumerable: true, get: function () { return providers_repo_1.readProvidersFileIfExists; } });
12
+ Object.defineProperty(exports, "writeProvidersFile", { enumerable: true, get: function () { return providers_repo_1.writeProvidersFile; } });
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.COMMON_TAG_CHOICES = void 0;
4
+ exports.collectAddInput = collectAddInput;
5
+ exports.createNonInteractiveAddError = createNonInteractiveAddError;
6
+ exports.promptTags = promptTags;
7
+ const errors_1 = require("../domain/errors");
8
+ exports.COMMON_TAG_CHOICES = ["free", "paid", "daily", "backup"];
9
+ /**
10
+ * Collects add command inputs interactively when required values are missing.
11
+ */
12
+ async function collectAddInput(runtime, defaults, providerExists, profileExists) {
13
+ runtime.writeLine("Interactive add mode");
14
+ runtime.writeLine("Provide the missing required fields. Press Enter to skip optional fields.");
15
+ const providerName = defaults.providerName
16
+ ? normalizeRequiredValue(defaults.providerName)
17
+ : await promptProviderName(runtime, providerExists);
18
+ const profile = defaults.profile ? normalizeRequiredValue(defaults.profile) : await promptRequiredValue(runtime, "Profile");
19
+ const apiKey = defaults.apiKey
20
+ ? normalizeRequiredValue(defaults.apiKey)
21
+ : await promptConfirmedSecret(runtime, "API key", "Confirm API key");
22
+ const createProfile = !profileExists(profile);
23
+ const model = createProfile ? await promptRequiredValue(runtime, `Model for new profile "${profile}"`) : null;
24
+ const baseUrl = createProfile
25
+ ? (defaults.baseUrl ? normalizeRequiredValue(defaults.baseUrl) : await promptRequiredValue(runtime, `Base URL for new profile "${profile}"`))
26
+ : defaults.baseUrl ?? normalizeOptionalValue(await runtime.inputText("Base URL (optional)"));
27
+ const note = defaults.note ?? normalizeOptionalValue(await runtime.inputText("Note (optional)"));
28
+ const tags = defaults.tags.length > 0 ? defaults.tags : await promptTags(runtime);
29
+ return {
30
+ providerName,
31
+ profile,
32
+ apiKey,
33
+ createProfile,
34
+ model,
35
+ baseUrl,
36
+ note,
37
+ tags,
38
+ };
39
+ }
40
+ /**
41
+ * Throws a consistent error when interactive add is unavailable.
42
+ */
43
+ function createNonInteractiveAddError() {
44
+ return (0, errors_1.cliError)("INVALID_ARGUMENT", "add requires <provider>, --profile, and --api-key when running without an interactive TTY.", {
45
+ suggestion: "Run in a terminal TTY or pass all required values explicitly.",
46
+ });
47
+ }
48
+ async function promptProviderName(runtime, providerExists) {
49
+ while (true) {
50
+ const providerName = await promptRequiredValue(runtime, "Provider name");
51
+ if (providerExists(providerName)) {
52
+ runtime.writeLine(`Provider "${providerName}" already exists. Choose a different name.`);
53
+ continue;
54
+ }
55
+ return providerName;
56
+ }
57
+ }
58
+ async function promptRequiredValue(runtime, label) {
59
+ while (true) {
60
+ const value = normalizeRequiredValue(await runtime.inputText(label));
61
+ if (value.length > 0) {
62
+ return value;
63
+ }
64
+ runtime.writeLine(`${label} is required.`);
65
+ }
66
+ }
67
+ async function promptConfirmedSecret(runtime, label, confirmationLabel) {
68
+ while (true) {
69
+ const first = normalizeRequiredValue(await runtime.inputSecret(label));
70
+ if (first.length === 0) {
71
+ runtime.writeLine(`${label} is required.`);
72
+ continue;
73
+ }
74
+ const second = normalizeRequiredValue(await runtime.inputSecret(confirmationLabel));
75
+ if (second.length === 0) {
76
+ runtime.writeLine(`${confirmationLabel} is required.`);
77
+ continue;
78
+ }
79
+ if (first !== second) {
80
+ runtime.writeLine("API key entries did not match. Try again.");
81
+ continue;
82
+ }
83
+ return first;
84
+ }
85
+ }
86
+ function normalizeRequiredValue(value) {
87
+ return value.trim();
88
+ }
89
+ function normalizeOptionalValue(value) {
90
+ const normalized = value.trim();
91
+ return normalized === "" ? null : normalized;
92
+ }
93
+ async function promptTags(runtime, defaults = []) {
94
+ const defaultPresetTags = defaults.filter(isCommonTag);
95
+ return runtime.selectMany("Select tags (optional)", exports.COMMON_TAG_CHOICES.map((tag) => ({ value: tag, label: tag })), { defaultValues: defaultPresetTags });
96
+ }
97
+ function isCommonTag(tag) {
98
+ return exports.COMMON_TAG_CHOICES.includes(tag);
99
+ }
@@ -0,0 +1,289 @@
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.getRollbackSummaryById = getRollbackSummaryById;
44
+ exports.confirmRollback = confirmRollback;
45
+ exports.chooseSetupStrategy = chooseSetupStrategy;
46
+ exports.chooseCodexDir = chooseCodexDir;
47
+ exports.confirmCreateCodexDir = confirmCreateCodexDir;
48
+ exports.chooseSetupProfiles = chooseSetupProfiles;
49
+ exports.collectSetupProviderDetails = collectSetupProviderDetails;
50
+ exports.collectEditInput = collectEditInput;
51
+ const fs = __importStar(require("node:fs"));
52
+ const path = __importStar(require("node:path"));
53
+ const errors_1 = require("../domain/errors");
54
+ const backups_1 = require("../domain/backups");
55
+ const codex_paths_1 = require("../storage/codex-paths");
56
+ const providers_repo_1 = require("../storage/providers-repo");
57
+ const backup_repo_1 = require("../storage/backup-repo");
58
+ const add_interactive_1 = require("./add-interactive");
59
+ /**
60
+ * Keeps CLI-side interactivity rules in one place so automation paths remain explicit.
61
+ */
62
+ function canPrompt(runtime, jsonMode) {
63
+ return !jsonMode && runtime.isInteractive();
64
+ }
65
+ /**
66
+ * Prompts the user to choose one configured provider when a command omitted its target.
67
+ */
68
+ async function promptForProviderSelection(runtime, providersPath, message) {
69
+ const providers = (0, providers_repo_1.readProvidersFile)(providersPath);
70
+ const choices = Object.entries(providers.providers)
71
+ .sort(([left], [right]) => left.localeCompare(right))
72
+ .map(([providerName, provider]) => ({
73
+ value: providerName,
74
+ label: providerName,
75
+ hint: provider.profile,
76
+ }));
77
+ if (choices.length === 0) {
78
+ throw (0, errors_1.cliError)("PROVIDER_NOT_FOUND", "No providers are configured.");
79
+ }
80
+ return runtime.selectOne(message, choices);
81
+ }
82
+ /**
83
+ * Confirms destructive provider removal and turns a declined prompt into a typed cancellation.
84
+ */
85
+ async function confirmProviderRemoval(runtime, providerName) {
86
+ const confirmed = await runtime.confirmAction(`Remove provider "${providerName}"?`, {
87
+ defaultValue: false,
88
+ });
89
+ if (!confirmed) {
90
+ throw (0, errors_1.cliError)("PROMPT_CANCELLED", `Removal cancelled for provider "${providerName}".`);
91
+ }
92
+ }
93
+ /**
94
+ * Confirms provider import semantics, including whether the file will merge or replace the registry.
95
+ */
96
+ async function confirmImport(runtime, sourceFile, merge = false) {
97
+ const confirmed = await runtime.confirmAction(merge
98
+ ? `Import providers from ${path.resolve(sourceFile)} and merge into the current registry?`
99
+ : `Import providers from ${path.resolve(sourceFile)} and replace the current registry?`, { defaultValue: false });
100
+ if (!confirmed) {
101
+ throw (0, errors_1.cliError)("PROMPT_CANCELLED", "Import cancelled.");
102
+ }
103
+ }
104
+ /**
105
+ * Confirms whether an existing export target may be overwritten.
106
+ */
107
+ async function confirmExportOverwrite(runtime, targetFile) {
108
+ return runtime.confirmAction(`Overwrite existing export target ${path.resolve(targetFile)}?`, {
109
+ defaultValue: false,
110
+ });
111
+ }
112
+ /**
113
+ * Resolves whether the export target already exists after normalizing to an absolute path.
114
+ */
115
+ function exportTargetExists(targetFile) {
116
+ return fs.existsSync(path.resolve(targetFile));
117
+ }
118
+ /**
119
+ * Builds a rollback preview for the latest managed backup.
120
+ */
121
+ function getRollbackSummary(latestBackupPath) {
122
+ const manifest = (0, backup_repo_1.loadLatestManifest)(latestBackupPath);
123
+ return buildRollbackSummary(manifest);
124
+ }
125
+ /**
126
+ * Builds a rollback preview for one explicit backup id.
127
+ */
128
+ function getRollbackSummaryById(backupsDir, backupId) {
129
+ const manifest = (0, backup_repo_1.loadManifestById)(backupsDir, backupId);
130
+ return buildRollbackSummary(manifest);
131
+ }
132
+ /**
133
+ * Converts a backup manifest into the human preview shown before rollback confirmation.
134
+ */
135
+ function buildRollbackSummary(manifest) {
136
+ const previewLines = [
137
+ "Rollback preview",
138
+ `Backup ID: ${(0, backups_1.getBackupId)(manifest.backupDir)}`,
139
+ `Backup: ${manifest.backupDir}`,
140
+ ...manifest.files.map((file) => {
141
+ const suffix = file.existed ? "restore" : "remove";
142
+ return `- ${file.relativePath} (${suffix})`;
143
+ }),
144
+ ];
145
+ return { manifest, previewLines };
146
+ }
147
+ /**
148
+ * Prints the rollback preview and requires explicit confirmation before restore proceeds.
149
+ */
150
+ async function confirmRollback(runtime, latestBackupPath, backupsDir, backupId) {
151
+ const { previewLines } = backupId && backupsDir
152
+ ? getRollbackSummaryById(backupsDir, backupId)
153
+ : getRollbackSummary(latestBackupPath);
154
+ for (const line of previewLines) {
155
+ runtime.writeLine(line);
156
+ }
157
+ const confirmed = await runtime.confirmAction(backupId ? `Restore files from backup "${backupId}"?` : "Restore files from the latest backup?", {
158
+ defaultValue: false,
159
+ });
160
+ if (!confirmed) {
161
+ throw (0, errors_1.cliError)("PROMPT_CANCELLED", "Rollback cancelled.");
162
+ }
163
+ }
164
+ /**
165
+ * Prompts for setup merge strategy when providers.json already exists.
166
+ */
167
+ async function chooseSetupStrategy(runtime) {
168
+ return runtime.selectOne("providers.json already exists. Choose a migrate strategy.", [
169
+ { value: "merge", label: "merge", hint: "keep existing providers and override by imported names" },
170
+ { value: "overwrite", label: "overwrite", hint: "replace the existing registry" },
171
+ { value: "cancel", label: "cancel", hint: "abort migrate without writing" },
172
+ ]);
173
+ }
174
+ /**
175
+ * Resolves the Codex directory from discovered candidates or a manually entered path.
176
+ */
177
+ async function chooseCodexDir(runtime, candidates) {
178
+ if (candidates.length === 0) {
179
+ const manual = (await runtime.inputText("Codex directory path")).trim();
180
+ if (!manual) {
181
+ throw (0, errors_1.cliError)("CODEX_DIR_NOT_FOUND", "No Codex directory was provided.");
182
+ }
183
+ return (0, codex_paths_1.resolveCodexDir)(manual);
184
+ }
185
+ if (candidates.length === 1) {
186
+ return candidates[0];
187
+ }
188
+ const selected = await runtime.selectOne("Choose a Codex directory", [
189
+ ...candidates.map((candidate) => ({
190
+ value: candidate,
191
+ label: candidate,
192
+ })),
193
+ {
194
+ value: "__manual__",
195
+ label: "Enter manually",
196
+ },
197
+ ]);
198
+ if (selected !== "__manual__") {
199
+ return selected;
200
+ }
201
+ const manual = (await runtime.inputText("Codex directory path")).trim();
202
+ if (!manual) {
203
+ throw (0, errors_1.cliError)("CODEX_DIR_NOT_FOUND", "No Codex directory was provided.");
204
+ }
205
+ return (0, codex_paths_1.resolveCodexDir)(manual);
206
+ }
207
+ /**
208
+ * Confirms whether a missing Codex directory should be created during init.
209
+ */
210
+ async function confirmCreateCodexDir(runtime, codexDir) {
211
+ return runtime.confirmAction(`Create missing Codex directory ${codexDir}?`, {
212
+ defaultValue: false,
213
+ });
214
+ }
215
+ /**
216
+ * Lets setup adopt a subset of unmanaged config profiles into providers.json.
217
+ */
218
+ async function chooseSetupProfiles(runtime, profiles) {
219
+ if (profiles.length === 0) {
220
+ return [];
221
+ }
222
+ return runtime.selectMany("Choose unmanaged config profiles to adopt into providers.json.", profiles.map((profile) => ({
223
+ value: profile.name,
224
+ label: profile.name,
225
+ hint: `${profile.model} | ${profile.baseUrl} | ${profile.envKey}`,
226
+ })));
227
+ }
228
+ /**
229
+ * Collects provider metadata for each adopted config profile during setup.
230
+ */
231
+ async function collectSetupProviderDetails(runtime, profiles, defaultsByProfile = {}) {
232
+ const result = {};
233
+ for (const profile of profiles) {
234
+ const defaults = defaultsByProfile[profile] ?? {};
235
+ const providerName = (await runtime.inputText(`Provider name for profile "${profile}"`, {
236
+ defaultValue: defaults.providerName ?? profile,
237
+ })).trim();
238
+ if (defaults.envKey) {
239
+ runtime.writeLine(`Runtime env key for "${profile}": ${defaults.envKey}`);
240
+ }
241
+ const apiKey = await promptRequiredSecret(runtime, `API key for profile "${profile}"`, defaults.apiKey?.trim() || undefined);
242
+ const baseUrl = (await runtime.inputText(`Base URL note for profile "${profile}" (optional)`, {
243
+ defaultValue: defaults.baseUrl ?? "",
244
+ })).trim();
245
+ const note = (await runtime.inputText(`Note for profile "${profile}" (optional)`, {
246
+ defaultValue: defaults.note ?? "",
247
+ })).trim();
248
+ const tags = await (0, add_interactive_1.promptTags)(runtime);
249
+ result[profile] = {
250
+ providerName: providerName || defaults.providerName || profile,
251
+ apiKey,
252
+ envKey: defaults.envKey,
253
+ baseUrl: baseUrl || defaults.baseUrl || undefined,
254
+ note: note || defaults.note || undefined,
255
+ // Empty selections are omitted so downstream setup validation can distinguish unset from explicit data.
256
+ tags: tags.length > 0 ? tags : undefined,
257
+ };
258
+ }
259
+ return result;
260
+ }
261
+ /**
262
+ * Re-prompts until a required secret value is provided, optionally falling back to a non-empty default.
263
+ */
264
+ async function promptRequiredSecret(runtime, label, defaultValue) {
265
+ while (true) {
266
+ const value = (await runtime.inputSecret(label)).trim() || defaultValue || "";
267
+ if (value.length > 0) {
268
+ return value;
269
+ }
270
+ runtime.writeLine(`${label} is required.`);
271
+ }
272
+ }
273
+ /**
274
+ * Collects editable provider fields, preserving current values when prompts are left blank.
275
+ */
276
+ async function collectEditInput(runtime, current) {
277
+ const profile = (await runtime.inputText("Profile", { defaultValue: current.profile })).trim();
278
+ const apiKey = (await runtime.inputSecret("API key")).trim() || current.apiKey;
279
+ const baseUrl = (await runtime.inputText("Base URL (optional)", { defaultValue: current.baseUrl ?? "" })).trim();
280
+ const note = (await runtime.inputText("Note (optional)", { defaultValue: current.note ?? "" })).trim();
281
+ const tags = await (0, add_interactive_1.promptTags)(runtime, current.tags ?? []);
282
+ return {
283
+ profile,
284
+ apiKey,
285
+ baseUrl: baseUrl || undefined,
286
+ note: note || undefined,
287
+ tags,
288
+ };
289
+ }