@halecraft/verify 1.0.0 → 1.2.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 CHANGED
@@ -1,14 +1,76 @@
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
1
  // src/config.ts
9
2
  import { existsSync } from "fs";
10
3
  import { join, resolve } from "path";
11
4
  import { pathToFileURL } from "url";
5
+ import { z } from "zod";
6
+ var ConfigError = class extends Error {
7
+ constructor(message, configPath) {
8
+ super(message);
9
+ this.configPath = configPath;
10
+ this.name = "ConfigError";
11
+ }
12
+ };
13
+ var EnvSchema = z.record(z.string(), z.string().nullable()).optional();
14
+ var VerificationCommandSchema = z.object({
15
+ cmd: z.string(),
16
+ args: z.array(z.string()),
17
+ cwd: z.string().optional(),
18
+ env: EnvSchema,
19
+ timeout: z.number().positive().optional()
20
+ });
21
+ var VerificationNodeSchema = z.lazy(
22
+ () => z.object({
23
+ key: z.string().min(1, "Task key cannot be empty").refine((key) => !key.includes(":"), {
24
+ message: "Task key cannot contain ':' (reserved for paths)"
25
+ }),
26
+ name: z.string().optional(),
27
+ run: z.union([z.string(), VerificationCommandSchema]).optional(),
28
+ children: z.array(VerificationNodeSchema).optional(),
29
+ strategy: z.enum(["parallel", "sequential", "fail-fast"]).optional(),
30
+ parser: z.string().optional(),
31
+ successLabel: z.string().optional(),
32
+ failureLabel: z.string().optional(),
33
+ reportingDependsOn: z.array(z.string()).optional(),
34
+ timeout: z.number().positive().optional(),
35
+ env: EnvSchema
36
+ })
37
+ );
38
+ var VerifyOptionsSchema = z.object({
39
+ logs: z.enum(["all", "failed", "none"]).optional(),
40
+ format: z.enum(["human", "json"]).optional(),
41
+ filter: z.array(z.string()).optional(),
42
+ cwd: z.string().optional(),
43
+ noColor: z.boolean().optional(),
44
+ topLevelOnly: z.boolean().optional(),
45
+ noTty: z.boolean().optional(),
46
+ passthrough: z.array(z.string()).optional()
47
+ });
48
+ var PackageDiscoveryOptionsSchema = z.object({
49
+ patterns: z.array(z.string()).optional(),
50
+ filter: z.array(z.string()).optional(),
51
+ changed: z.boolean().optional()
52
+ });
53
+ var VerifyConfigSchema = z.object({
54
+ tasks: z.array(VerificationNodeSchema),
55
+ packages: PackageDiscoveryOptionsSchema.optional(),
56
+ options: VerifyOptionsSchema.optional(),
57
+ env: EnvSchema
58
+ });
59
+ function validateConfig(value, configPath) {
60
+ const result = VerifyConfigSchema.safeParse(value);
61
+ if (!result.success) {
62
+ const errors = result.error.issues.map((issue) => {
63
+ const path = issue.path.join(".");
64
+ return path ? `${path}: ${issue.message}` : issue.message;
65
+ });
66
+ throw new ConfigError(
67
+ `Invalid config:
68
+ - ${errors.join("\n - ")}`,
69
+ configPath
70
+ );
71
+ }
72
+ return result.data;
73
+ }
12
74
  function defineConfig(config) {
13
75
  return config;
14
76
  }
@@ -38,9 +100,9 @@ async function loadConfig(configPath) {
38
100
  const fileUrl = pathToFileURL(absolutePath).href;
39
101
  const module = await import(fileUrl);
40
102
  if (!module.default) {
41
- throw new Error(`Config file ${configPath} must have a default export`);
103
+ throw new ConfigError(`Config file must have a default export`, configPath);
42
104
  }
43
- return module.default;
105
+ return validateConfig(module.default, configPath);
44
106
  }
45
107
  async function loadConfigFromCwd(cwd, configPath) {
46
108
  if (configPath) {
@@ -60,13 +122,32 @@ function mergeOptions(configOptions, cliOptions) {
60
122
  cwd: cliOptions?.cwd ?? configOptions?.cwd ?? process.cwd(),
61
123
  noColor: cliOptions?.noColor ?? configOptions?.noColor ?? false,
62
124
  topLevelOnly: cliOptions?.topLevelOnly ?? configOptions?.topLevelOnly ?? false,
63
- noTty: cliOptions?.noTty ?? configOptions?.noTty ?? false
125
+ noTty: cliOptions?.noTty ?? configOptions?.noTty ?? false,
126
+ passthrough: cliOptions?.passthrough ?? configOptions?.passthrough
64
127
  };
65
128
  }
66
129
 
67
130
  // src/discovery.ts
68
- import { existsSync as existsSync2, readdirSync, statSync } from "fs";
131
+ import { existsSync as existsSync2, readdirSync, readFileSync, statSync } from "fs";
69
132
  import { join as join2, relative } from "path";
133
+
134
+ // src/schemas/package-json.ts
135
+ import { z as z2 } from "zod";
136
+ var PackageJsonSchema = z2.object({
137
+ name: z2.string().optional(),
138
+ scripts: z2.record(z2.string(), z2.string()).optional()
139
+ }).passthrough();
140
+ function parsePackageJson(content) {
141
+ try {
142
+ const parsed = JSON.parse(content);
143
+ const result = PackageJsonSchema.safeParse(parsed);
144
+ return result.success ? result.data : null;
145
+ } catch {
146
+ return null;
147
+ }
148
+ }
149
+
150
+ // src/discovery.ts
70
151
  var DEFAULT_PATTERNS = ["packages/*", "apps/*"];
71
152
  function findMatchingDirs(rootDir, patterns) {
72
153
  const results = [];
@@ -98,8 +179,9 @@ function getPackageName(packageDir) {
98
179
  return null;
99
180
  }
100
181
  try {
101
- const content = __require(packageJsonPath);
102
- return content.name ?? null;
182
+ const content = readFileSync(packageJsonPath, "utf-8");
183
+ const parsed = parsePackageJson(content);
184
+ return parsed?.name ?? null;
103
185
  } catch {
104
186
  return null;
105
187
  }
@@ -132,8 +214,133 @@ async function hasPackageChanged(_packagePath, _baseBranch = "main") {
132
214
  return true;
133
215
  }
134
216
 
217
+ // src/filter.ts
218
+ import leven from "leven";
219
+
220
+ // src/tree.ts
221
+ var PATH_SEPARATOR = ":";
222
+ function buildTaskPath(parentPath, key) {
223
+ return parentPath ? `${parentPath}${PATH_SEPARATOR}${key}` : key;
224
+ }
225
+ function walkNodes(nodes, visitor, parentPath = "", depth = 0) {
226
+ for (const node of nodes) {
227
+ const path = buildTaskPath(parentPath, node.key);
228
+ visitor(node, path, depth);
229
+ if (node.children) {
230
+ walkNodes(node.children, visitor, path, depth + 1);
231
+ }
232
+ }
233
+ }
234
+ function collectPaths(nodes) {
235
+ const paths = [];
236
+ walkNodes(nodes, (_node, path) => {
237
+ paths.push(path);
238
+ });
239
+ return paths;
240
+ }
241
+
242
+ // src/filter.ts
243
+ var TaskNotFoundError = class extends Error {
244
+ constructor(filter, suggestion, availableTasks) {
245
+ super(buildErrorMessage(filter, suggestion, availableTasks));
246
+ this.filter = filter;
247
+ this.suggestion = suggestion;
248
+ this.availableTasks = availableTasks;
249
+ this.name = "TaskNotFoundError";
250
+ }
251
+ exitCode = 2;
252
+ };
253
+ var AmbiguousTaskError = class extends Error {
254
+ constructor(filter, matches) {
255
+ super(buildAmbiguousErrorMessage(filter, matches));
256
+ this.filter = filter;
257
+ this.matches = matches;
258
+ this.name = "AmbiguousTaskError";
259
+ }
260
+ exitCode = 2;
261
+ };
262
+ function buildErrorMessage(filter, suggestion, availableTasks) {
263
+ let message = `Task "${filter}" not found.`;
264
+ if (suggestion) {
265
+ message += `
266
+
267
+ Did you mean "${suggestion}"?`;
268
+ }
269
+ message += "\n\nAvailable tasks:";
270
+ for (const task of availableTasks) {
271
+ message += `
272
+ ${task}`;
273
+ }
274
+ return message;
275
+ }
276
+ function buildAmbiguousErrorMessage(filter, matches) {
277
+ let message = `Task "${filter}" is ambiguous.`;
278
+ message += "\n\nMatches multiple tasks:";
279
+ for (const match of matches) {
280
+ message += `
281
+ ${match}`;
282
+ }
283
+ return message;
284
+ }
285
+ function findBestSuggestion(availablePaths, invalidFilter) {
286
+ let bestPath;
287
+ let bestDistance = Number.POSITIVE_INFINITY;
288
+ const threshold = Math.max(2, Math.floor(invalidFilter.length / 3));
289
+ for (const path of availablePaths) {
290
+ const distance = leven(invalidFilter, path);
291
+ if (distance < bestDistance && distance <= threshold) {
292
+ bestDistance = distance;
293
+ bestPath = path;
294
+ }
295
+ const lastSegment = path.split(PATH_SEPARATOR).pop();
296
+ if (lastSegment && lastSegment !== path) {
297
+ const segmentDistance = leven(invalidFilter, lastSegment);
298
+ if (segmentDistance < bestDistance && segmentDistance <= threshold) {
299
+ bestDistance = segmentDistance;
300
+ bestPath = path;
301
+ }
302
+ }
303
+ }
304
+ return bestPath;
305
+ }
306
+ function resolveFilter(filter, availablePaths) {
307
+ if (availablePaths.includes(filter)) {
308
+ return { original: filter, resolved: filter, wasShortcut: false };
309
+ }
310
+ const prefixMatches = availablePaths.filter(
311
+ (path) => path === filter || path.startsWith(`${filter}${PATH_SEPARATOR}`)
312
+ );
313
+ if (prefixMatches.length > 0) {
314
+ return { original: filter, resolved: filter, wasShortcut: false };
315
+ }
316
+ const childMatches = availablePaths.filter((path) => {
317
+ const lastSegment = path.split(PATH_SEPARATOR).pop();
318
+ return lastSegment === filter;
319
+ });
320
+ if (childMatches.length === 1) {
321
+ return {
322
+ original: filter,
323
+ resolved: childMatches[0],
324
+ wasShortcut: true
325
+ };
326
+ }
327
+ if (childMatches.length > 1) {
328
+ throw new AmbiguousTaskError(filter, childMatches);
329
+ }
330
+ const suggestion = findBestSuggestion(availablePaths, filter);
331
+ throw new TaskNotFoundError(filter, suggestion, availablePaths);
332
+ }
333
+ function resolveFilters(nodes, filters) {
334
+ const availablePaths = collectPaths(nodes);
335
+ const resolved = [];
336
+ for (const filter of filters) {
337
+ resolved.push(resolveFilter(filter, availablePaths));
338
+ }
339
+ return resolved;
340
+ }
341
+
135
342
  // src/init/detect.ts
136
- import { existsSync as existsSync3, readFileSync } from "fs";
343
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
137
344
  import { join as join3 } from "path";
138
345
  var TOOL_PATTERNS = [
139
346
  // Biome
@@ -292,8 +499,8 @@ function readPackageJson(cwd) {
292
499
  return null;
293
500
  }
294
501
  try {
295
- const content = readFileSync(packageJsonPath, "utf-8");
296
- return JSON.parse(content);
502
+ const content = readFileSync2(packageJsonPath, "utf-8");
503
+ return parsePackageJson(content);
297
504
  } catch {
298
505
  return null;
299
506
  }
@@ -306,7 +513,7 @@ function extractOptimizedCommand(cwd, scriptContent) {
306
513
  const match = scriptContent.match(tool.pattern);
307
514
  if (match && binaryExists(cwd, tool.binary)) {
308
515
  const args = tool.getArgs(match, scriptContent);
309
- const command = args ? `./node_modules/.bin/${tool.binary} ${args}` : `./node_modules/.bin/${tool.binary}`;
516
+ const command = args ? `${tool.binary} ${args}` : tool.binary;
310
517
  return { command, parser: tool.parser };
311
518
  }
312
519
  }
@@ -383,7 +590,7 @@ function detectTasks(cwd) {
383
590
  const packageManager = detectPackageManager(cwd);
384
591
  const tasks = detectFromPackageJson(cwd);
385
592
  return tasks.map((task) => {
386
- if (task.command.startsWith("./")) {
593
+ if (!task.command.startsWith("npm run ")) {
387
594
  return task;
388
595
  }
389
596
  return {
@@ -419,12 +626,15 @@ function generateSkeleton(format) {
419
626
  return `${importStatement}
420
627
 
421
628
  export default defineConfig({
629
+ env: {
630
+ NO_COLOR: "1",
631
+ },
422
632
  tasks: [
423
633
  // Add your verification tasks here
424
634
  // Example:
425
- // { key: "format", run: "pnpm lint" },
426
- // { key: "types", run: "pnpm typecheck" },
427
- // { key: "test", run: "pnpm test" },
635
+ // { key: "format", run: "biome check ." },
636
+ // { key: "types", run: "tsc --noEmit" },
637
+ // { key: "test", run: "vitest run" },
428
638
  ],
429
639
  })
430
640
  `;
@@ -439,6 +649,9 @@ function generateConfigContent(tasks, format) {
439
649
  return `${importStatement}
440
650
 
441
651
  export default defineConfig({
652
+ env: {
653
+ NO_COLOR: "1",
654
+ },
442
655
  tasks: [
443
656
  ${taskLines.join(",\n")},
444
657
  ],
@@ -764,6 +977,18 @@ var vitestParser = {
764
977
  };
765
978
 
766
979
  // src/parsers/index.ts
980
+ var parsers = {
981
+ /** Vitest/Jest test runner output parser */
982
+ vitest: "vitest",
983
+ /** TypeScript compiler (tsc/tsgo) output parser */
984
+ tsc: "tsc",
985
+ /** Biome/ESLint linter output parser */
986
+ biome: "biome",
987
+ /** Go test runner output parser */
988
+ gotest: "gotest",
989
+ /** Generic fallback parser */
990
+ generic: "generic"
991
+ };
767
992
  var ParserRegistry = class {
768
993
  parsers = /* @__PURE__ */ new Map();
769
994
  constructor() {
@@ -791,24 +1016,24 @@ var ParserRegistry = class {
791
1016
  detectParser(cmd) {
792
1017
  const cmdLower = cmd.toLowerCase();
793
1018
  if (cmdLower.includes("vitest") || cmdLower.includes("jest")) {
794
- return "vitest";
1019
+ return parsers.vitest;
795
1020
  }
796
1021
  if (cmdLower.includes("tsc") || cmdLower.includes("tsgo")) {
797
- return "tsc";
1022
+ return parsers.tsc;
798
1023
  }
799
1024
  if (cmdLower.includes("biome") || cmdLower.includes("eslint")) {
800
- return "biome";
1025
+ return parsers.biome;
801
1026
  }
802
1027
  if (cmdLower.includes("go test") || cmdLower.includes("go") && cmdLower.includes("test")) {
803
- return "gotest";
1028
+ return parsers.gotest;
804
1029
  }
805
- return "generic";
1030
+ return parsers.generic;
806
1031
  }
807
1032
  /**
808
1033
  * Parse output using the specified or auto-detected parser
809
1034
  */
810
1035
  parse(output, exitCode, parserId, cmd) {
811
- const id = parserId ?? (cmd ? this.detectParser(cmd) : "generic");
1036
+ const id = parserId ?? (cmd ? this.detectParser(cmd) : parsers.generic);
812
1037
  const parser = this.parsers.get(id) ?? genericParser;
813
1038
  const result = parser.parse(output, exitCode);
814
1039
  if (result) {
@@ -940,16 +1165,12 @@ var BaseReporter = class {
940
1165
  return this.taskDepths.get(path) ?? 0;
941
1166
  }
942
1167
  /**
943
- * Recursively collect task depths from verification tree
1168
+ * Collect task depths from verification tree using walkNodes
944
1169
  */
945
- collectTaskDepths(nodes, parentPath, depth) {
946
- for (const node of nodes) {
947
- const path = parentPath ? `${parentPath}:${node.key}` : node.key;
1170
+ collectTaskDepths(nodes) {
1171
+ walkNodes(nodes, (_node, path, depth) => {
948
1172
  this.taskDepths.set(path, depth);
949
- if (node.children) {
950
- this.collectTaskDepths(node.children, path, depth + 1);
951
- }
952
- }
1173
+ });
953
1174
  }
954
1175
  /**
955
1176
  * Extract summary from task result
@@ -1027,16 +1248,15 @@ var LiveDashboardReporter = class extends BaseReporter {
1027
1248
  * Initialize task list from verification nodes
1028
1249
  */
1029
1250
  onStart(tasks) {
1030
- this.collectTasks(tasks, "", 0);
1251
+ this.collectTasks(tasks);
1031
1252
  this.stream.write(cursor.hide);
1032
1253
  this.spinner.start(() => this.redraw());
1033
1254
  }
1034
1255
  /**
1035
- * Recursively collect tasks from verification tree
1256
+ * Collect tasks from verification tree using walkNodes
1036
1257
  */
1037
- collectTasks(nodes, parentPath, depth) {
1038
- for (const node of nodes) {
1039
- const path = parentPath ? `${parentPath}:${node.key}` : node.key;
1258
+ collectTasks(nodes) {
1259
+ walkNodes(nodes, (node, path, depth) => {
1040
1260
  this.tasks.set(path, {
1041
1261
  key: node.key,
1042
1262
  path,
@@ -1045,10 +1265,7 @@ var LiveDashboardReporter = class extends BaseReporter {
1045
1265
  });
1046
1266
  this.taskOrder.push(path);
1047
1267
  this.taskDepths.set(path, depth);
1048
- if (node.children) {
1049
- this.collectTasks(node.children, path, depth + 1);
1050
- }
1051
- }
1268
+ });
1052
1269
  }
1053
1270
  /**
1054
1271
  * Get display key - shows :key for nested, key for root
@@ -1084,9 +1301,9 @@ var LiveDashboardReporter = class extends BaseReporter {
1084
1301
  }
1085
1302
  const summary = this.extractSummary(task.result);
1086
1303
  if (task.result.ok) {
1087
- return ` ${indent}${this.okMark()} verified ${this.c(ansi.bold, displayKey)} ${this.c(ansi.dim, `(${summary}, ${duration})`)}`;
1304
+ return `${indent}${this.okMark()} verified ${this.c(ansi.bold, displayKey)} ${this.c(ansi.dim, `(${summary}, ${duration})`)}`;
1088
1305
  }
1089
- return `${indent}${this.failMark()} failed ${this.c(ansi.bold, displayKey)} ${this.c(ansi.dim, `(${summary}, ${duration})`)}`;
1306
+ return `${indent}${this.failMark()} failed ${this.c(ansi.bold, displayKey)} ${this.c(ansi.dim, `(${summary}, ${duration})`)}`;
1090
1307
  }
1091
1308
  return "";
1092
1309
  }
@@ -1141,7 +1358,7 @@ var SequentialReporter = class extends BaseReporter {
1141
1358
  this.topLevelOnly = options.topLevelOnly ?? false;
1142
1359
  }
1143
1360
  onStart(tasks) {
1144
- this.collectTaskDepths(tasks, "", 0);
1361
+ this.collectTaskDepths(tasks);
1145
1362
  }
1146
1363
  /**
1147
1364
  * Check if task should be displayed based on topLevelOnly flag
@@ -1166,13 +1383,18 @@ var SequentialReporter = class extends BaseReporter {
1166
1383
  );
1167
1384
  return;
1168
1385
  }
1169
- const mark = result.ok ? this.okMark() : this.failMark();
1170
- const verb = result.ok ? "verified" : "failed";
1171
1386
  const summary = this.extractSummary(result);
1172
- this.stream.write(
1173
- `${mark} ${verb} ${this.c(ansi.bold, result.path)} ${this.c(ansi.dim, `(${summary}, ${duration})`)}
1387
+ if (result.ok) {
1388
+ this.stream.write(
1389
+ `${this.okMark()} verified ${this.c(ansi.bold, result.path)} ${this.c(ansi.dim, `(${summary}, ${duration})`)}
1174
1390
  `
1175
- );
1391
+ );
1392
+ } else {
1393
+ this.stream.write(
1394
+ `${this.failMark()} failed ${this.c(ansi.bold, result.path)} ${this.c(ansi.dim, `(${summary}, ${duration})`)}
1395
+ `
1396
+ );
1397
+ }
1176
1398
  }
1177
1399
  onFinish() {
1178
1400
  }
@@ -1242,23 +1464,11 @@ function createReporter(options) {
1242
1464
 
1243
1465
  // src/runner.ts
1244
1466
  import { spawn } from "child_process";
1467
+ import { dirname, join as join4, resolve as resolve3 } from "path";
1468
+ import treeKill2 from "tree-kill";
1469
+
1470
+ // src/dependency-tracker.ts
1245
1471
  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
1472
  var ReportingDependencyTracker = class {
1263
1473
  /** Map of task path/key to their results */
1264
1474
  results = /* @__PURE__ */ new Map();
@@ -1276,26 +1486,22 @@ var ReportingDependencyTracker = class {
1276
1486
  processes = /* @__PURE__ */ new Map();
1277
1487
  /** Set of task paths that have been killed */
1278
1488
  killedPaths = /* @__PURE__ */ new Set();
1489
+ /** Set of task paths that will actually run (based on filter) */
1490
+ activePaths = /* @__PURE__ */ new Set();
1279
1491
  /**
1280
1492
  * Initialize the tracker with all tasks from the verification tree.
1281
1493
  * Also validates for circular dependencies and builds reverse dependency map.
1282
1494
  */
1283
- initialize(nodes, parentPath = "") {
1284
- for (const node of nodes) {
1285
- const path = parentPath ? `${parentPath}:${node.key}` : node.key;
1495
+ initialize(nodes) {
1496
+ walkNodes(nodes, (node, path) => {
1286
1497
  this.pathToKey.set(path, node.key);
1287
1498
  this.keyToPath.set(node.key, path);
1288
1499
  if (node.reportingDependsOn && node.reportingDependsOn.length > 0) {
1289
1500
  this.dependencies.set(path, node.reportingDependsOn);
1290
1501
  }
1291
- if (node.children) {
1292
- this.initialize(node.children, path);
1293
- }
1294
- }
1295
- if (parentPath === "") {
1296
- this.validateNoCycles();
1297
- this.buildReverseDeps();
1298
- }
1502
+ });
1503
+ this.validateNoCycles();
1504
+ this.buildReverseDeps();
1299
1505
  }
1300
1506
  /**
1301
1507
  * Build reverse dependency map (task → tasks that depend on it)
@@ -1398,15 +1604,35 @@ var ReportingDependencyTracker = class {
1398
1604
  this.waiters.delete(key);
1399
1605
  }
1400
1606
  }
1607
+ /**
1608
+ * Mark a task as active (will actually run).
1609
+ * Called before running each task to track which tasks are in the execution set.
1610
+ */
1611
+ markActive(path) {
1612
+ this.activePaths.add(path);
1613
+ }
1614
+ /**
1615
+ * Check if a dependency is active (will run).
1616
+ * If not active, we shouldn't wait for it.
1617
+ */
1618
+ isDependencyActive(dep) {
1619
+ const resolvedPath = this.resolveDependency(dep);
1620
+ if (!resolvedPath) {
1621
+ return false;
1622
+ }
1623
+ return this.activePaths.has(resolvedPath);
1624
+ }
1401
1625
  /**
1402
1626
  * Wait for all dependencies of a task to complete.
1627
+ * Only waits for dependencies that are actually active (will run).
1403
1628
  */
1404
1629
  async waitForDependencies(path) {
1405
1630
  const deps = this.dependencies.get(path);
1406
1631
  if (!deps || deps.length === 0) {
1407
1632
  return;
1408
1633
  }
1409
- const waitPromises = deps.map((dep) => this.waitForResult(dep));
1634
+ const activeDeps = deps.filter((dep) => this.isDependencyActive(dep));
1635
+ const waitPromises = activeDeps.map((dep) => this.waitForResult(dep));
1410
1636
  await Promise.all(waitPromises);
1411
1637
  }
1412
1638
  /**
@@ -1420,9 +1646,9 @@ var ReportingDependencyTracker = class {
1420
1646
  if (this.results.has(pathOrKey)) {
1421
1647
  return Promise.resolve();
1422
1648
  }
1423
- return new Promise((resolve3) => {
1649
+ return new Promise((resolve4) => {
1424
1650
  const waiters = this.waiters.get(pathOrKey) ?? [];
1425
- waiters.push(resolve3);
1651
+ waiters.push(resolve4);
1426
1652
  this.waiters.set(pathOrKey, waiters);
1427
1653
  });
1428
1654
  }
@@ -1488,18 +1714,116 @@ var ReportingDependencyTracker = class {
1488
1714
  }
1489
1715
  }
1490
1716
  };
1717
+
1718
+ // src/runner.ts
1719
+ function mergeEnv(base, overlay) {
1720
+ if (!overlay) return { ...base };
1721
+ const result = { ...base };
1722
+ for (const [key, value] of Object.entries(overlay)) {
1723
+ if (value === null) {
1724
+ result[key] = null;
1725
+ } else {
1726
+ result[key] = value;
1727
+ }
1728
+ }
1729
+ return result;
1730
+ }
1731
+ function buildFinalEnv(processEnv, path, overlay) {
1732
+ const result = {};
1733
+ for (const [key, value] of Object.entries(processEnv)) {
1734
+ if (value !== void 0) {
1735
+ result[key] = value;
1736
+ }
1737
+ }
1738
+ result.PATH = path;
1739
+ if (overlay) {
1740
+ for (const [key, value] of Object.entries(overlay)) {
1741
+ if (value === null) {
1742
+ delete result[key];
1743
+ } else {
1744
+ result[key] = value;
1745
+ }
1746
+ }
1747
+ }
1748
+ return result;
1749
+ }
1750
+ function shellEscape(arg) {
1751
+ if (/[\s"'`$\\!&|;<>(){}[\]*?#~]/.test(arg)) {
1752
+ return `"${arg.replace(/"/g, '\\"')}"`;
1753
+ }
1754
+ return arg;
1755
+ }
1756
+ function buildNodeModulesPath(cwd) {
1757
+ const pathSeparator = process.platform === "win32" ? ";" : ":";
1758
+ const existingPath = process.env.PATH ?? "";
1759
+ const binPaths = [];
1760
+ let current = resolve3(cwd);
1761
+ while (true) {
1762
+ binPaths.push(join4(current, "node_modules", ".bin"));
1763
+ const parent = dirname(current);
1764
+ if (parent === current) break;
1765
+ current = parent;
1766
+ }
1767
+ return [...binPaths, existingPath].join(pathSeparator);
1768
+ }
1769
+ function normalizeCommand(run, nodeTimeout, passthrough, inheritedEnv) {
1770
+ if (typeof run === "string") {
1771
+ let cmd = run;
1772
+ if (passthrough && passthrough.length > 0) {
1773
+ const escapedArgs = passthrough.map(shellEscape).join(" ");
1774
+ cmd = `${run} ${escapedArgs}`;
1775
+ }
1776
+ return {
1777
+ cmd,
1778
+ args: [],
1779
+ shell: true,
1780
+ timeout: nodeTimeout,
1781
+ env: inheritedEnv
1782
+ };
1783
+ }
1784
+ const args = passthrough ? [...run.args, ...passthrough] : run.args;
1785
+ const env = mergeEnv(inheritedEnv ?? {}, run.env);
1786
+ return {
1787
+ cmd: run.cmd,
1788
+ args,
1789
+ cwd: run.cwd,
1790
+ env: Object.keys(env).length > 0 ? env : void 0,
1791
+ shell: false,
1792
+ // Command-level timeout takes precedence over node-level timeout
1793
+ timeout: run.timeout ?? nodeTimeout
1794
+ };
1795
+ }
1491
1796
  async function executeCommand(command, cwd, tracker, path) {
1492
1797
  const start = Date.now();
1493
- return new Promise((resolve3) => {
1798
+ return new Promise((resolve4) => {
1799
+ const useShell = command.shell || process.platform === "win32";
1800
+ const effectiveCwd = command.cwd ?? cwd;
1801
+ const finalEnv = buildFinalEnv(
1802
+ process.env,
1803
+ buildNodeModulesPath(effectiveCwd),
1804
+ command.env
1805
+ );
1494
1806
  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 }
1807
+ shell: useShell,
1808
+ cwd: effectiveCwd,
1809
+ env: finalEnv
1498
1810
  });
1499
1811
  if (tracker && path) {
1500
1812
  tracker.registerProcess(path, proc);
1501
1813
  }
1502
1814
  let output = "";
1815
+ let timedOut = false;
1816
+ let timeoutId;
1817
+ if (command.timeout && proc.pid) {
1818
+ const pid = proc.pid;
1819
+ timeoutId = setTimeout(() => {
1820
+ timedOut = true;
1821
+ treeKill2(pid, "SIGTERM", (err) => {
1822
+ if (err) {
1823
+ }
1824
+ });
1825
+ }, command.timeout);
1826
+ }
1503
1827
  proc.stdout.on("data", (data) => {
1504
1828
  output += data.toString();
1505
1829
  });
@@ -1507,35 +1831,41 @@ async function executeCommand(command, cwd, tracker, path) {
1507
1831
  output += data.toString();
1508
1832
  });
1509
1833
  proc.on("close", (code, signal) => {
1834
+ if (timeoutId) {
1835
+ clearTimeout(timeoutId);
1836
+ }
1510
1837
  if (tracker && path) {
1511
1838
  tracker.unregisterProcess(path);
1512
1839
  }
1513
1840
  const durationMs = Date.now() - start;
1514
1841
  const killed = signal === "SIGTERM" || code === 143 || (tracker?.wasKilled(path ?? "") ?? false);
1515
- resolve3({
1842
+ resolve4({
1516
1843
  code: code ?? 1,
1517
1844
  output,
1518
1845
  durationMs,
1519
- killed
1846
+ killed: killed && !timedOut,
1847
+ // Don't mark as killed if it was a timeout
1848
+ timedOut
1520
1849
  });
1521
1850
  });
1522
1851
  proc.on("error", (err) => {
1852
+ if (timeoutId) {
1853
+ clearTimeout(timeoutId);
1854
+ }
1523
1855
  if (tracker && path) {
1524
1856
  tracker.unregisterProcess(path);
1525
1857
  }
1526
1858
  const durationMs = Date.now() - start;
1527
- resolve3({
1859
+ resolve4({
1528
1860
  code: 1,
1529
1861
  output: `Failed to execute command: ${err.message}`,
1530
1862
  durationMs,
1531
- killed: false
1863
+ killed: false,
1864
+ timedOut: false
1532
1865
  });
1533
1866
  });
1534
1867
  });
1535
1868
  }
1536
- function buildPath(parentPath, key) {
1537
- return parentPath ? `${parentPath}:${key}` : key;
1538
- }
1539
1869
  function matchesFilter(path, filters) {
1540
1870
  if (!filters || filters.length === 0) {
1541
1871
  return true;
@@ -1545,7 +1875,7 @@ function matchesFilter(path, filters) {
1545
1875
  });
1546
1876
  }
1547
1877
  function hasMatchingDescendant(node, parentPath, filters) {
1548
- const path = buildPath(parentPath, node.key);
1878
+ const path = buildTaskPath(parentPath, node.key);
1549
1879
  if (matchesFilter(path, filters)) {
1550
1880
  return true;
1551
1881
  }
@@ -1561,11 +1891,13 @@ var VerificationRunner = class {
1561
1891
  options;
1562
1892
  callbacks;
1563
1893
  dependencyTracker;
1564
- constructor(options = {}, registry = defaultRegistry, callbacks = {}) {
1894
+ configEnv;
1895
+ constructor(options = {}, registry = defaultRegistry, callbacks = {}, configEnv = {}) {
1565
1896
  this.options = options;
1566
1897
  this.registry = registry;
1567
1898
  this.callbacks = callbacks;
1568
1899
  this.dependencyTracker = new ReportingDependencyTracker();
1900
+ this.configEnv = configEnv;
1569
1901
  }
1570
1902
  /**
1571
1903
  * Run all verification tasks
@@ -1574,7 +1906,8 @@ var VerificationRunner = class {
1574
1906
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
1575
1907
  const wallStart = Date.now();
1576
1908
  this.dependencyTracker.initialize(tasks);
1577
- const results = await this.runNodes(tasks, "");
1909
+ const baseEnv = mergeEnv({}, this.configEnv);
1910
+ const results = await this.runNodes(tasks, "", "parallel", baseEnv);
1578
1911
  const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
1579
1912
  const durationMs = Date.now() - wallStart;
1580
1913
  const allOk = results.every((r) => r.ok);
@@ -1589,7 +1922,7 @@ var VerificationRunner = class {
1589
1922
  /**
1590
1923
  * Run a list of nodes with the appropriate strategy
1591
1924
  */
1592
- async runNodes(nodes, parentPath, strategy = "parallel") {
1925
+ async runNodes(nodes, parentPath, strategy = "parallel", inheritedEnv = {}) {
1593
1926
  const filteredNodes = nodes.filter(
1594
1927
  (node) => hasMatchingDescendant(node, parentPath, this.options.filter)
1595
1928
  );
@@ -1599,19 +1932,21 @@ var VerificationRunner = class {
1599
1932
  switch (strategy) {
1600
1933
  case "parallel":
1601
1934
  return Promise.all(
1602
- filteredNodes.map((node) => this.runNode(node, parentPath))
1935
+ filteredNodes.map(
1936
+ (node) => this.runNode(node, parentPath, inheritedEnv)
1937
+ )
1603
1938
  );
1604
1939
  case "sequential": {
1605
1940
  const results = [];
1606
1941
  for (const node of filteredNodes) {
1607
- results.push(await this.runNode(node, parentPath));
1942
+ results.push(await this.runNode(node, parentPath, inheritedEnv));
1608
1943
  }
1609
1944
  return results;
1610
1945
  }
1611
1946
  case "fail-fast": {
1612
1947
  const results = [];
1613
1948
  for (const node of filteredNodes) {
1614
- const result = await this.runNode(node, parentPath);
1949
+ const result = await this.runNode(node, parentPath, inheritedEnv);
1615
1950
  results.push(result);
1616
1951
  if (!result.ok) {
1617
1952
  break;
@@ -1624,15 +1959,18 @@ var VerificationRunner = class {
1624
1959
  /**
1625
1960
  * Run a single node (leaf or group)
1626
1961
  */
1627
- async runNode(node, parentPath) {
1628
- const path = buildPath(parentPath, node.key);
1962
+ async runNode(node, parentPath, inheritedEnv = {}) {
1963
+ const path = buildTaskPath(parentPath, node.key);
1964
+ const nodeEnv = mergeEnv(inheritedEnv, node.env);
1965
+ this.dependencyTracker.markActive(path);
1629
1966
  this.callbacks.onTaskStart?.(path, node.key);
1630
1967
  if (node.children && node.children.length > 0) {
1631
1968
  const start = Date.now();
1632
1969
  const childResults = await this.runNodes(
1633
1970
  node.children,
1634
1971
  path,
1635
- node.strategy ?? "parallel"
1972
+ node.strategy ?? "parallel",
1973
+ nodeEnv
1636
1974
  );
1637
1975
  const durationMs2 = Date.now() - start;
1638
1976
  const allOk = childResults.every((r) => r.ok || r.suppressed);
@@ -1671,15 +2009,36 @@ var VerificationRunner = class {
1671
2009
  this.callbacks.onTaskComplete?.(result2);
1672
2010
  return result2;
1673
2011
  }
1674
- const command = normalizeCommand(node.run);
2012
+ const passthrough = this.options.passthrough;
2013
+ const command = normalizeCommand(
2014
+ node.run,
2015
+ node.timeout,
2016
+ passthrough,
2017
+ nodeEnv
2018
+ );
1675
2019
  const cwd = this.options.cwd ?? process.cwd();
1676
- const { code, output, durationMs, killed } = await executeCommand(
2020
+ const { code, output, durationMs, killed, timedOut } = await executeCommand(
1677
2021
  command,
1678
2022
  cwd,
1679
2023
  this.dependencyTracker,
1680
2024
  path
1681
2025
  );
1682
2026
  const ok = code === 0;
2027
+ if (timedOut) {
2028
+ const result2 = {
2029
+ key: node.key,
2030
+ path,
2031
+ ok: false,
2032
+ code,
2033
+ durationMs,
2034
+ output,
2035
+ summaryLine: `${node.key}: timed out after ${command.timeout}ms`,
2036
+ timedOut: true
2037
+ };
2038
+ this.dependencyTracker.recordResult(result2);
2039
+ this.callbacks.onTaskComplete?.(result2);
2040
+ return result2;
2041
+ }
1683
2042
  if (killed) {
1684
2043
  await this.dependencyTracker.waitForDependencies(path);
1685
2044
  const failedDep = this.dependencyTracker.getFailedDependency(path);
@@ -1698,7 +2057,7 @@ var VerificationRunner = class {
1698
2057
  this.callbacks.onTaskComplete?.(result2);
1699
2058
  return result2;
1700
2059
  }
1701
- const cmdString = `${command.cmd} ${command.args.join(" ")}`;
2060
+ const cmdString = command.shell ? command.cmd : `${command.cmd} ${command.args.join(" ")}`;
1702
2061
  const parsed = this.registry.parse(
1703
2062
  output,
1704
2063
  code,
@@ -1743,12 +2102,26 @@ var VerificationRunner = class {
1743
2102
  // src/index.ts
1744
2103
  async function verify(config, cliOptions) {
1745
2104
  const options = mergeOptions(config.options, cliOptions);
2105
+ if (options.filter && options.filter.length > 0) {
2106
+ const resolved = resolveFilters(config.tasks, options.filter);
2107
+ for (const r of resolved) {
2108
+ if (r.wasShortcut) {
2109
+ console.error(`\u2192 Resolving "${r.original}" to "${r.resolved}"`);
2110
+ }
2111
+ }
2112
+ options.filter = resolved.map((r) => r.resolved);
2113
+ }
1746
2114
  const reporter = createReporter(options);
1747
2115
  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
- });
2116
+ const runner = new VerificationRunner(
2117
+ options,
2118
+ void 0,
2119
+ {
2120
+ onTaskStart: (path, key) => reporter.onTaskStart(path, key),
2121
+ onTaskComplete: (result2) => reporter.onTaskComplete(result2)
2122
+ },
2123
+ config.env
2124
+ );
1752
2125
  const result = await runner.run(config.tasks);
1753
2126
  reporter.onFinish?.();
1754
2127
  reporter.outputLogs(result.tasks, options.logs ?? "failed");
@@ -1765,20 +2138,27 @@ async function verifyFromConfig(cwd = process.cwd(), cliOptions) {
1765
2138
  return verify(config, { ...cliOptions, cwd });
1766
2139
  }
1767
2140
  export {
2141
+ AmbiguousTaskError,
2142
+ ConfigError,
1768
2143
  JSONReporter,
1769
2144
  LiveDashboardReporter,
2145
+ PATH_SEPARATOR,
1770
2146
  ParserRegistry,
1771
2147
  QuietReporter,
1772
2148
  SequentialReporter,
1773
2149
  SequentialReporter as TTYReporter,
2150
+ TaskNotFoundError,
1774
2151
  VerificationRunner,
1775
2152
  biomeParser,
2153
+ buildTaskPath,
2154
+ collectPaths,
1776
2155
  createReporter,
1777
2156
  defaultRegistry,
1778
2157
  defineConfig,
1779
2158
  defineTask,
1780
2159
  detectTasks,
1781
2160
  discoverPackages,
2161
+ findBestSuggestion,
1782
2162
  findConfigFile,
1783
2163
  generateConfigContent,
1784
2164
  genericParser,
@@ -1787,10 +2167,14 @@ export {
1787
2167
  loadConfig,
1788
2168
  loadConfigFromCwd,
1789
2169
  mergeOptions,
2170
+ parsers,
2171
+ resolveFilters,
1790
2172
  runInit,
1791
2173
  tscParser,
2174
+ validateConfig,
1792
2175
  verify,
1793
2176
  verifyFromConfig,
1794
- vitestParser
2177
+ vitestParser,
2178
+ walkNodes
1795
2179
  };
1796
2180
  //# sourceMappingURL=index.js.map