@halecraft/verify 1.0.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/index.js ADDED
@@ -0,0 +1,1796 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ // src/config.ts
9
+ import { existsSync } from "fs";
10
+ import { join, resolve } from "path";
11
+ import { pathToFileURL } from "url";
12
+ function defineConfig(config) {
13
+ return config;
14
+ }
15
+ function defineTask(task) {
16
+ return task;
17
+ }
18
+ var CONFIG_FILES = [
19
+ "verify.config.ts",
20
+ "verify.config.mts",
21
+ "verify.config.js",
22
+ "verify.config.mjs"
23
+ ];
24
+ function findConfigFile(cwd) {
25
+ for (const filename of CONFIG_FILES) {
26
+ const filepath = join(cwd, filename);
27
+ if (existsSync(filepath)) {
28
+ return filepath;
29
+ }
30
+ }
31
+ return null;
32
+ }
33
+ async function loadConfig(configPath) {
34
+ const absolutePath = resolve(configPath);
35
+ if (!existsSync(absolutePath)) {
36
+ return null;
37
+ }
38
+ const fileUrl = pathToFileURL(absolutePath).href;
39
+ const module = await import(fileUrl);
40
+ if (!module.default) {
41
+ throw new Error(`Config file ${configPath} must have a default export`);
42
+ }
43
+ return module.default;
44
+ }
45
+ async function loadConfigFromCwd(cwd, configPath) {
46
+ if (configPath) {
47
+ return loadConfig(configPath);
48
+ }
49
+ const foundPath = findConfigFile(cwd);
50
+ if (!foundPath) {
51
+ return null;
52
+ }
53
+ return loadConfig(foundPath);
54
+ }
55
+ function mergeOptions(configOptions, cliOptions) {
56
+ return {
57
+ logs: cliOptions?.logs ?? configOptions?.logs ?? "failed",
58
+ format: cliOptions?.format ?? configOptions?.format ?? "human",
59
+ filter: cliOptions?.filter ?? configOptions?.filter,
60
+ cwd: cliOptions?.cwd ?? configOptions?.cwd ?? process.cwd(),
61
+ noColor: cliOptions?.noColor ?? configOptions?.noColor ?? false,
62
+ topLevelOnly: cliOptions?.topLevelOnly ?? configOptions?.topLevelOnly ?? false,
63
+ noTty: cliOptions?.noTty ?? configOptions?.noTty ?? false
64
+ };
65
+ }
66
+
67
+ // src/discovery.ts
68
+ import { existsSync as existsSync2, readdirSync, statSync } from "fs";
69
+ import { join as join2, relative } from "path";
70
+ var DEFAULT_PATTERNS = ["packages/*", "apps/*"];
71
+ function findMatchingDirs(rootDir, patterns) {
72
+ const results = [];
73
+ for (const pattern of patterns) {
74
+ if (pattern.endsWith("/*")) {
75
+ const parentDir = pattern.slice(0, -2);
76
+ const parentPath = join2(rootDir, parentDir);
77
+ if (existsSync2(parentPath) && statSync(parentPath).isDirectory()) {
78
+ const entries = readdirSync(parentPath);
79
+ for (const entry of entries) {
80
+ const entryPath = join2(parentPath, entry);
81
+ if (statSync(entryPath).isDirectory()) {
82
+ results.push(entryPath);
83
+ }
84
+ }
85
+ }
86
+ } else {
87
+ const dirPath = join2(rootDir, pattern);
88
+ if (existsSync2(dirPath) && statSync(dirPath).isDirectory()) {
89
+ results.push(dirPath);
90
+ }
91
+ }
92
+ }
93
+ return results;
94
+ }
95
+ function getPackageName(packageDir) {
96
+ const packageJsonPath = join2(packageDir, "package.json");
97
+ if (!existsSync2(packageJsonPath)) {
98
+ return null;
99
+ }
100
+ try {
101
+ const content = __require(packageJsonPath);
102
+ return content.name ?? null;
103
+ } catch {
104
+ return null;
105
+ }
106
+ }
107
+ async function discoverPackages(rootDir, options = {}) {
108
+ const patterns = options.patterns ?? DEFAULT_PATTERNS;
109
+ const matchingDirs = findMatchingDirs(rootDir, patterns);
110
+ const packages = [];
111
+ for (const dir of matchingDirs) {
112
+ const name = getPackageName(dir);
113
+ if (!name) continue;
114
+ if (options.filter && options.filter.length > 0) {
115
+ const matches = options.filter.some(
116
+ (f) => name === f || name.includes(f) || relative(rootDir, dir).includes(f)
117
+ );
118
+ if (!matches) continue;
119
+ }
120
+ const configPath = findConfigFile(dir);
121
+ const config = configPath ? await loadConfig(configPath) : null;
122
+ packages.push({
123
+ name,
124
+ path: relative(rootDir, dir),
125
+ absolutePath: dir,
126
+ config
127
+ });
128
+ }
129
+ return packages;
130
+ }
131
+ async function hasPackageChanged(_packagePath, _baseBranch = "main") {
132
+ return true;
133
+ }
134
+
135
+ // src/init/detect.ts
136
+ import { existsSync as existsSync3, readFileSync } from "fs";
137
+ import { join as join3 } from "path";
138
+ var TOOL_PATTERNS = [
139
+ // Biome
140
+ {
141
+ pattern: /\bbiome\s+(check|lint|format)/,
142
+ binary: "biome",
143
+ getArgs: (match, content) => {
144
+ const biomeMatch = content.match(/biome\s+([^&|;]+)/);
145
+ return biomeMatch ? biomeMatch[1].trim() : "check .";
146
+ },
147
+ parser: "biome"
148
+ },
149
+ // ESLint
150
+ {
151
+ pattern: /\beslint\b/,
152
+ binary: "eslint",
153
+ getArgs: (_, content) => {
154
+ const eslintMatch = content.match(/eslint\s+([^&|;]+)/);
155
+ return eslintMatch ? eslintMatch[1].trim() : ".";
156
+ }
157
+ },
158
+ // Prettier
159
+ {
160
+ pattern: /\bprettier\b/,
161
+ binary: "prettier",
162
+ getArgs: (_, content) => {
163
+ const prettierMatch = content.match(/prettier\s+([^&|;]+)/);
164
+ return prettierMatch ? prettierMatch[1].trim() : "--check .";
165
+ }
166
+ },
167
+ // TypeScript
168
+ {
169
+ pattern: /\btsc\b/,
170
+ binary: "tsc",
171
+ getArgs: (_, content) => {
172
+ const tscMatch = content.match(/tsc\s+([^&|;]+)/);
173
+ return tscMatch ? tscMatch[1].trim() : "--noEmit";
174
+ },
175
+ parser: "tsc"
176
+ },
177
+ // tsgo
178
+ {
179
+ pattern: /\btsgo\b/,
180
+ binary: "tsgo",
181
+ getArgs: (_, content) => {
182
+ const tsgoMatch = content.match(/tsgo\s+([^&|;]+)/);
183
+ return tsgoMatch ? tsgoMatch[1].trim() : "--noEmit";
184
+ },
185
+ parser: "tsc"
186
+ },
187
+ // Vitest
188
+ {
189
+ pattern: /\bvitest\b/,
190
+ binary: "vitest",
191
+ getArgs: (_, content) => {
192
+ if (content.includes("vitest run")) return "run";
193
+ if (content.includes("vitest watch")) return "run";
194
+ return "run";
195
+ },
196
+ parser: "vitest"
197
+ },
198
+ // Jest
199
+ {
200
+ pattern: /\bjest\b/,
201
+ binary: "jest",
202
+ getArgs: () => ""
203
+ },
204
+ // Mocha
205
+ {
206
+ pattern: /\bmocha\b/,
207
+ binary: "mocha",
208
+ getArgs: () => ""
209
+ },
210
+ // tsup
211
+ {
212
+ pattern: /\btsup\b/,
213
+ binary: "tsup",
214
+ getArgs: (_, content) => {
215
+ const tsupMatch = content.match(/tsup\s+([^&|;]+)/);
216
+ return tsupMatch ? tsupMatch[1].trim() : "";
217
+ }
218
+ },
219
+ // esbuild
220
+ {
221
+ pattern: /\besbuild\b/,
222
+ binary: "esbuild",
223
+ getArgs: (_, content) => {
224
+ const esbuildMatch = content.match(/esbuild\s+([^&|;]+)/);
225
+ return esbuildMatch ? esbuildMatch[1].trim() : "";
226
+ }
227
+ }
228
+ ];
229
+ var SCRIPT_NAME_PATTERNS = [
230
+ // Format/Lint patterns
231
+ {
232
+ pattern: /^(lint|eslint|biome|prettier|format)$/i,
233
+ key: "format",
234
+ name: "Format & Lint",
235
+ category: "format"
236
+ },
237
+ {
238
+ pattern: /^(lint:fix|format:fix|fix)$/i,
239
+ key: "format",
240
+ name: "Format & Lint",
241
+ category: "format"
242
+ },
243
+ {
244
+ pattern: /^verify:format$/i,
245
+ key: "format",
246
+ name: "Format",
247
+ category: "format"
248
+ },
249
+ // Type checking patterns
250
+ {
251
+ pattern: /^(typecheck|type-check|tsc|types|check-types)$/i,
252
+ key: "types",
253
+ name: "Type Check",
254
+ category: "types"
255
+ },
256
+ {
257
+ pattern: /^verify:types$/i,
258
+ key: "types",
259
+ name: "Types",
260
+ category: "types"
261
+ },
262
+ // Test patterns
263
+ {
264
+ pattern: /^(test|tests|vitest|jest|mocha|ava)$/i,
265
+ key: "test",
266
+ name: "Tests",
267
+ category: "logic"
268
+ },
269
+ {
270
+ pattern: /^test:(unit|integration|e2e)$/i,
271
+ key: "test",
272
+ name: "Tests",
273
+ category: "logic"
274
+ },
275
+ {
276
+ pattern: /^verify:logic$/i,
277
+ key: "logic",
278
+ name: "Logic Tests",
279
+ category: "logic"
280
+ },
281
+ // Build patterns
282
+ {
283
+ pattern: /^(build|compile)$/i,
284
+ key: "build",
285
+ name: "Build",
286
+ category: "build"
287
+ }
288
+ ];
289
+ function readPackageJson(cwd) {
290
+ const packageJsonPath = join3(cwd, "package.json");
291
+ if (!existsSync3(packageJsonPath)) {
292
+ return null;
293
+ }
294
+ try {
295
+ const content = readFileSync(packageJsonPath, "utf-8");
296
+ return JSON.parse(content);
297
+ } catch {
298
+ return null;
299
+ }
300
+ }
301
+ function binaryExists(cwd, binary) {
302
+ return existsSync3(join3(cwd, "node_modules", ".bin", binary));
303
+ }
304
+ function extractOptimizedCommand(cwd, scriptContent) {
305
+ for (const tool of TOOL_PATTERNS) {
306
+ const match = scriptContent.match(tool.pattern);
307
+ if (match && binaryExists(cwd, tool.binary)) {
308
+ const args = tool.getArgs(match, scriptContent);
309
+ const command = args ? `./node_modules/.bin/${tool.binary} ${args}` : `./node_modules/.bin/${tool.binary}`;
310
+ return { command, parser: tool.parser };
311
+ }
312
+ }
313
+ return null;
314
+ }
315
+ function detectFromPackageJson(cwd) {
316
+ const pkg = readPackageJson(cwd);
317
+ if (!pkg?.scripts) {
318
+ return [];
319
+ }
320
+ const detected = [];
321
+ const seenKeys = /* @__PURE__ */ new Set();
322
+ for (const [scriptName, scriptContent] of Object.entries(pkg.scripts)) {
323
+ if (scriptContent.includes("run-s") || scriptContent.includes("run-p") || scriptContent.includes("npm-run-all")) {
324
+ continue;
325
+ }
326
+ for (const { pattern, key, name, category } of SCRIPT_NAME_PATTERNS) {
327
+ if (pattern.test(scriptName)) {
328
+ const uniqueKey = seenKeys.has(key) ? `${key}-${scriptName}` : key;
329
+ if (!seenKeys.has(uniqueKey)) {
330
+ seenKeys.add(uniqueKey);
331
+ const optimized = extractOptimizedCommand(cwd, scriptContent);
332
+ detected.push({
333
+ key: uniqueKey,
334
+ name,
335
+ scriptName,
336
+ command: optimized?.command ?? `npm run ${scriptName}`,
337
+ category,
338
+ parser: optimized?.parser
339
+ });
340
+ }
341
+ break;
342
+ }
343
+ }
344
+ }
345
+ const categoryOrder = {
346
+ format: 0,
347
+ types: 1,
348
+ logic: 2,
349
+ build: 3,
350
+ other: 4
351
+ };
352
+ detected.sort((a, b) => categoryOrder[a.category] - categoryOrder[b.category]);
353
+ const hasFormatTask = detected.some((t) => t.category === "format");
354
+ if (hasFormatTask) {
355
+ for (const task of detected) {
356
+ if (task.category !== "format" && task.category !== "other") {
357
+ task.reportingDependsOn = ["format"];
358
+ }
359
+ }
360
+ }
361
+ return detected;
362
+ }
363
+ function detectPackageManager(cwd) {
364
+ if (existsSync3(join3(cwd, "pnpm-lock.yaml"))) {
365
+ return "pnpm";
366
+ }
367
+ if (existsSync3(join3(cwd, "yarn.lock"))) {
368
+ return "yarn";
369
+ }
370
+ return "npm";
371
+ }
372
+ function getRunCommand(packageManager, scriptName) {
373
+ switch (packageManager) {
374
+ case "pnpm":
375
+ return `pnpm ${scriptName}`;
376
+ case "yarn":
377
+ return `yarn ${scriptName}`;
378
+ default:
379
+ return `npm run ${scriptName}`;
380
+ }
381
+ }
382
+ function detectTasks(cwd) {
383
+ const packageManager = detectPackageManager(cwd);
384
+ const tasks = detectFromPackageJson(cwd);
385
+ return tasks.map((task) => {
386
+ if (task.command.startsWith("./")) {
387
+ return task;
388
+ }
389
+ return {
390
+ ...task,
391
+ command: getRunCommand(packageManager, task.scriptName)
392
+ };
393
+ });
394
+ }
395
+
396
+ // src/init/generate.ts
397
+ function getOutputFormat(filePath) {
398
+ if (filePath.endsWith(".mts")) return "mts";
399
+ if (filePath.endsWith(".mjs")) return "mjs";
400
+ if (filePath.endsWith(".js")) return "js";
401
+ return "ts";
402
+ }
403
+ function generateImport(format) {
404
+ return `import { defineConfig } from "@halecraft/verify"`;
405
+ }
406
+ function generateTask(task, indent) {
407
+ const parts = [`key: "${task.key}"`, `run: "${task.command}"`];
408
+ if (task.parser) {
409
+ parts.push(`parser: "${task.parser}"`);
410
+ }
411
+ if (task.reportingDependsOn && task.reportingDependsOn.length > 0) {
412
+ const deps = task.reportingDependsOn.map((d) => `"${d}"`).join(", ");
413
+ parts.push(`reportingDependsOn: [${deps}]`);
414
+ }
415
+ return `${indent}{ ${parts.join(", ")} }`;
416
+ }
417
+ function generateSkeleton(format) {
418
+ const importStatement = generateImport(format);
419
+ return `${importStatement}
420
+
421
+ export default defineConfig({
422
+ tasks: [
423
+ // Add your verification tasks here
424
+ // Example:
425
+ // { key: "format", run: "pnpm lint" },
426
+ // { key: "types", run: "pnpm typecheck" },
427
+ // { key: "test", run: "pnpm test" },
428
+ ],
429
+ })
430
+ `;
431
+ }
432
+ function generateConfigContent(tasks, format) {
433
+ if (tasks.length === 0) {
434
+ return generateSkeleton(format);
435
+ }
436
+ const importStatement = generateImport(format);
437
+ const indent = " ";
438
+ const taskLines = tasks.map((task) => generateTask(task, indent));
439
+ return `${importStatement}
440
+
441
+ export default defineConfig({
442
+ tasks: [
443
+ ${taskLines.join(",\n")},
444
+ ],
445
+ })
446
+ `;
447
+ }
448
+ function getDefaultConfigPath() {
449
+ return "verify.config.ts";
450
+ }
451
+
452
+ // src/init/prompts.ts
453
+ function shouldSkipPrompts(options) {
454
+ return options.yes || !options.isTTY;
455
+ }
456
+ async function promptForTasks(detectedTasks, options) {
457
+ if (detectedTasks.length === 0) {
458
+ if (!shouldSkipPrompts(options)) {
459
+ console.log(
460
+ "\n\u26A0\uFE0F No verification scripts detected in package.json.\n A skeleton config will be created.\n"
461
+ );
462
+ }
463
+ return { tasks: [], cancelled: false };
464
+ }
465
+ if (shouldSkipPrompts(options)) {
466
+ console.log(`
467
+ \u2713 Auto-selecting ${detectedTasks.length} detected task(s)
468
+ `);
469
+ return { tasks: detectedTasks, cancelled: false };
470
+ }
471
+ try {
472
+ const { checkbox } = await import("@inquirer/prompts");
473
+ console.log("\n\u{1F50D} Detected verification scripts in package.json:\n");
474
+ const choices = detectedTasks.map((task) => ({
475
+ name: `${task.name} (${task.command})`,
476
+ value: task,
477
+ checked: true
478
+ // Pre-select all by default
479
+ }));
480
+ const selected = await checkbox({
481
+ message: "Select tasks to include in your config:",
482
+ choices,
483
+ instructions: false
484
+ });
485
+ if (selected.length === 0) {
486
+ console.log(
487
+ "\n\u26A0\uFE0F No tasks selected. A skeleton config will be created.\n"
488
+ );
489
+ }
490
+ return { tasks: selected, cancelled: false };
491
+ } catch (error) {
492
+ if (error instanceof Error && (error.message.includes("User force closed") || error.name === "ExitPromptError")) {
493
+ return { tasks: [], cancelled: true };
494
+ }
495
+ throw error;
496
+ }
497
+ }
498
+
499
+ // src/init/write.ts
500
+ import { existsSync as existsSync4, writeFileSync } from "fs";
501
+ import { resolve as resolve2 } from "path";
502
+ function checkConfigExists(cwd, configPath) {
503
+ const absolutePath = resolve2(cwd, configPath);
504
+ return {
505
+ exists: existsSync4(absolutePath),
506
+ path: absolutePath
507
+ };
508
+ }
509
+ function writeConfigFile(cwd, configPath, content) {
510
+ const absolutePath = resolve2(cwd, configPath);
511
+ writeFileSync(absolutePath, content, "utf-8");
512
+ }
513
+ function printExistsWarning(path) {
514
+ console.error(`
515
+ \u26A0\uFE0F Config file already exists: ${path}`);
516
+ console.error(" Use --force to overwrite.\n");
517
+ }
518
+ function printSuccess(options) {
519
+ const { configPath, tasks, hasOptimizedCommands, removableScripts } = options;
520
+ console.log(`
521
+ \u2705 Created ${configPath}`);
522
+ console.log("");
523
+ console.log(" Quick start:");
524
+ console.log(" $ verify # Run all verifications");
525
+ console.log(" $ verify --top-level # Show only top-level tasks");
526
+ console.log(" $ verify format # Run only 'format' task");
527
+ console.log("");
528
+ if (hasOptimizedCommands) {
529
+ console.log(
530
+ " \u26A1 Performance: Using direct tool paths for faster execution"
531
+ );
532
+ console.log(
533
+ " (avoids ~250ms overhead per command from package manager)"
534
+ );
535
+ console.log("");
536
+ }
537
+ if (removableScripts.length > 0) {
538
+ console.log(" \u{1F4A1} Optional cleanup:");
539
+ console.log(
540
+ " You can remove these scripts from package.json if you only"
541
+ );
542
+ console.log(" run them via 'verify' (keeps package.json cleaner):");
543
+ for (const script of removableScripts) {
544
+ console.log(` - "${script}"`);
545
+ }
546
+ console.log("");
547
+ }
548
+ const tasksWithParsers = tasks.filter((t) => t.parser);
549
+ if (tasksWithParsers.length > 0) {
550
+ console.log(" \u{1F4CA} Rich output: Parsers detected for detailed summaries:");
551
+ for (const task of tasksWithParsers) {
552
+ console.log(` - ${task.key}: ${task.parser}`);
553
+ }
554
+ console.log("");
555
+ }
556
+ console.log(" \u{1F4D6} Docs: https://github.com/halecraft/verify");
557
+ console.log("");
558
+ }
559
+
560
+ // src/init/index.ts
561
+ async function runInit(options) {
562
+ const configPath = options.config ?? getDefaultConfigPath();
563
+ const format = getOutputFormat(configPath);
564
+ const fileCheck = checkConfigExists(options.cwd, configPath);
565
+ if (fileCheck.exists && !options.force) {
566
+ printExistsWarning(fileCheck.path);
567
+ return {
568
+ success: false,
569
+ error: "Config file already exists. Use --force to overwrite."
570
+ };
571
+ }
572
+ const detectedTasks = detectTasks(options.cwd);
573
+ const isTTY = process.stdout.isTTY ?? false;
574
+ const promptOptions = {
575
+ yes: options.yes,
576
+ isTTY
577
+ };
578
+ if (!shouldSkipPrompts(promptOptions)) {
579
+ console.log("\n\u{1F680} Initializing @halecraft/verify config...\n");
580
+ }
581
+ const promptResult = await promptForTasks(detectedTasks, promptOptions);
582
+ if (promptResult.cancelled) {
583
+ console.log("\n\u274C Cancelled.\n");
584
+ return {
585
+ success: false,
586
+ error: "User cancelled"
587
+ };
588
+ }
589
+ const content = generateConfigContent(promptResult.tasks, format);
590
+ const hasOptimizedCommands = promptResult.tasks.some(
591
+ (t) => t.command.startsWith("./node_modules/.bin/")
592
+ );
593
+ const removableScripts = promptResult.tasks.filter((t) => t.command.startsWith("./node_modules/.bin/")).map((t) => t.scriptName);
594
+ try {
595
+ writeConfigFile(options.cwd, configPath, content);
596
+ printSuccess({
597
+ configPath,
598
+ tasks: promptResult.tasks,
599
+ hasOptimizedCommands,
600
+ removableScripts
601
+ });
602
+ return {
603
+ success: true,
604
+ configPath
605
+ };
606
+ } catch (error) {
607
+ const message = error instanceof Error ? error.message : "Failed to write config file";
608
+ console.error(`
609
+ \u274C Error: ${message}
610
+ `);
611
+ return {
612
+ success: false,
613
+ error: message
614
+ };
615
+ }
616
+ }
617
+
618
+ // src/parsers/biome.ts
619
+ var biomeParser = {
620
+ id: "biome",
621
+ parse(output, exitCode) {
622
+ const filesMatch = output.match(
623
+ /Checked\s+(\d+)\s+files?\s+in\s+[\d.]+(?:ms|s)/i
624
+ );
625
+ const fileCount = filesMatch ? Number.parseInt(filesMatch[1], 10) : void 0;
626
+ const warningMatch = output.match(/Found\s+(\d+)\s+warning/i);
627
+ const warnings = warningMatch ? Number.parseInt(warningMatch[1], 10) : 0;
628
+ if (exitCode === 0) {
629
+ const filesPart = fileCount ? `passed ${fileCount} files` : "passed";
630
+ const warningSuffix = warnings > 0 ? `, ${warnings} warning${warnings === 1 ? "" : "s"}` : "";
631
+ return {
632
+ summary: `${filesPart}${warningSuffix}`,
633
+ metrics: { errors: 0, warnings, total: fileCount }
634
+ };
635
+ }
636
+ const summaryMatch = output.match(
637
+ /Found\s+(\d+)\s+error(?:s)?(?:\s+and\s+(\d+)\s+warning(?:s)?)?/i
638
+ );
639
+ if (summaryMatch) {
640
+ const errors2 = Number.parseInt(summaryMatch[1], 10);
641
+ const parsedWarnings = summaryMatch[2] ? Number.parseInt(summaryMatch[2], 10) : warnings;
642
+ const fileSuffix = fileCount ? ` in ${fileCount} files` : "";
643
+ return {
644
+ summary: `${errors2} error${errors2 === 1 ? "" : "s"}${parsedWarnings > 0 ? `, ${parsedWarnings} warning${parsedWarnings === 1 ? "" : "s"}` : ""}${fileSuffix}`,
645
+ metrics: { errors: errors2, warnings: parsedWarnings, total: fileCount }
646
+ };
647
+ }
648
+ const errorLines = output.match(/^\s*error\[/gm);
649
+ const warningLines = output.match(/^\s*warning\[/gm);
650
+ const errors = errorLines ? errorLines.length : 0;
651
+ const countedWarnings = warningLines ? warningLines.length : warnings;
652
+ if (errors > 0 || countedWarnings > 0) {
653
+ const fileSuffix = fileCount ? ` in ${fileCount} files` : "";
654
+ return {
655
+ summary: `${errors} error${errors === 1 ? "" : "s"}${countedWarnings > 0 ? `, ${countedWarnings} warning${countedWarnings === 1 ? "" : "s"}` : ""}${fileSuffix}`,
656
+ metrics: { errors, warnings: countedWarnings, total: fileCount }
657
+ };
658
+ }
659
+ if (fileCount) {
660
+ return {
661
+ summary: `passed ${fileCount} files`,
662
+ metrics: { errors: 0, warnings: 0, total: fileCount }
663
+ };
664
+ }
665
+ return null;
666
+ }
667
+ };
668
+
669
+ // src/parsers/generic.ts
670
+ var genericParser = {
671
+ id: "generic",
672
+ parse(_output, exitCode) {
673
+ return {
674
+ summary: exitCode === 0 ? "passed" : `failed (exit code ${exitCode})`
675
+ };
676
+ }
677
+ };
678
+
679
+ // src/parsers/gotest.ts
680
+ var gotestParser = {
681
+ id: "gotest",
682
+ parse(output, exitCode) {
683
+ const okMatches = output.match(/^ok\s+\S+/gm);
684
+ const failMatches = output.match(/^FAIL\s+\S+/gm);
685
+ const passed = okMatches ? okMatches.length : 0;
686
+ const failed = failMatches ? failMatches.length : 0;
687
+ const total = passed + failed;
688
+ if (total === 0) {
689
+ if (output.includes("no test files")) {
690
+ return {
691
+ summary: "no test files",
692
+ metrics: { passed: 0, failed: 0, total: 0 }
693
+ };
694
+ }
695
+ return null;
696
+ }
697
+ const durationMatch = output.match(/(?:PASS|FAIL)\s*$[\s\S]*?(\d+\.?\d*s)/m);
698
+ const duration = durationMatch ? durationMatch[1] : void 0;
699
+ if (exitCode === 0) {
700
+ return {
701
+ summary: `${passed} package${passed === 1 ? "" : "s"} passed${duration ? ` in ${duration}` : ""}`,
702
+ metrics: { passed, failed: 0, total: passed, duration }
703
+ };
704
+ }
705
+ return {
706
+ summary: `${failed}/${total} package${total === 1 ? "" : "s"} failed`,
707
+ metrics: { passed, failed, total, duration }
708
+ };
709
+ }
710
+ };
711
+
712
+ // src/parsers/tsc.ts
713
+ var tscParser = {
714
+ id: "tsc",
715
+ parse(output, exitCode) {
716
+ const filesMatch = output.match(/^Files:\s+(\d+)/m);
717
+ const fileCount = filesMatch ? Number.parseInt(filesMatch[1], 10) : void 0;
718
+ if (exitCode === 0) {
719
+ const filesPart = fileCount ? `passed ${fileCount} files` : "passed";
720
+ return {
721
+ summary: filesPart,
722
+ metrics: { errors: 0, total: fileCount }
723
+ };
724
+ }
725
+ const errorMatches = output.match(/error TS\d+:/g);
726
+ const errorCount = errorMatches ? errorMatches.length : 0;
727
+ if (errorCount === 0) {
728
+ return null;
729
+ }
730
+ const fileSuffix = fileCount ? ` in ${fileCount} files` : "";
731
+ return {
732
+ summary: `${errorCount} type error${errorCount === 1 ? "" : "s"}${fileSuffix}`,
733
+ metrics: { errors: errorCount, total: fileCount }
734
+ };
735
+ }
736
+ };
737
+
738
+ // src/parsers/vitest.ts
739
+ function stripAnsi(str) {
740
+ return str.replace(/\x1b\[[0-9;]*m/g, "");
741
+ }
742
+ var vitestParser = {
743
+ id: "vitest",
744
+ parse(output, exitCode) {
745
+ const cleanOutput = stripAnsi(output);
746
+ const testsMatch = cleanOutput.match(/Tests\s+(\d+)\s+passed\s*\((\d+)\)/m);
747
+ const durationMatch = cleanOutput.match(/Duration\s+([\d.]+(?:ms|s))\b/m);
748
+ if (!testsMatch) {
749
+ return null;
750
+ }
751
+ const passed = Number.parseInt(testsMatch[1], 10);
752
+ const total = Number.parseInt(testsMatch[2], 10);
753
+ const duration = durationMatch ? durationMatch[1] : void 0;
754
+ return {
755
+ summary: exitCode === 0 ? `passed ${passed}/${total} tests` : `passed ${passed}/${total} tests (some failed)`,
756
+ metrics: {
757
+ passed,
758
+ total,
759
+ failed: total - passed,
760
+ duration
761
+ }
762
+ };
763
+ }
764
+ };
765
+
766
+ // src/parsers/index.ts
767
+ var ParserRegistry = class {
768
+ parsers = /* @__PURE__ */ new Map();
769
+ constructor() {
770
+ this.register(genericParser);
771
+ this.register(vitestParser);
772
+ this.register(tscParser);
773
+ this.register(biomeParser);
774
+ this.register(gotestParser);
775
+ }
776
+ /**
777
+ * Register a custom parser
778
+ */
779
+ register(parser) {
780
+ this.parsers.set(parser.id, parser);
781
+ }
782
+ /**
783
+ * Get a parser by ID
784
+ */
785
+ get(id) {
786
+ return this.parsers.get(id);
787
+ }
788
+ /**
789
+ * Auto-detect parser based on command
790
+ */
791
+ detectParser(cmd) {
792
+ const cmdLower = cmd.toLowerCase();
793
+ if (cmdLower.includes("vitest") || cmdLower.includes("jest")) {
794
+ return "vitest";
795
+ }
796
+ if (cmdLower.includes("tsc") || cmdLower.includes("tsgo")) {
797
+ return "tsc";
798
+ }
799
+ if (cmdLower.includes("biome") || cmdLower.includes("eslint")) {
800
+ return "biome";
801
+ }
802
+ if (cmdLower.includes("go test") || cmdLower.includes("go") && cmdLower.includes("test")) {
803
+ return "gotest";
804
+ }
805
+ return "generic";
806
+ }
807
+ /**
808
+ * Parse output using the specified or auto-detected parser
809
+ */
810
+ parse(output, exitCode, parserId, cmd) {
811
+ const id = parserId ?? (cmd ? this.detectParser(cmd) : "generic");
812
+ const parser = this.parsers.get(id) ?? genericParser;
813
+ const result = parser.parse(output, exitCode);
814
+ if (result) {
815
+ return result;
816
+ }
817
+ const fallback = genericParser.parse(output, exitCode);
818
+ if (!fallback) {
819
+ throw new Error("genericParser unexpectedly returned null");
820
+ }
821
+ return fallback;
822
+ }
823
+ };
824
+ var defaultRegistry = new ParserRegistry();
825
+
826
+ // src/spinner.ts
827
+ var SPINNER_FRAMES = ["\u25DC", "\u25E0", "\u25DD", "\u25DE", "\u25E1", "\u25DF"];
828
+ var SPINNER_INTERVAL = 80;
829
+ var SpinnerManager = class {
830
+ frames = SPINNER_FRAMES;
831
+ frameIndex = 0;
832
+ interval = null;
833
+ /**
834
+ * Start the spinner animation
835
+ * @param onTick - Callback called on each frame update
836
+ */
837
+ start(onTick) {
838
+ if (this.interval) return;
839
+ this.interval = setInterval(() => {
840
+ this.frameIndex = (this.frameIndex + 1) % this.frames.length;
841
+ onTick();
842
+ }, SPINNER_INTERVAL);
843
+ }
844
+ /**
845
+ * Stop the spinner animation
846
+ */
847
+ stop() {
848
+ if (this.interval) {
849
+ clearInterval(this.interval);
850
+ this.interval = null;
851
+ }
852
+ }
853
+ /**
854
+ * Get the current spinner frame character
855
+ */
856
+ getFrame() {
857
+ return this.frames[this.frameIndex];
858
+ }
859
+ /**
860
+ * Check if spinner is currently running
861
+ */
862
+ isRunning() {
863
+ return this.interval !== null;
864
+ }
865
+ };
866
+
867
+ // src/reporter.ts
868
+ var ansi = {
869
+ reset: "\x1B[0m",
870
+ dim: "\x1B[2m",
871
+ red: "\x1B[31m",
872
+ green: "\x1B[32m",
873
+ yellow: "\x1B[33m",
874
+ cyan: "\x1B[36m",
875
+ bold: "\x1B[1m"
876
+ };
877
+ var cursor = {
878
+ hide: "\x1B[?25l",
879
+ show: "\x1B[?25h",
880
+ moveUp: (n) => `\x1B[${n}A`,
881
+ moveToStart: "\x1B[0G",
882
+ clearLine: "\x1B[2K"
883
+ };
884
+ function shouldUseColors(options) {
885
+ if (options.noColor) return false;
886
+ if (options.format === "json") return false;
887
+ if (!process.stdout.isTTY) return false;
888
+ if ("NO_COLOR" in process.env) return false;
889
+ if (process.env.TERM === "dumb") return false;
890
+ return true;
891
+ }
892
+ var BaseReporter = class {
893
+ colorEnabled;
894
+ stream;
895
+ taskDepths = /* @__PURE__ */ new Map();
896
+ constructor(options = {}) {
897
+ this.colorEnabled = shouldUseColors(options);
898
+ this.stream = options.format === "json" ? process.stderr : process.stdout;
899
+ }
900
+ /**
901
+ * Apply ANSI color code to string (if colors enabled)
902
+ */
903
+ c(code, s) {
904
+ return this.colorEnabled ? `${code}${s}${ansi.reset}` : s;
905
+ }
906
+ /**
907
+ * Get success mark (✓ or OK)
908
+ */
909
+ okMark() {
910
+ return this.colorEnabled ? this.c(ansi.green, "\u2713") : "OK";
911
+ }
912
+ /**
913
+ * Get failure mark (✗ or FAIL)
914
+ */
915
+ failMark() {
916
+ return this.colorEnabled ? this.c(ansi.red, "\u2717") : "FAIL";
917
+ }
918
+ /**
919
+ * Get suppressed mark (⊘ or SUPPRESSED)
920
+ */
921
+ suppressedMark() {
922
+ return this.colorEnabled ? this.c(ansi.yellow, "\u2298") : "SUPPRESSED";
923
+ }
924
+ /**
925
+ * Get arrow symbol (→ or ->)
926
+ */
927
+ arrow() {
928
+ return this.colorEnabled ? this.c(ansi.cyan, "\u2192") : "->";
929
+ }
930
+ /**
931
+ * Get indentation string for a given depth
932
+ */
933
+ getIndent(depth) {
934
+ return " ".repeat(depth);
935
+ }
936
+ /**
937
+ * Get task depth from path
938
+ */
939
+ getTaskDepth(path) {
940
+ return this.taskDepths.get(path) ?? 0;
941
+ }
942
+ /**
943
+ * Recursively collect task depths from verification tree
944
+ */
945
+ collectTaskDepths(nodes, parentPath, depth) {
946
+ for (const node of nodes) {
947
+ const path = parentPath ? `${parentPath}:${node.key}` : node.key;
948
+ this.taskDepths.set(path, depth);
949
+ if (node.children) {
950
+ this.collectTaskDepths(node.children, path, depth + 1);
951
+ }
952
+ }
953
+ }
954
+ /**
955
+ * Extract summary from task result
956
+ */
957
+ extractSummary(result) {
958
+ if (result.summaryLine) {
959
+ const colonIndex = result.summaryLine.indexOf(": ");
960
+ if (colonIndex !== -1) {
961
+ return result.summaryLine.slice(colonIndex + 2);
962
+ }
963
+ return result.summaryLine;
964
+ }
965
+ return result.ok ? "passed" : "failed";
966
+ }
967
+ /**
968
+ * Flatten nested task results into a single array
969
+ */
970
+ flattenResults(results) {
971
+ const flat = [];
972
+ for (const r of results) {
973
+ flat.push(r);
974
+ if (r.children) {
975
+ flat.push(...this.flattenResults(r.children));
976
+ }
977
+ }
978
+ return flat;
979
+ }
980
+ /**
981
+ * Output task logs
982
+ */
983
+ outputLogs(results, logsMode) {
984
+ if (logsMode === "none") return;
985
+ const flatResults = this.flattenResults(results);
986
+ for (const r of flatResults) {
987
+ if (r.children) continue;
988
+ if (logsMode === "failed" && r.ok) continue;
989
+ if (r.suppressed) continue;
990
+ const status = r.ok ? this.c(ansi.green, "OK") : this.c(ansi.red, "FAIL");
991
+ this.stream.write(
992
+ `
993
+ ${this.c(ansi.bold, "====")} ${this.c(ansi.bold, r.path.toUpperCase())} ${status} ${this.c(ansi.bold, "====")}
994
+ `
995
+ );
996
+ this.stream.write(r.output || "(no output)\n");
997
+ }
998
+ }
999
+ /**
1000
+ * Output final summary
1001
+ */
1002
+ outputSummary(result) {
1003
+ const finalMessage = result.ok ? this.c(ansi.green, "\n== verification: All correct ==") : this.c(ansi.red, "\n== verification: Failed ==");
1004
+ this.stream.write(`${finalMessage}
1005
+ `);
1006
+ }
1007
+ };
1008
+ var LiveDashboardReporter = class extends BaseReporter {
1009
+ topLevelOnly;
1010
+ tasks = /* @__PURE__ */ new Map();
1011
+ taskOrder = [];
1012
+ spinner;
1013
+ lineCount = 0;
1014
+ constructor(options = {}) {
1015
+ super(options);
1016
+ this.topLevelOnly = options.topLevelOnly ?? false;
1017
+ this.spinner = new SpinnerManager();
1018
+ const cleanup = () => {
1019
+ this.spinner.stop();
1020
+ this.stream.write(cursor.show);
1021
+ process.exit(130);
1022
+ };
1023
+ process.on("SIGINT", cleanup);
1024
+ process.on("SIGTERM", cleanup);
1025
+ }
1026
+ /**
1027
+ * Initialize task list from verification nodes
1028
+ */
1029
+ onStart(tasks) {
1030
+ this.collectTasks(tasks, "", 0);
1031
+ this.stream.write(cursor.hide);
1032
+ this.spinner.start(() => this.redraw());
1033
+ }
1034
+ /**
1035
+ * Recursively collect tasks from verification tree
1036
+ */
1037
+ collectTasks(nodes, parentPath, depth) {
1038
+ for (const node of nodes) {
1039
+ const path = parentPath ? `${parentPath}:${node.key}` : node.key;
1040
+ this.tasks.set(path, {
1041
+ key: node.key,
1042
+ path,
1043
+ depth,
1044
+ status: "pending"
1045
+ });
1046
+ this.taskOrder.push(path);
1047
+ this.taskDepths.set(path, depth);
1048
+ if (node.children) {
1049
+ this.collectTasks(node.children, path, depth + 1);
1050
+ }
1051
+ }
1052
+ }
1053
+ /**
1054
+ * Get display key - shows :key for nested, key for root
1055
+ */
1056
+ getDisplayKey(task) {
1057
+ if (task.depth === 0) {
1058
+ return task.key;
1059
+ }
1060
+ return `:${task.key}`;
1061
+ }
1062
+ /**
1063
+ * Check if task should be displayed based on topLevelOnly flag
1064
+ */
1065
+ shouldDisplay(task) {
1066
+ if (this.topLevelOnly) return task.depth === 0;
1067
+ return true;
1068
+ }
1069
+ /**
1070
+ * Format a single task line
1071
+ */
1072
+ formatLine(task) {
1073
+ const indent = this.getIndent(task.depth);
1074
+ const displayKey = this.getDisplayKey(task);
1075
+ if (task.status === "running") {
1076
+ const spinnerChar = this.c(ansi.dim, `(${this.spinner.getFrame()})`);
1077
+ return `${indent}${this.arrow()} verifying ${this.c(ansi.bold, displayKey)} ${spinnerChar}`;
1078
+ }
1079
+ if (task.status === "completed" && task.result) {
1080
+ const duration = this.c(ansi.dim, `${task.result.durationMs}ms`);
1081
+ if (task.result.suppressed) {
1082
+ const reason = task.result.suppressedBy ? `${task.result.suppressedBy} failed` : "dependency failed";
1083
+ return `${indent}${this.suppressedMark()} suppressed ${this.c(ansi.bold, displayKey)} ${this.c(ansi.dim, `(${reason}, ${duration})`)}`;
1084
+ }
1085
+ const summary = this.extractSummary(task.result);
1086
+ if (task.result.ok) {
1087
+ return ` ${indent}${this.okMark()} verified ${this.c(ansi.bold, displayKey)} ${this.c(ansi.dim, `(${summary}, ${duration})`)}`;
1088
+ }
1089
+ return `${indent}${this.failMark()} failed ${this.c(ansi.bold, displayKey)} ${this.c(ansi.dim, `(${summary}, ${duration})`)}`;
1090
+ }
1091
+ return "";
1092
+ }
1093
+ /**
1094
+ * Redraw all visible task lines
1095
+ */
1096
+ redraw() {
1097
+ if (this.lineCount > 0) {
1098
+ this.stream.write(cursor.moveUp(this.lineCount));
1099
+ }
1100
+ const lines = [];
1101
+ for (const path of this.taskOrder) {
1102
+ const task = this.tasks.get(path);
1103
+ if (!task) continue;
1104
+ if (!this.shouldDisplay(task)) continue;
1105
+ if (task.status === "pending") continue;
1106
+ const line = this.formatLine(task);
1107
+ if (line) {
1108
+ lines.push(line);
1109
+ }
1110
+ }
1111
+ for (const line of lines) {
1112
+ this.stream.write(`${cursor.clearLine}${cursor.moveToStart}${line}
1113
+ `);
1114
+ }
1115
+ this.lineCount = lines.length;
1116
+ }
1117
+ onTaskStart(path, _key) {
1118
+ const task = this.tasks.get(path);
1119
+ if (task) {
1120
+ task.status = "running";
1121
+ }
1122
+ }
1123
+ onTaskComplete(result) {
1124
+ const task = this.tasks.get(result.path);
1125
+ if (task) {
1126
+ task.status = "completed";
1127
+ task.result = result;
1128
+ }
1129
+ this.redraw();
1130
+ }
1131
+ onFinish() {
1132
+ this.spinner.stop();
1133
+ this.redraw();
1134
+ this.stream.write(cursor.show);
1135
+ }
1136
+ };
1137
+ var SequentialReporter = class extends BaseReporter {
1138
+ topLevelOnly;
1139
+ constructor(options = {}) {
1140
+ super(options);
1141
+ this.topLevelOnly = options.topLevelOnly ?? false;
1142
+ }
1143
+ onStart(tasks) {
1144
+ this.collectTaskDepths(tasks, "", 0);
1145
+ }
1146
+ /**
1147
+ * Check if task should be displayed based on topLevelOnly flag
1148
+ */
1149
+ shouldDisplay(path) {
1150
+ if (this.topLevelOnly) return this.getTaskDepth(path) === 0;
1151
+ return true;
1152
+ }
1153
+ onTaskStart(path, _key) {
1154
+ if (!this.shouldDisplay(path)) return;
1155
+ this.stream.write(`${this.arrow()} verifying ${this.c(ansi.bold, path)}
1156
+ `);
1157
+ }
1158
+ onTaskComplete(result) {
1159
+ if (!this.shouldDisplay(result.path)) return;
1160
+ const duration = this.c(ansi.dim, `${result.durationMs}ms`);
1161
+ if (result.suppressed) {
1162
+ const reason = result.suppressedBy ? `${result.suppressedBy} failed` : "dependency failed";
1163
+ this.stream.write(
1164
+ `${this.suppressedMark()} suppressed ${this.c(ansi.bold, result.path)} ${this.c(ansi.dim, `(${reason}, ${duration})`)}
1165
+ `
1166
+ );
1167
+ return;
1168
+ }
1169
+ const mark = result.ok ? this.okMark() : this.failMark();
1170
+ const verb = result.ok ? "verified" : "failed";
1171
+ const summary = this.extractSummary(result);
1172
+ this.stream.write(
1173
+ `${mark} ${verb} ${this.c(ansi.bold, result.path)} ${this.c(ansi.dim, `(${summary}, ${duration})`)}
1174
+ `
1175
+ );
1176
+ }
1177
+ onFinish() {
1178
+ }
1179
+ };
1180
+ var JSONReporter = class {
1181
+ onStart(_tasks) {
1182
+ }
1183
+ onTaskStart(_path, _key) {
1184
+ }
1185
+ onTaskComplete(_result) {
1186
+ }
1187
+ onFinish() {
1188
+ }
1189
+ outputLogs(_results, _logsMode) {
1190
+ }
1191
+ outputSummary(result) {
1192
+ const summary = {
1193
+ ok: result.ok,
1194
+ startedAt: result.startedAt,
1195
+ finishedAt: result.finishedAt,
1196
+ durationMs: result.durationMs,
1197
+ tasks: this.serializeTasks(result.tasks)
1198
+ };
1199
+ process.stdout.write(`${JSON.stringify(summary)}
1200
+ `);
1201
+ }
1202
+ serializeTasks(tasks) {
1203
+ return tasks.map((t) => ({
1204
+ key: t.key,
1205
+ path: t.path,
1206
+ ok: t.ok,
1207
+ code: t.code,
1208
+ durationMs: t.durationMs,
1209
+ summaryLine: t.summaryLine,
1210
+ ...t.suppressed ? { suppressed: t.suppressed } : {},
1211
+ ...t.suppressedBy ? { suppressedBy: t.suppressedBy } : {},
1212
+ ...t.children ? { children: this.serializeTasks(t.children) } : {}
1213
+ }));
1214
+ }
1215
+ };
1216
+ var QuietReporter = class extends BaseReporter {
1217
+ onStart(_tasks) {
1218
+ }
1219
+ onTaskStart(_path, _key) {
1220
+ }
1221
+ onTaskComplete(_result) {
1222
+ }
1223
+ onFinish() {
1224
+ }
1225
+ outputLogs(_results, _logsMode) {
1226
+ }
1227
+ outputSummary(result) {
1228
+ const message = result.ok ? this.c(ansi.green, "\u2713 All verifications passed") : this.c(ansi.red, "\u2717 Some verifications failed");
1229
+ process.stdout.write(`${message}
1230
+ `);
1231
+ }
1232
+ };
1233
+ function createReporter(options) {
1234
+ if (options.format === "json") {
1235
+ return new JSONReporter();
1236
+ }
1237
+ if (process.stdout.isTTY && !options.noTty) {
1238
+ return new LiveDashboardReporter(options);
1239
+ }
1240
+ return new SequentialReporter(options);
1241
+ }
1242
+
1243
+ // src/runner.ts
1244
+ import { spawn } from "child_process";
1245
+ import treeKill from "tree-kill";
1246
+ function normalizeCommand(run) {
1247
+ if (typeof run === "string") {
1248
+ const parts = run.split(/\s+/);
1249
+ return {
1250
+ cmd: parts[0],
1251
+ args: parts.slice(1)
1252
+ };
1253
+ }
1254
+ if (Array.isArray(run)) {
1255
+ return {
1256
+ cmd: run[0],
1257
+ args: run[1]
1258
+ };
1259
+ }
1260
+ return run;
1261
+ }
1262
+ var ReportingDependencyTracker = class {
1263
+ /** Map of task path/key to their results */
1264
+ results = /* @__PURE__ */ new Map();
1265
+ /** Map of task path/key to waiters (callbacks to resolve when result is available) */
1266
+ waiters = /* @__PURE__ */ new Map();
1267
+ /** Map of task path to its key (for key-based lookups) */
1268
+ pathToKey = /* @__PURE__ */ new Map();
1269
+ /** Map of task key to its path (for key-based lookups) */
1270
+ keyToPath = /* @__PURE__ */ new Map();
1271
+ /** Map of task path to its reportingDependsOn array */
1272
+ dependencies = /* @__PURE__ */ new Map();
1273
+ /** Reverse map: task path → list of tasks that depend on it */
1274
+ reverseDeps = /* @__PURE__ */ new Map();
1275
+ /** Map of task path to its running ChildProcess */
1276
+ processes = /* @__PURE__ */ new Map();
1277
+ /** Set of task paths that have been killed */
1278
+ killedPaths = /* @__PURE__ */ new Set();
1279
+ /**
1280
+ * Initialize the tracker with all tasks from the verification tree.
1281
+ * Also validates for circular dependencies and builds reverse dependency map.
1282
+ */
1283
+ initialize(nodes, parentPath = "") {
1284
+ for (const node of nodes) {
1285
+ const path = parentPath ? `${parentPath}:${node.key}` : node.key;
1286
+ this.pathToKey.set(path, node.key);
1287
+ this.keyToPath.set(node.key, path);
1288
+ if (node.reportingDependsOn && node.reportingDependsOn.length > 0) {
1289
+ this.dependencies.set(path, node.reportingDependsOn);
1290
+ }
1291
+ if (node.children) {
1292
+ this.initialize(node.children, path);
1293
+ }
1294
+ }
1295
+ if (parentPath === "") {
1296
+ this.validateNoCycles();
1297
+ this.buildReverseDeps();
1298
+ }
1299
+ }
1300
+ /**
1301
+ * Build reverse dependency map (task → tasks that depend on it)
1302
+ */
1303
+ buildReverseDeps() {
1304
+ for (const [path, deps] of this.dependencies.entries()) {
1305
+ for (const dep of deps) {
1306
+ const resolvedDep = this.resolveDependency(dep);
1307
+ if (resolvedDep) {
1308
+ const existing = this.reverseDeps.get(resolvedDep) ?? [];
1309
+ existing.push(path);
1310
+ this.reverseDeps.set(resolvedDep, existing);
1311
+ }
1312
+ }
1313
+ }
1314
+ }
1315
+ /**
1316
+ * Validate that there are no circular dependencies using DFS with coloring.
1317
+ * Throws an error with the cycle path if a cycle is detected.
1318
+ */
1319
+ validateNoCycles() {
1320
+ const WHITE = 0;
1321
+ const GRAY = 1;
1322
+ const BLACK = 2;
1323
+ const colors = /* @__PURE__ */ new Map();
1324
+ const parent = /* @__PURE__ */ new Map();
1325
+ for (const path of this.pathToKey.keys()) {
1326
+ colors.set(path, WHITE);
1327
+ }
1328
+ const dfs = (path) => {
1329
+ colors.set(path, GRAY);
1330
+ const deps = this.dependencies.get(path) ?? [];
1331
+ for (const dep of deps) {
1332
+ const depPath = this.resolveDependency(dep);
1333
+ if (!depPath) continue;
1334
+ const color = colors.get(depPath) ?? WHITE;
1335
+ if (color === GRAY) {
1336
+ const cycle = [depPath];
1337
+ let current = path;
1338
+ while (current !== depPath) {
1339
+ cycle.unshift(current);
1340
+ current = parent.get(current) ?? "";
1341
+ }
1342
+ cycle.unshift(depPath);
1343
+ return cycle.join(" \u2192 ");
1344
+ }
1345
+ if (color === WHITE) {
1346
+ parent.set(depPath, path);
1347
+ const cyclePath = dfs(depPath);
1348
+ if (cyclePath) return cyclePath;
1349
+ }
1350
+ }
1351
+ colors.set(path, BLACK);
1352
+ return null;
1353
+ };
1354
+ for (const path of this.pathToKey.keys()) {
1355
+ if (colors.get(path) === WHITE) {
1356
+ const cyclePath = dfs(path);
1357
+ if (cyclePath) {
1358
+ throw new Error(
1359
+ `Circular reporting dependency detected: ${cyclePath}`
1360
+ );
1361
+ }
1362
+ }
1363
+ }
1364
+ }
1365
+ /**
1366
+ * Resolve a dependency identifier to a task path.
1367
+ * Tries exact path match first, then key match.
1368
+ */
1369
+ resolveDependency(dep) {
1370
+ if (this.pathToKey.has(dep)) {
1371
+ return dep;
1372
+ }
1373
+ if (this.keyToPath.has(dep)) {
1374
+ return this.keyToPath.get(dep) ?? null;
1375
+ }
1376
+ return null;
1377
+ }
1378
+ /**
1379
+ * Record a task result and notify any waiters.
1380
+ * If the task failed, kills all dependent processes for early termination.
1381
+ */
1382
+ recordResult(result) {
1383
+ this.results.set(result.path, result);
1384
+ if (!result.ok) {
1385
+ this.killDependents(result.path);
1386
+ }
1387
+ const pathWaiters = this.waiters.get(result.path) ?? [];
1388
+ for (const waiter of pathWaiters) {
1389
+ waiter();
1390
+ }
1391
+ this.waiters.delete(result.path);
1392
+ const key = result.key;
1393
+ if (key !== result.path) {
1394
+ const keyWaiters = this.waiters.get(key) ?? [];
1395
+ for (const waiter of keyWaiters) {
1396
+ waiter();
1397
+ }
1398
+ this.waiters.delete(key);
1399
+ }
1400
+ }
1401
+ /**
1402
+ * Wait for all dependencies of a task to complete.
1403
+ */
1404
+ async waitForDependencies(path) {
1405
+ const deps = this.dependencies.get(path);
1406
+ if (!deps || deps.length === 0) {
1407
+ return;
1408
+ }
1409
+ const waitPromises = deps.map((dep) => this.waitForResult(dep));
1410
+ await Promise.all(waitPromises);
1411
+ }
1412
+ /**
1413
+ * Wait for a specific task result to be available.
1414
+ */
1415
+ waitForResult(pathOrKey) {
1416
+ const resolvedPath = this.resolveDependency(pathOrKey);
1417
+ if (resolvedPath && this.results.has(resolvedPath)) {
1418
+ return Promise.resolve();
1419
+ }
1420
+ if (this.results.has(pathOrKey)) {
1421
+ return Promise.resolve();
1422
+ }
1423
+ return new Promise((resolve3) => {
1424
+ const waiters = this.waiters.get(pathOrKey) ?? [];
1425
+ waiters.push(resolve3);
1426
+ this.waiters.set(pathOrKey, waiters);
1427
+ });
1428
+ }
1429
+ /**
1430
+ * Check if any dependency of a task has failed.
1431
+ * Returns the path of the first failed dependency, or null if all passed.
1432
+ */
1433
+ getFailedDependency(path) {
1434
+ const deps = this.dependencies.get(path);
1435
+ if (!deps || deps.length === 0) {
1436
+ return null;
1437
+ }
1438
+ for (const dep of deps) {
1439
+ const resolvedPath = this.resolveDependency(dep);
1440
+ if (!resolvedPath) continue;
1441
+ const result = this.results.get(resolvedPath);
1442
+ if (result && !result.ok) {
1443
+ return resolvedPath;
1444
+ }
1445
+ }
1446
+ return null;
1447
+ }
1448
+ /**
1449
+ * Check if a task has any reporting dependencies.
1450
+ */
1451
+ hasDependencies(path) {
1452
+ const deps = this.dependencies.get(path);
1453
+ return deps !== void 0 && deps.length > 0;
1454
+ }
1455
+ /**
1456
+ * Register a running process for a task.
1457
+ */
1458
+ registerProcess(path, proc) {
1459
+ this.processes.set(path, proc);
1460
+ }
1461
+ /**
1462
+ * Unregister a process (called when it completes naturally).
1463
+ */
1464
+ unregisterProcess(path) {
1465
+ this.processes.delete(path);
1466
+ }
1467
+ /**
1468
+ * Check if a task was killed.
1469
+ */
1470
+ wasKilled(path) {
1471
+ return this.killedPaths.has(path);
1472
+ }
1473
+ /**
1474
+ * Kill all processes that depend on the failed task.
1475
+ * Called when a task fails to terminate dependent tasks early.
1476
+ */
1477
+ killDependents(failedPath) {
1478
+ const dependents = this.reverseDeps.get(failedPath) ?? [];
1479
+ for (const depPath of dependents) {
1480
+ const proc = this.processes.get(depPath);
1481
+ if (proc?.pid) {
1482
+ this.killedPaths.add(depPath);
1483
+ treeKill(proc.pid, "SIGTERM", (err) => {
1484
+ if (err) {
1485
+ }
1486
+ });
1487
+ }
1488
+ }
1489
+ }
1490
+ };
1491
+ async function executeCommand(command, cwd, tracker, path) {
1492
+ const start = Date.now();
1493
+ return new Promise((resolve3) => {
1494
+ const proc = spawn(command.cmd, command.args, {
1495
+ shell: process.platform === "win32",
1496
+ cwd: command.cwd ?? cwd,
1497
+ env: { ...process.env, NO_COLOR: "1", ...command.env }
1498
+ });
1499
+ if (tracker && path) {
1500
+ tracker.registerProcess(path, proc);
1501
+ }
1502
+ let output = "";
1503
+ proc.stdout.on("data", (data) => {
1504
+ output += data.toString();
1505
+ });
1506
+ proc.stderr.on("data", (data) => {
1507
+ output += data.toString();
1508
+ });
1509
+ proc.on("close", (code, signal) => {
1510
+ if (tracker && path) {
1511
+ tracker.unregisterProcess(path);
1512
+ }
1513
+ const durationMs = Date.now() - start;
1514
+ const killed = signal === "SIGTERM" || code === 143 || (tracker?.wasKilled(path ?? "") ?? false);
1515
+ resolve3({
1516
+ code: code ?? 1,
1517
+ output,
1518
+ durationMs,
1519
+ killed
1520
+ });
1521
+ });
1522
+ proc.on("error", (err) => {
1523
+ if (tracker && path) {
1524
+ tracker.unregisterProcess(path);
1525
+ }
1526
+ const durationMs = Date.now() - start;
1527
+ resolve3({
1528
+ code: 1,
1529
+ output: `Failed to execute command: ${err.message}`,
1530
+ durationMs,
1531
+ killed: false
1532
+ });
1533
+ });
1534
+ });
1535
+ }
1536
+ function buildPath(parentPath, key) {
1537
+ return parentPath ? `${parentPath}:${key}` : key;
1538
+ }
1539
+ function matchesFilter(path, filters) {
1540
+ if (!filters || filters.length === 0) {
1541
+ return true;
1542
+ }
1543
+ return filters.some((filter) => {
1544
+ return path === filter || path.startsWith(`${filter}:`);
1545
+ });
1546
+ }
1547
+ function hasMatchingDescendant(node, parentPath, filters) {
1548
+ const path = buildPath(parentPath, node.key);
1549
+ if (matchesFilter(path, filters)) {
1550
+ return true;
1551
+ }
1552
+ if (node.children) {
1553
+ return node.children.some(
1554
+ (child) => hasMatchingDescendant(child, path, filters)
1555
+ );
1556
+ }
1557
+ return false;
1558
+ }
1559
+ var VerificationRunner = class {
1560
+ registry;
1561
+ options;
1562
+ callbacks;
1563
+ dependencyTracker;
1564
+ constructor(options = {}, registry = defaultRegistry, callbacks = {}) {
1565
+ this.options = options;
1566
+ this.registry = registry;
1567
+ this.callbacks = callbacks;
1568
+ this.dependencyTracker = new ReportingDependencyTracker();
1569
+ }
1570
+ /**
1571
+ * Run all verification tasks
1572
+ */
1573
+ async run(tasks) {
1574
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
1575
+ const wallStart = Date.now();
1576
+ this.dependencyTracker.initialize(tasks);
1577
+ const results = await this.runNodes(tasks, "");
1578
+ const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
1579
+ const durationMs = Date.now() - wallStart;
1580
+ const allOk = results.every((r) => r.ok);
1581
+ return {
1582
+ ok: allOk,
1583
+ startedAt,
1584
+ finishedAt,
1585
+ durationMs,
1586
+ tasks: results
1587
+ };
1588
+ }
1589
+ /**
1590
+ * Run a list of nodes with the appropriate strategy
1591
+ */
1592
+ async runNodes(nodes, parentPath, strategy = "parallel") {
1593
+ const filteredNodes = nodes.filter(
1594
+ (node) => hasMatchingDescendant(node, parentPath, this.options.filter)
1595
+ );
1596
+ if (filteredNodes.length === 0) {
1597
+ return [];
1598
+ }
1599
+ switch (strategy) {
1600
+ case "parallel":
1601
+ return Promise.all(
1602
+ filteredNodes.map((node) => this.runNode(node, parentPath))
1603
+ );
1604
+ case "sequential": {
1605
+ const results = [];
1606
+ for (const node of filteredNodes) {
1607
+ results.push(await this.runNode(node, parentPath));
1608
+ }
1609
+ return results;
1610
+ }
1611
+ case "fail-fast": {
1612
+ const results = [];
1613
+ for (const node of filteredNodes) {
1614
+ const result = await this.runNode(node, parentPath);
1615
+ results.push(result);
1616
+ if (!result.ok) {
1617
+ break;
1618
+ }
1619
+ }
1620
+ return results;
1621
+ }
1622
+ }
1623
+ }
1624
+ /**
1625
+ * Run a single node (leaf or group)
1626
+ */
1627
+ async runNode(node, parentPath) {
1628
+ const path = buildPath(parentPath, node.key);
1629
+ this.callbacks.onTaskStart?.(path, node.key);
1630
+ if (node.children && node.children.length > 0) {
1631
+ const start = Date.now();
1632
+ const childResults = await this.runNodes(
1633
+ node.children,
1634
+ path,
1635
+ node.strategy ?? "parallel"
1636
+ );
1637
+ const durationMs2 = Date.now() - start;
1638
+ const allOk = childResults.every((r) => r.ok || r.suppressed);
1639
+ const allSuppressed = childResults.length > 0 && childResults.every((r) => r.suppressed);
1640
+ const anySuppressed = childResults.some((r) => r.suppressed);
1641
+ const result2 = {
1642
+ key: node.key,
1643
+ path,
1644
+ ok: allOk,
1645
+ code: allOk ? 0 : 1,
1646
+ durationMs: durationMs2,
1647
+ output: "",
1648
+ summaryLine: allOk ? node.successLabel ?? `${node.key}: all passed` : node.failureLabel ?? `${node.key}: some failed`,
1649
+ children: childResults
1650
+ };
1651
+ if (allSuppressed) {
1652
+ result2.suppressed = true;
1653
+ result2.suppressedBy = childResults[0].suppressedBy;
1654
+ } else if (anySuppressed && !allOk) {
1655
+ }
1656
+ this.dependencyTracker.recordResult(result2);
1657
+ this.callbacks.onTaskComplete?.(result2);
1658
+ return result2;
1659
+ }
1660
+ if (!node.run) {
1661
+ const result2 = {
1662
+ key: node.key,
1663
+ path,
1664
+ ok: true,
1665
+ code: 0,
1666
+ durationMs: 0,
1667
+ output: "",
1668
+ summaryLine: `${node.key}: no command specified`
1669
+ };
1670
+ this.dependencyTracker.recordResult(result2);
1671
+ this.callbacks.onTaskComplete?.(result2);
1672
+ return result2;
1673
+ }
1674
+ const command = normalizeCommand(node.run);
1675
+ const cwd = this.options.cwd ?? process.cwd();
1676
+ const { code, output, durationMs, killed } = await executeCommand(
1677
+ command,
1678
+ cwd,
1679
+ this.dependencyTracker,
1680
+ path
1681
+ );
1682
+ const ok = code === 0;
1683
+ if (killed) {
1684
+ await this.dependencyTracker.waitForDependencies(path);
1685
+ const failedDep = this.dependencyTracker.getFailedDependency(path);
1686
+ const result2 = {
1687
+ key: node.key,
1688
+ path,
1689
+ ok: false,
1690
+ code,
1691
+ durationMs,
1692
+ output,
1693
+ summaryLine: `${node.key}: terminated`,
1694
+ suppressed: true,
1695
+ suppressedBy: failedDep ?? "unknown"
1696
+ };
1697
+ this.dependencyTracker.recordResult(result2);
1698
+ this.callbacks.onTaskComplete?.(result2);
1699
+ return result2;
1700
+ }
1701
+ const cmdString = `${command.cmd} ${command.args.join(" ")}`;
1702
+ const parsed = this.registry.parse(
1703
+ output,
1704
+ code,
1705
+ node.parser,
1706
+ cmdString
1707
+ );
1708
+ let summaryLine;
1709
+ if (ok) {
1710
+ summaryLine = node.successLabel ? `${node.key}: ${node.successLabel}` : `${node.key}: ${parsed.summary}`;
1711
+ } else {
1712
+ summaryLine = node.failureLabel ? `${node.key}: ${node.failureLabel}` : `${node.key}: ${parsed.summary}`;
1713
+ }
1714
+ let result = {
1715
+ key: node.key,
1716
+ path,
1717
+ ok,
1718
+ code,
1719
+ durationMs,
1720
+ output,
1721
+ summaryLine,
1722
+ metrics: parsed.metrics
1723
+ };
1724
+ if (this.dependencyTracker.hasDependencies(path)) {
1725
+ await this.dependencyTracker.waitForDependencies(path);
1726
+ if (!ok) {
1727
+ const failedDep = this.dependencyTracker.getFailedDependency(path);
1728
+ if (failedDep) {
1729
+ result = {
1730
+ ...result,
1731
+ suppressed: true,
1732
+ suppressedBy: failedDep
1733
+ };
1734
+ }
1735
+ }
1736
+ }
1737
+ this.dependencyTracker.recordResult(result);
1738
+ this.callbacks.onTaskComplete?.(result);
1739
+ return result;
1740
+ }
1741
+ };
1742
+
1743
+ // src/index.ts
1744
+ async function verify(config, cliOptions) {
1745
+ const options = mergeOptions(config.options, cliOptions);
1746
+ const reporter = createReporter(options);
1747
+ reporter.onStart?.(config.tasks);
1748
+ const runner = new VerificationRunner(options, void 0, {
1749
+ onTaskStart: (path, key) => reporter.onTaskStart(path, key),
1750
+ onTaskComplete: (result2) => reporter.onTaskComplete(result2)
1751
+ });
1752
+ const result = await runner.run(config.tasks);
1753
+ reporter.onFinish?.();
1754
+ reporter.outputLogs(result.tasks, options.logs ?? "failed");
1755
+ reporter.outputSummary(result);
1756
+ return result;
1757
+ }
1758
+ async function verifyFromConfig(cwd = process.cwd(), cliOptions) {
1759
+ const config = await loadConfigFromCwd(cwd, cliOptions?.cwd);
1760
+ if (!config) {
1761
+ throw new Error(
1762
+ `No verify config found in ${cwd}. Create a verify.config.ts file.`
1763
+ );
1764
+ }
1765
+ return verify(config, { ...cliOptions, cwd });
1766
+ }
1767
+ export {
1768
+ JSONReporter,
1769
+ LiveDashboardReporter,
1770
+ ParserRegistry,
1771
+ QuietReporter,
1772
+ SequentialReporter,
1773
+ SequentialReporter as TTYReporter,
1774
+ VerificationRunner,
1775
+ biomeParser,
1776
+ createReporter,
1777
+ defaultRegistry,
1778
+ defineConfig,
1779
+ defineTask,
1780
+ detectTasks,
1781
+ discoverPackages,
1782
+ findConfigFile,
1783
+ generateConfigContent,
1784
+ genericParser,
1785
+ gotestParser,
1786
+ hasPackageChanged,
1787
+ loadConfig,
1788
+ loadConfigFromCwd,
1789
+ mergeOptions,
1790
+ runInit,
1791
+ tscParser,
1792
+ verify,
1793
+ verifyFromConfig,
1794
+ vitestParser
1795
+ };
1796
+ //# sourceMappingURL=index.js.map