@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/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
 
@@ -58,6 +58,16 @@ pnpm exec verify
58
58
  # Run specific task
59
59
  pnpm exec verify format
60
60
 
61
+ # Run nested task with full path
62
+ pnpm exec verify types:tsc
63
+
64
+ # Run nested task with shortcut (if unambiguous)
65
+ pnpm exec verify tsc
66
+ # → Resolving "tsc" to "types:tsc"
67
+
68
+ # Pass arguments to underlying command
69
+ pnpm exec verify logic -- -t "specific test name"
70
+
61
71
  # Run with verbose output
62
72
  pnpm exec verify --verbose
63
73
 
@@ -87,11 +97,16 @@ interface VerificationNode {
87
97
  name?: string;
88
98
 
89
99
  // Command to run (leaf nodes only)
90
- // Supports: string, object with cmd/args/cwd, or [cmd, args] tuple
100
+ // Supports: string, object with cmd/args/cwd/env/timeout
91
101
  run?:
92
102
  | string
93
- | { cmd: string; args: string[]; cwd?: string }
94
- | [string, string[]];
103
+ | {
104
+ cmd: string;
105
+ args: string[];
106
+ cwd?: string;
107
+ env?: Record<string, string | null>;
108
+ timeout?: number;
109
+ };
95
110
 
96
111
  // Child tasks (for grouping)
97
112
  children?: VerificationNode[];
@@ -105,6 +120,13 @@ interface VerificationNode {
105
120
  // Tasks that must pass for this task's failure to be reported
106
121
  reportingDependsOn?: string[];
107
122
 
123
+ // Timeout in milliseconds (for string commands)
124
+ timeout?: number;
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
+
108
130
  // Custom success message template (optional)
109
131
  successLabel?: string;
110
132
 
@@ -222,13 +244,99 @@ Control how child tasks are executed:
222
244
  - `sequential`: Run tasks one after another
223
245
  - `fail-fast`: Run sequentially, stop on first failure
224
246
 
247
+ ### Command Timeouts
248
+
249
+ Prevent hung processes by setting a timeout (in milliseconds):
250
+
251
+ ```typescript
252
+ import { defineConfig } from "@halecraft/verify";
253
+
254
+ export default defineConfig({
255
+ tasks: [
256
+ // String command with timeout
257
+ { key: "test", run: "vitest run", timeout: 60000 }, // 60 seconds
258
+
259
+ // Object command with timeout
260
+ {
261
+ key: "build",
262
+ run: {
263
+ cmd: "tsup",
264
+ args: [],
265
+ timeout: 120000, // 2 minutes
266
+ },
267
+ },
268
+ ],
269
+ });
270
+ ```
271
+
272
+ When a command exceeds its timeout:
273
+
274
+ - The process is killed with `SIGTERM` (including child processes)
275
+ - The task is marked as failed with `timedOut: true`
276
+ - The summary shows: `task: timed out after 60000ms`
277
+
278
+ **Note:** For object commands, the `timeout` on the command takes precedence over the node-level `timeout`.
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
+
225
333
  ## CLI Options
226
334
 
227
335
  ```
228
336
  Usage:
229
- verify [options] [filter...]
337
+ verify [flags...] [task] [--] [passthrough...]
230
338
 
231
- Options:
339
+ Flags:
232
340
  --json Output results as JSON
233
341
  --verbose, -v Show all task output
234
342
  --quiet, -q Show only final result
@@ -236,13 +344,83 @@ Options:
236
344
  --no-tty Force sequential output (disable live dashboard)
237
345
  --logs=MODE Log verbosity: all, failed, none (default: failed)
238
346
  --config, -c PATH Path to config file (or output path for --init)
239
- --filter, -f PATH Filter to specific task paths
240
347
  --init Initialize a new verify.config.ts file
241
348
  --force Overwrite existing config file (with --init)
242
349
  --yes, -y Skip interactive prompts, auto-accept detected tasks
243
350
  --help, -h Show this help message
244
351
  ```
245
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
+
380
+ ### Exit Codes
381
+
382
+ - `0` - All tasks passed
383
+ - `1` - Some tasks failed
384
+ - `2` - Configuration/usage error (invalid task name, missing config)
385
+
386
+ ### Task Name Validation
387
+
388
+ Verify validates task names before running any tasks. If you specify a task that doesn't exist, you'll get a helpful error:
389
+
390
+ ```bash
391
+ $ verify test
392
+ Error: Task "test" not found.
393
+
394
+ Did you mean "types:tsc"?
395
+
396
+ Available tasks:
397
+ format
398
+ logic
399
+ types
400
+ types:tsc
401
+ types:tsgo
402
+ build
403
+ ```
404
+
405
+ **Child task shortcuts:** You can use just the child key if it's unambiguous:
406
+
407
+ ```bash
408
+ $ verify tsc
409
+ → Resolving "tsc" to "types:tsc"
410
+ ✓ verified types:tsc
411
+ ```
412
+
413
+ If the shortcut is ambiguous (multiple tasks have the same child key), you'll get an error listing the options:
414
+
415
+ ```bash
416
+ $ verify test # if both unit:test and e2e:test exist
417
+ Error: Task "test" is ambiguous.
418
+
419
+ Matches multiple tasks:
420
+ unit:test
421
+ e2e:test
422
+ ```
423
+
246
424
  ## Programmatic API
247
425
 
248
426
  ```typescript
@@ -293,6 +471,7 @@ interface TaskResult {
293
471
  summaryLine: string; // Parsed summary
294
472
  suppressed?: boolean; // True if output was suppressed
295
473
  suppressedBy?: string; // Path of dependency that caused suppression
474
+ timedOut?: boolean; // True if task exceeded its timeout
296
475
  children?: TaskResult[]; // Child results (for group nodes)
297
476
  }
298
477
  ```
@@ -301,14 +480,38 @@ interface TaskResult {
301
480
 
302
481
  Built-in parsers for common tools:
303
482
 
304
- - **vitest** - Vitest test runner
305
- - **tsc** - TypeScript compiler
306
- - **biome** - Biome linter/formatter
483
+ - **vitest** - Vitest/Jest test runner
484
+ - **tsc** - TypeScript compiler (tsc/tsgo)
485
+ - **biome** - Biome/ESLint linter/formatter
307
486
  - **gotest** - Go test runner
308
487
  - **generic** - Fallback for unknown tools
309
488
 
310
489
  Parsers automatically extract metrics (passed/failed counts, duration) and provide concise summaries.
311
490
 
491
+ ### Parser ID Constants
492
+
493
+ Use `parsers` for type-safe parser references instead of magic strings:
494
+
495
+ ```typescript
496
+ import { defineConfig, parsers } from "@halecraft/verify";
497
+
498
+ export default defineConfig({
499
+ tasks: [
500
+ { key: "test", run: "vitest run", parser: parsers.vitest },
501
+ { key: "types", run: "tsc --noEmit", parser: parsers.tsc },
502
+ { key: "lint", run: "biome check .", parser: parsers.biome },
503
+ ],
504
+ });
505
+ ```
506
+
507
+ Available constants:
508
+
509
+ - `parsers.vitest` - `"vitest"`
510
+ - `parsers.tsc` - `"tsc"`
511
+ - `parsers.biome` - `"biome"`
512
+ - `parsers.gotest` - `"gotest"`
513
+ - `parsers.generic` - `"generic"`
514
+
312
515
  ## License
313
516
 
314
517
  MIT
package/bin/verify.mjs CHANGED
@@ -1,140 +1,110 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { runInit, verifyFromConfig } from "../dist/index.js"
4
-
5
- /**
6
- * Parse CLI arguments
7
- */
8
- function parseArgs(args) {
9
- const options = {
10
- json: false,
11
- verbose: false,
12
- quiet: false,
13
- logs: undefined,
14
- filter: [],
15
- config: undefined,
16
- help: false,
17
- init: false,
18
- force: false,
19
- yes: false,
20
- topLevelOnly: false,
21
- noTty: false,
22
- }
23
-
24
- for (let i = 0; i < args.length; i++) {
25
- const arg = args[i]
26
-
27
- if (arg === "--json") {
28
- options.json = true
29
- } else if (arg === "--verbose" || arg === "-v") {
30
- options.verbose = true
31
- } else if (arg === "--quiet" || arg === "-q") {
32
- options.quiet = true
33
- } else if (arg === "--help" || arg === "-h") {
34
- options.help = true
35
- } else if (arg === "--init") {
36
- options.init = true
37
- } else if (arg === "--force") {
38
- options.force = true
39
- } else if (arg === "--yes" || arg === "-y") {
40
- options.yes = true
41
- } else if (arg === "--top-level" || arg === "-t") {
42
- options.topLevelOnly = true
43
- } else if (arg === "--no-tty") {
44
- options.noTty = true
45
- } else if (arg.startsWith("--logs=")) {
46
- options.logs = arg.slice(7)
47
- } else if (arg === "--logs") {
48
- options.logs = args[++i]
49
- } else if (arg.startsWith("--config=")) {
50
- options.config = arg.slice(9)
51
- } else if (arg === "--config" || arg === "-c") {
52
- options.config = args[++i]
53
- } else if (arg.startsWith("--filter=")) {
54
- options.filter.push(arg.slice(9))
55
- } else if (arg === "--filter" || arg === "-f") {
56
- options.filter.push(args[++i])
57
- } else if (!arg.startsWith("-")) {
58
- // Positional argument treated as filter
59
- options.filter.push(arg)
60
- }
61
- }
62
-
63
- return options
64
- }
65
-
66
- /**
67
- * Print help message
68
- */
69
- function printHelp() {
70
- console.log(`
71
- @halecraft/verify - Hierarchical verification runner
72
-
73
- Usage:
74
- verify [options] [filter...]
75
-
76
- Options:
77
- --json Output results as JSON
78
- --verbose, -v Show all task output
79
- --quiet, -q Show only final result
80
- --top-level, -t Show only top-level tasks (hide descendants)
81
- --no-tty Force sequential output (disable live dashboard)
82
- --logs=MODE Log verbosity: all, failed, none (default: failed)
83
- --config, -c PATH Path to config file (or output path for --init)
84
- --filter, -f PATH Filter to specific task paths
85
- --init Initialize a new verify.config.ts file
86
- --force Overwrite existing config file (with --init)
87
- --yes, -y Skip interactive prompts, auto-accept detected tasks
88
- --help, -h Show this help message
89
-
90
- Examples:
91
- verify Run all verifications
92
- verify logic Run only 'logic' tasks
93
- verify logic:ts Run only 'logic:ts' task
94
- verify --top-level Show only top-level tasks
95
- verify --json Output JSON for CI
96
- verify --logs=all Show all output
97
- verify --init Create config interactively
98
- verify --init -y Create config with all detected tasks
99
- verify --init --force Overwrite existing config
100
-
101
- Config:
102
- Create a verify.config.ts file in your project root:
103
-
104
- import { defineConfig } from '@halecraft/verify'
3
+ import { cli } from "cleye"
4
+ import {
5
+ AmbiguousTaskError,
6
+ runInit,
7
+ TaskNotFoundError,
8
+ verifyFromConfig,
9
+ } from "../dist/index.js"
10
+
11
+ const argv = cli(
12
+ {
13
+ name: "verify",
14
+ version: "1.0.0",
15
+ description: "Hierarchical verification runner with parallel execution",
16
+
17
+ parameters: [
18
+ "[task]", // Optional single task filter
19
+ "--", // End-of-flags separator
20
+ "[passthrough...]", // Args to pass to underlying command
21
+ ],
105
22
 
106
- export default defineConfig({
107
- tasks: [
108
- { key: 'format', run: 'pnpm verify:format' },
109
- { key: 'types', run: 'pnpm verify:types' },
110
- {
111
- key: 'logic',
112
- children: [
113
- { key: 'ts', run: 'vitest run' },
114
- { key: 'go', run: 'go test ./...' },
115
- ],
23
+ flags: {
24
+ json: {
25
+ type: Boolean,
26
+ description: "Output results as JSON",
27
+ default: false,
116
28
  },
117
- ],
118
- })
119
- `)
120
- }
29
+ verbose: {
30
+ type: Boolean,
31
+ alias: "v",
32
+ description: "Show all task output",
33
+ default: false,
34
+ },
35
+ quiet: {
36
+ type: Boolean,
37
+ alias: "q",
38
+ description: "Show only final result",
39
+ default: false,
40
+ },
41
+ topLevel: {
42
+ type: Boolean,
43
+ alias: "t",
44
+ description: "Show only top-level tasks (hide descendants)",
45
+ default: false,
46
+ },
47
+ noTty: {
48
+ type: Boolean,
49
+ description: "Force sequential output (disable live dashboard)",
50
+ default: false,
51
+ },
52
+ logs: {
53
+ type: String,
54
+ description: "Log verbosity: all, failed, none (default: failed)",
55
+ },
56
+ config: {
57
+ type: String,
58
+ alias: "c",
59
+ description: "Path to config file (or output path for --init)",
60
+ },
61
+ init: {
62
+ type: Boolean,
63
+ description: "Initialize a new verify.config.ts file",
64
+ default: false,
65
+ },
66
+ force: {
67
+ type: Boolean,
68
+ description: "Overwrite existing config file (with --init)",
69
+ default: false,
70
+ },
71
+ yes: {
72
+ type: Boolean,
73
+ alias: "y",
74
+ description: "Skip interactive prompts, auto-accept detected tasks",
75
+ default: false,
76
+ },
77
+ },
78
+
79
+ help: {
80
+ examples: [
81
+ "verify Run all verifications",
82
+ "verify logic Run only 'logic' tasks",
83
+ "verify logic:ts Run only 'logic:ts' task",
84
+ "verify logic -- -t foo Run 'logic' with passthrough args",
85
+ "verify --top-level Show only top-level tasks",
86
+ "verify --json Output JSON for CI",
87
+ "verify --logs=all Show all output",
88
+ "verify --init Create config interactively",
89
+ "verify --init -y Create config with all detected tasks",
90
+ "verify --init --force Overwrite existing config",
91
+ ],
92
+ },
93
+ },
94
+ undefined,
95
+ undefined,
96
+ )
121
97
 
122
98
  async function main() {
123
- const args = process.argv.slice(2)
124
- const options = parseArgs(args)
125
-
126
- if (options.help) {
127
- printHelp()
128
- process.exit(0)
129
- }
99
+ const { flags, _ } = argv
130
100
 
131
101
  // Handle --init command
132
- if (options.init) {
102
+ if (flags.init) {
133
103
  try {
134
104
  const result = await runInit({
135
- config: options.config,
136
- force: options.force,
137
- yes: options.yes,
105
+ config: flags.config,
106
+ force: flags.force,
107
+ yes: flags.yes,
138
108
  cwd: process.cwd(),
139
109
  })
140
110
  process.exit(result.success ? 0 : 1)
@@ -148,22 +118,38 @@ async function main() {
148
118
  }
149
119
  }
150
120
 
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
+ }
131
+
151
132
  // Build verify options
152
133
  const verifyOptions = {
153
- format: options.json ? "json" : "human",
134
+ format: flags.json ? "json" : "human",
154
135
  logs:
155
- options.logs ??
156
- (options.verbose ? "all" : options.quiet ? "none" : "failed"),
157
- filter: options.filter.length > 0 ? options.filter : undefined,
158
- cwd: options.config,
159
- topLevelOnly: options.topLevelOnly,
160
- noTty: options.noTty,
136
+ flags.logs ?? (flags.verbose ? "all" : flags.quiet ? "none" : "failed"),
137
+ filter: task ? [task] : undefined,
138
+ cwd: flags.config,
139
+ topLevelOnly: flags.topLevel,
140
+ noTty: flags.noTty,
141
+ passthrough: passthrough && passthrough.length > 0 ? passthrough : undefined,
161
142
  }
162
143
 
163
144
  try {
164
145
  const result = await verifyFromConfig(process.cwd(), verifyOptions)
165
146
  process.exit(result.ok ? 0 : 1)
166
147
  } catch (error) {
148
+ // Handle task not found / ambiguous task errors with exit code 2
149
+ if (error instanceof TaskNotFoundError || error instanceof AmbiguousTaskError) {
150
+ console.error(`Error: ${error.message}`)
151
+ process.exit(2)
152
+ }
167
153
  if (error instanceof Error) {
168
154
  console.error(`Error: ${error.message}`)
169
155
  } else {