tryscript 0.1.1 → 0.1.3

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
@@ -1,61 +1,26 @@
1
1
  # tryscript
2
2
 
3
- [![CI](https://github.com/jlevy/tryscript/actions/workflows/ci.yml/badge.svg)](https://github.com/jlevy/tryscript/actions/workflows/ci.yml)
4
- [![Coverage](https://raw.githubusercontent.com/jlevy/tryscript/main/badges/packages/tryscript/coverage-total.svg)](https://raw.githubusercontent.com/jlevy/tryscript/main/badges/packages/tryscript/coverage-total.svg)
3
+ [![CI](https://github.com/jlevy/tryscript/actions/workflows/ci.yml/badge.svg)](https://github.com/jlevy/tryscript/actions/runs/20774584634)
4
+ [![Coverage](https://raw.githubusercontent.com/jlevy/tryscript/main/badges/packages/tryscript/coverage-total.svg)](https://github.com/jlevy/tryscript/actions/runs/20774584634)
5
5
  [![npm version](https://img.shields.io/npm/v/tryscript)](https://www.npmjs.com/package/tryscript)
6
6
  [![X Follow](https://img.shields.io/twitter/follow/ojoshe)](https://x.com/ojoshe)
7
7
 
8
8
  Golden testing for CLI applications - a TypeScript port of [trycmd](https://github.com/assert-rs/trycmd).
9
9
 
10
+ > [!NOTE]
11
+ > 100% of the code and specs in this repository were written by Claude Code.
12
+ > The design and management and prompting was by me ([jlevy](https://github.com/jlevy)) supported by the workflows, agent rules,
13
+ > and other research docs in [Speculate](https://github.com/jlevy/speculate).
14
+ >
15
+ > You can see what you think, but I find the code quality higher than most agent-written code I've
16
+ > seen because of the spec-driven process.
17
+ > You can review the architecture doc and all of the specs all of the specs in [docs/project](docs/project).
18
+ > The general research, guideline, and rules docs I use are in [docs/general](docs/general).
19
+
10
20
  ## What It Does
11
21
 
12
22
  Write CLI tests as Markdown. tryscript runs commands, captures output, and compares against expected results. Tests become documentation; documentation becomes tests.
13
23
 
14
- ````markdown
15
- # Test: Basic echo
16
-
17
- ```console
18
- $ echo "hello world"
19
- hello world
20
- ? 0
21
- ```
22
-
23
- # Test: Grep with pattern matching
24
-
25
- ```console
26
- $ ls -la | grep ".md"
27
- [..]README.md
28
- ...
29
- ? 0
30
- ```
31
- ````
32
-
33
- The `[..]` matches any text on that line. The `...` matches zero or more lines. These "elision patterns" let tests handle dynamic output gracefully.
34
-
35
- ## Quick Start
36
-
37
- ```bash
38
- # Install
39
- pnpm add -D tryscript
40
-
41
- # Run tests
42
- npx tryscript run tests/
43
-
44
- # Update expected output when behavior changes
45
- npx tryscript run --update tests/
46
- ```
47
-
48
- ## Features
49
-
50
- - **Markdown format** - Tests are readable documentation
51
- - **Elision patterns** - Handle variable output: `[..]`, `...`, `[CWD]`, `[ROOT]`, `[EXE]`
52
- - **Custom patterns** - Define regex patterns for timestamps, versions, UUIDs
53
- - **Update mode** - Regenerate expected output with `--update`
54
- - **Sandbox mode** - Isolate tests in temp directories
55
- - **Code coverage** - Track coverage from subprocess execution with `--coverage`
56
-
57
- ## Example Test File
58
-
59
24
  ````markdown
60
25
  ---
61
26
  env:
@@ -91,15 +56,54 @@ $ my-cli unknown-command 2>&1
91
56
  Error: unknown command 'unknown-command'
92
57
  ? 1
93
58
  ```
59
+
60
+ # Test: Check output file contents
61
+
62
+ ```console
63
+ $ my-cli process data.json > output.txt && grep "success" output.txt
64
+ [..]success[..]
65
+ ? 0
66
+ ```
94
67
  ````
95
68
 
69
+ The `[..]` matches any text on that line. The `...` matches zero or more lines. These "elision patterns" let tests handle dynamic output gracefully. Any shell command works - pipes, redirects, environment variables, etc.
70
+
71
+ ## Quick Start
72
+
73
+ ```bash
74
+ # Install
75
+ pnpm add -D tryscript
76
+
77
+ # For coverage support (optional)
78
+ pnpm add -D c8
79
+
80
+ # For accurate line counts when merging with vitest (optional)
81
+ pnpm add -D c8 monocart-coverage-reports
82
+
83
+ # Run tests
84
+ npx tryscript run tests/
85
+
86
+ # Update expected output when behavior changes
87
+ npx tryscript run --update tests/
88
+ ```
89
+
90
+ ## Features
91
+
92
+ - **Markdown format** - Tests are readable documentation
93
+ - **Elision patterns** - Handle variable output: `[..]`, `...`, `[CWD]`, `[ROOT]`, `[EXE]`
94
+ - **Custom patterns** - Define regex patterns for timestamps, versions, UUIDs
95
+ - **Update mode** - Regenerate expected output with `--update`
96
+ - **Sandbox mode** - Isolate tests in temp directories
97
+ - **Code coverage** - Track coverage from subprocess execution with `--coverage` (experimental; use `--coverage-monocart` for best accuracy)
98
+
96
99
  ## CLI Reference
97
100
 
98
101
  ```bash
99
- tryscript run [files...] # Run golden tests
100
- tryscript docs # Show syntax quick reference
101
- tryscript readme # Show this documentation
102
- tryscript --help # Show all options
102
+ tryscript run [files...] # Run golden tests
103
+ tryscript coverage <commands...> # Run commands with merged coverage
104
+ tryscript docs # Show syntax quick reference
105
+ tryscript readme # Show this documentation
106
+ tryscript --help # Show all options
103
107
  ```
104
108
 
105
109
  For complete syntax reference, run `tryscript docs` or see the [reference documentation](https://github.com/jlevy/tryscript/blob/main/docs/tryscript-reference.md).
@@ -113,6 +117,11 @@ For complete syntax reference, run `tryscript docs` or see the [reference docume
113
117
  | `--filter <regex>` | Filter tests by name |
114
118
  | `--verbose` | Show detailed output |
115
119
  | `--coverage` | Collect code coverage (requires c8) |
120
+ | `--coverage-monocart` | Use monocart for accurate line counts (requires monocart-coverage-reports) |
121
+ | `--coverage-exclude-node-modules` | Exclude node_modules from coverage (default: true) |
122
+ | `--coverage-exclude <pattern>` | Exclude patterns from coverage |
123
+
124
+ > **Note**: Coverage features are experimental. See the [reference documentation](packages/tryscript/docs/tryscript-reference.md#code-coverage) for details on merged coverage, monocart integration, and sourcemap requirements.
116
125
 
117
126
  ## Development
118
127
 
package/dist/bin.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
 
4
- const require_src = require('./src-CxUUK92Q.cjs');
4
+ const require_src = require('./src-rTwoOhL4.cjs');
5
5
  let node_url = require("node:url");
6
6
  let node_fs = require("node:fs");
7
7
  let node_path = require("node:path");
@@ -23,10 +23,8 @@ let atomically = require("atomically");
23
23
  const colors = {
24
24
  success: (s) => picocolors.default.green(s),
25
25
  error: (s) => picocolors.default.red(s),
26
- info: (s) => picocolors.default.cyan(s),
27
26
  warn: (s) => picocolors.default.yellow(s),
28
- muted: (s) => picocolors.default.gray(s),
29
- bold: (s) => picocolors.default.bold(s)
27
+ info: (s) => picocolors.default.cyan(s)
30
28
  };
31
29
  /**
32
30
  * Status indicators with emoji.
@@ -183,7 +181,7 @@ function buildUpdatedBlock(block, result) {
183
181
  * Find the c8 executable path.
184
182
  * Checks local node_modules/.bin first, then falls back to npx.
185
183
  */
186
- function findC8Path() {
184
+ function findC8Path$1() {
187
185
  const localPaths = [(0, node_path.resolve)(process.cwd(), "node_modules", ".bin", "c8"), (0, node_path.resolve)(process.cwd(), "..", "..", "node_modules", ".bin", "c8")];
188
186
  for (const localPath of localPaths) if ((0, node_fs.existsSync)(localPath)) return localPath;
189
187
  return "npx c8";
@@ -192,18 +190,18 @@ function findC8Path() {
192
190
  * Check if c8 is available in the current environment.
193
191
  */
194
192
  async function isC8Available() {
195
- const c8Path = findC8Path();
196
- return new Promise((resolve$1) => {
193
+ const c8Path = findC8Path$1();
194
+ return new Promise((resolve$2) => {
197
195
  const isNpx = c8Path === "npx c8";
198
196
  const proc = (0, node_child_process.spawn)(isNpx ? "npx" : c8Path, isNpx ? ["c8", "--version"] : ["--version"], {
199
197
  shell: false,
200
198
  stdio: "ignore"
201
199
  });
202
200
  proc.on("close", (code) => {
203
- resolve$1(code === 0);
201
+ resolve$2(code === 0);
204
202
  });
205
203
  proc.on("error", () => {
206
- resolve$1(false);
204
+ resolve$2(false);
207
205
  });
208
206
  });
209
207
  }
@@ -229,7 +227,7 @@ function getCoverageEnv(ctx) {
229
227
  */
230
228
  async function generateCoverageReport(ctx) {
231
229
  const { options, tempDir } = ctx;
232
- const c8Path = findC8Path();
230
+ const c8Path = findC8Path$1();
233
231
  const reportArgs = [
234
232
  "report",
235
233
  "--temp-directory",
@@ -240,6 +238,12 @@ async function generateCoverageReport(ctx) {
240
238
  options.src,
241
239
  "--all",
242
240
  ...options.include.flatMap((pattern) => ["--include", pattern]),
241
+ ...options.exclude.flatMap((pattern) => ["--exclude", pattern]),
242
+ ...options.excludeNodeModules ? ["--exclude-node-modules"] : ["--no-exclude-node-modules"],
243
+ ...options.excludeAfterRemap ? ["--exclude-after-remap"] : [],
244
+ ...options.skipFull ? ["--skip-full"] : [],
245
+ ...options.allowExternal ? ["--allowExternal"] : [],
246
+ ...options.monocart ? ["--experimental-monocart"] : [],
243
247
  ...options.reporters.flatMap((reporter) => ["--reporter", reporter])
244
248
  ];
245
249
  const isNpx = c8Path === "npx c8";
@@ -278,9 +282,9 @@ async function cleanupCoverageContext(ctx) {
278
282
  * Register the run command.
279
283
  */
280
284
  function registerRunCommand(program) {
281
- program.command("run").description("Run golden tests").argument("[files...]", "Test files to run (default: **/*.tryscript.md)").option("--update", "Update golden files with actual output").option("--diff", "Show diff on failure (default: true)").option("--no-diff", "Hide diff on failure").option("--fail-fast", "Stop on first failure").option("--filter <pattern>", "Filter tests by name pattern").option("--verbose", "Show detailed output including passing test output").option("--quiet", "Suppress non-essential output (only show failures)").option("--coverage", "Enable code coverage collection (requires c8)").option("--coverage-dir <dir>", "Coverage output directory (default: coverage-tryscript)").option("--coverage-reporter <reporter...>", "Coverage reporters (default: text, html). Can be specified multiple times.").action(runCommand);
285
+ program.command("run").description("Run golden tests").argument("[files...]", "Test files to run (default: **/*.tryscript.md)").option("--update", "Update golden files with actual output").option("--diff", "Show diff on failure (default: true)").option("--no-diff", "Hide diff on failure").option("--fail-fast", "Stop on first failure").option("--filter <pattern>", "Filter tests by name pattern").option("--verbose", "Show detailed output including passing test output").option("--quiet", "Suppress non-essential output (only show failures)").option("--coverage", "Enable code coverage collection (requires c8)").option("--coverage-dir <dir>", "Coverage output directory (default: coverage-tryscript)").option("--coverage-reporter <reporter...>", "Coverage reporters (default: text, html). Can be specified multiple times.").option("--coverage-exclude <pattern...>", "Patterns to exclude from coverage (c8 --exclude). Can be specified multiple times.").option("--coverage-exclude-node-modules", "Exclude node_modules from coverage (c8 --exclude-node-modules, default: true)").option("--no-coverage-exclude-node-modules", "Include node_modules in coverage (c8 --no-exclude-node-modules)").option("--coverage-exclude-after-remap", "Apply exclude logic after sourcemap remapping (c8 --exclude-after-remap)").option("--coverage-skip-full", "Hide files with 100% coverage (c8 --skip-full)").option("--coverage-allow-external", "Allow files from outside cwd (c8 --allowExternal)").option("--coverage-monocart", "Use monocart for accurate line counts, better for merging with vitest (c8 --experimental-monocart)").action(runCommand$1);
282
286
  }
283
- async function runCommand(files, options) {
287
+ async function runCommand$1(files, options) {
284
288
  const startTime = Date.now();
285
289
  const opts = {
286
290
  diff: options.diff !== false,
@@ -310,7 +314,13 @@ async function runCommand(files, options) {
310
314
  coverageCtx = await createCoverageContext({
311
315
  ...globalConfig.coverage,
312
316
  reportsDir: options.coverageDir ?? globalConfig.coverage?.reportsDir,
313
- reporters: options.coverageReporter ?? globalConfig.coverage?.reporters
317
+ reporters: options.coverageReporter ?? globalConfig.coverage?.reporters,
318
+ exclude: options.coverageExclude ?? globalConfig.coverage?.exclude,
319
+ excludeNodeModules: options.coverageExcludeNodeModules ?? globalConfig.coverage?.excludeNodeModules,
320
+ excludeAfterRemap: options.coverageExcludeAfterRemap ?? globalConfig.coverage?.excludeAfterRemap,
321
+ skipFull: options.coverageSkipFull ?? globalConfig.coverage?.skipFull,
322
+ allowExternal: options.coverageAllowExternal ?? globalConfig.coverage?.allowExternal,
323
+ monocart: options.coverageMonocart ?? globalConfig.coverage?.monocart
314
324
  });
315
325
  coverageEnv = getCoverageEnv(coverageCtx);
316
326
  }
@@ -394,6 +404,223 @@ async function runCommand(files, options) {
394
404
  process.exit(summary.totalFailed > 0 ? 1 : 0);
395
405
  }
396
406
 
407
+ //#endregion
408
+ //#region src/cli/commands/coverage.ts
409
+ /**
410
+ * Register the coverage command.
411
+ */
412
+ function registerCoverageCommand(program) {
413
+ program.command("coverage").description("Run commands with merged V8 coverage").argument("<commands...>", "Commands to run (each will inherit coverage environment)").option("--reports-dir <dir>", "Coverage output directory (default: coverage)").option("--reporters <reporters>", "Comma-separated coverage reporters (default: text,json,json-summary,lcov,html)").option("--include <patterns>", "Comma-separated patterns to include in coverage").option("--exclude <patterns>", "Comma-separated patterns to exclude from coverage").option("--exclude-node-modules", "Exclude node_modules from coverage (default: true)", true).option("--no-exclude-node-modules", "Include node_modules in coverage").option("--exclude-after-remap", "Apply exclude logic after sourcemap remapping").option("--skip-full", "Hide files with 100% coverage").option("--allow-external", "Allow files from outside cwd").option("--monocart", "Use monocart for accurate line counts (recommended for merging)").option("--src <dir>", "Source directory for sourcemap remapping (default: src)").option("--verbose", "Show coverage summary after each command for debugging").action(coverageCommand);
414
+ }
415
+ /**
416
+ * Run a command with inherited coverage environment.
417
+ */
418
+ async function runCommand(command, env) {
419
+ return new Promise((resolve$2) => {
420
+ const proc = (0, node_child_process.spawn)(command, [], {
421
+ stdio: "inherit",
422
+ env: {
423
+ ...process.env,
424
+ ...env
425
+ },
426
+ shell: true
427
+ });
428
+ proc.on("close", (code) => {
429
+ resolve$2({
430
+ success: code === 0,
431
+ code: code ?? 1
432
+ });
433
+ });
434
+ proc.on("error", (err) => {
435
+ logError(`Failed to run command: ${err.message}`);
436
+ resolve$2({
437
+ success: false,
438
+ code: 1
439
+ });
440
+ });
441
+ });
442
+ }
443
+ /**
444
+ * Find the c8 executable path.
445
+ * Can be overridden via TRYSCRIPT_C8_COMMAND env var for testing.
446
+ */
447
+ function findC8Path() {
448
+ const override = process.env.TRYSCRIPT_C8_COMMAND;
449
+ if (override) return override;
450
+ const localPaths = [(0, node_path.resolve)(process.cwd(), "node_modules", ".bin", "c8"), (0, node_path.resolve)(process.cwd(), "..", "..", "node_modules", ".bin", "c8")];
451
+ for (const localPath of localPaths) if ((0, node_fs.existsSync)(localPath)) return localPath;
452
+ return null;
453
+ }
454
+ /**
455
+ * Get coverage file statistics from temp directory.
456
+ */
457
+ async function getCoverageStats(tempDir) {
458
+ try {
459
+ const coverageFiles = (await (0, node_fs_promises.readdir)(tempDir)).filter((f) => f.startsWith("coverage-") && f.endsWith(".json"));
460
+ let totalBytes = 0;
461
+ for (const file of coverageFiles) {
462
+ const fileStat = await (0, node_fs_promises.stat)((0, node_path.join)(tempDir, file));
463
+ totalBytes += fileStat.size;
464
+ }
465
+ return {
466
+ fileCount: coverageFiles.length,
467
+ totalBytes,
468
+ files: coverageFiles
469
+ };
470
+ } catch {
471
+ return {
472
+ fileCount: 0,
473
+ totalBytes: 0,
474
+ files: []
475
+ };
476
+ }
477
+ }
478
+ /**
479
+ * Generate a text-only coverage report for debugging (doesn't write files).
480
+ */
481
+ async function generateTextReport(tempDir, options, label) {
482
+ const c8Path = findC8Path();
483
+ if (!c8Path) return;
484
+ const include = options.include ?? ["dist/**"];
485
+ const exclude = options.exclude ?? [];
486
+ const tempReportsDir = await (0, node_fs_promises.mkdtemp)((0, node_path.join)((0, node_os.tmpdir)(), "tryscript-coverage-report-"));
487
+ const reportArgs = [
488
+ "report",
489
+ "--temp-directory",
490
+ tempDir,
491
+ "--reports-dir",
492
+ tempReportsDir,
493
+ "--src",
494
+ options.src ?? "src",
495
+ "--all",
496
+ ...include.flatMap((pattern) => ["--include", pattern]),
497
+ ...exclude.flatMap((pattern) => ["--exclude", pattern]),
498
+ ...options.excludeNodeModules !== false ? ["--exclude-node-modules"] : ["--no-exclude-node-modules"],
499
+ ...options.excludeAfterRemap ? ["--exclude-after-remap"] : [],
500
+ ...options.monocart ? ["--experimental-monocart"] : [],
501
+ "--reporter",
502
+ "text"
503
+ ];
504
+ console.error(colors.info(`\n--- Coverage after: ${label} ---`));
505
+ await new Promise((resolve$2) => {
506
+ const proc = (0, node_child_process.spawn)(c8Path, reportArgs, {
507
+ stdio: "inherit",
508
+ shell: false
509
+ });
510
+ proc.on("close", () => {
511
+ resolve$2();
512
+ });
513
+ proc.on("error", () => {
514
+ resolve$2();
515
+ });
516
+ });
517
+ await (0, node_fs_promises.rm)(tempReportsDir, {
518
+ recursive: true,
519
+ force: true
520
+ });
521
+ }
522
+ /**
523
+ * Generate c8 coverage report.
524
+ */
525
+ async function generateReport(tempDir, options) {
526
+ const c8Path = findC8Path();
527
+ if (!c8Path) {
528
+ logError("c8 not found. Install with: npm install -D c8");
529
+ return false;
530
+ }
531
+ const reporters = options.reporters ?? [
532
+ "text",
533
+ "json",
534
+ "json-summary",
535
+ "lcov",
536
+ "html"
537
+ ];
538
+ const include = options.include ?? ["dist/**"];
539
+ const exclude = options.exclude ?? [];
540
+ const reportArgs = [
541
+ "report",
542
+ "--temp-directory",
543
+ tempDir,
544
+ "--reports-dir",
545
+ options.reportsDir ?? "coverage",
546
+ "--src",
547
+ options.src ?? "src",
548
+ "--all",
549
+ ...include.flatMap((pattern) => ["--include", pattern]),
550
+ ...exclude.flatMap((pattern) => ["--exclude", pattern]),
551
+ ...options.excludeNodeModules !== false ? ["--exclude-node-modules"] : ["--no-exclude-node-modules"],
552
+ ...options.excludeAfterRemap ? ["--exclude-after-remap"] : [],
553
+ ...options.skipFull ? ["--skip-full"] : [],
554
+ ...options.allowExternal ? ["--allowExternal"] : [],
555
+ ...options.monocart ? ["--experimental-monocart"] : [],
556
+ ...reporters.flatMap((reporter) => ["--reporter", reporter])
557
+ ];
558
+ return new Promise((resolve$2) => {
559
+ const proc = (0, node_child_process.spawn)(c8Path, reportArgs, {
560
+ stdio: "inherit",
561
+ shell: false
562
+ });
563
+ proc.on("close", (code) => {
564
+ resolve$2(code === 0);
565
+ });
566
+ proc.on("error", (err) => {
567
+ logError(`Failed to generate coverage report: ${err.message}`);
568
+ resolve$2(false);
569
+ });
570
+ });
571
+ }
572
+ async function coverageCommand(commands, options) {
573
+ if (commands.length === 0) {
574
+ logError("No commands specified. Usage: tryscript coverage \"cmd1\" \"cmd2\"");
575
+ process.exit(1);
576
+ }
577
+ if (!findC8Path()) {
578
+ logError("Coverage requires c8. Install with: npm install -D c8");
579
+ process.exit(1);
580
+ }
581
+ const parsedOptions = {
582
+ ...options,
583
+ reporters: options.reporters ? typeof options.reporters === "string" ? options.reporters.split(",") : options.reporters : void 0,
584
+ include: options.include ? typeof options.include === "string" ? options.include.split(",") : options.include : void 0,
585
+ exclude: options.exclude ? typeof options.exclude === "string" ? options.exclude.split(",") : options.exclude : void 0
586
+ };
587
+ const coverageTemp = await (0, node_fs_promises.mkdtemp)((0, node_path.join)((0, node_os.tmpdir)(), "tryscript-coverage-"));
588
+ const coverageEnv = { NODE_V8_COVERAGE: coverageTemp };
589
+ console.error(colors.info(`Collecting V8 coverage to ${coverageTemp}`));
590
+ let hasFailures = false;
591
+ let previousFileCount = 0;
592
+ try {
593
+ for (let i = 0; i < commands.length; i++) {
594
+ const command = commands[i];
595
+ console.error(colors.info(`\n=== Running command ${i + 1}/${commands.length}: ${command} ===`));
596
+ const result = await runCommand(command, coverageEnv);
597
+ if (!result.success) {
598
+ logWarn(`Command exited with code ${result.code}: ${command}`);
599
+ hasFailures = true;
600
+ }
601
+ const stats = await getCoverageStats(coverageTemp);
602
+ const newFiles = stats.fileCount - previousFileCount;
603
+ const bytesKB = (stats.totalBytes / 1024).toFixed(1);
604
+ console.error(colors.info(`\nV8 coverage: ${stats.fileCount} files (${newFiles} new), ${bytesKB} KB total`));
605
+ if (newFiles === 0) logWarn("No new coverage files from this command. This may indicate the command doesn't write to NODE_V8_COVERAGE.");
606
+ if (parsedOptions.verbose && stats.fileCount > 0) await generateTextReport(coverageTemp, parsedOptions, command);
607
+ previousFileCount = stats.fileCount;
608
+ }
609
+ console.error(colors.info("\n=== Generating merged coverage report ==="));
610
+ if (!await generateReport(coverageTemp, parsedOptions)) {
611
+ logError("Failed to generate coverage report");
612
+ process.exit(1);
613
+ }
614
+ console.error(colors.success(`\nCoverage report written to ${parsedOptions.reportsDir ?? "coverage"}/`));
615
+ } finally {
616
+ await (0, node_fs_promises.rm)(coverageTemp, {
617
+ recursive: true,
618
+ force: true
619
+ });
620
+ }
621
+ if (hasFailures) process.exit(1);
622
+ }
623
+
397
624
  //#endregion
398
625
  //#region src/cli/commands/readme.ts
399
626
  /**
@@ -473,7 +700,7 @@ function isInteractive$1() {
473
700
  */
474
701
  function showReadme(options) {
475
702
  try {
476
- const formatted = formatMarkdown$1(loadReadme(), !options?.raw && isInteractive$1());
703
+ const formatted = formatMarkdown$1(loadReadme(), options?.color ?? (!options?.raw && isInteractive$1()));
477
704
  console.log(formatted);
478
705
  } catch (error) {
479
706
  const message = error instanceof Error ? error.message : String(error);
@@ -485,7 +712,7 @@ function showReadme(options) {
485
712
  * Register the readme command.
486
713
  */
487
714
  function registerReadmeCommand(program) {
488
- program.command("readme").description("Display README documentation").option("--raw", "Output raw markdown without formatting").action(showReadme);
715
+ program.command("readme").description("Display README documentation").option("--raw", "Output raw markdown without formatting").option("--color", "Force colorized output (for testing)").action(showReadme);
489
716
  }
490
717
 
491
718
  //#endregion
@@ -565,9 +792,9 @@ function isInteractive() {
565
792
  * Register the docs command.
566
793
  */
567
794
  function registerDocsCommand(program) {
568
- program.command("docs").description("Display concise syntax reference").option("--raw", "Output raw markdown without formatting").action((options) => {
795
+ program.command("docs").description("Display concise syntax reference").option("--raw", "Output raw markdown without formatting").option("--color", "Force colorized output (for testing)").action((options) => {
569
796
  try {
570
- const formatted = formatMarkdown(loadDocs(), !options.raw && isInteractive());
797
+ const formatted = formatMarkdown(loadDocs(), options.color ?? (!options.raw && isInteractive()));
571
798
  console.log(formatted);
572
799
  } catch (error) {
573
800
  const message = error instanceof Error ? error.message : String(error);
@@ -587,6 +814,7 @@ function registerDocsCommand(program) {
587
814
  function run(argv) {
588
815
  const program = withColoredHelp(new commander.Command().name("tryscript").version(require_src.VERSION, "--version", "Show version number").description("Golden testing for CLI applications").showHelpAfterError("(use --help for usage)"));
589
816
  registerRunCommand(program);
817
+ registerCoverageCommand(program);
590
818
  registerReadmeCommand(program);
591
819
  registerDocsCommand(program);
592
820
  program.action(() => {