@minniexcode/codex-switch 0.0.3 → 0.0.5

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 (44) hide show
  1. package/README.AI.md +8 -3
  2. package/README.md +160 -91
  3. package/dist/app/add-provider.js +32 -1
  4. package/dist/app/edit-provider.js +137 -0
  5. package/dist/app/get-status.js +9 -2
  6. package/dist/app/import-providers.js +47 -2
  7. package/dist/app/list-backups.js +17 -0
  8. package/dist/app/list-config-profiles.js +29 -0
  9. package/dist/app/remove-provider.js +34 -2
  10. package/dist/app/rollback-backup.js +30 -0
  11. package/dist/app/run-doctor.js +22 -21
  12. package/dist/app/setup-codex.js +155 -0
  13. package/dist/app/show-config.js +34 -0
  14. package/dist/app/show-provider.js +22 -0
  15. package/dist/app/switch-provider.js +5 -2
  16. package/dist/cli/add-interactive.js +25 -31
  17. package/dist/cli/args.js +19 -5
  18. package/dist/cli/help.js +109 -14
  19. package/dist/cli/interactive.js +123 -8
  20. package/dist/cli/output.js +56 -1
  21. package/dist/cli/prompt.js +19 -2
  22. package/dist/cli.js +250 -13
  23. package/dist/domain/backups.js +103 -0
  24. package/dist/domain/config.js +471 -39
  25. package/dist/domain/errors.js +3 -3
  26. package/dist/domain/providers.js +10 -0
  27. package/dist/domain/setup.js +30 -0
  28. package/dist/infra/backup-repo.js +65 -6
  29. package/dist/infra/codex-cli.js +79 -2
  30. package/dist/infra/codex-discovery.js +10 -0
  31. package/dist/infra/codex-paths.js +14 -1
  32. package/dist/infra/config-repo.js +102 -9
  33. package/dist/infra/providers-repo.js +29 -0
  34. package/docs/Design/codex-switch-v0.0.4-design.md +874 -0
  35. package/docs/Design/codex-switch-v0.0.5-design.md +922 -0
  36. package/docs/PRD/codex-switch-prd-v0.0.5-to-v0.1.0.md +308 -0
  37. package/docs/PRD/codex-switch-prd-v0.1.0.md +343 -0
  38. package/docs/{codex-switch-prd.md → PRD/codex-switch-prd.md} +9 -5
  39. package/docs/cli-usage.md +580 -0
  40. package/docs/codex-switch-command-design.md +1 -1
  41. package/docs/codex-switch-product-overview.md +1 -1
  42. package/docs/codex-switch-product-research.md +2 -2
  43. package/docs/codex-switch-technical-architecture.md +1 -1
  44. package/package.json +1 -1
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.listConfigProfilesView = listConfigProfilesView;
4
+ const config_1 = require("../domain/config");
5
+ const config_repo_1 = require("../infra/config-repo");
6
+ const providers_repo_1 = require("../infra/providers-repo");
7
+ /**
8
+ * Returns the lightweight config profile listing.
9
+ */
10
+ function listConfigProfilesView(args) {
11
+ const document = (0, config_repo_1.readStructuredConfig)(args.configPath);
12
+ const providers = (0, providers_repo_1.readProvidersFileIfExists)(args.providersPath);
13
+ const profiles = (0, config_1.buildManagedProfileViews)(document, providers).map((profile) => ({
14
+ name: profile.name,
15
+ managed: profile.managed,
16
+ isActive: profile.isActive,
17
+ linkedProviders: profile.linkedProviders,
18
+ model: profile.model,
19
+ baseUrl: profile.baseUrl,
20
+ source: profile.source,
21
+ }));
22
+ return {
23
+ data: {
24
+ activeProfile: document.activeProfile,
25
+ profiles,
26
+ count: profiles.length,
27
+ },
28
+ };
29
+ }
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.removeProvider = removeProvider;
4
4
  const errors_1 = require("../domain/errors");
5
+ const config_1 = require("../domain/config");
6
+ const config_repo_1 = require("../infra/config-repo");
5
7
  const providers_repo_1 = require("../infra/providers-repo");
6
8
  const run_mutation_1 = require("./run-mutation");
7
9
  /**
@@ -9,22 +11,52 @@ const run_mutation_1 = require("./run-mutation");
9
11
  */
10
12
  function removeProvider(args) {
11
13
  const providers = (0, providers_repo_1.readProvidersFile)(args.providersPath);
12
- if (!providers.providers[args.providerName]) {
14
+ const document = (0, config_repo_1.readStructuredConfig)(args.configPath);
15
+ const current = providers.providers[args.providerName];
16
+ if (!current) {
13
17
  throw (0, errors_1.cliError)("PROVIDER_NOT_FOUND", `Provider "${args.providerName}" was not found.`);
14
18
  }
15
19
  const nextProviders = { ...providers.providers };
16
20
  // Delete against a copied object so the original parsed state stays untouched.
17
21
  delete nextProviders[args.providerName];
22
+ const remainingLinksByProfile = new Map();
23
+ for (const [name, provider] of Object.entries(nextProviders)) {
24
+ const list = remainingLinksByProfile.get(provider.profile) ?? [];
25
+ list.push(name);
26
+ remainingLinksByProfile.set(provider.profile, list);
27
+ }
28
+ const lifecycle = (0, config_1.planProfileLifecycleOutcome)({
29
+ providerName: args.providerName,
30
+ oldProfile: current.profile,
31
+ newProfile: null,
32
+ activeProfile: document.activeProfile,
33
+ remainingLinksByProfile,
34
+ switchToProfile: args.switchToProfile ?? null,
35
+ });
18
36
  return (0, run_mutation_1.runMutation)({
19
37
  codexDir: args.codexDir,
20
38
  backupsDir: args.backupsDir,
21
39
  latestBackupPath: args.latestBackupPath,
22
40
  operation: "remove",
23
- files: [{ absolutePath: args.providersPath, relativePath: "providers.json" }],
41
+ files: [
42
+ { absolutePath: args.providersPath, relativePath: "providers.json" },
43
+ { absolutePath: args.configPath, relativePath: "config.toml" },
44
+ ],
24
45
  mutate: () => {
46
+ const configPlan = (0, config_repo_1.createConfigMutationPlan)(document, {
47
+ deleteProfiles: lifecycle.deletedProfileSections,
48
+ setActiveProfile: lifecycle.nextActiveProfile,
49
+ });
25
50
  (0, providers_repo_1.writeProvidersFile)(args.providersPath, { providers: nextProviders });
51
+ (0, config_repo_1.applyConfigMutation)(args.configPath, document, configPlan);
26
52
  return {
27
53
  provider: args.providerName,
54
+ createdProfileSections: configPlan.createdProfileSections,
55
+ deletedProfileSections: configPlan.deletedProfileSections,
56
+ keptSharedProfiles: lifecycle.keptSharedProfiles,
57
+ switchedActiveProfile: lifecycle.switchedActiveProfile,
58
+ adoptedProfiles: [],
59
+ repairedProfiles: [],
28
60
  };
29
61
  },
30
62
  });
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.rollbackBackup = rollbackBackup;
4
+ const errors_1 = require("../domain/errors");
5
+ const backup_repo_1 = require("../infra/backup-repo");
6
+ /**
7
+ * Restores either the latest backup or a specific historical backup by id.
8
+ */
9
+ function rollbackBackup(args) {
10
+ const manifest = args.backupId
11
+ ? (0, backup_repo_1.loadManifestById)(args.backupsDir, args.backupId)
12
+ : (0, backup_repo_1.loadLatestManifest)(args.latestBackupPath);
13
+ try {
14
+ (0, backup_repo_1.restoreManifest)(manifest);
15
+ return {
16
+ data: {
17
+ restoredFiles: manifest.files.map((file) => file.relativePath),
18
+ backupId: args.backupId ?? null,
19
+ backupPath: manifest.backupDir,
20
+ },
21
+ };
22
+ }
23
+ catch (error) {
24
+ throw (0, errors_1.cliError)("ROLLBACK_FAILED", "Rollback failed.", {
25
+ cause: (0, errors_1.normalizeError)(error).message,
26
+ backupPath: manifest.backupDir,
27
+ backupId: args.backupId ?? null,
28
+ });
29
+ }
30
+ }
@@ -38,6 +38,7 @@ const fs = __importStar(require("node:fs"));
38
38
  const config_1 = require("../domain/config");
39
39
  const runtime_state_1 = require("../domain/runtime-state");
40
40
  const codex_cli_1 = require("../infra/codex-cli");
41
+ const config_repo_1 = require("../infra/config-repo");
41
42
  const providers_repo_1 = require("../infra/providers-repo");
42
43
  const errors_1 = require("../domain/errors");
43
44
  /**
@@ -45,9 +46,9 @@ const errors_1 = require("../domain/errors");
45
46
  */
46
47
  function runDoctor(args) {
47
48
  const issues = [];
48
- let configProfiles = new Set();
49
49
  let currentProfile = null;
50
50
  let providers = null;
51
+ let document = null;
51
52
  if (!fs.existsSync(args.configPath)) {
52
53
  issues.push({
53
54
  code: "CONFIG_NOT_FOUND",
@@ -56,9 +57,8 @@ function runDoctor(args) {
56
57
  });
57
58
  }
58
59
  else {
59
- const configContent = fs.readFileSync(args.configPath, "utf8");
60
- configProfiles = (0, config_1.parseProfileNames)(configContent);
61
- currentProfile = (0, config_1.parseTopLevelProfile)(configContent);
60
+ document = (0, config_repo_1.readStructuredConfig)(args.configPath);
61
+ currentProfile = document.activeProfile;
62
62
  if (!currentProfile) {
63
63
  issues.push({
64
64
  code: "PROFILE_NOT_FOUND",
@@ -77,14 +77,11 @@ function runDoctor(args) {
77
77
  else {
78
78
  try {
79
79
  providers = (0, providers_repo_1.readProvidersFile)(args.providersPath);
80
- // Every managed provider must map to a profile that still exists in config.toml.
81
- for (const [name, provider] of Object.entries(providers.providers)) {
82
- if (!configProfiles.has(provider.profile)) {
80
+ if (document) {
81
+ for (const issue of (0, config_1.collectConfigConsistencyIssues)(document, providers)) {
83
82
  issues.push({
84
- code: "PROFILE_NOT_FOUND",
85
- message: `Provider "${name}" maps to missing profile "${provider.profile}".`,
86
- provider: name,
87
- profile: provider.profile,
83
+ ...issue,
84
+ message: renderConfigIssueMessage(issue),
88
85
  });
89
86
  }
90
87
  }
@@ -99,16 +96,6 @@ function runDoctor(args) {
99
96
  }
100
97
  }
101
98
  const drift = (0, runtime_state_1.inspectLiveStateDrift)(currentProfile, providers);
102
- if (drift.canBackfillActiveProvider) {
103
- // Distinguish unmanaged live state from hard parse/configuration errors.
104
- issues.push({
105
- code: "LIVE_STATE_DRIFT",
106
- message: `Active profile "${drift.currentProfile}" is present in config.toml but not mapped by providers.json.`,
107
- currentProfile: drift.currentProfile,
108
- suggestedAction: "backfill-active-provider",
109
- storage: (0, runtime_state_1.getStorageRoles)(),
110
- });
111
- }
112
99
  const codexCheck = (0, codex_cli_1.checkCodexAvailable)();
113
100
  if (!codexCheck.ok) {
114
101
  issues.push({
@@ -128,3 +115,17 @@ function runDoctor(args) {
128
115
  warnings: issues.length === 0 ? [] : [`doctor found ${issues.length} issue(s)`],
129
116
  };
130
117
  }
118
+ function renderConfigIssueMessage(issue) {
119
+ switch (issue.code) {
120
+ case "ORPHANED_PROFILE_REFERENCE":
121
+ return `Profile "${issue.profile}" is referenced by providers but missing from config.toml.`;
122
+ case "UNMANAGED_ACTIVE_PROFILE":
123
+ return `Active profile "${issue.profile}" is not mapped by providers.json.`;
124
+ case "SHARED_PROFILE_REFERENCE":
125
+ return `Profile "${issue.profile}" is shared by multiple providers.`;
126
+ case "ORPHANED_PROFILE_SECTION":
127
+ return `Profile section "${issue.profile}" is not linked to any provider.`;
128
+ case "DESTRUCTIVE_REMOVE_BLOCKED":
129
+ return `Provider "${issue.provider}" cannot be removed while "${issue.activeProfile}" remains active.`;
130
+ }
131
+ }
@@ -0,0 +1,155 @@
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.setupCodex = setupCodex;
37
+ const fs = __importStar(require("node:fs"));
38
+ const setup_1 = require("../domain/setup");
39
+ const errors_1 = require("../domain/errors");
40
+ const config_1 = require("../domain/config");
41
+ const codex_cli_1 = require("../infra/codex-cli");
42
+ const config_repo_1 = require("../infra/config-repo");
43
+ const fs_utils_1 = require("../infra/fs-utils");
44
+ const providers_repo_1 = require("../infra/providers-repo");
45
+ const run_doctor_1 = require("./run-doctor");
46
+ const run_mutation_1 = require("./run-mutation");
47
+ const MIN_CODEX_VERSION = "0.0.1";
48
+ /**
49
+ * Bootstraps a managed providers.json from the existing Codex directory.
50
+ */
51
+ function setupCodex(args) {
52
+ const available = (0, codex_cli_1.checkCodexAvailable)();
53
+ if (!available.ok) {
54
+ throw (0, errors_1.cliError)("CODEX_NOT_INSTALLED", "codex CLI is not available.", {
55
+ cause: available.cause,
56
+ });
57
+ }
58
+ const version = (0, codex_cli_1.checkCodexVersion)(MIN_CODEX_VERSION);
59
+ if (!version.ok) {
60
+ throw (0, errors_1.cliError)("CODEX_VERSION_UNSUPPORTED", "codex CLI version is below the supported minimum.", {
61
+ minimumVersion: MIN_CODEX_VERSION,
62
+ currentVersion: version.currentVersion ?? null,
63
+ cause: version.cause,
64
+ });
65
+ }
66
+ if (!fs.existsSync(args.codexDir)) {
67
+ throw (0, errors_1.cliError)("CODEX_DIR_NOT_FOUND", "The requested Codex directory does not exist.", {
68
+ codexDir: args.codexDir,
69
+ });
70
+ }
71
+ const document = (0, config_repo_1.readStructuredConfig)(args.configPath);
72
+ const profileViews = (0, config_1.buildManagedProfileViews)(document, null);
73
+ const adoptableProfiles = profileViews
74
+ .filter((view) => view.source === "unmanaged" && view.model && view.baseUrl)
75
+ .map((view) => view.name)
76
+ .sort();
77
+ if (profileViews.length === 0) {
78
+ throw (0, errors_1.cliError)("PROFILE_NOT_FOUND", "No profiles were found in config.toml.", {
79
+ file: args.configPath,
80
+ });
81
+ }
82
+ const invalidAdoptProfiles = args.adoptProfiles.filter((profile) => !adoptableProfiles.includes(profile));
83
+ if (invalidAdoptProfiles.length > 0) {
84
+ throw (0, errors_1.cliError)("INVALID_ARGUMENT", "setup only adopts unmanaged profiles that already contain model and base_url.", {
85
+ invalidProfiles: invalidAdoptProfiles.sort(),
86
+ adoptableProfiles,
87
+ });
88
+ }
89
+ if (args.adoptProfiles.length === 0) {
90
+ throw (0, errors_1.cliError)("INVALID_ARGUMENT", "setup requires at least one explicit profile to adopt.", {
91
+ adoptableProfiles,
92
+ });
93
+ }
94
+ const drafts = (0, setup_1.buildSetupDrafts)(args.adoptProfiles, args.providerDetailsByProfile);
95
+ const incompleteProfiles = (0, setup_1.findIncompleteSetupProfiles)(drafts);
96
+ if (incompleteProfiles.length > 0) {
97
+ throw (0, errors_1.cliError)("INVALID_ARGUMENT", "setup requires complete provider data for every selected profile.", {
98
+ incompleteProfiles,
99
+ });
100
+ }
101
+ (0, fs_utils_1.ensureDir)(args.codexDir);
102
+ const currentProviders = (0, providers_repo_1.readProvidersFileIfExists)(args.providersPath);
103
+ const providersExists = fs.existsSync(args.providersPath);
104
+ if (providersExists && args.strategy !== "merge" && args.strategy !== "overwrite") {
105
+ throw (0, errors_1.cliError)("PROVIDERS_ALREADY_EXISTS", "providers.json already exists.", {
106
+ file: args.providersPath,
107
+ });
108
+ }
109
+ const nextProviders = {
110
+ providers: drafts.reduce((accumulator, draft) => {
111
+ accumulator[draft.providerName] = draft.record;
112
+ return accumulator;
113
+ }, {}),
114
+ };
115
+ const finalProviders = args.strategy === "merge" ? (0, providers_repo_1.mergeProviders)(currentProviders, nextProviders) : nextProviders;
116
+ const result = (0, run_mutation_1.runMutation)({
117
+ codexDir: args.codexDir,
118
+ backupsDir: args.backupsDir,
119
+ latestBackupPath: args.latestBackupPath,
120
+ operation: "setup",
121
+ files: [
122
+ { absolutePath: args.providersPath, relativePath: "providers.json" },
123
+ { absolutePath: args.configPath, relativePath: "config.toml" },
124
+ ],
125
+ mutate: () => {
126
+ const configPlan = (0, config_repo_1.createConfigMutationPlan)(document, {});
127
+ (0, providers_repo_1.writeProvidersFile)(args.providersPath, finalProviders);
128
+ (0, config_repo_1.applyConfigMutation)(args.configPath, document, configPlan);
129
+ return {
130
+ codexDir: args.codexDir,
131
+ strategy: args.strategy,
132
+ providersInitialized: Object.keys(nextProviders.providers).length,
133
+ providerNames: Object.keys(finalProviders.providers).sort(),
134
+ createdProfileSections: configPlan.createdProfileSections,
135
+ deletedProfileSections: configPlan.deletedProfileSections,
136
+ keptSharedProfiles: [],
137
+ switchedActiveProfile: false,
138
+ adoptedProfiles: [...args.adoptProfiles].sort(),
139
+ repairedProfiles: [],
140
+ };
141
+ },
142
+ });
143
+ const doctor = (0, run_doctor_1.runDoctor)({
144
+ codexDir: args.codexDir,
145
+ configPath: args.configPath,
146
+ providersPath: args.providersPath,
147
+ });
148
+ return {
149
+ data: {
150
+ ...result.data,
151
+ doctor: doctor.data,
152
+ },
153
+ warnings: doctor.warnings,
154
+ };
155
+ }
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.showConfig = showConfig;
4
+ const config_1 = require("../domain/config");
5
+ const errors_1 = require("../domain/errors");
6
+ const config_repo_1 = require("../infra/config-repo");
7
+ const providers_repo_1 = require("../infra/providers-repo");
8
+ /**
9
+ * Returns the structured config view, optionally filtered to one profile.
10
+ */
11
+ function showConfig(args) {
12
+ const document = (0, config_repo_1.readStructuredConfig)(args.configPath);
13
+ const providers = (0, providers_repo_1.readProvidersFileIfExists)(args.providersPath);
14
+ const views = (0, config_1.buildManagedProfileViews)(document, providers);
15
+ let selectedProfile = null;
16
+ let profiles = views;
17
+ if (args.profileName) {
18
+ const found = views.find((view) => view.name === args.profileName);
19
+ if (!found) {
20
+ throw (0, errors_1.cliError)("PROFILE_NOT_FOUND", `Profile "${args.profileName}" was not found.`, {
21
+ profile: args.profileName,
22
+ });
23
+ }
24
+ selectedProfile = args.profileName;
25
+ profiles = [found];
26
+ }
27
+ return {
28
+ data: {
29
+ activeProfile: document.activeProfile,
30
+ selectedProfile,
31
+ profiles,
32
+ },
33
+ };
34
+ }
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.showProvider = showProvider;
4
+ const providers_1 = require("../domain/providers");
5
+ const providers_repo_1 = require("../infra/providers-repo");
6
+ /**
7
+ * Returns a single provider record, with text-mode callers able to use a masked preview.
8
+ */
9
+ function showProvider(args) {
10
+ const provider = (0, providers_repo_1.readProviderRecord)(args.providersPath, args.providerName);
11
+ return {
12
+ data: {
13
+ providerName: args.providerName,
14
+ provider: args.includeSecret
15
+ ? provider
16
+ : {
17
+ ...provider,
18
+ apiKey: (0, providers_1.maskSecret)(provider.apiKey),
19
+ },
20
+ },
21
+ };
22
+ }
@@ -17,7 +17,7 @@ function switchProvider(args) {
17
17
  availableProviders: Object.keys(providers.providers).sort(),
18
18
  });
19
19
  }
20
- const configContent = (0, config_repo_1.ensureProfileExists)(args.configPath, provider.profile, args.providerName);
20
+ const document = (0, config_repo_1.ensureProfileExists)(args.configPath, provider.profile, args.providerName);
21
21
  return (0, run_mutation_1.runMutation)({
22
22
  codexDir: args.codexDir,
23
23
  backupsDir: args.backupsDir,
@@ -28,8 +28,11 @@ function switchProvider(args) {
28
28
  { absolutePath: args.authPath, relativePath: "auth.json" },
29
29
  ],
30
30
  mutate: () => {
31
+ const configPlan = (0, config_repo_1.createConfigMutationPlan)(document, {
32
+ setActiveProfile: provider.profile,
33
+ });
31
34
  // Update the runtime profile first so any subsequent login is associated with the new target.
32
- (0, config_repo_1.updateTopLevelProfile)(args.configPath, configContent, provider.profile);
35
+ (0, config_repo_1.applyConfigMutation)(args.configPath, document, configPlan);
33
36
  if (!args.noLogin) {
34
37
  (0, codex_cli_1.runCodexLogin)(provider.apiKey, args.codexDir);
35
38
  }
@@ -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
- const config_repo_1 = require("../infra/config-repo");
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, configPath) {
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)("INVALID_IMPORT_FILE", "add requires <provider>, --profile, and --api-key when running without an interactive TTY.", {
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
@@ -11,6 +11,7 @@ const codex_paths_1 = require("../infra/codex-paths");
11
11
  function parseArgs(argv) {
12
12
  let json = false;
13
13
  let codexDir = (0, codex_paths_1.resolveCodexDir)();
14
+ let codexDirExplicit = false;
14
15
  const remaining = [];
15
16
  for (let index = 0; index < argv.length; index += 1) {
16
17
  const value = argv[index];
@@ -21,25 +22,28 @@ function parseArgs(argv) {
21
22
  if (value === "--codex-dir") {
22
23
  const next = argv[index + 1];
23
24
  if (!next) {
24
- throw (0, errors_1.cliError)("INVALID_IMPORT_FILE", "--codex-dir requires a path value.");
25
+ throw (0, errors_1.cliError)("INVALID_ARGUMENT", "--codex-dir requires a path value.");
25
26
  }
26
27
  codexDir = (0, codex_paths_1.resolveCodexDir)(next);
28
+ codexDirExplicit = true;
27
29
  index += 1;
28
30
  continue;
29
31
  }
30
32
  remaining.push(value);
31
33
  }
32
34
  if (remaining[0] === "help") {
35
+ const helpTarget = remaining[1] === "backups" && remaining[2] === "list" ? "backups" : remaining[1] ?? null;
33
36
  return {
34
37
  command: null,
35
38
  positionals: [],
36
39
  globalOptions: {
37
40
  json,
38
41
  codexDir,
42
+ codexDirExplicit,
39
43
  },
40
44
  commandOptions: new Map(),
41
45
  helpRequested: true,
42
- helpTarget: remaining[1] ?? null,
46
+ helpTarget,
43
47
  versionRequested: false,
44
48
  };
45
49
  }
@@ -51,11 +55,19 @@ function parseArgs(argv) {
51
55
  versionRequested: true,
52
56
  });
53
57
  }
54
- const command = remaining[0] ?? null;
58
+ const commandToken = remaining[0] ?? null;
59
+ const command = commandToken === "backups" && remaining[1] === "list"
60
+ ? "backups-list"
61
+ : commandToken === "config" && remaining[1] === "show"
62
+ ? "config-show"
63
+ : commandToken === "config" && remaining[1] === "list-profiles"
64
+ ? "config-list-profiles"
65
+ : commandToken;
55
66
  const positionals = [];
56
67
  const commandOptions = new Map();
57
68
  let helpRequested = false;
58
- for (let index = 1; index < remaining.length; index += 1) {
69
+ const startIndex = command === "backups-list" || command === "config-show" || command === "config-list-profiles" ? 2 : 1;
70
+ for (let index = startIndex; index < remaining.length; index += 1) {
59
71
  const value = remaining[index];
60
72
  if (value === "--help" || value === "-h") {
61
73
  helpRequested = true;
@@ -83,10 +95,11 @@ function parseArgs(argv) {
83
95
  globalOptions: {
84
96
  json,
85
97
  codexDir,
98
+ codexDirExplicit,
86
99
  },
87
100
  commandOptions,
88
101
  helpRequested,
89
- helpTarget: helpRequested ? command : null,
102
+ helpTarget: helpRequested ? (command === "backups-list" ? "backups" : command) : null,
90
103
  versionRequested: false,
91
104
  };
92
105
  }
@@ -100,6 +113,7 @@ function defaultParsed(command, overrides) {
100
113
  globalOptions: {
101
114
  json: overrides?.json ?? false,
102
115
  codexDir: overrides?.codexDir ?? (0, codex_paths_1.resolveCodexDir)(),
116
+ codexDirExplicit: false,
103
117
  },
104
118
  commandOptions: new Map(),
105
119
  helpRequested: overrides?.helpRequested ?? false,