@minniexcode/codex-switch 0.0.11 → 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,217 +1,183 @@
1
- ## @minniexcode/codex-switch
2
-
3
- `codex-switch` is a local-first CLI for managing and switching Codex provider/profile configuration safely.
4
-
5
- It is designed for users who work with multiple Codex providers, API keys, or profiles and want a repeatable, backup-first workflow instead of manually editing files under `~/.codex/`.
6
-
7
- 中文版: [README.CN.md](./README.CN.md)
8
-
9
- ## Overview
10
-
11
- What it does in `0.0.11`:
12
-
13
- - Initializes a dedicated `codex-switch` tool home
14
- - Adopts unmanaged runtime profiles from an existing Codex directory
15
- - Lists, shows, adds, edits, and removes provider records
16
- - Switches the active provider/profile safely
17
- - Supports explicit GitHub Copilot upstream onboarding
18
- - Manages the local Copilot bridge runtime explicitly
19
- - Imports and exports provider definitions
20
- - Runs diagnostics and detects local drift
21
- - Lists backups and rolls back to a previous managed state
22
- - Inspects `config.toml` profiles through structured read commands
23
-
24
- Current version: `0.0.11`
25
-
26
- ## Install
27
-
28
- Install globally:
29
-
30
- ```bash
31
- npm install -g @minniexcode/codex-switch
32
- ```
33
-
34
- Or run directly:
35
-
36
- ```bash
37
- npx @minniexcode/codex-switch --help
38
- ```
39
-
40
- CLI entry:
41
-
42
- ```bash
43
- codexs --help
44
- ```
45
-
46
- ## Quick Start
47
-
48
- Initialize tool state and adopt an existing Codex runtime:
49
-
50
- ```bash
51
- codexs init
52
- codexs migrate
53
- ```
54
-
55
- Inspect managed providers and config:
56
-
57
- ```bash
58
- codexs list
59
- codexs show my-provider
60
- codexs config show
61
- ```
62
-
63
- Add and switch a direct provider:
64
-
65
- ```bash
66
- codexs add my-provider --profile my-provider --api-key sk-xxx
67
- codexs switch my-provider
68
- ```
69
-
70
- Prepare GitHub Copilot and manage its bridge:
71
-
72
- ```bash
73
- codexs login copilot
74
- codexs add copilot-main --copilot --profile copilot-main
75
- codexs bridge start copilot-main
76
- ```
77
-
78
- Check runtime state:
79
-
80
- ```bash
81
- codexs current
82
- codexs status
83
- codexs doctor
84
- ```
85
-
86
- ## Common Commands
87
-
88
- ```bash
89
- codexs init
90
- codexs login copilot
91
- codexs migrate
92
- codexs list
93
- codexs show <provider>
94
- codexs current
95
- codexs status
96
- codexs config show [profile]
97
- codexs config list-profiles
98
- codexs add <provider> --profile <name> --api-key <key>
99
- codexs add <provider> --copilot --profile <name>
100
- codexs edit <provider>
101
- codexs switch <provider>
102
- codexs bridge start [provider]
103
- codexs bridge status [provider]
104
- codexs bridge stop [provider]
105
- codexs remove <provider> [--force] [--switch-to <profile>]
106
- codexs import <file> [--merge]
107
- codexs export <file> [--force]
108
- codexs backups list
109
- codexs rollback [backup-id]
110
- codexs doctor
111
- ```
112
-
113
- Command help:
114
-
115
- ```bash
116
- codexs help init
117
- codexs help login
118
- codexs help add
119
- codexs help bridge
120
- codexs help config
121
- codexs help migrate
122
- ```
123
-
124
- ## How It Works
125
-
126
- Starting in `0.0.11`, `codex-switch` uses a dual-path model.
127
-
128
- Tool home:
129
-
130
- ```text
131
- ~/.config/codex-switch/
132
- codex-switch.json
133
- providers.json
134
- backups/
135
- runtime/
136
- runtimes/
137
- ```
138
-
139
- Target Codex runtime:
140
-
141
- ```text
142
- ~/.codex/
143
- config.toml
144
- auth.json
145
- ```
146
-
147
- Notes:
148
-
149
- - `providers.json` is the managed provider registry and now lives under the tool home
150
- - `codex-switch.json` stores tool-level metadata such as `defaultCodexDir`
151
- - `config.toml` is still the active runtime-routing file in the target Codex directory
152
- - `auth.json` is the active auth projection file
153
- - direct-provider switches rewrite `OPENAI_API_KEY`
154
- - Copilot bridge providers keep upstream login in the official Copilot runtime while `codex-switch` manages the local bridge secret, bridge state, and routing
155
- - mutating commands back up before writing and rollback stays available after failed or undesired changes
156
-
157
- Path overrides and resolution:
158
-
159
- - `--codex-dir <path>` explicitly targets a Codex runtime directory
160
- - `CODEXS_CODEX_DIR` sets the default target when `--codex-dir` is not passed
161
- - `CODEXS_HOME` overrides the tool home location
162
-
163
- ## Automation
164
-
165
- This CLI supports both human TTY use and non-interactive automation.
166
-
167
- Current exceptions:
168
-
169
- - `login copilot` requires a real TTY and does not support `--json`
170
- - `migrate` remains intentionally interactive for adopt profile selection and provider detail collection
171
-
172
- Recommended global flags:
173
-
174
- ```bash
175
- --json
176
- --codex-dir <path>
177
- --help
178
- --version
179
- ```
180
-
181
- Recommendations:
182
-
183
- - use `--json` for stable machine-readable output
184
- - pass all required arguments explicitly in scripts or CI
185
- - use `--codex-dir <path>` for sandbox or test environments
186
- - use `CODEXS_HOME` when you want tool state isolated from your default workstation setup
187
-
188
- ## Testing
189
-
190
- Build and test locally:
191
-
192
- ```bash
193
- npm run build
194
- npm test
195
- npx tsc --noEmit
196
- ```
197
-
198
- The repository includes a development fixture under `dev-codex/local-sandbox` plus dedicated test docs:
199
-
200
- - [Testing Guide](./docs/Tests/testing.md)
201
- - [Bridge Testing Notes](./docs/Tests/testing-bridge-v0.0.9.md)
202
- - [Test Report for 0.0.7](./docs/Tests/test-report-0.0.7.md)
203
-
204
- ## Documentation
205
-
206
- - [Chinese README](./README.CN.md)
207
- - [AI README](./README.AI.md)
208
- - [Detailed CLI Usage](./docs/cli-usage.md)
209
- - [Testing Guide](./docs/Tests/testing.md)
210
- - [Product Overview](./docs/codex-switch-product-overview.md)
211
- - [Technical Architecture](./docs/codex-switch-technical-architecture.md)
212
- - [PRD 0.0.11](./docs/PRD/codex-switch-prd-v0.0.11.md)
213
- - [Design Doc 0.0.11](./docs/Design/codex-switch-v0.0.11-design.md)
214
-
215
- ## License
216
-
217
- 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,
@@ -67,8 +67,10 @@ async function getStatus(codexDir, configPath, providersPath, authPath, options)
67
67
  }
68
68
  }
69
69
  const liveState = (0, runtime_state_1.inspectLiveStateDrift)(currentProfile, providers);
70
- const activeProviderCandidates = currentProfile && providers ? (0, providers_1.findProvidersByProfile)(providers, currentProfile) : [];
71
- const activeProvider = activeProviderCandidates.length === 1 && providers ? providers.providers[activeProviderCandidates[0]] : null;
70
+ const activeProviderCandidates = liveState.mappedProviders;
71
+ const activeProvider = liveState.providerResolvable && providers && liveState.mappedProvider
72
+ ? providers.providers[liveState.mappedProvider]
73
+ : null;
72
74
  const copilotInstall = (0, copilot_installer_1.probeCopilotSdkInstall)(options?.runtimesDir);
73
75
  const runtimeStateInspection = (0, runtime_state_repo_1.inspectCopilotBridgeState)(options?.runtimeDir);
74
76
  const runtimeState = runtimeStateInspection.state;
@@ -108,6 +110,9 @@ async function getStatus(codexDir, configPath, providersPath, authPath, options)
108
110
  // Surface unmanaged live state without mutating anything during a read-only status call.
109
111
  warnings.push("Current config profile is not mapped in providers.json. Backfill would be required before treating live state as managed.");
110
112
  }
113
+ if (liveState.reason === "shared-profile") {
114
+ warnings.push(`Current config profile "${currentProfile}" is shared by multiple providers in providers.json, so the active provider cannot be resolved uniquely.`);
115
+ }
111
116
  if (runtimeStateInspection.exists && !runtimeStateInspection.valid) {
112
117
  warnings.push(`Copilot bridge runtime state is unreadable: ${runtimeStateInspection.parseError ?? "unknown parse failure"}`);
113
118
  }
@@ -128,7 +133,7 @@ async function getStatus(codexDir, configPath, providersPath, authPath, options)
128
133
  currentProfile,
129
134
  currentProfileMapped: liveState.profileMapped,
130
135
  provider: liveState.mappedProvider,
131
- activeProviderResolvable: activeProviderCandidates.length === 1,
136
+ activeProviderResolvable: liveState.providerResolvable,
132
137
  activeProviderCandidates,
133
138
  runtimeProvider: activeProvider && (0, providers_1.isCopilotBridgeProvider)(activeProvider) ? activeProvider.runtime?.kind ?? null : null,
134
139
  copilotSdk: {
@@ -1,16 +1,59 @@
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
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.listProviders = listProviders;
37
+ const fs = __importStar(require("node:fs"));
38
+ const providers_1 = require("../domain/providers");
39
+ const runtime_state_1 = require("../domain/runtime-state");
40
+ const config_repo_1 = require("../storage/config-repo");
4
41
  const providers_repo_1 = require("../storage/providers-repo");
5
42
  /**
6
43
  * Returns the sorted list of configured providers for display.
7
44
  */
8
- function listProviders(providersPath) {
45
+ function listProviders(providersPath, configPath) {
9
46
  const providers = (0, providers_repo_1.readProvidersFile)(providersPath);
10
47
  const names = Object.keys(providers.providers).sort();
48
+ const currentProfile = configPath && fs.existsSync(configPath)
49
+ ? (0, config_repo_1.readStructuredConfig)(configPath).activeProfile
50
+ : null;
51
+ const liveState = (0, runtime_state_1.inspectLiveStateDrift)(currentProfile, providers);
11
52
  const items = names.map((name) => ({
12
53
  name,
13
54
  profile: providers.providers[name].profile,
55
+ providerType: (0, providers_1.isCopilotBridgeProvider)(providers.providers[name]) ? "copilot" : "direct",
56
+ isActive: liveState.providerResolvable && liveState.mappedProvider === name,
14
57
  note: providers.providers[name].note ?? null,
15
58
  tags: providers.providers[name].tags ?? [],
16
59
  }));
@@ -18,6 +61,10 @@ function listProviders(providersPath) {
18
61
  data: {
19
62
  providers: items,
20
63
  count: items.length,
64
+ currentProfile,
65
+ activeProvider: liveState.mappedProvider,
66
+ activeProviderResolvable: liveState.providerResolvable,
67
+ activeProviderCandidates: liveState.mappedProviders,
21
68
  },
22
69
  };
23
70
  }
@@ -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":