tryscript 0.1.0 → 0.1.2

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,71 +1,150 @@
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)
5
+ [![npm version](https://img.shields.io/npm/v/tryscript)](https://www.npmjs.com/package/tryscript)
6
+ [![X Follow](https://img.shields.io/twitter/follow/ojoshe)](https://x.com/ojoshe)
7
+
3
8
  Golden testing for CLI applications - a TypeScript port of [trycmd](https://github.com/assert-rs/trycmd).
4
9
 
5
- ## Overview
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
+
20
+ ## What It Does
6
21
 
7
- tryscript enables golden testing for any CLI application. Write test cases in Markdown with console code blocks, and tryscript runs the commands, compares output, and reports differences.
22
+ Write CLI tests as Markdown. tryscript runs commands, captures output, and compares against expected results. Tests become documentation; documentation becomes tests.
8
23
 
9
24
  ````markdown
10
- # Test: Hello World
25
+ # Test: Basic echo
11
26
 
12
27
  ```console
13
28
  $ echo "hello world"
14
29
  hello world
15
30
  ? 0
16
31
  ```
17
- ````
18
32
 
19
- ## Features
33
+ # Test: Grep with pattern matching
20
34
 
21
- - **Markdown test format** - Tests are readable documentation
22
- - **Elision patterns** - Match dynamic output with `[..]`, `...`, `[EXE]`, `[ROOT]`, `[CWD]`
23
- - **Custom patterns** - Define regex patterns for timestamps, versions, UUIDs
24
- - **Update mode** - Regenerate golden files with `--update`
25
- - **Self-bootstrapping** - tryscript tests itself
35
+ ```console
36
+ $ ls -la | grep ".md"
37
+ [..]README.md
38
+ ...
39
+ ? 0
40
+ ```
41
+ ````
42
+
43
+ The `[..]` matches any text on that line. The `...` matches zero or more lines. These "elision patterns" let tests handle dynamic output gracefully.
26
44
 
27
45
  ## Quick Start
28
46
 
29
47
  ```bash
30
48
  # Install
31
- pnpm add tryscript
49
+ pnpm add -D tryscript
50
+
51
+ # For coverage support (optional)
52
+ pnpm add -D c8
53
+
54
+ # For accurate line counts when merging with vitest (optional)
55
+ pnpm add -D c8 monocart-coverage-reports
32
56
 
33
57
  # Run tests
34
- npx tryscript tests/
58
+ npx tryscript run tests/
35
59
 
36
- # Update golden files
37
- npx tryscript --update
60
+ # Update expected output when behavior changes
61
+ npx tryscript run --update tests/
38
62
  ```
39
63
 
40
- ## Documentation
64
+ ## Features
65
+
66
+ - **Markdown format** - Tests are readable documentation
67
+ - **Elision patterns** - Handle variable output: `[..]`, `...`, `[CWD]`, `[ROOT]`, `[EXE]`
68
+ - **Custom patterns** - Define regex patterns for timestamps, versions, UUIDs
69
+ - **Update mode** - Regenerate expected output with `--update`
70
+ - **Sandbox mode** - Isolate tests in temp directories
71
+ - **Code coverage** - Track coverage from subprocess execution with `--coverage`
72
+
73
+ ## Example Test File
41
74
 
42
- See [packages/tryscript/README.md](packages/tryscript/README.md) for full documentation.
75
+ ````markdown
76
+ ---
77
+ env:
78
+ NO_COLOR: "1"
79
+ sandbox: true
80
+ ---
43
81
 
44
- ## Project Structure
82
+ # Test: CLI help
45
83
 
84
+ ```console
85
+ $ my-cli --help
86
+ Usage: my-cli [options] <command>
87
+
88
+ Options:
89
+ --version Show version
90
+ --help Show this help
91
+ ...
92
+ ? 0
46
93
  ```
47
- tryscript/
48
- ├── packages/
49
- │ └── tryscript/ # Main package
50
- │ ├── src/ # TypeScript source
51
- │ └── tests/ # Self-tests
52
- └── docs/ # Documentation
94
+
95
+ # Test: Version output
96
+
97
+ ```console
98
+ $ my-cli --version
99
+ my-cli v[..]
100
+ ? 0
101
+ ```
102
+
103
+ # Test: Error handling
104
+
105
+ ```console
106
+ $ my-cli unknown-command 2>&1
107
+ Error: unknown command 'unknown-command'
108
+ ? 1
53
109
  ```
110
+ ````
111
+
112
+ ## CLI Reference
113
+
114
+ ```bash
115
+ tryscript run [files...] # Run golden tests
116
+ tryscript coverage <commands...> # Run commands with merged coverage
117
+ tryscript docs # Show syntax quick reference
118
+ tryscript readme # Show this documentation
119
+ tryscript --help # Show all options
120
+ ```
121
+
122
+ For complete syntax reference, run `tryscript docs` or see the [reference documentation](https://github.com/jlevy/tryscript/blob/main/docs/tryscript-reference.md).
123
+
124
+ ### Common Options
125
+
126
+ | Option | Description |
127
+ | --- | --- |
128
+ | `--update` | Update test files with actual output |
129
+ | `--fail-fast` | Stop on first failure |
130
+ | `--filter <regex>` | Filter tests by name |
131
+ | `--verbose` | Show detailed output |
132
+ | `--coverage` | Collect code coverage (requires c8) |
133
+ | `--coverage-monocart` | Use monocart for accurate line counts (requires monocart-coverage-reports) |
134
+ | `--coverage-exclude-node-modules` | Exclude node_modules from coverage (default: true) |
135
+ | `--coverage-exclude <pattern>` | Exclude patterns from coverage |
54
136
 
55
137
  ## Development
56
138
 
57
139
  ```bash
58
- # Install dependencies
140
+ # Clone and install
141
+ git clone https://github.com/jlevy/tryscript.git
142
+ cd tryscript
59
143
  pnpm install
60
144
 
61
- # Build
145
+ # Build and test
62
146
  pnpm build
63
-
64
- # Run tests
65
147
  pnpm test
66
-
67
- # Run self-tests
68
- pnpm -r tryscript tests/
69
148
  ```
70
149
 
71
150
  ## License
package/dist/bin.cjs CHANGED
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
 
4
- const require_src = require('./src-CP4-Q-U5.cjs');
4
+ const require_src = require('./src-BR3CZ3tc.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");
8
+ let node_child_process = require("node:child_process");
8
9
  let node_fs_promises = require("node:fs/promises");
10
+ let node_os = require("node:os");
9
11
  let commander = require("commander");
10
12
  let fast_glob = require("fast-glob");
11
13
  fast_glob = require_src.__toESM(fast_glob);
@@ -21,10 +23,8 @@ let atomically = require("atomically");
21
23
  const colors = {
22
24
  success: (s) => picocolors.default.green(s),
23
25
  error: (s) => picocolors.default.red(s),
24
- info: (s) => picocolors.default.cyan(s),
25
26
  warn: (s) => picocolors.default.yellow(s),
26
- muted: (s) => picocolors.default.gray(s),
27
- bold: (s) => picocolors.default.bold(s)
27
+ info: (s) => picocolors.default.cyan(s)
28
28
  };
29
29
  /**
30
30
  * Status indicators with emoji.
@@ -170,15 +170,121 @@ function buildUpdatedBlock(block, result) {
170
170
  return lines.join("\n");
171
171
  }
172
172
 
173
+ //#endregion
174
+ //#region src/lib/coverage.ts
175
+ /**
176
+ * Coverage collection for CLI subprocess testing.
177
+ *
178
+ * Uses c8 and NODE_V8_COVERAGE to collect coverage from spawned processes.
179
+ */
180
+ /**
181
+ * Find the c8 executable path.
182
+ * Checks local node_modules/.bin first, then falls back to npx.
183
+ */
184
+ function findC8Path$1() {
185
+ const localPaths = [(0, node_path.resolve)(process.cwd(), "node_modules", ".bin", "c8"), (0, node_path.resolve)(process.cwd(), "..", "..", "node_modules", ".bin", "c8")];
186
+ for (const localPath of localPaths) if ((0, node_fs.existsSync)(localPath)) return localPath;
187
+ return "npx c8";
188
+ }
189
+ /**
190
+ * Check if c8 is available in the current environment.
191
+ */
192
+ async function isC8Available() {
193
+ const c8Path = findC8Path$1();
194
+ return new Promise((resolve$2) => {
195
+ const isNpx = c8Path === "npx c8";
196
+ const proc = (0, node_child_process.spawn)(isNpx ? "npx" : c8Path, isNpx ? ["c8", "--version"] : ["--version"], {
197
+ shell: false,
198
+ stdio: "ignore"
199
+ });
200
+ proc.on("close", (code) => {
201
+ resolve$2(code === 0);
202
+ });
203
+ proc.on("error", () => {
204
+ resolve$2(false);
205
+ });
206
+ });
207
+ }
208
+ /**
209
+ * Create a coverage context for collecting V8 coverage data.
210
+ */
211
+ async function createCoverageContext(config) {
212
+ const options = require_src.resolveCoverageConfig(config);
213
+ return {
214
+ tempDir: await (0, node_fs_promises.mkdtemp)((0, node_path.join)((0, node_os.tmpdir)(), "tryscript-coverage-")),
215
+ options
216
+ };
217
+ }
218
+ /**
219
+ * Get environment variables for enabling V8 coverage in spawned processes.
220
+ */
221
+ function getCoverageEnv(ctx) {
222
+ return { NODE_V8_COVERAGE: ctx.tempDir };
223
+ }
224
+ /**
225
+ * Generate coverage report from collected V8 coverage data using c8.
226
+ * Throws an error if coverage report generation fails.
227
+ */
228
+ async function generateCoverageReport(ctx) {
229
+ const { options, tempDir } = ctx;
230
+ const c8Path = findC8Path$1();
231
+ const reportArgs = [
232
+ "report",
233
+ "--temp-directory",
234
+ tempDir,
235
+ "--reports-dir",
236
+ options.reportsDir,
237
+ "--src",
238
+ options.src,
239
+ "--all",
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"] : [],
247
+ ...options.reporters.flatMap((reporter) => ["--reporter", reporter])
248
+ ];
249
+ const isNpx = c8Path === "npx c8";
250
+ const command = isNpx ? "npx" : c8Path;
251
+ const args = isNpx ? ["c8", ...reportArgs] : reportArgs;
252
+ await new Promise((resolvePromise, reject) => {
253
+ const proc = (0, node_child_process.spawn)(command, args, {
254
+ shell: false,
255
+ stdio: "inherit"
256
+ });
257
+ proc.on("close", (code) => {
258
+ if (code === 0) resolvePromise();
259
+ else reject(/* @__PURE__ */ new Error(`c8 report exited with code ${code}`));
260
+ });
261
+ proc.on("error", (err) => {
262
+ reject(/* @__PURE__ */ new Error(`Failed to run c8 report: ${err.message}`));
263
+ });
264
+ });
265
+ }
266
+ /**
267
+ * Clean up coverage context by removing the temporary directory.
268
+ */
269
+ async function cleanupCoverageContext(ctx) {
270
+ try {
271
+ await (0, node_fs_promises.access)(ctx.tempDir);
272
+ await (0, node_fs_promises.rm)(ctx.tempDir, {
273
+ recursive: true,
274
+ force: true
275
+ });
276
+ } catch {}
277
+ }
278
+
173
279
  //#endregion
174
280
  //#region src/cli/commands/run.ts
175
281
  /**
176
282
  * Register the run command.
177
283
  */
178
284
  function registerRunCommand(program) {
179
- 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)").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);
180
286
  }
181
- async function runCommand(files, options) {
287
+ async function runCommand$1(files, options) {
182
288
  const startTime = Date.now();
183
289
  const opts = {
184
290
  diff: options.diff !== false,
@@ -198,6 +304,26 @@ async function runCommand(files, options) {
198
304
  process.exit(1);
199
305
  }
200
306
  const globalConfig = await require_src.loadConfig(process.cwd());
307
+ let coverageCtx;
308
+ let coverageEnv = {};
309
+ if (options.coverage) {
310
+ if (!await isC8Available()) {
311
+ logError("Coverage requires c8. Install with: npm install -D c8");
312
+ process.exit(1);
313
+ }
314
+ coverageCtx = await createCoverageContext({
315
+ ...globalConfig.coverage,
316
+ reportsDir: options.coverageDir ?? globalConfig.coverage?.reportsDir,
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
324
+ });
325
+ coverageEnv = getCoverageEnv(coverageCtx);
326
+ }
201
327
  const fileResults = [];
202
328
  let shouldStop = false;
203
329
  for (const filePath of testFiles) {
@@ -212,7 +338,7 @@ async function runCommand(files, options) {
212
338
  const onlyBlocks = blocksToRun.filter((b) => b.only);
213
339
  if (onlyBlocks.length > 0) blocksToRun = onlyBlocks;
214
340
  if (blocksToRun.length === 0) continue;
215
- const ctx = await require_src.createExecutionContext(config, filePath);
341
+ const ctx = await require_src.createExecutionContext(config, filePath, coverageEnv);
216
342
  const results = [];
217
343
  try {
218
344
  for (const block of blocksToRun) {
@@ -264,9 +390,161 @@ async function runCommand(files, options) {
264
390
  duration: Date.now() - startTime
265
391
  };
266
392
  reportSummary(summary, opts);
393
+ if (coverageCtx) {
394
+ console.error("\nGenerating coverage report...");
395
+ try {
396
+ await generateCoverageReport(coverageCtx);
397
+ console.error(colors.success(`Coverage report written to ${coverageCtx.options.reportsDir}/`));
398
+ } catch (error) {
399
+ logError(`Failed to generate coverage report: ${error instanceof Error ? error.message : String(error)}`);
400
+ } finally {
401
+ await cleanupCoverageContext(coverageCtx);
402
+ }
403
+ }
267
404
  process.exit(summary.totalFailed > 0 ? 1 : 0);
268
405
  }
269
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)").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
+ * Generate c8 coverage report.
456
+ */
457
+ async function generateReport(tempDir, options) {
458
+ const c8Path = findC8Path();
459
+ if (!c8Path) {
460
+ logError("c8 not found. Install with: npm install -D c8");
461
+ return false;
462
+ }
463
+ const reporters = options.reporters ?? [
464
+ "text",
465
+ "json",
466
+ "json-summary",
467
+ "lcov",
468
+ "html"
469
+ ];
470
+ const include = options.include ?? ["dist/**"];
471
+ const exclude = options.exclude ?? [];
472
+ const reportArgs = [
473
+ "report",
474
+ "--temp-directory",
475
+ tempDir,
476
+ "--reports-dir",
477
+ options.reportsDir ?? "coverage",
478
+ "--src",
479
+ options.src ?? "src",
480
+ "--all",
481
+ ...include.flatMap((pattern) => ["--include", pattern]),
482
+ ...exclude.flatMap((pattern) => ["--exclude", pattern]),
483
+ ...options.excludeNodeModules !== false ? ["--exclude-node-modules"] : ["--no-exclude-node-modules"],
484
+ ...options.excludeAfterRemap ? ["--exclude-after-remap"] : [],
485
+ ...options.skipFull ? ["--skip-full"] : [],
486
+ ...options.allowExternal ? ["--allowExternal"] : [],
487
+ ...options.monocart ? ["--experimental-monocart"] : [],
488
+ ...reporters.flatMap((reporter) => ["--reporter", reporter])
489
+ ];
490
+ return new Promise((resolve$2) => {
491
+ const proc = (0, node_child_process.spawn)(c8Path, reportArgs, {
492
+ stdio: "inherit",
493
+ shell: false
494
+ });
495
+ proc.on("close", (code) => {
496
+ resolve$2(code === 0);
497
+ });
498
+ proc.on("error", (err) => {
499
+ logError(`Failed to generate coverage report: ${err.message}`);
500
+ resolve$2(false);
501
+ });
502
+ });
503
+ }
504
+ async function coverageCommand(commands, options) {
505
+ if (commands.length === 0) {
506
+ logError("No commands specified. Usage: tryscript coverage \"cmd1\" \"cmd2\"");
507
+ process.exit(1);
508
+ }
509
+ if (!findC8Path()) {
510
+ logError("Coverage requires c8. Install with: npm install -D c8");
511
+ process.exit(1);
512
+ }
513
+ const coverageTemp = await (0, node_fs_promises.mkdtemp)((0, node_path.join)((0, node_os.tmpdir)(), "tryscript-coverage-"));
514
+ const coverageEnv = { NODE_V8_COVERAGE: coverageTemp };
515
+ console.error(colors.info(`Collecting V8 coverage to ${coverageTemp}`));
516
+ let hasFailures = false;
517
+ try {
518
+ for (let i = 0; i < commands.length; i++) {
519
+ const command = commands[i];
520
+ console.error(colors.info(`\n=== Running command ${i + 1}/${commands.length}: ${command} ===`));
521
+ const result = await runCommand(command, coverageEnv);
522
+ if (!result.success) {
523
+ logWarn(`Command exited with code ${result.code}: ${command}`);
524
+ hasFailures = true;
525
+ }
526
+ }
527
+ console.error(colors.info("\n=== Generating merged coverage report ==="));
528
+ const parsedOptions = {
529
+ ...options,
530
+ reporters: options.reporters ? typeof options.reporters === "string" ? options.reporters.split(",") : options.reporters : void 0,
531
+ include: options.include ? typeof options.include === "string" ? options.include.split(",") : options.include : void 0,
532
+ exclude: options.exclude ? typeof options.exclude === "string" ? options.exclude.split(",") : options.exclude : void 0
533
+ };
534
+ if (!await generateReport(coverageTemp, parsedOptions)) {
535
+ logError("Failed to generate coverage report");
536
+ process.exit(1);
537
+ }
538
+ console.error(colors.success(`\nCoverage report written to ${parsedOptions.reportsDir ?? "coverage"}/`));
539
+ } finally {
540
+ await (0, node_fs_promises.rm)(coverageTemp, {
541
+ recursive: true,
542
+ force: true
543
+ });
544
+ }
545
+ if (hasFailures) process.exit(1);
546
+ }
547
+
270
548
  //#endregion
271
549
  //#region src/cli/commands/readme.ts
272
550
  /**
@@ -346,7 +624,7 @@ function isInteractive$1() {
346
624
  */
347
625
  function showReadme(options) {
348
626
  try {
349
- const formatted = formatMarkdown$1(loadReadme(), !options?.raw && isInteractive$1());
627
+ const formatted = formatMarkdown$1(loadReadme(), options?.color ?? (!options?.raw && isInteractive$1()));
350
628
  console.log(formatted);
351
629
  } catch (error) {
352
630
  const message = error instanceof Error ? error.message : String(error);
@@ -358,7 +636,7 @@ function showReadme(options) {
358
636
  * Register the readme command.
359
637
  */
360
638
  function registerReadmeCommand(program) {
361
- program.command("readme").description("Display README documentation").option("--raw", "Output raw markdown without formatting").action(showReadme);
639
+ program.command("readme").description("Display README documentation").option("--raw", "Output raw markdown without formatting").option("--color", "Force colorized output (for testing)").action(showReadme);
362
640
  }
363
641
 
364
642
  //#endregion
@@ -438,9 +716,9 @@ function isInteractive() {
438
716
  * Register the docs command.
439
717
  */
440
718
  function registerDocsCommand(program) {
441
- program.command("docs").description("Display concise syntax reference").option("--raw", "Output raw markdown without formatting").action((options) => {
719
+ program.command("docs").description("Display concise syntax reference").option("--raw", "Output raw markdown without formatting").option("--color", "Force colorized output (for testing)").action((options) => {
442
720
  try {
443
- const formatted = formatMarkdown(loadDocs(), !options.raw && isInteractive());
721
+ const formatted = formatMarkdown(loadDocs(), options.color ?? (!options.raw && isInteractive()));
444
722
  console.log(formatted);
445
723
  } catch (error) {
446
724
  const message = error instanceof Error ? error.message : String(error);
@@ -460,6 +738,7 @@ function registerDocsCommand(program) {
460
738
  function run(argv) {
461
739
  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)"));
462
740
  registerRunCommand(program);
741
+ registerCoverageCommand(program);
463
742
  registerReadmeCommand(program);
464
743
  registerDocsCommand(program);
465
744
  program.action(() => {