@minniexcode/codex-switch 0.0.3 → 0.0.5

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