@minniexcode/codex-switch 0.0.4 → 0.0.6

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 (64) hide show
  1. package/README.md +35 -97
  2. package/dist/app/add-provider.js +40 -3
  3. package/dist/app/edit-provider.js +76 -3
  4. package/dist/app/export-providers.js +2 -2
  5. package/dist/app/get-current-profile.js +1 -1
  6. package/dist/app/get-status.js +10 -3
  7. package/dist/app/import-providers.js +47 -3
  8. package/dist/app/list-backups.js +1 -1
  9. package/dist/app/list-config-profiles.js +30 -0
  10. package/dist/app/list-providers.js +1 -1
  11. package/dist/app/remove-provider.js +35 -3
  12. package/dist/app/rollback-backup.js +1 -1
  13. package/dist/app/rollback-latest.js +1 -1
  14. package/dist/app/run-doctor.js +44 -26
  15. package/dist/app/run-mutation.js +2 -2
  16. package/dist/app/setup-codex.js +37 -20
  17. package/dist/app/show-config.js +34 -0
  18. package/dist/app/show-provider.js +1 -1
  19. package/dist/app/switch-provider.js +8 -5
  20. package/dist/cli/add-interactive.js +7 -106
  21. package/dist/cli/args.js +5 -126
  22. package/dist/cli/help.js +5 -276
  23. package/dist/cli/interactive.js +16 -171
  24. package/dist/cli/output.js +23 -1
  25. package/dist/cli/prompt.js +3 -108
  26. package/dist/cli.js +10 -315
  27. package/dist/commands/args.js +132 -0
  28. package/dist/commands/dispatch.js +16 -0
  29. package/dist/commands/handlers.js +391 -0
  30. package/dist/commands/help.js +119 -0
  31. package/dist/commands/registry.js +291 -0
  32. package/dist/commands/types.js +2 -0
  33. package/dist/domain/config.js +548 -39
  34. package/dist/infra/backup-repo.js +8 -208
  35. package/dist/infra/codex-cli.js +8 -113
  36. package/dist/infra/codex-discovery.js +3 -41
  37. package/dist/infra/codex-paths.js +5 -69
  38. package/dist/infra/config-repo.js +161 -9
  39. package/dist/infra/fs-utils.js +7 -95
  40. package/dist/infra/lock-repo.js +3 -97
  41. package/dist/infra/providers-repo.js +7 -96
  42. package/dist/interaction/add-interactive.js +108 -0
  43. package/dist/interaction/interactive.js +216 -0
  44. package/dist/interaction/prompt.js +110 -0
  45. package/dist/runtime/codex-cli.js +130 -0
  46. package/dist/runtime/codex-probe.js +50 -0
  47. package/dist/runtime/types.js +2 -0
  48. package/dist/storage/backup-repo.js +210 -0
  49. package/dist/storage/codex-paths.js +71 -0
  50. package/dist/storage/config-repo.js +208 -0
  51. package/dist/storage/fs-utils.js +97 -0
  52. package/dist/storage/lock-repo.js +99 -0
  53. package/dist/storage/providers-repo.js +98 -0
  54. package/docs/Design/codex-switch-v0.0.5-design.md +932 -0
  55. package/docs/Design/codex-switch-v0.0.6-design.md +708 -0
  56. package/docs/PRD/codex-switch-prd-v0.0.5-to-v0.1.0.md +340 -0
  57. package/docs/PRD/codex-switch-prd-v0.1.0.md +215 -291
  58. package/docs/PRD/codex-switch-prd.md +1 -1
  59. package/docs/cli-usage.md +2 -1
  60. package/docs/codex-switch-technical-architecture.md +73 -4
  61. package/docs/test-report-0.0.5.md +163 -0
  62. package/docs/testing.md +131 -0
  63. package/package.json +1 -1
  64. /package/docs/{codex-switch-v0.0.4-design.md → Design/codex-switch-v0.0.4-design.md} +0 -0
@@ -33,74 +33,583 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.DEFAULT_LINE_ENDING = void 0;
36
37
  exports.parseTopLevelProfile = parseTopLevelProfile;
37
38
  exports.parseProfileNames = parseProfileNames;
38
39
  exports.replaceTopLevelProfile = replaceTopLevelProfile;
40
+ exports.parseStructuredConfig = parseStructuredConfig;
41
+ exports.buildManagedProfileViews = buildManagedProfileViews;
42
+ exports.collectConfigConsistencyIssues = collectConfigConsistencyIssues;
43
+ exports.validateManagedProfileCreation = validateManagedProfileCreation;
44
+ exports.planProfileLifecycleOutcome = planProfileLifecycleOutcome;
45
+ exports.planConfigMutation = planConfigMutation;
46
+ exports.applyPatchOperations = applyPatchOperations;
39
47
  const os = __importStar(require("node:os"));
48
+ const errors_1 = require("./errors");
40
49
  /**
41
50
  * Reads the active top-level profile from config.toml content.
42
51
  */
43
52
  function parseTopLevelProfile(configContent) {
53
+ return parseStructuredConfig(configContent).activeProfile;
54
+ }
55
+ /**
56
+ * Collects all named profile sections declared in config.toml content.
57
+ */
58
+ function parseProfileNames(configContent) {
59
+ return new Set(parseStructuredConfig(configContent).profiles.map((profile) => profile.name));
60
+ }
61
+ /**
62
+ * Replaces or inserts the top-level profile assignment while preserving the rest of the file.
63
+ */
64
+ function replaceTopLevelProfile(configContent, profile) {
65
+ const plan = planConfigMutation(parseStructuredConfig(configContent), { setActiveProfile: profile });
66
+ return applyPatchOperations(configContent, plan.operations);
67
+ }
68
+ /**
69
+ * Parses the supported config.toml subset into a structured document with stable text ranges.
70
+ */
71
+ function parseStructuredConfig(configContent) {
72
+ const lineEnding = configContent.includes("\r\n") ? "\r\n" : "\n";
73
+ const lines = splitWithOffsets(configContent);
74
+ let activeProfile = null;
75
+ let activeProfileRange = null;
76
+ const profiles = [];
77
+ const modelProviders = [];
78
+ let currentProfile = null;
79
+ let currentModelProvider = null;
44
80
  let inRoot = true;
45
- for (const line of configContent.split(/\r?\n/)) {
46
- const trimmed = line.trim();
47
- if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
81
+ for (const line of lines) {
82
+ const trimmed = line.content.trim();
83
+ const headerMatch = trimmed.match(/^\[profiles\.([^\]]+)\]$/);
84
+ if (headerMatch) {
85
+ if (currentProfile) {
86
+ currentProfile.sectionEnd = line.start;
87
+ }
88
+ if (currentModelProvider) {
89
+ currentModelProvider.sectionEnd = line.start;
90
+ currentModelProvider = null;
91
+ }
92
+ currentProfile = {
93
+ name: headerMatch[1],
94
+ headerStart: line.start,
95
+ sectionStart: line.start,
96
+ sectionEnd: configContent.length,
97
+ managedFieldInsertIndex: configContent.length,
98
+ modelValueRange: null,
99
+ modelProviderValueRange: null,
100
+ model: null,
101
+ modelProvider: null,
102
+ };
103
+ profiles.push(currentProfile);
48
104
  inRoot = false;
49
105
  continue;
50
106
  }
51
- if (!inRoot || trimmed === "" || trimmed.startsWith("#")) {
107
+ const modelProviderHeaderMatch = trimmed.match(/^\[model_providers\.([^\]]+)\]$/);
108
+ if (modelProviderHeaderMatch) {
109
+ if (currentProfile) {
110
+ currentProfile.sectionEnd = line.start;
111
+ currentProfile = null;
112
+ }
113
+ if (currentModelProvider) {
114
+ currentModelProvider.sectionEnd = line.start;
115
+ }
116
+ currentModelProvider = {
117
+ name: modelProviderHeaderMatch[1],
118
+ sectionStart: line.start,
119
+ sectionEnd: configContent.length,
120
+ baseUrlValueRange: null,
121
+ baseUrl: null,
122
+ };
123
+ modelProviders.push(currentModelProvider);
124
+ inRoot = false;
52
125
  continue;
53
126
  }
54
- const match = trimmed.match(/^profile\s*=\s*["']([^"']+)["']/);
55
- if (match) {
56
- return match[1];
127
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
128
+ if (currentProfile) {
129
+ currentProfile.sectionEnd = line.start;
130
+ currentProfile = null;
131
+ }
132
+ if (currentModelProvider) {
133
+ currentModelProvider.sectionEnd = line.start;
134
+ currentModelProvider = null;
135
+ }
136
+ inRoot = false;
137
+ continue;
138
+ }
139
+ if (inRoot) {
140
+ const profileMatch = matchKeyValueLine(line.content, "profile");
141
+ if (profileMatch && !activeProfile) {
142
+ activeProfile = profileMatch.value;
143
+ activeProfileRange = {
144
+ start: line.start + profileMatch.valueStart,
145
+ end: line.start + profileMatch.valueEnd,
146
+ };
147
+ }
148
+ }
149
+ if (currentProfile) {
150
+ const modelMatch = matchKeyValueLine(line.content, "model");
151
+ if (modelMatch) {
152
+ currentProfile.model = modelMatch.value;
153
+ currentProfile.modelValueRange = {
154
+ start: line.start + modelMatch.valueStart,
155
+ end: line.start + modelMatch.valueEnd,
156
+ };
157
+ }
158
+ const modelProviderMatch = matchKeyValueLine(line.content, "model_provider");
159
+ if (modelProviderMatch) {
160
+ currentProfile.modelProvider = modelProviderMatch.value;
161
+ currentProfile.modelProviderValueRange = {
162
+ start: line.start + modelProviderMatch.valueStart,
163
+ end: line.start + modelProviderMatch.valueEnd,
164
+ };
165
+ }
166
+ }
167
+ if (currentModelProvider) {
168
+ const baseUrlMatch = matchKeyValueLine(line.content, "base_url");
169
+ if (baseUrlMatch) {
170
+ currentModelProvider.baseUrl = baseUrlMatch.value;
171
+ currentModelProvider.baseUrlValueRange = {
172
+ start: line.start + baseUrlMatch.valueStart,
173
+ end: line.start + baseUrlMatch.valueEnd,
174
+ };
175
+ }
57
176
  }
58
177
  }
59
- return null;
178
+ return {
179
+ rawText: configContent,
180
+ lineEnding,
181
+ activeProfile,
182
+ activeProfileRange,
183
+ profiles: profiles.map((profile) => ({
184
+ ...profile,
185
+ managedFieldInsertIndex: findManagedFieldInsertIndex(configContent, profile.sectionStart, profile.sectionEnd),
186
+ })),
187
+ modelProviders,
188
+ };
60
189
  }
61
190
  /**
62
- * Collects all named profile sections declared in config.toml content.
191
+ * Builds the managed/unmanaged/orphaned profile views used by config commands and diagnostics.
63
192
  */
64
- function parseProfileNames(configContent) {
65
- const result = new Set();
66
- for (const line of configContent.split(/\r?\n/)) {
67
- const trimmed = line.trim();
68
- const match = trimmed.match(/^\[profiles\.([^\]]+)\]$/);
69
- if (match) {
70
- result.add(match[1]);
193
+ function buildManagedProfileViews(document, providers) {
194
+ const linkMap = buildProfileLinkMap(providers);
195
+ const modelProviderMap = new Map(document.modelProviders.map((provider) => [provider.name, provider]));
196
+ const views = [];
197
+ const seen = new Set();
198
+ for (const section of document.profiles) {
199
+ const linkInfo = linkMap.get(section.name) ?? { linkedProviders: [], managed: false };
200
+ const modelProviderSection = section.modelProvider ? modelProviderMap.get(section.modelProvider) ?? null : null;
201
+ seen.add(section.name);
202
+ views.push({
203
+ name: section.name,
204
+ managed: linkInfo.managed,
205
+ isActive: document.activeProfile === section.name,
206
+ linkedProviders: [...linkInfo.linkedProviders].sort(),
207
+ model: section.model,
208
+ modelProvider: section.modelProvider,
209
+ baseUrl: modelProviderSection?.baseUrl ?? null,
210
+ managedFields: collectManagedFields(section.model, section.modelProvider),
211
+ source: linkInfo.managed ? "managed" : "unmanaged",
212
+ });
213
+ }
214
+ for (const [profile, linkInfo] of [...linkMap.entries()].sort(([left], [right]) => left.localeCompare(right))) {
215
+ if (seen.has(profile)) {
216
+ continue;
71
217
  }
218
+ views.push({
219
+ name: profile,
220
+ managed: true,
221
+ isActive: document.activeProfile === profile,
222
+ linkedProviders: [...linkInfo.linkedProviders].sort(),
223
+ model: null,
224
+ modelProvider: null,
225
+ baseUrl: null,
226
+ managedFields: [],
227
+ source: "orphaned-reference",
228
+ });
72
229
  }
73
- return result;
230
+ return views.sort((left, right) => left.name.localeCompare(right.name));
74
231
  }
75
232
  /**
76
- * Replaces or inserts the top-level profile assignment while preserving the rest of the file.
233
+ * Collects structured config consistency issues for doctor and status.
77
234
  */
78
- function replaceTopLevelProfile(configContent, profile) {
79
- const lines = configContent.split(/\r?\n/);
80
- let inRoot = true;
81
- let replaced = false;
82
- const nextLines = lines.map((line) => {
83
- const trimmed = line.trim();
84
- if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
85
- // Only the root section may contain the active `profile = ...` switch.
86
- inRoot = false;
87
- return line;
235
+ function collectConfigConsistencyIssues(document, providers) {
236
+ const issues = [];
237
+ for (const view of buildManagedProfileViews(document, providers)) {
238
+ if (view.source === "orphaned-reference") {
239
+ issues.push({
240
+ code: "ORPHANED_PROFILE_REFERENCE",
241
+ profile: view.name,
242
+ providers: [...view.linkedProviders],
243
+ });
244
+ }
245
+ if (view.source === "unmanaged" && view.linkedProviders.length === 0) {
246
+ issues.push({
247
+ code: "ORPHANED_PROFILE_SECTION",
248
+ profile: view.name,
249
+ });
250
+ }
251
+ if (view.linkedProviders.length > 1) {
252
+ issues.push({
253
+ code: "SHARED_PROFILE_REFERENCE",
254
+ profile: view.name,
255
+ providers: [...view.linkedProviders],
256
+ });
257
+ }
258
+ if (view.source !== "orphaned-reference") {
259
+ if (!view.modelProvider) {
260
+ issues.push({
261
+ code: "MODEL_PROVIDER_MISSING",
262
+ profile: view.name,
263
+ });
264
+ }
265
+ else {
266
+ if (view.modelProvider !== view.name) {
267
+ issues.push({
268
+ code: "MODEL_PROVIDER_NAME_MISMATCH",
269
+ profile: view.name,
270
+ modelProvider: view.modelProvider,
271
+ });
272
+ }
273
+ const modelProviderSection = document.modelProviders.find((entry) => entry.name === view.modelProvider);
274
+ if (!modelProviderSection) {
275
+ issues.push({
276
+ code: "MODEL_PROVIDER_SECTION_MISSING",
277
+ profile: view.name,
278
+ modelProvider: view.modelProvider,
279
+ });
280
+ }
281
+ else if (!modelProviderSection.baseUrl) {
282
+ issues.push({
283
+ code: "MODEL_PROVIDER_BASE_URL_MISSING",
284
+ profile: view.name,
285
+ modelProvider: view.modelProvider,
286
+ });
287
+ }
288
+ }
88
289
  }
89
- if (!replaced && inRoot && /^profile\s*=/.test(trimmed)) {
90
- replaced = true;
91
- return `profile = "${profile}"`;
290
+ }
291
+ if (document.activeProfile) {
292
+ const activeLinkInfo = buildProfileLinkMap(providers).get(document.activeProfile);
293
+ if (!activeLinkInfo) {
294
+ issues.push({
295
+ code: "UNMANAGED_ACTIVE_PROFILE",
296
+ profile: document.activeProfile,
297
+ });
298
+ }
299
+ }
300
+ return issues.sort((left, right) => {
301
+ if (left.profile === right.profile) {
302
+ return left.code.localeCompare(right.code);
92
303
  }
93
- return line;
304
+ return left.profile.localeCompare(right.profile);
94
305
  });
95
- if (!replaced) {
96
- // When no root-level profile exists yet, insert it before the first section header.
97
- const insertAt = nextLines.findIndex((line) => line.trim().startsWith("["));
98
- if (insertAt === -1) {
99
- nextLines.push(`profile = "${profile}"`);
306
+ }
307
+ /**
308
+ * Ensures the minimal managed profile fields are available before a new section is created.
309
+ */
310
+ function validateManagedProfileCreation(profile, fields) {
311
+ const model = fields.model?.trim() ?? "";
312
+ const modelProvider = fields.modelProvider?.trim() ?? "";
313
+ if (!model || !modelProvider) {
314
+ throw (0, errors_1.cliError)("MANAGED_PROFILE_FIELDS_MISSING", `Managed profile "${profile}" requires both model and model_provider.`, {
315
+ profile,
316
+ missingFields: [
317
+ !model ? "model" : null,
318
+ !modelProvider ? "model_provider" : null,
319
+ ].filter((value) => Boolean(value)),
320
+ });
321
+ }
322
+ return {
323
+ model,
324
+ modelProvider,
325
+ };
326
+ }
327
+ /**
328
+ * Computes keep/delete/switch outcomes when a provider leaves or changes profiles.
329
+ */
330
+ function planProfileLifecycleOutcome(args) {
331
+ if (!args.oldProfile || args.oldProfile === args.newProfile) {
332
+ return {
333
+ deletedProfileSections: [],
334
+ keptSharedProfiles: [],
335
+ switchedActiveProfile: false,
336
+ nextActiveProfile: args.activeProfile,
337
+ };
338
+ }
339
+ const remainingLinks = args.remainingLinksByProfile.get(args.oldProfile) ?? [];
340
+ if (remainingLinks.length > 0) {
341
+ return {
342
+ deletedProfileSections: [],
343
+ keptSharedProfiles: [args.oldProfile],
344
+ switchedActiveProfile: false,
345
+ nextActiveProfile: args.activeProfile,
346
+ };
347
+ }
348
+ if (args.activeProfile === args.oldProfile) {
349
+ if (!args.switchToProfile) {
350
+ throw (0, errors_1.cliError)("PROFILE_IN_USE", `Profile "${args.oldProfile}" is still the active profile. Switch first before removing the last linked provider.`, {
351
+ profile: args.oldProfile,
352
+ provider: args.providerName,
353
+ activeProfile: args.activeProfile,
354
+ linkedProviders: [],
355
+ });
356
+ }
357
+ return {
358
+ deletedProfileSections: [args.oldProfile],
359
+ keptSharedProfiles: [],
360
+ switchedActiveProfile: true,
361
+ nextActiveProfile: args.switchToProfile,
362
+ };
363
+ }
364
+ return {
365
+ deletedProfileSections: [args.oldProfile],
366
+ keptSharedProfiles: [],
367
+ switchedActiveProfile: false,
368
+ nextActiveProfile: args.activeProfile,
369
+ };
370
+ }
371
+ /**
372
+ * Builds a text patch plan for top-level profile changes and profile section lifecycle changes.
373
+ */
374
+ function planConfigMutation(document, args) {
375
+ const operations = [];
376
+ const createdProfileSections = [];
377
+ const deletedProfileSections = [];
378
+ const updatedProfiles = [];
379
+ const sectionMap = new Map(document.profiles.map((profile) => [profile.name, profile]));
380
+ if (args.setActiveProfile && args.setActiveProfile !== document.activeProfile) {
381
+ const quoted = `"${args.setActiveProfile}"`;
382
+ if (document.activeProfileRange) {
383
+ operations.push({
384
+ kind: "replace-range",
385
+ start: document.activeProfileRange.start,
386
+ end: document.activeProfileRange.end,
387
+ text: quoted,
388
+ });
389
+ }
390
+ else {
391
+ const insertAt = findTopLevelInsertIndex(document.rawText);
392
+ const text = `profile = ${quoted}${document.lineEnding}`;
393
+ operations.push({
394
+ kind: "insert-at",
395
+ index: insertAt,
396
+ text,
397
+ });
398
+ }
399
+ }
400
+ for (const profileName of args.deleteProfiles ?? []) {
401
+ const section = sectionMap.get(profileName);
402
+ if (!section) {
403
+ continue;
404
+ }
405
+ operations.push({
406
+ kind: "delete-range",
407
+ start: section.sectionStart,
408
+ end: expandDeletionEnd(document.rawText, section.sectionStart, section.sectionEnd),
409
+ });
410
+ deletedProfileSections.push(profileName);
411
+ }
412
+ for (const [profileName, fields] of Object.entries(args.upsertProfiles ?? {}).sort(([left], [right]) => left.localeCompare(right))) {
413
+ const section = sectionMap.get(profileName);
414
+ if (!section) {
415
+ const requiredFields = validateManagedProfileCreation(profileName, fields);
416
+ const prefix = document.rawText.length > 0 && !document.rawText.endsWith(document.lineEnding)
417
+ ? document.lineEnding
418
+ : "";
419
+ operations.push({
420
+ kind: "insert-at",
421
+ index: document.rawText.length,
422
+ text: `${prefix}[profiles.${profileName}]${document.lineEnding}` +
423
+ `model = ${JSON.stringify(requiredFields.model)}${document.lineEnding}` +
424
+ `model_provider = ${JSON.stringify(requiredFields.modelProvider)}${document.lineEnding}`,
425
+ });
426
+ createdProfileSections.push(profileName);
427
+ continue;
428
+ }
429
+ const sectionUpdated = planSectionFieldMutation(document, section, fields, operations);
430
+ if (sectionUpdated) {
431
+ updatedProfiles.push(profileName);
432
+ }
433
+ }
434
+ return {
435
+ operations,
436
+ createdProfileSections,
437
+ deletedProfileSections,
438
+ updatedProfiles,
439
+ switchedActiveProfile: Boolean(args.setActiveProfile && args.setActiveProfile !== document.activeProfile),
440
+ };
441
+ }
442
+ /**
443
+ * Applies a patch plan to raw config text. Callers should sort by reverse offsets only once here.
444
+ */
445
+ function applyPatchOperations(rawText, operations) {
446
+ const sorted = [...operations].sort((left, right) => getOperationStart(right) - getOperationStart(left));
447
+ let nextText = rawText;
448
+ for (const operation of sorted) {
449
+ if (operation.kind === "replace-range") {
450
+ nextText = `${nextText.slice(0, operation.start)}${operation.text}${nextText.slice(operation.end)}`;
451
+ continue;
452
+ }
453
+ if (operation.kind === "delete-range") {
454
+ nextText = `${nextText.slice(0, operation.start)}${nextText.slice(operation.end)}`;
455
+ continue;
456
+ }
457
+ nextText = `${nextText.slice(0, operation.index)}${operation.text}${nextText.slice(operation.index)}`;
458
+ }
459
+ return nextText;
460
+ }
461
+ function planSectionFieldMutation(document, section, fields, operations) {
462
+ let updated = false;
463
+ const modelText = fields.model !== undefined ? JSON.stringify(fields.model) : null;
464
+ const modelProviderText = fields.modelProvider !== undefined ? JSON.stringify(fields.modelProvider) : null;
465
+ const inserts = [];
466
+ if (modelText !== null && section.modelValueRange) {
467
+ if (section.model !== fields.model) {
468
+ operations.push({
469
+ kind: "replace-range",
470
+ start: section.modelValueRange.start,
471
+ end: section.modelValueRange.end,
472
+ text: modelText,
473
+ });
474
+ updated = true;
475
+ }
476
+ }
477
+ else if (modelText !== null && !section.modelValueRange) {
478
+ inserts.push(`model = ${modelText}${document.lineEnding}`);
479
+ updated = true;
480
+ }
481
+ if (modelProviderText !== null && section.modelProviderValueRange) {
482
+ if (section.modelProvider !== fields.modelProvider) {
483
+ operations.push({
484
+ kind: "replace-range",
485
+ start: section.modelProviderValueRange.start,
486
+ end: section.modelProviderValueRange.end,
487
+ text: modelProviderText,
488
+ });
489
+ updated = true;
490
+ }
491
+ }
492
+ else if (modelProviderText !== null && !section.modelProviderValueRange) {
493
+ inserts.push(`model_provider = ${modelProviderText}${document.lineEnding}`);
494
+ updated = true;
495
+ }
496
+ if (inserts.length > 0) {
497
+ operations.push({
498
+ kind: "insert-at",
499
+ index: section.managedFieldInsertIndex,
500
+ text: inserts.join(""),
501
+ });
502
+ }
503
+ return updated;
504
+ }
505
+ function splitWithOffsets(value) {
506
+ if (value.length === 0) {
507
+ return [];
508
+ }
509
+ const result = [];
510
+ let index = 0;
511
+ while (index < value.length) {
512
+ let nextBreak = value.indexOf("\n", index);
513
+ if (nextBreak === -1) {
514
+ nextBreak = value.length;
100
515
  }
101
516
  else {
102
- nextLines.splice(insertAt, 0, `profile = "${profile}"`);
517
+ nextBreak += 1;
518
+ }
519
+ result.push({
520
+ content: value.slice(index, nextBreak).replace(/\r?\n$/, ""),
521
+ start: index,
522
+ end: nextBreak,
523
+ });
524
+ index = nextBreak;
525
+ }
526
+ return result;
527
+ }
528
+ function matchKeyValueLine(line, key) {
529
+ const match = line.match(new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*(["'])(.*?)\\1\\s*(#.*)?$`));
530
+ if (!match || match.index === undefined) {
531
+ return null;
532
+ }
533
+ const value = match[2];
534
+ const openingQuoteIndex = line.indexOf(match[1], match.index);
535
+ if (openingQuoteIndex === -1) {
536
+ return null;
537
+ }
538
+ const valueStart = openingQuoteIndex;
539
+ const valueEnd = openingQuoteIndex + match[1].length + value.length + match[1].length;
540
+ return {
541
+ value,
542
+ valueStart,
543
+ valueEnd,
544
+ };
545
+ }
546
+ function findManagedFieldInsertIndex(rawText, sectionStart, sectionEnd) {
547
+ const sectionText = rawText.slice(sectionStart, sectionEnd);
548
+ const lines = splitWithOffsets(sectionText);
549
+ let lastMeaningfulIndex = lines.length - 1;
550
+ while (lastMeaningfulIndex >= 0) {
551
+ const trimmed = lines[lastMeaningfulIndex].content.trim();
552
+ if (trimmed === "" || trimmed.startsWith("#")) {
553
+ lastMeaningfulIndex -= 1;
554
+ continue;
555
+ }
556
+ break;
557
+ }
558
+ if (lastMeaningfulIndex < 0) {
559
+ return sectionEnd;
560
+ }
561
+ return sectionStart + lines[lastMeaningfulIndex].end;
562
+ }
563
+ function collectManagedFields(model, modelProvider) {
564
+ const fields = [];
565
+ if (model !== null) {
566
+ fields.push("model");
567
+ }
568
+ if (modelProvider !== null) {
569
+ fields.push("model_provider");
570
+ }
571
+ return fields;
572
+ }
573
+ function buildProfileLinkMap(providers) {
574
+ const map = new Map();
575
+ for (const [providerName, provider] of Object.entries(providers?.providers ?? {})) {
576
+ const current = map.get(provider.profile) ?? { linkedProviders: [], managed: true };
577
+ current.linkedProviders.push(providerName);
578
+ current.managed = true;
579
+ map.set(provider.profile, current);
580
+ }
581
+ for (const value of map.values()) {
582
+ value.linkedProviders.sort();
583
+ }
584
+ return map;
585
+ }
586
+ function getOperationStart(operation) {
587
+ if (operation.kind === "insert-at") {
588
+ return operation.index;
589
+ }
590
+ return operation.start;
591
+ }
592
+ function findTopLevelInsertIndex(rawText) {
593
+ const sectionMatch = rawText.match(/^\s*\[/m);
594
+ return sectionMatch && sectionMatch.index !== undefined ? sectionMatch.index : rawText.length;
595
+ }
596
+ function expandDeletionEnd(rawText, sectionStart, sectionEnd) {
597
+ let end = sectionEnd;
598
+ while (end < rawText.length && (rawText[end] === "\r" || rawText[end] === "\n")) {
599
+ end += 1;
600
+ }
601
+ if (sectionStart > 0) {
602
+ let cursor = sectionStart - 1;
603
+ while (cursor >= 0 && (rawText[cursor] === "\r" || rawText[cursor] === "\n")) {
604
+ cursor -= 1;
605
+ }
606
+ if (cursor < sectionStart - 1) {
607
+ return end;
103
608
  }
104
609
  }
105
- return nextLines.join(os.EOL);
610
+ return end;
611
+ }
612
+ function escapeRegExp(value) {
613
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
106
614
  }
615
+ exports.DEFAULT_LINE_ENDING = os.EOL === "\r\n" ? "\r\n" : "\n";