@minniexcode/codex-switch 0.1.0 → 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.
@@ -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,
@@ -269,109 +293,60 @@ function buildManagedProfileViews(document, providers) {
269
293
  function collectConfigConsistencyIssues(document, providers) {
270
294
  const issues = [];
271
295
  const providerMap = providers?.providers ?? null;
272
- const profileLinkMap = buildProfileLinkMap(providers);
273
- for (const view of buildManagedProfileViews(document, providers)) {
274
- if (view.source === "orphaned-reference") {
275
- issues.push({
276
- code: "ORPHANED_PROFILE_REFERENCE",
277
- profile: view.name,
278
- providers: [...view.linkedProviders],
279
- });
280
- }
281
- if (view.source === "unmanaged" && view.linkedProviders.length === 0) {
282
- issues.push({
283
- code: "ORPHANED_PROFILE_SECTION",
284
- profile: view.name,
285
- });
286
- }
287
- if (view.linkedProviders.length > 1) {
288
- issues.push({
289
- code: "SHARED_PROFILE_REFERENCE",
290
- profile: view.name,
291
- providers: [...view.linkedProviders],
292
- });
293
- }
294
- if (view.source !== "orphaned-reference") {
295
- if (!view.modelProvider) {
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) {
296
335
  issues.push({
297
- code: "MODEL_PROVIDER_MISSING",
298
- 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",
299
342
  });
300
343
  }
301
- else {
302
- if (view.modelProvider !== view.name) {
303
- issues.push({
304
- code: "MODEL_PROVIDER_NAME_MISMATCH",
305
- profile: view.name,
306
- modelProvider: view.modelProvider,
307
- });
308
- }
309
- const modelProviderSection = document.modelProviders.find((entry) => entry.name === view.modelProvider);
310
- if (!modelProviderSection) {
311
- issues.push({
312
- code: "MODEL_PROVIDER_SECTION_MISSING",
313
- profile: view.name,
314
- modelProvider: view.modelProvider,
315
- });
316
- }
317
- else if (!modelProviderSection.baseUrl) {
318
- issues.push({
319
- code: "MODEL_PROVIDER_BASE_URL_MISSING",
320
- profile: view.name,
321
- modelProvider: view.modelProvider,
322
- });
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
- }
347
- }
348
- }
349
- }
350
- if (document.activeProfile) {
351
- const activeLinkInfo = profileLinkMap.get(document.activeProfile);
352
- if (!activeLinkInfo) {
353
- issues.push({
354
- code: "UNMANAGED_ACTIVE_PROFILE",
355
- profile: document.activeProfile,
356
- });
357
- }
358
- else if (activeLinkInfo.linkedProviders.length > 1) {
359
- issues.push({
360
- code: "ACTIVE_PROVIDER_UNRESOLVED",
361
- profile: document.activeProfile,
362
- providers: [...activeLinkInfo.linkedProviders],
363
- });
364
344
  }
365
345
  }
366
- return issues.sort((left, right) => {
367
- if (left.profile === right.profile) {
368
- return left.code.localeCompare(right.code);
369
- }
370
- return left.profile.localeCompare(right.profile);
371
- });
346
+ return issues.sort((left, right) => left.code.localeCompare(right.code));
372
347
  }
373
348
  /**
374
- * 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.
375
350
  */
376
351
  function validateManagedProfileCreation(profile, fields) {
377
352
  const model = fields.model?.trim() ?? "";
@@ -391,7 +366,7 @@ function validateManagedProfileCreation(profile, fields) {
391
366
  };
392
367
  }
393
368
  /**
394
- * 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.
395
370
  */
396
371
  function planProfileLifecycleOutcome(args) {
397
372
  if (!args.oldProfile || args.oldProfile === args.newProfile) {
@@ -435,7 +410,7 @@ function planProfileLifecycleOutcome(args) {
435
410
  };
436
411
  }
437
412
  /**
438
- * 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.
439
414
  */
440
415
  function planConfigMutation(document, args) {
441
416
  const operations = [];
@@ -446,27 +421,19 @@ function planConfigMutation(document, args) {
446
421
  const updatedModelProviders = [];
447
422
  const sectionMap = new Map(document.profiles.map((profile) => [profile.name, profile]));
448
423
  const modelProviderSectionMap = new Map(document.modelProviders.map((entry) => [entry.name, entry]));
449
- if (args.setActiveProfile && args.setActiveProfile !== document.activeProfile) {
450
- const quoted = `"${args.setActiveProfile}"`;
451
- if (document.activeProfileRange) {
452
- operations.push({
453
- kind: "replace-range",
454
- start: document.activeProfileRange.start,
455
- end: document.activeProfileRange.end,
456
- text: quoted,
457
- });
458
- }
459
- else {
460
- const insertAt = findTopLevelInsertIndex(document.rawText);
461
- const text = `profile = ${quoted}${document.lineEnding}`;
462
- operations.push({
463
- kind: "insert-at",
464
- index: insertAt,
465
- text,
466
- });
467
- }
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
+ });
468
435
  }
469
- for (const profileName of args.deleteProfiles ?? []) {
436
+ for (const profileName of [...(args.deleteProfiles ?? []), ...(args.deleteLegacyProfilesByName ?? [])]) {
470
437
  const section = sectionMap.get(profileName);
471
438
  if (!section) {
472
439
  continue;
@@ -503,39 +470,47 @@ function planConfigMutation(document, args) {
503
470
  for (const [profileName, fields] of Object.entries(args.upsertModelProviders ?? {}).sort(([left], [right]) => left.localeCompare(right))) {
504
471
  const section = modelProviderSectionMap.get(profileName);
505
472
  if (!section) {
506
- const baseUrl = fields.baseUrl?.trim() ?? "";
507
- const providerName = fields.name?.trim() ?? "";
508
- if (!baseUrl) {
509
- throw (0, errors_1.cliError)("MANAGED_PROFILE_FIELDS_MISSING", `Model provider "${profileName}" requires base_url.`, {
510
- profile: profileName,
511
- modelProvider: profileName,
512
- missingFields: [
513
- !baseUrl ? "base_url" : null,
514
- ].filter((value) => Boolean(value)),
515
- });
516
- }
473
+ const normalizedFields = normalizeManagedModelProviderFields(profileName, fields);
517
474
  const prefix = document.rawText.length > 0 && !document.rawText.endsWith(document.lineEnding)
518
475
  ? document.lineEnding
519
476
  : "";
520
- const requiresOpenAiAuth = fields.requiresOpenAiAuth;
521
- const wireApi = fields.wireApi?.trim() ?? "";
522
477
  operations.push({
523
478
  kind: "insert-at",
524
479
  index: document.rawText.length,
525
480
  text: `${prefix}[model_providers.${profileName}]${document.lineEnding}` +
526
- `base_url = ${JSON.stringify(baseUrl)}${document.lineEnding}` +
527
- (providerName ? `name = ${JSON.stringify(providerName)}${document.lineEnding}` : "") +
528
- (requiresOpenAiAuth !== undefined ? `requires_openai_auth = ${String(requiresOpenAiAuth)}${document.lineEnding}` : "") +
529
- (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}`,
530
485
  });
531
486
  createdModelProviderSections.push(profileName);
532
487
  continue;
533
488
  }
534
- const sectionUpdated = planModelProviderFieldMutation(section, fields, operations);
489
+ const sectionUpdated = planModelProviderFieldMutation(document, section, normalizeManagedModelProviderFields(profileName, fields), operations);
535
490
  if (sectionUpdated) {
536
491
  updatedModelProviders.push(profileName);
537
492
  }
538
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
+ }
539
514
  return {
540
515
  operations,
541
516
  createdProfileSections,
@@ -543,7 +518,8 @@ function planConfigMutation(document, args) {
543
518
  deletedProfileSections,
544
519
  updatedProfiles,
545
520
  updatedModelProviders,
546
- 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),
547
523
  };
548
524
  }
549
525
  /**
@@ -565,6 +541,9 @@ function applyPatchOperations(rawText, operations) {
565
541
  }
566
542
  return nextText;
567
543
  }
544
+ /**
545
+ * Plans managed field updates for one legacy profile section.
546
+ */
568
547
  function planSectionFieldMutation(document, section, fields, operations) {
569
548
  let updated = false;
570
549
  const modelText = fields.model !== undefined ? JSON.stringify(fields.model) : null;
@@ -612,14 +591,14 @@ function planSectionFieldMutation(document, section, fields, operations) {
612
591
  /**
613
592
  * Plans managed field updates for one model_providers section.
614
593
  */
615
- function planModelProviderFieldMutation(section, fields, operations) {
594
+ function planModelProviderFieldMutation(document, section, fields, operations) {
616
595
  let updated = false;
617
- const baseUrlText = fields.baseUrl !== undefined ? JSON.stringify(fields.baseUrl) : null;
618
- const nameText = fields.name !== undefined ? JSON.stringify(fields.name) : null;
619
- const requiresOpenAiAuthText = fields.requiresOpenAiAuth !== undefined ? String(fields.requiresOpenAiAuth) : null;
620
- 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);
621
600
  const inserts = [];
622
- if (baseUrlText !== null && section.baseUrlValueRange) {
601
+ if (section.baseUrlValueRange) {
623
602
  if (section.baseUrl !== fields.baseUrl) {
624
603
  operations.push({
625
604
  kind: "replace-range",
@@ -630,11 +609,11 @@ function planModelProviderFieldMutation(section, fields, operations) {
630
609
  updated = true;
631
610
  }
632
611
  }
633
- else if (baseUrlText !== null) {
612
+ else {
634
613
  inserts.push(`base_url = ${baseUrlText}`);
635
614
  updated = true;
636
615
  }
637
- if (nameText !== null && section.nameValueRange) {
616
+ if (section.nameValueRange) {
638
617
  if (section.providerName !== fields.name) {
639
618
  operations.push({
640
619
  kind: "replace-range",
@@ -645,11 +624,11 @@ function planModelProviderFieldMutation(section, fields, operations) {
645
624
  updated = true;
646
625
  }
647
626
  }
648
- else if (nameText !== null) {
627
+ else {
649
628
  inserts.push(`name = ${nameText}`);
650
629
  updated = true;
651
630
  }
652
- if (requiresOpenAiAuthText !== null && section.requiresOpenAiAuthValueRange) {
631
+ if (section.requiresOpenAiAuthValueRange) {
653
632
  if (section.requiresOpenAiAuth !== fields.requiresOpenAiAuth) {
654
633
  operations.push({
655
634
  kind: "replace-range",
@@ -660,11 +639,11 @@ function planModelProviderFieldMutation(section, fields, operations) {
660
639
  updated = true;
661
640
  }
662
641
  }
663
- else if (requiresOpenAiAuthText !== null) {
642
+ else {
664
643
  inserts.push(`requires_openai_auth = ${requiresOpenAiAuthText}`);
665
644
  updated = true;
666
645
  }
667
- if (wireApiText !== null && section.wireApiValueRange) {
646
+ if (section.wireApiValueRange) {
668
647
  if (section.wireApi !== fields.wireApi) {
669
648
  operations.push({
670
649
  kind: "replace-range",
@@ -675,7 +654,7 @@ function planModelProviderFieldMutation(section, fields, operations) {
675
654
  updated = true;
676
655
  }
677
656
  }
678
- else if (wireApiText !== null) {
657
+ else {
679
658
  inserts.push(`wire_api = ${wireApiText}`);
680
659
  updated = true;
681
660
  }
@@ -683,11 +662,59 @@ function planModelProviderFieldMutation(section, fields, operations) {
683
662
  operations.push({
684
663
  kind: "insert-at",
685
664
  index: section.managedFieldInsertIndex,
686
- text: `${inserts.join("\n")}\n`,
665
+ text: `${inserts.join(document.lineEnding)}${document.lineEnding}`,
687
666
  });
688
667
  }
689
668
  return updated;
690
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
+ }
691
718
  function splitWithOffsets(value) {
692
719
  if (value.length === 0) {
693
720
  return [];
@@ -812,6 +839,19 @@ function expandDeletionEnd(rawText, sectionStart, sectionEnd) {
812
839
  }
813
840
  return end;
814
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
+ }
815
855
  function escapeRegExp(value) {
816
856
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
817
857
  }