@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
@@ -47,22 +47,22 @@ exports.applyPatchOperations = applyPatchOperations;
47
47
  const os = __importStar(require("node:os"));
48
48
  const errors_1 = require("./errors");
49
49
  /**
50
- * Reads the active top-level profile from config.toml content.
50
+ * Reads the legacy top-level profile selector from config.toml content.
51
51
  */
52
52
  function parseTopLevelProfile(configContent) {
53
- return parseStructuredConfig(configContent).activeProfile;
53
+ return parseStructuredConfig(configContent).legacyProfile;
54
54
  }
55
55
  /**
56
- * Collects all named profile sections declared in config.toml content.
56
+ * Collects all named legacy profile sections declared in config.toml content.
57
57
  */
58
58
  function parseProfileNames(configContent) {
59
59
  return new Set(parseStructuredConfig(configContent).profiles.map((profile) => profile.name));
60
60
  }
61
61
  /**
62
- * Replaces or inserts the top-level profile assignment while preserving the rest of the file.
62
+ * Replaces or inserts the legacy top-level profile assignment while preserving the rest of the file.
63
63
  */
64
64
  function replaceTopLevelProfile(configContent, profile) {
65
- const plan = planConfigMutation(parseStructuredConfig(configContent), { setActiveProfile: profile });
65
+ const plan = planConfigMutation(parseStructuredConfig(configContent), { setLegacyProfile: profile });
66
66
  return applyPatchOperations(configContent, plan.operations);
67
67
  }
68
68
  /**
@@ -71,12 +71,19 @@ function replaceTopLevelProfile(configContent, profile) {
71
71
  function parseStructuredConfig(configContent) {
72
72
  const lineEnding = configContent.includes("\r\n") ? "\r\n" : "\n";
73
73
  const lines = splitWithOffsets(configContent);
74
- let activeProfile = null;
75
- let activeProfileRange = null;
74
+ let currentModel = null;
75
+ let currentModelRange = null;
76
+ let currentModelLineRange = null;
77
+ let currentModelProvider = null;
78
+ let currentModelProviderRange = null;
79
+ let currentModelProviderLineRange = null;
80
+ let legacyProfile = null;
81
+ let legacyProfileRange = null;
82
+ let legacyProfileLineRange = null;
76
83
  const profiles = [];
77
84
  const modelProviders = [];
78
85
  let currentProfile = null;
79
- let currentModelProvider = null;
86
+ let currentModelProviderSection = null;
80
87
  let inRoot = true;
81
88
  for (const line of lines) {
82
89
  const trimmed = line.content.trim();
@@ -85,9 +92,9 @@ function parseStructuredConfig(configContent) {
85
92
  if (currentProfile) {
86
93
  currentProfile.sectionEnd = line.start;
87
94
  }
88
- if (currentModelProvider) {
89
- currentModelProvider.sectionEnd = line.start;
90
- currentModelProvider = null;
95
+ if (currentModelProviderSection) {
96
+ currentModelProviderSection.sectionEnd = line.start;
97
+ currentModelProviderSection = null;
91
98
  }
92
99
  currentProfile = {
93
100
  name: headerMatch[1],
@@ -110,10 +117,10 @@ function parseStructuredConfig(configContent) {
110
117
  currentProfile.sectionEnd = line.start;
111
118
  currentProfile = null;
112
119
  }
113
- if (currentModelProvider) {
114
- currentModelProvider.sectionEnd = line.start;
120
+ if (currentModelProviderSection) {
121
+ currentModelProviderSection.sectionEnd = line.start;
115
122
  }
116
- currentModelProvider = {
123
+ currentModelProviderSection = {
117
124
  name: modelProviderHeaderMatch[1],
118
125
  sectionStart: line.start,
119
126
  sectionEnd: configContent.length,
@@ -126,8 +133,14 @@ function parseStructuredConfig(configContent) {
126
133
  requiresOpenAiAuth: null,
127
134
  wireApiValueRange: null,
128
135
  wireApi: null,
136
+ envKeyValueRange: null,
137
+ envKey: null,
138
+ envKeyInstructionsValueRange: null,
139
+ envKeyInstructions: null,
140
+ envKeyLineRange: null,
141
+ envKeyInstructionsLineRange: null,
129
142
  };
130
- modelProviders.push(currentModelProvider);
143
+ modelProviders.push(currentModelProviderSection);
131
144
  inRoot = false;
132
145
  continue;
133
146
  }
@@ -136,81 +149,92 @@ function parseStructuredConfig(configContent) {
136
149
  currentProfile.sectionEnd = line.start;
137
150
  currentProfile = null;
138
151
  }
139
- if (currentModelProvider) {
140
- currentModelProvider.sectionEnd = line.start;
141
- currentModelProvider = null;
152
+ if (currentModelProviderSection) {
153
+ currentModelProviderSection.sectionEnd = line.start;
154
+ currentModelProviderSection = null;
142
155
  }
143
156
  inRoot = false;
144
157
  continue;
145
158
  }
146
159
  if (inRoot) {
160
+ const modelMatch = matchKeyValueLine(line.content, "model");
161
+ if (modelMatch && !currentModel) {
162
+ currentModel = modelMatch.value;
163
+ currentModelRange = toAbsoluteRange(line.start, modelMatch.valueStart, modelMatch.valueEnd);
164
+ currentModelLineRange = { start: line.start, end: line.end };
165
+ }
166
+ const modelProviderMatch = matchKeyValueLine(line.content, "model_provider");
167
+ if (modelProviderMatch && !currentModelProvider) {
168
+ currentModelProvider = modelProviderMatch.value;
169
+ currentModelProviderRange = toAbsoluteRange(line.start, modelProviderMatch.valueStart, modelProviderMatch.valueEnd);
170
+ currentModelProviderLineRange = { start: line.start, end: line.end };
171
+ }
147
172
  const profileMatch = matchKeyValueLine(line.content, "profile");
148
- if (profileMatch && !activeProfile) {
149
- activeProfile = profileMatch.value;
150
- activeProfileRange = {
151
- start: line.start + profileMatch.valueStart,
152
- end: line.start + profileMatch.valueEnd,
153
- };
173
+ if (profileMatch && !legacyProfile) {
174
+ legacyProfile = profileMatch.value;
175
+ legacyProfileRange = toAbsoluteRange(line.start, profileMatch.valueStart, profileMatch.valueEnd);
176
+ legacyProfileLineRange = { start: line.start, end: line.end };
154
177
  }
155
178
  }
156
179
  if (currentProfile) {
157
180
  const modelMatch = matchKeyValueLine(line.content, "model");
158
181
  if (modelMatch) {
159
182
  currentProfile.model = modelMatch.value;
160
- currentProfile.modelValueRange = {
161
- start: line.start + modelMatch.valueStart,
162
- end: line.start + modelMatch.valueEnd,
163
- };
183
+ currentProfile.modelValueRange = toAbsoluteRange(line.start, modelMatch.valueStart, modelMatch.valueEnd);
164
184
  }
165
185
  const modelProviderMatch = matchKeyValueLine(line.content, "model_provider");
166
186
  if (modelProviderMatch) {
167
187
  currentProfile.modelProvider = modelProviderMatch.value;
168
- currentProfile.modelProviderValueRange = {
169
- start: line.start + modelProviderMatch.valueStart,
170
- end: line.start + modelProviderMatch.valueEnd,
171
- };
188
+ currentProfile.modelProviderValueRange = toAbsoluteRange(line.start, modelProviderMatch.valueStart, modelProviderMatch.valueEnd);
172
189
  }
173
190
  }
174
- if (currentModelProvider) {
191
+ if (currentModelProviderSection) {
175
192
  const baseUrlMatch = matchKeyValueLine(line.content, "base_url");
176
193
  if (baseUrlMatch) {
177
- currentModelProvider.baseUrl = baseUrlMatch.value;
178
- currentModelProvider.baseUrlValueRange = {
179
- start: line.start + baseUrlMatch.valueStart,
180
- end: line.start + baseUrlMatch.valueEnd,
181
- };
194
+ currentModelProviderSection.baseUrl = baseUrlMatch.value;
195
+ currentModelProviderSection.baseUrlValueRange = toAbsoluteRange(line.start, baseUrlMatch.valueStart, baseUrlMatch.valueEnd);
182
196
  }
183
197
  const nameMatch = matchKeyValueLine(line.content, "name");
184
198
  if (nameMatch) {
185
- currentModelProvider.providerName = nameMatch.value;
186
- currentModelProvider.nameValueRange = {
187
- start: line.start + nameMatch.valueStart,
188
- end: line.start + nameMatch.valueEnd,
189
- };
199
+ currentModelProviderSection.providerName = nameMatch.value;
200
+ currentModelProviderSection.nameValueRange = toAbsoluteRange(line.start, nameMatch.valueStart, nameMatch.valueEnd);
190
201
  }
191
202
  const requiresOpenAiAuthMatch = matchBooleanKeyValueLine(line.content, "requires_openai_auth");
192
203
  if (requiresOpenAiAuthMatch) {
193
- currentModelProvider.requiresOpenAiAuth = requiresOpenAiAuthMatch.value;
194
- currentModelProvider.requiresOpenAiAuthValueRange = {
195
- start: line.start + requiresOpenAiAuthMatch.valueStart,
196
- end: line.start + requiresOpenAiAuthMatch.valueEnd,
197
- };
204
+ currentModelProviderSection.requiresOpenAiAuth = requiresOpenAiAuthMatch.value;
205
+ currentModelProviderSection.requiresOpenAiAuthValueRange = toAbsoluteRange(line.start, requiresOpenAiAuthMatch.valueStart, requiresOpenAiAuthMatch.valueEnd);
198
206
  }
199
207
  const wireApiMatch = matchKeyValueLine(line.content, "wire_api");
200
208
  if (wireApiMatch) {
201
- currentModelProvider.wireApi = wireApiMatch.value;
202
- currentModelProvider.wireApiValueRange = {
203
- start: line.start + wireApiMatch.valueStart,
204
- end: line.start + wireApiMatch.valueEnd,
205
- };
209
+ currentModelProviderSection.wireApi = wireApiMatch.value;
210
+ currentModelProviderSection.wireApiValueRange = toAbsoluteRange(line.start, wireApiMatch.valueStart, wireApiMatch.valueEnd);
211
+ }
212
+ const envKeyMatch = matchKeyValueLine(line.content, "env_key");
213
+ if (envKeyMatch) {
214
+ currentModelProviderSection.envKey = envKeyMatch.value;
215
+ currentModelProviderSection.envKeyValueRange = toAbsoluteRange(line.start, envKeyMatch.valueStart, envKeyMatch.valueEnd);
216
+ currentModelProviderSection.envKeyLineRange = { start: line.start, end: line.end };
217
+ }
218
+ const envKeyInstructionsMatch = matchKeyValueLine(line.content, "env_key_instructions");
219
+ if (envKeyInstructionsMatch) {
220
+ currentModelProviderSection.envKeyInstructions = envKeyInstructionsMatch.value;
221
+ currentModelProviderSection.envKeyInstructionsValueRange = toAbsoluteRange(line.start, envKeyInstructionsMatch.valueStart, envKeyInstructionsMatch.valueEnd);
222
+ currentModelProviderSection.envKeyInstructionsLineRange = { start: line.start, end: line.end };
206
223
  }
207
224
  }
208
225
  }
209
226
  return {
210
227
  rawText: configContent,
211
228
  lineEnding,
212
- activeProfile,
213
- activeProfileRange,
229
+ currentModel,
230
+ currentModelRange,
231
+ currentModelLineRange,
232
+ currentModelProvider,
233
+ currentModelProviderRange,
234
+ currentModelProviderLineRange,
235
+ legacyProfile,
236
+ legacyProfileRange,
237
+ legacyProfileLineRange,
214
238
  profiles: profiles.map((profile) => ({
215
239
  ...profile,
216
240
  managedFieldInsertIndex: findManagedFieldInsertIndex(configContent, profile.sectionStart, profile.sectionEnd),
@@ -222,7 +246,7 @@ function parseStructuredConfig(configContent) {
222
246
  };
223
247
  }
224
248
  /**
225
- * Builds the managed/unmanaged/orphaned profile views used by config commands and diagnostics.
249
+ * Builds the legacy profile inspection views used by config commands and diagnostics.
226
250
  */
227
251
  function buildManagedProfileViews(document, providers) {
228
252
  const linkMap = buildProfileLinkMap(providers);
@@ -236,7 +260,7 @@ function buildManagedProfileViews(document, providers) {
236
260
  views.push({
237
261
  name: section.name,
238
262
  managed: linkInfo.managed,
239
- isActive: document.activeProfile === section.name,
263
+ isActive: document.currentModelProvider === section.name,
240
264
  linkedProviders: [...linkInfo.linkedProviders].sort(),
241
265
  model: section.model,
242
266
  modelProvider: section.modelProvider,
@@ -252,7 +276,7 @@ function buildManagedProfileViews(document, providers) {
252
276
  views.push({
253
277
  name: profile,
254
278
  managed: true,
255
- isActive: document.activeProfile === profile,
279
+ isActive: document.currentModelProvider === profile,
256
280
  linkedProviders: [...linkInfo.linkedProviders].sort(),
257
281
  model: null,
258
282
  modelProvider: null,
@@ -268,85 +292,61 @@ function buildManagedProfileViews(document, providers) {
268
292
  */
269
293
  function collectConfigConsistencyIssues(document, providers) {
270
294
  const issues = [];
271
- for (const view of buildManagedProfileViews(document, providers)) {
272
- if (view.source === "orphaned-reference") {
273
- issues.push({
274
- code: "ORPHANED_PROFILE_REFERENCE",
275
- profile: view.name,
276
- providers: [...view.linkedProviders],
277
- });
278
- }
279
- if (view.source === "unmanaged" && view.linkedProviders.length === 0) {
280
- issues.push({
281
- code: "ORPHANED_PROFILE_SECTION",
282
- profile: view.name,
283
- });
284
- }
285
- if (view.linkedProviders.length > 1) {
286
- issues.push({
287
- code: "SHARED_PROFILE_REFERENCE",
288
- profile: view.name,
289
- providers: [...view.linkedProviders],
290
- });
291
- }
292
- if (view.source !== "orphaned-reference") {
293
- if (!view.modelProvider) {
295
+ const providerMap = providers?.providers ?? null;
296
+ const activeModelProvider = document.currentModelProvider;
297
+ const activeProviderSection = activeModelProvider
298
+ ? document.modelProviders.find((entry) => entry.name === activeModelProvider) ?? null
299
+ : null;
300
+ if (!document.currentModel) {
301
+ issues.push({ code: "MODEL_MISSING", modelProvider: activeModelProvider ?? "(none)" });
302
+ }
303
+ if (!document.currentModelProvider) {
304
+ issues.push({ code: "MODEL_PROVIDER_MISSING" });
305
+ }
306
+ if (document.legacyProfile) {
307
+ issues.push({ code: "LEGACY_PROFILE_SELECTOR", profile: document.legacyProfile });
308
+ }
309
+ for (const profile of document.profiles) {
310
+ issues.push({ code: "LEGACY_PROFILE_SECTION", profile: profile.name });
311
+ }
312
+ if (activeModelProvider && !activeProviderSection) {
313
+ issues.push({ code: "MODEL_PROVIDER_SECTION_MISSING", modelProvider: activeModelProvider });
314
+ }
315
+ if (activeModelProvider && activeProviderSection && !activeProviderSection.baseUrl) {
316
+ issues.push({ code: "MODEL_PROVIDER_BASE_URL_MISSING", modelProvider: activeModelProvider });
317
+ }
318
+ if (activeProviderSection?.envKey) {
319
+ issues.push({
320
+ code: "LEGACY_MODEL_PROVIDER_ENV_KEY",
321
+ modelProvider: activeProviderSection.name,
322
+ envKey: activeProviderSection.envKey,
323
+ });
324
+ }
325
+ if (activeModelProvider && providerMap) {
326
+ const linkedProviders = Object.entries(providerMap)
327
+ .filter(([, provider]) => provider.profile === activeModelProvider)
328
+ .sort(([left], [right]) => left.localeCompare(right));
329
+ if (linkedProviders.length === 1 && activeProviderSection?.baseUrl) {
330
+ const [providerName, provider] = linkedProviders[0];
331
+ if (!provider.runtime &&
332
+ typeof provider.baseUrl === "string" &&
333
+ provider.baseUrl.trim() !== "" &&
334
+ provider.baseUrl !== activeProviderSection.baseUrl) {
294
335
  issues.push({
295
- code: "MODEL_PROVIDER_MISSING",
296
- profile: view.name,
336
+ code: "PROVIDER_BASE_URL_MISMATCH",
337
+ modelProvider: activeModelProvider,
338
+ provider: providerName,
339
+ providerBaseUrl: provider.baseUrl,
340
+ configBaseUrl: activeProviderSection.baseUrl,
341
+ providerType: "direct",
297
342
  });
298
343
  }
299
- else {
300
- if (view.modelProvider !== view.name) {
301
- issues.push({
302
- code: "MODEL_PROVIDER_NAME_MISMATCH",
303
- profile: view.name,
304
- modelProvider: view.modelProvider,
305
- });
306
- }
307
- const modelProviderSection = document.modelProviders.find((entry) => entry.name === view.modelProvider);
308
- if (!modelProviderSection) {
309
- issues.push({
310
- code: "MODEL_PROVIDER_SECTION_MISSING",
311
- profile: view.name,
312
- modelProvider: view.modelProvider,
313
- });
314
- }
315
- else if (!modelProviderSection.baseUrl) {
316
- issues.push({
317
- code: "MODEL_PROVIDER_BASE_URL_MISSING",
318
- profile: view.name,
319
- modelProvider: view.modelProvider,
320
- });
321
- }
322
- }
323
344
  }
324
345
  }
325
- if (document.activeProfile) {
326
- const activeLinkInfo = buildProfileLinkMap(providers).get(document.activeProfile);
327
- if (!activeLinkInfo) {
328
- issues.push({
329
- code: "UNMANAGED_ACTIVE_PROFILE",
330
- profile: document.activeProfile,
331
- });
332
- }
333
- else if (activeLinkInfo.linkedProviders.length > 1) {
334
- issues.push({
335
- code: "ACTIVE_PROVIDER_UNRESOLVED",
336
- profile: document.activeProfile,
337
- providers: [...activeLinkInfo.linkedProviders],
338
- });
339
- }
340
- }
341
- return issues.sort((left, right) => {
342
- if (left.profile === right.profile) {
343
- return left.code.localeCompare(right.code);
344
- }
345
- return left.profile.localeCompare(right.profile);
346
- });
346
+ return issues.sort((left, right) => left.code.localeCompare(right.code));
347
347
  }
348
348
  /**
349
- * Ensures the minimal managed profile fields are available before a new section is created.
349
+ * Ensures the minimal managed profile fields are available before a new legacy section is created.
350
350
  */
351
351
  function validateManagedProfileCreation(profile, fields) {
352
352
  const model = fields.model?.trim() ?? "";
@@ -366,7 +366,7 @@ function validateManagedProfileCreation(profile, fields) {
366
366
  };
367
367
  }
368
368
  /**
369
- * Computes keep/delete/switch outcomes when a provider leaves or changes profiles.
369
+ * Computes keep/delete/switch outcomes when a provider leaves or changes model-provider bindings.
370
370
  */
371
371
  function planProfileLifecycleOutcome(args) {
372
372
  if (!args.oldProfile || args.oldProfile === args.newProfile) {
@@ -410,7 +410,7 @@ function planProfileLifecycleOutcome(args) {
410
410
  };
411
411
  }
412
412
  /**
413
- * Builds a text patch plan for top-level profile changes and profile section lifecycle changes.
413
+ * Builds a text patch plan for route fields, legacy selectors, and provider-section mutations.
414
414
  */
415
415
  function planConfigMutation(document, args) {
416
416
  const operations = [];
@@ -421,27 +421,19 @@ function planConfigMutation(document, args) {
421
421
  const updatedModelProviders = [];
422
422
  const sectionMap = new Map(document.profiles.map((profile) => [profile.name, profile]));
423
423
  const modelProviderSectionMap = new Map(document.modelProviders.map((entry) => [entry.name, entry]));
424
- if (args.setActiveProfile && args.setActiveProfile !== document.activeProfile) {
425
- const quoted = `"${args.setActiveProfile}"`;
426
- if (document.activeProfileRange) {
427
- operations.push({
428
- kind: "replace-range",
429
- start: document.activeProfileRange.start,
430
- end: document.activeProfileRange.end,
431
- text: quoted,
432
- });
433
- }
434
- else {
435
- const insertAt = findTopLevelInsertIndex(document.rawText);
436
- const text = `profile = ${quoted}${document.lineEnding}`;
437
- operations.push({
438
- kind: "insert-at",
439
- index: insertAt,
440
- text,
441
- });
442
- }
424
+ planRootFieldMutation(document, "model", document.currentModel, document.currentModelRange, document.currentModelLineRange, args.setCurrentModel, operations);
425
+ planRootFieldMutation(document, "model_provider", document.currentModelProvider, document.currentModelProviderRange, document.currentModelProviderLineRange, args.setCurrentModelProvider, operations);
426
+ if (args.setLegacyProfile !== undefined) {
427
+ planRootFieldMutation(document, "profile", document.legacyProfile, document.legacyProfileRange, document.legacyProfileLineRange, args.setLegacyProfile, operations);
428
+ }
429
+ if (args.deleteLegacyProfile && document.legacyProfileLineRange) {
430
+ operations.push({
431
+ kind: "delete-range",
432
+ start: document.legacyProfileLineRange.start,
433
+ end: expandLineDeletionStart(document.rawText, document.legacyProfileLineRange.start, document.legacyProfileLineRange.end),
434
+ });
443
435
  }
444
- for (const profileName of args.deleteProfiles ?? []) {
436
+ for (const profileName of [...(args.deleteProfiles ?? []), ...(args.deleteLegacyProfilesByName ?? [])]) {
445
437
  const section = sectionMap.get(profileName);
446
438
  if (!section) {
447
439
  continue;
@@ -478,39 +470,47 @@ function planConfigMutation(document, args) {
478
470
  for (const [profileName, fields] of Object.entries(args.upsertModelProviders ?? {}).sort(([left], [right]) => left.localeCompare(right))) {
479
471
  const section = modelProviderSectionMap.get(profileName);
480
472
  if (!section) {
481
- const baseUrl = fields.baseUrl?.trim() ?? "";
482
- const providerName = fields.name?.trim() ?? "";
483
- if (!baseUrl) {
484
- throw (0, errors_1.cliError)("MANAGED_PROFILE_FIELDS_MISSING", `Model provider "${profileName}" requires base_url.`, {
485
- profile: profileName,
486
- modelProvider: profileName,
487
- missingFields: [
488
- !baseUrl ? "base_url" : null,
489
- ].filter((value) => Boolean(value)),
490
- });
491
- }
473
+ const normalizedFields = normalizeManagedModelProviderFields(profileName, fields);
492
474
  const prefix = document.rawText.length > 0 && !document.rawText.endsWith(document.lineEnding)
493
475
  ? document.lineEnding
494
476
  : "";
495
- const requiresOpenAiAuth = fields.requiresOpenAiAuth;
496
- const wireApi = fields.wireApi?.trim() ?? "";
497
477
  operations.push({
498
478
  kind: "insert-at",
499
479
  index: document.rawText.length,
500
480
  text: `${prefix}[model_providers.${profileName}]${document.lineEnding}` +
501
- `base_url = ${JSON.stringify(baseUrl)}${document.lineEnding}` +
502
- (providerName ? `name = ${JSON.stringify(providerName)}${document.lineEnding}` : "") +
503
- (requiresOpenAiAuth !== undefined ? `requires_openai_auth = ${String(requiresOpenAiAuth)}${document.lineEnding}` : "") +
504
- (wireApi ? `wire_api = ${JSON.stringify(wireApi)}${document.lineEnding}` : ""),
481
+ `base_url = ${JSON.stringify(normalizedFields.baseUrl)}${document.lineEnding}` +
482
+ `name = ${JSON.stringify(normalizedFields.name)}${document.lineEnding}` +
483
+ `requires_openai_auth = ${String(normalizedFields.requiresOpenAiAuth)}${document.lineEnding}` +
484
+ `wire_api = ${JSON.stringify(normalizedFields.wireApi)}${document.lineEnding}`,
505
485
  });
506
486
  createdModelProviderSections.push(profileName);
507
487
  continue;
508
488
  }
509
- const sectionUpdated = planModelProviderFieldMutation(section, fields, operations);
489
+ const sectionUpdated = planModelProviderFieldMutation(document, section, normalizeManagedModelProviderFields(profileName, fields), operations);
510
490
  if (sectionUpdated) {
511
491
  updatedModelProviders.push(profileName);
512
492
  }
513
493
  }
494
+ for (const profileName of args.scrubModelProviderEnvKeys ?? []) {
495
+ const section = modelProviderSectionMap.get(profileName);
496
+ if (!section) {
497
+ continue;
498
+ }
499
+ if (section.envKeyLineRange) {
500
+ operations.push({
501
+ kind: "delete-range",
502
+ start: section.envKeyLineRange.start,
503
+ end: expandLineDeletionStart(document.rawText, section.envKeyLineRange.start, section.envKeyLineRange.end),
504
+ });
505
+ }
506
+ if (section.envKeyInstructionsLineRange) {
507
+ operations.push({
508
+ kind: "delete-range",
509
+ start: section.envKeyInstructionsLineRange.start,
510
+ end: expandLineDeletionStart(document.rawText, section.envKeyInstructionsLineRange.start, section.envKeyInstructionsLineRange.end),
511
+ });
512
+ }
513
+ }
514
514
  return {
515
515
  operations,
516
516
  createdProfileSections,
@@ -518,7 +518,8 @@ function planConfigMutation(document, args) {
518
518
  deletedProfileSections,
519
519
  updatedProfiles,
520
520
  updatedModelProviders,
521
- switchedActiveProfile: Boolean(args.setActiveProfile && args.setActiveProfile !== document.activeProfile),
521
+ switchedActiveProfile: Boolean(args.setCurrentModelProvider !== undefined && args.setCurrentModelProvider !== document.currentModelProvider) ||
522
+ Boolean(args.setLegacyProfile !== undefined && args.setLegacyProfile !== document.legacyProfile),
522
523
  };
523
524
  }
524
525
  /**
@@ -540,6 +541,9 @@ function applyPatchOperations(rawText, operations) {
540
541
  }
541
542
  return nextText;
542
543
  }
544
+ /**
545
+ * Plans managed field updates for one legacy profile section.
546
+ */
543
547
  function planSectionFieldMutation(document, section, fields, operations) {
544
548
  let updated = false;
545
549
  const modelText = fields.model !== undefined ? JSON.stringify(fields.model) : null;
@@ -587,14 +591,14 @@ function planSectionFieldMutation(document, section, fields, operations) {
587
591
  /**
588
592
  * Plans managed field updates for one model_providers section.
589
593
  */
590
- function planModelProviderFieldMutation(section, fields, operations) {
594
+ function planModelProviderFieldMutation(document, section, fields, operations) {
591
595
  let updated = false;
592
- const baseUrlText = fields.baseUrl !== undefined ? JSON.stringify(fields.baseUrl) : null;
593
- const nameText = fields.name !== undefined ? JSON.stringify(fields.name) : null;
594
- const requiresOpenAiAuthText = fields.requiresOpenAiAuth !== undefined ? String(fields.requiresOpenAiAuth) : null;
595
- const wireApiText = fields.wireApi !== undefined ? JSON.stringify(fields.wireApi) : null;
596
+ const baseUrlText = JSON.stringify(fields.baseUrl);
597
+ const nameText = JSON.stringify(fields.name);
598
+ const requiresOpenAiAuthText = String(fields.requiresOpenAiAuth);
599
+ const wireApiText = JSON.stringify(fields.wireApi);
596
600
  const inserts = [];
597
- if (baseUrlText !== null && section.baseUrlValueRange) {
601
+ if (section.baseUrlValueRange) {
598
602
  if (section.baseUrl !== fields.baseUrl) {
599
603
  operations.push({
600
604
  kind: "replace-range",
@@ -605,11 +609,11 @@ function planModelProviderFieldMutation(section, fields, operations) {
605
609
  updated = true;
606
610
  }
607
611
  }
608
- else if (baseUrlText !== null) {
612
+ else {
609
613
  inserts.push(`base_url = ${baseUrlText}`);
610
614
  updated = true;
611
615
  }
612
- if (nameText !== null && section.nameValueRange) {
616
+ if (section.nameValueRange) {
613
617
  if (section.providerName !== fields.name) {
614
618
  operations.push({
615
619
  kind: "replace-range",
@@ -620,11 +624,11 @@ function planModelProviderFieldMutation(section, fields, operations) {
620
624
  updated = true;
621
625
  }
622
626
  }
623
- else if (nameText !== null) {
627
+ else {
624
628
  inserts.push(`name = ${nameText}`);
625
629
  updated = true;
626
630
  }
627
- if (requiresOpenAiAuthText !== null && section.requiresOpenAiAuthValueRange) {
631
+ if (section.requiresOpenAiAuthValueRange) {
628
632
  if (section.requiresOpenAiAuth !== fields.requiresOpenAiAuth) {
629
633
  operations.push({
630
634
  kind: "replace-range",
@@ -635,11 +639,11 @@ function planModelProviderFieldMutation(section, fields, operations) {
635
639
  updated = true;
636
640
  }
637
641
  }
638
- else if (requiresOpenAiAuthText !== null) {
642
+ else {
639
643
  inserts.push(`requires_openai_auth = ${requiresOpenAiAuthText}`);
640
644
  updated = true;
641
645
  }
642
- if (wireApiText !== null && section.wireApiValueRange) {
646
+ if (section.wireApiValueRange) {
643
647
  if (section.wireApi !== fields.wireApi) {
644
648
  operations.push({
645
649
  kind: "replace-range",
@@ -650,7 +654,7 @@ function planModelProviderFieldMutation(section, fields, operations) {
650
654
  updated = true;
651
655
  }
652
656
  }
653
- else if (wireApiText !== null) {
657
+ else {
654
658
  inserts.push(`wire_api = ${wireApiText}`);
655
659
  updated = true;
656
660
  }
@@ -658,11 +662,59 @@ function planModelProviderFieldMutation(section, fields, operations) {
658
662
  operations.push({
659
663
  kind: "insert-at",
660
664
  index: section.managedFieldInsertIndex,
661
- text: `${inserts.join("\n")}\n`,
665
+ text: `${inserts.join(document.lineEnding)}${document.lineEnding}`,
662
666
  });
663
667
  }
664
668
  return updated;
665
669
  }
670
+ function planRootFieldMutation(document, key, currentValue, currentValueRange, currentLineRange, nextValue, operations) {
671
+ if (nextValue === undefined) {
672
+ return;
673
+ }
674
+ if (nextValue === null) {
675
+ if (currentLineRange) {
676
+ operations.push({
677
+ kind: "delete-range",
678
+ start: currentLineRange.start,
679
+ end: expandLineDeletionStart(document.rawText, currentLineRange.start, currentLineRange.end),
680
+ });
681
+ }
682
+ return;
683
+ }
684
+ if (currentValueRange) {
685
+ if (currentValue !== nextValue) {
686
+ operations.push({
687
+ kind: "replace-range",
688
+ start: currentValueRange.start,
689
+ end: currentValueRange.end,
690
+ text: JSON.stringify(nextValue),
691
+ });
692
+ }
693
+ return;
694
+ }
695
+ const insertAt = findTopLevelInsertIndex(document.rawText);
696
+ operations.push({
697
+ kind: "insert-at",
698
+ index: insertAt,
699
+ text: `${key} = ${JSON.stringify(nextValue)}${document.lineEnding}`,
700
+ });
701
+ }
702
+ function normalizeManagedModelProviderFields(profileName, fields) {
703
+ const baseUrl = fields.baseUrl?.trim() ?? "";
704
+ if (!baseUrl) {
705
+ throw (0, errors_1.cliError)("MANAGED_PROFILE_FIELDS_MISSING", `Model provider "${profileName}" requires base_url.`, {
706
+ profile: profileName,
707
+ modelProvider: profileName,
708
+ missingFields: ["base_url"],
709
+ });
710
+ }
711
+ return {
712
+ baseUrl,
713
+ name: fields.name?.trim() || profileName,
714
+ requiresOpenAiAuth: fields.requiresOpenAiAuth ?? true,
715
+ wireApi: fields.wireApi?.trim() || "responses",
716
+ };
717
+ }
666
718
  function splitWithOffsets(value) {
667
719
  if (value.length === 0) {
668
720
  return [];
@@ -787,6 +839,19 @@ function expandDeletionEnd(rawText, sectionStart, sectionEnd) {
787
839
  }
788
840
  return end;
789
841
  }
842
+ function expandLineDeletionStart(rawText, start, end) {
843
+ let nextEnd = end;
844
+ while (nextEnd < rawText.length && (rawText[nextEnd] === "\r" || rawText[nextEnd] === "\n")) {
845
+ nextEnd += 1;
846
+ }
847
+ return nextEnd;
848
+ }
849
+ function toAbsoluteRange(lineStart, valueStart, valueEnd) {
850
+ return {
851
+ start: lineStart + valueStart,
852
+ end: lineStart + valueEnd,
853
+ };
854
+ }
790
855
  function escapeRegExp(value) {
791
856
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
792
857
  }