@halecraft/verify 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,14 +1,72 @@
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 VerificationCommandSchema = z.object({
14
+ cmd: z.string(),
15
+ args: z.array(z.string()),
16
+ cwd: z.string().optional(),
17
+ env: z.record(z.string(), z.string()).optional(),
18
+ timeout: z.number().positive().optional()
19
+ });
20
+ var VerificationNodeSchema = z.lazy(
21
+ () => z.object({
22
+ key: z.string().min(1, "Task key cannot be empty").refine((key) => !key.includes(":"), {
23
+ message: "Task key cannot contain ':' (reserved for paths)"
24
+ }),
25
+ name: z.string().optional(),
26
+ run: z.union([z.string(), VerificationCommandSchema]).optional(),
27
+ children: z.array(VerificationNodeSchema).optional(),
28
+ strategy: z.enum(["parallel", "sequential", "fail-fast"]).optional(),
29
+ parser: z.string().optional(),
30
+ successLabel: z.string().optional(),
31
+ failureLabel: z.string().optional(),
32
+ reportingDependsOn: z.array(z.string()).optional(),
33
+ timeout: z.number().positive().optional()
34
+ })
35
+ );
36
+ var VerifyOptionsSchema = z.object({
37
+ logs: z.enum(["all", "failed", "none"]).optional(),
38
+ format: z.enum(["human", "json"]).optional(),
39
+ filter: z.array(z.string()).optional(),
40
+ cwd: z.string().optional(),
41
+ noColor: z.boolean().optional(),
42
+ topLevelOnly: z.boolean().optional(),
43
+ noTty: z.boolean().optional()
44
+ });
45
+ var PackageDiscoveryOptionsSchema = z.object({
46
+ patterns: z.array(z.string()).optional(),
47
+ filter: z.array(z.string()).optional(),
48
+ changed: z.boolean().optional()
49
+ });
50
+ var VerifyConfigSchema = z.object({
51
+ tasks: z.array(VerificationNodeSchema),
52
+ packages: PackageDiscoveryOptionsSchema.optional(),
53
+ options: VerifyOptionsSchema.optional()
54
+ });
55
+ function validateConfig(value, configPath) {
56
+ const result = VerifyConfigSchema.safeParse(value);
57
+ if (!result.success) {
58
+ const errors = result.error.issues.map((issue) => {
59
+ const path = issue.path.join(".");
60
+ return path ? `${path}: ${issue.message}` : issue.message;
61
+ });
62
+ throw new ConfigError(
63
+ `Invalid config:
64
+ - ${errors.join("\n - ")}`,
65
+ configPath
66
+ );
67
+ }
68
+ return result.data;
69
+ }
12
70
  function defineConfig(config) {
13
71
  return config;
14
72
  }
@@ -38,9 +96,9 @@ async function loadConfig(configPath) {
38
96
  const fileUrl = pathToFileURL(absolutePath).href;
39
97
  const module = await import(fileUrl);
40
98
  if (!module.default) {
41
- throw new Error(`Config file ${configPath} must have a default export`);
99
+ throw new ConfigError(`Config file must have a default export`, configPath);
42
100
  }
43
- return module.default;
101
+ return validateConfig(module.default, configPath);
44
102
  }
45
103
  async function loadConfigFromCwd(cwd, configPath) {
46
104
  if (configPath) {
@@ -65,8 +123,26 @@ function mergeOptions(configOptions, cliOptions) {
65
123
  }
66
124
 
67
125
  // src/discovery.ts
68
- import { existsSync as existsSync2, readdirSync, statSync } from "fs";
126
+ import { existsSync as existsSync2, readdirSync, readFileSync, statSync } from "fs";
69
127
  import { join as join2, relative } from "path";
128
+
129
+ // src/schemas/package-json.ts
130
+ import { z as z2 } from "zod";
131
+ var PackageJsonSchema = z2.object({
132
+ name: z2.string().optional(),
133
+ scripts: z2.record(z2.string(), z2.string()).optional()
134
+ }).passthrough();
135
+ function parsePackageJson(content) {
136
+ try {
137
+ const parsed = JSON.parse(content);
138
+ const result = PackageJsonSchema.safeParse(parsed);
139
+ return result.success ? result.data : null;
140
+ } catch {
141
+ return null;
142
+ }
143
+ }
144
+
145
+ // src/discovery.ts
70
146
  var DEFAULT_PATTERNS = ["packages/*", "apps/*"];
71
147
  function findMatchingDirs(rootDir, patterns) {
72
148
  const results = [];
@@ -98,8 +174,9 @@ function getPackageName(packageDir) {
98
174
  return null;
99
175
  }
100
176
  try {
101
- const content = __require(packageJsonPath);
102
- return content.name ?? null;
177
+ const content = readFileSync(packageJsonPath, "utf-8");
178
+ const parsed = parsePackageJson(content);
179
+ return parsed?.name ?? null;
103
180
  } catch {
104
181
  return null;
105
182
  }
@@ -132,8 +209,133 @@ async function hasPackageChanged(_packagePath, _baseBranch = "main") {
132
209
  return true;
133
210
  }
134
211
 
212
+ // src/filter.ts
213
+ import leven from "leven";
214
+
215
+ // src/tree.ts
216
+ var PATH_SEPARATOR = ":";
217
+ function buildTaskPath(parentPath, key) {
218
+ return parentPath ? `${parentPath}${PATH_SEPARATOR}${key}` : key;
219
+ }
220
+ function walkNodes(nodes, visitor, parentPath = "", depth = 0) {
221
+ for (const node of nodes) {
222
+ const path = buildTaskPath(parentPath, node.key);
223
+ visitor(node, path, depth);
224
+ if (node.children) {
225
+ walkNodes(node.children, visitor, path, depth + 1);
226
+ }
227
+ }
228
+ }
229
+ function collectPaths(nodes) {
230
+ const paths = [];
231
+ walkNodes(nodes, (_node, path) => {
232
+ paths.push(path);
233
+ });
234
+ return paths;
235
+ }
236
+
237
+ // src/filter.ts
238
+ var TaskNotFoundError = class extends Error {
239
+ constructor(filter, suggestion, availableTasks) {
240
+ super(buildErrorMessage(filter, suggestion, availableTasks));
241
+ this.filter = filter;
242
+ this.suggestion = suggestion;
243
+ this.availableTasks = availableTasks;
244
+ this.name = "TaskNotFoundError";
245
+ }
246
+ exitCode = 2;
247
+ };
248
+ var AmbiguousTaskError = class extends Error {
249
+ constructor(filter, matches) {
250
+ super(buildAmbiguousErrorMessage(filter, matches));
251
+ this.filter = filter;
252
+ this.matches = matches;
253
+ this.name = "AmbiguousTaskError";
254
+ }
255
+ exitCode = 2;
256
+ };
257
+ function buildErrorMessage(filter, suggestion, availableTasks) {
258
+ let message = `Task "${filter}" not found.`;
259
+ if (suggestion) {
260
+ message += `
261
+
262
+ Did you mean "${suggestion}"?`;
263
+ }
264
+ message += "\n\nAvailable tasks:";
265
+ for (const task of availableTasks) {
266
+ message += `
267
+ ${task}`;
268
+ }
269
+ return message;
270
+ }
271
+ function buildAmbiguousErrorMessage(filter, matches) {
272
+ let message = `Task "${filter}" is ambiguous.`;
273
+ message += "\n\nMatches multiple tasks:";
274
+ for (const match of matches) {
275
+ message += `
276
+ ${match}`;
277
+ }
278
+ return message;
279
+ }
280
+ function findBestSuggestion(availablePaths, invalidFilter) {
281
+ let bestPath;
282
+ let bestDistance = Number.POSITIVE_INFINITY;
283
+ const threshold = Math.max(2, Math.floor(invalidFilter.length / 3));
284
+ for (const path of availablePaths) {
285
+ const distance = leven(invalidFilter, path);
286
+ if (distance < bestDistance && distance <= threshold) {
287
+ bestDistance = distance;
288
+ bestPath = path;
289
+ }
290
+ const lastSegment = path.split(PATH_SEPARATOR).pop();
291
+ if (lastSegment && lastSegment !== path) {
292
+ const segmentDistance = leven(invalidFilter, lastSegment);
293
+ if (segmentDistance < bestDistance && segmentDistance <= threshold) {
294
+ bestDistance = segmentDistance;
295
+ bestPath = path;
296
+ }
297
+ }
298
+ }
299
+ return bestPath;
300
+ }
301
+ function resolveFilter(filter, availablePaths) {
302
+ if (availablePaths.includes(filter)) {
303
+ return { original: filter, resolved: filter, wasShortcut: false };
304
+ }
305
+ const prefixMatches = availablePaths.filter(
306
+ (path) => path === filter || path.startsWith(`${filter}${PATH_SEPARATOR}`)
307
+ );
308
+ if (prefixMatches.length > 0) {
309
+ return { original: filter, resolved: filter, wasShortcut: false };
310
+ }
311
+ const childMatches = availablePaths.filter((path) => {
312
+ const lastSegment = path.split(PATH_SEPARATOR).pop();
313
+ return lastSegment === filter;
314
+ });
315
+ if (childMatches.length === 1) {
316
+ return {
317
+ original: filter,
318
+ resolved: childMatches[0],
319
+ wasShortcut: true
320
+ };
321
+ }
322
+ if (childMatches.length > 1) {
323
+ throw new AmbiguousTaskError(filter, childMatches);
324
+ }
325
+ const suggestion = findBestSuggestion(availablePaths, filter);
326
+ throw new TaskNotFoundError(filter, suggestion, availablePaths);
327
+ }
328
+ function resolveFilters(nodes, filters) {
329
+ const availablePaths = collectPaths(nodes);
330
+ const resolved = [];
331
+ for (const filter of filters) {
332
+ resolved.push(resolveFilter(filter, availablePaths));
333
+ }
334
+ return resolved;
335
+ }
336
+
135
337
  // src/init/detect.ts
136
- import { existsSync as existsSync3, readFileSync } from "fs";
338
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
137
339
  import { join as join3 } from "path";
138
340
  var TOOL_PATTERNS = [
139
341
  // Biome
@@ -292,8 +494,8 @@ function readPackageJson(cwd) {
292
494
  return null;
293
495
  }
294
496
  try {
295
- const content = readFileSync(packageJsonPath, "utf-8");
296
- return JSON.parse(content);
497
+ const content = readFileSync2(packageJsonPath, "utf-8");
498
+ return parsePackageJson(content);
297
499
  } catch {
298
500
  return null;
299
501
  }
@@ -764,6 +966,18 @@ var vitestParser = {
764
966
  };
765
967
 
766
968
  // src/parsers/index.ts
969
+ var parsers = {
970
+ /** Vitest/Jest test runner output parser */
971
+ vitest: "vitest",
972
+ /** TypeScript compiler (tsc/tsgo) output parser */
973
+ tsc: "tsc",
974
+ /** Biome/ESLint linter output parser */
975
+ biome: "biome",
976
+ /** Go test runner output parser */
977
+ gotest: "gotest",
978
+ /** Generic fallback parser */
979
+ generic: "generic"
980
+ };
767
981
  var ParserRegistry = class {
768
982
  parsers = /* @__PURE__ */ new Map();
769
983
  constructor() {
@@ -791,24 +1005,24 @@ var ParserRegistry = class {
791
1005
  detectParser(cmd) {
792
1006
  const cmdLower = cmd.toLowerCase();
793
1007
  if (cmdLower.includes("vitest") || cmdLower.includes("jest")) {
794
- return "vitest";
1008
+ return parsers.vitest;
795
1009
  }
796
1010
  if (cmdLower.includes("tsc") || cmdLower.includes("tsgo")) {
797
- return "tsc";
1011
+ return parsers.tsc;
798
1012
  }
799
1013
  if (cmdLower.includes("biome") || cmdLower.includes("eslint")) {
800
- return "biome";
1014
+ return parsers.biome;
801
1015
  }
802
1016
  if (cmdLower.includes("go test") || cmdLower.includes("go") && cmdLower.includes("test")) {
803
- return "gotest";
1017
+ return parsers.gotest;
804
1018
  }
805
- return "generic";
1019
+ return parsers.generic;
806
1020
  }
807
1021
  /**
808
1022
  * Parse output using the specified or auto-detected parser
809
1023
  */
810
1024
  parse(output, exitCode, parserId, cmd) {
811
- const id = parserId ?? (cmd ? this.detectParser(cmd) : "generic");
1025
+ const id = parserId ?? (cmd ? this.detectParser(cmd) : parsers.generic);
812
1026
  const parser = this.parsers.get(id) ?? genericParser;
813
1027
  const result = parser.parse(output, exitCode);
814
1028
  if (result) {
@@ -940,16 +1154,12 @@ var BaseReporter = class {
940
1154
  return this.taskDepths.get(path) ?? 0;
941
1155
  }
942
1156
  /**
943
- * Recursively collect task depths from verification tree
1157
+ * Collect task depths from verification tree using walkNodes
944
1158
  */
945
- collectTaskDepths(nodes, parentPath, depth) {
946
- for (const node of nodes) {
947
- const path = parentPath ? `${parentPath}:${node.key}` : node.key;
1159
+ collectTaskDepths(nodes) {
1160
+ walkNodes(nodes, (_node, path, depth) => {
948
1161
  this.taskDepths.set(path, depth);
949
- if (node.children) {
950
- this.collectTaskDepths(node.children, path, depth + 1);
951
- }
952
- }
1162
+ });
953
1163
  }
954
1164
  /**
955
1165
  * Extract summary from task result
@@ -1027,16 +1237,15 @@ var LiveDashboardReporter = class extends BaseReporter {
1027
1237
  * Initialize task list from verification nodes
1028
1238
  */
1029
1239
  onStart(tasks) {
1030
- this.collectTasks(tasks, "", 0);
1240
+ this.collectTasks(tasks);
1031
1241
  this.stream.write(cursor.hide);
1032
1242
  this.spinner.start(() => this.redraw());
1033
1243
  }
1034
1244
  /**
1035
- * Recursively collect tasks from verification tree
1245
+ * Collect tasks from verification tree using walkNodes
1036
1246
  */
1037
- collectTasks(nodes, parentPath, depth) {
1038
- for (const node of nodes) {
1039
- const path = parentPath ? `${parentPath}:${node.key}` : node.key;
1247
+ collectTasks(nodes) {
1248
+ walkNodes(nodes, (node, path, depth) => {
1040
1249
  this.tasks.set(path, {
1041
1250
  key: node.key,
1042
1251
  path,
@@ -1045,10 +1254,7 @@ var LiveDashboardReporter = class extends BaseReporter {
1045
1254
  });
1046
1255
  this.taskOrder.push(path);
1047
1256
  this.taskDepths.set(path, depth);
1048
- if (node.children) {
1049
- this.collectTasks(node.children, path, depth + 1);
1050
- }
1051
- }
1257
+ });
1052
1258
  }
1053
1259
  /**
1054
1260
  * Get display key - shows :key for nested, key for root
@@ -1084,9 +1290,9 @@ var LiveDashboardReporter = class extends BaseReporter {
1084
1290
  }
1085
1291
  const summary = this.extractSummary(task.result);
1086
1292
  if (task.result.ok) {
1087
- return ` ${indent}${this.okMark()} verified ${this.c(ansi.bold, displayKey)} ${this.c(ansi.dim, `(${summary}, ${duration})`)}`;
1293
+ return `${indent}${this.okMark()} verified ${this.c(ansi.bold, displayKey)} ${this.c(ansi.dim, `(${summary}, ${duration})`)}`;
1088
1294
  }
1089
- return `${indent}${this.failMark()} failed ${this.c(ansi.bold, displayKey)} ${this.c(ansi.dim, `(${summary}, ${duration})`)}`;
1295
+ return `${indent}${this.failMark()} failed ${this.c(ansi.bold, displayKey)} ${this.c(ansi.dim, `(${summary}, ${duration})`)}`;
1090
1296
  }
1091
1297
  return "";
1092
1298
  }
@@ -1141,7 +1347,7 @@ var SequentialReporter = class extends BaseReporter {
1141
1347
  this.topLevelOnly = options.topLevelOnly ?? false;
1142
1348
  }
1143
1349
  onStart(tasks) {
1144
- this.collectTaskDepths(tasks, "", 0);
1350
+ this.collectTaskDepths(tasks);
1145
1351
  }
1146
1352
  /**
1147
1353
  * Check if task should be displayed based on topLevelOnly flag
@@ -1166,13 +1372,18 @@ var SequentialReporter = class extends BaseReporter {
1166
1372
  );
1167
1373
  return;
1168
1374
  }
1169
- const mark = result.ok ? this.okMark() : this.failMark();
1170
- const verb = result.ok ? "verified" : "failed";
1171
1375
  const summary = this.extractSummary(result);
1172
- this.stream.write(
1173
- `${mark} ${verb} ${this.c(ansi.bold, result.path)} ${this.c(ansi.dim, `(${summary}, ${duration})`)}
1376
+ if (result.ok) {
1377
+ this.stream.write(
1378
+ `${this.okMark()} verified ${this.c(ansi.bold, result.path)} ${this.c(ansi.dim, `(${summary}, ${duration})`)}
1174
1379
  `
1175
- );
1380
+ );
1381
+ } else {
1382
+ this.stream.write(
1383
+ `${this.failMark()} failed ${this.c(ansi.bold, result.path)} ${this.c(ansi.dim, `(${summary}, ${duration})`)}
1384
+ `
1385
+ );
1386
+ }
1176
1387
  }
1177
1388
  onFinish() {
1178
1389
  }
@@ -1242,23 +1453,10 @@ function createReporter(options) {
1242
1453
 
1243
1454
  // src/runner.ts
1244
1455
  import { spawn } from "child_process";
1456
+ import treeKill2 from "tree-kill";
1457
+
1458
+ // src/dependency-tracker.ts
1245
1459
  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
1460
  var ReportingDependencyTracker = class {
1263
1461
  /** Map of task path/key to their results */
1264
1462
  results = /* @__PURE__ */ new Map();
@@ -1276,26 +1474,22 @@ var ReportingDependencyTracker = class {
1276
1474
  processes = /* @__PURE__ */ new Map();
1277
1475
  /** Set of task paths that have been killed */
1278
1476
  killedPaths = /* @__PURE__ */ new Set();
1477
+ /** Set of task paths that will actually run (based on filter) */
1478
+ activePaths = /* @__PURE__ */ new Set();
1279
1479
  /**
1280
1480
  * Initialize the tracker with all tasks from the verification tree.
1281
1481
  * Also validates for circular dependencies and builds reverse dependency map.
1282
1482
  */
1283
- initialize(nodes, parentPath = "") {
1284
- for (const node of nodes) {
1285
- const path = parentPath ? `${parentPath}:${node.key}` : node.key;
1483
+ initialize(nodes) {
1484
+ walkNodes(nodes, (node, path) => {
1286
1485
  this.pathToKey.set(path, node.key);
1287
1486
  this.keyToPath.set(node.key, path);
1288
1487
  if (node.reportingDependsOn && node.reportingDependsOn.length > 0) {
1289
1488
  this.dependencies.set(path, node.reportingDependsOn);
1290
1489
  }
1291
- if (node.children) {
1292
- this.initialize(node.children, path);
1293
- }
1294
- }
1295
- if (parentPath === "") {
1296
- this.validateNoCycles();
1297
- this.buildReverseDeps();
1298
- }
1490
+ });
1491
+ this.validateNoCycles();
1492
+ this.buildReverseDeps();
1299
1493
  }
1300
1494
  /**
1301
1495
  * Build reverse dependency map (task → tasks that depend on it)
@@ -1398,15 +1592,35 @@ var ReportingDependencyTracker = class {
1398
1592
  this.waiters.delete(key);
1399
1593
  }
1400
1594
  }
1595
+ /**
1596
+ * Mark a task as active (will actually run).
1597
+ * Called before running each task to track which tasks are in the execution set.
1598
+ */
1599
+ markActive(path) {
1600
+ this.activePaths.add(path);
1601
+ }
1602
+ /**
1603
+ * Check if a dependency is active (will run).
1604
+ * If not active, we shouldn't wait for it.
1605
+ */
1606
+ isDependencyActive(dep) {
1607
+ const resolvedPath = this.resolveDependency(dep);
1608
+ if (!resolvedPath) {
1609
+ return false;
1610
+ }
1611
+ return this.activePaths.has(resolvedPath);
1612
+ }
1401
1613
  /**
1402
1614
  * Wait for all dependencies of a task to complete.
1615
+ * Only waits for dependencies that are actually active (will run).
1403
1616
  */
1404
1617
  async waitForDependencies(path) {
1405
1618
  const deps = this.dependencies.get(path);
1406
1619
  if (!deps || deps.length === 0) {
1407
1620
  return;
1408
1621
  }
1409
- const waitPromises = deps.map((dep) => this.waitForResult(dep));
1622
+ const activeDeps = deps.filter((dep) => this.isDependencyActive(dep));
1623
+ const waitPromises = activeDeps.map((dep) => this.waitForResult(dep));
1410
1624
  await Promise.all(waitPromises);
1411
1625
  }
1412
1626
  /**
@@ -1488,11 +1702,30 @@ var ReportingDependencyTracker = class {
1488
1702
  }
1489
1703
  }
1490
1704
  };
1705
+
1706
+ // src/runner.ts
1707
+ function normalizeCommand(run, nodeTimeout) {
1708
+ if (typeof run === "string") {
1709
+ return {
1710
+ cmd: run,
1711
+ args: [],
1712
+ shell: true,
1713
+ timeout: nodeTimeout
1714
+ };
1715
+ }
1716
+ return {
1717
+ ...run,
1718
+ shell: false,
1719
+ // Command-level timeout takes precedence over node-level timeout
1720
+ timeout: run.timeout ?? nodeTimeout
1721
+ };
1722
+ }
1491
1723
  async function executeCommand(command, cwd, tracker, path) {
1492
1724
  const start = Date.now();
1493
1725
  return new Promise((resolve3) => {
1726
+ const useShell = command.shell || process.platform === "win32";
1494
1727
  const proc = spawn(command.cmd, command.args, {
1495
- shell: process.platform === "win32",
1728
+ shell: useShell,
1496
1729
  cwd: command.cwd ?? cwd,
1497
1730
  env: { ...process.env, NO_COLOR: "1", ...command.env }
1498
1731
  });
@@ -1500,6 +1733,18 @@ async function executeCommand(command, cwd, tracker, path) {
1500
1733
  tracker.registerProcess(path, proc);
1501
1734
  }
1502
1735
  let output = "";
1736
+ let timedOut = false;
1737
+ let timeoutId;
1738
+ if (command.timeout && proc.pid) {
1739
+ const pid = proc.pid;
1740
+ timeoutId = setTimeout(() => {
1741
+ timedOut = true;
1742
+ treeKill2(pid, "SIGTERM", (err) => {
1743
+ if (err) {
1744
+ }
1745
+ });
1746
+ }, command.timeout);
1747
+ }
1503
1748
  proc.stdout.on("data", (data) => {
1504
1749
  output += data.toString();
1505
1750
  });
@@ -1507,6 +1752,9 @@ async function executeCommand(command, cwd, tracker, path) {
1507
1752
  output += data.toString();
1508
1753
  });
1509
1754
  proc.on("close", (code, signal) => {
1755
+ if (timeoutId) {
1756
+ clearTimeout(timeoutId);
1757
+ }
1510
1758
  if (tracker && path) {
1511
1759
  tracker.unregisterProcess(path);
1512
1760
  }
@@ -1516,10 +1764,15 @@ async function executeCommand(command, cwd, tracker, path) {
1516
1764
  code: code ?? 1,
1517
1765
  output,
1518
1766
  durationMs,
1519
- killed
1767
+ killed: killed && !timedOut,
1768
+ // Don't mark as killed if it was a timeout
1769
+ timedOut
1520
1770
  });
1521
1771
  });
1522
1772
  proc.on("error", (err) => {
1773
+ if (timeoutId) {
1774
+ clearTimeout(timeoutId);
1775
+ }
1523
1776
  if (tracker && path) {
1524
1777
  tracker.unregisterProcess(path);
1525
1778
  }
@@ -1528,14 +1781,12 @@ async function executeCommand(command, cwd, tracker, path) {
1528
1781
  code: 1,
1529
1782
  output: `Failed to execute command: ${err.message}`,
1530
1783
  durationMs,
1531
- killed: false
1784
+ killed: false,
1785
+ timedOut: false
1532
1786
  });
1533
1787
  });
1534
1788
  });
1535
1789
  }
1536
- function buildPath(parentPath, key) {
1537
- return parentPath ? `${parentPath}:${key}` : key;
1538
- }
1539
1790
  function matchesFilter(path, filters) {
1540
1791
  if (!filters || filters.length === 0) {
1541
1792
  return true;
@@ -1545,7 +1796,7 @@ function matchesFilter(path, filters) {
1545
1796
  });
1546
1797
  }
1547
1798
  function hasMatchingDescendant(node, parentPath, filters) {
1548
- const path = buildPath(parentPath, node.key);
1799
+ const path = buildTaskPath(parentPath, node.key);
1549
1800
  if (matchesFilter(path, filters)) {
1550
1801
  return true;
1551
1802
  }
@@ -1625,7 +1876,8 @@ var VerificationRunner = class {
1625
1876
  * Run a single node (leaf or group)
1626
1877
  */
1627
1878
  async runNode(node, parentPath) {
1628
- const path = buildPath(parentPath, node.key);
1879
+ const path = buildTaskPath(parentPath, node.key);
1880
+ this.dependencyTracker.markActive(path);
1629
1881
  this.callbacks.onTaskStart?.(path, node.key);
1630
1882
  if (node.children && node.children.length > 0) {
1631
1883
  const start = Date.now();
@@ -1671,15 +1923,30 @@ var VerificationRunner = class {
1671
1923
  this.callbacks.onTaskComplete?.(result2);
1672
1924
  return result2;
1673
1925
  }
1674
- const command = normalizeCommand(node.run);
1926
+ const command = normalizeCommand(node.run, node.timeout);
1675
1927
  const cwd = this.options.cwd ?? process.cwd();
1676
- const { code, output, durationMs, killed } = await executeCommand(
1928
+ const { code, output, durationMs, killed, timedOut } = await executeCommand(
1677
1929
  command,
1678
1930
  cwd,
1679
1931
  this.dependencyTracker,
1680
1932
  path
1681
1933
  );
1682
1934
  const ok = code === 0;
1935
+ if (timedOut) {
1936
+ const result2 = {
1937
+ key: node.key,
1938
+ path,
1939
+ ok: false,
1940
+ code,
1941
+ durationMs,
1942
+ output,
1943
+ summaryLine: `${node.key}: timed out after ${command.timeout}ms`,
1944
+ timedOut: true
1945
+ };
1946
+ this.dependencyTracker.recordResult(result2);
1947
+ this.callbacks.onTaskComplete?.(result2);
1948
+ return result2;
1949
+ }
1683
1950
  if (killed) {
1684
1951
  await this.dependencyTracker.waitForDependencies(path);
1685
1952
  const failedDep = this.dependencyTracker.getFailedDependency(path);
@@ -1698,7 +1965,7 @@ var VerificationRunner = class {
1698
1965
  this.callbacks.onTaskComplete?.(result2);
1699
1966
  return result2;
1700
1967
  }
1701
- const cmdString = `${command.cmd} ${command.args.join(" ")}`;
1968
+ const cmdString = command.shell ? command.cmd : `${command.cmd} ${command.args.join(" ")}`;
1702
1969
  const parsed = this.registry.parse(
1703
1970
  output,
1704
1971
  code,
@@ -1743,6 +2010,15 @@ var VerificationRunner = class {
1743
2010
  // src/index.ts
1744
2011
  async function verify(config, cliOptions) {
1745
2012
  const options = mergeOptions(config.options, cliOptions);
2013
+ if (options.filter && options.filter.length > 0) {
2014
+ const resolved = resolveFilters(config.tasks, options.filter);
2015
+ for (const r of resolved) {
2016
+ if (r.wasShortcut) {
2017
+ console.error(`\u2192 Resolving "${r.original}" to "${r.resolved}"`);
2018
+ }
2019
+ }
2020
+ options.filter = resolved.map((r) => r.resolved);
2021
+ }
1746
2022
  const reporter = createReporter(options);
1747
2023
  reporter.onStart?.(config.tasks);
1748
2024
  const runner = new VerificationRunner(options, void 0, {
@@ -1765,20 +2041,27 @@ async function verifyFromConfig(cwd = process.cwd(), cliOptions) {
1765
2041
  return verify(config, { ...cliOptions, cwd });
1766
2042
  }
1767
2043
  export {
2044
+ AmbiguousTaskError,
2045
+ ConfigError,
1768
2046
  JSONReporter,
1769
2047
  LiveDashboardReporter,
2048
+ PATH_SEPARATOR,
1770
2049
  ParserRegistry,
1771
2050
  QuietReporter,
1772
2051
  SequentialReporter,
1773
2052
  SequentialReporter as TTYReporter,
2053
+ TaskNotFoundError,
1774
2054
  VerificationRunner,
1775
2055
  biomeParser,
2056
+ buildTaskPath,
2057
+ collectPaths,
1776
2058
  createReporter,
1777
2059
  defaultRegistry,
1778
2060
  defineConfig,
1779
2061
  defineTask,
1780
2062
  detectTasks,
1781
2063
  discoverPackages,
2064
+ findBestSuggestion,
1782
2065
  findConfigFile,
1783
2066
  generateConfigContent,
1784
2067
  genericParser,
@@ -1787,10 +2070,14 @@ export {
1787
2070
  loadConfig,
1788
2071
  loadConfigFromCwd,
1789
2072
  mergeOptions,
2073
+ parsers,
2074
+ resolveFilters,
1790
2075
  runInit,
1791
2076
  tscParser,
2077
+ validateConfig,
1792
2078
  verify,
1793
2079
  verifyFromConfig,
1794
- vitestParser
2080
+ vitestParser,
2081
+ walkNodes
1795
2082
  };
1796
2083
  //# sourceMappingURL=index.js.map