tryscript 0.0.1
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 +223 -0
- package/dist/bin.cjs +390 -0
- package/dist/bin.cjs.map +1 -0
- package/dist/bin.d.cts +2 -0
- package/dist/bin.d.mts +2 -0
- package/dist/bin.mjs +389 -0
- package/dist/bin.mjs.map +1 -0
- package/dist/index.cjs +11 -0
- package/dist/index.d.cts +165 -0
- package/dist/index.d.mts +165 -0
- package/dist/index.mjs +4 -0
- package/dist/src-CeUA446P.cjs +422 -0
- package/dist/src-CeUA446P.cjs.map +1 -0
- package/dist/src-UjaSQrqA.mjs +328 -0
- package/dist/src-UjaSQrqA.mjs.map +1 -0
- package/docs/tryscript-reference.md +163 -0
- package/package.json +76 -0
package/README.md
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# tryscript
|
|
2
|
+
|
|
3
|
+
Golden testing for CLI applications - a TypeScript port of [trycmd](https://github.com/assert-rs/trycmd).
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- **Node.js 20+** (tested with v20, v22, v24)
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
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
|
|
59
|
+
$ echo "hello world"
|
|
60
|
+
hello world
|
|
61
|
+
? 0
|
|
62
|
+
\`\`\`
|
|
63
|
+
|
|
64
|
+
# Test: Exit with error
|
|
65
|
+
|
|
66
|
+
\`\`\`console
|
|
67
|
+
$ exit 1
|
|
68
|
+
? 1
|
|
69
|
+
\`\`\`
|
|
70
|
+
```
|
|
71
|
+
|
|
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` |
|
|
83
|
+
|
|
84
|
+
### Example with Elision
|
|
85
|
+
|
|
86
|
+
```markdown
|
|
87
|
+
\`\`\`console
|
|
88
|
+
$ time-command
|
|
89
|
+
Elapsed: [..]ms
|
|
90
|
+
? 0
|
|
91
|
+
\`\`\`
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Configuration
|
|
95
|
+
|
|
96
|
+
### YAML Frontmatter
|
|
97
|
+
|
|
98
|
+
Add configuration at the top of your test file:
|
|
99
|
+
|
|
100
|
+
```markdown
|
|
101
|
+
---
|
|
102
|
+
bin: ./my-cli
|
|
103
|
+
env:
|
|
104
|
+
NO_COLOR: "1"
|
|
105
|
+
timeout: 5000
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
# Test: Custom binary
|
|
109
|
+
|
|
110
|
+
\`\`\`console
|
|
111
|
+
$ my-cli --version
|
|
112
|
+
1.0.0
|
|
113
|
+
? 0
|
|
114
|
+
\`\`\`
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Config File
|
|
118
|
+
|
|
119
|
+
Create `tryscript.config.ts` in your project root:
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
import { defineConfig } from 'tryscript';
|
|
123
|
+
|
|
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
|
+
```
|
|
136
|
+
|
|
137
|
+
## CLI Options
|
|
138
|
+
|
|
139
|
+
```
|
|
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
|
+
|
|
159
|
+
When your CLI output changes, update all test files at once:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
npx tryscript --update
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
This rewrites test files with the actual output from running the commands.
|
|
166
|
+
|
|
167
|
+
## Programmatic API
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
import { parseTestFile, runBlock, createExecutionContext, matchOutput } from 'tryscript';
|
|
171
|
+
|
|
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
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
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
|
+
## License
|
|
222
|
+
|
|
223
|
+
MIT
|
package/dist/bin.cjs
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
const require_src = require('./src-CeUA446P.cjs');
|
|
5
|
+
let node_url = require("node:url");
|
|
6
|
+
let node_fs = require("node:fs");
|
|
7
|
+
let node_path = require("node:path");
|
|
8
|
+
let node_fs_promises = require("node:fs/promises");
|
|
9
|
+
let commander = require("commander");
|
|
10
|
+
let picocolors = require("picocolors");
|
|
11
|
+
picocolors = require_src.__toESM(picocolors);
|
|
12
|
+
let fast_glob = require("fast-glob");
|
|
13
|
+
fast_glob = require_src.__toESM(fast_glob);
|
|
14
|
+
let diff = require("diff");
|
|
15
|
+
let atomically = require("atomically");
|
|
16
|
+
|
|
17
|
+
//#region src/lib/reporter.ts
|
|
18
|
+
/**
|
|
19
|
+
* Create a unified diff between expected and actual output.
|
|
20
|
+
*/
|
|
21
|
+
function createDiff(expected, actual, filename) {
|
|
22
|
+
return (0, diff.createPatch)(filename, expected, actual, "expected", "actual").split("\n").slice(4).map((line) => {
|
|
23
|
+
if (line.startsWith("+")) return picocolors.default.green(line);
|
|
24
|
+
if (line.startsWith("-")) return picocolors.default.red(line);
|
|
25
|
+
if (line.startsWith("@")) return picocolors.default.cyan(line);
|
|
26
|
+
return line;
|
|
27
|
+
}).join("\n");
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
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
|
+
* Report results for a single file.
|
|
38
|
+
*/
|
|
39
|
+
function reportFile(result, options) {
|
|
40
|
+
const filename = result.file.path;
|
|
41
|
+
const status = result.passed ? picocolors.default.green(picocolors.default.bold("PASS")) : picocolors.default.red(picocolors.default.bold("FAIL"));
|
|
42
|
+
if (options.quiet && result.passed) return;
|
|
43
|
+
console.error(`${status} ${filename}`);
|
|
44
|
+
for (const blockResult of result.results) {
|
|
45
|
+
const name = blockResult.block.name ?? `Line ${blockResult.block.lineNumber}`;
|
|
46
|
+
if (blockResult.passed) {
|
|
47
|
+
if (!options.quiet) console.error(` ${picocolors.default.green("✓")} ${name}`);
|
|
48
|
+
} else {
|
|
49
|
+
console.error(` ${picocolors.default.red("✗")} ${name}`);
|
|
50
|
+
if (blockResult.error) console.error(` ${picocolors.default.red(blockResult.error)}`);
|
|
51
|
+
else {
|
|
52
|
+
if (blockResult.actualExitCode !== blockResult.block.expectedExitCode) console.error(` Expected exit code ${blockResult.block.expectedExitCode}, got ${blockResult.actualExitCode}`);
|
|
53
|
+
if (options.diff && blockResult.diff) {
|
|
54
|
+
console.error("");
|
|
55
|
+
console.error(blockResult.diff);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
console.error("");
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Report final summary.
|
|
64
|
+
*/
|
|
65
|
+
function reportSummary(summary, _options) {
|
|
66
|
+
const parts = [];
|
|
67
|
+
if (summary.totalPassed > 0) parts.push(picocolors.default.green(`${summary.totalPassed} passed`));
|
|
68
|
+
if (summary.totalFailed > 0) parts.push(picocolors.default.red(`${summary.totalFailed} failed`));
|
|
69
|
+
const duration = formatDuration(summary.duration);
|
|
70
|
+
const line = `${parts.join(", ")} (${duration})`;
|
|
71
|
+
console.log(line);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region src/lib/updater.ts
|
|
76
|
+
/**
|
|
77
|
+
* Update a test file with actual output from test results.
|
|
78
|
+
*/
|
|
79
|
+
async function updateTestFile(file, results) {
|
|
80
|
+
let content = file.rawContent;
|
|
81
|
+
const changes = [];
|
|
82
|
+
const blocksWithResults = file.blocks.map((block, i) => ({
|
|
83
|
+
block,
|
|
84
|
+
result: results[i]
|
|
85
|
+
})).reverse();
|
|
86
|
+
for (const { block, result } of blocksWithResults) {
|
|
87
|
+
if (!result) continue;
|
|
88
|
+
if (result.passed) continue;
|
|
89
|
+
if (result.error) continue;
|
|
90
|
+
const newBlockContent = buildUpdatedBlock(block, result);
|
|
91
|
+
const blockStart = content.indexOf(block.rawContent);
|
|
92
|
+
if (blockStart !== -1) {
|
|
93
|
+
content = content.slice(0, blockStart) + newBlockContent + content.slice(blockStart + block.rawContent.length);
|
|
94
|
+
changes.push(block.name ?? `Line ${block.lineNumber}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (changes.length > 0) await (0, atomically.writeFile)(file.path, content);
|
|
98
|
+
return {
|
|
99
|
+
updated: changes.length > 0,
|
|
100
|
+
changes
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Build an updated console block with new expected output.
|
|
105
|
+
*/
|
|
106
|
+
function buildUpdatedBlock(block, result) {
|
|
107
|
+
const lines = ["```console", ...block.command.split("\n").map((line, i) => {
|
|
108
|
+
return i === 0 ? `$ ${line}` : `> ${line}`;
|
|
109
|
+
})];
|
|
110
|
+
const trimmedOutput = result.actualOutput.trimEnd();
|
|
111
|
+
if (trimmedOutput) lines.push(trimmedOutput);
|
|
112
|
+
lines.push(`? ${result.actualExitCode}`, "```");
|
|
113
|
+
return lines.join("\n");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
//#endregion
|
|
117
|
+
//#region src/cli/commands/run.ts
|
|
118
|
+
async function runCommand(files, options) {
|
|
119
|
+
const startTime = Date.now();
|
|
120
|
+
const opts = {
|
|
121
|
+
diff: options.diff !== false,
|
|
122
|
+
verbose: options.verbose ?? false,
|
|
123
|
+
quiet: options.quiet ?? false,
|
|
124
|
+
update: options.update ?? false,
|
|
125
|
+
failFast: options.failFast ?? false,
|
|
126
|
+
filter: options.filter
|
|
127
|
+
};
|
|
128
|
+
const testFiles = await (0, fast_glob.default)(files.length > 0 ? files : ["**/*.tryscript.md"], {
|
|
129
|
+
ignore: ["**/node_modules/**", "**/dist/**"],
|
|
130
|
+
absolute: true,
|
|
131
|
+
dot: false
|
|
132
|
+
});
|
|
133
|
+
if (testFiles.length === 0) {
|
|
134
|
+
console.error(picocolors.default.yellow("No test files found"));
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
const globalConfig = await require_src.loadConfig(process.cwd());
|
|
138
|
+
const fileResults = [];
|
|
139
|
+
let shouldStop = false;
|
|
140
|
+
for (const filePath of testFiles) {
|
|
141
|
+
if (shouldStop) break;
|
|
142
|
+
const testFile = require_src.parseTestFile(await (0, node_fs_promises.readFile)(filePath, "utf-8"), filePath);
|
|
143
|
+
const config = require_src.mergeConfig(globalConfig, testFile.config);
|
|
144
|
+
let blocksToRun = testFile.blocks;
|
|
145
|
+
if (opts.filter) {
|
|
146
|
+
const filterPattern = new RegExp(opts.filter, "i");
|
|
147
|
+
blocksToRun = blocksToRun.filter((b) => b.name ? filterPattern.test(b.name) : true);
|
|
148
|
+
}
|
|
149
|
+
if (blocksToRun.length === 0) continue;
|
|
150
|
+
const ctx = await require_src.createExecutionContext(config, filePath);
|
|
151
|
+
const results = [];
|
|
152
|
+
try {
|
|
153
|
+
for (const block of blocksToRun) {
|
|
154
|
+
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
|
|
158
|
+
}, config.patterns ?? {});
|
|
159
|
+
const exitCodeMatches = result.actualExitCode === block.expectedExitCode;
|
|
160
|
+
result.passed = matches && exitCodeMatches && !result.error;
|
|
161
|
+
if (!result.passed && opts.diff) result.diff = createDiff(block.expectedOutput, result.actualOutput, `${filePath}:${block.lineNumber}`);
|
|
162
|
+
results.push(result);
|
|
163
|
+
if (!result.passed && opts.failFast) {
|
|
164
|
+
shouldStop = true;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
} finally {
|
|
169
|
+
await require_src.cleanupExecutionContext(ctx);
|
|
170
|
+
}
|
|
171
|
+
const fileResult = {
|
|
172
|
+
file: testFile,
|
|
173
|
+
results,
|
|
174
|
+
passed: results.every((r) => r.passed),
|
|
175
|
+
duration: results.reduce((sum, r) => sum + r.duration, 0)
|
|
176
|
+
};
|
|
177
|
+
fileResults.push(fileResult);
|
|
178
|
+
reportFile(fileResult, opts);
|
|
179
|
+
if (opts.update && !fileResult.passed) {
|
|
180
|
+
const { updated, changes } = await updateTestFile(testFile, results);
|
|
181
|
+
if (updated) console.error(picocolors.default.yellow(` ↻ Updated: ${changes.join(", ")}`));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const summary = {
|
|
185
|
+
files: fileResults,
|
|
186
|
+
totalPassed: fileResults.reduce((sum, f) => sum + f.results.filter((r) => r.passed).length, 0),
|
|
187
|
+
totalFailed: fileResults.reduce((sum, f) => sum + f.results.filter((r) => !r.passed).length, 0),
|
|
188
|
+
totalBlocks: fileResults.reduce((sum, f) => sum + f.results.length, 0),
|
|
189
|
+
duration: Date.now() - startTime
|
|
190
|
+
};
|
|
191
|
+
reportSummary(summary, opts);
|
|
192
|
+
process.exit(summary.totalFailed > 0 ? 1 : 0);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
//#endregion
|
|
196
|
+
//#region src/cli/commands/readme.ts
|
|
197
|
+
/**
|
|
198
|
+
* Get the path to the README.md file.
|
|
199
|
+
* Works both during development and when installed as a package.
|
|
200
|
+
*/
|
|
201
|
+
function getReadmePath() {
|
|
202
|
+
const thisDir = (0, node_path.dirname)((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
|
|
203
|
+
if (thisDir.split(/[/\\]/).pop() === "dist") return (0, node_path.join)((0, node_path.dirname)(thisDir), "README.md");
|
|
204
|
+
return (0, node_path.join)((0, node_path.dirname)((0, node_path.dirname)((0, node_path.dirname)(thisDir))), "README.md");
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Load the README content.
|
|
208
|
+
*/
|
|
209
|
+
function loadReadme() {
|
|
210
|
+
const readmePath = getReadmePath();
|
|
211
|
+
try {
|
|
212
|
+
return (0, node_fs.readFileSync)(readmePath, "utf-8");
|
|
213
|
+
} catch (error) {
|
|
214
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
215
|
+
throw new Error(`Failed to load README from ${readmePath}: ${message}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Apply basic terminal formatting to markdown content.
|
|
220
|
+
* Colorizes headers, code blocks, and other elements for better readability.
|
|
221
|
+
*/
|
|
222
|
+
function formatMarkdown$1(content, useColors) {
|
|
223
|
+
if (!useColors) return content;
|
|
224
|
+
const lines = content.split("\n");
|
|
225
|
+
const formatted = [];
|
|
226
|
+
let inCodeBlock = false;
|
|
227
|
+
for (const line of lines) {
|
|
228
|
+
if (line.startsWith("```")) {
|
|
229
|
+
inCodeBlock = !inCodeBlock;
|
|
230
|
+
formatted.push(picocolors.default.dim(line));
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (inCodeBlock) {
|
|
234
|
+
formatted.push(picocolors.default.dim(line));
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (line.startsWith("# ")) {
|
|
238
|
+
formatted.push(picocolors.default.bold(picocolors.default.cyan(line)));
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (line.startsWith("## ")) {
|
|
242
|
+
formatted.push(picocolors.default.bold(picocolors.default.blue(line)));
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (line.startsWith("### ")) {
|
|
246
|
+
formatted.push(picocolors.default.bold(line));
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
let formattedLine = line.replace(/`([^`]+)`/g, (_match, code) => {
|
|
250
|
+
return picocolors.default.yellow(code);
|
|
251
|
+
});
|
|
252
|
+
formattedLine = formattedLine.replace(/\*\*([^*]+)\*\*/g, (_match, text) => {
|
|
253
|
+
return picocolors.default.bold(text);
|
|
254
|
+
});
|
|
255
|
+
formattedLine = formattedLine.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, text, url) => {
|
|
256
|
+
return `${picocolors.default.cyan(text)} ${picocolors.default.dim(`(${url})`)}`;
|
|
257
|
+
});
|
|
258
|
+
formatted.push(formattedLine);
|
|
259
|
+
}
|
|
260
|
+
return formatted.join("\n");
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Check if stdout is an interactive terminal.
|
|
264
|
+
*/
|
|
265
|
+
function isInteractive$1() {
|
|
266
|
+
return process.stdout.isTTY === true;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Register the readme command.
|
|
270
|
+
*/
|
|
271
|
+
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
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
//#endregion
|
|
285
|
+
//#region src/cli/commands/docs.ts
|
|
286
|
+
/**
|
|
287
|
+
* Get the path to the tryscript-reference.md file.
|
|
288
|
+
* Works both during development and when installed as a package.
|
|
289
|
+
*/
|
|
290
|
+
function getDocsPath() {
|
|
291
|
+
const thisDir = (0, node_path.dirname)((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
|
|
292
|
+
if (thisDir.split(/[/\\]/).pop() === "dist") return (0, node_path.join)((0, node_path.dirname)(thisDir), "docs", "tryscript-reference.md");
|
|
293
|
+
return (0, node_path.join)((0, node_path.dirname)((0, node_path.dirname)((0, node_path.dirname)(thisDir))), "docs", "tryscript-reference.md");
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Load the docs content.
|
|
297
|
+
*/
|
|
298
|
+
function loadDocs() {
|
|
299
|
+
const docsPath = getDocsPath();
|
|
300
|
+
try {
|
|
301
|
+
return (0, node_fs.readFileSync)(docsPath, "utf-8");
|
|
302
|
+
} catch (error) {
|
|
303
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
304
|
+
throw new Error(`Failed to load reference docs from ${docsPath}: ${message}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Apply basic terminal formatting to markdown content.
|
|
309
|
+
* Colorizes headers, code blocks, and other elements for better readability.
|
|
310
|
+
*/
|
|
311
|
+
function formatMarkdown(content, useColors) {
|
|
312
|
+
if (!useColors) return content;
|
|
313
|
+
const lines = content.split("\n");
|
|
314
|
+
const formatted = [];
|
|
315
|
+
let inCodeBlock = false;
|
|
316
|
+
for (const line of lines) {
|
|
317
|
+
if (line.startsWith("```")) {
|
|
318
|
+
inCodeBlock = !inCodeBlock;
|
|
319
|
+
formatted.push(picocolors.default.dim(line));
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
if (inCodeBlock) {
|
|
323
|
+
formatted.push(picocolors.default.dim(line));
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (line.startsWith("# ")) {
|
|
327
|
+
formatted.push(picocolors.default.bold(picocolors.default.cyan(line)));
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (line.startsWith("## ")) {
|
|
331
|
+
formatted.push(picocolors.default.bold(picocolors.default.blue(line)));
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if (line.startsWith("### ")) {
|
|
335
|
+
formatted.push(picocolors.default.bold(line));
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
let formattedLine = line.replace(/`([^`]+)`/g, (_match, code) => {
|
|
339
|
+
return picocolors.default.yellow(code);
|
|
340
|
+
});
|
|
341
|
+
formattedLine = formattedLine.replace(/\*\*([^*]+)\*\*/g, (_match, text) => {
|
|
342
|
+
return picocolors.default.bold(text);
|
|
343
|
+
});
|
|
344
|
+
formattedLine = formattedLine.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, text, url) => {
|
|
345
|
+
return `${picocolors.default.cyan(text)} ${picocolors.default.dim(`(${url})`)}`;
|
|
346
|
+
});
|
|
347
|
+
formatted.push(formattedLine);
|
|
348
|
+
}
|
|
349
|
+
return formatted.join("\n");
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Check if stdout is an interactive terminal.
|
|
353
|
+
*/
|
|
354
|
+
function isInteractive() {
|
|
355
|
+
return process.stdout.isTTY === true;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Register the docs command.
|
|
359
|
+
*/
|
|
360
|
+
function registerDocsCommand(program) {
|
|
361
|
+
program.command("docs").description("Display concise syntax reference").option("--raw", "Output raw markdown without formatting").action((options) => {
|
|
362
|
+
try {
|
|
363
|
+
const formatted = formatMarkdown(loadDocs(), !options.raw && isInteractive());
|
|
364
|
+
console.log(formatted);
|
|
365
|
+
} catch (error) {
|
|
366
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
367
|
+
console.error(picocolors.default.red(`Error: ${message}`));
|
|
368
|
+
process.exit(1);
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
//#endregion
|
|
374
|
+
//#region src/cli/cli.ts
|
|
375
|
+
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);
|
|
377
|
+
registerReadmeCommand(program);
|
|
378
|
+
registerDocsCommand(program);
|
|
379
|
+
program.parseAsync(argv).catch((err) => {
|
|
380
|
+
console.error(picocolors.default.red(`Error: ${err.message}`));
|
|
381
|
+
process.exit(2);
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
//#endregion
|
|
386
|
+
//#region src/bin.ts
|
|
387
|
+
run(process.argv);
|
|
388
|
+
|
|
389
|
+
//#endregion
|
|
390
|
+
//# sourceMappingURL=bin.cjs.map
|
package/dist/bin.cjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bin.cjs","names":["pc","parts: string[]","changes: string[]","lines: string[]","pc","loadConfig","fileResults: TestFileResult[]","parseTestFile","mergeConfig","createExecutionContext","results: TestBlockResult[]","runBlock","matchOutput","cleanupExecutionContext","fileResult: TestFileResult","summary: TestRunSummary","formatMarkdown","formatted: string[]","pc","isInteractive","formatted: string[]","pc","Command","VERSION","pc"],"sources":["../src/lib/reporter.ts","../src/lib/updater.ts","../src/cli/commands/run.ts","../src/cli/commands/readme.ts","../src/cli/commands/docs.ts","../src/cli/cli.ts","../src/bin.ts"],"sourcesContent":["import pc from 'picocolors';\nimport { createPatch } from 'diff';\nimport type { TestFileResult, TestRunSummary } from './types.js';\n\nexport interface ReporterOptions {\n diff: boolean;\n verbose: boolean;\n quiet: boolean;\n}\n\n/**\n * Create a unified diff between expected and actual output.\n */\nexport function createDiff(expected: string, actual: string, filename: string): string {\n const patch = createPatch(filename, expected, actual, 'expected', 'actual');\n // Remove the header lines (first 4 lines)\n const lines = patch.split('\\n').slice(4);\n return lines\n .map((line) => {\n if (line.startsWith('+')) {\n return pc.green(line);\n }\n if (line.startsWith('-')) {\n return pc.red(line);\n }\n if (line.startsWith('@')) {\n return pc.cyan(line);\n }\n return line;\n })\n .join('\\n');\n}\n\n/**\n * Format a duration in milliseconds for display.\n */\nfunction formatDuration(ms: number): string {\n if (ms < 1000) {\n return `${ms}ms`;\n }\n return `${(ms / 1000).toFixed(2)}s`;\n}\n\n/**\n * Report results for a single file.\n */\nexport function reportFile(result: TestFileResult, options: ReporterOptions): void {\n const filename = result.file.path;\n const status = result.passed ? pc.green(pc.bold('PASS')) : pc.red(pc.bold('FAIL'));\n\n if (options.quiet && result.passed) {\n return;\n }\n\n // File header\n console.error(`${status} ${filename}`);\n\n // Individual block results\n for (const blockResult of result.results) {\n const name = blockResult.block.name ?? `Line ${blockResult.block.lineNumber}`;\n\n if (blockResult.passed) {\n if (!options.quiet) {\n console.error(` ${pc.green('✓')} ${name}`);\n }\n } else {\n console.error(` ${pc.red('✗')} ${name}`);\n\n // Show error details\n if (blockResult.error) {\n console.error(` ${pc.red(blockResult.error)}`);\n } else {\n // Exit code mismatch\n if (blockResult.actualExitCode !== blockResult.block.expectedExitCode) {\n console.error(\n ` Expected exit code ${blockResult.block.expectedExitCode}, got ${blockResult.actualExitCode}`,\n );\n }\n\n // Output mismatch with diff\n if (options.diff && blockResult.diff) {\n console.error('');\n console.error(blockResult.diff);\n }\n }\n }\n }\n\n console.error('');\n}\n\n/**\n * Report final summary.\n */\nexport function reportSummary(summary: TestRunSummary, _options: ReporterOptions): void {\n const parts: string[] = [];\n\n if (summary.totalPassed > 0) {\n parts.push(pc.green(`${summary.totalPassed} passed`));\n }\n if (summary.totalFailed > 0) {\n parts.push(pc.red(`${summary.totalFailed} failed`));\n }\n\n const duration = formatDuration(summary.duration);\n const line = `${parts.join(', ')} (${duration})`;\n\n // Summary goes to stdout (can be piped/parsed)\n console.log(line);\n}\n","import { writeFile } from 'atomically';\nimport type { TestFile, TestBlock, TestBlockResult } from './types.js';\n\n/**\n * Update a test file with actual output from test results.\n */\nexport async function updateTestFile(\n file: TestFile,\n results: TestBlockResult[],\n): Promise<{ updated: boolean; changes: string[] }> {\n let content = file.rawContent;\n const changes: string[] = [];\n\n // Process blocks in reverse order to maintain correct offsets\n const blocksWithResults = file.blocks\n .map((block, i) => ({ block, result: results[i] }))\n .reverse();\n\n for (const { block, result } of blocksWithResults) {\n if (!result) {\n continue;\n }\n\n if (result.passed) {\n continue; // Don't touch passing tests\n }\n\n if (result.error) {\n // Execution error, can't update\n continue;\n }\n\n // Build the new block content\n const newBlockContent = buildUpdatedBlock(block, result);\n\n // Find and replace the block in the file\n const blockStart = content.indexOf(block.rawContent);\n if (blockStart !== -1) {\n content =\n content.slice(0, blockStart) +\n newBlockContent +\n content.slice(blockStart + block.rawContent.length);\n\n changes.push(block.name ?? `Line ${block.lineNumber}`);\n }\n }\n\n if (changes.length > 0) {\n await writeFile(file.path, content);\n }\n\n return { updated: changes.length > 0, changes };\n}\n\n/**\n * Build an updated console block with new expected output.\n */\nfunction buildUpdatedBlock(block: TestBlock, result: TestBlockResult): string {\n // Reconstruct the command line(s)\n const commandLines = block.command.split('\\n').map((line, i) => {\n return i === 0 ? `$ ${line}` : `> ${line}`;\n });\n\n // Build the block\n const lines: string[] = ['```console', ...commandLines];\n\n // Add output if present\n const trimmedOutput = result.actualOutput.trimEnd();\n if (trimmedOutput) {\n lines.push(trimmedOutput);\n }\n\n // Add exit code\n lines.push(`? ${result.actualExitCode}`, '```');\n\n return lines.join('\\n');\n}\n","import { readFile } from 'node:fs/promises';\nimport fg from 'fast-glob';\nimport pc from 'picocolors';\nimport { loadConfig, mergeConfig } from '../../lib/config.js';\nimport { parseTestFile } from '../../lib/parser.js';\nimport { runBlock, createExecutionContext, cleanupExecutionContext } from '../../lib/runner.js';\nimport { matchOutput } from '../../lib/matcher.js';\nimport { createDiff, reportFile, reportSummary } from '../../lib/reporter.js';\nimport { updateTestFile } from '../../lib/updater.js';\nimport type { TestBlockResult, TestFileResult, TestRunSummary } from '../../lib/types.js';\n\ninterface RunOptions {\n update?: boolean;\n diff?: boolean;\n failFast?: boolean;\n filter?: string;\n verbose?: boolean;\n quiet?: boolean;\n}\n\nexport async function runCommand(files: string[], options: RunOptions): Promise<void> {\n const startTime = Date.now();\n\n // Default options\n const opts = {\n diff: options.diff !== false,\n verbose: options.verbose ?? false,\n quiet: options.quiet ?? false,\n update: options.update ?? false,\n failFast: options.failFast ?? false,\n filter: options.filter,\n };\n\n // Find test files (fast-glob respects .gitignore by default)\n const patterns = files.length > 0 ? files : ['**/*.tryscript.md'];\n const testFiles = await fg(patterns, {\n ignore: ['**/node_modules/**', '**/dist/**'],\n absolute: true,\n dot: false,\n });\n\n if (testFiles.length === 0) {\n console.error(pc.yellow('No test files found'));\n process.exit(1);\n }\n\n // Load global config\n const globalConfig = await loadConfig(process.cwd());\n\n // Run tests\n const fileResults: TestFileResult[] = [];\n let shouldStop = false;\n\n for (const filePath of testFiles) {\n if (shouldStop) {\n break;\n }\n\n const content = await readFile(filePath, 'utf-8');\n const testFile = parseTestFile(content, filePath);\n const config = mergeConfig(globalConfig, testFile.config);\n\n // Filter blocks by name if specified\n let blocksToRun = testFile.blocks;\n if (opts.filter) {\n const filterPattern = new RegExp(opts.filter, 'i');\n blocksToRun = blocksToRun.filter((b) => (b.name ? filterPattern.test(b.name) : true));\n }\n\n if (blocksToRun.length === 0) {\n continue;\n }\n\n const ctx = await createExecutionContext(config, filePath);\n const results: TestBlockResult[] = [];\n\n try {\n for (const block of blocksToRun) {\n const result = await runBlock(block, ctx);\n\n // Check if output matches expected\n const matches = matchOutput(\n result.actualOutput,\n block.expectedOutput,\n { root: ctx.tempDir, cwd: ctx.tempDir },\n config.patterns ?? {},\n );\n\n const exitCodeMatches = result.actualExitCode === block.expectedExitCode;\n result.passed = matches && exitCodeMatches && !result.error;\n\n if (!result.passed && opts.diff) {\n result.diff = createDiff(\n block.expectedOutput,\n result.actualOutput,\n `${filePath}:${block.lineNumber}`,\n );\n }\n\n results.push(result);\n\n if (!result.passed && opts.failFast) {\n shouldStop = true;\n break;\n }\n }\n } finally {\n await cleanupExecutionContext(ctx);\n }\n\n const fileResult: TestFileResult = {\n file: testFile,\n results,\n passed: results.every((r) => r.passed),\n duration: results.reduce((sum, r) => sum + r.duration, 0),\n };\n\n fileResults.push(fileResult);\n reportFile(fileResult, opts);\n\n // Update mode\n if (opts.update && !fileResult.passed) {\n const { updated, changes } = await updateTestFile(testFile, results);\n if (updated) {\n console.error(pc.yellow(` ↻ Updated: ${changes.join(', ')}`));\n }\n }\n }\n\n // Summary\n const summary: TestRunSummary = {\n files: fileResults,\n totalPassed: fileResults.reduce((sum, f) => sum + f.results.filter((r) => r.passed).length, 0),\n totalFailed: fileResults.reduce((sum, f) => sum + f.results.filter((r) => !r.passed).length, 0),\n totalBlocks: fileResults.reduce((sum, f) => sum + f.results.length, 0),\n duration: Date.now() - startTime,\n };\n\n reportSummary(summary, opts);\n\n // Exit code\n process.exit(summary.totalFailed > 0 ? 1 : 0);\n}\n","/**\n * Readme command - Display the README documentation.\n *\n * Shows the package README.md, formatted for the terminal when interactive,\n * or as plain text when piped.\n */\n\nimport type { Command } from 'commander';\n\nimport { readFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport pc from 'picocolors';\n\n/**\n * Get the path to the README.md file.\n * Works both during development and when installed as a package.\n */\nfunction getReadmePath(): string {\n const thisDir = dirname(fileURLToPath(import.meta.url));\n const dirName = thisDir.split(/[/\\\\]/).pop();\n\n if (dirName === 'dist') {\n // Bundled: dist -> package root -> README.md\n return join(dirname(thisDir), 'README.md');\n }\n\n // Development: src/cli/commands -> src/cli -> src -> package root -> README.md\n return join(dirname(dirname(dirname(thisDir))), 'README.md');\n}\n\n/**\n * Load the README content.\n */\nfunction loadReadme(): string {\n const readmePath = getReadmePath();\n try {\n return readFileSync(readmePath, 'utf-8');\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new Error(`Failed to load README from ${readmePath}: ${message}`);\n }\n}\n\n/**\n * Apply basic terminal formatting to markdown content.\n * Colorizes headers, code blocks, and other elements for better readability.\n */\nfunction formatMarkdown(content: string, useColors: boolean): string {\n if (!useColors) {\n return content;\n }\n\n const lines = content.split('\\n');\n const formatted: string[] = [];\n let inCodeBlock = false;\n\n for (const line of lines) {\n // Track code blocks\n if (line.startsWith('```')) {\n inCodeBlock = !inCodeBlock;\n formatted.push(pc.dim(line));\n continue;\n }\n\n if (inCodeBlock) {\n formatted.push(pc.dim(line));\n continue;\n }\n\n // Headers\n if (line.startsWith('# ')) {\n formatted.push(pc.bold(pc.cyan(line)));\n continue;\n }\n if (line.startsWith('## ')) {\n formatted.push(pc.bold(pc.blue(line)));\n continue;\n }\n if (line.startsWith('### ')) {\n formatted.push(pc.bold(line));\n continue;\n }\n\n // Inline code (backticks)\n let formattedLine = line.replace(/`([^`]+)`/g, (_match, code: string) => {\n return pc.yellow(code);\n });\n\n // Bold text\n formattedLine = formattedLine.replace(/\\*\\*([^*]+)\\*\\*/g, (_match, text: string) => {\n return pc.bold(text);\n });\n\n // Links - show text in cyan, URL dimmed\n formattedLine = formattedLine.replace(\n /\\[([^\\]]+)\\]\\(([^)]+)\\)/g,\n (_match, text: string, url: string) => {\n return `${pc.cyan(text)} ${pc.dim(`(${url})`)}`;\n },\n );\n\n formatted.push(formattedLine);\n }\n\n return formatted.join('\\n');\n}\n\n/**\n * Check if stdout is an interactive terminal.\n */\nfunction isInteractive(): boolean {\n return process.stdout.isTTY === true;\n}\n\n/**\n * Register the readme command.\n */\nexport function registerReadmeCommand(program: Command): void {\n program\n .command('readme')\n .description('Display README documentation')\n .option('--raw', 'Output raw markdown without formatting')\n .action((options: { raw?: boolean }) => {\n try {\n const readme = loadReadme();\n\n // Determine if we should colorize\n const shouldColorize = !options.raw && isInteractive();\n\n const formatted = formatMarkdown(readme, shouldColorize);\n console.log(formatted);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n console.error(pc.red(`Error: ${message}`));\n process.exit(1);\n }\n });\n}\n","/**\n * Docs command - Display the tryscript quick reference.\n *\n * Shows the tryscript-reference.md file, formatted for the terminal when interactive,\n * or as plain text when piped.\n */\n\nimport type { Command } from 'commander';\n\nimport { readFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport pc from 'picocolors';\n\n/**\n * Get the path to the tryscript-reference.md file.\n * Works both during development and when installed as a package.\n */\nfunction getDocsPath(): string {\n const thisDir = dirname(fileURLToPath(import.meta.url));\n const dirName = thisDir.split(/[/\\\\]/).pop();\n\n if (dirName === 'dist') {\n // Bundled: dist -> package root -> docs/tryscript-reference.md\n return join(dirname(thisDir), 'docs', 'tryscript-reference.md');\n }\n\n // Development: src/cli/commands -> src/cli -> src -> package root -> docs/tryscript-reference.md\n return join(dirname(dirname(dirname(thisDir))), 'docs', 'tryscript-reference.md');\n}\n\n/**\n * Load the docs content.\n */\nfunction loadDocs(): string {\n const docsPath = getDocsPath();\n try {\n return readFileSync(docsPath, 'utf-8');\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new Error(`Failed to load reference docs from ${docsPath}: ${message}`);\n }\n}\n\n/**\n * Apply basic terminal formatting to markdown content.\n * Colorizes headers, code blocks, and other elements for better readability.\n */\nfunction formatMarkdown(content: string, useColors: boolean): string {\n if (!useColors) {\n return content;\n }\n\n const lines = content.split('\\n');\n const formatted: string[] = [];\n let inCodeBlock = false;\n\n for (const line of lines) {\n // Track code blocks\n if (line.startsWith('```')) {\n inCodeBlock = !inCodeBlock;\n formatted.push(pc.dim(line));\n continue;\n }\n\n if (inCodeBlock) {\n formatted.push(pc.dim(line));\n continue;\n }\n\n // Headers\n if (line.startsWith('# ')) {\n formatted.push(pc.bold(pc.cyan(line)));\n continue;\n }\n if (line.startsWith('## ')) {\n formatted.push(pc.bold(pc.blue(line)));\n continue;\n }\n if (line.startsWith('### ')) {\n formatted.push(pc.bold(line));\n continue;\n }\n\n // Inline code (backticks)\n let formattedLine = line.replace(/`([^`]+)`/g, (_match, code: string) => {\n return pc.yellow(code);\n });\n\n // Bold text\n formattedLine = formattedLine.replace(/\\*\\*([^*]+)\\*\\*/g, (_match, text: string) => {\n return pc.bold(text);\n });\n\n // Links - show text in cyan, URL dimmed\n formattedLine = formattedLine.replace(\n /\\[([^\\]]+)\\]\\(([^)]+)\\)/g,\n (_match, text: string, url: string) => {\n return `${pc.cyan(text)} ${pc.dim(`(${url})`)}`;\n },\n );\n\n formatted.push(formattedLine);\n }\n\n return formatted.join('\\n');\n}\n\n/**\n * Check if stdout is an interactive terminal.\n */\nfunction isInteractive(): boolean {\n return process.stdout.isTTY === true;\n}\n\n/**\n * Register the docs command.\n */\nexport function registerDocsCommand(program: Command): void {\n program\n .command('docs')\n .description('Display concise syntax reference')\n .option('--raw', 'Output raw markdown without formatting')\n .action((options: { raw?: boolean }) => {\n try {\n const docs = loadDocs();\n\n // Determine if we should colorize\n const shouldColorize = !options.raw && isInteractive();\n\n const formatted = formatMarkdown(docs, shouldColorize);\n console.log(formatted);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n console.error(pc.red(`Error: ${message}`));\n process.exit(1);\n }\n });\n}\n","import { Command } from 'commander';\nimport pc from 'picocolors';\nimport { VERSION } from '../index.js';\nimport { runCommand } from './commands/run.js';\nimport { registerReadmeCommand } from './commands/readme.js';\nimport { registerDocsCommand } from './commands/docs.js';\n\nexport function run(argv: string[]): void {\n const program = new Command()\n .name('tryscript')\n .version(VERSION, '--version', 'Show version number')\n .description('Golden testing for CLI applications')\n .showHelpAfterError('(use --help for usage)')\n .argument('[files...]', 'Test files to run (default: **/*.tryscript.md)')\n .option('--update', 'Update golden files with actual output')\n .option('--diff', 'Show diff on failure (default: true)')\n .option('--no-diff', 'Hide diff on failure')\n .option('--fail-fast', 'Stop on first failure')\n .option('--filter <pattern>', 'Filter tests by name pattern')\n .option('--verbose', 'Show detailed output including passing test output')\n .option('--quiet', 'Suppress non-essential output (only show failures)')\n .action(runCommand);\n\n // Register subcommands\n registerReadmeCommand(program);\n registerDocsCommand(program);\n\n program.parseAsync(argv).catch((err: Error) => {\n console.error(pc.red(`Error: ${err.message}`));\n process.exit(2);\n });\n}\n","#!/usr/bin/env node\nimport { run } from './cli/cli.js';\n\nrun(process.argv);\n"],"mappings":"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"}
|
package/dist/bin.d.cts
ADDED
package/dist/bin.d.mts
ADDED