@minniexcode/codex-switch 0.0.12 → 0.1.0

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.md CHANGED
@@ -1,183 +1,183 @@
1
- # @minniexcode/codex-switch
2
-
3
- `@minniexcode/codex-switch` is a local-first CLI for managing and switching Codex provider and profile configuration safely.
4
-
5
- It keeps `codex-switch` tool state separate from the target Codex runtime, so provider management, backup flow, and runtime projection stay explicit instead of relying on manual file edits.
6
-
7
- Chinese version: [README.CN.md](./README.CN.md)
8
-
9
- ## Version
10
-
11
- Current package version: `0.0.12`
12
-
13
- This repository is still on a development-version line. The current release focuses on making the primary workflows, help text, and operational boundaries consistent with the implementation.
14
-
15
- ## Install
16
-
17
- ```bash
18
- npm install -g @minniexcode/codex-switch
19
- ```
20
-
21
- Run without a global install:
22
-
23
- ```bash
24
- npx @minniexcode/codex-switch --help
25
- ```
26
-
27
- Built CLI entrypoint:
28
-
29
- ```bash
30
- codexs --help
31
- ```
32
-
33
- ## Primary Workflows
34
-
35
- Direct provider workflow:
36
-
37
- ```bash
38
- codexs init
39
- codexs add my-provider --profile my-provider --api-key sk-xxx
40
- codexs switch my-provider
41
- codexs status
42
- codexs doctor
43
- ```
44
-
45
- GitHub Copilot workflow:
46
-
47
- ```bash
48
- codexs init
49
- codexs login copilot
50
- codexs add copilot-main --copilot --profile copilot-main
51
- codexs switch copilot-main
52
- codexs status
53
- codexs doctor
54
- ```
55
-
56
- Notes:
57
-
58
- - `init` prepares the `codex-switch` tool home and managed state.
59
- - `login copilot` handles upstream Copilot onboarding and auth readiness.
60
- - `add --copilot` does not perform login for you; it assumes Copilot login is already ready.
61
- - `status` is the main read command after switching.
62
- - `doctor` is the main repair-oriented diagnostic command.
63
-
64
- ## Advanced Adopt Workflow
65
-
66
- Use `migrate` only when you already have Codex runtime state that should be adopted into managed `providers.json` state:
67
-
68
- ```bash
69
- codexs init
70
- codexs migrate
71
- ```
72
-
73
- `migrate` is an advanced adopt helper. It is not the default first step for a fresh install.
74
-
75
- ## Command Surface
76
-
77
- ```bash
78
- codexs init
79
- codexs login copilot
80
- codexs migrate
81
- codexs list
82
- codexs show <provider>
83
- codexs current
84
- codexs status
85
- codexs config show [profile]
86
- codexs config list-profiles
87
- codexs add <provider> --profile <name> --api-key <key>
88
- codexs add <provider> --copilot --profile <name>
89
- codexs edit <provider>
90
- codexs switch <provider>
91
- codexs remove <provider> [--force] [--switch-to <profile>]
92
- codexs import <file>
93
- codexs export <file> [--force]
94
- codexs bridge start [provider]
95
- codexs bridge status [provider]
96
- codexs bridge stop [provider]
97
- codexs backups list
98
- codexs rollback [backup-id]
99
- codexs doctor
100
- ```
101
-
102
- `setup` still exists only as a deprecated compatibility entry that points callers to `init` or `migrate`.
103
-
104
- ## Runtime Model
105
-
106
- `codex-switch` uses a dual-path model.
107
-
108
- Tool home:
109
-
110
- ```text
111
- ~/.config/codex-switch/
112
- codex-switch.json
113
- providers.json
114
- backups/
115
- runtime/
116
- runtimes/
117
- ```
118
-
119
- Target Codex runtime:
120
-
121
- ```text
122
- ~/.codex/
123
- config.toml
124
- auth.json
125
- ```
126
-
127
- Key points:
128
-
129
- - `providers.json` is the managed provider registry and lives under the tool home.
130
- - `codex-switch.json` stores tool-level metadata such as `defaultCodexDir`.
131
- - `config.toml` remains the active runtime routing file in the target Codex directory.
132
- - `auth.json` remains the active auth projection file in the target Codex directory.
133
- - Direct providers rewrite `OPENAI_API_KEY` into the active runtime projection.
134
- - Copilot providers keep upstream GitHub authentication in the official Copilot runtime while `codex-switch` manages local bridge state and routing.
135
-
136
- Path controls:
137
-
138
- - `--codex-dir <path>` targets a specific Codex runtime directory.
139
- - `CODEXS_CODEX_DIR` provides the default target runtime when `--codex-dir` is not passed.
140
- - `CODEXS_HOME` overrides the tool home location.
141
-
142
- ## Automation Notes
143
-
144
- This CLI supports both human TTY usage and non-interactive automation.
145
-
146
- Global flags:
147
-
148
- ```bash
149
- --json
150
- --codex-dir <path>
151
- --help
152
- --version
153
- ```
154
-
155
- Operational limits:
156
-
157
- - `login copilot` requires a real TTY and does not support `--json`.
158
- - `migrate` still depends on interactive profile selection and provider-detail collection in this release.
159
- - Automation should pass explicit arguments and prefer `--json` for stable parsing.
160
-
161
- ## Local Development
162
-
163
- ```bash
164
- npm run build
165
- npm test
166
- npx tsc --noEmit
167
- node dist/cli.js --help
168
- npm pack --dry-run
169
- ```
170
-
171
- ## Documentation
172
-
173
- - [Chinese README](./README.CN.md)
174
- - [AI README](./README.AI.md)
175
- - [Detailed CLI Usage](./docs/cli-usage.md)
176
- - [Testing Guide](./docs/Tests/testing.md)
177
- - [Product Overview](./docs/codex-switch-product-overview.md)
178
- - [PRD 0.0.12](./docs/PRD/codex-switch-prd-v0.0.12.md)
179
- - [Release Gate PRD 0.1.0](./docs/PRD/codex-switch-prd-v0.1.0.md)
180
-
181
- ## License
182
-
183
- MIT
1
+ # @minniexcode/codex-switch
2
+
3
+ `@minniexcode/codex-switch` is a local-first CLI for managing and switching Codex provider and profile configuration safely.
4
+
5
+ It keeps `codex-switch` tool state separate from the target Codex runtime, so provider management, backup flow, and runtime projection stay explicit instead of relying on manual file edits.
6
+
7
+ Chinese version: [README.CN.md](./README.CN.md)
8
+
9
+ ## Version
10
+
11
+ Current package version: `0.1.0`
12
+
13
+ This is the first stable release line. The current release focuses on keeping the primary workflows, help text, operational boundaries, and release docs aligned with the implementation.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ npm install -g @minniexcode/codex-switch
19
+ ```
20
+
21
+ Run without a global install:
22
+
23
+ ```bash
24
+ npx @minniexcode/codex-switch --help
25
+ ```
26
+
27
+ Built CLI entrypoint:
28
+
29
+ ```bash
30
+ codexs --help
31
+ ```
32
+
33
+ ## Primary Workflows
34
+
35
+ Direct provider workflow:
36
+
37
+ ```bash
38
+ codexs init
39
+ codexs add my-provider --profile my-provider --api-key sk-xxx
40
+ codexs switch my-provider
41
+ codexs status
42
+ codexs doctor
43
+ ```
44
+
45
+ GitHub Copilot workflow:
46
+
47
+ ```bash
48
+ codexs init
49
+ codexs login copilot
50
+ codexs add copilot-main --copilot --profile copilot-main
51
+ codexs switch copilot-main
52
+ codexs status
53
+ codexs doctor
54
+ ```
55
+
56
+ Notes:
57
+
58
+ - `init` prepares the `codex-switch` tool home and managed state.
59
+ - `login copilot` handles upstream Copilot onboarding and auth readiness.
60
+ - `add --copilot` does not perform login for you; it assumes Copilot login is already ready.
61
+ - `status` is the main read command after switching.
62
+ - `doctor` is the main repair-oriented diagnostic command.
63
+
64
+ ## Advanced Adopt Workflow
65
+
66
+ Use `migrate` only when you already have Codex runtime state that should be adopted into managed `providers.json` state:
67
+
68
+ ```bash
69
+ codexs init
70
+ codexs migrate
71
+ ```
72
+
73
+ `migrate` is an advanced adopt helper. It is not the default first step for a fresh install.
74
+
75
+ ## Command Surface
76
+
77
+ ```bash
78
+ codexs init
79
+ codexs login copilot
80
+ codexs migrate
81
+ codexs list
82
+ codexs show <provider>
83
+ codexs current
84
+ codexs status
85
+ codexs config show [profile]
86
+ codexs config list-profiles
87
+ codexs add <provider> --profile <name> --api-key <key>
88
+ codexs add <provider> --copilot --profile <name>
89
+ codexs edit <provider>
90
+ codexs switch <provider>
91
+ codexs remove <provider> [--force] [--switch-to <profile>]
92
+ codexs import <file>
93
+ codexs export <file> [--force]
94
+ codexs bridge start [provider]
95
+ codexs bridge status [provider]
96
+ codexs bridge stop [provider]
97
+ codexs backups list
98
+ codexs rollback [backup-id]
99
+ codexs doctor
100
+ ```
101
+
102
+ `setup` still exists only as a deprecated compatibility entry that points callers to `init` or `migrate`.
103
+
104
+ ## Runtime Model
105
+
106
+ `codex-switch` uses a dual-path model.
107
+
108
+ Tool home:
109
+
110
+ ```text
111
+ ~/.config/codex-switch/
112
+ codex-switch.json
113
+ providers.json
114
+ backups/
115
+ runtime/
116
+ runtimes/
117
+ ```
118
+
119
+ Target Codex runtime:
120
+
121
+ ```text
122
+ ~/.codex/
123
+ config.toml
124
+ auth.json
125
+ ```
126
+
127
+ Key points:
128
+
129
+ - `providers.json` is the managed provider registry and lives under the tool home.
130
+ - `codex-switch.json` stores tool-level metadata such as `defaultCodexDir`.
131
+ - `config.toml` remains the active runtime routing file in the target Codex directory.
132
+ - `auth.json` remains the active auth projection file in the target Codex directory.
133
+ - Direct providers rewrite `OPENAI_API_KEY` into the active runtime projection.
134
+ - Copilot providers keep upstream GitHub authentication in the official Copilot runtime while `codex-switch` manages local bridge state and routing.
135
+
136
+ Path controls:
137
+
138
+ - `--codex-dir <path>` targets a specific Codex runtime directory.
139
+ - `CODEXS_CODEX_DIR` provides the default target runtime when `--codex-dir` is not passed.
140
+ - `CODEXS_HOME` overrides the tool home location.
141
+
142
+ ## Automation Notes
143
+
144
+ This CLI supports both human TTY usage and non-interactive automation.
145
+
146
+ Global flags:
147
+
148
+ ```bash
149
+ --json
150
+ --codex-dir <path>
151
+ --help
152
+ --version
153
+ ```
154
+
155
+ Operational limits:
156
+
157
+ - `login copilot` requires a real TTY and does not support `--json`.
158
+ - `migrate` remains interactive when provider adoption requires human input.
159
+ - Automation should pass explicit arguments and prefer `--json` for stable parsing.
160
+
161
+ ## Local Development
162
+
163
+ ```bash
164
+ npm run build
165
+ npm test
166
+ npx tsc --noEmit
167
+ node dist/cli.js --help
168
+ npm pack --dry-run
169
+ ```
170
+
171
+ ## Documentation
172
+
173
+ - [Chinese README](./README.CN.md)
174
+ - [AI README](./README.AI.md)
175
+ - [Detailed CLI Usage](./docs/cli-usage.md)
176
+ - [Testing Guide](./docs/Tests/testing.md)
177
+ - [Product Overview](./docs/codex-switch-product-overview.md)
178
+ - [Release PRD 0.1.0](./docs/PRD/codex-switch-prd-v0.1.0.md)
179
+ - [Release Gate PRD 0.1.0](./docs/PRD/codex-switch-prd-v0.1.0.md)
180
+
181
+ ## License
182
+
183
+ MIT
@@ -99,6 +99,7 @@ async function addProvider(args) {
99
99
  provider: args.providerName,
100
100
  });
101
101
  }
102
+ const directBaseUrl = args.baseUrl;
102
103
  const upsertProfiles = !existingProfile && args.createProfile
103
104
  ? {
104
105
  [args.profile]: (0, config_1.validateManagedProfileCreation)(args.profile, {
@@ -107,15 +108,20 @@ async function addProvider(args) {
107
108
  }),
108
109
  }
109
110
  : undefined;
111
+ if (!args.copilot && !existingModelProvider && args.createProfile && (!directBaseUrl || directBaseUrl.trim() === "")) {
112
+ throw (0, errors_1.cliError)("MANAGED_PROFILE_FIELDS_MISSING", `Model provider "${args.profile}" requires base_url.`, {
113
+ profile: args.profile,
114
+ modelProvider: args.profile,
115
+ missingFields: ["base_url"],
116
+ });
117
+ }
110
118
  const upsertModelProviders = args.copilot
111
119
  ? {
112
120
  [args.profile]: (0, providers_1.buildCopilotModelProviderProjection)(runtime),
113
121
  }
114
122
  : !existingModelProvider && args.createProfile
115
123
  ? {
116
- [args.profile]: {
117
- baseUrl: args.baseUrl ?? undefined,
118
- },
124
+ [args.profile]: (0, providers_1.buildDirectModelProviderProjection)(args.profile, directBaseUrl),
119
125
  }
120
126
  : undefined;
121
127
  if (existingProfile) {
@@ -53,6 +53,13 @@ function editProvider(args) {
53
53
  provider: args.providerName,
54
54
  });
55
55
  }
56
+ if (!args.baseUrl || args.baseUrl.trim() === "") {
57
+ throw (0, errors_1.cliError)("MANAGED_PROFILE_FIELDS_MISSING", `Model provider "${newProfile}" requires base_url.`, {
58
+ profile: newProfile,
59
+ modelProvider: newProfile,
60
+ missingFields: ["base_url"],
61
+ });
62
+ }
56
63
  upsertProfiles = {
57
64
  [newProfile]: (0, config_1.validateManagedProfileCreation)(newProfile, {
58
65
  model: args.model ?? undefined,
@@ -60,14 +67,21 @@ function editProvider(args) {
60
67
  }),
61
68
  };
62
69
  upsertModelProviders = {
63
- [newProfile]: {
64
- baseUrl: args.baseUrl ?? undefined,
65
- },
70
+ [newProfile]: (0, providers_1.buildDirectModelProviderProjection)(newProfile, args.baseUrl),
66
71
  };
67
72
  }
68
73
  else {
69
74
  (0, config_repo_1.requireManagedProfileRuntime)(document, providers, newProfile);
70
75
  }
76
+ if (targetProfileExists &&
77
+ !current.runtime &&
78
+ args.baseUrl !== undefined &&
79
+ args.baseUrl !== null) {
80
+ upsertModelProviders = {
81
+ ...(upsertModelProviders ?? {}),
82
+ [newProfile]: (0, providers_1.buildDirectModelProviderProjection)(newProfile, args.baseUrl),
83
+ };
84
+ }
71
85
  const nextRecord = (0, providers_1.cleanProviderRecord)({
72
86
  profile: newProfile,
73
87
  apiKey: args.apiKey ?? current.apiKey,
@@ -245,6 +245,10 @@ function renderConfigIssueMessage(issue) {
245
245
  return `Model provider section "${issue.modelProvider}" for profile "${issue.profile}" is missing from config.toml.`;
246
246
  case "MODEL_PROVIDER_BASE_URL_MISSING":
247
247
  return `Model provider section "${issue.modelProvider}" for profile "${issue.profile}" is missing base_url.`;
248
+ case "PROVIDER_BASE_URL_MISMATCH":
249
+ return issue.providerType === "direct"
250
+ ? `Direct provider "${issue.provider}" baseUrl does not match config.toml model provider "${issue.profile}" base_url.`
251
+ : String(issue.code ?? "UNKNOWN_ISSUE");
248
252
  case "ACTIVE_PROVIDER_UNRESOLVED":
249
253
  return `Active profile "${issue.profile}" maps to multiple providers, so the active managed provider cannot be resolved uniquely.`;
250
254
  case "AUTH_JSON_INVALID":
@@ -93,6 +93,9 @@ function renderHumanSuccess(command, data, warnings) {
93
93
  if (!activeProviderResolvable && activeCandidates.length > 1) {
94
94
  lines.push(`Current provider: ambiguous (${activeCandidates.join(", ")})`);
95
95
  }
96
+ else if (!activeProviderResolvable) {
97
+ lines.push("Current provider: unmanaged or unresolved");
98
+ }
96
99
  }
97
100
  for (const provider of providers) {
98
101
  const tags = Array.isArray(provider.tags) && provider.tags.length > 0
@@ -265,6 +268,9 @@ function renderStatusHealth(data) {
265
268
  if (issues.some((issue) => issue.code === "ACTIVE_PROVIDER_UNRESOLVED")) {
266
269
  return "active provider ambiguous";
267
270
  }
271
+ if (issues.some((issue) => issue.code === "PROVIDER_BASE_URL_MISMATCH")) {
272
+ return "provider projection drift";
273
+ }
268
274
  if (activePathUsesCopilot && copilotSdk.installed === false) {
269
275
  return "copilot sdk missing";
270
276
  }
@@ -335,6 +341,8 @@ function renderDoctorIssueNextStep(issue) {
335
341
  case "ACTIVE_PROVIDER_UNRESOLVED":
336
342
  case "SHARED_PROFILE_REFERENCE":
337
343
  return "make provider-to-profile mappings unique before relying on current-provider detection";
344
+ case "PROVIDER_BASE_URL_MISMATCH":
345
+ return "rerun `codexs edit <provider> --base-url <url>` or `codexs switch <provider>` to repair the runtime projection";
338
346
  default:
339
347
  return "inspect the issue details and rerun `codexs doctor` after fixing the state";
340
348
  }
@@ -150,9 +150,13 @@ exports.COMMANDS = [
150
150
  tokens: ["list"],
151
151
  handler: handlers_1.handleRegisteredCommand,
152
152
  group: "read",
153
- summary: "List configured providers from providers.json.",
153
+ summary: "List managed providers with profile, type, and current-state hints.",
154
154
  usage: ["codexs list [--json] [--codex-dir <path>]"],
155
- details: ["Reads providers.json and prints provider-to-profile mappings.", "Use --json for machine-readable automation output."],
155
+ details: [
156
+ "Reads providers.json and prints provider-to-profile mappings together with provider type.",
157
+ "When the active profile is shared by multiple providers, list surfaces the ambiguity instead of inventing one current provider.",
158
+ "Use --json for machine-readable automation output.",
159
+ ],
156
160
  examples: ["codexs list", "codexs list --json"],
157
161
  },
158
162
  {
@@ -184,12 +188,13 @@ exports.COMMANDS = [
184
188
  tokens: ["status"],
185
189
  handler: handlers_1.handleRegisteredCommand,
186
190
  group: "read",
187
- summary: "Show tool-home, target-runtime, provider, and runtime-health status.",
191
+ summary: "Show tool-home, target-runtime, provider-path, and runtime-health status.",
188
192
  usage: ["codexs status [--json] [--codex-dir <path>]"],
189
193
  details: [
190
194
  "Reports the target Codex runtime, tool-home storage roles, current profile, and whether the live profile is mapped.",
191
195
  "When the active provider uses a local runtime bridge, status also reports bridge, Copilot SDK, and upstream auth state.",
192
196
  "Surfaces dual-path config consistency signals without mutating any files.",
197
+ "Organizes the human-readable view around current state, health impact, and next step.",
193
198
  "Use doctor for deeper diagnostics.",
194
199
  ],
195
200
  examples: ["codexs status", "codexs status --json --codex-dir ./.tmp-codex"],
@@ -324,7 +329,7 @@ exports.COMMANDS = [
324
329
  tokens: ["doctor"],
325
330
  handler: handlers_1.handleRegisteredCommand,
326
331
  group: "recovery",
327
- summary: "Run repair-oriented diagnostics across tool-home and target-runtime state.",
332
+ summary: "Run issue-first diagnostics across tool-home and target-runtime state.",
328
333
  usage: ["codexs doctor [--json] [--codex-dir <path>]"],
329
334
  details: [
330
335
  "Checks the expected config files, provider/profile consistency, and Codex CLI availability.",
@@ -268,6 +268,8 @@ function buildManagedProfileViews(document, providers) {
268
268
  */
269
269
  function collectConfigConsistencyIssues(document, providers) {
270
270
  const issues = [];
271
+ const providerMap = providers?.providers ?? null;
272
+ const profileLinkMap = buildProfileLinkMap(providers);
271
273
  for (const view of buildManagedProfileViews(document, providers)) {
272
274
  if (view.source === "orphaned-reference") {
273
275
  issues.push({
@@ -319,11 +321,34 @@ function collectConfigConsistencyIssues(document, providers) {
319
321
  modelProvider: view.modelProvider,
320
322
  });
321
323
  }
324
+ else {
325
+ const profileLinkInfo = profileLinkMap.get(view.name);
326
+ if (profileLinkInfo &&
327
+ profileLinkInfo.linkedProviders.length === 1 &&
328
+ providerMap) {
329
+ const providerName = profileLinkInfo.linkedProviders[0];
330
+ const provider = providerMap[providerName];
331
+ if (provider &&
332
+ !provider.runtime &&
333
+ typeof provider.baseUrl === "string" &&
334
+ provider.baseUrl.trim() !== "" &&
335
+ provider.baseUrl !== modelProviderSection.baseUrl) {
336
+ issues.push({
337
+ code: "PROVIDER_BASE_URL_MISMATCH",
338
+ profile: view.name,
339
+ provider: providerName,
340
+ providerBaseUrl: provider.baseUrl,
341
+ configBaseUrl: modelProviderSection.baseUrl,
342
+ providerType: "direct",
343
+ });
344
+ }
345
+ }
346
+ }
322
347
  }
323
348
  }
324
349
  }
325
350
  if (document.activeProfile) {
326
- const activeLinkInfo = buildProfileLinkMap(providers).get(document.activeProfile);
351
+ const activeLinkInfo = profileLinkMap.get(document.activeProfile);
327
352
  if (!activeLinkInfo) {
328
353
  issues.push({
329
354
  code: "UNMANAGED_ACTIVE_PROFILE",
@@ -10,6 +10,7 @@ exports.isRuntimeBackedProvider = isRuntimeBackedProvider;
10
10
  exports.isCopilotBridgeProvider = isCopilotBridgeProvider;
11
11
  exports.buildCopilotBridgeBaseUrl = buildCopilotBridgeBaseUrl;
12
12
  exports.buildCopilotModelProviderProjection = buildCopilotModelProviderProjection;
13
+ exports.buildDirectModelProviderProjection = buildDirectModelProviderProjection;
13
14
  /**
14
15
  * Validates and normalizes unknown JSON into the providers.json domain model.
15
16
  */
@@ -162,6 +163,21 @@ function buildCopilotModelProviderProjection(runtime) {
162
163
  wireApi: "responses",
163
164
  };
164
165
  }
166
+ /**
167
+ * Builds the Codex-facing custom model_provider projection for a direct provider.
168
+ */
169
+ function buildDirectModelProviderProjection(profile, baseUrl) {
170
+ const normalizedBaseUrl = baseUrl.trim();
171
+ if (!normalizedBaseUrl) {
172
+ throw new Error(`Direct model provider "${profile}" requires a non-empty base_url.`);
173
+ }
174
+ return {
175
+ baseUrl: normalizedBaseUrl,
176
+ name: profile.trim(),
177
+ requiresOpenAiAuth: true,
178
+ wireApi: "responses",
179
+ };
180
+ }
165
181
  /**
166
182
  * Validates one runtime-backed provider block.
167
183
  */