@minniexcode/codex-switch 0.0.4 → 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.
@@ -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";
@@ -9,6 +9,18 @@ exports.checkCodexVersion = checkCodexVersion;
9
9
  const node_child_process_1 = require("node:child_process");
10
10
  const errors_1 = require("../domain/errors");
11
11
  let spawnImplementation = node_child_process_1.spawnSync;
12
+ function getCodexInvocation(args) {
13
+ if (process.platform === "win32") {
14
+ return {
15
+ command: process.env.ComSpec || "cmd.exe",
16
+ args: ["/d", "/s", "/c", ["codex", ...args].join(" ")],
17
+ };
18
+ }
19
+ return {
20
+ command: "codex",
21
+ args,
22
+ };
23
+ }
12
24
  /**
13
25
  * Overrides the spawn implementation for tests.
14
26
  */
@@ -25,7 +37,8 @@ function resetCodexSpawnImplementation() {
25
37
  * Runs `codex login --with-api-key` in the target Codex directory.
26
38
  */
27
39
  function runCodexLogin(apiKey, workingDir) {
28
- const result = spawnImplementation("codex", ["login", "--with-api-key"], {
40
+ const invocation = getCodexInvocation(["login", "--with-api-key"]);
41
+ const result = spawnImplementation(invocation.command, invocation.args, {
29
42
  cwd: workingDir,
30
43
  input: `${apiKey}\n`,
31
44
  stdio: "pipe",
@@ -41,7 +54,8 @@ function runCodexLogin(apiKey, workingDir) {
41
54
  * Checks whether the Codex CLI is available on PATH.
42
55
  */
43
56
  function checkCodexAvailable() {
44
- const result = spawnImplementation("codex", ["--version"], {
57
+ const invocation = getCodexInvocation(["--version"]);
58
+ const result = spawnImplementation(invocation.command, invocation.args, {
45
59
  stdio: "pipe",
46
60
  encoding: "utf8",
47
61
  });
@@ -57,7 +71,8 @@ function checkCodexAvailable() {
57
71
  * Reads the installed codex CLI version string.
58
72
  */
59
73
  function readCodexVersion() {
60
- const result = spawnImplementation("codex", ["--version"], {
74
+ const invocation = getCodexInvocation(["--version"]);
75
+ const result = spawnImplementation(invocation.command, invocation.args, {
61
76
  stdio: "pipe",
62
77
  encoding: "utf8",
63
78
  });
@@ -1,48 +1,10 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
2
  Object.defineProperty(exports, "__esModule", { value: true });
36
3
  exports.findCodexDirCandidates = findCodexDirCandidates;
37
- const fs = __importStar(require("node:fs"));
38
- const codex_paths_1 = require("./codex-paths");
4
+ const config_repo_1 = require("./config-repo");
39
5
  /**
40
- * Finds candidate Codex home directories. v0.0.4 only supports explicit and default locations.
6
+ * Finds candidate Codex home directories using the shared config-aware discovery rules.
41
7
  */
42
8
  function findCodexDirCandidates(explicitCodexDir) {
43
- if (explicitCodexDir) {
44
- return [(0, codex_paths_1.resolveCodexDir)(explicitCodexDir)];
45
- }
46
- const defaultDir = (0, codex_paths_1.resolveCodexDir)();
47
- return fs.existsSync(defaultDir) ? [defaultDir] : [];
9
+ return (0, config_repo_1.findCodexDirCandidates)(explicitCodexDir);
48
10
  }
@@ -39,7 +39,7 @@ exports.createCodexPaths = createCodexPaths;
39
39
  const os = __importStar(require("node:os"));
40
40
  const path = __importStar(require("node:path"));
41
41
  exports.CODEX_DIR_ENV_NAME = "CODEXS_CODEX_DIR";
42
- const DEVELOPMENT_DEFAULT_CODEX_DIR = path.resolve(process.cwd(), "test-fixtures", "sample-codex");
42
+ const DEVELOPMENT_DEFAULT_CODEX_DIR = path.resolve(process.cwd(), "dev-codex", "local-sandbox");
43
43
  /**
44
44
  * Resolves the working Codex directory, defaulting to `~/.codex`.
45
45
  */