@halecraft/verify 1.1.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/README.md CHANGED
@@ -40,14 +40,14 @@ import { defineConfig } from "@halecraft/verify";
40
40
 
41
41
  export default defineConfig({
42
42
  tasks: [
43
- { key: "format", run: "pnpm lint" },
44
- { key: "types", run: "pnpm typecheck" },
45
- { key: "test", run: "pnpm test" },
43
+ { key: "format", run: "biome check ." },
44
+ { key: "types", run: "tsc --noEmit" },
45
+ { key: "test", run: "vitest run" },
46
46
  ],
47
47
  });
48
48
  ```
49
49
 
50
- Note that you can shave off ~150ms for each command if you skip your package manager (e.g. `./node_modules/.bin/eslint` instead of `pnpm lint`).
50
+ **Note:** Commands automatically have access to binaries in `node_modules/.bin` directories. You can write `run: "biome check ."` instead of `run: "./node_modules/.bin/biome check ."`. This works in monorepos too—verify walks up the directory tree to find all `node_modules/.bin` directories, just like npm/pnpm/yarn do when running package.json scripts.
51
51
 
52
52
  ### Run Verification
53
53
 
@@ -65,6 +65,9 @@ pnpm exec verify types:tsc
65
65
  pnpm exec verify tsc
66
66
  # → Resolving "tsc" to "types:tsc"
67
67
 
68
+ # Pass arguments to underlying command
69
+ pnpm exec verify logic -- -t "specific test name"
70
+
68
71
  # Run with verbose output
69
72
  pnpm exec verify --verbose
70
73
 
@@ -94,10 +97,16 @@ interface VerificationNode {
94
97
  name?: string;
95
98
 
96
99
  // Command to run (leaf nodes only)
97
- // Supports: string, object with cmd/args/cwd/timeout
100
+ // Supports: string, object with cmd/args/cwd/env/timeout
98
101
  run?:
99
102
  | string
100
- | { cmd: string; args: string[]; cwd?: string; timeout?: number };
103
+ | {
104
+ cmd: string;
105
+ args: string[];
106
+ cwd?: string;
107
+ env?: Record<string, string | null>;
108
+ timeout?: number;
109
+ };
101
110
 
102
111
  // Child tasks (for grouping)
103
112
  children?: VerificationNode[];
@@ -114,6 +123,10 @@ interface VerificationNode {
114
123
  // Timeout in milliseconds (for string commands)
115
124
  timeout?: number;
116
125
 
126
+ // Environment variables for this task and its children
127
+ // Set to null to unset an inherited variable
128
+ env?: Record<string, string | null>;
129
+
117
130
  // Custom success message template (optional)
118
131
  successLabel?: string;
119
132
 
@@ -264,13 +277,66 @@ When a command exceeds its timeout:
264
277
 
265
278
  **Note:** For object commands, the `timeout` on the command takes precedence over the node-level `timeout`.
266
279
 
280
+ ### Environment Variables
281
+
282
+ Set environment variables at the config level (applies to all tasks) or at the task level (inherits to children):
283
+
284
+ ```typescript
285
+ import { defineConfig } from "@halecraft/verify";
286
+
287
+ export default defineConfig({
288
+ // Global env vars - applied to all tasks
289
+ env: {
290
+ NO_COLOR: "1", // Recommended: disable colors for cleaner output parsing
291
+ CI: "true",
292
+ },
293
+ tasks: [
294
+ { key: "format", run: "biome check ." },
295
+ {
296
+ key: "test",
297
+ // Enable colors for test output by unsetting NO_COLOR
298
+ env: { NO_COLOR: null },
299
+ children: [
300
+ { key: "unit", run: "vitest run" },
301
+ {
302
+ key: "e2e",
303
+ run: "playwright test",
304
+ // E2E-specific env (still inherits CI: "true")
305
+ env: { PLAYWRIGHT_BROWSERS_PATH: "0" },
306
+ },
307
+ ],
308
+ },
309
+ ],
310
+ });
311
+ ```
312
+
313
+ **Environment merge order** (most specific wins):
314
+
315
+ 1. `process.env` - System environment
316
+ 2. `config.env` - Global config-level
317
+ 3. Parent task `env` - Inherited from parent tasks
318
+ 4. Node `env` - Current task
319
+ 5. Command `env` - VerificationCommand object only
320
+
321
+ **Unsetting variables:** Set a value to `null` to explicitly unset an inherited variable:
322
+
323
+ ```typescript
324
+ {
325
+ key: "test",
326
+ env: { NO_COLOR: null }, // Re-enables colors for this task
327
+ run: "vitest run",
328
+ }
329
+ ```
330
+
331
+ **Note:** Generated configs (via `--init`) include `env: { NO_COLOR: "1" }` by default to ensure consistent output parsing.
332
+
267
333
  ## CLI Options
268
334
 
269
335
  ```
270
336
  Usage:
271
- verify [options] [filter...]
337
+ verify [flags...] [task] [--] [passthrough...]
272
338
 
273
- Options:
339
+ Flags:
274
340
  --json Output results as JSON
275
341
  --verbose, -v Show all task output
276
342
  --quiet, -q Show only final result
@@ -278,13 +344,39 @@ Options:
278
344
  --no-tty Force sequential output (disable live dashboard)
279
345
  --logs=MODE Log verbosity: all, failed, none (default: failed)
280
346
  --config, -c PATH Path to config file (or output path for --init)
281
- --filter, -f PATH Filter to specific task paths
282
347
  --init Initialize a new verify.config.ts file
283
348
  --force Overwrite existing config file (with --init)
284
349
  --yes, -y Skip interactive prompts, auto-accept detected tasks
285
350
  --help, -h Show this help message
286
351
  ```
287
352
 
353
+ ### Passthrough Arguments
354
+
355
+ You can pass arguments directly to the underlying command using `--` (double-dash):
356
+
357
+ ```bash
358
+ # Run a specific vitest test
359
+ verify logic -- -t "should handle edge case"
360
+
361
+ # Run with coverage
362
+ verify logic -- --coverage
363
+
364
+ # Multiple passthrough args
365
+ verify logic -- -t "foo" --reporter=verbose
366
+
367
+ # Combine with verify flags
368
+ verify logic --verbose -- -t "foo"
369
+ ```
370
+
371
+ **Requirements:**
372
+ - Passthrough arguments require exactly one task filter
373
+ - The task must be a leaf node (has a `run` command)
374
+ - Arguments are appended to the command string
375
+
376
+ **How it works:**
377
+ - For string commands: args are shell-escaped and appended to the command
378
+ - For object commands: args are appended to the `args` array
379
+
288
380
  ### Exit Codes
289
381
 
290
382
  - `0` - All tasks passed
package/bin/verify.mjs CHANGED
@@ -14,6 +14,12 @@ const argv = cli(
14
14
  version: "1.0.0",
15
15
  description: "Hierarchical verification runner with parallel execution",
16
16
 
17
+ parameters: [
18
+ "[task]", // Optional single task filter
19
+ "--", // End-of-flags separator
20
+ "[passthrough...]", // Args to pass to underlying command
21
+ ],
22
+
17
23
  flags: {
18
24
  json: {
19
25
  type: Boolean,
@@ -52,12 +58,6 @@ const argv = cli(
52
58
  alias: "c",
53
59
  description: "Path to config file (or output path for --init)",
54
60
  },
55
- filter: {
56
- type: [String],
57
- alias: "f",
58
- description: "Filter to specific task paths",
59
- default: [],
60
- },
61
61
  init: {
62
62
  type: Boolean,
63
63
  description: "Initialize a new verify.config.ts file",
@@ -81,6 +81,7 @@ const argv = cli(
81
81
  "verify Run all verifications",
82
82
  "verify logic Run only 'logic' tasks",
83
83
  "verify logic:ts Run only 'logic:ts' task",
84
+ "verify logic -- -t foo Run 'logic' with passthrough args",
84
85
  "verify --top-level Show only top-level tasks",
85
86
  "verify --json Output JSON for CI",
86
87
  "verify --logs=all Show all output",
@@ -117,18 +118,27 @@ async function main() {
117
118
  }
118
119
  }
119
120
 
120
- // Combine --filter flags with positional arguments
121
- const allFilters = [...flags.filter, ..._]
121
+ // Get task filter from named parameter
122
+ const task = _.task
123
+ const passthrough = _.passthrough
124
+
125
+ // Validate: passthrough requires a single task filter
126
+ if (passthrough && passthrough.length > 0 && !task) {
127
+ console.error("Error: Passthrough arguments (after --) require a task filter")
128
+ console.error("Usage: verify <task> -- [args...]")
129
+ process.exit(2)
130
+ }
122
131
 
123
132
  // Build verify options
124
133
  const verifyOptions = {
125
134
  format: flags.json ? "json" : "human",
126
135
  logs:
127
136
  flags.logs ?? (flags.verbose ? "all" : flags.quiet ? "none" : "failed"),
128
- filter: allFilters.length > 0 ? allFilters : undefined,
137
+ filter: task ? [task] : undefined,
129
138
  cwd: flags.config,
130
139
  topLevelOnly: flags.topLevel,
131
140
  noTty: flags.noTty,
141
+ passthrough: passthrough && passthrough.length > 0 ? passthrough : undefined,
132
142
  }
133
143
 
134
144
  try {
package/dist/index.d.ts CHANGED
@@ -8,8 +8,11 @@ interface VerificationCommand {
8
8
  args: string[];
9
9
  /** Working directory (defaults to cwd) */
10
10
  cwd?: string;
11
- /** Environment variables to set */
12
- env?: Record<string, string>;
11
+ /**
12
+ * Environment variables to set.
13
+ * Set to null to explicitly unset an inherited variable.
14
+ */
15
+ env?: Record<string, string | null>;
13
16
  /** Timeout in milliseconds (process killed with SIGTERM if exceeded) */
14
17
  timeout?: number;
15
18
  }
@@ -74,6 +77,14 @@ interface VerificationNode {
74
77
  * For VerificationCommand objects, use the timeout field on the command itself.
75
78
  */
76
79
  timeout?: number;
80
+ /**
81
+ * Environment variables for this task and its children.
82
+ * Inherits from parent tasks and config-level env.
83
+ * Child tasks can override parent values.
84
+ * Set to null to explicitly unset an inherited variable.
85
+ * For VerificationCommand objects, command-level env takes precedence.
86
+ */
87
+ env?: Record<string, string | null>;
77
88
  }
78
89
  /**
79
90
  * Options for the verification runner
@@ -93,6 +104,8 @@ interface VerifyOptions {
93
104
  topLevelOnly?: boolean;
94
105
  /** Force sequential output (disable live dashboard) */
95
106
  noTty?: boolean;
107
+ /** Arguments to pass through to the underlying command (requires single task filter) */
108
+ passthrough?: string[];
96
109
  }
97
110
  /**
98
111
  * Package discovery options for monorepos
@@ -115,6 +128,12 @@ interface VerifyConfig {
115
128
  packages?: PackageDiscoveryOptions;
116
129
  /** Default options */
117
130
  options?: VerifyOptions;
131
+ /**
132
+ * Global environment variables applied to all tasks.
133
+ * Set to null to explicitly unset an inherited variable.
134
+ * Recommend setting NO_COLOR: "1" here to disable colors in output.
135
+ */
136
+ env?: Record<string, string | null>;
118
137
  }
119
138
  /**
120
139
  * Result of a single verification task
@@ -304,7 +323,7 @@ interface DetectedTask {
304
323
  }
305
324
  /**
306
325
  * Detect tasks with proper package manager commands
307
- * Uses optimized direct paths when possible, falls back to package manager
326
+ * Uses optimized direct binary names when possible, falls back to package manager
308
327
  */
309
328
  declare function detectTasks(cwd: string): DetectedTask[];
310
329
 
@@ -605,7 +624,8 @@ declare class VerificationRunner {
605
624
  private options;
606
625
  private callbacks;
607
626
  private dependencyTracker;
608
- constructor(options?: VerifyOptions, registry?: ParserRegistry, callbacks?: RunnerCallbacks);
627
+ private configEnv;
628
+ constructor(options?: VerifyOptions, registry?: ParserRegistry, callbacks?: RunnerCallbacks, configEnv?: Record<string, string | null>);
609
629
  /**
610
630
  * Run all verification tasks
611
631
  */
package/dist/index.js CHANGED
@@ -10,11 +10,12 @@ var ConfigError = class extends Error {
10
10
  this.name = "ConfigError";
11
11
  }
12
12
  };
13
+ var EnvSchema = z.record(z.string(), z.string().nullable()).optional();
13
14
  var VerificationCommandSchema = z.object({
14
15
  cmd: z.string(),
15
16
  args: z.array(z.string()),
16
17
  cwd: z.string().optional(),
17
- env: z.record(z.string(), z.string()).optional(),
18
+ env: EnvSchema,
18
19
  timeout: z.number().positive().optional()
19
20
  });
20
21
  var VerificationNodeSchema = z.lazy(
@@ -30,7 +31,8 @@ var VerificationNodeSchema = z.lazy(
30
31
  successLabel: z.string().optional(),
31
32
  failureLabel: z.string().optional(),
32
33
  reportingDependsOn: z.array(z.string()).optional(),
33
- timeout: z.number().positive().optional()
34
+ timeout: z.number().positive().optional(),
35
+ env: EnvSchema
34
36
  })
35
37
  );
36
38
  var VerifyOptionsSchema = z.object({
@@ -40,7 +42,8 @@ var VerifyOptionsSchema = z.object({
40
42
  cwd: z.string().optional(),
41
43
  noColor: z.boolean().optional(),
42
44
  topLevelOnly: z.boolean().optional(),
43
- noTty: z.boolean().optional()
45
+ noTty: z.boolean().optional(),
46
+ passthrough: z.array(z.string()).optional()
44
47
  });
45
48
  var PackageDiscoveryOptionsSchema = z.object({
46
49
  patterns: z.array(z.string()).optional(),
@@ -50,7 +53,8 @@ var PackageDiscoveryOptionsSchema = z.object({
50
53
  var VerifyConfigSchema = z.object({
51
54
  tasks: z.array(VerificationNodeSchema),
52
55
  packages: PackageDiscoveryOptionsSchema.optional(),
53
- options: VerifyOptionsSchema.optional()
56
+ options: VerifyOptionsSchema.optional(),
57
+ env: EnvSchema
54
58
  });
55
59
  function validateConfig(value, configPath) {
56
60
  const result = VerifyConfigSchema.safeParse(value);
@@ -118,7 +122,8 @@ function mergeOptions(configOptions, cliOptions) {
118
122
  cwd: cliOptions?.cwd ?? configOptions?.cwd ?? process.cwd(),
119
123
  noColor: cliOptions?.noColor ?? configOptions?.noColor ?? false,
120
124
  topLevelOnly: cliOptions?.topLevelOnly ?? configOptions?.topLevelOnly ?? false,
121
- noTty: cliOptions?.noTty ?? configOptions?.noTty ?? false
125
+ noTty: cliOptions?.noTty ?? configOptions?.noTty ?? false,
126
+ passthrough: cliOptions?.passthrough ?? configOptions?.passthrough
122
127
  };
123
128
  }
124
129
 
@@ -508,7 +513,7 @@ function extractOptimizedCommand(cwd, scriptContent) {
508
513
  const match = scriptContent.match(tool.pattern);
509
514
  if (match && binaryExists(cwd, tool.binary)) {
510
515
  const args = tool.getArgs(match, scriptContent);
511
- const command = args ? `./node_modules/.bin/${tool.binary} ${args}` : `./node_modules/.bin/${tool.binary}`;
516
+ const command = args ? `${tool.binary} ${args}` : tool.binary;
512
517
  return { command, parser: tool.parser };
513
518
  }
514
519
  }
@@ -585,7 +590,7 @@ function detectTasks(cwd) {
585
590
  const packageManager = detectPackageManager(cwd);
586
591
  const tasks = detectFromPackageJson(cwd);
587
592
  return tasks.map((task) => {
588
- if (task.command.startsWith("./")) {
593
+ if (!task.command.startsWith("npm run ")) {
589
594
  return task;
590
595
  }
591
596
  return {
@@ -621,12 +626,15 @@ function generateSkeleton(format) {
621
626
  return `${importStatement}
622
627
 
623
628
  export default defineConfig({
629
+ env: {
630
+ NO_COLOR: "1",
631
+ },
624
632
  tasks: [
625
633
  // Add your verification tasks here
626
634
  // Example:
627
- // { key: "format", run: "pnpm lint" },
628
- // { key: "types", run: "pnpm typecheck" },
629
- // { key: "test", run: "pnpm test" },
635
+ // { key: "format", run: "biome check ." },
636
+ // { key: "types", run: "tsc --noEmit" },
637
+ // { key: "test", run: "vitest run" },
630
638
  ],
631
639
  })
632
640
  `;
@@ -641,6 +649,9 @@ function generateConfigContent(tasks, format) {
641
649
  return `${importStatement}
642
650
 
643
651
  export default defineConfig({
652
+ env: {
653
+ NO_COLOR: "1",
654
+ },
644
655
  tasks: [
645
656
  ${taskLines.join(",\n")},
646
657
  ],
@@ -1453,6 +1464,7 @@ function createReporter(options) {
1453
1464
 
1454
1465
  // src/runner.ts
1455
1466
  import { spawn } from "child_process";
1467
+ import { dirname, join as join4, resolve as resolve3 } from "path";
1456
1468
  import treeKill2 from "tree-kill";
1457
1469
 
1458
1470
  // src/dependency-tracker.ts
@@ -1634,9 +1646,9 @@ var ReportingDependencyTracker = class {
1634
1646
  if (this.results.has(pathOrKey)) {
1635
1647
  return Promise.resolve();
1636
1648
  }
1637
- return new Promise((resolve3) => {
1649
+ return new Promise((resolve4) => {
1638
1650
  const waiters = this.waiters.get(pathOrKey) ?? [];
1639
- waiters.push(resolve3);
1651
+ waiters.push(resolve4);
1640
1652
  this.waiters.set(pathOrKey, waiters);
1641
1653
  });
1642
1654
  }
@@ -1704,17 +1716,78 @@ var ReportingDependencyTracker = class {
1704
1716
  };
1705
1717
 
1706
1718
  // src/runner.ts
1707
- function normalizeCommand(run, nodeTimeout) {
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) {
1708
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
+ }
1709
1776
  return {
1710
- cmd: run,
1777
+ cmd,
1711
1778
  args: [],
1712
1779
  shell: true,
1713
- timeout: nodeTimeout
1780
+ timeout: nodeTimeout,
1781
+ env: inheritedEnv
1714
1782
  };
1715
1783
  }
1784
+ const args = passthrough ? [...run.args, ...passthrough] : run.args;
1785
+ const env = mergeEnv(inheritedEnv ?? {}, run.env);
1716
1786
  return {
1717
- ...run,
1787
+ cmd: run.cmd,
1788
+ args,
1789
+ cwd: run.cwd,
1790
+ env: Object.keys(env).length > 0 ? env : void 0,
1718
1791
  shell: false,
1719
1792
  // Command-level timeout takes precedence over node-level timeout
1720
1793
  timeout: run.timeout ?? nodeTimeout
@@ -1722,12 +1795,18 @@ function normalizeCommand(run, nodeTimeout) {
1722
1795
  }
1723
1796
  async function executeCommand(command, cwd, tracker, path) {
1724
1797
  const start = Date.now();
1725
- return new Promise((resolve3) => {
1798
+ return new Promise((resolve4) => {
1726
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
+ );
1727
1806
  const proc = spawn(command.cmd, command.args, {
1728
1807
  shell: useShell,
1729
- cwd: command.cwd ?? cwd,
1730
- env: { ...process.env, NO_COLOR: "1", ...command.env }
1808
+ cwd: effectiveCwd,
1809
+ env: finalEnv
1731
1810
  });
1732
1811
  if (tracker && path) {
1733
1812
  tracker.registerProcess(path, proc);
@@ -1760,7 +1839,7 @@ async function executeCommand(command, cwd, tracker, path) {
1760
1839
  }
1761
1840
  const durationMs = Date.now() - start;
1762
1841
  const killed = signal === "SIGTERM" || code === 143 || (tracker?.wasKilled(path ?? "") ?? false);
1763
- resolve3({
1842
+ resolve4({
1764
1843
  code: code ?? 1,
1765
1844
  output,
1766
1845
  durationMs,
@@ -1777,7 +1856,7 @@ async function executeCommand(command, cwd, tracker, path) {
1777
1856
  tracker.unregisterProcess(path);
1778
1857
  }
1779
1858
  const durationMs = Date.now() - start;
1780
- resolve3({
1859
+ resolve4({
1781
1860
  code: 1,
1782
1861
  output: `Failed to execute command: ${err.message}`,
1783
1862
  durationMs,
@@ -1812,11 +1891,13 @@ var VerificationRunner = class {
1812
1891
  options;
1813
1892
  callbacks;
1814
1893
  dependencyTracker;
1815
- constructor(options = {}, registry = defaultRegistry, callbacks = {}) {
1894
+ configEnv;
1895
+ constructor(options = {}, registry = defaultRegistry, callbacks = {}, configEnv = {}) {
1816
1896
  this.options = options;
1817
1897
  this.registry = registry;
1818
1898
  this.callbacks = callbacks;
1819
1899
  this.dependencyTracker = new ReportingDependencyTracker();
1900
+ this.configEnv = configEnv;
1820
1901
  }
1821
1902
  /**
1822
1903
  * Run all verification tasks
@@ -1825,7 +1906,8 @@ var VerificationRunner = class {
1825
1906
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
1826
1907
  const wallStart = Date.now();
1827
1908
  this.dependencyTracker.initialize(tasks);
1828
- const results = await this.runNodes(tasks, "");
1909
+ const baseEnv = mergeEnv({}, this.configEnv);
1910
+ const results = await this.runNodes(tasks, "", "parallel", baseEnv);
1829
1911
  const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
1830
1912
  const durationMs = Date.now() - wallStart;
1831
1913
  const allOk = results.every((r) => r.ok);
@@ -1840,7 +1922,7 @@ var VerificationRunner = class {
1840
1922
  /**
1841
1923
  * Run a list of nodes with the appropriate strategy
1842
1924
  */
1843
- async runNodes(nodes, parentPath, strategy = "parallel") {
1925
+ async runNodes(nodes, parentPath, strategy = "parallel", inheritedEnv = {}) {
1844
1926
  const filteredNodes = nodes.filter(
1845
1927
  (node) => hasMatchingDescendant(node, parentPath, this.options.filter)
1846
1928
  );
@@ -1850,19 +1932,21 @@ var VerificationRunner = class {
1850
1932
  switch (strategy) {
1851
1933
  case "parallel":
1852
1934
  return Promise.all(
1853
- filteredNodes.map((node) => this.runNode(node, parentPath))
1935
+ filteredNodes.map(
1936
+ (node) => this.runNode(node, parentPath, inheritedEnv)
1937
+ )
1854
1938
  );
1855
1939
  case "sequential": {
1856
1940
  const results = [];
1857
1941
  for (const node of filteredNodes) {
1858
- results.push(await this.runNode(node, parentPath));
1942
+ results.push(await this.runNode(node, parentPath, inheritedEnv));
1859
1943
  }
1860
1944
  return results;
1861
1945
  }
1862
1946
  case "fail-fast": {
1863
1947
  const results = [];
1864
1948
  for (const node of filteredNodes) {
1865
- const result = await this.runNode(node, parentPath);
1949
+ const result = await this.runNode(node, parentPath, inheritedEnv);
1866
1950
  results.push(result);
1867
1951
  if (!result.ok) {
1868
1952
  break;
@@ -1875,8 +1959,9 @@ var VerificationRunner = class {
1875
1959
  /**
1876
1960
  * Run a single node (leaf or group)
1877
1961
  */
1878
- async runNode(node, parentPath) {
1962
+ async runNode(node, parentPath, inheritedEnv = {}) {
1879
1963
  const path = buildTaskPath(parentPath, node.key);
1964
+ const nodeEnv = mergeEnv(inheritedEnv, node.env);
1880
1965
  this.dependencyTracker.markActive(path);
1881
1966
  this.callbacks.onTaskStart?.(path, node.key);
1882
1967
  if (node.children && node.children.length > 0) {
@@ -1884,7 +1969,8 @@ var VerificationRunner = class {
1884
1969
  const childResults = await this.runNodes(
1885
1970
  node.children,
1886
1971
  path,
1887
- node.strategy ?? "parallel"
1972
+ node.strategy ?? "parallel",
1973
+ nodeEnv
1888
1974
  );
1889
1975
  const durationMs2 = Date.now() - start;
1890
1976
  const allOk = childResults.every((r) => r.ok || r.suppressed);
@@ -1923,7 +2009,13 @@ var VerificationRunner = class {
1923
2009
  this.callbacks.onTaskComplete?.(result2);
1924
2010
  return result2;
1925
2011
  }
1926
- const command = normalizeCommand(node.run, node.timeout);
2012
+ const passthrough = this.options.passthrough;
2013
+ const command = normalizeCommand(
2014
+ node.run,
2015
+ node.timeout,
2016
+ passthrough,
2017
+ nodeEnv
2018
+ );
1927
2019
  const cwd = this.options.cwd ?? process.cwd();
1928
2020
  const { code, output, durationMs, killed, timedOut } = await executeCommand(
1929
2021
  command,
@@ -2021,10 +2113,15 @@ async function verify(config, cliOptions) {
2021
2113
  }
2022
2114
  const reporter = createReporter(options);
2023
2115
  reporter.onStart?.(config.tasks);
2024
- const runner = new VerificationRunner(options, void 0, {
2025
- onTaskStart: (path, key) => reporter.onTaskStart(path, key),
2026
- onTaskComplete: (result2) => reporter.onTaskComplete(result2)
2027
- });
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
+ );
2028
2125
  const result = await runner.run(config.tasks);
2029
2126
  reporter.onFinish?.();
2030
2127
  reporter.outputLogs(result.tasks, options.logs ?? "failed");