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.
@@ -1,163 +1,441 @@
1
- # tryscript Quick Reference
1
+ # tryscript Reference
2
2
 
3
- Concise syntax reference for writing tryscript test files.
3
+ Complete reference for writing tryscript golden tests. This document covers all syntax,
4
+ configuration, and patterns needed to write accurate CLI tests on the first try.
4
5
 
5
- ## Test File Format
6
+ ## Overview
6
7
 
7
- Test files use `.tryscript.md` extension. Each file contains Markdown with console code blocks:
8
+ Tryscript is a markdown-based CLI golden testing format. Test files are markdown documents
9
+ with embedded console code blocks specifying commands and expected output.
8
10
 
9
- ```markdown
10
- # Test: Description
11
+ **Design Philosophy:**
12
+ - **Shell delegation**: Commands run in a real shell with full shell features
13
+ - **Markdown-first**: Test files are valid markdown, readable as documentation
14
+ - **Output matching**: Patterns like `[..]` match variable output; they're not for commands
11
15
 
12
- \`\`\`console
13
- $ command
14
- expected output
15
- ? exit_code
16
- \`\`\`
17
- ```
16
+ ## Quick Start Example
18
17
 
19
- ## Basic Example
18
+ ````markdown
19
+ ---
20
+ sandbox: true
21
+ env:
22
+ NO_COLOR: "1"
23
+ ---
20
24
 
21
- ```markdown
22
- # Test: Echo command
25
+ # Test: Basic echo
23
26
 
24
- \`\`\`console
27
+ ```console
25
28
  $ echo "hello world"
26
29
  hello world
27
30
  ? 0
28
- \`\`\`
29
31
  ```
30
32
 
31
- ## Exit Codes
33
+ # Test: Command with variable output
34
+
35
+ ```console
36
+ $ date +%Y
37
+ [..]
38
+ ? 0
39
+ ```
40
+ ````
41
+
42
+ ## Test File Structure
43
+
44
+ ```
45
+ ┌──────────────────────────────────────┐
46
+ │ --- │ YAML Frontmatter (optional)
47
+ │ env: │ - Configuration
48
+ │ MY_VAR: value │ - Environment variables
49
+ │ sandbox: true │ - Patterns
50
+ │ --- │
51
+ ├──────────────────────────────────────┤
52
+ │ # Test: Description │ Test heading (# or ##)
53
+ │ │
54
+ │ ```console │ Test block
55
+ │ $ command --flag │ - Command starts with $
56
+ │ expected output │ - Expected stdout follows
57
+ │ ? 0 │ - Exit code (optional, default 0)
58
+ │ ``` │
59
+ └──────────────────────────────────────┘
60
+ ```
61
+
62
+ ## Command Block Syntax
32
63
 
33
- Use `? N` to specify expected exit code:
64
+ ```
65
+ $ command [arguments...] # Command to execute (required)
66
+ > continuation line # Multi-line command continuation
67
+ expected output # Expected stdout (line by line)
68
+ ! stderr line # Expected stderr (when separating streams)
69
+ ? exit_code # Expected exit code (default: 0)
70
+ ```
71
+
72
+ ### Examples
34
73
 
74
+ **Simple command:**
75
+ ```console
76
+ $ echo "hello"
77
+ hello
78
+ ? 0
79
+ ```
80
+
81
+ **Non-zero exit code:**
35
82
  ```console
36
83
  $ exit 42
37
84
  ? 42
38
85
  ```
39
86
 
87
+ **Multi-line command:**
88
+ ```console
89
+ $ ls -la | \
90
+ > grep ".md" | \
91
+ > wc -l
92
+ 5
93
+ ```
94
+
95
+ **Stderr handling:**
96
+ ```console
97
+ $ cat nonexistent 2>&1
98
+ cat: nonexistent: No such file or directory
99
+ ? 1
100
+ ```
101
+
102
+ **Separate stderr assertion:**
103
+ ```console
104
+ $ ./script.sh
105
+ stdout line
106
+ ! stderr line
107
+ ? 0
108
+ ```
109
+
40
110
  ## Elision Patterns
41
111
 
42
- | Pattern | Matches | Example |
43
- | -------- | -------------------------------- | --------------------- |
44
- | `[..]` | Any characters on current line | `Built in [..]ms` |
45
- | `...` | Zero or more complete lines | `...\nDone` |
46
- | `[EXE]` | `.exe` on Windows, empty on Unix | `my-cli[EXE]` |
47
- | `[ROOT]` | Test root directory | `[ROOT]/output.txt` |
48
- | `[CWD]` | Current working directory | `[CWD]/file.txt` |
112
+ Patterns in expected output match variable content:
49
113
 
50
- ## YAML Frontmatter
114
+ | Pattern | Matches | Example |
115
+ |---------|---------|---------|
116
+ | `[..]` | Any text on a single line | `Built in [..]ms` |
117
+ | `...` | Zero or more complete lines | `...\nDone` |
118
+ | `[CWD]` | Current working directory | `[CWD]/output.txt` |
119
+ | `[ROOT]` | Test file directory | `[ROOT]/fixtures/` |
120
+ | `[EXE]` | `.exe` on Windows, empty otherwise | `my-cli[EXE]` |
121
+ | `[PATTERN]` | Custom pattern from config | User-defined regex |
51
122
 
52
- Configure test behavior at the top of the file:
123
+ ### Pattern Examples
124
+
125
+ **Single-line wildcard:**
126
+ ```console
127
+ $ date
128
+ [..]
129
+ ? 0
130
+ ```
131
+
132
+ **Multi-line wildcard:**
133
+ ```console
134
+ $ ls -la
135
+ total [..]
136
+ ...
137
+ -rw-r--r-- 1 user user [..] README.md
138
+ ```
139
+
140
+ **Custom pattern:**
141
+ ```yaml
142
+ patterns:
143
+ VERSION: '\d+\.\d+\.\d+'
144
+ ```
145
+ ```console
146
+ $ my-cli --version
147
+ my-cli version [VERSION]
148
+ ```
149
+
150
+ ## Configuration (Frontmatter)
151
+
152
+ All options are optional. Place at the top of the file:
53
153
 
54
154
  ```yaml
55
155
  ---
56
- bin: ./my-cli
57
- env:
156
+ cwd: ./subdir # Working directory (relative to test file)
157
+ sandbox: true # Run in isolated temp directory
158
+ env: # Environment variables
58
159
  NO_COLOR: "1"
59
- timeout: 5000
60
- patterns:
61
- VERSION: "\\d+\\.\\d+\\.\\d+"
160
+ MY_VAR: value
161
+ timeout: 5000 # Command timeout in milliseconds
162
+ patterns: # Custom elision patterns
163
+ UUID: '[0-9a-f]{8}-...'
164
+ fixtures: # Files to copy to sandbox
165
+ - data/input.txt
166
+ before: npm run build # Run before first test
167
+ after: rm -rf ./cache # Run after all tests
62
168
  ---
63
169
  ```
64
170
 
65
- ### Config Options
171
+ ### Config Options Reference
172
+
173
+ | Option | Type | Default | Description |
174
+ |--------|------|---------|-------------|
175
+ | `cwd` | path | `"."` | Working directory (relative to test file) |
176
+ | `sandbox` | `boolean \| path` | `false` | Run in isolated temp directory |
177
+ | `env` | `object` | `{}` | Environment variables passed to shell |
178
+ | `timeout` | `number` | `30000` | Command timeout in milliseconds |
179
+ | `patterns` | `object` | `{}` | Custom regex patterns for `[NAME]` |
180
+ | `fixtures` | `array` | `[]` | Files to copy to sandbox |
181
+ | `before` | `string` | - | Shell command before first test |
182
+ | `after` | `string` | - | Shell command after all tests |
183
+
184
+ ## Sandbox Mode
66
185
 
67
- | Option | Type | Description |
68
- | ---------- | -------- | ------------------------------------- |
69
- | `bin` | string | Path to CLI binary |
70
- | `env` | object | Environment variables |
71
- | `timeout` | number | Command timeout in milliseconds |
72
- | `patterns` | object | Custom regex patterns for `[NAME]` |
186
+ Sandbox provides test isolation by running commands in a temporary directory:
73
187
 
74
- ## Custom Patterns
188
+ | Configuration | Behavior |
189
+ |--------------|----------|
190
+ | `sandbox: false` (default) | Commands run in `cwd` (test file dir) |
191
+ | `sandbox: true` | Creates empty temp dir, commands run there |
192
+ | `sandbox: ./fixtures` | Copies `./fixtures/` to temp dir, runs there |
75
193
 
76
- Define reusable patterns in frontmatter:
194
+ **When sandbox is enabled:**
195
+ - Fresh temp directory created for each test file
196
+ - Fixtures are copied before tests run
197
+ - `[CWD]` matches the sandbox directory
198
+ - Files created by tests don't pollute source
199
+
200
+ ### Sandbox with Fixtures
77
201
 
78
202
  ```yaml
79
- patterns:
80
- VERSION: "\\d+\\.\\d+\\.\\d+"
81
- UUID: "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
203
+ ---
204
+ sandbox: true
205
+ fixtures:
206
+ - data/input.txt # Copies to sandbox/input.txt
207
+ - source: config/settings.json # Copies to sandbox/custom.json
208
+ dest: custom.json
209
+ ---
82
210
  ```
83
211
 
84
- Use in output:
212
+ ## Environment Variables
213
+
214
+ Use `env` to set variables. The **shell** handles `$VAR` expansion:
215
+
216
+ ```yaml
217
+ env:
218
+ CLI: ./dist/cli.mjs
219
+ DEBUG: "true"
220
+ ```
85
221
 
86
222
  ```console
87
- $ my-cli --version
223
+ $ $CLI --version
224
+ 1.0.0
225
+ ```
226
+
227
+ **Important:** Variables are for the shell, not for output matching.
228
+
229
+ ## Test Annotations
230
+
231
+ Control test execution with HTML comments:
232
+
233
+ ```markdown
234
+ ## This test is skipped <!-- skip -->
235
+
236
+ ## Only run this test <!-- only -->
237
+ ```
238
+
239
+ | Annotation | Effect |
240
+ |------------|--------|
241
+ | `<!-- skip -->` | Test is skipped, marked as passed |
242
+ | `<!-- only -->` | Only tests with this annotation run |
243
+
244
+ ## Complete Example
245
+
246
+ Here's a complete test file demonstrating all features:
247
+
248
+ ````markdown
249
+ ---
250
+ sandbox: true
251
+ env:
252
+ NO_COLOR: "1"
253
+ CLI: ./dist/my-cli.mjs
254
+ timeout: 5000
255
+ patterns:
256
+ VERSION: '\d+\.\d+\.\d+'
257
+ TIMESTAMP: '\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}'
258
+ fixtures:
259
+ - test-data/config.json
260
+ before: echo "Setup complete"
261
+ ---
262
+
263
+ # CLI Golden Tests
264
+
265
+ These tests validate the my-cli command-line tool.
266
+
267
+ ## Basic Commands
268
+
269
+ # Test: Show version
270
+
271
+ ```console
272
+ $ $CLI --version
88
273
  my-cli version [VERSION]
89
274
  ? 0
90
275
  ```
91
276
 
92
- ## Multiple Commands
277
+ # Test: Show help
93
278
 
94
- Multiple tests per file, each with its own heading:
279
+ ```console
280
+ $ $CLI --help
281
+ Usage: my-cli [options] [command]
95
282
 
96
- ```markdown
97
- # Test: First test
283
+ Options:
284
+ --version Show version
285
+ --help Show help
286
+ ...
287
+ ? 0
288
+ ```
289
+
290
+ ## Error Handling
291
+
292
+ # Test: Missing required argument
293
+
294
+ ```console
295
+ $ $CLI process
296
+ Error: missing required argument 'file'
297
+ ? 1
298
+ ```
299
+
300
+ # Test: File not found
301
+
302
+ ```console
303
+ $ $CLI process nonexistent.txt 2>&1
304
+ Error: file not found: nonexistent.txt
305
+ ? 1
306
+ ```
307
+
308
+ ## Feature Tests
309
+
310
+ # Test: Process config file
98
311
 
99
- \`\`\`console
100
- $ echo one
101
- one
312
+ ```console
313
+ $ $CLI process config.json
314
+ Processing: config.json
315
+ Done at [TIMESTAMP][..]
102
316
  ? 0
103
- \`\`\`
317
+ ```
104
318
 
105
- # Test: Second test
319
+ # Test: Verbose output <!-- skip -->
106
320
 
107
- \`\`\`console
108
- $ echo two
109
- two
321
+ ```console
322
+ $ $CLI --verbose process config.json
323
+ [DEBUG] Loading config.json
324
+ ...
325
+ Done
110
326
  ? 0
111
- \`\`\`
112
327
  ```
328
+ ````
113
329
 
114
330
  ## CLI Usage
115
331
 
116
332
  ```bash
117
- # Run all tests
118
- npx tryscript
333
+ tryscript # Show help (same as --help)
334
+ tryscript run [files...] # Run golden tests
335
+ tryscript docs # Show this reference
336
+ tryscript readme # Show README
337
+ ```
338
+
339
+ ### Run Options
340
+
341
+ | Option | Description |
342
+ |--------|-------------|
343
+ | `--update` | Update test files with actual output |
344
+ | `--diff` / `--no-diff` | Show/hide diff on failure |
345
+ | `--fail-fast` | Stop on first failure |
346
+ | `--filter <pattern>` | Filter tests by name |
347
+ | `--verbose` | Show detailed output |
348
+ | `--quiet` | Suppress non-essential output |
119
349
 
120
- # Run specific files
121
- npx tryscript tests/foo.tryscript.md
350
+ ## Best Practices
351
+
352
+ ### DO: Use shell features directly
353
+
354
+ ```console
355
+ $ echo "hello" | tr 'a-z' 'A-Z'
356
+ HELLO
357
+
358
+ $ cat file.txt 2>/dev/null || echo "not found"
359
+ not found
360
+ ```
361
+
362
+ ### DO: Use env for CLI paths
363
+
364
+ ```yaml
365
+ env:
366
+ BIN: ./dist/cli.mjs
367
+ ```
368
+ ```console
369
+ $ $BIN --version
370
+ 1.0.0
371
+ ```
122
372
 
123
- # Update golden files
124
- npx tryscript --update
373
+ ### DO: Use sandbox for file operations
125
374
 
126
- # Filter tests by name
127
- npx tryscript --filter "pattern"
375
+ ```yaml
376
+ sandbox: true
377
+ ```
378
+ ```console
379
+ $ echo "test" > output.txt
380
+ $ cat output.txt
381
+ test
382
+ ```
128
383
 
129
- # Fail fast on first error
130
- npx tryscript --fail-fast
384
+ ### DON'T: Use patterns in commands
131
385
 
132
- # Verbose output
133
- npx tryscript --verbose
386
+ ```console
387
+ # WRONG: Patterns are for output matching only
388
+ $ cat [CWD]/file.txt
134
389
  ```
135
390
 
136
- ## Options
391
+ ### DON'T: Rely on exact timestamps or paths
137
392
 
138
- | Option | Description |
139
- | ------------------ | ---------------------------------------- |
140
- | `--update` | Update golden files with actual output |
141
- | `--diff` | Show diff on failure (default: true) |
142
- | `--no-diff` | Hide diff on failure |
143
- | `--fail-fast` | Stop on first failure |
144
- | `--filter <regex>` | Filter tests by name pattern |
145
- | `--verbose` | Show detailed output |
146
- | `--quiet` | Suppress non-essential output |
393
+ ```console
394
+ # WRONG: Exact match will fail
395
+ $ date
396
+ Mon Jan 3 12:34:56 UTC 2026
397
+
398
+ # RIGHT: Use elision
399
+ $ date
400
+ [..]
401
+ ```
147
402
 
148
403
  ## Config File
149
404
 
150
- Create `tryscript.config.ts` in your project root:
405
+ For project-wide settings, create `tryscript.config.ts`:
151
406
 
152
407
  ```typescript
153
408
  import { defineConfig } from 'tryscript';
154
409
 
155
410
  export default defineConfig({
156
- bin: './dist/cli.js',
157
411
  env: { NO_COLOR: '1' },
158
412
  timeout: 30000,
159
413
  patterns: {
160
414
  VERSION: '\\d+\\.\\d+\\.\\d+',
415
+ UUID: '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}',
161
416
  },
162
417
  });
163
418
  ```
419
+
420
+ ## Execution Model
421
+
422
+ ```
423
+ Test File → Parse YAML + Blocks → Create Execution Context
424
+
425
+ ┌────────────────────┴────────────────────┐
426
+ │ │
427
+ sandbox: false sandbox: true
428
+ cwd = testDir/config.cwd cwd = /tmp/tryscript-xxx/
429
+ │ │
430
+ └────────────────────┬────────────────────┘
431
+
432
+ spawn(command, { shell: true, cwd, env })
433
+
434
+ Capture stdout + stderr → Match against expected
435
+ ```
436
+
437
+ **Key points:**
438
+ 1. Commands run in a real shell (`shell: true`)
439
+ 2. Shell handles all variable expansion (`$VAR`)
440
+ 3. Patterns (`[..]`, `[CWD]`) only apply to output matching
441
+ 4. Sandbox creates isolated temp directory per test file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tryscript",
3
- "version": "0.0.1",
3
+ "version": "0.1.0",
4
4
  "description": "Golden testing for CLI applications - TypeScript port of trycmd",
5
5
  "license": "MIT",
6
6
  "author": "Joshua Levy",
@@ -37,20 +37,6 @@
37
37
  "engines": {
38
38
  "node": ">=20"
39
39
  },
40
- "scripts": {
41
- "build": "tsdown",
42
- "dev": "tsdown --watch",
43
- "typecheck": "tsc -p tsconfig.json --noEmit",
44
- "test": "vitest run",
45
- "test:watch": "vitest",
46
- "test:golden": "node dist/bin.mjs 'tests/**/*.tryscript.md'",
47
- "test:golden:coverage": "c8 --src src --all --include 'dist/**' --reporter text --reporter html --reports-dir coverage-golden node dist/bin.mjs 'tests/**/*.tryscript.md'",
48
- "test:coverage": "vitest run --coverage",
49
- "test:all:coverage": "pnpm test:golden:coverage && pnpm test:coverage",
50
- "publint": "publint",
51
- "prepack": "pnpm build",
52
- "tryscript": "tsx src/bin.ts"
53
- },
54
40
  "dependencies": {
55
41
  "atomically": "^2.0.0",
56
42
  "commander": "^14.0.0",
@@ -72,5 +58,20 @@
72
58
  "tsx": "^4.21.0",
73
59
  "typescript": "^5.0.0",
74
60
  "vitest": "^4.0.0"
61
+ },
62
+ "scripts": {
63
+ "copy-docs": "cp ../../README.md . && mkdir -p docs && cp ../../docs/tryscript-reference.md docs/",
64
+ "build": "pnpm copy-docs && tsdown",
65
+ "dev": "tsdown --watch",
66
+ "typecheck": "tsc -p tsconfig.json --noEmit",
67
+ "test": "vitest run",
68
+ "test:watch": "vitest",
69
+ "test:self": "tsx src/bin.ts",
70
+ "test:golden": "node dist/bin.mjs run 'tests/**/*.tryscript.md'",
71
+ "test:golden:coverage": "c8 --src src --all --include 'dist/**' --reporter text --reporter html --reports-dir coverage-golden node dist/bin.mjs run 'tests/**/*.tryscript.md'",
72
+ "test:coverage": "vitest run --coverage",
73
+ "test:all:coverage": "pnpm test:golden:coverage && pnpm test:coverage",
74
+ "publint": "publint",
75
+ "tryscript": "tsx src/bin.ts"
75
76
  }
76
- }
77
+ }
@@ -1 +0,0 @@
1
- {"version":3,"file":"src-CeUA446P.cjs","names":["module","config: TestConfig","blocks: TestBlock[]","match: RegExpExecArray | null","commandLines: string[]","outputLines: string[]","chunks: Buffer[]","VERSION: string"],"sources":["../src/lib/config.ts","../src/lib/parser.ts","../src/lib/runner.ts","../src/lib/matcher.ts","../src/index.ts"],"sourcesContent":["import { pathToFileURL } from 'node:url';\nimport { existsSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport type { TestConfig } from './types.js';\n\nexport interface TryscriptConfig {\n bin?: string;\n env?: Record<string, string>;\n timeout?: number;\n patterns?: Record<string, RegExp | string>;\n tests?: string[];\n}\n\nconst CONFIG_FILES = ['tryscript.config.ts', 'tryscript.config.js', 'tryscript.config.mjs'];\n\n/**\n * Load config file using dynamic import.\n * Supports both TypeScript (via tsx/ts-node) and JavaScript configs.\n */\nexport async function loadConfig(baseDir: string): Promise<TryscriptConfig> {\n for (const filename of CONFIG_FILES) {\n const configPath = resolve(baseDir, filename);\n if (existsSync(configPath)) {\n const configUrl = pathToFileURL(configPath).href;\n const module = (await import(configUrl)) as { default?: TryscriptConfig } | TryscriptConfig;\n return (module as { default?: TryscriptConfig }).default ?? (module as TryscriptConfig);\n }\n }\n return {};\n}\n\n/**\n * Merge config with frontmatter overrides.\n * Frontmatter takes precedence over config file.\n */\nexport function mergeConfig(base: TryscriptConfig, frontmatter: TestConfig): TryscriptConfig {\n return {\n ...base,\n ...frontmatter,\n env: { ...base.env, ...frontmatter.env },\n patterns: { ...base.patterns, ...frontmatter.patterns },\n };\n}\n\n/**\n * Helper for typed config files.\n */\nexport function defineConfig(config: TryscriptConfig): TryscriptConfig {\n return config;\n}\n","import { parse as parseYaml } from 'yaml';\nimport type { TestConfig, TestBlock, TestFile } from './types.js';\n\n/** Regex to match YAML frontmatter at the start of a file */\nconst FRONTMATTER_REGEX = /^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n/;\n\n/** Regex to match fenced code blocks with console/bash info string */\nconst CODE_BLOCK_REGEX = /```(console|bash)\\r?\\n([\\s\\S]*?)```/g;\n\n/** Regex to match markdown headings (for test names) */\nconst HEADING_REGEX = /^#+\\s+(?:Test:\\s*)?(.+)$/m;\n\n/**\n * Parse a .tryscript.md file into structured test data.\n */\nexport function parseTestFile(content: string, filePath: string): TestFile {\n const rawContent = content;\n let config: TestConfig = {};\n let body = content;\n\n // Extract frontmatter if present\n const frontmatterMatch = FRONTMATTER_REGEX.exec(content);\n if (frontmatterMatch) {\n const yamlContent = frontmatterMatch[1] ?? '';\n config = parseYaml(yamlContent) as TestConfig;\n body = content.slice(frontmatterMatch[0].length);\n }\n\n // Parse all console blocks\n const blocks: TestBlock[] = [];\n\n // Reset regex lastIndex\n CODE_BLOCK_REGEX.lastIndex = 0;\n\n let match: RegExpExecArray | null;\n while ((match = CODE_BLOCK_REGEX.exec(body)) !== null) {\n const blockContent = match[2] ?? '';\n const blockStart = match.index;\n\n // Find the line number (1-indexed)\n const precedingContent = content.slice(0, content.indexOf(match[0]));\n const lineNumber = precedingContent.split('\\n').length;\n\n // Look for a heading before this block (for test name)\n const contentBefore = body.slice(0, blockStart);\n const lastHeadingMatch = [\n ...contentBefore.matchAll(new RegExp(HEADING_REGEX.source, 'gm')),\n ].pop();\n const name = lastHeadingMatch?.[1]?.trim();\n\n // Parse the block content\n const parsed = parseBlockContent(blockContent);\n if (parsed) {\n blocks.push({\n name,\n command: parsed.command,\n expectedOutput: parsed.expectedOutput,\n expectedExitCode: parsed.expectedExitCode,\n lineNumber,\n rawContent: match[0],\n });\n }\n }\n\n return { path: filePath, config, blocks, rawContent };\n}\n\n/**\n * Parse the content of a single console block.\n */\nfunction parseBlockContent(content: string): {\n command: string;\n expectedOutput: string;\n expectedExitCode: number;\n} | null {\n const lines = content.split('\\n');\n const commandLines: string[] = [];\n const outputLines: string[] = [];\n let expectedExitCode = 0;\n let inCommand = false;\n\n for (const line of lines) {\n if (line.startsWith('$ ')) {\n // Start of a command\n inCommand = true;\n commandLines.push(line.slice(2));\n } else if (line.startsWith('> ') && inCommand) {\n // Continuation of a multi-line command\n commandLines.push(line.slice(2));\n } else if (line.startsWith('? ')) {\n // Exit code specification\n inCommand = false;\n expectedExitCode = parseInt(line.slice(2).trim(), 10);\n } else {\n // Output line\n inCommand = false;\n outputLines.push(line);\n }\n }\n\n if (commandLines.length === 0) {\n return null;\n }\n\n // Join command lines, handling shell continuations\n let command = '';\n for (let i = 0; i < commandLines.length; i++) {\n const line = commandLines[i] ?? '';\n if (line.endsWith('\\\\')) {\n command += line.slice(0, -1) + ' ';\n } else {\n command += line;\n if (i < commandLines.length - 1) {\n command += ' ';\n }\n }\n }\n\n // Join output lines, preserving blank lines but trimming trailing empty lines\n let expectedOutput = outputLines.join('\\n');\n expectedOutput = expectedOutput.replace(/\\n+$/, '');\n if (expectedOutput) {\n expectedOutput += '\\n';\n }\n\n return { command: command.trim(), expectedOutput, expectedExitCode };\n}\n","import { spawn } from 'node:child_process';\nimport { mkdtemp, realpath, rm } from 'node:fs/promises';\nimport { tmpdir } from 'node:os';\nimport { join, dirname, resolve } from 'node:path';\nimport treeKill from 'tree-kill';\nimport type { TestBlock, TestBlockResult } from './types.js';\nimport type { TryscriptConfig } from './config.js';\n\n/** Default timeout in milliseconds */\nconst DEFAULT_TIMEOUT = 30_000;\n\n/**\n * Execution context for a test file.\n * Created once per file, contains the temp directory.\n */\nexport interface ExecutionContext {\n /** Temporary directory for this test file (resolved, no symlinks) */\n tempDir: string;\n /** Directory containing the test file (for portable test commands) */\n testDir: string;\n /** Resolved binary path */\n binPath: string;\n /** Environment variables */\n env: Record<string, string>;\n /** Timeout per command */\n timeout: number;\n}\n\n/**\n * Create an execution context for a test file.\n */\nexport async function createExecutionContext(\n config: TryscriptConfig,\n testFilePath: string,\n): Promise<ExecutionContext> {\n // Create temp directory and resolve symlinks (e.g., /var -> /private/var on macOS)\n // This ensures [CWD] and [ROOT] patterns match pwd output\n const rawTempDir = await mkdtemp(join(tmpdir(), 'tryscript-'));\n const tempDir = await realpath(rawTempDir);\n\n // Resolve test file directory for portable test commands\n const testDir = resolve(dirname(testFilePath));\n\n // Resolve binary path relative to test file directory\n let binPath = config.bin ?? '';\n if (binPath && !binPath.startsWith('/')) {\n binPath = join(testDir, binPath);\n }\n\n return {\n tempDir,\n testDir,\n binPath,\n env: {\n ...process.env,\n ...config.env,\n // Disable colors by default for deterministic output\n NO_COLOR: config.env?.NO_COLOR ?? '1',\n FORCE_COLOR: '0',\n // Provide test directory for portable test commands\n TRYSCRIPT_TEST_DIR: testDir,\n } as Record<string, string>,\n timeout: config.timeout ?? DEFAULT_TIMEOUT,\n };\n}\n\n/**\n * Clean up execution context (remove temp directory).\n */\nexport async function cleanupExecutionContext(ctx: ExecutionContext): Promise<void> {\n await rm(ctx.tempDir, { recursive: true, force: true });\n}\n\n/**\n * Run a single test block and return the result.\n */\nexport async function runBlock(block: TestBlock, ctx: ExecutionContext): Promise<TestBlockResult> {\n const startTime = Date.now();\n\n try {\n const { output, exitCode } = await executeCommand(block.command, ctx);\n\n const duration = Date.now() - startTime;\n\n return {\n block,\n passed: true, // Matching handled separately\n actualOutput: output,\n actualExitCode: exitCode,\n duration,\n };\n } catch (error) {\n const duration = Date.now() - startTime;\n const message = error instanceof Error ? error.message : String(error);\n\n return {\n block,\n passed: false,\n actualOutput: '',\n actualExitCode: -1,\n duration,\n error: message,\n };\n }\n}\n\n/**\n * Execute a command and capture output.\n */\nasync function executeCommand(\n command: string,\n ctx: ExecutionContext,\n): Promise<{ output: string; exitCode: number }> {\n return new Promise((resolve, reject) => {\n const proc = spawn(command, {\n shell: true,\n cwd: ctx.tempDir,\n env: ctx.env as NodeJS.ProcessEnv,\n // Pipe both to capture\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n\n const chunks: Buffer[] = [];\n\n // Capture data as it comes in to preserve order\n proc.stdout.on('data', (data: Buffer) => chunks.push(data));\n proc.stderr.on('data', (data: Buffer) => chunks.push(data));\n\n const timeoutId = setTimeout(() => {\n if (proc.pid) {\n treeKill(proc.pid, 'SIGKILL');\n }\n reject(new Error(`Command timed out after ${ctx.timeout}ms`));\n }, ctx.timeout);\n\n proc.on('close', (code) => {\n clearTimeout(timeoutId);\n const output = Buffer.concat(chunks).toString('utf-8');\n resolve({\n output,\n exitCode: code ?? 0,\n });\n });\n\n proc.on('error', (err) => {\n clearTimeout(timeoutId);\n reject(err);\n });\n });\n}\n","import stripAnsi from 'strip-ansi';\n\n/**\n * Escape special regex characters in a string.\n */\nfunction escapeRegex(str: string): string {\n return str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n// Marker prefix for patterns (uses Unicode private use chars that won't appear in normal output)\nconst MARKER = '\\uE000';\n\n/**\n * Convert expected output with elision patterns to a regex.\n *\n * Handles (matching trycmd):\n * - [..] — matches any characters on the same line (trycmd: [^\\n]*?)\n * - ... — matches zero or more complete lines (trycmd: \\n(([^\\n]*\\n)*)?)\n * - [EXE] — matches .exe on Windows, empty otherwise\n * - [ROOT] — replaced with test root directory (pre-processed)\n * - [CWD] — replaced with current working directory (pre-processed)\n * - Custom [NAME] patterns from config (trycmd: TestCases::insert_var)\n */\nfunction patternToRegex(\n expected: string,\n customPatterns: Record<string, string | RegExp> = {},\n): RegExp {\n // Build a map of markers to their regex replacements\n const replacements = new Map<string, string>();\n let markerIndex = 0;\n\n const getMarker = (): string => {\n return `${MARKER}${markerIndex++}${MARKER}`;\n };\n\n let processed = expected;\n\n // Replace [..] with marker\n const dotdotMarker = getMarker();\n replacements.set(dotdotMarker, '[^\\\\n]*');\n processed = processed.replaceAll('[..]', dotdotMarker);\n\n // Replace ... (followed by newline) with marker\n const ellipsisMarker = getMarker();\n replacements.set(ellipsisMarker, '(?:[^\\\\n]*\\\\n)*');\n processed = processed.replace(/\\.\\.\\.\\n/g, ellipsisMarker);\n\n // Replace [EXE] with marker\n const exeMarker = getMarker();\n const exe = process.platform === 'win32' ? '\\\\.exe' : '';\n replacements.set(exeMarker, exe);\n processed = processed.replaceAll('[EXE]', exeMarker);\n\n // Replace custom patterns with markers\n for (const [name, pattern] of Object.entries(customPatterns)) {\n const placeholder = `[${name}]`;\n const patternStr = pattern instanceof RegExp ? pattern.source : pattern;\n const marker = getMarker();\n replacements.set(marker, `(${patternStr})`);\n processed = processed.replaceAll(placeholder, marker);\n }\n\n // Escape special regex characters\n let regex = escapeRegex(processed);\n\n // Restore markers to their regex replacements\n for (const [marker, replacement] of replacements) {\n regex = regex.replaceAll(escapeRegex(marker), replacement);\n }\n\n // Match the entire string (dotall mode for . to match newlines if needed)\n return new RegExp(`^${regex}$`, 's');\n}\n\n/**\n * Pre-process expected output to replace path placeholders with actual paths.\n * This happens BEFORE pattern matching.\n */\nfunction preprocessPaths(expected: string, context: { root: string; cwd: string }): string {\n let result = expected;\n // Normalize paths for comparison (use forward slashes)\n const normalizedRoot = context.root.replace(/\\\\/g, '/');\n const normalizedCwd = context.cwd.replace(/\\\\/g, '/');\n result = result.replaceAll('[ROOT]', normalizedRoot);\n result = result.replaceAll('[CWD]', normalizedCwd);\n return result;\n}\n\n/**\n * Normalize actual output for comparison.\n * - Remove ANSI escape codes (colors, etc.)\n * - Normalize line endings to \\n\n * - Normalize paths (Windows backslashes to forward slashes)\n * - Trim trailing whitespace from lines\n * - Ensure single trailing newline\n */\nexport function normalizeOutput(output: string): string {\n // Remove ANSI escape codes first\n let normalized = stripAnsi(output);\n\n normalized = normalized\n .replace(/\\r\\n/g, '\\n')\n .replace(/\\r/g, '\\n')\n .split('\\n')\n .map((line) => line.trimEnd())\n .join('\\n')\n .replace(/\\n+$/, '\\n');\n\n // Handle empty output\n if (normalized === '\\n') {\n normalized = '';\n }\n\n return normalized;\n}\n\n/**\n * Check if actual output matches expected pattern.\n */\nexport function matchOutput(\n actual: string,\n expected: string,\n context: { root: string; cwd: string },\n customPatterns: Record<string, string | RegExp> = {},\n): boolean {\n const normalizedActual = normalizeOutput(actual);\n const normalizedExpected = normalizeOutput(expected);\n\n // Empty expected matches empty actual\n if (normalizedExpected === '' && normalizedActual === '') {\n return true;\n }\n\n const preprocessed = preprocessPaths(normalizedExpected, context);\n const regex = patternToRegex(preprocessed, customPatterns);\n return regex.test(normalizedActual);\n}\n","// Public API exports\n\n// Version constant (injected at build time)\ndeclare const __VERSION__: string;\nexport const VERSION: string = typeof __VERSION__ !== 'undefined' ? __VERSION__ : 'development';\n\n// Config helper\nexport { defineConfig } from './lib/config.js';\nexport type { TryscriptConfig } from './lib/config.js';\n\n// Types\nexport type {\n TestConfig,\n TestBlock,\n TestFile,\n TestBlockResult,\n TestFileResult,\n TestRunSummary,\n} from './lib/types.js';\n\n// Core functions (for programmatic use)\nexport { parseTestFile } from './lib/parser.js';\nexport { runBlock, createExecutionContext, cleanupExecutionContext } from './lib/runner.js';\nexport type { ExecutionContext } from './lib/runner.js';\nexport { matchOutput, normalizeOutput } from './lib/matcher.js';\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"src-UjaSQrqA.mjs","names":["config: TestConfig","parseYaml","blocks: TestBlock[]","match: RegExpExecArray | null","commandLines: string[]","outputLines: string[]","chunks: Buffer[]","VERSION: string"],"sources":["../src/lib/config.ts","../src/lib/parser.ts","../src/lib/runner.ts","../src/lib/matcher.ts","../src/index.ts"],"sourcesContent":["import { pathToFileURL } from 'node:url';\nimport { existsSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport type { TestConfig } from './types.js';\n\nexport interface TryscriptConfig {\n bin?: string;\n env?: Record<string, string>;\n timeout?: number;\n patterns?: Record<string, RegExp | string>;\n tests?: string[];\n}\n\nconst CONFIG_FILES = ['tryscript.config.ts', 'tryscript.config.js', 'tryscript.config.mjs'];\n\n/**\n * Load config file using dynamic import.\n * Supports both TypeScript (via tsx/ts-node) and JavaScript configs.\n */\nexport async function loadConfig(baseDir: string): Promise<TryscriptConfig> {\n for (const filename of CONFIG_FILES) {\n const configPath = resolve(baseDir, filename);\n if (existsSync(configPath)) {\n const configUrl = pathToFileURL(configPath).href;\n const module = (await import(configUrl)) as { default?: TryscriptConfig } | TryscriptConfig;\n return (module as { default?: TryscriptConfig }).default ?? (module as TryscriptConfig);\n }\n }\n return {};\n}\n\n/**\n * Merge config with frontmatter overrides.\n * Frontmatter takes precedence over config file.\n */\nexport function mergeConfig(base: TryscriptConfig, frontmatter: TestConfig): TryscriptConfig {\n return {\n ...base,\n ...frontmatter,\n env: { ...base.env, ...frontmatter.env },\n patterns: { ...base.patterns, ...frontmatter.patterns },\n };\n}\n\n/**\n * Helper for typed config files.\n */\nexport function defineConfig(config: TryscriptConfig): TryscriptConfig {\n return config;\n}\n","import { parse as parseYaml } from 'yaml';\nimport type { TestConfig, TestBlock, TestFile } from './types.js';\n\n/** Regex to match YAML frontmatter at the start of a file */\nconst FRONTMATTER_REGEX = /^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n/;\n\n/** Regex to match fenced code blocks with console/bash info string */\nconst CODE_BLOCK_REGEX = /```(console|bash)\\r?\\n([\\s\\S]*?)```/g;\n\n/** Regex to match markdown headings (for test names) */\nconst HEADING_REGEX = /^#+\\s+(?:Test:\\s*)?(.+)$/m;\n\n/**\n * Parse a .tryscript.md file into structured test data.\n */\nexport function parseTestFile(content: string, filePath: string): TestFile {\n const rawContent = content;\n let config: TestConfig = {};\n let body = content;\n\n // Extract frontmatter if present\n const frontmatterMatch = FRONTMATTER_REGEX.exec(content);\n if (frontmatterMatch) {\n const yamlContent = frontmatterMatch[1] ?? '';\n config = parseYaml(yamlContent) as TestConfig;\n body = content.slice(frontmatterMatch[0].length);\n }\n\n // Parse all console blocks\n const blocks: TestBlock[] = [];\n\n // Reset regex lastIndex\n CODE_BLOCK_REGEX.lastIndex = 0;\n\n let match: RegExpExecArray | null;\n while ((match = CODE_BLOCK_REGEX.exec(body)) !== null) {\n const blockContent = match[2] ?? '';\n const blockStart = match.index;\n\n // Find the line number (1-indexed)\n const precedingContent = content.slice(0, content.indexOf(match[0]));\n const lineNumber = precedingContent.split('\\n').length;\n\n // Look for a heading before this block (for test name)\n const contentBefore = body.slice(0, blockStart);\n const lastHeadingMatch = [\n ...contentBefore.matchAll(new RegExp(HEADING_REGEX.source, 'gm')),\n ].pop();\n const name = lastHeadingMatch?.[1]?.trim();\n\n // Parse the block content\n const parsed = parseBlockContent(blockContent);\n if (parsed) {\n blocks.push({\n name,\n command: parsed.command,\n expectedOutput: parsed.expectedOutput,\n expectedExitCode: parsed.expectedExitCode,\n lineNumber,\n rawContent: match[0],\n });\n }\n }\n\n return { path: filePath, config, blocks, rawContent };\n}\n\n/**\n * Parse the content of a single console block.\n */\nfunction parseBlockContent(content: string): {\n command: string;\n expectedOutput: string;\n expectedExitCode: number;\n} | null {\n const lines = content.split('\\n');\n const commandLines: string[] = [];\n const outputLines: string[] = [];\n let expectedExitCode = 0;\n let inCommand = false;\n\n for (const line of lines) {\n if (line.startsWith('$ ')) {\n // Start of a command\n inCommand = true;\n commandLines.push(line.slice(2));\n } else if (line.startsWith('> ') && inCommand) {\n // Continuation of a multi-line command\n commandLines.push(line.slice(2));\n } else if (line.startsWith('? ')) {\n // Exit code specification\n inCommand = false;\n expectedExitCode = parseInt(line.slice(2).trim(), 10);\n } else {\n // Output line\n inCommand = false;\n outputLines.push(line);\n }\n }\n\n if (commandLines.length === 0) {\n return null;\n }\n\n // Join command lines, handling shell continuations\n let command = '';\n for (let i = 0; i < commandLines.length; i++) {\n const line = commandLines[i] ?? '';\n if (line.endsWith('\\\\')) {\n command += line.slice(0, -1) + ' ';\n } else {\n command += line;\n if (i < commandLines.length - 1) {\n command += ' ';\n }\n }\n }\n\n // Join output lines, preserving blank lines but trimming trailing empty lines\n let expectedOutput = outputLines.join('\\n');\n expectedOutput = expectedOutput.replace(/\\n+$/, '');\n if (expectedOutput) {\n expectedOutput += '\\n';\n }\n\n return { command: command.trim(), expectedOutput, expectedExitCode };\n}\n","import { spawn } from 'node:child_process';\nimport { mkdtemp, realpath, rm } from 'node:fs/promises';\nimport { tmpdir } from 'node:os';\nimport { join, dirname, resolve } from 'node:path';\nimport treeKill from 'tree-kill';\nimport type { TestBlock, TestBlockResult } from './types.js';\nimport type { TryscriptConfig } from './config.js';\n\n/** Default timeout in milliseconds */\nconst DEFAULT_TIMEOUT = 30_000;\n\n/**\n * Execution context for a test file.\n * Created once per file, contains the temp directory.\n */\nexport interface ExecutionContext {\n /** Temporary directory for this test file (resolved, no symlinks) */\n tempDir: string;\n /** Directory containing the test file (for portable test commands) */\n testDir: string;\n /** Resolved binary path */\n binPath: string;\n /** Environment variables */\n env: Record<string, string>;\n /** Timeout per command */\n timeout: number;\n}\n\n/**\n * Create an execution context for a test file.\n */\nexport async function createExecutionContext(\n config: TryscriptConfig,\n testFilePath: string,\n): Promise<ExecutionContext> {\n // Create temp directory and resolve symlinks (e.g., /var -> /private/var on macOS)\n // This ensures [CWD] and [ROOT] patterns match pwd output\n const rawTempDir = await mkdtemp(join(tmpdir(), 'tryscript-'));\n const tempDir = await realpath(rawTempDir);\n\n // Resolve test file directory for portable test commands\n const testDir = resolve(dirname(testFilePath));\n\n // Resolve binary path relative to test file directory\n let binPath = config.bin ?? '';\n if (binPath && !binPath.startsWith('/')) {\n binPath = join(testDir, binPath);\n }\n\n return {\n tempDir,\n testDir,\n binPath,\n env: {\n ...process.env,\n ...config.env,\n // Disable colors by default for deterministic output\n NO_COLOR: config.env?.NO_COLOR ?? '1',\n FORCE_COLOR: '0',\n // Provide test directory for portable test commands\n TRYSCRIPT_TEST_DIR: testDir,\n } as Record<string, string>,\n timeout: config.timeout ?? DEFAULT_TIMEOUT,\n };\n}\n\n/**\n * Clean up execution context (remove temp directory).\n */\nexport async function cleanupExecutionContext(ctx: ExecutionContext): Promise<void> {\n await rm(ctx.tempDir, { recursive: true, force: true });\n}\n\n/**\n * Run a single test block and return the result.\n */\nexport async function runBlock(block: TestBlock, ctx: ExecutionContext): Promise<TestBlockResult> {\n const startTime = Date.now();\n\n try {\n const { output, exitCode } = await executeCommand(block.command, ctx);\n\n const duration = Date.now() - startTime;\n\n return {\n block,\n passed: true, // Matching handled separately\n actualOutput: output,\n actualExitCode: exitCode,\n duration,\n };\n } catch (error) {\n const duration = Date.now() - startTime;\n const message = error instanceof Error ? error.message : String(error);\n\n return {\n block,\n passed: false,\n actualOutput: '',\n actualExitCode: -1,\n duration,\n error: message,\n };\n }\n}\n\n/**\n * Execute a command and capture output.\n */\nasync function executeCommand(\n command: string,\n ctx: ExecutionContext,\n): Promise<{ output: string; exitCode: number }> {\n return new Promise((resolve, reject) => {\n const proc = spawn(command, {\n shell: true,\n cwd: ctx.tempDir,\n env: ctx.env as NodeJS.ProcessEnv,\n // Pipe both to capture\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n\n const chunks: Buffer[] = [];\n\n // Capture data as it comes in to preserve order\n proc.stdout.on('data', (data: Buffer) => chunks.push(data));\n proc.stderr.on('data', (data: Buffer) => chunks.push(data));\n\n const timeoutId = setTimeout(() => {\n if (proc.pid) {\n treeKill(proc.pid, 'SIGKILL');\n }\n reject(new Error(`Command timed out after ${ctx.timeout}ms`));\n }, ctx.timeout);\n\n proc.on('close', (code) => {\n clearTimeout(timeoutId);\n const output = Buffer.concat(chunks).toString('utf-8');\n resolve({\n output,\n exitCode: code ?? 0,\n });\n });\n\n proc.on('error', (err) => {\n clearTimeout(timeoutId);\n reject(err);\n });\n });\n}\n","import stripAnsi from 'strip-ansi';\n\n/**\n * Escape special regex characters in a string.\n */\nfunction escapeRegex(str: string): string {\n return str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n// Marker prefix for patterns (uses Unicode private use chars that won't appear in normal output)\nconst MARKER = '\\uE000';\n\n/**\n * Convert expected output with elision patterns to a regex.\n *\n * Handles (matching trycmd):\n * - [..] — matches any characters on the same line (trycmd: [^\\n]*?)\n * - ... — matches zero or more complete lines (trycmd: \\n(([^\\n]*\\n)*)?)\n * - [EXE] — matches .exe on Windows, empty otherwise\n * - [ROOT] — replaced with test root directory (pre-processed)\n * - [CWD] — replaced with current working directory (pre-processed)\n * - Custom [NAME] patterns from config (trycmd: TestCases::insert_var)\n */\nfunction patternToRegex(\n expected: string,\n customPatterns: Record<string, string | RegExp> = {},\n): RegExp {\n // Build a map of markers to their regex replacements\n const replacements = new Map<string, string>();\n let markerIndex = 0;\n\n const getMarker = (): string => {\n return `${MARKER}${markerIndex++}${MARKER}`;\n };\n\n let processed = expected;\n\n // Replace [..] with marker\n const dotdotMarker = getMarker();\n replacements.set(dotdotMarker, '[^\\\\n]*');\n processed = processed.replaceAll('[..]', dotdotMarker);\n\n // Replace ... (followed by newline) with marker\n const ellipsisMarker = getMarker();\n replacements.set(ellipsisMarker, '(?:[^\\\\n]*\\\\n)*');\n processed = processed.replace(/\\.\\.\\.\\n/g, ellipsisMarker);\n\n // Replace [EXE] with marker\n const exeMarker = getMarker();\n const exe = process.platform === 'win32' ? '\\\\.exe' : '';\n replacements.set(exeMarker, exe);\n processed = processed.replaceAll('[EXE]', exeMarker);\n\n // Replace custom patterns with markers\n for (const [name, pattern] of Object.entries(customPatterns)) {\n const placeholder = `[${name}]`;\n const patternStr = pattern instanceof RegExp ? pattern.source : pattern;\n const marker = getMarker();\n replacements.set(marker, `(${patternStr})`);\n processed = processed.replaceAll(placeholder, marker);\n }\n\n // Escape special regex characters\n let regex = escapeRegex(processed);\n\n // Restore markers to their regex replacements\n for (const [marker, replacement] of replacements) {\n regex = regex.replaceAll(escapeRegex(marker), replacement);\n }\n\n // Match the entire string (dotall mode for . to match newlines if needed)\n return new RegExp(`^${regex}$`, 's');\n}\n\n/**\n * Pre-process expected output to replace path placeholders with actual paths.\n * This happens BEFORE pattern matching.\n */\nfunction preprocessPaths(expected: string, context: { root: string; cwd: string }): string {\n let result = expected;\n // Normalize paths for comparison (use forward slashes)\n const normalizedRoot = context.root.replace(/\\\\/g, '/');\n const normalizedCwd = context.cwd.replace(/\\\\/g, '/');\n result = result.replaceAll('[ROOT]', normalizedRoot);\n result = result.replaceAll('[CWD]', normalizedCwd);\n return result;\n}\n\n/**\n * Normalize actual output for comparison.\n * - Remove ANSI escape codes (colors, etc.)\n * - Normalize line endings to \\n\n * - Normalize paths (Windows backslashes to forward slashes)\n * - Trim trailing whitespace from lines\n * - Ensure single trailing newline\n */\nexport function normalizeOutput(output: string): string {\n // Remove ANSI escape codes first\n let normalized = stripAnsi(output);\n\n normalized = normalized\n .replace(/\\r\\n/g, '\\n')\n .replace(/\\r/g, '\\n')\n .split('\\n')\n .map((line) => line.trimEnd())\n .join('\\n')\n .replace(/\\n+$/, '\\n');\n\n // Handle empty output\n if (normalized === '\\n') {\n normalized = '';\n }\n\n return normalized;\n}\n\n/**\n * Check if actual output matches expected pattern.\n */\nexport function matchOutput(\n actual: string,\n expected: string,\n context: { root: string; cwd: string },\n customPatterns: Record<string, string | RegExp> = {},\n): boolean {\n const normalizedActual = normalizeOutput(actual);\n const normalizedExpected = normalizeOutput(expected);\n\n // Empty expected matches empty actual\n if (normalizedExpected === '' && normalizedActual === '') {\n return true;\n }\n\n const preprocessed = preprocessPaths(normalizedExpected, context);\n const regex = patternToRegex(preprocessed, customPatterns);\n return regex.test(normalizedActual);\n}\n","// Public API exports\n\n// Version constant (injected at build time)\ndeclare const __VERSION__: string;\nexport const VERSION: string = typeof __VERSION__ !== 'undefined' ? __VERSION__ : 'development';\n\n// Config helper\nexport { defineConfig } from './lib/config.js';\nexport type { TryscriptConfig } from './lib/config.js';\n\n// Types\nexport type {\n TestConfig,\n TestBlock,\n TestFile,\n TestBlockResult,\n TestFileResult,\n TestRunSummary,\n} from './lib/types.js';\n\n// Core functions (for programmatic use)\nexport { parseTestFile } from './lib/parser.js';\nexport { runBlock, createExecutionContext, cleanupExecutionContext } from './lib/runner.js';\nexport type { ExecutionContext } from './lib/runner.js';\nexport { matchOutput, normalizeOutput } from './lib/matcher.js';\n"],"mappings":"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"}