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 +109 -30
- package/dist/bin.cjs +290 -11
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.mjs +293 -14
- package/dist/bin.mjs.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +69 -2
- package/dist/index.d.mts +69 -2
- package/dist/index.mjs +1 -1
- package/dist/{src-CP4-Q-U5.cjs → src-BR3CZ3tc.cjs} +43 -3
- package/dist/src-BR3CZ3tc.cjs.map +1 -0
- package/dist/{src-Ca6X7ul-.mjs → src-DKrim0QL.mjs} +38 -4
- package/dist/src-DKrim0QL.mjs.map +1 -0
- package/docs/tryscript-reference.md +219 -4
- package/package.json +15 -4
- package/dist/src-CP4-Q-U5.cjs.map +0 -1
- package/dist/src-Ca6X7ul-.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,71 +1,150 @@
|
|
|
1
1
|
# tryscript
|
|
2
2
|
|
|
3
|
+
[](https://github.com/jlevy/tryscript/actions/workflows/ci.yml)
|
|
4
|
+
[](https://raw.githubusercontent.com/jlevy/tryscript/main/badges/packages/tryscript/coverage-total.svg)
|
|
5
|
+
[](https://www.npmjs.com/package/tryscript)
|
|
6
|
+
[](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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
33
|
+
# Test: Grep with pattern matching
|
|
20
34
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
37
|
-
npx tryscript --update
|
|
60
|
+
# Update expected output when behavior changes
|
|
61
|
+
npx tryscript run --update tests/
|
|
38
62
|
```
|
|
39
63
|
|
|
40
|
-
##
|
|
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
|
-
|
|
75
|
+
````markdown
|
|
76
|
+
---
|
|
77
|
+
env:
|
|
78
|
+
NO_COLOR: "1"
|
|
79
|
+
sandbox: true
|
|
80
|
+
---
|
|
43
81
|
|
|
44
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
#
|
|
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-
|
|
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
|
-
|
|
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(() => {
|