tryscript 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Joshua Levy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -2,222 +2,72 @@
2
2
 
3
3
  Golden testing for CLI applications - a TypeScript port of [trycmd](https://github.com/assert-rs/trycmd).
4
4
 
5
- ## Requirements
5
+ ## Overview
6
6
 
7
- - **Node.js 20+** (tested with v20, v22, v24)
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.
8
8
 
9
- ## Installation
9
+ ````markdown
10
+ # Test: Hello World
10
11
 
11
- ```bash
12
- npm install tryscript
13
- # or
14
- pnpm add tryscript
15
- ```
16
-
17
- ## Quick Start
18
-
19
- Create a test file with the `.tryscript.md` extension:
20
-
21
- ```markdown
22
- # Test: Help command
23
-
24
- \`\`\`console
25
- $ my-cli --help
26
- Usage: my-cli [options]
27
-
28
- Options:
29
- --version Show version
30
- --help Show help
31
- ? 0
32
- \`\`\`
33
- ```
34
-
35
- Run the tests:
36
-
37
- ```bash
38
- npx tryscript tests/
39
- ```
40
-
41
- ## Test File Format
42
-
43
- Test files are Markdown documents with console code blocks. Each code block represents a test case:
44
-
45
- ```markdown
46
- \`\`\`console
47
- $ <command>
48
- <expected output>
49
- ? <exit code>
50
- \`\`\`
51
- ```
52
-
53
- ### Example
54
-
55
- ```markdown
56
- # Test: Echo command
57
-
58
- \`\`\`console
12
+ ```console
59
13
  $ echo "hello world"
60
14
  hello world
61
15
  ? 0
62
- \`\`\`
63
-
64
- # Test: Exit with error
65
-
66
- \`\`\`console
67
- $ exit 1
68
- ? 1
69
- \`\`\`
70
16
  ```
17
+ ````
71
18
 
72
- ## Elision Patterns
73
-
74
- Use elision patterns to match dynamic or platform-specific output:
75
-
76
- | Pattern | Description | Example |
77
- | -------- | ---------------------------------------- | -------------------------- |
78
- | `[..]` | Match any characters on the current line | `Built in [..]ms` |
79
- | `...` | Match zero or more complete lines | `...\nDone` |
80
- | `[EXE]` | Match `.exe` on Windows, empty otherwise | `my-cli[EXE] --help` |
81
- | `[ROOT]` | Match the test's root directory | `[ROOT]/output.txt` |
82
- | `[CWD]` | Match the current working directory | `[CWD]/file.txt` |
19
+ ## Features
83
20
 
84
- ### Example with Elision
85
-
86
- ```markdown
87
- \`\`\`console
88
- $ time-command
89
- Elapsed: [..]ms
90
- ? 0
91
- \`\`\`
92
- ```
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
93
26
 
94
- ## Configuration
95
-
96
- ### YAML Frontmatter
97
-
98
- Add configuration at the top of your test file:
27
+ ## Quick Start
99
28
 
100
- ```markdown
101
- ---
102
- bin: ./my-cli
103
- env:
104
- NO_COLOR: "1"
105
- timeout: 5000
106
- ---
29
+ ```bash
30
+ # Install
31
+ pnpm add tryscript
107
32
 
108
- # Test: Custom binary
33
+ # Run tests
34
+ npx tryscript tests/
109
35
 
110
- \`\`\`console
111
- $ my-cli --version
112
- 1.0.0
113
- ? 0
114
- \`\`\`
36
+ # Update golden files
37
+ npx tryscript --update
115
38
  ```
116
39
 
117
- ### Config File
40
+ ## Documentation
118
41
 
119
- Create `tryscript.config.ts` in your project root:
42
+ See [packages/tryscript/README.md](packages/tryscript/README.md) for full documentation.
120
43
 
121
- ```typescript
122
- import { defineConfig } from 'tryscript';
44
+ ## Project Structure
123
45
 
124
- export default defineConfig({
125
- bin: './dist/cli.js',
126
- env: {
127
- NO_COLOR: '1',
128
- },
129
- timeout: 30000,
130
- patterns: {
131
- VERSION: '\\d+\\.\\d+\\.\\d+',
132
- UUID: '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}',
133
- },
134
- });
135
46
  ```
136
-
137
- ## CLI Options
138
-
47
+ tryscript/
48
+ ├── packages/
49
+ │ └── tryscript/ # Main package
50
+ │ ├── src/ # TypeScript source
51
+ │ └── tests/ # Self-tests
52
+ └── docs/ # Documentation
139
53
  ```
140
- tryscript [options] [files...]
141
-
142
- Arguments:
143
- files Test files to run (default: **/*.tryscript.md)
144
-
145
- Options:
146
- --version Show version number
147
- --update Update golden files with actual output
148
- --diff Show diff on failure (default: true)
149
- --no-diff Hide diff on failure
150
- --fail-fast Stop on first failure
151
- --filter <pattern> Filter tests by name pattern
152
- --verbose Show detailed output
153
- --quiet Suppress non-essential output
154
- --help Show help
155
- ```
156
-
157
- ## Update Mode
158
54
 
159
- When your CLI output changes, update all test files at once:
55
+ ## Development
160
56
 
161
57
  ```bash
162
- npx tryscript --update
163
- ```
164
-
165
- This rewrites test files with the actual output from running the commands.
58
+ # Install dependencies
59
+ pnpm install
166
60
 
167
- ## Programmatic API
61
+ # Build
62
+ pnpm build
168
63
 
169
- ```typescript
170
- import { parseTestFile, runBlock, createExecutionContext, matchOutput } from 'tryscript';
64
+ # Run tests
65
+ pnpm test
171
66
 
172
- const content = await fs.readFile('test.tryscript.md', 'utf-8');
173
- const testFile = parseTestFile(content, 'test.tryscript.md');
174
-
175
- const ctx = await createExecutionContext({}, 'test.tryscript.md');
176
- for (const block of testFile.blocks) {
177
- const result = await runBlock(block, ctx);
178
- const matches = matchOutput(
179
- result.actualOutput,
180
- block.expectedOutput,
181
- { root: ctx.tempDir, cwd: ctx.tempDir },
182
- );
183
- console.log(`${block.name}: ${matches ? 'PASS' : 'FAIL'}`);
184
- }
185
- ```
186
-
187
- ## Measuring Coverage
188
-
189
- When testing CLI tools as subprocesses, standard coverage tools don't track execution. Use [c8](https://github.com/bcoe/c8) which leverages Node's V8 coverage collection:
190
-
191
- ```bash
192
- npm install -D c8
193
- ```
194
-
195
- Add scripts to your `package.json`:
196
-
197
- ```json
198
- {
199
- "scripts": {
200
- "test:golden": "tryscript 'tests/**/*.tryscript.md'",
201
- "test:golden:coverage": "c8 --src src --all --include 'dist/**' tryscript 'tests/**/*.tryscript.md'"
202
- }
203
- }
67
+ # Run self-tests
68
+ pnpm -r tryscript tests/
204
69
  ```
205
70
 
206
- Key c8 flags:
207
- - `--src src` — Map coverage back to source files
208
- - `--all` — Include files with 0% coverage
209
- - `--include 'dist/**'` — Track your built CLI output
210
-
211
- This captures coverage from actual CLI usage—the most realistic testing possible.
212
-
213
- ## Comparison with trycmd
214
-
215
- tryscript is a TypeScript port of the Rust [trycmd](https://github.com/assert-rs/trycmd) crate. Key differences:
216
-
217
- - **Language**: TypeScript/Node.js instead of Rust
218
- - **Format**: Uses console code blocks (trycmd uses `.toml` or `.trycmd` files)
219
- - **Integration**: Works with Node.js test frameworks (Vitest, Jest)
220
-
221
71
  ## License
222
72
 
223
73
  MIT
package/dist/bin.cjs CHANGED
@@ -1,21 +1,85 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
 
4
- const require_src = require('./src-CeUA446P.cjs');
4
+ const require_src = require('./src-CP4-Q-U5.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
8
  let node_fs_promises = require("node:fs/promises");
9
9
  let commander = require("commander");
10
- let picocolors = require("picocolors");
11
- picocolors = require_src.__toESM(picocolors);
12
10
  let fast_glob = require("fast-glob");
13
11
  fast_glob = require_src.__toESM(fast_glob);
12
+ let picocolors = require("picocolors");
13
+ picocolors = require_src.__toESM(picocolors);
14
14
  let diff = require("diff");
15
15
  let atomically = require("atomically");
16
16
 
17
+ //#region src/cli/lib/shared.ts
18
+ /**
19
+ * Shared color utilities for consistent terminal output.
20
+ */
21
+ const colors = {
22
+ success: (s) => picocolors.default.green(s),
23
+ error: (s) => picocolors.default.red(s),
24
+ info: (s) => picocolors.default.cyan(s),
25
+ warn: (s) => picocolors.default.yellow(s),
26
+ muted: (s) => picocolors.default.gray(s),
27
+ bold: (s) => picocolors.default.bold(s)
28
+ };
29
+ /**
30
+ * Status indicators with emoji.
31
+ */
32
+ const status = {
33
+ pass: picocolors.default.green("✓"),
34
+ fail: picocolors.default.red("✗"),
35
+ skip: picocolors.default.yellow("○"),
36
+ update: picocolors.default.yellow("↻")
37
+ };
38
+ /**
39
+ * Configure Commander.js with colored help text.
40
+ * Applies consistent styling: cyan titles, green commands, yellow options.
41
+ */
42
+ function withColoredHelp(cmd) {
43
+ cmd.configureHelp({
44
+ styleTitle: (str) => picocolors.default.bold(picocolors.default.cyan(str)),
45
+ styleCommandText: (str) => picocolors.default.green(str),
46
+ styleOptionText: (str) => picocolors.default.yellow(str),
47
+ showGlobalOptions: true
48
+ });
49
+ return cmd;
50
+ }
51
+ /**
52
+ * Log a warning message to stderr.
53
+ */
54
+ function logWarn(message) {
55
+ console.error(colors.warn(message));
56
+ }
57
+ /**
58
+ * Log an error message to stderr.
59
+ */
60
+ function logError(message) {
61
+ console.error(colors.error(message));
62
+ }
63
+
64
+ //#endregion
17
65
  //#region src/lib/reporter.ts
18
66
  /**
67
+ * Test result reporting utilities.
68
+ *
69
+ * Handles output formatting for test results, diffs, and summaries.
70
+ */
71
+ const statusIcon = {
72
+ pass: picocolors.default.green("✓"),
73
+ fail: picocolors.default.red("✗")
74
+ };
75
+ /**
76
+ * Format a duration in milliseconds for display.
77
+ */
78
+ function formatDuration(ms) {
79
+ if (ms < 1e3) return `${ms}ms`;
80
+ return `${(ms / 1e3).toFixed(2)}s`;
81
+ }
82
+ /**
19
83
  * Create a unified diff between expected and actual output.
20
84
  */
21
85
  function createDiff(expected, actual, filename) {
@@ -27,26 +91,19 @@ function createDiff(expected, actual, filename) {
27
91
  }).join("\n");
28
92
  }
29
93
  /**
30
- * Format a duration in milliseconds for display.
31
- */
32
- function formatDuration(ms) {
33
- if (ms < 1e3) return `${ms}ms`;
34
- return `${(ms / 1e3).toFixed(2)}s`;
35
- }
36
- /**
37
94
  * Report results for a single file.
38
95
  */
39
96
  function reportFile(result, options) {
40
97
  const filename = result.file.path;
41
- const status = result.passed ? picocolors.default.green(picocolors.default.bold("PASS")) : picocolors.default.red(picocolors.default.bold("FAIL"));
98
+ const status$1 = result.passed ? picocolors.default.green(picocolors.default.bold("PASS")) : picocolors.default.red(picocolors.default.bold("FAIL"));
42
99
  if (options.quiet && result.passed) return;
43
- console.error(`${status} ${filename}`);
100
+ console.error(`${status$1} ${filename}`);
44
101
  for (const blockResult of result.results) {
45
102
  const name = blockResult.block.name ?? `Line ${blockResult.block.lineNumber}`;
46
103
  if (blockResult.passed) {
47
- if (!options.quiet) console.error(` ${picocolors.default.green("✓")} ${name}`);
104
+ if (!options.quiet) console.error(` ${statusIcon.pass} ${name}`);
48
105
  } else {
49
- console.error(` ${picocolors.default.red("✗")} ${name}`);
106
+ console.error(` ${statusIcon.fail} ${name}`);
50
107
  if (blockResult.error) console.error(` ${picocolors.default.red(blockResult.error)}`);
51
108
  else {
52
109
  if (blockResult.actualExitCode !== blockResult.block.expectedExitCode) console.error(` Expected exit code ${blockResult.block.expectedExitCode}, got ${blockResult.actualExitCode}`);
@@ -115,6 +172,12 @@ function buildUpdatedBlock(block, result) {
115
172
 
116
173
  //#endregion
117
174
  //#region src/cli/commands/run.ts
175
+ /**
176
+ * Register the run command.
177
+ */
178
+ 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);
180
+ }
118
181
  async function runCommand(files, options) {
119
182
  const startTime = Date.now();
120
183
  const opts = {
@@ -131,7 +194,7 @@ async function runCommand(files, options) {
131
194
  dot: false
132
195
  });
133
196
  if (testFiles.length === 0) {
134
- console.error(picocolors.default.yellow("No test files found"));
197
+ logWarn("No test files found");
135
198
  process.exit(1);
136
199
  }
137
200
  const globalConfig = await require_src.loadConfig(process.cwd());
@@ -146,18 +209,29 @@ async function runCommand(files, options) {
146
209
  const filterPattern = new RegExp(opts.filter, "i");
147
210
  blocksToRun = blocksToRun.filter((b) => b.name ? filterPattern.test(b.name) : true);
148
211
  }
212
+ const onlyBlocks = blocksToRun.filter((b) => b.only);
213
+ if (onlyBlocks.length > 0) blocksToRun = onlyBlocks;
149
214
  if (blocksToRun.length === 0) continue;
150
215
  const ctx = await require_src.createExecutionContext(config, filePath);
151
216
  const results = [];
152
217
  try {
153
218
  for (const block of blocksToRun) {
154
219
  const result = await require_src.runBlock(block, ctx);
155
- const matches = require_src.matchOutput(result.actualOutput, block.expectedOutput, {
156
- root: ctx.tempDir,
157
- cwd: ctx.tempDir
220
+ if (result.skipped) {
221
+ results.push(result);
222
+ continue;
223
+ }
224
+ const outputMatches = require_src.matchOutput(block.expectedStderr ? result.actualStdout ?? "" : result.actualOutput, block.expectedOutput, {
225
+ root: ctx.testDir,
226
+ cwd: ctx.cwd
227
+ }, config.patterns ?? {});
228
+ let stderrMatches = true;
229
+ if (block.expectedStderr) stderrMatches = require_src.matchOutput(result.actualStderr ?? "", block.expectedStderr, {
230
+ root: ctx.testDir,
231
+ cwd: ctx.cwd
158
232
  }, config.patterns ?? {});
159
233
  const exitCodeMatches = result.actualExitCode === block.expectedExitCode;
160
- result.passed = matches && exitCodeMatches && !result.error;
234
+ result.passed = outputMatches && stderrMatches && exitCodeMatches && !result.error;
161
235
  if (!result.passed && opts.diff) result.diff = createDiff(block.expectedOutput, result.actualOutput, `${filePath}:${block.lineNumber}`);
162
236
  results.push(result);
163
237
  if (!result.passed && opts.failFast) {
@@ -165,6 +239,7 @@ async function runCommand(files, options) {
165
239
  break;
166
240
  }
167
241
  }
242
+ await require_src.runAfterHook(ctx);
168
243
  } finally {
169
244
  await require_src.cleanupExecutionContext(ctx);
170
245
  }
@@ -178,7 +253,7 @@ async function runCommand(files, options) {
178
253
  reportFile(fileResult, opts);
179
254
  if (opts.update && !fileResult.passed) {
180
255
  const { updated, changes } = await updateTestFile(testFile, results);
181
- if (updated) console.error(picocolors.default.yellow(` Updated: ${changes.join(", ")}`));
256
+ if (updated) console.error(colors.warn(` ${status.update} Updated: ${changes.join(", ")}`));
182
257
  }
183
258
  }
184
259
  const summary = {
@@ -266,19 +341,24 @@ function isInteractive$1() {
266
341
  return process.stdout.isTTY === true;
267
342
  }
268
343
  /**
344
+ * Display the README content.
345
+ * Exported for use as the default command.
346
+ */
347
+ function showReadme(options) {
348
+ try {
349
+ const formatted = formatMarkdown$1(loadReadme(), !options?.raw && isInteractive$1());
350
+ console.log(formatted);
351
+ } catch (error) {
352
+ const message = error instanceof Error ? error.message : String(error);
353
+ console.error(picocolors.default.red(`Error: ${message}`));
354
+ process.exit(1);
355
+ }
356
+ }
357
+ /**
269
358
  * Register the readme command.
270
359
  */
271
360
  function registerReadmeCommand(program) {
272
- program.command("readme").description("Display README documentation").option("--raw", "Output raw markdown without formatting").action((options) => {
273
- try {
274
- const formatted = formatMarkdown$1(loadReadme(), !options.raw && isInteractive$1());
275
- console.log(formatted);
276
- } catch (error) {
277
- const message = error instanceof Error ? error.message : String(error);
278
- console.error(picocolors.default.red(`Error: ${message}`));
279
- process.exit(1);
280
- }
281
- });
361
+ program.command("readme").description("Display README documentation").option("--raw", "Output raw markdown without formatting").action(showReadme);
282
362
  }
283
363
 
284
364
  //#endregion
@@ -372,12 +452,21 @@ function registerDocsCommand(program) {
372
452
 
373
453
  //#endregion
374
454
  //#region src/cli/cli.ts
455
+ /**
456
+ * CLI entry point for tryscript.
457
+ *
458
+ * Configures Commander.js with colored help and registers all subcommands.
459
+ */
375
460
  function run(argv) {
376
- const program = new commander.Command().name("tryscript").version(require_src.VERSION, "--version", "Show version number").description("Golden testing for CLI applications").showHelpAfterError("(use --help for usage)").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);
461
+ 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
+ registerRunCommand(program);
377
463
  registerReadmeCommand(program);
378
464
  registerDocsCommand(program);
465
+ program.action(() => {
466
+ program.help();
467
+ });
379
468
  program.parseAsync(argv).catch((err) => {
380
- console.error(picocolors.default.red(`Error: ${err.message}`));
469
+ logError(`Error: ${err.message}`);
381
470
  process.exit(2);
382
471
  });
383
472
  }