@minniexcode/codex-switch 0.0.12 → 0.1.1

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 (36) hide show
  1. package/README.AI.md +37 -6
  2. package/README.CN.md +45 -11
  3. package/README.md +45 -13
  4. package/dist/app/add-provider.js +22 -24
  5. package/dist/app/edit-provider.js +34 -55
  6. package/dist/app/get-current-profile.js +15 -3
  7. package/dist/app/get-status.js +11 -8
  8. package/dist/app/list-config-profiles.js +3 -1
  9. package/dist/app/list-providers.js +10 -4
  10. package/dist/app/remove-provider.js +52 -19
  11. package/dist/app/run-doctor.js +29 -28
  12. package/dist/app/setup-codex.js +3 -3
  13. package/dist/app/show-config.js +3 -1
  14. package/dist/app/switch-provider.js +36 -5
  15. package/dist/cli/output.js +36 -18
  16. package/dist/commands/handlers.js +2 -2
  17. package/dist/commands/help.js +3 -3
  18. package/dist/commands/registry.js +35 -30
  19. package/dist/domain/config.js +250 -185
  20. package/dist/domain/providers.js +23 -0
  21. package/dist/domain/runtime-state.js +15 -15
  22. package/dist/domain/setup.js +3 -1
  23. package/dist/interaction/interactive.js +2 -2
  24. package/dist/runtime/codex-version.js +7 -0
  25. package/dist/storage/config-repo.js +6 -14
  26. package/docs/Design/codex-switch-v0.1.0-design.md +152 -0
  27. package/docs/Design/codex-switch-v0.1.1-design.md +33 -0
  28. package/docs/PRD/codex-switch-prd-v0.1.0.md +217 -205
  29. package/docs/Reference/codex-config-reference.md +41 -0
  30. package/docs/Reference/codex-config-reference.zh-CN.md +41 -0
  31. package/docs/Tests/testing.md +31 -78
  32. package/docs/cli-usage.md +86 -27
  33. package/docs/codex-switch-command-design.md +649 -649
  34. package/docs/codex-switch-product-overview.md +81 -80
  35. package/docs/codex-switch-technical-architecture.md +1115 -1115
  36. package/package.json +51 -51
@@ -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
  */
@@ -33,6 +34,9 @@ function validateProvidersShape(input) {
33
34
  if (typeof provider.apiKey !== "string" || provider.apiKey.trim() === "") {
34
35
  throw new Error(`Provider "${name}" is missing a valid apiKey.`);
35
36
  }
37
+ if (provider.model !== undefined && typeof provider.model !== "string") {
38
+ throw new Error(`Provider "${name}" has an invalid model.`);
39
+ }
36
40
  if (provider.baseUrl !== undefined && typeof provider.baseUrl !== "string") {
37
41
  throw new Error(`Provider "${name}" has an invalid baseUrl.`);
38
42
  }
@@ -54,6 +58,7 @@ function validateProvidersShape(input) {
54
58
  providers[name] = cleanProviderRecord({
55
59
  profile: provider.profile,
56
60
  apiKey: provider.apiKey,
61
+ model: provider.model,
57
62
  baseUrl: provider.baseUrl,
58
63
  note: provider.note,
59
64
  tags: provider.tags,
@@ -70,6 +75,9 @@ function cleanProviderRecord(record) {
70
75
  profile: record.profile.trim(),
71
76
  apiKey: record.apiKey.trim(),
72
77
  };
78
+ if (record.model && record.model.trim() !== "") {
79
+ next.model = record.model.trim();
80
+ }
73
81
  if (record.baseUrl && record.baseUrl.trim() !== "") {
74
82
  next.baseUrl = record.baseUrl.trim();
75
83
  }
@@ -162,6 +170,21 @@ function buildCopilotModelProviderProjection(runtime) {
162
170
  wireApi: "responses",
163
171
  };
164
172
  }
173
+ /**
174
+ * Builds the Codex-facing custom model_provider projection for a direct provider.
175
+ */
176
+ function buildDirectModelProviderProjection(profile, baseUrl) {
177
+ const normalizedBaseUrl = baseUrl.trim();
178
+ if (!normalizedBaseUrl) {
179
+ throw new Error(`Direct model provider "${profile}" requires a non-empty base_url.`);
180
+ }
181
+ return {
182
+ baseUrl: normalizedBaseUrl,
183
+ name: profile.trim(),
184
+ requiresOpenAiAuth: true,
185
+ wireApi: "responses",
186
+ };
187
+ }
165
188
  /**
166
189
  * Validates one runtime-backed provider block.
167
190
  */
@@ -90,26 +90,26 @@ function getStorageRoles(args) {
90
90
  };
91
91
  }
92
92
  /**
93
- * Compares the live active profile against managed providers to detect drift.
93
+ * Compares the live active model_provider against managed providers to detect drift.
94
94
  */
95
- function inspectLiveStateDrift(currentProfile, providers) {
96
- if (currentProfile === null) {
95
+ function inspectLiveStateDrift(currentModelProvider, providers) {
96
+ if (currentModelProvider === null) {
97
97
  return {
98
- currentProfile,
98
+ currentModelProvider,
99
99
  mappedProvider: null,
100
100
  mappedProviders: [],
101
- profileMapped: false,
101
+ modelProviderMapped: false,
102
102
  providerResolvable: false,
103
103
  canBackfillActiveProvider: false,
104
- reason: providers ? "profile-missing" : "config-missing",
104
+ reason: providers ? "model-provider-missing" : "config-missing",
105
105
  };
106
106
  }
107
107
  if (!providers) {
108
108
  return {
109
- currentProfile,
109
+ currentModelProvider,
110
110
  mappedProvider: null,
111
111
  mappedProviders: [],
112
- profileMapped: false,
112
+ modelProviderMapped: false,
113
113
  providerResolvable: false,
114
114
  canBackfillActiveProvider: false,
115
115
  reason: "providers-missing",
@@ -117,16 +117,16 @@ function inspectLiveStateDrift(currentProfile, providers) {
117
117
  }
118
118
  const mappedProviders = [];
119
119
  for (const [name, provider] of Object.entries(providers.providers)) {
120
- if (provider.profile === currentProfile) {
120
+ if (provider.profile === currentModelProvider) {
121
121
  mappedProviders.push(name);
122
122
  }
123
123
  }
124
124
  if (mappedProviders.length === 1) {
125
125
  return {
126
- currentProfile,
126
+ currentModelProvider,
127
127
  mappedProvider: mappedProviders[0],
128
128
  mappedProviders,
129
- profileMapped: true,
129
+ modelProviderMapped: true,
130
130
  providerResolvable: true,
131
131
  canBackfillActiveProvider: false,
132
132
  reason: "ok",
@@ -134,20 +134,20 @@ function inspectLiveStateDrift(currentProfile, providers) {
134
134
  }
135
135
  if (mappedProviders.length > 1) {
136
136
  return {
137
- currentProfile,
137
+ currentModelProvider,
138
138
  mappedProvider: null,
139
139
  mappedProviders,
140
- profileMapped: true,
140
+ modelProviderMapped: true,
141
141
  providerResolvable: false,
142
142
  canBackfillActiveProvider: false,
143
143
  reason: "shared-profile",
144
144
  };
145
145
  }
146
146
  return {
147
- currentProfile,
147
+ currentModelProvider,
148
148
  mappedProvider: null,
149
149
  mappedProviders: [],
150
- profileMapped: false,
150
+ modelProviderMapped: false,
151
151
  providerResolvable: false,
152
152
  canBackfillActiveProvider: true,
153
153
  reason: "provider-unmapped",
@@ -16,8 +16,9 @@ function buildSetupDrafts(profiles, detailsByProfile, runtimeByProfile) {
16
16
  return {
17
17
  providerName,
18
18
  record: (0, providers_1.cleanProviderRecord)({
19
- profile,
19
+ profile: runtime?.modelProvider ?? profile,
20
20
  apiKey: detail.apiKey ?? "",
21
+ model: runtime?.model,
21
22
  baseUrl: detail.baseUrl ?? runtime?.baseUrl,
22
23
  note: detail.note,
23
24
  tags: detail.tags,
@@ -71,6 +72,7 @@ function collectMigrateAdoptability(document, providers) {
71
72
  adoptableProfileDetails.push({
72
73
  name: view.name,
73
74
  model: view.model,
75
+ modelProvider: view.modelProvider,
74
76
  baseUrl: view.baseUrl,
75
77
  });
76
78
  continue;
@@ -70,8 +70,8 @@ function canPrompt(runtime, jsonMode) {
70
70
  */
71
71
  async function promptForProviderSelection(runtime, providersPath, configPath, message) {
72
72
  const providers = (0, providers_repo_1.readProvidersFile)(providersPath);
73
- const currentProfile = fs.existsSync(configPath) ? (0, config_repo_1.readStructuredConfig)(configPath).activeProfile : null;
74
- const liveState = (0, runtime_state_1.inspectLiveStateDrift)(currentProfile, providers);
73
+ const currentModelProvider = fs.existsSync(configPath) ? (0, config_repo_1.readStructuredConfig)(configPath).currentModelProvider : null;
74
+ const liveState = (0, runtime_state_1.inspectLiveStateDrift)(currentModelProvider, providers);
75
75
  const choices = Object.entries(providers.providers)
76
76
  .sort(([left], [right]) => left.localeCompare(right))
77
77
  .map(([providerName, provider]) => {
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MIN_SUPPORTED_CODEX_VERSION = void 0;
4
+ /**
5
+ * Minimum Codex CLI version supported by the managed runtime workflow.
6
+ */
7
+ exports.MIN_SUPPORTED_CODEX_VERSION = "0.134.0";
@@ -73,12 +73,12 @@ function readStructuredConfig(configPath) {
73
73
  }
74
74
  }
75
75
  /**
76
- * Reads the active top-level profile from config.toml.
76
+ * Reads the active top-level model_provider route from config.toml.
77
77
  */
78
78
  function readCurrentProfile(configPath) {
79
- const profile = readStructuredConfig(configPath).activeProfile ?? (0, config_1.parseTopLevelProfile)(readConfigFile(configPath));
79
+ const profile = readStructuredConfig(configPath).currentModelProvider;
80
80
  if (!profile) {
81
- throw (0, errors_1.cliError)("PROFILE_NOT_FOUND", "No top-level profile is set in config.toml.", {
81
+ throw (0, errors_1.cliError)("PROFILE_NOT_FOUND", "No top-level model_provider is set in config.toml.", {
82
82
  file: configPath,
83
83
  });
84
84
  }
@@ -91,18 +91,10 @@ function listConfigProfiles(configPath) {
91
91
  return new Set(readStructuredConfig(configPath).profiles.map((profile) => profile.name));
92
92
  }
93
93
  /**
94
- * Verifies that a provider's target profile exists before a switch operation proceeds.
94
+ * Loads config.toml for commands that project one model_provider route.
95
95
  */
96
96
  function ensureProfileExists(configPath, profile, provider) {
97
- const document = readStructuredConfig(configPath);
98
- if (!document.profiles.some((entry) => entry.name === profile)) {
99
- throw (0, errors_1.cliError)("PROFILE_NOT_FOUND", `Profile "${profile}" does not exist in config.toml.`, {
100
- file: configPath,
101
- provider,
102
- profile,
103
- });
104
- }
105
- return document;
97
+ return readStructuredConfig(configPath);
106
98
  }
107
99
  /**
108
100
  * Resolves one profile view and enforces the managed model_provider contract.
@@ -166,7 +158,7 @@ function requireModelProviderRuntimeSection(document, profile) {
166
158
  */
167
159
  function updateTopLevelProfile(configPath, configContent, profile) {
168
160
  (0, fs_utils_1.writeTextFileAtomic)(configPath, (0, config_1.applyPatchOperations)(configContent, (0, config_1.planConfigMutation)((0, config_1.parseStructuredConfig)(configContent), {
169
- setActiveProfile: profile,
161
+ setLegacyProfile: profile,
170
162
  }).operations));
171
163
  }
172
164
  /**
@@ -0,0 +1,152 @@
1
+ # codex-switch `0.1.0` Design
2
+
3
+ ## 文档信息
4
+
5
+ - 文档类型:实现约束设计文档
6
+ - 适用版本:`0.1.0`
7
+ - 当前定位:release-hardening only
8
+ - 关联 PRD:[`../PRD/codex-switch-prd-v0.1.0.md`](../PRD/codex-switch-prd-v0.1.0.md)
9
+ - 关联 beta 设计:[`./codex-switch-v0.0.12-design.md`](./codex-switch-v0.0.12-design.md)
10
+
11
+ ## 1. 设计原则
12
+
13
+ `0.1.0` 的实现只做 release-hardening,不新增命令面,不改 JSON envelope,不引入兼容层,也不把历史草案继续扩成平台设想。
14
+
15
+ 这个版本的实现目标是收口,不是扩张:
16
+
17
+ - 把用户看见的主路径讲清楚。
18
+ - 把 `list`、`status`、`doctor` 的可读语义讲清楚。
19
+ - 把 `migrate` 和 `setup` 的产品定位讲清楚。
20
+ - 把文档、help、输出、测试和包内容收口成同一套事实。
21
+
22
+ ## 2. 当前阻塞项
23
+
24
+ 实现收口前必须先正视以下阻塞项:
25
+
26
+ 1. `tests/` 仍被忽略,导致回归测试无法稳定版本化。
27
+ 2. README 仍引用不存在的 `docs/Tests/testing.md`,说明文档入口还未收口。
28
+ 3. 版本叙事仍以 `0.0.12` 为中心,`0.1.0` 还没有被写成稳定发布线。
29
+ 4. 主工作流、`migrate` 定位、`setup` 定位和真实实现状态还没有完全对齐。
30
+
31
+ 只要这些阻塞项还存在,就不应把当前实现视为 `0.1.0` ready。
32
+
33
+ ## 3. 收口矩阵
34
+
35
+ 以下子系统必须完成对应收口。
36
+
37
+ | 子系统 | 必须稳定的内容 | 约束 |
38
+ | --- | --- | --- |
39
+ | 文档 | PRD、design、README、CLI usage、product overview、changelog、testing guide | 所有面向用户的文档必须与 `0.1.0` 事实一致 |
40
+ | 帮助 | 顶层 help、命令 help、示例顺序 | direct/Copilot 主路径优先,`migrate` 降级,`setup` 仅保留 deprecated 语义 |
41
+ | 输出 | `init`、`list`、`status`、`doctor`、`login` 的 human-readable 文案 | 只收口语义,不改 JSON envelope |
42
+ | 读路径 | tool home / runtime separation、dual-path model、ambiguous active profile 处理 | 不新增兼容层,不伪造 current 状态 |
43
+ | 测试 | release gate、回归测试、fixture 检查 | 回归测试必须落仓库,不能继续停留在忽略状态 |
44
+
45
+ ## 4. 必须稳定的用户可见语义
46
+
47
+ ### 4.1 `list`
48
+
49
+ `list` 必须能让用户直接看出:
50
+
51
+ - provider 属于 `direct` 还是 `copilot`
52
+ - 哪个 provider 是 current
53
+ - 当前 active profile 是否可唯一解析
54
+
55
+ 如果当前 active profile 对应多个 provider,就必须显式表现为 ambiguous,而不是把任何一个 provider 假装成 current。
56
+
57
+ `list --json` 仍然使用既有 envelope,只允许追加字段,不允许改顶层契约。
58
+
59
+ ### 4.2 `status`
60
+
61
+ `status` 必须把以下内容讲清楚:
62
+
63
+ - tool home 是什么
64
+ - target runtime 是什么
65
+ - 当前 active provider 是什么
66
+ - 当前路径是 direct 还是 Copilot
67
+ - 下一步应该做什么
68
+
69
+ `status` 是摘要,不是字段堆叠。输出顺序必须围绕“当前状态 -> 影响 -> 下一步”组织。
70
+
71
+ ### 4.3 `doctor`
72
+
73
+ `doctor` 必须先给整体健康结论,再列 issue,再给修复建议。
74
+
75
+ 每条 issue 至少要表达:
76
+
77
+ - 问题是什么
78
+ - 为什么重要
79
+ - 下一步怎么修
80
+
81
+ `doctor` 的目标不是罗列内部数据结构,而是把用户推到下一步修复动作。
82
+
83
+ ### 4.4 provider picker
84
+
85
+ list 和 provider picker 必须一致处理 ambiguous active profile。
86
+
87
+ 选择器提示至少要包含:
88
+
89
+ - `profile`
90
+ - `providerType`
91
+ - `current` 标记,仅在唯一解析时出现
92
+
93
+ ### 4.5 命令定位
94
+
95
+ `0.1.0` 还必须稳定以下产品定位:
96
+
97
+ - 稳定命令面以 `init`、`login`、`list`、`show`、`current`、`status`、`doctor`、`config`、`add`、`edit`、`switch`、`remove`、`import`、`export`、`bridge`、`backups`、`rollback` 为准。
98
+ - `migrate` 只能被表述为高级 adopt helper。
99
+ - `setup` 只能被表述为 deprecated entry。
100
+ - `--json` 顶层 envelope 继续固定为 `ok / command / data / warnings / error`。
101
+
102
+ ## 5. 文档同步要求
103
+
104
+ 以下面向用户的文档必须与 `0.1.0` 事实一致:
105
+
106
+ - `README.md`
107
+ - `README.CN.md`
108
+ - `README.AI.md`
109
+ - `docs/cli-usage.md`
110
+ - `docs/codex-switch-product-overview.md`
111
+ - `docs/PRD/codex-switch-prd-v0.1.0.md`
112
+ - `docs/Design/codex-switch-v0.1.0-design.md`
113
+ - `CHANGELOG.md`
114
+ - `docs/Tests/testing.md`
115
+
116
+ 历史大文档不需要在本版全文重写,但必须明确它们只是历史参考,不是当前 release contract。
117
+
118
+ ## 6. 最小测试计划
119
+
120
+ `0.1.0` 的最小测试计划必须包含以下内容:
121
+
122
+ 1. `npm run build`
123
+ 2. `npm test`
124
+ 3. `npx tsc --noEmit`
125
+ 4. `npm pack --dry-run`
126
+ 5. built CLI `--help`
127
+ 6. built CLI `--version`
128
+ 7. fresh direct provider flow
129
+ 8. fresh Copilot provider flow
130
+ 9. `list/status/doctor` 输出语义检查
131
+ 10. `migrate` 高级 adopt helper 检查
132
+ 11. `setup` deprecated entry 检查
133
+
134
+ 测试结论必须落在仓库中的正式测试内容里,不能继续依赖忽略目录或口头约定。
135
+
136
+ ## 7. 明确不做
137
+
138
+ 本版不做以下事情:
139
+
140
+ - 新 upstream
141
+ - GUI / TUI
142
+ - daemon
143
+ - plugin system
144
+ - auto migration
145
+ - 兼容层
146
+ - dual-read / dual-write
147
+ - 重新设计公开 JSON envelope
148
+ - 重新扩张命令面
149
+
150
+ ## 8. 结论
151
+
152
+ `0.1.0` 设计的核心不是“再造一个版本叙事”,而是把已存在的实现收口成稳定合同。实现只要偏离这条线,就不应被视为 `0.1.0` 的合理内容。
@@ -0,0 +1,33 @@
1
+ # codex-switch v0.1.1 Design
2
+
3
+ ## Scope
4
+
5
+ - Support Codex `0.134.0+` only.
6
+ - Treat top-level `model` and `model_provider` as the runtime routing source of truth.
7
+ - Treat legacy top-level `profile` and legacy `[profiles.*]` sections as adopt-only and diagnostic inputs.
8
+ - Project provider auth through `auth.json` with `OPENAI_API_KEY`.
9
+ - Do not write `env_key` or `env_key_instructions` into managed `[model_providers.<id>]` sections.
10
+
11
+ ## Command Contract
12
+
13
+ - `--profile <name>` remains a CLI alias for the stored `model_provider` id.
14
+ - `--model <name>` stores the provider default switch model in `providers.json`.
15
+ - `switch` writes top-level `model` and `model_provider`, repairs `[model_providers.<id>]`, rewrites `auth.json`, and removes the targeted legacy route selector/profile section.
16
+ - `add` and `edit` repair `[model_providers.<id>]` and scrub `env_key` / `env_key_instructions`.
17
+ - `remove --switch-to <provider-name>` switches by managed provider name, not by profile id.
18
+
19
+ ## Persistence
20
+
21
+ - `providers.json` keeps `profile` as the persisted `model_provider` id alias.
22
+ - `providers.json` adds `model` for default route projection.
23
+ - Managed `[model_providers.<id>]` sections write:
24
+ - `base_url`
25
+ - `name`
26
+ - `requires_openai_auth = true`
27
+ - `wire_api = "responses"`
28
+
29
+ ## Legacy Handling
30
+
31
+ - `config show` and `config list-profiles` expose legacy profile inspection views.
32
+ - `migrate` remains a legacy adoption helper and does not modify the active top-level route.
33
+ - `doctor` flags missing top-level route fields, legacy selectors/sections, and legacy `env_key` wiring in the active model provider section.