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 +60 -51
- package/dist/bin.cjs +245 -17
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.mjs +243 -15
- package/dist/bin.mjs.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +26 -0
- package/dist/index.d.mts +26 -0
- package/dist/index.mjs +1 -1
- package/dist/{src-CndHSuTT.mjs → src-BBeKy_V9.mjs} +16 -4
- package/dist/src-BBeKy_V9.mjs.map +1 -0
- package/dist/{src-CxUUK92Q.cjs → src-rTwoOhL4.cjs} +16 -4
- package/dist/src-rTwoOhL4.cjs.map +1 -0
- package/docs/tryscript-reference.md +203 -8
- package/package.json +8 -3
- package/dist/src-CndHSuTT.mjs.map +0 -1
- package/dist/src-CxUUK92Q.cjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,61 +1,26 @@
|
|
|
1
1
|
# tryscript
|
|
2
2
|
|
|
3
|
-
[](https://github.com/jlevy/tryscript/actions/
|
|
4
|
-
[](https://
|
|
3
|
+
[](https://github.com/jlevy/tryscript/actions/runs/20774584634)
|
|
4
|
+
[](https://github.com/jlevy/tryscript/actions/runs/20774584634)
|
|
5
5
|
[](https://www.npmjs.com/package/tryscript)
|
|
6
6
|
[](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...]
|
|
100
|
-
tryscript
|
|
101
|
-
tryscript
|
|
102
|
-
tryscript
|
|
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-
|
|
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
|
-
|
|
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$
|
|
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$
|
|
201
|
+
resolve$2(code === 0);
|
|
204
202
|
});
|
|
205
203
|
proc.on("error", () => {
|
|
206
|
-
resolve$
|
|
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(() => {
|