@outcomeeng/spx 0.1.0

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.
package/dist/cli.js ADDED
@@ -0,0 +1,4023 @@
1
+ import {
2
+ LEAF_KIND,
3
+ parseWorkItemName
4
+ } from "./chunk-5L7CHFBC.js";
5
+
6
+ // src/cli.ts
7
+ import { Command } from "commander";
8
+
9
+ // src/commands/claude/init.ts
10
+ import { execa } from "execa";
11
+ async function initCommand(options = {}) {
12
+ const cwd = options.cwd || process.cwd();
13
+ try {
14
+ const { stdout: listOutput } = await execa(
15
+ "claude",
16
+ ["plugin", "marketplace", "list"],
17
+ { cwd }
18
+ );
19
+ const exists = listOutput.includes("spx-claude");
20
+ if (!exists) {
21
+ await execa(
22
+ "claude",
23
+ ["plugin", "marketplace", "add", "simonheimlicher/spx-claude"],
24
+ { cwd }
25
+ );
26
+ return "\u2713 spx-claude marketplace installed successfully\n\nRun 'claude plugin marketplace list' to view all marketplaces.";
27
+ } else {
28
+ await execa("claude", ["plugin", "marketplace", "update", "spx-claude"], {
29
+ cwd
30
+ });
31
+ return "\u2713 spx-claude marketplace updated successfully\n\nThe marketplace is now up to date.";
32
+ }
33
+ } catch (error) {
34
+ if (error instanceof Error) {
35
+ if (error.message.includes("ENOENT") || error.message.includes("command not found")) {
36
+ throw new Error(
37
+ "Claude CLI not found. Please install Claude Code first.\n\nVisit: https://docs.anthropic.com/claude-code"
38
+ );
39
+ }
40
+ throw new Error(`Failed to initialize marketplace: ${error.message}`);
41
+ }
42
+ throw error;
43
+ }
44
+ }
45
+
46
+ // src/commands/claude/settings/consolidate.ts
47
+ import os2 from "os";
48
+ import path4 from "path";
49
+
50
+ // src/lib/claude/permissions/discovery.ts
51
+ import fs from "fs/promises";
52
+ import path from "path";
53
+ async function findSettingsFiles(root, visited = /* @__PURE__ */ new Set()) {
54
+ const normalizedRoot = path.resolve(root.replace(/^~/, process.env.HOME || "~"));
55
+ if (visited.has(normalizedRoot)) {
56
+ return [];
57
+ }
58
+ visited.add(normalizedRoot);
59
+ try {
60
+ const stats = await fs.stat(normalizedRoot);
61
+ if (!stats.isDirectory()) {
62
+ throw new Error(`Path is not a directory: ${normalizedRoot}`);
63
+ }
64
+ const entries = await fs.readdir(normalizedRoot, { withFileTypes: true });
65
+ const results = [];
66
+ for (const entry of entries) {
67
+ const fullPath = path.join(normalizedRoot, entry.name);
68
+ if (entry.isDirectory() && entry.name === ".claude") {
69
+ const settingsPath = path.join(fullPath, "settings.local.json");
70
+ if (await isValidSettingsFile(settingsPath)) {
71
+ results.push(settingsPath);
72
+ }
73
+ }
74
+ if (entry.isDirectory() && entry.name !== ".claude") {
75
+ const subFiles = await findSettingsFiles(fullPath, visited);
76
+ results.push(...subFiles);
77
+ }
78
+ }
79
+ return results;
80
+ } catch (error) {
81
+ if (error instanceof Error) {
82
+ if (error.message.includes("ENOENT")) {
83
+ throw new Error(`Directory not found: ${normalizedRoot}`);
84
+ }
85
+ if (error.message.includes("EACCES")) {
86
+ throw new Error(`Permission denied: ${normalizedRoot}`);
87
+ }
88
+ throw new Error(
89
+ `Failed to search directory "${normalizedRoot}": ${error.message}`
90
+ );
91
+ }
92
+ throw error;
93
+ }
94
+ }
95
+ async function isValidSettingsFile(filePath) {
96
+ try {
97
+ await fs.access(filePath, fs.constants.R_OK);
98
+ const stats = await fs.stat(filePath);
99
+ if (!stats.isFile()) {
100
+ return false;
101
+ }
102
+ return path.extname(filePath) === ".json";
103
+ } catch {
104
+ return false;
105
+ }
106
+ }
107
+
108
+ // src/lib/claude/permissions/parser.ts
109
+ import fs2 from "fs/promises";
110
+ async function parseSettingsFile(filePath) {
111
+ try {
112
+ const content = await fs2.readFile(filePath, "utf-8");
113
+ const parsed = JSON.parse(content);
114
+ if (typeof parsed !== "object" || parsed === null) {
115
+ return null;
116
+ }
117
+ return parsed;
118
+ } catch {
119
+ return null;
120
+ }
121
+ }
122
+ function parsePermission(raw, category) {
123
+ const match = raw.match(/^([^(]+)\((.+)\)$/);
124
+ if (!match) {
125
+ throw new Error(`Malformed permission string: "${raw}"`);
126
+ }
127
+ const [, type, scope] = match;
128
+ return {
129
+ raw,
130
+ type: type.trim(),
131
+ scope: scope.trim(),
132
+ category
133
+ };
134
+ }
135
+ async function parseAllSettings(filePaths) {
136
+ const results = [];
137
+ for (const filePath of filePaths) {
138
+ const settings = await parseSettingsFile(filePath);
139
+ if (settings?.permissions) {
140
+ results.push(settings.permissions);
141
+ }
142
+ }
143
+ return results;
144
+ }
145
+
146
+ // src/scanner/walk.ts
147
+ import fs3 from "fs/promises";
148
+ import path2 from "path";
149
+ async function walkDirectory(root, visited = /* @__PURE__ */ new Set()) {
150
+ const normalizedRoot = path2.resolve(root);
151
+ if (visited.has(normalizedRoot)) {
152
+ return [];
153
+ }
154
+ visited.add(normalizedRoot);
155
+ try {
156
+ const entries = await fs3.readdir(normalizedRoot, { withFileTypes: true });
157
+ const results = [];
158
+ for (const entry of entries) {
159
+ const fullPath = path2.join(normalizedRoot, entry.name);
160
+ if (entry.isDirectory()) {
161
+ results.push({
162
+ name: entry.name,
163
+ path: fullPath,
164
+ isDirectory: true
165
+ });
166
+ const subEntries = await walkDirectory(fullPath, visited);
167
+ results.push(...subEntries);
168
+ }
169
+ }
170
+ return results;
171
+ } catch (error) {
172
+ if (error instanceof Error) {
173
+ throw new Error(
174
+ `Failed to walk directory "${normalizedRoot}": ${error.message}`
175
+ );
176
+ }
177
+ throw error;
178
+ }
179
+ }
180
+ function filterWorkItemDirectories(entries) {
181
+ return entries.filter((entry) => {
182
+ try {
183
+ parseWorkItemName(entry.name);
184
+ return true;
185
+ } catch {
186
+ return false;
187
+ }
188
+ });
189
+ }
190
+ function buildWorkItemList(entries) {
191
+ return entries.map((entry) => ({
192
+ ...parseWorkItemName(entry.name),
193
+ path: entry.path
194
+ }));
195
+ }
196
+ function normalizePath(filepath) {
197
+ return filepath.replace(/\\/g, "/");
198
+ }
199
+
200
+ // src/lib/claude/permissions/subsumption.ts
201
+ function parseScopePattern(scope) {
202
+ if (scope.includes("file_path:") || scope.includes("directory_path:") || scope.includes("path:")) {
203
+ const colonIndex = scope.indexOf(":");
204
+ const pattern = colonIndex >= 0 ? scope.substring(colonIndex + 1) : scope;
205
+ return { type: "path", pattern };
206
+ }
207
+ return { type: "command", pattern: scope };
208
+ }
209
+ function subsumes(broader, narrower) {
210
+ if (broader.type !== narrower.type) {
211
+ return false;
212
+ }
213
+ if (broader.scope === narrower.scope) {
214
+ return false;
215
+ }
216
+ const broaderScope = parseScopePattern(broader.scope);
217
+ const narrowerScope = parseScopePattern(narrower.scope);
218
+ if (broaderScope.type === "command" && narrowerScope.type === "command") {
219
+ const broaderBase = broaderScope.pattern.replace(/:?\*+$/, "");
220
+ const narrowerFull = narrowerScope.pattern.replace(/:?\*+$/, "");
221
+ if (narrowerFull.startsWith(broaderBase)) {
222
+ return narrowerFull.length > broaderBase.length;
223
+ }
224
+ }
225
+ if (broaderScope.type === "path" && narrowerScope.type === "path") {
226
+ const broaderPath = normalizePath(broaderScope.pattern.replace(/\/?\*+$/, ""));
227
+ const narrowerPath = normalizePath(narrowerScope.pattern.replace(/\/?\*+$/, ""));
228
+ return narrowerPath.startsWith(broaderPath + "/");
229
+ }
230
+ return false;
231
+ }
232
+ function detectSubsumptions(permissions) {
233
+ const results = [];
234
+ const processedBroader = /* @__PURE__ */ new Set();
235
+ for (let i = 0; i < permissions.length; i++) {
236
+ const broader = permissions[i];
237
+ if (processedBroader.has(broader.raw)) {
238
+ continue;
239
+ }
240
+ const narrowerPerms = [];
241
+ for (let j = 0; j < permissions.length; j++) {
242
+ if (i === j) continue;
243
+ const narrower = permissions[j];
244
+ if (subsumes(broader, narrower)) {
245
+ narrowerPerms.push(narrower);
246
+ }
247
+ }
248
+ if (narrowerPerms.length > 0) {
249
+ results.push({
250
+ broader,
251
+ narrower: narrowerPerms
252
+ });
253
+ processedBroader.add(broader.raw);
254
+ }
255
+ }
256
+ return results;
257
+ }
258
+ function removeSubsumed(permissionStrings, category) {
259
+ const permissions = permissionStrings.map((raw) => {
260
+ try {
261
+ return parsePermission(raw, category);
262
+ } catch {
263
+ return null;
264
+ }
265
+ }).filter((p) => p !== null);
266
+ const subsumptions = detectSubsumptions(permissions);
267
+ const toRemove = /* @__PURE__ */ new Set();
268
+ for (const result of subsumptions) {
269
+ for (const narrower of result.narrower) {
270
+ toRemove.add(narrower.raw);
271
+ }
272
+ }
273
+ return permissionStrings.filter((perm) => !toRemove.has(perm));
274
+ }
275
+
276
+ // src/lib/claude/permissions/merger.ts
277
+ function mergePermissions(global, local) {
278
+ const originalGlobal = {
279
+ allow: new Set(global.allow || []),
280
+ deny: new Set(global.deny || []),
281
+ ask: new Set(global.ask || [])
282
+ };
283
+ const combined = {
284
+ allow: [...global.allow || []],
285
+ deny: [...global.deny || []],
286
+ ask: [...global.ask || []]
287
+ };
288
+ let filesProcessed = 0;
289
+ let filesSkipped = 0;
290
+ for (const localPerms of local) {
291
+ let hasPerms = false;
292
+ if (localPerms.allow && localPerms.allow.length > 0) {
293
+ combined.allow?.push(...localPerms.allow);
294
+ hasPerms = true;
295
+ }
296
+ if (localPerms.deny && localPerms.deny.length > 0) {
297
+ combined.deny?.push(...localPerms.deny);
298
+ hasPerms = true;
299
+ }
300
+ if (localPerms.ask && localPerms.ask.length > 0) {
301
+ combined.ask?.push(...localPerms.ask);
302
+ hasPerms = true;
303
+ }
304
+ if (hasPerms) {
305
+ filesProcessed++;
306
+ } else {
307
+ filesSkipped++;
308
+ }
309
+ }
310
+ const allSubsumed = [];
311
+ const afterSubsumption = {};
312
+ if (combined.allow && combined.allow.length > 0) {
313
+ const before = new Set(combined.allow);
314
+ combined.allow = removeSubsumed(combined.allow, "allow");
315
+ const after = new Set(combined.allow);
316
+ for (const perm of before) {
317
+ if (!after.has(perm)) {
318
+ allSubsumed.push(perm);
319
+ }
320
+ }
321
+ }
322
+ if (combined.deny && combined.deny.length > 0) {
323
+ const before = new Set(combined.deny);
324
+ combined.deny = removeSubsumed(combined.deny, "deny");
325
+ const after = new Set(combined.deny);
326
+ for (const perm of before) {
327
+ if (!after.has(perm)) {
328
+ allSubsumed.push(perm);
329
+ }
330
+ }
331
+ }
332
+ if (combined.ask && combined.ask.length > 0) {
333
+ const before = new Set(combined.ask);
334
+ combined.ask = removeSubsumed(combined.ask, "ask");
335
+ const after = new Set(combined.ask);
336
+ for (const perm of before) {
337
+ if (!after.has(perm)) {
338
+ allSubsumed.push(perm);
339
+ }
340
+ }
341
+ }
342
+ afterSubsumption.allow = combined.allow;
343
+ afterSubsumption.deny = combined.deny;
344
+ afterSubsumption.ask = combined.ask;
345
+ const {
346
+ resolved,
347
+ conflictCount,
348
+ subsumed: conflictSubsumed
349
+ } = resolveConflicts(afterSubsumption);
350
+ const subsumed = [...allSubsumed, ...conflictSubsumed];
351
+ const merged = {};
352
+ if (resolved.allow && resolved.allow.length > 0) {
353
+ merged.allow = Array.from(new Set(resolved.allow)).sort();
354
+ }
355
+ if (resolved.deny && resolved.deny.length > 0) {
356
+ merged.deny = Array.from(new Set(resolved.deny)).sort();
357
+ }
358
+ if (resolved.ask && resolved.ask.length > 0) {
359
+ merged.ask = Array.from(new Set(resolved.ask)).sort();
360
+ }
361
+ const added = {
362
+ allow: [],
363
+ deny: [],
364
+ ask: []
365
+ };
366
+ for (const perm of merged.allow || []) {
367
+ if (!originalGlobal.allow.has(perm)) {
368
+ added.allow.push(perm);
369
+ }
370
+ }
371
+ for (const perm of merged.deny || []) {
372
+ if (!originalGlobal.deny.has(perm)) {
373
+ added.deny.push(perm);
374
+ }
375
+ }
376
+ for (const perm of merged.ask || []) {
377
+ if (!originalGlobal.ask.has(perm)) {
378
+ added.ask.push(perm);
379
+ }
380
+ }
381
+ const result = {
382
+ filesScanned: local.length,
383
+ filesProcessed,
384
+ filesSkipped,
385
+ added,
386
+ subsumed,
387
+ conflictsResolved: conflictCount
388
+ };
389
+ return { merged, result };
390
+ }
391
+ function resolveConflicts(permissions) {
392
+ const allow = permissions.allow || [];
393
+ const deny = permissions.deny || [];
394
+ const ask = permissions.ask || [];
395
+ const denySet = new Set(deny);
396
+ const subsumed = [];
397
+ let conflictCount = 0;
398
+ const allowToRemove = /* @__PURE__ */ new Set();
399
+ for (const allowPerm of allow) {
400
+ if (denySet.has(allowPerm)) {
401
+ allowToRemove.add(allowPerm);
402
+ conflictCount++;
403
+ continue;
404
+ }
405
+ for (const denyPerm of deny) {
406
+ try {
407
+ const allowParsed = parsePermission(allowPerm, "allow");
408
+ const denyParsed = parsePermission(denyPerm, "deny");
409
+ if (subsumes(denyParsed, allowParsed)) {
410
+ allowToRemove.add(allowPerm);
411
+ subsumed.push(allowPerm);
412
+ conflictCount++;
413
+ break;
414
+ }
415
+ } catch {
416
+ continue;
417
+ }
418
+ }
419
+ }
420
+ const resolved = {
421
+ allow: allow.filter((p) => !allowToRemove.has(p)),
422
+ deny,
423
+ ask
424
+ };
425
+ return { resolved, conflictCount, subsumed };
426
+ }
427
+
428
+ // src/lib/claude/settings/backup.ts
429
+ import fs4 from "fs/promises";
430
+ async function createBackup(settingsPath) {
431
+ try {
432
+ await fs4.access(settingsPath, fs4.constants.R_OK);
433
+ const now = /* @__PURE__ */ new Date();
434
+ const timestamp = [
435
+ now.getFullYear(),
436
+ String(now.getMonth() + 1).padStart(2, "0"),
437
+ String(now.getDate()).padStart(2, "0")
438
+ ].join("-") + "-" + [
439
+ String(now.getHours()).padStart(2, "0"),
440
+ String(now.getMinutes()).padStart(2, "0"),
441
+ String(now.getSeconds()).padStart(2, "0")
442
+ ].join("");
443
+ const backupPath = `${settingsPath}.backup.${timestamp}`;
444
+ await fs4.copyFile(settingsPath, backupPath);
445
+ return backupPath;
446
+ } catch (error) {
447
+ if (error instanceof Error) {
448
+ if (error.message.includes("ENOENT")) {
449
+ throw new Error(`Settings file not found: ${settingsPath}`);
450
+ }
451
+ if (error.message.includes("EACCES")) {
452
+ throw new Error(`Permission denied: ${settingsPath}`);
453
+ }
454
+ throw new Error(`Failed to create backup: ${error.message}`);
455
+ }
456
+ throw error;
457
+ }
458
+ }
459
+
460
+ // src/lib/claude/settings/reporter.ts
461
+ function formatReport(result, previewOnly, globalSettingsPath, outputFile) {
462
+ const lines = [];
463
+ lines.push("Scanning for Claude Code settings files...");
464
+ lines.push("");
465
+ lines.push(`Found ${result.filesScanned} settings files`);
466
+ lines.push(` Processed: ${result.filesProcessed}`);
467
+ if (result.filesSkipped > 0) {
468
+ lines.push(` Skipped: ${result.filesSkipped} (no permissions)`);
469
+ }
470
+ lines.push("");
471
+ const totalAdded = result.added.allow.length + result.added.deny.length + result.added.ask.length;
472
+ if (totalAdded > 0) {
473
+ lines.push(`Permissions to add: ${totalAdded}`);
474
+ if (result.added.allow.length > 0) {
475
+ lines.push("");
476
+ lines.push(" allow:");
477
+ for (const perm of result.added.allow) {
478
+ lines.push(` + ${perm}`);
479
+ }
480
+ }
481
+ if (result.added.deny.length > 0) {
482
+ lines.push("");
483
+ lines.push(" deny:");
484
+ for (const perm of result.added.deny) {
485
+ lines.push(` + ${perm}`);
486
+ }
487
+ }
488
+ if (result.added.ask.length > 0) {
489
+ lines.push("");
490
+ lines.push(" ask:");
491
+ for (const perm of result.added.ask) {
492
+ lines.push(` + ${perm}`);
493
+ }
494
+ }
495
+ } else {
496
+ lines.push("No new permissions to add (all permissions already in global settings)");
497
+ }
498
+ lines.push("");
499
+ if (result.subsumed.length > 0) {
500
+ lines.push(`Subsumed permissions removed: ${result.subsumed.length}`);
501
+ lines.push(" (narrower permissions replaced by broader ones)");
502
+ for (const perm of result.subsumed) {
503
+ lines.push(` - ${perm}`);
504
+ }
505
+ lines.push("");
506
+ }
507
+ if (result.conflictsResolved > 0) {
508
+ lines.push(`Conflicts resolved: ${result.conflictsResolved}`);
509
+ lines.push(" (permissions moved from allow to deny)");
510
+ lines.push("");
511
+ }
512
+ if (result.backupPath) {
513
+ lines.push(`Backup created: ${result.backupPath}`);
514
+ lines.push("");
515
+ }
516
+ lines.push("Summary:");
517
+ lines.push(` Files scanned: ${result.filesScanned}`);
518
+ lines.push(
519
+ ` Permissions added: ${result.added.allow.length} allow, ${result.added.deny.length} deny, ${result.added.ask.length} ask`
520
+ );
521
+ if (result.subsumed.length > 0) {
522
+ lines.push(` Subsumed removed: ${result.subsumed.length}`);
523
+ }
524
+ if (result.conflictsResolved > 0) {
525
+ lines.push(` Conflicts resolved: ${result.conflictsResolved}`);
526
+ }
527
+ lines.push("");
528
+ if (previewOnly) {
529
+ lines.push("\u2139\uFE0F Preview mode: No changes written");
530
+ lines.push("");
531
+ lines.push("To apply changes:");
532
+ lines.push(` \u2022 Modify global settings: spx claude settings consolidate --write`);
533
+ lines.push(` \u2022 Write to file: spx claude settings consolidate --output-file /path/to/file`);
534
+ } else if (outputFile) {
535
+ lines.push(`\u2713 Settings written to: ${result.outputPath || outputFile}`);
536
+ lines.push("");
537
+ lines.push("To apply to your global settings:");
538
+ lines.push(` \u2022 Review the file, then copy to: ${globalSettingsPath || "~/.claude/settings.json"}`);
539
+ lines.push(` \u2022 Or run: spx claude settings consolidate --write`);
540
+ } else {
541
+ lines.push(`\u2713 Global settings updated: ${globalSettingsPath || "~/.claude/settings.json"}`);
542
+ }
543
+ return lines.join("\n");
544
+ }
545
+
546
+ // src/lib/claude/settings/writer.ts
547
+ import fs5 from "fs/promises";
548
+ import os from "os";
549
+ import path3 from "path";
550
+ var realFs = {
551
+ writeFile: (path8, content) => fs5.writeFile(path8, content, "utf-8"),
552
+ rename: fs5.rename,
553
+ unlink: fs5.unlink,
554
+ mkdir: async (path8, options) => {
555
+ await fs5.mkdir(path8, options);
556
+ }
557
+ };
558
+ async function writeSettings(filePath, settings, deps = { fs: realFs }) {
559
+ const dir = path3.dirname(filePath);
560
+ await deps.fs.mkdir(dir, { recursive: true });
561
+ const tempPath = path3.join(
562
+ os.tmpdir(),
563
+ `settings-${Date.now()}-${Math.random().toString(36).substring(7)}.json`
564
+ );
565
+ try {
566
+ const content = JSON.stringify(settings, null, 2) + "\n";
567
+ await deps.fs.writeFile(tempPath, content);
568
+ await deps.fs.rename(tempPath, filePath);
569
+ } catch (error) {
570
+ try {
571
+ await deps.fs.unlink(tempPath);
572
+ } catch {
573
+ }
574
+ if (error instanceof Error) {
575
+ throw new Error(`Failed to write settings: ${error.message}`);
576
+ }
577
+ throw error;
578
+ }
579
+ }
580
+
581
+ // src/commands/claude/settings/consolidate.ts
582
+ async function consolidateCommand(options = {}) {
583
+ const root = options.root ? path4.resolve(options.root.replace(/^~/, os2.homedir())) : path4.join(os2.homedir(), "Code");
584
+ const globalSettingsPath = options.globalSettings || path4.join(os2.homedir(), ".claude", "settings.json");
585
+ const shouldWrite = options.write || false;
586
+ const outputFile = options.outputFile;
587
+ const previewOnly = !shouldWrite && !outputFile;
588
+ const settingsFiles = await findSettingsFiles(root);
589
+ if (settingsFiles.length === 0) {
590
+ return `No settings files found in ${root}
591
+
592
+ Searched for: **/.claude/settings.local.json`;
593
+ }
594
+ const localPermissions = await parseAllSettings(settingsFiles);
595
+ let globalSettings = await parseSettingsFile(globalSettingsPath);
596
+ if (!globalSettings) {
597
+ globalSettings = {
598
+ permissions: {
599
+ allow: [],
600
+ deny: [],
601
+ ask: []
602
+ }
603
+ };
604
+ }
605
+ if (!globalSettings.permissions) {
606
+ globalSettings.permissions = {
607
+ allow: [],
608
+ deny: [],
609
+ ask: []
610
+ };
611
+ }
612
+ const { merged, result } = mergePermissions(
613
+ globalSettings.permissions,
614
+ localPermissions
615
+ );
616
+ if (shouldWrite) {
617
+ try {
618
+ result.backupPath = await createBackup(globalSettingsPath);
619
+ } catch (error) {
620
+ if (error instanceof Error && !error.message.includes("not found")) {
621
+ throw error;
622
+ }
623
+ }
624
+ }
625
+ if (shouldWrite) {
626
+ const updatedSettings = {
627
+ ...globalSettings,
628
+ permissions: merged
629
+ };
630
+ await writeSettings(globalSettingsPath, updatedSettings);
631
+ } else if (outputFile) {
632
+ const updatedSettings = {
633
+ ...globalSettings,
634
+ permissions: merged
635
+ };
636
+ const resolvedOutputPath = path4.resolve(outputFile.replace(/^~/, os2.homedir()));
637
+ await writeSettings(resolvedOutputPath, updatedSettings);
638
+ result.outputPath = resolvedOutputPath;
639
+ }
640
+ return formatReport(result, previewOnly, globalSettingsPath, outputFile);
641
+ }
642
+
643
+ // src/domains/claude/index.ts
644
+ function registerClaudeCommands(claudeCmd) {
645
+ claudeCmd.command("init").description("Initialize or update spx-claude marketplace plugin").action(async () => {
646
+ try {
647
+ const output = await initCommand({ cwd: process.cwd() });
648
+ console.log(output);
649
+ } catch (error) {
650
+ console.error(
651
+ "Error:",
652
+ error instanceof Error ? error.message : String(error)
653
+ );
654
+ process.exit(1);
655
+ }
656
+ });
657
+ const settingsCmd = claudeCmd.command("settings").description("Manage Claude Code settings");
658
+ settingsCmd.command("consolidate").description(
659
+ "Consolidate permissions from project-specific settings into global settings"
660
+ ).option("--write", "Write changes to global settings file (default: preview only)").option(
661
+ "--output-file <path>",
662
+ "Write merged settings to specified file instead of global settings"
663
+ ).option(
664
+ "--root <path>",
665
+ "Root directory to scan for settings files (default: ~/Code)"
666
+ ).option(
667
+ "--global-settings <path>",
668
+ "Path to global settings file (default: ~/.claude/settings.json)"
669
+ ).action(
670
+ async (options) => {
671
+ try {
672
+ if (options.write && options.outputFile) {
673
+ console.error(
674
+ "Error: --write and --output-file are mutually exclusive\nUse --write to modify global settings, or --output-file to write to a different location"
675
+ );
676
+ process.exit(1);
677
+ }
678
+ const output = await consolidateCommand({
679
+ write: options.write,
680
+ outputFile: options.outputFile,
681
+ root: options.root,
682
+ globalSettings: options.globalSettings
683
+ });
684
+ console.log(output);
685
+ } catch (error) {
686
+ console.error(
687
+ "Error:",
688
+ error instanceof Error ? error.message : String(error)
689
+ );
690
+ process.exit(1);
691
+ }
692
+ }
693
+ );
694
+ }
695
+ var claudeDomain = {
696
+ name: "claude",
697
+ description: "Manage Claude Code settings and plugins",
698
+ register: (program2) => {
699
+ const claudeCmd = program2.command("claude").description("Manage Claude Code settings and plugins");
700
+ registerClaudeCommands(claudeCmd);
701
+ }
702
+ };
703
+
704
+ // src/commands/session/archive.ts
705
+ import { mkdir, rename, stat } from "fs/promises";
706
+ import { dirname, join as join2 } from "path";
707
+
708
+ // src/session/errors.ts
709
+ var SessionError = class extends Error {
710
+ constructor(message) {
711
+ super(message);
712
+ this.name = "SessionError";
713
+ }
714
+ };
715
+ var SessionNotFoundError = class extends SessionError {
716
+ /** The session ID that was not found */
717
+ sessionId;
718
+ constructor(sessionId) {
719
+ super(`Session not found: ${sessionId}. Check the session ID and try again.`);
720
+ this.name = "SessionNotFoundError";
721
+ this.sessionId = sessionId;
722
+ }
723
+ };
724
+ var SessionNotAvailableError = class extends SessionError {
725
+ /** The session ID that is not available */
726
+ sessionId;
727
+ constructor(sessionId) {
728
+ super(`Session not available: ${sessionId}. It may have been claimed by another agent.`);
729
+ this.name = "SessionNotAvailableError";
730
+ this.sessionId = sessionId;
731
+ }
732
+ };
733
+ var SessionInvalidContentError = class extends SessionError {
734
+ constructor(reason) {
735
+ super(`Invalid session content: ${reason}`);
736
+ this.name = "SessionInvalidContentError";
737
+ }
738
+ };
739
+ var SessionNotClaimedError = class extends SessionError {
740
+ /** The session ID that is not claimed */
741
+ sessionId;
742
+ constructor(sessionId) {
743
+ super(`Session not claimed: ${sessionId}. The session is not in the doing directory.`);
744
+ this.name = "SessionNotClaimedError";
745
+ this.sessionId = sessionId;
746
+ }
747
+ };
748
+ var NoSessionsAvailableError = class extends SessionError {
749
+ constructor() {
750
+ super("No sessions available. The todo directory is empty.");
751
+ this.name = "NoSessionsAvailableError";
752
+ }
753
+ };
754
+
755
+ // src/session/show.ts
756
+ import { join } from "path";
757
+
758
+ // src/config/defaults.ts
759
+ var DEFAULT_CONFIG = {
760
+ specs: {
761
+ root: "specs",
762
+ work: {
763
+ dir: "work",
764
+ statusDirs: {
765
+ doing: "doing",
766
+ backlog: "backlog",
767
+ done: "archive"
768
+ }
769
+ },
770
+ decisions: "decisions",
771
+ templates: "templates"
772
+ },
773
+ sessions: {
774
+ dir: ".spx/sessions",
775
+ statusDirs: {
776
+ todo: "todo",
777
+ doing: "doing",
778
+ archive: "archive"
779
+ }
780
+ }
781
+ };
782
+
783
+ // src/session/list.ts
784
+ import { parse as parseYaml } from "yaml";
785
+
786
+ // src/session/timestamp.ts
787
+ var SESSION_ID_PATTERN = /^(\d{4})-(\d{2})-(\d{2})_(\d{2})-(\d{2})-(\d{2})$/;
788
+ var SESSION_ID_SEPARATOR = "_";
789
+ function generateSessionId(options = {}) {
790
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
791
+ const date = now();
792
+ const year = date.getFullYear();
793
+ const month = String(date.getMonth() + 1).padStart(2, "0");
794
+ const day = String(date.getDate()).padStart(2, "0");
795
+ const hours = String(date.getHours()).padStart(2, "0");
796
+ const minutes = String(date.getMinutes()).padStart(2, "0");
797
+ const seconds = String(date.getSeconds()).padStart(2, "0");
798
+ return `${year}-${month}-${day}${SESSION_ID_SEPARATOR}${hours}-${minutes}-${seconds}`;
799
+ }
800
+ function parseSessionId(id) {
801
+ const match = SESSION_ID_PATTERN.exec(id);
802
+ if (!match) {
803
+ return null;
804
+ }
805
+ const [, yearStr, monthStr, dayStr, hoursStr, minutesStr, secondsStr] = match;
806
+ const year = parseInt(yearStr, 10);
807
+ const month = parseInt(monthStr, 10) - 1;
808
+ const day = parseInt(dayStr, 10);
809
+ const hours = parseInt(hoursStr, 10);
810
+ const minutes = parseInt(minutesStr, 10);
811
+ const seconds = parseInt(secondsStr, 10);
812
+ if (month < 0 || month > 11) return null;
813
+ if (day < 1 || day > 31) return null;
814
+ if (hours < 0 || hours > 23) return null;
815
+ if (minutes < 0 || minutes > 59) return null;
816
+ if (seconds < 0 || seconds > 59) return null;
817
+ return new Date(year, month, day, hours, minutes, seconds);
818
+ }
819
+
820
+ // src/session/types.ts
821
+ var PRIORITY_ORDER = {
822
+ high: 0,
823
+ medium: 1,
824
+ low: 2
825
+ };
826
+ var DEFAULT_PRIORITY = "medium";
827
+
828
+ // src/session/list.ts
829
+ var FRONT_MATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n(?:---|\.\.\.)\r?\n?/;
830
+ function isValidPriority(value) {
831
+ return value === "high" || value === "medium" || value === "low";
832
+ }
833
+ function parseSessionMetadata(content) {
834
+ const match = FRONT_MATTER_PATTERN.exec(content);
835
+ if (!match) {
836
+ return {
837
+ priority: DEFAULT_PRIORITY,
838
+ tags: []
839
+ };
840
+ }
841
+ try {
842
+ const parsed = parseYaml(match[1]);
843
+ if (!parsed || typeof parsed !== "object") {
844
+ return {
845
+ priority: DEFAULT_PRIORITY,
846
+ tags: []
847
+ };
848
+ }
849
+ const priority = isValidPriority(parsed.priority) ? parsed.priority : DEFAULT_PRIORITY;
850
+ let tags = [];
851
+ if (Array.isArray(parsed.tags)) {
852
+ tags = parsed.tags.filter((t) => typeof t === "string");
853
+ }
854
+ const metadata = {
855
+ priority,
856
+ tags
857
+ };
858
+ if (typeof parsed.id === "string") {
859
+ metadata.id = parsed.id;
860
+ }
861
+ if (typeof parsed.branch === "string") {
862
+ metadata.branch = parsed.branch;
863
+ }
864
+ if (typeof parsed.created_at === "string") {
865
+ metadata.createdAt = parsed.created_at;
866
+ }
867
+ if (typeof parsed.working_directory === "string") {
868
+ metadata.workingDirectory = parsed.working_directory;
869
+ }
870
+ if (Array.isArray(parsed.specs)) {
871
+ metadata.specs = parsed.specs.filter(
872
+ (s) => typeof s === "string"
873
+ );
874
+ }
875
+ if (Array.isArray(parsed.files)) {
876
+ metadata.files = parsed.files.filter(
877
+ (f) => typeof f === "string"
878
+ );
879
+ }
880
+ return metadata;
881
+ } catch {
882
+ return {
883
+ priority: DEFAULT_PRIORITY,
884
+ tags: []
885
+ };
886
+ }
887
+ }
888
+ function sortSessions(sessions) {
889
+ return [...sessions].sort((a, b) => {
890
+ const priorityA = PRIORITY_ORDER[a.metadata.priority];
891
+ const priorityB = PRIORITY_ORDER[b.metadata.priority];
892
+ if (priorityA !== priorityB) {
893
+ return priorityA - priorityB;
894
+ }
895
+ const dateA = parseSessionId(a.id);
896
+ const dateB = parseSessionId(b.id);
897
+ if (!dateA && !dateB) return 0;
898
+ if (!dateA) return 1;
899
+ if (!dateB) return -1;
900
+ return dateB.getTime() - dateA.getTime();
901
+ });
902
+ }
903
+
904
+ // src/session/show.ts
905
+ var { dir: sessionsBaseDir, statusDirs } = DEFAULT_CONFIG.sessions;
906
+ var DEFAULT_SESSION_CONFIG = {
907
+ todoDir: join(sessionsBaseDir, statusDirs.todo),
908
+ doingDir: join(sessionsBaseDir, statusDirs.doing),
909
+ archiveDir: join(sessionsBaseDir, statusDirs.archive)
910
+ };
911
+ var SEARCH_ORDER = ["todo", "doing", "archive"];
912
+ function resolveSessionPaths(id, config = DEFAULT_SESSION_CONFIG) {
913
+ const filename = `${id}.md`;
914
+ return [
915
+ `${config.todoDir}/${filename}`,
916
+ `${config.doingDir}/${filename}`,
917
+ `${config.archiveDir}/${filename}`
918
+ ];
919
+ }
920
+ function formatShowOutput(content, options) {
921
+ const metadata = parseSessionMetadata(content);
922
+ const headerLines = [
923
+ `Status: ${options.status}`,
924
+ `Priority: ${metadata.priority}`
925
+ ];
926
+ if (metadata.id) {
927
+ headerLines.unshift(`ID: ${metadata.id}`);
928
+ }
929
+ if (metadata.branch) {
930
+ headerLines.push(`Branch: ${metadata.branch}`);
931
+ }
932
+ if (metadata.tags.length > 0) {
933
+ headerLines.push(`Tags: ${metadata.tags.join(", ")}`);
934
+ }
935
+ if (metadata.createdAt) {
936
+ headerLines.push(`Created: ${metadata.createdAt}`);
937
+ }
938
+ const header = headerLines.join("\n");
939
+ const separator = "\n" + "\u2500".repeat(40) + "\n\n";
940
+ return header + separator + content;
941
+ }
942
+
943
+ // src/commands/session/archive.ts
944
+ var SessionAlreadyArchivedError = class extends Error {
945
+ /** The session ID that is already archived */
946
+ sessionId;
947
+ constructor(sessionId) {
948
+ super(`Session already archived: ${sessionId}.`);
949
+ this.name = "SessionAlreadyArchivedError";
950
+ this.sessionId = sessionId;
951
+ }
952
+ };
953
+ async function resolveArchivePaths(sessionId, config) {
954
+ const filename = `${sessionId}.md`;
955
+ const todoPath = join2(config.todoDir, filename);
956
+ const doingPath = join2(config.doingDir, filename);
957
+ const archivePath = join2(config.archiveDir, filename);
958
+ try {
959
+ const archiveStats = await stat(archivePath);
960
+ if (archiveStats.isFile()) {
961
+ throw new SessionAlreadyArchivedError(sessionId);
962
+ }
963
+ } catch (error) {
964
+ if (error instanceof Error && "code" in error && error.code !== "ENOENT") {
965
+ throw error;
966
+ }
967
+ if (error instanceof SessionAlreadyArchivedError) {
968
+ throw error;
969
+ }
970
+ }
971
+ try {
972
+ const todoStats = await stat(todoPath);
973
+ if (todoStats.isFile()) {
974
+ return { source: todoPath, target: archivePath };
975
+ }
976
+ } catch {
977
+ }
978
+ try {
979
+ const doingStats = await stat(doingPath);
980
+ if (doingStats.isFile()) {
981
+ return { source: doingPath, target: archivePath };
982
+ }
983
+ } catch {
984
+ }
985
+ throw new SessionNotFoundError(sessionId);
986
+ }
987
+ async function archiveCommand(options) {
988
+ const config = options.sessionsDir ? {
989
+ todoDir: join2(options.sessionsDir, "todo"),
990
+ doingDir: join2(options.sessionsDir, "doing"),
991
+ archiveDir: join2(options.sessionsDir, "archive")
992
+ } : DEFAULT_SESSION_CONFIG;
993
+ const { source, target } = await resolveArchivePaths(options.sessionId, config);
994
+ await mkdir(dirname(target), { recursive: true });
995
+ await rename(source, target);
996
+ return `Archived session: ${options.sessionId}
997
+ Archive location: ${target}`;
998
+ }
999
+
1000
+ // src/commands/session/delete.ts
1001
+ import { stat as stat2, unlink } from "fs/promises";
1002
+ import { join as join3 } from "path";
1003
+
1004
+ // src/session/delete.ts
1005
+ function resolveDeletePath(sessionId, existingPaths) {
1006
+ const matchingPath = existingPaths.find((path8) => path8.includes(sessionId));
1007
+ if (!matchingPath) {
1008
+ throw new SessionNotFoundError(sessionId);
1009
+ }
1010
+ return matchingPath;
1011
+ }
1012
+
1013
+ // src/commands/session/delete.ts
1014
+ async function findExistingPaths(paths) {
1015
+ const existing = [];
1016
+ for (const path8 of paths) {
1017
+ try {
1018
+ const stats = await stat2(path8);
1019
+ if (stats.isFile()) {
1020
+ existing.push(path8);
1021
+ }
1022
+ } catch {
1023
+ }
1024
+ }
1025
+ return existing;
1026
+ }
1027
+ async function deleteCommand(options) {
1028
+ const config = options.sessionsDir ? {
1029
+ todoDir: join3(options.sessionsDir, "todo"),
1030
+ doingDir: join3(options.sessionsDir, "doing"),
1031
+ archiveDir: join3(options.sessionsDir, "archive")
1032
+ } : DEFAULT_SESSION_CONFIG;
1033
+ const paths = resolveSessionPaths(options.sessionId, config);
1034
+ const existingPaths = await findExistingPaths(paths);
1035
+ const pathToDelete = resolveDeletePath(options.sessionId, existingPaths);
1036
+ await unlink(pathToDelete);
1037
+ return `Deleted session: ${options.sessionId}`;
1038
+ }
1039
+
1040
+ // src/commands/session/handoff.ts
1041
+ import { mkdir as mkdir2, writeFile } from "fs/promises";
1042
+ import { join as join5, resolve } from "path";
1043
+
1044
+ // src/git/root.ts
1045
+ import { execa as execa2 } from "execa";
1046
+ import { join as join4 } from "path";
1047
+ var defaultDeps = {
1048
+ execa: (command, args, options) => execa2(command, args, options)
1049
+ };
1050
+ var NOT_GIT_REPO_WARNING = "Warning: Not in a git repository. Sessions will be created relative to current directory.";
1051
+ async function detectGitRoot(cwd = process.cwd(), deps = defaultDeps) {
1052
+ try {
1053
+ const result = await deps.execa(
1054
+ "git",
1055
+ ["rev-parse", "--show-toplevel"],
1056
+ { cwd, reject: false }
1057
+ );
1058
+ if (result.exitCode === 0 && result.stdout) {
1059
+ const stdout = typeof result.stdout === "string" ? result.stdout : result.stdout.toString();
1060
+ const gitRoot = stdout.trim().replace(/\/+$/, "");
1061
+ return {
1062
+ root: gitRoot,
1063
+ isGitRepo: true
1064
+ };
1065
+ }
1066
+ return {
1067
+ root: cwd,
1068
+ isGitRepo: false,
1069
+ warning: NOT_GIT_REPO_WARNING
1070
+ };
1071
+ } catch {
1072
+ return {
1073
+ root: cwd,
1074
+ isGitRepo: false,
1075
+ warning: NOT_GIT_REPO_WARNING
1076
+ };
1077
+ }
1078
+ }
1079
+ function buildSessionPathFromRoot(gitRoot, sessionId, config) {
1080
+ const filename = `${sessionId}.md`;
1081
+ return join4(gitRoot, config.todoDir, filename);
1082
+ }
1083
+
1084
+ // src/session/create.ts
1085
+ var MIN_CONTENT_LENGTH = 1;
1086
+ function validateSessionContent(content) {
1087
+ if (!content || content.trim().length < MIN_CONTENT_LENGTH) {
1088
+ return {
1089
+ valid: false,
1090
+ error: "Session content cannot be empty"
1091
+ };
1092
+ }
1093
+ if (content.trim().length === 0) {
1094
+ return {
1095
+ valid: false,
1096
+ error: "Session content cannot be only whitespace"
1097
+ };
1098
+ }
1099
+ return { valid: true };
1100
+ }
1101
+
1102
+ // src/commands/session/handoff.ts
1103
+ var { statusDirs: statusDirs2 } = DEFAULT_CONFIG.sessions;
1104
+ var FRONT_MATTER_START = /^---\r?\n/;
1105
+ function hasFrontmatter(content) {
1106
+ return FRONT_MATTER_START.test(content);
1107
+ }
1108
+ function buildSessionContent(content) {
1109
+ if (!content || content.trim().length === 0) {
1110
+ return `---
1111
+ priority: medium
1112
+ ---
1113
+
1114
+ # New Session
1115
+
1116
+ Describe your task here.`;
1117
+ }
1118
+ if (hasFrontmatter(content)) {
1119
+ return content;
1120
+ }
1121
+ return `---
1122
+ priority: medium
1123
+ ---
1124
+
1125
+ ${content}`;
1126
+ }
1127
+ async function handoffCommand(options) {
1128
+ const config = options.sessionsDir ? {
1129
+ todoDir: join5(options.sessionsDir, statusDirs2.todo),
1130
+ doingDir: join5(options.sessionsDir, statusDirs2.doing),
1131
+ archiveDir: join5(options.sessionsDir, statusDirs2.archive)
1132
+ } : DEFAULT_SESSION_CONFIG;
1133
+ let baseDir;
1134
+ let warningMessage;
1135
+ if (options.sessionsDir) {
1136
+ baseDir = options.sessionsDir;
1137
+ } else {
1138
+ const gitResult = await detectGitRoot();
1139
+ baseDir = gitResult.root;
1140
+ warningMessage = gitResult.warning;
1141
+ }
1142
+ const sessionId = generateSessionId();
1143
+ const fullContent = buildSessionContent(options.content);
1144
+ const validation = validateSessionContent(fullContent);
1145
+ if (!validation.valid) {
1146
+ throw new SessionInvalidContentError(validation.error ?? "Unknown validation error");
1147
+ }
1148
+ const filename = `${sessionId}.md`;
1149
+ const sessionPath = options.sessionsDir ? join5(config.todoDir, filename) : buildSessionPathFromRoot(baseDir, sessionId, config);
1150
+ const absolutePath = resolve(sessionPath);
1151
+ const todoDir = options.sessionsDir ? config.todoDir : join5(baseDir, config.todoDir);
1152
+ await mkdir2(todoDir, { recursive: true });
1153
+ await writeFile(sessionPath, fullContent, "utf-8");
1154
+ let output = `Created handoff session <HANDOFF_ID>${sessionId}</HANDOFF_ID>
1155
+ <SESSION_FILE>${absolutePath}</SESSION_FILE>`;
1156
+ if (warningMessage) {
1157
+ process.stderr.write(`${warningMessage}
1158
+ `);
1159
+ }
1160
+ return output;
1161
+ }
1162
+
1163
+ // src/commands/session/list.ts
1164
+ import { readdir, readFile } from "fs/promises";
1165
+ import { join as join6 } from "path";
1166
+ async function loadSessionsFromDir(dir, status) {
1167
+ try {
1168
+ const files = await readdir(dir);
1169
+ const sessions = [];
1170
+ for (const file of files) {
1171
+ if (!file.endsWith(".md")) continue;
1172
+ const id = file.replace(".md", "");
1173
+ const filePath = join6(dir, file);
1174
+ const content = await readFile(filePath, "utf-8");
1175
+ const metadata = parseSessionMetadata(content);
1176
+ sessions.push({
1177
+ id,
1178
+ status,
1179
+ path: filePath,
1180
+ metadata
1181
+ });
1182
+ }
1183
+ return sessions;
1184
+ } catch (error) {
1185
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
1186
+ return [];
1187
+ }
1188
+ throw error;
1189
+ }
1190
+ }
1191
+ function formatTextOutput(sessions, _status) {
1192
+ if (sessions.length === 0) {
1193
+ return ` (no sessions)`;
1194
+ }
1195
+ return sessions.map((s) => {
1196
+ const priority = s.metadata.priority !== "medium" ? ` [${s.metadata.priority}]` : "";
1197
+ const tags = s.metadata.tags.length > 0 ? ` (${s.metadata.tags.join(", ")})` : "";
1198
+ return ` ${s.id}${priority}${tags}`;
1199
+ }).join("\n");
1200
+ }
1201
+ async function listCommand(options) {
1202
+ const config = options.sessionsDir ? {
1203
+ todoDir: join6(options.sessionsDir, "todo"),
1204
+ doingDir: join6(options.sessionsDir, "doing"),
1205
+ archiveDir: join6(options.sessionsDir, "archive")
1206
+ } : DEFAULT_SESSION_CONFIG;
1207
+ const statuses = options.status ? [options.status] : ["todo", "doing", "archive"];
1208
+ const allSessions = {
1209
+ todo: [],
1210
+ doing: [],
1211
+ archive: []
1212
+ };
1213
+ for (const status of statuses) {
1214
+ const dir = status === "todo" ? config.todoDir : status === "doing" ? config.doingDir : config.archiveDir;
1215
+ const sessions = await loadSessionsFromDir(dir, status);
1216
+ allSessions[status] = sortSessions(sessions);
1217
+ }
1218
+ if (options.format === "json") {
1219
+ return JSON.stringify(allSessions, null, 2);
1220
+ }
1221
+ const lines = [];
1222
+ if (statuses.includes("doing")) {
1223
+ lines.push("DOING:");
1224
+ lines.push(formatTextOutput(allSessions.doing, "doing"));
1225
+ lines.push("");
1226
+ }
1227
+ if (statuses.includes("todo")) {
1228
+ lines.push("TODO:");
1229
+ lines.push(formatTextOutput(allSessions.todo, "todo"));
1230
+ lines.push("");
1231
+ }
1232
+ if (statuses.includes("archive")) {
1233
+ lines.push("ARCHIVE:");
1234
+ lines.push(formatTextOutput(allSessions.archive, "archive"));
1235
+ }
1236
+ return lines.join("\n").trim();
1237
+ }
1238
+
1239
+ // src/commands/session/pickup.ts
1240
+ import { mkdir as mkdir3, readdir as readdir2, readFile as readFile2, rename as rename2 } from "fs/promises";
1241
+ import { join as join7 } from "path";
1242
+
1243
+ // src/session/pickup.ts
1244
+ function buildClaimPaths(sessionId, config) {
1245
+ return {
1246
+ source: `${config.todoDir}/${sessionId}.md`,
1247
+ target: `${config.doingDir}/${sessionId}.md`
1248
+ };
1249
+ }
1250
+ function classifyClaimError(error, sessionId) {
1251
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
1252
+ return new SessionNotAvailableError(sessionId);
1253
+ }
1254
+ throw error;
1255
+ }
1256
+ function selectBestSession(sessions) {
1257
+ if (sessions.length === 0) {
1258
+ return null;
1259
+ }
1260
+ const sorted = [...sessions].sort((a, b) => {
1261
+ const priorityA = PRIORITY_ORDER[a.metadata.priority];
1262
+ const priorityB = PRIORITY_ORDER[b.metadata.priority];
1263
+ if (priorityA !== priorityB) {
1264
+ return priorityA - priorityB;
1265
+ }
1266
+ const dateA = parseSessionId(a.id);
1267
+ const dateB = parseSessionId(b.id);
1268
+ if (!dateA && !dateB) return 0;
1269
+ if (!dateA) return 1;
1270
+ if (!dateB) return -1;
1271
+ return dateA.getTime() - dateB.getTime();
1272
+ });
1273
+ return sorted[0];
1274
+ }
1275
+
1276
+ // src/commands/session/pickup.ts
1277
+ async function loadTodoSessions(config) {
1278
+ try {
1279
+ const files = await readdir2(config.todoDir);
1280
+ const sessions = [];
1281
+ for (const file of files) {
1282
+ if (!file.endsWith(".md")) continue;
1283
+ const id = file.replace(".md", "");
1284
+ const filePath = join7(config.todoDir, file);
1285
+ const content = await readFile2(filePath, "utf-8");
1286
+ const metadata = parseSessionMetadata(content);
1287
+ sessions.push({
1288
+ id,
1289
+ status: "todo",
1290
+ path: filePath,
1291
+ metadata
1292
+ });
1293
+ }
1294
+ return sessions;
1295
+ } catch (error) {
1296
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
1297
+ return [];
1298
+ }
1299
+ throw error;
1300
+ }
1301
+ }
1302
+ async function pickupCommand(options) {
1303
+ const config = options.sessionsDir ? {
1304
+ todoDir: join7(options.sessionsDir, "todo"),
1305
+ doingDir: join7(options.sessionsDir, "doing"),
1306
+ archiveDir: join7(options.sessionsDir, "archive")
1307
+ } : DEFAULT_SESSION_CONFIG;
1308
+ let sessionId;
1309
+ if (options.auto) {
1310
+ const sessions = await loadTodoSessions(config);
1311
+ const selected = selectBestSession(sessions);
1312
+ if (!selected) {
1313
+ throw new NoSessionsAvailableError();
1314
+ }
1315
+ sessionId = selected.id;
1316
+ } else if (options.sessionId) {
1317
+ sessionId = options.sessionId;
1318
+ } else {
1319
+ throw new Error("Either session ID or --auto flag is required");
1320
+ }
1321
+ const paths = buildClaimPaths(sessionId, config);
1322
+ await mkdir3(config.doingDir, { recursive: true });
1323
+ try {
1324
+ await rename2(paths.source, paths.target);
1325
+ } catch (error) {
1326
+ throw classifyClaimError(error, sessionId);
1327
+ }
1328
+ const content = await readFile2(paths.target, "utf-8");
1329
+ const output = formatShowOutput(content, { status: "doing" });
1330
+ return `Claimed session <PICKUP_ID>${sessionId}</PICKUP_ID>
1331
+
1332
+ ${output}`;
1333
+ }
1334
+
1335
+ // src/commands/session/prune.ts
1336
+ import { readdir as readdir3, readFile as readFile3, unlink as unlink2 } from "fs/promises";
1337
+ import { join as join8 } from "path";
1338
+ var DEFAULT_KEEP_COUNT = 5;
1339
+ var PruneValidationError = class extends Error {
1340
+ constructor(message) {
1341
+ super(message);
1342
+ this.name = "PruneValidationError";
1343
+ }
1344
+ };
1345
+ function validatePruneOptions(options) {
1346
+ if (options.keep !== void 0) {
1347
+ if (!Number.isInteger(options.keep) || options.keep < 1) {
1348
+ throw new PruneValidationError(
1349
+ `Invalid --keep value: ${options.keep}. Must be a positive integer.`
1350
+ );
1351
+ }
1352
+ }
1353
+ }
1354
+ async function loadArchiveSessions(config) {
1355
+ try {
1356
+ const files = await readdir3(config.archiveDir);
1357
+ const sessions = [];
1358
+ for (const file of files) {
1359
+ if (!file.endsWith(".md")) continue;
1360
+ const id = file.replace(".md", "");
1361
+ const filePath = join8(config.archiveDir, file);
1362
+ const content = await readFile3(filePath, "utf-8");
1363
+ const metadata = parseSessionMetadata(content);
1364
+ sessions.push({
1365
+ id,
1366
+ status: "archive",
1367
+ path: filePath,
1368
+ metadata
1369
+ });
1370
+ }
1371
+ return sessions;
1372
+ } catch (error) {
1373
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
1374
+ return [];
1375
+ }
1376
+ throw error;
1377
+ }
1378
+ }
1379
+ function selectSessionsToPrune(sessions, keep) {
1380
+ const sorted = sortSessions(sessions);
1381
+ if (sorted.length <= keep) {
1382
+ return [];
1383
+ }
1384
+ return sorted.slice(keep);
1385
+ }
1386
+ async function pruneCommand(options) {
1387
+ validatePruneOptions(options);
1388
+ const keep = options.keep ?? DEFAULT_KEEP_COUNT;
1389
+ const dryRun = options.dryRun ?? false;
1390
+ const config = options.sessionsDir ? {
1391
+ todoDir: join8(options.sessionsDir, "todo"),
1392
+ doingDir: join8(options.sessionsDir, "doing"),
1393
+ archiveDir: join8(options.sessionsDir, "archive")
1394
+ } : DEFAULT_SESSION_CONFIG;
1395
+ const sessions = await loadArchiveSessions(config);
1396
+ const toPrune = selectSessionsToPrune(sessions, keep);
1397
+ if (toPrune.length === 0) {
1398
+ return `No sessions to prune. ${sessions.length} sessions kept.`;
1399
+ }
1400
+ if (dryRun) {
1401
+ const lines2 = [
1402
+ `Would delete ${toPrune.length} sessions:`,
1403
+ ...toPrune.map((s) => ` - ${s.id}`),
1404
+ "",
1405
+ `${sessions.length - toPrune.length} sessions would be kept.`
1406
+ ];
1407
+ return lines2.join("\n");
1408
+ }
1409
+ for (const session of toPrune) {
1410
+ await unlink2(session.path);
1411
+ }
1412
+ const lines = [
1413
+ `Deleted ${toPrune.length} sessions:`,
1414
+ ...toPrune.map((s) => ` - ${s.id}`),
1415
+ "",
1416
+ `${sessions.length - toPrune.length} sessions kept.`
1417
+ ];
1418
+ return lines.join("\n");
1419
+ }
1420
+
1421
+ // src/commands/session/release.ts
1422
+ import { readdir as readdir4, rename as rename3 } from "fs/promises";
1423
+ import { join as join9 } from "path";
1424
+
1425
+ // src/session/release.ts
1426
+ function buildReleasePaths(sessionId, config) {
1427
+ return {
1428
+ source: `${config.doingDir}/${sessionId}.md`,
1429
+ target: `${config.todoDir}/${sessionId}.md`
1430
+ };
1431
+ }
1432
+ function findCurrentSession(doingSessions) {
1433
+ if (doingSessions.length === 0) {
1434
+ return null;
1435
+ }
1436
+ const sorted = [...doingSessions].sort((a, b) => {
1437
+ const dateA = parseSessionId(a.id);
1438
+ const dateB = parseSessionId(b.id);
1439
+ if (!dateA && !dateB) return 0;
1440
+ if (!dateA) return 1;
1441
+ if (!dateB) return -1;
1442
+ return dateB.getTime() - dateA.getTime();
1443
+ });
1444
+ return sorted[0];
1445
+ }
1446
+
1447
+ // src/commands/session/release.ts
1448
+ async function loadDoingSessions(config) {
1449
+ try {
1450
+ const files = await readdir4(config.doingDir);
1451
+ return files.filter((file) => file.endsWith(".md")).map((file) => ({ id: file.replace(".md", "") }));
1452
+ } catch (error) {
1453
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
1454
+ return [];
1455
+ }
1456
+ throw error;
1457
+ }
1458
+ }
1459
+ async function releaseCommand(options) {
1460
+ const config = options.sessionsDir ? {
1461
+ todoDir: join9(options.sessionsDir, "todo"),
1462
+ doingDir: join9(options.sessionsDir, "doing"),
1463
+ archiveDir: join9(options.sessionsDir, "archive")
1464
+ } : DEFAULT_SESSION_CONFIG;
1465
+ let sessionId;
1466
+ if (options.sessionId) {
1467
+ sessionId = options.sessionId;
1468
+ } else {
1469
+ const sessions = await loadDoingSessions(config);
1470
+ const current = findCurrentSession(sessions);
1471
+ if (!current) {
1472
+ throw new SessionNotClaimedError("(none)");
1473
+ }
1474
+ sessionId = current.id;
1475
+ }
1476
+ const paths = buildReleasePaths(sessionId, config);
1477
+ try {
1478
+ await rename3(paths.source, paths.target);
1479
+ } catch (error) {
1480
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
1481
+ throw new SessionNotClaimedError(sessionId);
1482
+ }
1483
+ throw error;
1484
+ }
1485
+ return `Released session: ${sessionId}
1486
+ Session returned to todo directory.`;
1487
+ }
1488
+
1489
+ // src/commands/session/show.ts
1490
+ import { readFile as readFile4, stat as stat3 } from "fs/promises";
1491
+ import { join as join10 } from "path";
1492
+ async function findExistingPath(paths, _config) {
1493
+ for (let i = 0; i < paths.length; i++) {
1494
+ const filePath = paths[i];
1495
+ try {
1496
+ const stats = await stat3(filePath);
1497
+ if (stats.isFile()) {
1498
+ return { path: filePath, status: SEARCH_ORDER[i] };
1499
+ }
1500
+ } catch {
1501
+ }
1502
+ }
1503
+ return null;
1504
+ }
1505
+ async function showCommand(options) {
1506
+ const config = options.sessionsDir ? {
1507
+ todoDir: join10(options.sessionsDir, "todo"),
1508
+ doingDir: join10(options.sessionsDir, "doing"),
1509
+ archiveDir: join10(options.sessionsDir, "archive")
1510
+ } : DEFAULT_SESSION_CONFIG;
1511
+ const paths = resolveSessionPaths(options.sessionId, config);
1512
+ const found = await findExistingPath(paths, config);
1513
+ if (!found) {
1514
+ throw new SessionNotFoundError(options.sessionId);
1515
+ }
1516
+ const content = await readFile4(found.path, "utf-8");
1517
+ return formatShowOutput(content, { status: found.status });
1518
+ }
1519
+
1520
+ // src/domains/session/help.ts
1521
+ var SESSION_FORMAT_HELP = `
1522
+ Session File Format:
1523
+ Sessions are markdown files with YAML frontmatter for metadata.
1524
+
1525
+ ---
1526
+ priority: high | medium | low
1527
+ tags: [tag1, tag2]
1528
+ ---
1529
+ # Session Title
1530
+
1531
+ Session content...
1532
+
1533
+ Workflow:
1534
+ 1. handoff - Create session (todo)
1535
+ 2. pickup - Claim session (todo -> doing)
1536
+ 3. release - Return session (doing -> todo)
1537
+ 4. delete - Remove session
1538
+ `;
1539
+ var HANDOFF_FRONTMATTER_HELP = `
1540
+ Usage:
1541
+ Option 1: Pipe content with frontmatter via stdin
1542
+ Option 2: Run without stdin, then edit the created file directly
1543
+
1544
+ Frontmatter Format:
1545
+ ---
1546
+ priority: high # high | medium | low (default: medium)
1547
+ tags: [feat, api] # optional labels for categorization
1548
+ ---
1549
+ # Your session content here...
1550
+
1551
+ Output Tags (for automation):
1552
+ <HANDOFF_ID>session-id</HANDOFF_ID> - Session identifier
1553
+ <SESSION_FILE>/path/to/file</SESSION_FILE> - Absolute path to edit
1554
+
1555
+ Examples:
1556
+ # With stdin content:
1557
+ echo '---
1558
+ priority: high
1559
+ ---
1560
+ # Fix login' | spx session handoff
1561
+
1562
+ # Without stdin (creates empty session, edit file directly):
1563
+ spx session handoff
1564
+ `;
1565
+ var PICKUP_SELECTION_HELP = `
1566
+ Selection Logic (--auto):
1567
+ Sessions are selected by priority, then age (FIFO):
1568
+ 1. high priority first
1569
+ 2. medium priority second
1570
+ 3. low priority last
1571
+ 4. Within same priority: oldest session first
1572
+
1573
+ Output:
1574
+ <PICKUP_ID>session-id</PICKUP_ID> tag for automation parsing
1575
+ `;
1576
+
1577
+ // src/domains/session/index.ts
1578
+ async function readStdin() {
1579
+ if (process.stdin.isTTY) {
1580
+ return void 0;
1581
+ }
1582
+ return new Promise((resolve2) => {
1583
+ let data = "";
1584
+ process.stdin.setEncoding("utf-8");
1585
+ process.stdin.on("data", (chunk) => {
1586
+ data += chunk;
1587
+ });
1588
+ process.stdin.on("end", () => {
1589
+ resolve2(data.trim() || void 0);
1590
+ });
1591
+ process.stdin.on("error", () => {
1592
+ resolve2(void 0);
1593
+ });
1594
+ });
1595
+ }
1596
+ function handleError(error) {
1597
+ console.error("Error:", error instanceof Error ? error.message : String(error));
1598
+ process.exit(1);
1599
+ }
1600
+ function registerSessionCommands(sessionCmd) {
1601
+ sessionCmd.command("list").description("List all sessions").option("--status <status>", "Filter by status (todo|doing|archive)").option("--json", "Output as JSON").option("--sessions-dir <path>", "Custom sessions directory").action(async (options) => {
1602
+ try {
1603
+ const output = await listCommand({
1604
+ status: options.status,
1605
+ format: options.json ? "json" : "text",
1606
+ sessionsDir: options.sessionsDir
1607
+ });
1608
+ console.log(output);
1609
+ } catch (error) {
1610
+ handleError(error);
1611
+ }
1612
+ });
1613
+ sessionCmd.command("show <id>").description("Show session content").option("--sessions-dir <path>", "Custom sessions directory").action(async (id, options) => {
1614
+ try {
1615
+ const output = await showCommand({
1616
+ sessionId: id,
1617
+ sessionsDir: options.sessionsDir
1618
+ });
1619
+ console.log(output);
1620
+ } catch (error) {
1621
+ handleError(error);
1622
+ }
1623
+ });
1624
+ sessionCmd.command("pickup [id]").description("Claim a session (move from todo to doing)").option("--auto", "Auto-select highest priority session").option("--sessions-dir <path>", "Custom sessions directory").addHelpText("after", PICKUP_SELECTION_HELP).action(async (id, options) => {
1625
+ try {
1626
+ if (!id && !options.auto) {
1627
+ console.error("Error: Either session ID or --auto flag is required");
1628
+ process.exit(1);
1629
+ }
1630
+ const output = await pickupCommand({
1631
+ sessionId: id,
1632
+ auto: options.auto,
1633
+ sessionsDir: options.sessionsDir
1634
+ });
1635
+ console.log(output);
1636
+ } catch (error) {
1637
+ handleError(error);
1638
+ }
1639
+ });
1640
+ sessionCmd.command("release [id]").description("Release a session (move from doing to todo)").option("--sessions-dir <path>", "Custom sessions directory").action(async (id, options) => {
1641
+ try {
1642
+ const output = await releaseCommand({
1643
+ sessionId: id,
1644
+ sessionsDir: options.sessionsDir
1645
+ });
1646
+ console.log(output);
1647
+ } catch (error) {
1648
+ handleError(error);
1649
+ }
1650
+ });
1651
+ sessionCmd.command("handoff").description("Create a handoff session (reads content with frontmatter from stdin)").option("--sessions-dir <path>", "Custom sessions directory").addHelpText("after", HANDOFF_FRONTMATTER_HELP).action(async (options) => {
1652
+ try {
1653
+ const content = await readStdin();
1654
+ const output = await handoffCommand({
1655
+ content,
1656
+ sessionsDir: options.sessionsDir
1657
+ });
1658
+ console.log(output);
1659
+ } catch (error) {
1660
+ handleError(error);
1661
+ }
1662
+ });
1663
+ sessionCmd.command("delete <id>").description("Delete a session").option("--sessions-dir <path>", "Custom sessions directory").action(async (id, options) => {
1664
+ try {
1665
+ const output = await deleteCommand({
1666
+ sessionId: id,
1667
+ sessionsDir: options.sessionsDir
1668
+ });
1669
+ console.log(output);
1670
+ } catch (error) {
1671
+ handleError(error);
1672
+ }
1673
+ });
1674
+ sessionCmd.command("prune").description("Remove old todo sessions, keeping the most recent N").option("--keep <count>", "Number of sessions to keep (default: 5)", "5").option("--dry-run", "Show what would be deleted without deleting").option("--sessions-dir <path>", "Custom sessions directory").action(async (options) => {
1675
+ try {
1676
+ const keep = options.keep ? Number.parseInt(options.keep, 10) : void 0;
1677
+ const output = await pruneCommand({
1678
+ keep,
1679
+ dryRun: options.dryRun,
1680
+ sessionsDir: options.sessionsDir
1681
+ });
1682
+ console.log(output);
1683
+ } catch (error) {
1684
+ if (error instanceof PruneValidationError) {
1685
+ console.error("Error:", error.message);
1686
+ process.exit(1);
1687
+ }
1688
+ handleError(error);
1689
+ }
1690
+ });
1691
+ sessionCmd.command("archive <id>").description("Move a session to the archive directory").option("--sessions-dir <path>", "Custom sessions directory").action(async (id, options) => {
1692
+ try {
1693
+ const output = await archiveCommand({
1694
+ sessionId: id,
1695
+ sessionsDir: options.sessionsDir
1696
+ });
1697
+ console.log(output);
1698
+ } catch (error) {
1699
+ if (error instanceof SessionAlreadyArchivedError) {
1700
+ console.error("Error:", error.message);
1701
+ process.exit(1);
1702
+ }
1703
+ handleError(error);
1704
+ }
1705
+ });
1706
+ }
1707
+ var sessionDomain = {
1708
+ name: "session",
1709
+ description: "Manage session workflow",
1710
+ register: (program2) => {
1711
+ const sessionCmd = program2.command("session").description("Manage session workflow").addHelpText("after", SESSION_FORMAT_HELP);
1712
+ registerSessionCommands(sessionCmd);
1713
+ }
1714
+ };
1715
+
1716
+ // src/scanner/scanner.ts
1717
+ import path5 from "path";
1718
+ var Scanner = class {
1719
+ /**
1720
+ * Create a new Scanner instance
1721
+ *
1722
+ * @param projectRoot - Absolute path to the project root directory
1723
+ * @param config - Configuration object defining directory structure
1724
+ */
1725
+ constructor(projectRoot, config) {
1726
+ this.projectRoot = projectRoot;
1727
+ this.config = config;
1728
+ }
1729
+ /**
1730
+ * Scan the project for work items in the "doing" status directory
1731
+ *
1732
+ * Walks the configured specs/work/doing directory, filters for valid
1733
+ * work item directories, and returns structured work item data.
1734
+ *
1735
+ * @returns Array of work items found in the doing directory
1736
+ * @throws Error if the directory doesn't exist or is inaccessible
1737
+ */
1738
+ async scan() {
1739
+ const doingPath = this.getDoingPath();
1740
+ const allEntries = await walkDirectory(doingPath);
1741
+ const workItemEntries = filterWorkItemDirectories(allEntries);
1742
+ return buildWorkItemList(workItemEntries);
1743
+ }
1744
+ /**
1745
+ * Get the full path to the "doing" status directory
1746
+ *
1747
+ * Constructs path from config: {projectRoot}/{specs.root}/{work.dir}/{statusDirs.doing}
1748
+ *
1749
+ * @returns Absolute path to the doing directory
1750
+ */
1751
+ getDoingPath() {
1752
+ return path5.join(
1753
+ this.projectRoot,
1754
+ this.config.specs.root,
1755
+ this.config.specs.work.dir,
1756
+ this.config.specs.work.statusDirs.doing
1757
+ );
1758
+ }
1759
+ /**
1760
+ * Get the full path to the "backlog" status directory
1761
+ *
1762
+ * @returns Absolute path to the backlog directory
1763
+ */
1764
+ getBacklogPath() {
1765
+ return path5.join(
1766
+ this.projectRoot,
1767
+ this.config.specs.root,
1768
+ this.config.specs.work.dir,
1769
+ this.config.specs.work.statusDirs.backlog
1770
+ );
1771
+ }
1772
+ /**
1773
+ * Get the full path to the "done" status directory
1774
+ *
1775
+ * @returns Absolute path to the done/archive directory
1776
+ */
1777
+ getDonePath() {
1778
+ return path5.join(
1779
+ this.projectRoot,
1780
+ this.config.specs.root,
1781
+ this.config.specs.work.dir,
1782
+ this.config.specs.work.statusDirs.done
1783
+ );
1784
+ }
1785
+ /**
1786
+ * Get the full path to the specs root directory
1787
+ *
1788
+ * @returns Absolute path to the specs root
1789
+ */
1790
+ getSpecsRootPath() {
1791
+ return path5.join(this.projectRoot, this.config.specs.root);
1792
+ }
1793
+ /**
1794
+ * Get the full path to the work directory
1795
+ *
1796
+ * @returns Absolute path to the work directory
1797
+ */
1798
+ getWorkPath() {
1799
+ return path5.join(
1800
+ this.projectRoot,
1801
+ this.config.specs.root,
1802
+ this.config.specs.work.dir
1803
+ );
1804
+ }
1805
+ };
1806
+
1807
+ // src/status/state.ts
1808
+ import { access, readdir as readdir5, stat as stat4 } from "fs/promises";
1809
+ import path6 from "path";
1810
+ function determineStatus(flags) {
1811
+ if (!flags.hasTestsDir) {
1812
+ return "OPEN";
1813
+ }
1814
+ if (flags.hasDoneMd) {
1815
+ return "DONE";
1816
+ }
1817
+ if (flags.testsIsEmpty) {
1818
+ return "OPEN";
1819
+ }
1820
+ return "IN_PROGRESS";
1821
+ }
1822
+ var StatusDeterminationError = class extends Error {
1823
+ constructor(workItemPath, cause) {
1824
+ const errorMessage = cause instanceof Error ? cause.message : String(cause);
1825
+ super(`Failed to determine status for ${workItemPath}: ${errorMessage}`);
1826
+ this.workItemPath = workItemPath;
1827
+ this.cause = cause;
1828
+ this.name = "StatusDeterminationError";
1829
+ }
1830
+ };
1831
+ async function getWorkItemStatus(workItemPath) {
1832
+ try {
1833
+ try {
1834
+ await access(workItemPath);
1835
+ } catch (error) {
1836
+ if (error.code === "ENOENT") {
1837
+ throw new Error(`Work item not found: ${workItemPath}`);
1838
+ }
1839
+ throw error;
1840
+ }
1841
+ const testsPath = path6.join(workItemPath, "tests");
1842
+ let hasTests;
1843
+ try {
1844
+ await access(testsPath);
1845
+ hasTests = true;
1846
+ } catch (error) {
1847
+ if (error.code === "ENOENT") {
1848
+ hasTests = false;
1849
+ } else {
1850
+ throw error;
1851
+ }
1852
+ }
1853
+ if (!hasTests) {
1854
+ return determineStatus({
1855
+ hasTestsDir: false,
1856
+ hasDoneMd: false,
1857
+ testsIsEmpty: true
1858
+ });
1859
+ }
1860
+ const entries = await readdir5(testsPath);
1861
+ const hasDone = entries.includes("DONE.md");
1862
+ if (hasDone) {
1863
+ const donePath = path6.join(testsPath, "DONE.md");
1864
+ const stats = await stat4(donePath);
1865
+ if (!stats.isFile()) {
1866
+ return determineStatus({
1867
+ hasTestsDir: true,
1868
+ hasDoneMd: false,
1869
+ testsIsEmpty: isEmptyFromEntries(entries)
1870
+ });
1871
+ }
1872
+ }
1873
+ const isEmpty = isEmptyFromEntries(entries);
1874
+ return determineStatus({
1875
+ hasTestsDir: true,
1876
+ hasDoneMd: hasDone,
1877
+ testsIsEmpty: isEmpty
1878
+ });
1879
+ } catch (error) {
1880
+ throw new StatusDeterminationError(workItemPath, error);
1881
+ }
1882
+ }
1883
+ function isEmptyFromEntries(entries) {
1884
+ const testFiles = entries.filter((entry) => {
1885
+ if (entry === "DONE.md") {
1886
+ return false;
1887
+ }
1888
+ if (entry.startsWith(".")) {
1889
+ return false;
1890
+ }
1891
+ return true;
1892
+ });
1893
+ return testFiles.length === 0;
1894
+ }
1895
+
1896
+ // src/tree/build.ts
1897
+ async function buildTree(workItems, deps = {}) {
1898
+ const getStatus = deps.getStatus || getWorkItemStatus;
1899
+ const itemsWithStatus = await Promise.all(
1900
+ workItems.map(async (item) => ({
1901
+ ...item,
1902
+ status: await getStatus(item.path)
1903
+ }))
1904
+ );
1905
+ const capabilities = itemsWithStatus.filter(
1906
+ (item) => item.kind === "capability"
1907
+ );
1908
+ const features = itemsWithStatus.filter((item) => item.kind === "feature");
1909
+ const stories = itemsWithStatus.filter((item) => item.kind === "story");
1910
+ const storyNodes = stories.map((item) => createTreeNode(item, []));
1911
+ const featureNodes = features.map((item) => {
1912
+ const children = storyNodes.filter((story) => isChildOf(story.path, item.path)).sort((a, b) => a.number - b.number);
1913
+ return createTreeNode(item, children);
1914
+ });
1915
+ const capabilityNodes = capabilities.map((item) => {
1916
+ const children = featureNodes.filter((feature) => isChildOf(feature.path, item.path)).sort((a, b) => a.number - b.number);
1917
+ return createTreeNode(item, children);
1918
+ });
1919
+ detectOrphans(stories, featureNodes);
1920
+ detectOrphans(features, capabilityNodes);
1921
+ const sortedCapabilities = capabilityNodes.sort((a, b) => a.number - b.number);
1922
+ rollupStatus(sortedCapabilities);
1923
+ return {
1924
+ nodes: sortedCapabilities
1925
+ };
1926
+ }
1927
+ function createTreeNode(item, children) {
1928
+ return {
1929
+ kind: item.kind,
1930
+ number: item.number,
1931
+ slug: item.slug,
1932
+ path: item.path,
1933
+ status: item.status,
1934
+ children
1935
+ };
1936
+ }
1937
+ function isChildOf(childPath, parentPath) {
1938
+ const normalizedChild = childPath.replace(/\/$/, "");
1939
+ const normalizedParent = parentPath.replace(/\/$/, "");
1940
+ if (!normalizedChild.startsWith(normalizedParent + "/")) {
1941
+ return false;
1942
+ }
1943
+ const relativePath = normalizedChild.slice(normalizedParent.length + 1);
1944
+ return !relativePath.includes("/");
1945
+ }
1946
+ function detectOrphans(items, potentialParents) {
1947
+ for (const item of items) {
1948
+ const hasParent = potentialParents.some((parent) => isChildOf(item.path, parent.path));
1949
+ if (!hasParent) {
1950
+ throw new Error(
1951
+ `Orphan work item detected: ${item.kind} "${item.slug}" at ${item.path} has no valid parent`
1952
+ );
1953
+ }
1954
+ }
1955
+ }
1956
+ function rollupStatus(nodes) {
1957
+ for (const node of nodes) {
1958
+ if (node.children.length > 0) {
1959
+ rollupStatus(node.children);
1960
+ const ownStatus = node.status;
1961
+ const childStatuses = node.children.map((child) => child.status);
1962
+ const allChildrenDone = childStatuses.every(
1963
+ (status) => status === "DONE"
1964
+ );
1965
+ const allChildrenOpen = childStatuses.every(
1966
+ (status) => status === "OPEN"
1967
+ );
1968
+ if (ownStatus === "DONE" && allChildrenDone) {
1969
+ node.status = "DONE";
1970
+ } else if (ownStatus === "OPEN" && allChildrenOpen) {
1971
+ node.status = "OPEN";
1972
+ } else {
1973
+ node.status = "IN_PROGRESS";
1974
+ }
1975
+ }
1976
+ }
1977
+ }
1978
+
1979
+ // src/commands/spec/next.ts
1980
+ function findNextWorkItem(tree) {
1981
+ return findFirstNonDoneLeaf(tree.nodes);
1982
+ }
1983
+ function findFirstNonDoneLeaf(nodes) {
1984
+ for (const node of nodes) {
1985
+ if (node.kind === LEAF_KIND) {
1986
+ if (node.status !== "DONE") {
1987
+ return node;
1988
+ }
1989
+ } else {
1990
+ const found = findFirstNonDoneLeaf(node.children);
1991
+ if (found) {
1992
+ return found;
1993
+ }
1994
+ }
1995
+ }
1996
+ return null;
1997
+ }
1998
+ function formatWorkItemName(node) {
1999
+ const displayNum = node.kind === "capability" ? node.number + 1 : node.number;
2000
+ return `${node.kind}-${displayNum}_${node.slug}`;
2001
+ }
2002
+ async function nextCommand(options = {}) {
2003
+ const cwd = options.cwd || process.cwd();
2004
+ const scanner = new Scanner(cwd, DEFAULT_CONFIG);
2005
+ const workItems = await scanner.scan();
2006
+ if (workItems.length === 0) {
2007
+ return `No work items found in ${DEFAULT_CONFIG.specs.root}/${DEFAULT_CONFIG.specs.work.dir}/${DEFAULT_CONFIG.specs.work.statusDirs.doing}`;
2008
+ }
2009
+ const tree = await buildTree(workItems);
2010
+ const next = findNextWorkItem(tree);
2011
+ if (!next) {
2012
+ return "All work items are complete! \u{1F389}";
2013
+ }
2014
+ const parents = findParents(tree.nodes, next);
2015
+ const lines = [];
2016
+ lines.push("Next work item:");
2017
+ lines.push("");
2018
+ if (parents.capability && parents.feature) {
2019
+ lines.push(
2020
+ ` ${formatWorkItemName(parents.capability)} > ${formatWorkItemName(parents.feature)} > ${formatWorkItemName(next)}`
2021
+ );
2022
+ } else {
2023
+ lines.push(` ${formatWorkItemName(next)}`);
2024
+ }
2025
+ lines.push("");
2026
+ lines.push(` Status: ${next.status}`);
2027
+ lines.push(` Path: ${next.path}`);
2028
+ return lines.join("\n");
2029
+ }
2030
+ function findParents(nodes, target) {
2031
+ for (const capability of nodes) {
2032
+ for (const feature of capability.children) {
2033
+ for (const story of feature.children) {
2034
+ if (story.path === target.path) {
2035
+ return { capability, feature };
2036
+ }
2037
+ }
2038
+ }
2039
+ }
2040
+ return {};
2041
+ }
2042
+
2043
+ // src/reporter/json.ts
2044
+ var JSON_INDENT = 2;
2045
+ function formatJSON(tree, config) {
2046
+ const capabilities = tree.nodes.map((node) => nodeToJSON(node));
2047
+ const summary = calculateSummary(tree);
2048
+ const output = {
2049
+ config: {
2050
+ specs: config.specs,
2051
+ sessions: config.sessions
2052
+ },
2053
+ summary,
2054
+ capabilities
2055
+ };
2056
+ return JSON.stringify(output, null, JSON_INDENT);
2057
+ }
2058
+ function nodeToJSON(node) {
2059
+ const displayNumber = getDisplayNumber(node);
2060
+ const base = {
2061
+ kind: node.kind,
2062
+ number: displayNumber,
2063
+ slug: node.slug,
2064
+ status: node.status
2065
+ };
2066
+ if (node.kind === "capability") {
2067
+ return {
2068
+ ...base,
2069
+ features: node.children.map((child) => nodeToJSON(child))
2070
+ };
2071
+ } else if (node.kind === "feature") {
2072
+ return {
2073
+ ...base,
2074
+ stories: node.children.map((child) => nodeToJSON(child))
2075
+ };
2076
+ } else {
2077
+ return base;
2078
+ }
2079
+ }
2080
+ function calculateSummary(tree) {
2081
+ const summary = {
2082
+ done: 0,
2083
+ inProgress: 0,
2084
+ open: 0
2085
+ };
2086
+ for (const capability of tree.nodes) {
2087
+ countNode(capability, summary);
2088
+ for (const feature of capability.children) {
2089
+ countNode(feature, summary);
2090
+ }
2091
+ }
2092
+ return summary;
2093
+ }
2094
+ function countNode(node, summary) {
2095
+ switch (node.status) {
2096
+ case "DONE":
2097
+ summary.done++;
2098
+ break;
2099
+ case "IN_PROGRESS":
2100
+ summary.inProgress++;
2101
+ break;
2102
+ case "OPEN":
2103
+ summary.open++;
2104
+ break;
2105
+ }
2106
+ }
2107
+ function getDisplayNumber(node) {
2108
+ return node.kind === "capability" ? node.number + 1 : node.number;
2109
+ }
2110
+
2111
+ // src/reporter/markdown.ts
2112
+ function formatMarkdown(tree) {
2113
+ const sections = [];
2114
+ for (const node of tree.nodes) {
2115
+ formatNode(node, 1, sections);
2116
+ }
2117
+ return sections.join("\n\n");
2118
+ }
2119
+ function formatNode(node, level, sections) {
2120
+ const displayNumber = getDisplayNumber2(node);
2121
+ const name = `${node.kind}-${displayNumber}_${node.slug}`;
2122
+ const heading = "#".repeat(level);
2123
+ sections.push(`${heading} ${name}`);
2124
+ sections.push(`Status: ${node.status}`);
2125
+ for (const child of node.children) {
2126
+ formatNode(child, level + 1, sections);
2127
+ }
2128
+ }
2129
+ function getDisplayNumber2(node) {
2130
+ return node.kind === "capability" ? node.number + 1 : node.number;
2131
+ }
2132
+
2133
+ // src/reporter/table.ts
2134
+ function formatTable(tree) {
2135
+ const rows = [];
2136
+ for (const node of tree.nodes) {
2137
+ collectRows(node, 0, rows);
2138
+ }
2139
+ const widths = calculateColumnWidths(rows);
2140
+ const lines = [];
2141
+ lines.push(
2142
+ formatRow(
2143
+ {
2144
+ level: "Level",
2145
+ number: "Number",
2146
+ name: "Name",
2147
+ status: "Status"
2148
+ },
2149
+ widths
2150
+ )
2151
+ );
2152
+ lines.push(
2153
+ `|${"-".repeat(widths.level + 2)}|${"-".repeat(widths.number + 2)}|${"-".repeat(widths.name + 2)}|${"-".repeat(widths.status + 2)}|`
2154
+ );
2155
+ for (const row of rows) {
2156
+ lines.push(formatRow(row, widths));
2157
+ }
2158
+ return lines.join("\n");
2159
+ }
2160
+ function collectRows(node, depth, rows) {
2161
+ const indent = " ".repeat(depth);
2162
+ const levelName = getLevelName(node.kind);
2163
+ const displayNumber = getDisplayNumber3(node);
2164
+ rows.push({
2165
+ level: `${indent}${levelName}`,
2166
+ number: String(displayNumber),
2167
+ name: node.slug,
2168
+ status: node.status
2169
+ });
2170
+ for (const child of node.children) {
2171
+ collectRows(child, depth + 1, rows);
2172
+ }
2173
+ }
2174
+ function getLevelName(kind) {
2175
+ return kind.charAt(0).toUpperCase() + kind.slice(1);
2176
+ }
2177
+ function getDisplayNumber3(node) {
2178
+ return node.kind === "capability" ? node.number + 1 : node.number;
2179
+ }
2180
+ function calculateColumnWidths(rows) {
2181
+ const widths = {
2182
+ level: "Level".length,
2183
+ number: "Number".length,
2184
+ name: "Name".length,
2185
+ status: "Status".length
2186
+ };
2187
+ for (const row of rows) {
2188
+ widths.level = Math.max(widths.level, row.level.length);
2189
+ widths.number = Math.max(widths.number, row.number.length);
2190
+ widths.name = Math.max(widths.name, row.name.length);
2191
+ widths.status = Math.max(widths.status, row.status.length);
2192
+ }
2193
+ return widths;
2194
+ }
2195
+ function formatRow(row, widths) {
2196
+ return `| ${row.level.padEnd(widths.level)} | ${row.number.padEnd(widths.number)} | ${row.name.padEnd(widths.name)} | ${row.status.padEnd(widths.status)} |`;
2197
+ }
2198
+
2199
+ // src/reporter/text.ts
2200
+ import chalk from "chalk";
2201
+ function formatText(tree) {
2202
+ const lines = [];
2203
+ for (const node of tree.nodes) {
2204
+ lines.push(formatNode2(node, 0));
2205
+ }
2206
+ return lines.join("\n");
2207
+ }
2208
+ function formatNode2(node, indent) {
2209
+ const lines = [];
2210
+ const name = formatWorkItemName2(node.kind, node.number, node.slug);
2211
+ const prefix = " ".repeat(indent);
2212
+ const status = formatStatus(node.status);
2213
+ const line = `${prefix}${name} ${status}`;
2214
+ lines.push(line);
2215
+ for (const child of node.children) {
2216
+ lines.push(formatNode2(child, indent + 2));
2217
+ }
2218
+ return lines.join("\n");
2219
+ }
2220
+ function formatWorkItemName2(kind, number, slug) {
2221
+ const displayNumber = kind === "capability" ? number + 1 : number;
2222
+ return `${kind}-${displayNumber}_${slug}`;
2223
+ }
2224
+ function formatStatus(status) {
2225
+ switch (status) {
2226
+ case "DONE":
2227
+ return chalk.green(`[${status}]`);
2228
+ case "IN_PROGRESS":
2229
+ return chalk.yellow(`[${status}]`);
2230
+ case "OPEN":
2231
+ return chalk.gray(`[${status}]`);
2232
+ default:
2233
+ return `[${status}]`;
2234
+ }
2235
+ }
2236
+
2237
+ // src/commands/spec/status.ts
2238
+ async function statusCommand(options = {}) {
2239
+ const cwd = options.cwd || process.cwd();
2240
+ const format2 = options.format || "text";
2241
+ const scanner = new Scanner(cwd, DEFAULT_CONFIG);
2242
+ let workItems;
2243
+ try {
2244
+ workItems = await scanner.scan();
2245
+ } catch (error) {
2246
+ if (error instanceof Error && error.message.includes("ENOENT")) {
2247
+ const doingPath = `${DEFAULT_CONFIG.specs.root}/${DEFAULT_CONFIG.specs.work.dir}/${DEFAULT_CONFIG.specs.work.statusDirs.doing}`;
2248
+ throw new Error(
2249
+ `Directory ${doingPath} not found.
2250
+
2251
+ This command is for legacy specs/ projects. For CODE framework projects, check the spx/ directory for specifications.`
2252
+ );
2253
+ }
2254
+ throw error;
2255
+ }
2256
+ if (workItems.length === 0) {
2257
+ return `No work items found in ${DEFAULT_CONFIG.specs.root}/${DEFAULT_CONFIG.specs.work.dir}/${DEFAULT_CONFIG.specs.work.statusDirs.doing}`;
2258
+ }
2259
+ const tree = await buildTree(workItems);
2260
+ switch (format2) {
2261
+ case "json":
2262
+ return formatJSON(tree, DEFAULT_CONFIG);
2263
+ case "markdown":
2264
+ return formatMarkdown(tree);
2265
+ case "table":
2266
+ return formatTable(tree);
2267
+ case "text":
2268
+ default:
2269
+ return formatText(tree);
2270
+ }
2271
+ }
2272
+
2273
+ // src/domains/spec/index.ts
2274
+ function registerSpecCommands(specCmd) {
2275
+ specCmd.command("status").description("Get project status").option("--json", "Output as JSON").option("--format <format>", "Output format (text|json|markdown|table)").action(async (options) => {
2276
+ try {
2277
+ let format2 = "text";
2278
+ if (options.json) {
2279
+ format2 = "json";
2280
+ } else if (options.format) {
2281
+ const validFormats = ["text", "json", "markdown", "table"];
2282
+ if (validFormats.includes(options.format)) {
2283
+ format2 = options.format;
2284
+ } else {
2285
+ console.error(
2286
+ `Error: Invalid format "${options.format}". Must be one of: ${validFormats.join(", ")}`
2287
+ );
2288
+ process.exit(1);
2289
+ }
2290
+ }
2291
+ const output = await statusCommand({ cwd: process.cwd(), format: format2 });
2292
+ console.log(output);
2293
+ } catch (error) {
2294
+ console.error(
2295
+ "Error:",
2296
+ error instanceof Error ? error.message : String(error)
2297
+ );
2298
+ process.exit(1);
2299
+ }
2300
+ });
2301
+ specCmd.command("next").description("Find next work item to work on").action(async () => {
2302
+ try {
2303
+ const output = await nextCommand({ cwd: process.cwd() });
2304
+ console.log(output);
2305
+ } catch (error) {
2306
+ console.error(
2307
+ "Error:",
2308
+ error instanceof Error ? error.message : String(error)
2309
+ );
2310
+ process.exit(1);
2311
+ }
2312
+ });
2313
+ }
2314
+ var specDomain = {
2315
+ name: "spec",
2316
+ description: "Manage spec workflow",
2317
+ register: (program2) => {
2318
+ const specCmd = program2.command("spec").description("Manage spec workflow");
2319
+ registerSpecCommands(specCmd);
2320
+ }
2321
+ };
2322
+
2323
+ // node_modules/.pnpm/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/scanner.js
2324
+ function createScanner(text, ignoreTrivia = false) {
2325
+ const len = text.length;
2326
+ let pos = 0, value = "", tokenOffset = 0, token = 16, lineNumber = 0, lineStartOffset = 0, tokenLineStartOffset = 0, prevTokenLineStartOffset = 0, scanError = 0;
2327
+ function scanHexDigits(count, exact) {
2328
+ let digits = 0;
2329
+ let value2 = 0;
2330
+ while (digits < count || !exact) {
2331
+ let ch = text.charCodeAt(pos);
2332
+ if (ch >= 48 && ch <= 57) {
2333
+ value2 = value2 * 16 + ch - 48;
2334
+ } else if (ch >= 65 && ch <= 70) {
2335
+ value2 = value2 * 16 + ch - 65 + 10;
2336
+ } else if (ch >= 97 && ch <= 102) {
2337
+ value2 = value2 * 16 + ch - 97 + 10;
2338
+ } else {
2339
+ break;
2340
+ }
2341
+ pos++;
2342
+ digits++;
2343
+ }
2344
+ if (digits < count) {
2345
+ value2 = -1;
2346
+ }
2347
+ return value2;
2348
+ }
2349
+ function setPosition(newPosition) {
2350
+ pos = newPosition;
2351
+ value = "";
2352
+ tokenOffset = 0;
2353
+ token = 16;
2354
+ scanError = 0;
2355
+ }
2356
+ function scanNumber() {
2357
+ let start = pos;
2358
+ if (text.charCodeAt(pos) === 48) {
2359
+ pos++;
2360
+ } else {
2361
+ pos++;
2362
+ while (pos < text.length && isDigit(text.charCodeAt(pos))) {
2363
+ pos++;
2364
+ }
2365
+ }
2366
+ if (pos < text.length && text.charCodeAt(pos) === 46) {
2367
+ pos++;
2368
+ if (pos < text.length && isDigit(text.charCodeAt(pos))) {
2369
+ pos++;
2370
+ while (pos < text.length && isDigit(text.charCodeAt(pos))) {
2371
+ pos++;
2372
+ }
2373
+ } else {
2374
+ scanError = 3;
2375
+ return text.substring(start, pos);
2376
+ }
2377
+ }
2378
+ let end = pos;
2379
+ if (pos < text.length && (text.charCodeAt(pos) === 69 || text.charCodeAt(pos) === 101)) {
2380
+ pos++;
2381
+ if (pos < text.length && text.charCodeAt(pos) === 43 || text.charCodeAt(pos) === 45) {
2382
+ pos++;
2383
+ }
2384
+ if (pos < text.length && isDigit(text.charCodeAt(pos))) {
2385
+ pos++;
2386
+ while (pos < text.length && isDigit(text.charCodeAt(pos))) {
2387
+ pos++;
2388
+ }
2389
+ end = pos;
2390
+ } else {
2391
+ scanError = 3;
2392
+ }
2393
+ }
2394
+ return text.substring(start, end);
2395
+ }
2396
+ function scanString() {
2397
+ let result = "", start = pos;
2398
+ while (true) {
2399
+ if (pos >= len) {
2400
+ result += text.substring(start, pos);
2401
+ scanError = 2;
2402
+ break;
2403
+ }
2404
+ const ch = text.charCodeAt(pos);
2405
+ if (ch === 34) {
2406
+ result += text.substring(start, pos);
2407
+ pos++;
2408
+ break;
2409
+ }
2410
+ if (ch === 92) {
2411
+ result += text.substring(start, pos);
2412
+ pos++;
2413
+ if (pos >= len) {
2414
+ scanError = 2;
2415
+ break;
2416
+ }
2417
+ const ch2 = text.charCodeAt(pos++);
2418
+ switch (ch2) {
2419
+ case 34:
2420
+ result += '"';
2421
+ break;
2422
+ case 92:
2423
+ result += "\\";
2424
+ break;
2425
+ case 47:
2426
+ result += "/";
2427
+ break;
2428
+ case 98:
2429
+ result += "\b";
2430
+ break;
2431
+ case 102:
2432
+ result += "\f";
2433
+ break;
2434
+ case 110:
2435
+ result += "\n";
2436
+ break;
2437
+ case 114:
2438
+ result += "\r";
2439
+ break;
2440
+ case 116:
2441
+ result += " ";
2442
+ break;
2443
+ case 117:
2444
+ const ch3 = scanHexDigits(4, true);
2445
+ if (ch3 >= 0) {
2446
+ result += String.fromCharCode(ch3);
2447
+ } else {
2448
+ scanError = 4;
2449
+ }
2450
+ break;
2451
+ default:
2452
+ scanError = 5;
2453
+ }
2454
+ start = pos;
2455
+ continue;
2456
+ }
2457
+ if (ch >= 0 && ch <= 31) {
2458
+ if (isLineBreak(ch)) {
2459
+ result += text.substring(start, pos);
2460
+ scanError = 2;
2461
+ break;
2462
+ } else {
2463
+ scanError = 6;
2464
+ }
2465
+ }
2466
+ pos++;
2467
+ }
2468
+ return result;
2469
+ }
2470
+ function scanNext() {
2471
+ value = "";
2472
+ scanError = 0;
2473
+ tokenOffset = pos;
2474
+ lineStartOffset = lineNumber;
2475
+ prevTokenLineStartOffset = tokenLineStartOffset;
2476
+ if (pos >= len) {
2477
+ tokenOffset = len;
2478
+ return token = 17;
2479
+ }
2480
+ let code = text.charCodeAt(pos);
2481
+ if (isWhiteSpace(code)) {
2482
+ do {
2483
+ pos++;
2484
+ value += String.fromCharCode(code);
2485
+ code = text.charCodeAt(pos);
2486
+ } while (isWhiteSpace(code));
2487
+ return token = 15;
2488
+ }
2489
+ if (isLineBreak(code)) {
2490
+ pos++;
2491
+ value += String.fromCharCode(code);
2492
+ if (code === 13 && text.charCodeAt(pos) === 10) {
2493
+ pos++;
2494
+ value += "\n";
2495
+ }
2496
+ lineNumber++;
2497
+ tokenLineStartOffset = pos;
2498
+ return token = 14;
2499
+ }
2500
+ switch (code) {
2501
+ // tokens: []{}:,
2502
+ case 123:
2503
+ pos++;
2504
+ return token = 1;
2505
+ case 125:
2506
+ pos++;
2507
+ return token = 2;
2508
+ case 91:
2509
+ pos++;
2510
+ return token = 3;
2511
+ case 93:
2512
+ pos++;
2513
+ return token = 4;
2514
+ case 58:
2515
+ pos++;
2516
+ return token = 6;
2517
+ case 44:
2518
+ pos++;
2519
+ return token = 5;
2520
+ // strings
2521
+ case 34:
2522
+ pos++;
2523
+ value = scanString();
2524
+ return token = 10;
2525
+ // comments
2526
+ case 47:
2527
+ const start = pos - 1;
2528
+ if (text.charCodeAt(pos + 1) === 47) {
2529
+ pos += 2;
2530
+ while (pos < len) {
2531
+ if (isLineBreak(text.charCodeAt(pos))) {
2532
+ break;
2533
+ }
2534
+ pos++;
2535
+ }
2536
+ value = text.substring(start, pos);
2537
+ return token = 12;
2538
+ }
2539
+ if (text.charCodeAt(pos + 1) === 42) {
2540
+ pos += 2;
2541
+ const safeLength = len - 1;
2542
+ let commentClosed = false;
2543
+ while (pos < safeLength) {
2544
+ const ch = text.charCodeAt(pos);
2545
+ if (ch === 42 && text.charCodeAt(pos + 1) === 47) {
2546
+ pos += 2;
2547
+ commentClosed = true;
2548
+ break;
2549
+ }
2550
+ pos++;
2551
+ if (isLineBreak(ch)) {
2552
+ if (ch === 13 && text.charCodeAt(pos) === 10) {
2553
+ pos++;
2554
+ }
2555
+ lineNumber++;
2556
+ tokenLineStartOffset = pos;
2557
+ }
2558
+ }
2559
+ if (!commentClosed) {
2560
+ pos++;
2561
+ scanError = 1;
2562
+ }
2563
+ value = text.substring(start, pos);
2564
+ return token = 13;
2565
+ }
2566
+ value += String.fromCharCode(code);
2567
+ pos++;
2568
+ return token = 16;
2569
+ // numbers
2570
+ case 45:
2571
+ value += String.fromCharCode(code);
2572
+ pos++;
2573
+ if (pos === len || !isDigit(text.charCodeAt(pos))) {
2574
+ return token = 16;
2575
+ }
2576
+ // found a minus, followed by a number so
2577
+ // we fall through to proceed with scanning
2578
+ // numbers
2579
+ case 48:
2580
+ case 49:
2581
+ case 50:
2582
+ case 51:
2583
+ case 52:
2584
+ case 53:
2585
+ case 54:
2586
+ case 55:
2587
+ case 56:
2588
+ case 57:
2589
+ value += scanNumber();
2590
+ return token = 11;
2591
+ // literals and unknown symbols
2592
+ default:
2593
+ while (pos < len && isUnknownContentCharacter(code)) {
2594
+ pos++;
2595
+ code = text.charCodeAt(pos);
2596
+ }
2597
+ if (tokenOffset !== pos) {
2598
+ value = text.substring(tokenOffset, pos);
2599
+ switch (value) {
2600
+ case "true":
2601
+ return token = 8;
2602
+ case "false":
2603
+ return token = 9;
2604
+ case "null":
2605
+ return token = 7;
2606
+ }
2607
+ return token = 16;
2608
+ }
2609
+ value += String.fromCharCode(code);
2610
+ pos++;
2611
+ return token = 16;
2612
+ }
2613
+ }
2614
+ function isUnknownContentCharacter(code) {
2615
+ if (isWhiteSpace(code) || isLineBreak(code)) {
2616
+ return false;
2617
+ }
2618
+ switch (code) {
2619
+ case 125:
2620
+ case 93:
2621
+ case 123:
2622
+ case 91:
2623
+ case 34:
2624
+ case 58:
2625
+ case 44:
2626
+ case 47:
2627
+ return false;
2628
+ }
2629
+ return true;
2630
+ }
2631
+ function scanNextNonTrivia() {
2632
+ let result;
2633
+ do {
2634
+ result = scanNext();
2635
+ } while (result >= 12 && result <= 15);
2636
+ return result;
2637
+ }
2638
+ return {
2639
+ setPosition,
2640
+ getPosition: () => pos,
2641
+ scan: ignoreTrivia ? scanNextNonTrivia : scanNext,
2642
+ getToken: () => token,
2643
+ getTokenValue: () => value,
2644
+ getTokenOffset: () => tokenOffset,
2645
+ getTokenLength: () => pos - tokenOffset,
2646
+ getTokenStartLine: () => lineStartOffset,
2647
+ getTokenStartCharacter: () => tokenOffset - prevTokenLineStartOffset,
2648
+ getTokenError: () => scanError
2649
+ };
2650
+ }
2651
+ function isWhiteSpace(ch) {
2652
+ return ch === 32 || ch === 9;
2653
+ }
2654
+ function isLineBreak(ch) {
2655
+ return ch === 10 || ch === 13;
2656
+ }
2657
+ function isDigit(ch) {
2658
+ return ch >= 48 && ch <= 57;
2659
+ }
2660
+ var CharacterCodes;
2661
+ (function(CharacterCodes2) {
2662
+ CharacterCodes2[CharacterCodes2["lineFeed"] = 10] = "lineFeed";
2663
+ CharacterCodes2[CharacterCodes2["carriageReturn"] = 13] = "carriageReturn";
2664
+ CharacterCodes2[CharacterCodes2["space"] = 32] = "space";
2665
+ CharacterCodes2[CharacterCodes2["_0"] = 48] = "_0";
2666
+ CharacterCodes2[CharacterCodes2["_1"] = 49] = "_1";
2667
+ CharacterCodes2[CharacterCodes2["_2"] = 50] = "_2";
2668
+ CharacterCodes2[CharacterCodes2["_3"] = 51] = "_3";
2669
+ CharacterCodes2[CharacterCodes2["_4"] = 52] = "_4";
2670
+ CharacterCodes2[CharacterCodes2["_5"] = 53] = "_5";
2671
+ CharacterCodes2[CharacterCodes2["_6"] = 54] = "_6";
2672
+ CharacterCodes2[CharacterCodes2["_7"] = 55] = "_7";
2673
+ CharacterCodes2[CharacterCodes2["_8"] = 56] = "_8";
2674
+ CharacterCodes2[CharacterCodes2["_9"] = 57] = "_9";
2675
+ CharacterCodes2[CharacterCodes2["a"] = 97] = "a";
2676
+ CharacterCodes2[CharacterCodes2["b"] = 98] = "b";
2677
+ CharacterCodes2[CharacterCodes2["c"] = 99] = "c";
2678
+ CharacterCodes2[CharacterCodes2["d"] = 100] = "d";
2679
+ CharacterCodes2[CharacterCodes2["e"] = 101] = "e";
2680
+ CharacterCodes2[CharacterCodes2["f"] = 102] = "f";
2681
+ CharacterCodes2[CharacterCodes2["g"] = 103] = "g";
2682
+ CharacterCodes2[CharacterCodes2["h"] = 104] = "h";
2683
+ CharacterCodes2[CharacterCodes2["i"] = 105] = "i";
2684
+ CharacterCodes2[CharacterCodes2["j"] = 106] = "j";
2685
+ CharacterCodes2[CharacterCodes2["k"] = 107] = "k";
2686
+ CharacterCodes2[CharacterCodes2["l"] = 108] = "l";
2687
+ CharacterCodes2[CharacterCodes2["m"] = 109] = "m";
2688
+ CharacterCodes2[CharacterCodes2["n"] = 110] = "n";
2689
+ CharacterCodes2[CharacterCodes2["o"] = 111] = "o";
2690
+ CharacterCodes2[CharacterCodes2["p"] = 112] = "p";
2691
+ CharacterCodes2[CharacterCodes2["q"] = 113] = "q";
2692
+ CharacterCodes2[CharacterCodes2["r"] = 114] = "r";
2693
+ CharacterCodes2[CharacterCodes2["s"] = 115] = "s";
2694
+ CharacterCodes2[CharacterCodes2["t"] = 116] = "t";
2695
+ CharacterCodes2[CharacterCodes2["u"] = 117] = "u";
2696
+ CharacterCodes2[CharacterCodes2["v"] = 118] = "v";
2697
+ CharacterCodes2[CharacterCodes2["w"] = 119] = "w";
2698
+ CharacterCodes2[CharacterCodes2["x"] = 120] = "x";
2699
+ CharacterCodes2[CharacterCodes2["y"] = 121] = "y";
2700
+ CharacterCodes2[CharacterCodes2["z"] = 122] = "z";
2701
+ CharacterCodes2[CharacterCodes2["A"] = 65] = "A";
2702
+ CharacterCodes2[CharacterCodes2["B"] = 66] = "B";
2703
+ CharacterCodes2[CharacterCodes2["C"] = 67] = "C";
2704
+ CharacterCodes2[CharacterCodes2["D"] = 68] = "D";
2705
+ CharacterCodes2[CharacterCodes2["E"] = 69] = "E";
2706
+ CharacterCodes2[CharacterCodes2["F"] = 70] = "F";
2707
+ CharacterCodes2[CharacterCodes2["G"] = 71] = "G";
2708
+ CharacterCodes2[CharacterCodes2["H"] = 72] = "H";
2709
+ CharacterCodes2[CharacterCodes2["I"] = 73] = "I";
2710
+ CharacterCodes2[CharacterCodes2["J"] = 74] = "J";
2711
+ CharacterCodes2[CharacterCodes2["K"] = 75] = "K";
2712
+ CharacterCodes2[CharacterCodes2["L"] = 76] = "L";
2713
+ CharacterCodes2[CharacterCodes2["M"] = 77] = "M";
2714
+ CharacterCodes2[CharacterCodes2["N"] = 78] = "N";
2715
+ CharacterCodes2[CharacterCodes2["O"] = 79] = "O";
2716
+ CharacterCodes2[CharacterCodes2["P"] = 80] = "P";
2717
+ CharacterCodes2[CharacterCodes2["Q"] = 81] = "Q";
2718
+ CharacterCodes2[CharacterCodes2["R"] = 82] = "R";
2719
+ CharacterCodes2[CharacterCodes2["S"] = 83] = "S";
2720
+ CharacterCodes2[CharacterCodes2["T"] = 84] = "T";
2721
+ CharacterCodes2[CharacterCodes2["U"] = 85] = "U";
2722
+ CharacterCodes2[CharacterCodes2["V"] = 86] = "V";
2723
+ CharacterCodes2[CharacterCodes2["W"] = 87] = "W";
2724
+ CharacterCodes2[CharacterCodes2["X"] = 88] = "X";
2725
+ CharacterCodes2[CharacterCodes2["Y"] = 89] = "Y";
2726
+ CharacterCodes2[CharacterCodes2["Z"] = 90] = "Z";
2727
+ CharacterCodes2[CharacterCodes2["asterisk"] = 42] = "asterisk";
2728
+ CharacterCodes2[CharacterCodes2["backslash"] = 92] = "backslash";
2729
+ CharacterCodes2[CharacterCodes2["closeBrace"] = 125] = "closeBrace";
2730
+ CharacterCodes2[CharacterCodes2["closeBracket"] = 93] = "closeBracket";
2731
+ CharacterCodes2[CharacterCodes2["colon"] = 58] = "colon";
2732
+ CharacterCodes2[CharacterCodes2["comma"] = 44] = "comma";
2733
+ CharacterCodes2[CharacterCodes2["dot"] = 46] = "dot";
2734
+ CharacterCodes2[CharacterCodes2["doubleQuote"] = 34] = "doubleQuote";
2735
+ CharacterCodes2[CharacterCodes2["minus"] = 45] = "minus";
2736
+ CharacterCodes2[CharacterCodes2["openBrace"] = 123] = "openBrace";
2737
+ CharacterCodes2[CharacterCodes2["openBracket"] = 91] = "openBracket";
2738
+ CharacterCodes2[CharacterCodes2["plus"] = 43] = "plus";
2739
+ CharacterCodes2[CharacterCodes2["slash"] = 47] = "slash";
2740
+ CharacterCodes2[CharacterCodes2["formFeed"] = 12] = "formFeed";
2741
+ CharacterCodes2[CharacterCodes2["tab"] = 9] = "tab";
2742
+ })(CharacterCodes || (CharacterCodes = {}));
2743
+
2744
+ // node_modules/.pnpm/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/string-intern.js
2745
+ var cachedSpaces = new Array(20).fill(0).map((_, index) => {
2746
+ return " ".repeat(index);
2747
+ });
2748
+ var maxCachedValues = 200;
2749
+ var cachedBreakLinesWithSpaces = {
2750
+ " ": {
2751
+ "\n": new Array(maxCachedValues).fill(0).map((_, index) => {
2752
+ return "\n" + " ".repeat(index);
2753
+ }),
2754
+ "\r": new Array(maxCachedValues).fill(0).map((_, index) => {
2755
+ return "\r" + " ".repeat(index);
2756
+ }),
2757
+ "\r\n": new Array(maxCachedValues).fill(0).map((_, index) => {
2758
+ return "\r\n" + " ".repeat(index);
2759
+ })
2760
+ },
2761
+ " ": {
2762
+ "\n": new Array(maxCachedValues).fill(0).map((_, index) => {
2763
+ return "\n" + " ".repeat(index);
2764
+ }),
2765
+ "\r": new Array(maxCachedValues).fill(0).map((_, index) => {
2766
+ return "\r" + " ".repeat(index);
2767
+ }),
2768
+ "\r\n": new Array(maxCachedValues).fill(0).map((_, index) => {
2769
+ return "\r\n" + " ".repeat(index);
2770
+ })
2771
+ }
2772
+ };
2773
+
2774
+ // node_modules/.pnpm/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/parser.js
2775
+ var ParseOptions;
2776
+ (function(ParseOptions2) {
2777
+ ParseOptions2.DEFAULT = {
2778
+ allowTrailingComma: false
2779
+ };
2780
+ })(ParseOptions || (ParseOptions = {}));
2781
+ function parse(text, errors = [], options = ParseOptions.DEFAULT) {
2782
+ let currentProperty = null;
2783
+ let currentParent = [];
2784
+ const previousParents = [];
2785
+ function onValue(value) {
2786
+ if (Array.isArray(currentParent)) {
2787
+ currentParent.push(value);
2788
+ } else if (currentProperty !== null) {
2789
+ currentParent[currentProperty] = value;
2790
+ }
2791
+ }
2792
+ const visitor = {
2793
+ onObjectBegin: () => {
2794
+ const object = {};
2795
+ onValue(object);
2796
+ previousParents.push(currentParent);
2797
+ currentParent = object;
2798
+ currentProperty = null;
2799
+ },
2800
+ onObjectProperty: (name) => {
2801
+ currentProperty = name;
2802
+ },
2803
+ onObjectEnd: () => {
2804
+ currentParent = previousParents.pop();
2805
+ },
2806
+ onArrayBegin: () => {
2807
+ const array = [];
2808
+ onValue(array);
2809
+ previousParents.push(currentParent);
2810
+ currentParent = array;
2811
+ currentProperty = null;
2812
+ },
2813
+ onArrayEnd: () => {
2814
+ currentParent = previousParents.pop();
2815
+ },
2816
+ onLiteralValue: onValue,
2817
+ onError: (error, offset, length) => {
2818
+ errors.push({ error, offset, length });
2819
+ }
2820
+ };
2821
+ visit(text, visitor, options);
2822
+ return currentParent[0];
2823
+ }
2824
+ function visit(text, visitor, options = ParseOptions.DEFAULT) {
2825
+ const _scanner = createScanner(text, false);
2826
+ const _jsonPath = [];
2827
+ let suppressedCallbacks = 0;
2828
+ function toNoArgVisit(visitFunction) {
2829
+ return visitFunction ? () => suppressedCallbacks === 0 && visitFunction(_scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter()) : () => true;
2830
+ }
2831
+ function toOneArgVisit(visitFunction) {
2832
+ return visitFunction ? (arg) => suppressedCallbacks === 0 && visitFunction(arg, _scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter()) : () => true;
2833
+ }
2834
+ function toOneArgVisitWithPath(visitFunction) {
2835
+ return visitFunction ? (arg) => suppressedCallbacks === 0 && visitFunction(arg, _scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter(), () => _jsonPath.slice()) : () => true;
2836
+ }
2837
+ function toBeginVisit(visitFunction) {
2838
+ return visitFunction ? () => {
2839
+ if (suppressedCallbacks > 0) {
2840
+ suppressedCallbacks++;
2841
+ } else {
2842
+ let cbReturn = visitFunction(_scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter(), () => _jsonPath.slice());
2843
+ if (cbReturn === false) {
2844
+ suppressedCallbacks = 1;
2845
+ }
2846
+ }
2847
+ } : () => true;
2848
+ }
2849
+ function toEndVisit(visitFunction) {
2850
+ return visitFunction ? () => {
2851
+ if (suppressedCallbacks > 0) {
2852
+ suppressedCallbacks--;
2853
+ }
2854
+ if (suppressedCallbacks === 0) {
2855
+ visitFunction(_scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter());
2856
+ }
2857
+ } : () => true;
2858
+ }
2859
+ const onObjectBegin = toBeginVisit(visitor.onObjectBegin), onObjectProperty = toOneArgVisitWithPath(visitor.onObjectProperty), onObjectEnd = toEndVisit(visitor.onObjectEnd), onArrayBegin = toBeginVisit(visitor.onArrayBegin), onArrayEnd = toEndVisit(visitor.onArrayEnd), onLiteralValue = toOneArgVisitWithPath(visitor.onLiteralValue), onSeparator = toOneArgVisit(visitor.onSeparator), onComment = toNoArgVisit(visitor.onComment), onError = toOneArgVisit(visitor.onError);
2860
+ const disallowComments = options && options.disallowComments;
2861
+ const allowTrailingComma = options && options.allowTrailingComma;
2862
+ function scanNext() {
2863
+ while (true) {
2864
+ const token = _scanner.scan();
2865
+ switch (_scanner.getTokenError()) {
2866
+ case 4:
2867
+ handleError2(
2868
+ 14
2869
+ /* ParseErrorCode.InvalidUnicode */
2870
+ );
2871
+ break;
2872
+ case 5:
2873
+ handleError2(
2874
+ 15
2875
+ /* ParseErrorCode.InvalidEscapeCharacter */
2876
+ );
2877
+ break;
2878
+ case 3:
2879
+ handleError2(
2880
+ 13
2881
+ /* ParseErrorCode.UnexpectedEndOfNumber */
2882
+ );
2883
+ break;
2884
+ case 1:
2885
+ if (!disallowComments) {
2886
+ handleError2(
2887
+ 11
2888
+ /* ParseErrorCode.UnexpectedEndOfComment */
2889
+ );
2890
+ }
2891
+ break;
2892
+ case 2:
2893
+ handleError2(
2894
+ 12
2895
+ /* ParseErrorCode.UnexpectedEndOfString */
2896
+ );
2897
+ break;
2898
+ case 6:
2899
+ handleError2(
2900
+ 16
2901
+ /* ParseErrorCode.InvalidCharacter */
2902
+ );
2903
+ break;
2904
+ }
2905
+ switch (token) {
2906
+ case 12:
2907
+ case 13:
2908
+ if (disallowComments) {
2909
+ handleError2(
2910
+ 10
2911
+ /* ParseErrorCode.InvalidCommentToken */
2912
+ );
2913
+ } else {
2914
+ onComment();
2915
+ }
2916
+ break;
2917
+ case 16:
2918
+ handleError2(
2919
+ 1
2920
+ /* ParseErrorCode.InvalidSymbol */
2921
+ );
2922
+ break;
2923
+ case 15:
2924
+ case 14:
2925
+ break;
2926
+ default:
2927
+ return token;
2928
+ }
2929
+ }
2930
+ }
2931
+ function handleError2(error, skipUntilAfter = [], skipUntil = []) {
2932
+ onError(error);
2933
+ if (skipUntilAfter.length + skipUntil.length > 0) {
2934
+ let token = _scanner.getToken();
2935
+ while (token !== 17) {
2936
+ if (skipUntilAfter.indexOf(token) !== -1) {
2937
+ scanNext();
2938
+ break;
2939
+ } else if (skipUntil.indexOf(token) !== -1) {
2940
+ break;
2941
+ }
2942
+ token = scanNext();
2943
+ }
2944
+ }
2945
+ }
2946
+ function parseString(isValue) {
2947
+ const value = _scanner.getTokenValue();
2948
+ if (isValue) {
2949
+ onLiteralValue(value);
2950
+ } else {
2951
+ onObjectProperty(value);
2952
+ _jsonPath.push(value);
2953
+ }
2954
+ scanNext();
2955
+ return true;
2956
+ }
2957
+ function parseLiteral() {
2958
+ switch (_scanner.getToken()) {
2959
+ case 11:
2960
+ const tokenValue = _scanner.getTokenValue();
2961
+ let value = Number(tokenValue);
2962
+ if (isNaN(value)) {
2963
+ handleError2(
2964
+ 2
2965
+ /* ParseErrorCode.InvalidNumberFormat */
2966
+ );
2967
+ value = 0;
2968
+ }
2969
+ onLiteralValue(value);
2970
+ break;
2971
+ case 7:
2972
+ onLiteralValue(null);
2973
+ break;
2974
+ case 8:
2975
+ onLiteralValue(true);
2976
+ break;
2977
+ case 9:
2978
+ onLiteralValue(false);
2979
+ break;
2980
+ default:
2981
+ return false;
2982
+ }
2983
+ scanNext();
2984
+ return true;
2985
+ }
2986
+ function parseProperty() {
2987
+ if (_scanner.getToken() !== 10) {
2988
+ handleError2(3, [], [
2989
+ 2,
2990
+ 5
2991
+ /* SyntaxKind.CommaToken */
2992
+ ]);
2993
+ return false;
2994
+ }
2995
+ parseString(false);
2996
+ if (_scanner.getToken() === 6) {
2997
+ onSeparator(":");
2998
+ scanNext();
2999
+ if (!parseValue()) {
3000
+ handleError2(4, [], [
3001
+ 2,
3002
+ 5
3003
+ /* SyntaxKind.CommaToken */
3004
+ ]);
3005
+ }
3006
+ } else {
3007
+ handleError2(5, [], [
3008
+ 2,
3009
+ 5
3010
+ /* SyntaxKind.CommaToken */
3011
+ ]);
3012
+ }
3013
+ _jsonPath.pop();
3014
+ return true;
3015
+ }
3016
+ function parseObject() {
3017
+ onObjectBegin();
3018
+ scanNext();
3019
+ let needsComma = false;
3020
+ while (_scanner.getToken() !== 2 && _scanner.getToken() !== 17) {
3021
+ if (_scanner.getToken() === 5) {
3022
+ if (!needsComma) {
3023
+ handleError2(4, [], []);
3024
+ }
3025
+ onSeparator(",");
3026
+ scanNext();
3027
+ if (_scanner.getToken() === 2 && allowTrailingComma) {
3028
+ break;
3029
+ }
3030
+ } else if (needsComma) {
3031
+ handleError2(6, [], []);
3032
+ }
3033
+ if (!parseProperty()) {
3034
+ handleError2(4, [], [
3035
+ 2,
3036
+ 5
3037
+ /* SyntaxKind.CommaToken */
3038
+ ]);
3039
+ }
3040
+ needsComma = true;
3041
+ }
3042
+ onObjectEnd();
3043
+ if (_scanner.getToken() !== 2) {
3044
+ handleError2(7, [
3045
+ 2
3046
+ /* SyntaxKind.CloseBraceToken */
3047
+ ], []);
3048
+ } else {
3049
+ scanNext();
3050
+ }
3051
+ return true;
3052
+ }
3053
+ function parseArray() {
3054
+ onArrayBegin();
3055
+ scanNext();
3056
+ let isFirstElement = true;
3057
+ let needsComma = false;
3058
+ while (_scanner.getToken() !== 4 && _scanner.getToken() !== 17) {
3059
+ if (_scanner.getToken() === 5) {
3060
+ if (!needsComma) {
3061
+ handleError2(4, [], []);
3062
+ }
3063
+ onSeparator(",");
3064
+ scanNext();
3065
+ if (_scanner.getToken() === 4 && allowTrailingComma) {
3066
+ break;
3067
+ }
3068
+ } else if (needsComma) {
3069
+ handleError2(6, [], []);
3070
+ }
3071
+ if (isFirstElement) {
3072
+ _jsonPath.push(0);
3073
+ isFirstElement = false;
3074
+ } else {
3075
+ _jsonPath[_jsonPath.length - 1]++;
3076
+ }
3077
+ if (!parseValue()) {
3078
+ handleError2(4, [], [
3079
+ 4,
3080
+ 5
3081
+ /* SyntaxKind.CommaToken */
3082
+ ]);
3083
+ }
3084
+ needsComma = true;
3085
+ }
3086
+ onArrayEnd();
3087
+ if (!isFirstElement) {
3088
+ _jsonPath.pop();
3089
+ }
3090
+ if (_scanner.getToken() !== 4) {
3091
+ handleError2(8, [
3092
+ 4
3093
+ /* SyntaxKind.CloseBracketToken */
3094
+ ], []);
3095
+ } else {
3096
+ scanNext();
3097
+ }
3098
+ return true;
3099
+ }
3100
+ function parseValue() {
3101
+ switch (_scanner.getToken()) {
3102
+ case 3:
3103
+ return parseArray();
3104
+ case 1:
3105
+ return parseObject();
3106
+ case 10:
3107
+ return parseString(true);
3108
+ default:
3109
+ return parseLiteral();
3110
+ }
3111
+ }
3112
+ scanNext();
3113
+ if (_scanner.getToken() === 17) {
3114
+ if (options.allowEmptyContent) {
3115
+ return true;
3116
+ }
3117
+ handleError2(4, [], []);
3118
+ return false;
3119
+ }
3120
+ if (!parseValue()) {
3121
+ handleError2(4, [], []);
3122
+ return false;
3123
+ }
3124
+ if (_scanner.getToken() !== 17) {
3125
+ handleError2(9, [], []);
3126
+ }
3127
+ return true;
3128
+ }
3129
+
3130
+ // node_modules/.pnpm/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/main.js
3131
+ var ScanError;
3132
+ (function(ScanError2) {
3133
+ ScanError2[ScanError2["None"] = 0] = "None";
3134
+ ScanError2[ScanError2["UnexpectedEndOfComment"] = 1] = "UnexpectedEndOfComment";
3135
+ ScanError2[ScanError2["UnexpectedEndOfString"] = 2] = "UnexpectedEndOfString";
3136
+ ScanError2[ScanError2["UnexpectedEndOfNumber"] = 3] = "UnexpectedEndOfNumber";
3137
+ ScanError2[ScanError2["InvalidUnicode"] = 4] = "InvalidUnicode";
3138
+ ScanError2[ScanError2["InvalidEscapeCharacter"] = 5] = "InvalidEscapeCharacter";
3139
+ ScanError2[ScanError2["InvalidCharacter"] = 6] = "InvalidCharacter";
3140
+ })(ScanError || (ScanError = {}));
3141
+ var SyntaxKind;
3142
+ (function(SyntaxKind2) {
3143
+ SyntaxKind2[SyntaxKind2["OpenBraceToken"] = 1] = "OpenBraceToken";
3144
+ SyntaxKind2[SyntaxKind2["CloseBraceToken"] = 2] = "CloseBraceToken";
3145
+ SyntaxKind2[SyntaxKind2["OpenBracketToken"] = 3] = "OpenBracketToken";
3146
+ SyntaxKind2[SyntaxKind2["CloseBracketToken"] = 4] = "CloseBracketToken";
3147
+ SyntaxKind2[SyntaxKind2["CommaToken"] = 5] = "CommaToken";
3148
+ SyntaxKind2[SyntaxKind2["ColonToken"] = 6] = "ColonToken";
3149
+ SyntaxKind2[SyntaxKind2["NullKeyword"] = 7] = "NullKeyword";
3150
+ SyntaxKind2[SyntaxKind2["TrueKeyword"] = 8] = "TrueKeyword";
3151
+ SyntaxKind2[SyntaxKind2["FalseKeyword"] = 9] = "FalseKeyword";
3152
+ SyntaxKind2[SyntaxKind2["StringLiteral"] = 10] = "StringLiteral";
3153
+ SyntaxKind2[SyntaxKind2["NumericLiteral"] = 11] = "NumericLiteral";
3154
+ SyntaxKind2[SyntaxKind2["LineCommentTrivia"] = 12] = "LineCommentTrivia";
3155
+ SyntaxKind2[SyntaxKind2["BlockCommentTrivia"] = 13] = "BlockCommentTrivia";
3156
+ SyntaxKind2[SyntaxKind2["LineBreakTrivia"] = 14] = "LineBreakTrivia";
3157
+ SyntaxKind2[SyntaxKind2["Trivia"] = 15] = "Trivia";
3158
+ SyntaxKind2[SyntaxKind2["Unknown"] = 16] = "Unknown";
3159
+ SyntaxKind2[SyntaxKind2["EOF"] = 17] = "EOF";
3160
+ })(SyntaxKind || (SyntaxKind = {}));
3161
+ var parse2 = parse;
3162
+ var ParseErrorCode;
3163
+ (function(ParseErrorCode2) {
3164
+ ParseErrorCode2[ParseErrorCode2["InvalidSymbol"] = 1] = "InvalidSymbol";
3165
+ ParseErrorCode2[ParseErrorCode2["InvalidNumberFormat"] = 2] = "InvalidNumberFormat";
3166
+ ParseErrorCode2[ParseErrorCode2["PropertyNameExpected"] = 3] = "PropertyNameExpected";
3167
+ ParseErrorCode2[ParseErrorCode2["ValueExpected"] = 4] = "ValueExpected";
3168
+ ParseErrorCode2[ParseErrorCode2["ColonExpected"] = 5] = "ColonExpected";
3169
+ ParseErrorCode2[ParseErrorCode2["CommaExpected"] = 6] = "CommaExpected";
3170
+ ParseErrorCode2[ParseErrorCode2["CloseBraceExpected"] = 7] = "CloseBraceExpected";
3171
+ ParseErrorCode2[ParseErrorCode2["CloseBracketExpected"] = 8] = "CloseBracketExpected";
3172
+ ParseErrorCode2[ParseErrorCode2["EndOfFileExpected"] = 9] = "EndOfFileExpected";
3173
+ ParseErrorCode2[ParseErrorCode2["InvalidCommentToken"] = 10] = "InvalidCommentToken";
3174
+ ParseErrorCode2[ParseErrorCode2["UnexpectedEndOfComment"] = 11] = "UnexpectedEndOfComment";
3175
+ ParseErrorCode2[ParseErrorCode2["UnexpectedEndOfString"] = 12] = "UnexpectedEndOfString";
3176
+ ParseErrorCode2[ParseErrorCode2["UnexpectedEndOfNumber"] = 13] = "UnexpectedEndOfNumber";
3177
+ ParseErrorCode2[ParseErrorCode2["InvalidUnicode"] = 14] = "InvalidUnicode";
3178
+ ParseErrorCode2[ParseErrorCode2["InvalidEscapeCharacter"] = 15] = "InvalidEscapeCharacter";
3179
+ ParseErrorCode2[ParseErrorCode2["InvalidCharacter"] = 16] = "InvalidCharacter";
3180
+ })(ParseErrorCode || (ParseErrorCode = {}));
3181
+
3182
+ // src/validation/config/scope.ts
3183
+ import { existsSync, readdirSync, readFileSync } from "fs";
3184
+ import { join as join11 } from "path";
3185
+ var TSCONFIG_FILES = {
3186
+ full: "tsconfig.json",
3187
+ production: "tsconfig.production.json"
3188
+ };
3189
+ var defaultScopeDeps = {
3190
+ readFileSync,
3191
+ existsSync,
3192
+ readdirSync
3193
+ };
3194
+ function parseTypeScriptConfig(configPath, deps = defaultScopeDeps) {
3195
+ try {
3196
+ const configContent = deps.readFileSync(configPath, "utf-8");
3197
+ const parsed = parse2(configContent);
3198
+ return parsed;
3199
+ } catch {
3200
+ return {
3201
+ include: ["**/*.ts", "**/*.tsx"],
3202
+ exclude: ["node_modules/**", ".pnpm-store/**", "dist/**"]
3203
+ };
3204
+ }
3205
+ }
3206
+ function resolveTypeScriptConfig(scope, deps = defaultScopeDeps) {
3207
+ const configFile = TSCONFIG_FILES[scope];
3208
+ const config = parseTypeScriptConfig(configFile, deps);
3209
+ if (config.extends) {
3210
+ const baseConfig = parseTypeScriptConfig(config.extends, deps);
3211
+ return {
3212
+ include: config.include ?? baseConfig.include ?? [],
3213
+ exclude: [...baseConfig.exclude ?? [], ...config.exclude ?? []]
3214
+ };
3215
+ }
3216
+ return {
3217
+ include: config.include ?? [],
3218
+ exclude: config.exclude ?? []
3219
+ };
3220
+ }
3221
+ function hasTypeScriptFilesRecursive(dirPath, maxDepth = 2, deps = defaultScopeDeps) {
3222
+ if (maxDepth <= 0) return false;
3223
+ try {
3224
+ const items = deps.readdirSync(dirPath, { withFileTypes: true });
3225
+ const hasDirectTsFiles = items.some(
3226
+ (item) => item.isFile() && (item.name.endsWith(".ts") || item.name.endsWith(".tsx"))
3227
+ );
3228
+ if (hasDirectTsFiles) return true;
3229
+ const subdirs = items.filter((item) => item.isDirectory() && !item.name.startsWith("."));
3230
+ for (const subdir of subdirs.slice(0, 5)) {
3231
+ if (hasTypeScriptFilesRecursive(join11(dirPath, subdir.name), maxDepth - 1, deps)) {
3232
+ return true;
3233
+ }
3234
+ }
3235
+ return false;
3236
+ } catch {
3237
+ return false;
3238
+ }
3239
+ }
3240
+ function getTopLevelDirectoriesWithTypeScript(config, deps = defaultScopeDeps) {
3241
+ const allTopLevelItems = deps.readdirSync(".", { withFileTypes: true });
3242
+ const directories = /* @__PURE__ */ new Set();
3243
+ const topLevelDirs = allTopLevelItems.filter((item) => item.isDirectory()).map((item) => item.name).filter((name) => !name.startsWith("."));
3244
+ for (const dir of topLevelDirs) {
3245
+ const isExcluded = config.exclude?.some((pattern) => {
3246
+ if (pattern.includes("/**")) {
3247
+ const dirPattern = pattern.split("/**")[0];
3248
+ return dirPattern === dir;
3249
+ }
3250
+ return pattern === dir || pattern.startsWith(dir + "/") || pattern === dir + "/**";
3251
+ });
3252
+ if (!isExcluded) {
3253
+ try {
3254
+ const hasTypeScriptFiles = hasTypeScriptFilesRecursive(dir, 2, deps);
3255
+ if (hasTypeScriptFiles) {
3256
+ directories.add(dir);
3257
+ }
3258
+ } catch {
3259
+ continue;
3260
+ }
3261
+ }
3262
+ }
3263
+ if (config.include) {
3264
+ for (const pattern of config.include) {
3265
+ if (pattern.includes("/")) {
3266
+ const topLevelDir = pattern.split("/")[0];
3267
+ if (topLevelDir && !topLevelDir.includes("*") && !topLevelDir.startsWith(".")) {
3268
+ directories.add(topLevelDir);
3269
+ }
3270
+ }
3271
+ }
3272
+ }
3273
+ return Array.from(directories).sort();
3274
+ }
3275
+ function getValidationDirectories(scope, deps = defaultScopeDeps) {
3276
+ const config = resolveTypeScriptConfig(scope, deps);
3277
+ const configDirectories = getTopLevelDirectoriesWithTypeScript(config, deps);
3278
+ const existingDirectories = configDirectories.filter((dir) => deps.existsSync(dir));
3279
+ return existingDirectories;
3280
+ }
3281
+ function getTypeScriptScope(scope, deps = defaultScopeDeps) {
3282
+ const directories = getValidationDirectories(scope, deps);
3283
+ const config = resolveTypeScriptConfig(scope, deps);
3284
+ return {
3285
+ directories,
3286
+ filePatterns: config.include ?? [],
3287
+ excludePatterns: config.exclude ?? []
3288
+ };
3289
+ }
3290
+
3291
+ // src/validation/discovery/tool-finder.ts
3292
+ import { execSync } from "child_process";
3293
+ import fs6 from "fs";
3294
+ import { createRequire } from "module";
3295
+ import path7 from "path";
3296
+
3297
+ // src/validation/discovery/constants.ts
3298
+ var TOOL_DISCOVERY = {
3299
+ /** Tool source identifiers */
3300
+ SOURCES: {
3301
+ /** Tool bundled with spx-cli */
3302
+ BUNDLED: "bundled",
3303
+ /** Tool in project's node_modules */
3304
+ PROJECT: "project",
3305
+ /** Tool in system PATH */
3306
+ GLOBAL: "global"
3307
+ },
3308
+ /** Message templates */
3309
+ MESSAGES: {
3310
+ /** Prefix for skip messages */
3311
+ SKIP_PREFIX: "\u23ED",
3312
+ // ⏭ emoji
3313
+ /**
3314
+ * Format not found reason message.
3315
+ * @param tool - The tool name that was not found
3316
+ */
3317
+ NOT_FOUND_REASON: (tool) => `${tool} not found in bundled deps, project node_modules, or system PATH`,
3318
+ /**
3319
+ * Format skip message for graceful degradation.
3320
+ * @param step - The validation step name
3321
+ * @param tool - The tool that was not found
3322
+ */
3323
+ SKIP_FORMAT: (step, tool) => `${TOOL_DISCOVERY.MESSAGES.SKIP_PREFIX} Skipping ${step} (${tool} not available)`
3324
+ }
3325
+ };
3326
+
3327
+ // src/validation/discovery/tool-finder.ts
3328
+ var require2 = createRequire(import.meta.url);
3329
+ var defaultToolDiscoveryDeps = {
3330
+ resolveModule: (modulePath) => {
3331
+ try {
3332
+ return require2.resolve(modulePath);
3333
+ } catch {
3334
+ return null;
3335
+ }
3336
+ },
3337
+ existsSync: fs6.existsSync,
3338
+ whichSync: (tool) => {
3339
+ try {
3340
+ const result = execSync(`which ${tool}`, {
3341
+ encoding: "utf-8",
3342
+ stdio: ["pipe", "pipe", "pipe"]
3343
+ });
3344
+ return result.trim() || null;
3345
+ } catch {
3346
+ return null;
3347
+ }
3348
+ }
3349
+ };
3350
+ async function discoverTool(tool, options = {}) {
3351
+ const { projectRoot = process.cwd(), deps = defaultToolDiscoveryDeps } = options;
3352
+ const bundledPath = deps.resolveModule(`${tool}/package.json`);
3353
+ if (bundledPath) {
3354
+ return {
3355
+ found: true,
3356
+ location: {
3357
+ tool,
3358
+ path: path7.dirname(bundledPath),
3359
+ source: TOOL_DISCOVERY.SOURCES.BUNDLED
3360
+ }
3361
+ };
3362
+ }
3363
+ const projectBinPath = path7.join(projectRoot, "node_modules", ".bin", tool);
3364
+ if (deps.existsSync(projectBinPath)) {
3365
+ return {
3366
+ found: true,
3367
+ location: {
3368
+ tool,
3369
+ path: projectBinPath,
3370
+ source: TOOL_DISCOVERY.SOURCES.PROJECT
3371
+ }
3372
+ };
3373
+ }
3374
+ const globalPath = deps.whichSync(tool);
3375
+ if (globalPath) {
3376
+ return {
3377
+ found: true,
3378
+ location: {
3379
+ tool,
3380
+ path: globalPath,
3381
+ source: TOOL_DISCOVERY.SOURCES.GLOBAL
3382
+ }
3383
+ };
3384
+ }
3385
+ return {
3386
+ found: false,
3387
+ notFound: {
3388
+ tool,
3389
+ reason: TOOL_DISCOVERY.MESSAGES.NOT_FOUND_REASON(tool)
3390
+ }
3391
+ };
3392
+ }
3393
+ function formatSkipMessage(stepName, result) {
3394
+ if (result.found) {
3395
+ return "";
3396
+ }
3397
+ return TOOL_DISCOVERY.MESSAGES.SKIP_FORMAT(stepName, result.notFound.tool);
3398
+ }
3399
+
3400
+ // src/validation/steps/circular.ts
3401
+ import madge from "madge";
3402
+
3403
+ // src/validation/steps/constants.ts
3404
+ var STEP_IDS = {
3405
+ CIRCULAR: "circular-deps",
3406
+ ESLINT: "eslint",
3407
+ TYPESCRIPT: "typescript",
3408
+ KNIP: "knip"
3409
+ };
3410
+ var STEP_NAMES = {
3411
+ CIRCULAR: "Circular Dependencies",
3412
+ ESLINT: "ESLint",
3413
+ TYPESCRIPT: "TypeScript",
3414
+ KNIP: "Unused Code"
3415
+ };
3416
+ var STEP_DESCRIPTIONS = {
3417
+ CIRCULAR: "Checking for circular dependencies",
3418
+ ESLINT: "Validating ESLint compliance",
3419
+ TYPESCRIPT: "Validating TypeScript",
3420
+ KNIP: "Detecting unused exports, dependencies, and files"
3421
+ };
3422
+ var CACHE_PATHS = {
3423
+ ESLINT: "dist/.eslintcache",
3424
+ TIMINGS: "dist/.validation-timings.json"
3425
+ };
3426
+ var VALIDATION_KEYS = {
3427
+ TYPESCRIPT: "TYPESCRIPT",
3428
+ ESLINT: "ESLINT",
3429
+ KNIP: "KNIP"
3430
+ };
3431
+ var VALIDATION_DEFAULTS = {
3432
+ [VALIDATION_KEYS.TYPESCRIPT]: true,
3433
+ [VALIDATION_KEYS.ESLINT]: true,
3434
+ [VALIDATION_KEYS.KNIP]: false
3435
+ };
3436
+
3437
+ // src/validation/steps/circular.ts
3438
+ var defaultCircularDeps = {
3439
+ madge
3440
+ };
3441
+ async function validateCircularDependencies(scope, typescriptScope, deps = defaultCircularDeps) {
3442
+ try {
3443
+ const analyzeDirectories = typescriptScope.directories;
3444
+ if (analyzeDirectories.length === 0) {
3445
+ return { success: true };
3446
+ }
3447
+ const tsConfigFile = TSCONFIG_FILES[scope];
3448
+ const excludeRegExps = typescriptScope.excludePatterns.map((pattern) => {
3449
+ const cleanPattern = pattern.replace(/\/\*\*?\/\*$/, "");
3450
+ const escaped = cleanPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3451
+ return new RegExp(escaped);
3452
+ });
3453
+ const result = await deps.madge(analyzeDirectories, {
3454
+ fileExtensions: ["ts", "tsx"],
3455
+ tsConfig: tsConfigFile,
3456
+ excludeRegExp: excludeRegExps
3457
+ });
3458
+ const circular = result.circular();
3459
+ if (circular.length === 0) {
3460
+ return { success: true };
3461
+ } else {
3462
+ return {
3463
+ success: false,
3464
+ error: `Found ${circular.length} circular dependency cycle(s)`,
3465
+ circularDependencies: circular
3466
+ };
3467
+ }
3468
+ } catch (error) {
3469
+ const errorMessage = error instanceof Error ? error.message : String(error);
3470
+ return { success: false, error: errorMessage };
3471
+ }
3472
+ }
3473
+ var circularDependencyStep = {
3474
+ id: STEP_IDS.CIRCULAR,
3475
+ name: STEP_NAMES.CIRCULAR,
3476
+ description: STEP_DESCRIPTIONS.CIRCULAR,
3477
+ enabled: (context) => !context.isFileSpecificMode && context.enabledValidations[VALIDATION_KEYS.TYPESCRIPT] === true,
3478
+ execute: async (context) => {
3479
+ const startTime = performance.now();
3480
+ try {
3481
+ const result = await validateCircularDependencies(context.scope, context.scopeConfig);
3482
+ return {
3483
+ success: result.success,
3484
+ error: result.error,
3485
+ duration: performance.now() - startTime
3486
+ };
3487
+ } catch (error) {
3488
+ return {
3489
+ success: false,
3490
+ error: error instanceof Error ? error.message : String(error),
3491
+ duration: performance.now() - startTime
3492
+ };
3493
+ }
3494
+ }
3495
+ };
3496
+
3497
+ // src/commands/validation/circular.ts
3498
+ async function circularCommand(options) {
3499
+ const { cwd, quiet } = options;
3500
+ const startTime = Date.now();
3501
+ const toolResult = await discoverTool("madge", { projectRoot: cwd });
3502
+ if (!toolResult.found) {
3503
+ const skipMessage = formatSkipMessage("circular dependency check", toolResult);
3504
+ return { exitCode: 0, output: skipMessage, durationMs: Date.now() - startTime };
3505
+ }
3506
+ const scopeConfig = getTypeScriptScope("full");
3507
+ const result = await validateCircularDependencies("full", scopeConfig);
3508
+ const durationMs = Date.now() - startTime;
3509
+ if (result.success) {
3510
+ const output = quiet ? "" : `Circular dependencies: \u2713 None found`;
3511
+ return { exitCode: 0, output, durationMs };
3512
+ } else {
3513
+ let output = result.error ?? "Circular dependencies found";
3514
+ if (result.circularDependencies && result.circularDependencies.length > 0) {
3515
+ const cycles = result.circularDependencies.map((cycle) => ` ${cycle.join(" \u2192 ")}`).join("\n");
3516
+ output = `Circular dependencies found:
3517
+ ${cycles}`;
3518
+ }
3519
+ return { exitCode: 1, output, durationMs };
3520
+ }
3521
+ }
3522
+
3523
+ // src/commands/validation/format.ts
3524
+ var DURATION_THRESHOLD_MS = 1e3;
3525
+ var VALIDATION_SYMBOLS = {
3526
+ SUCCESS: "\u2713",
3527
+ FAILURE: "\u2717"
3528
+ };
3529
+ function formatDuration(ms) {
3530
+ if (ms < DURATION_THRESHOLD_MS) {
3531
+ return `${ms}ms`;
3532
+ }
3533
+ const seconds = ms / 1e3;
3534
+ return `${seconds.toFixed(1)}s`;
3535
+ }
3536
+ function formatSummary(options) {
3537
+ const { success, totalDurationMs } = options;
3538
+ const symbol = success ? VALIDATION_SYMBOLS.SUCCESS : VALIDATION_SYMBOLS.FAILURE;
3539
+ const status = success ? "passed" : "failed";
3540
+ const duration = formatDuration(totalDurationMs);
3541
+ return `${symbol} Validation ${status} (${duration} total)`;
3542
+ }
3543
+
3544
+ // src/validation/steps/eslint.ts
3545
+ import { spawn } from "child_process";
3546
+
3547
+ // src/validation/types.ts
3548
+ var VALIDATION_SCOPES = {
3549
+ /** Validate entire codebase including tests and scripts */
3550
+ FULL: "full",
3551
+ /** Validate production files only */
3552
+ PRODUCTION: "production"
3553
+ };
3554
+ var EXECUTION_MODES = {
3555
+ /** Read-only mode - report errors without fixing */
3556
+ READ: "read",
3557
+ /** Write mode - fix errors when possible (e.g., eslint --fix) */
3558
+ WRITE: "write"
3559
+ };
3560
+
3561
+ // src/validation/steps/eslint.ts
3562
+ var defaultEslintProcessRunner = { spawn };
3563
+ function buildEslintArgs(context) {
3564
+ const { validatedFiles, mode, cacheFile } = context;
3565
+ const fixArg = mode === EXECUTION_MODES.WRITE ? ["--fix"] : [];
3566
+ const cacheArgs = ["--cache", "--cache-location", cacheFile];
3567
+ if (validatedFiles && validatedFiles.length > 0) {
3568
+ return ["eslint", "--config", "eslint.config.ts", ...cacheArgs, ...fixArg, "--", ...validatedFiles];
3569
+ }
3570
+ return ["eslint", ".", "--config", "eslint.config.ts", ...cacheArgs, ...fixArg];
3571
+ }
3572
+ async function validateESLint(context, runner = defaultEslintProcessRunner) {
3573
+ const { scope, validatedFiles, mode } = context;
3574
+ return new Promise((resolve2) => {
3575
+ if (!validatedFiles || validatedFiles.length === 0) {
3576
+ if (scope === VALIDATION_SCOPES.PRODUCTION) {
3577
+ process.env.ESLINT_PRODUCTION_ONLY = "1";
3578
+ } else {
3579
+ delete process.env.ESLINT_PRODUCTION_ONLY;
3580
+ }
3581
+ }
3582
+ const eslintArgs = buildEslintArgs({
3583
+ validatedFiles,
3584
+ mode,
3585
+ cacheFile: CACHE_PATHS.ESLINT
3586
+ });
3587
+ const eslintProcess = runner.spawn("npx", eslintArgs, {
3588
+ cwd: process.cwd(),
3589
+ stdio: "inherit"
3590
+ });
3591
+ eslintProcess.on("close", (code) => {
3592
+ if (code === 0) {
3593
+ resolve2({ success: true });
3594
+ } else {
3595
+ resolve2({ success: false, error: `ESLint exited with code ${code}` });
3596
+ }
3597
+ });
3598
+ eslintProcess.on("error", (error) => {
3599
+ resolve2({ success: false, error: error.message });
3600
+ });
3601
+ });
3602
+ }
3603
+ function validationEnabled(envVarKey, defaults = {}) {
3604
+ const envVar = `${envVarKey}_VALIDATION_ENABLED`;
3605
+ const explicitlyDisabled = process.env[envVar] === "0";
3606
+ const explicitlyEnabled = process.env[envVar] === "1";
3607
+ const defaultValue = defaults[envVarKey] ?? true;
3608
+ if (defaultValue) {
3609
+ return !explicitlyDisabled;
3610
+ }
3611
+ return explicitlyEnabled;
3612
+ }
3613
+ var eslintStep = {
3614
+ id: STEP_IDS.ESLINT,
3615
+ name: STEP_NAMES.ESLINT,
3616
+ description: STEP_DESCRIPTIONS.ESLINT,
3617
+ enabled: (context) => context.enabledValidations[VALIDATION_KEYS.ESLINT] === true && validationEnabled(VALIDATION_KEYS.ESLINT),
3618
+ execute: async (context) => {
3619
+ const startTime = performance.now();
3620
+ try {
3621
+ const result = await validateESLint(context);
3622
+ return {
3623
+ success: result.success,
3624
+ error: result.error,
3625
+ duration: performance.now() - startTime
3626
+ };
3627
+ } catch (error) {
3628
+ return {
3629
+ success: false,
3630
+ error: error instanceof Error ? error.message : String(error),
3631
+ duration: performance.now() - startTime
3632
+ };
3633
+ }
3634
+ }
3635
+ };
3636
+
3637
+ // src/validation/steps/knip.ts
3638
+ import { spawn as spawn2 } from "child_process";
3639
+ var defaultKnipProcessRunner = { spawn: spawn2 };
3640
+ async function validateKnip(typescriptScope, runner = defaultKnipProcessRunner) {
3641
+ try {
3642
+ const analyzeDirectories = typescriptScope.directories;
3643
+ if (analyzeDirectories.length === 0) {
3644
+ return { success: true };
3645
+ }
3646
+ return new Promise((resolve2) => {
3647
+ const knipProcess = runner.spawn("npx", ["knip"], {
3648
+ cwd: process.cwd(),
3649
+ stdio: "pipe"
3650
+ });
3651
+ let knipOutput = "";
3652
+ let knipError = "";
3653
+ knipProcess.stdout?.on("data", (data) => {
3654
+ knipOutput += data.toString();
3655
+ });
3656
+ knipProcess.stderr?.on("data", (data) => {
3657
+ knipError += data.toString();
3658
+ });
3659
+ knipProcess.on("close", (code) => {
3660
+ if (code === 0) {
3661
+ resolve2({ success: true });
3662
+ } else {
3663
+ const errorOutput = knipOutput || knipError || "Unused code detected";
3664
+ resolve2({
3665
+ success: false,
3666
+ error: errorOutput
3667
+ });
3668
+ }
3669
+ });
3670
+ knipProcess.on("error", (error) => {
3671
+ resolve2({ success: false, error: error.message });
3672
+ });
3673
+ });
3674
+ } catch (error) {
3675
+ const errorMessage = error instanceof Error ? error.message : String(error);
3676
+ return { success: false, error: errorMessage };
3677
+ }
3678
+ }
3679
+ var knipStep = {
3680
+ id: STEP_IDS.KNIP,
3681
+ name: STEP_NAMES.KNIP,
3682
+ description: STEP_DESCRIPTIONS.KNIP,
3683
+ enabled: (context) => context.enabledValidations[VALIDATION_KEYS.KNIP] === true && validationEnabled(VALIDATION_KEYS.KNIP, VALIDATION_DEFAULTS) && !context.isFileSpecificMode,
3684
+ execute: async (context) => {
3685
+ const startTime = performance.now();
3686
+ try {
3687
+ const result = await validateKnip(context.scopeConfig);
3688
+ return {
3689
+ success: result.success,
3690
+ error: result.error,
3691
+ duration: performance.now() - startTime
3692
+ };
3693
+ } catch (error) {
3694
+ return {
3695
+ success: false,
3696
+ error: error instanceof Error ? error.message : String(error),
3697
+ duration: performance.now() - startTime
3698
+ };
3699
+ }
3700
+ }
3701
+ };
3702
+
3703
+ // src/commands/validation/knip.ts
3704
+ async function knipCommand(options) {
3705
+ const { cwd, quiet } = options;
3706
+ const startTime = Date.now();
3707
+ if (!validationEnabled("KNIP", { KNIP: false })) {
3708
+ const output = quiet ? "" : "Knip: skipped (disabled by default, set KNIP_VALIDATION_ENABLED=1 to enable)";
3709
+ return { exitCode: 0, output, durationMs: Date.now() - startTime };
3710
+ }
3711
+ const toolResult = await discoverTool("knip", { projectRoot: cwd });
3712
+ if (!toolResult.found) {
3713
+ const skipMessage = formatSkipMessage("unused code detection", toolResult);
3714
+ return { exitCode: 0, output: skipMessage, durationMs: Date.now() - startTime };
3715
+ }
3716
+ const scopeConfig = getTypeScriptScope("full");
3717
+ const result = await validateKnip(scopeConfig);
3718
+ const durationMs = Date.now() - startTime;
3719
+ if (result.success) {
3720
+ const output = quiet ? "" : `Knip: \u2713 No unused code found`;
3721
+ return { exitCode: 0, output, durationMs };
3722
+ } else {
3723
+ const output = result.error ?? "Unused code found";
3724
+ return { exitCode: 1, output, durationMs };
3725
+ }
3726
+ }
3727
+
3728
+ // src/commands/validation/lint.ts
3729
+ async function lintCommand(options) {
3730
+ const { cwd, scope = "full", files, fix, quiet } = options;
3731
+ const startTime = Date.now();
3732
+ const toolResult = await discoverTool("eslint", { projectRoot: cwd });
3733
+ if (!toolResult.found) {
3734
+ const skipMessage = formatSkipMessage("ESLint", toolResult);
3735
+ return { exitCode: 0, output: skipMessage, durationMs: Date.now() - startTime };
3736
+ }
3737
+ const scopeConfig = getTypeScriptScope(scope);
3738
+ const context = {
3739
+ projectRoot: cwd,
3740
+ scope,
3741
+ scopeConfig,
3742
+ mode: fix ? "write" : "read",
3743
+ enabledValidations: { ESLINT: true },
3744
+ validatedFiles: files,
3745
+ isFileSpecificMode: Boolean(files && files.length > 0)
3746
+ };
3747
+ const result = await validateESLint(context);
3748
+ const durationMs = Date.now() - startTime;
3749
+ if (result.success) {
3750
+ const output = quiet ? "" : `ESLint: \u2713 No issues found`;
3751
+ return { exitCode: 0, output, durationMs };
3752
+ } else {
3753
+ const output = result.error ?? "ESLint validation failed";
3754
+ return { exitCode: 1, output, durationMs };
3755
+ }
3756
+ }
3757
+
3758
+ // src/validation/steps/typescript.ts
3759
+ import { spawn as spawn3 } from "child_process";
3760
+ import { existsSync as existsSync2, mkdirSync, rmSync, writeFileSync } from "fs";
3761
+ import { mkdtemp } from "fs/promises";
3762
+ import { tmpdir } from "os";
3763
+ import { isAbsolute, join as join12 } from "path";
3764
+ var defaultTypeScriptProcessRunner = { spawn: spawn3 };
3765
+ var defaultTypeScriptDeps = {
3766
+ mkdtemp,
3767
+ writeFileSync,
3768
+ rmSync,
3769
+ existsSync: existsSync2,
3770
+ mkdirSync
3771
+ };
3772
+ function buildTypeScriptArgs(context) {
3773
+ const { scope, configFile } = context;
3774
+ return scope === VALIDATION_SCOPES.FULL ? ["tsc", "--noEmit"] : ["tsc", "--project", configFile];
3775
+ }
3776
+ async function createFileSpecificTsconfig(scope, files, deps = defaultTypeScriptDeps) {
3777
+ const tempDir = await deps.mkdtemp(join12(tmpdir(), "validate-ts-"));
3778
+ const configPath = join12(tempDir, "tsconfig.json");
3779
+ const baseConfigFile = TSCONFIG_FILES[scope];
3780
+ const projectRoot = process.cwd();
3781
+ const absoluteFiles = files.map((file) => isAbsolute(file) ? file : join12(projectRoot, file));
3782
+ const tempConfig = {
3783
+ extends: join12(projectRoot, baseConfigFile),
3784
+ files: absoluteFiles,
3785
+ include: [],
3786
+ exclude: [],
3787
+ compilerOptions: {
3788
+ noEmit: true,
3789
+ typeRoots: [join12(projectRoot, "node_modules", "@types")],
3790
+ types: ["node"]
3791
+ }
3792
+ };
3793
+ deps.writeFileSync(configPath, JSON.stringify(tempConfig, null, 2));
3794
+ const cleanup = () => {
3795
+ try {
3796
+ deps.rmSync(tempDir, { recursive: true, force: true });
3797
+ } catch {
3798
+ }
3799
+ };
3800
+ return { configPath, tempDir, cleanup };
3801
+ }
3802
+ async function validateTypeScript(scope, typescriptScope, files, runner = defaultTypeScriptProcessRunner, deps = defaultTypeScriptDeps) {
3803
+ const configFile = TSCONFIG_FILES[scope];
3804
+ let tool;
3805
+ let tscArgs;
3806
+ if (files && files.length > 0) {
3807
+ const { configPath, cleanup } = await createFileSpecificTsconfig(scope, files, deps);
3808
+ try {
3809
+ return await new Promise((resolve2) => {
3810
+ const tscProcess = runner.spawn("npx", ["tsc", "--project", configPath], {
3811
+ cwd: process.cwd(),
3812
+ stdio: "inherit"
3813
+ });
3814
+ tscProcess.on("close", (code) => {
3815
+ cleanup();
3816
+ if (code === 0) {
3817
+ resolve2({ success: true, skipped: false });
3818
+ } else {
3819
+ resolve2({ success: false, error: `TypeScript exited with code ${code}` });
3820
+ }
3821
+ });
3822
+ tscProcess.on("error", (error) => {
3823
+ cleanup();
3824
+ resolve2({ success: false, error: error.message });
3825
+ });
3826
+ });
3827
+ } catch (error) {
3828
+ cleanup();
3829
+ const errorMessage = error instanceof Error ? error.message : String(error);
3830
+ return { success: false, error: `Failed to create temporary config: ${errorMessage}` };
3831
+ }
3832
+ } else {
3833
+ tool = "npx";
3834
+ tscArgs = buildTypeScriptArgs({ scope, configFile });
3835
+ }
3836
+ return new Promise((resolve2) => {
3837
+ const tscProcess = runner.spawn(tool, tscArgs, {
3838
+ cwd: process.cwd(),
3839
+ stdio: "inherit"
3840
+ });
3841
+ tscProcess.on("close", (code) => {
3842
+ if (code === 0) {
3843
+ resolve2({ success: true, skipped: false });
3844
+ } else {
3845
+ resolve2({ success: false, error: `TypeScript exited with code ${code}` });
3846
+ }
3847
+ });
3848
+ tscProcess.on("error", (error) => {
3849
+ resolve2({ success: false, error: error.message });
3850
+ });
3851
+ });
3852
+ }
3853
+ var typescriptStep = {
3854
+ id: STEP_IDS.TYPESCRIPT,
3855
+ name: STEP_NAMES.TYPESCRIPT,
3856
+ description: STEP_DESCRIPTIONS.TYPESCRIPT,
3857
+ enabled: (context) => context.enabledValidations[VALIDATION_KEYS.TYPESCRIPT] === true && validationEnabled(VALIDATION_KEYS.TYPESCRIPT),
3858
+ execute: async (context) => {
3859
+ const startTime = performance.now();
3860
+ try {
3861
+ const result = await validateTypeScript(
3862
+ context.scope,
3863
+ context.scopeConfig,
3864
+ context.validatedFiles
3865
+ );
3866
+ return {
3867
+ success: result.success,
3868
+ error: result.error,
3869
+ duration: performance.now() - startTime,
3870
+ skipped: result.skipped
3871
+ };
3872
+ } catch (error) {
3873
+ return {
3874
+ success: false,
3875
+ error: error instanceof Error ? error.message : String(error),
3876
+ duration: performance.now() - startTime
3877
+ };
3878
+ }
3879
+ }
3880
+ };
3881
+
3882
+ // src/commands/validation/typescript.ts
3883
+ async function typescriptCommand(options) {
3884
+ const { cwd, scope = "full", files, quiet } = options;
3885
+ const startTime = Date.now();
3886
+ const toolResult = await discoverTool("typescript", { projectRoot: cwd });
3887
+ if (!toolResult.found) {
3888
+ const skipMessage = formatSkipMessage("TypeScript", toolResult);
3889
+ return { exitCode: 0, output: skipMessage, durationMs: Date.now() - startTime };
3890
+ }
3891
+ const scopeConfig = getTypeScriptScope(scope);
3892
+ const result = await validateTypeScript(scope, scopeConfig, files);
3893
+ const durationMs = Date.now() - startTime;
3894
+ if (result.success) {
3895
+ const output = quiet ? "" : `TypeScript: \u2713 No type errors`;
3896
+ return { exitCode: 0, output, durationMs };
3897
+ } else {
3898
+ const output = result.error ?? "TypeScript validation failed";
3899
+ return { exitCode: 1, output, durationMs };
3900
+ }
3901
+ }
3902
+
3903
+ // src/commands/validation/all.ts
3904
+ var TOTAL_STEPS = 4;
3905
+ function formatStepWithTiming(stepNumber, result, quiet) {
3906
+ if (quiet || !result.output) return "";
3907
+ const timing = result.durationMs === void 0 ? "" : ` (${formatDuration(result.durationMs)})`;
3908
+ return `[${stepNumber}/${TOTAL_STEPS}] ${result.output}${timing}`;
3909
+ }
3910
+ async function allCommand(options) {
3911
+ const { cwd, scope, files, fix, quiet = false, json } = options;
3912
+ const startTime = Date.now();
3913
+ const outputs = [];
3914
+ let hasFailure = false;
3915
+ const circularResult = await circularCommand({ cwd, quiet, json });
3916
+ const circularOutput = formatStepWithTiming(1, circularResult, quiet);
3917
+ if (circularOutput) outputs.push(circularOutput);
3918
+ if (circularResult.exitCode !== 0) hasFailure = true;
3919
+ const knipResult = await knipCommand({ cwd, quiet, json });
3920
+ const knipOutput = formatStepWithTiming(2, knipResult, quiet);
3921
+ if (knipOutput) outputs.push(knipOutput);
3922
+ const lintResult = await lintCommand({ cwd, scope, files, fix, quiet, json });
3923
+ const lintOutput = formatStepWithTiming(3, lintResult, quiet);
3924
+ if (lintOutput) outputs.push(lintOutput);
3925
+ if (lintResult.exitCode !== 0) hasFailure = true;
3926
+ const tsResult = await typescriptCommand({ cwd, scope, files, quiet, json });
3927
+ const tsOutput = formatStepWithTiming(4, tsResult, quiet);
3928
+ if (tsOutput) outputs.push(tsOutput);
3929
+ if (tsResult.exitCode !== 0) hasFailure = true;
3930
+ const totalDurationMs = Date.now() - startTime;
3931
+ if (!quiet) {
3932
+ const summary = formatSummary({ success: !hasFailure, totalDurationMs });
3933
+ outputs.push("", summary);
3934
+ }
3935
+ return {
3936
+ exitCode: hasFailure ? 1 : 0,
3937
+ output: outputs.join("\n"),
3938
+ durationMs: totalDurationMs
3939
+ };
3940
+ }
3941
+
3942
+ // src/domains/validation/index.ts
3943
+ function addCommonOptions(cmd) {
3944
+ return cmd.option("--scope <scope>", "Validation scope (full|production)", "full").option("--files <paths...>", "Specific files/directories to validate").option("--quiet", "Suppress progress output").option("--json", "Output results as JSON");
3945
+ }
3946
+ function registerValidationCommands(validationCmd) {
3947
+ const tsCmd = validationCmd.command("typescript").alias("ts").description("Run TypeScript type checking").action(async (options) => {
3948
+ const result = await typescriptCommand({
3949
+ cwd: process.cwd(),
3950
+ scope: options.scope,
3951
+ files: options.files,
3952
+ quiet: options.quiet,
3953
+ json: options.json
3954
+ });
3955
+ if (result.output) console.log(result.output);
3956
+ process.exit(result.exitCode);
3957
+ });
3958
+ addCommonOptions(tsCmd);
3959
+ const lintCmd = validationCmd.command("lint").description("Run ESLint").option("--fix", "Auto-fix issues").action(async (options) => {
3960
+ const result = await lintCommand({
3961
+ cwd: process.cwd(),
3962
+ scope: options.scope,
3963
+ files: options.files,
3964
+ fix: options.fix,
3965
+ quiet: options.quiet,
3966
+ json: options.json
3967
+ });
3968
+ if (result.output) console.log(result.output);
3969
+ process.exit(result.exitCode);
3970
+ });
3971
+ addCommonOptions(lintCmd);
3972
+ const circularCmd = validationCmd.command("circular").description("Check for circular dependencies").action(async (options) => {
3973
+ const result = await circularCommand({
3974
+ cwd: process.cwd(),
3975
+ quiet: options.quiet,
3976
+ json: options.json
3977
+ });
3978
+ if (result.output) console.log(result.output);
3979
+ process.exit(result.exitCode);
3980
+ });
3981
+ addCommonOptions(circularCmd);
3982
+ const knipCmd = validationCmd.command("knip").description("Detect unused code").action(async (options) => {
3983
+ const result = await knipCommand({
3984
+ cwd: process.cwd(),
3985
+ quiet: options.quiet,
3986
+ json: options.json
3987
+ });
3988
+ if (result.output) console.log(result.output);
3989
+ process.exit(result.exitCode);
3990
+ });
3991
+ addCommonOptions(knipCmd);
3992
+ const allCmd = validationCmd.command("all", { isDefault: true }).description("Run all validations").option("--fix", "Auto-fix ESLint issues").action(async (options) => {
3993
+ const result = await allCommand({
3994
+ cwd: process.cwd(),
3995
+ scope: options.scope,
3996
+ files: options.files,
3997
+ fix: options.fix,
3998
+ quiet: options.quiet,
3999
+ json: options.json
4000
+ });
4001
+ if (result.output) console.log(result.output);
4002
+ process.exit(result.exitCode);
4003
+ });
4004
+ addCommonOptions(allCmd);
4005
+ }
4006
+ var validationDomain = {
4007
+ name: "validation",
4008
+ description: "Run code validation tools",
4009
+ register: (program2) => {
4010
+ const validationCmd = program2.command("validation").alias("v").description("Run code validation tools");
4011
+ registerValidationCommands(validationCmd);
4012
+ }
4013
+ };
4014
+
4015
+ // src/cli.ts
4016
+ var program = new Command();
4017
+ program.name("spx").description("Fast, deterministic CLI tool for spec workflow management").version("0.2.0");
4018
+ claudeDomain.register(program);
4019
+ sessionDomain.register(program);
4020
+ specDomain.register(program);
4021
+ validationDomain.register(program);
4022
+ program.parse();
4023
+ //# sourceMappingURL=cli.js.map