memd-cli 3.0.1 → 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +20 -0
- package/README.md +25 -0
- package/main.js +1 -0
- package/package.json +2 -2
- package/test/memd.test.js +91 -99
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(echo:*)",
|
|
5
|
+
"Bash(FORCE_COLOR=1 echo:*)",
|
|
6
|
+
"Bash(script:*)",
|
|
7
|
+
"Bash(FORCE_COLOR=1 node:*)",
|
|
8
|
+
"Bash(node -e \"import\\('beautiful-mermaid/src/ascii/ansi.ts'\\).then\\(m=>console.log\\(Object.keys\\(m\\)\\)\\).catch\\(e=>console.error\\('ERROR:',e.message\\)\\)\" 2>&1 | head -5)",
|
|
9
|
+
"Bash(node main.js test/test-highlight.md --no-pager --theme catppuccin-mocha 2>&1 | head -3 | xxd | head -10)",
|
|
10
|
+
"Bash(FORCE_COLOR=1 node main.js test/test-highlight.md --no-pager --theme catppuccin-mocha 2>&1 | head -3 | xxd | head -5)",
|
|
11
|
+
"Bash(FORCE_COLOR=1 node main.js test/test-highlight.md --no-pager --theme zinc-dark 2>&1 | head -3 | xxd | head -5)",
|
|
12
|
+
"mcp__grep-github__searchGitHub",
|
|
13
|
+
"Bash(npx npm-check-updates:*)",
|
|
14
|
+
"Bash(MEMD_THEME= pnpm test 2>&1 | tail -20)",
|
|
15
|
+
"Bash(MEMD_THEME= npx vitest run 2>&1)",
|
|
16
|
+
"Bash(node:*)",
|
|
17
|
+
"Bash(printf:*)"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
}
|
package/README.md
CHANGED
|
@@ -363,6 +363,31 @@ Note:
|
|
|
363
363
|
* Each worker loads the Mermaid rendering library in an independent V8 isolate. Each worker consumes approximately 80-120 MB of memory. The default is `min(num_CPUs-1, 4)` workers. In memory-constrained environments, specify `--workers 1`. Recommended memory: 512 MB + (number of workers x 120 MB).
|
|
364
364
|
|
|
365
365
|
|
|
366
|
+
## Environment Variables
|
|
367
|
+
|
|
368
|
+
| Variable | Description | Default |
|
|
369
|
+
|---|---|---|
|
|
370
|
+
| `MEMD_THEME` | Color theme for both terminal and HTML output | `nord` |
|
|
371
|
+
| `NO_COLOR` | Disable colored terminal output (any value) | _(unset)_ |
|
|
372
|
+
| `NO_PAGER` | Disable pager (any value) | _(unset)_ |
|
|
373
|
+
| `PAGER` | Pager command | `less` |
|
|
374
|
+
| `MEMD_SERVE_RESPAWN_MAX` | Max worker respawns before marking dead | `5` |
|
|
375
|
+
| `MEMD_SERVE_RESPAWN_WINDOW_MS` | Time window for respawn limit (ms) | `60000` |
|
|
376
|
+
| `MEMD_SERVE_RENDER_TIMEOUT_MS` | Per-request render timeout (ms) | `30000` |
|
|
377
|
+
| `MEMD_SERVE_DEAD_RECOVERY_MS` | Cooldown before recovering a dead worker (ms) | `300000` |
|
|
378
|
+
| `MEMD_SERVE_CACHE_MAX_ENTRIES` | Max number of cached rendered pages | `200` |
|
|
379
|
+
| `MEMD_SERVE_CACHE_MAX_BYTES` | Max total bytes for render cache | `52428800` (50 MB) |
|
|
380
|
+
| `MEMD_SERVE_MD_MAX_SIZE` | Max markdown file size to render (bytes) | `10485760` (10 MB) |
|
|
381
|
+
| `MEMD_SERVE_GZIP_CACHE_MAX` | Max number of cached gzip responses | `200` |
|
|
382
|
+
|
|
383
|
+
```bash
|
|
384
|
+
# Examples
|
|
385
|
+
MEMD_THEME=github-dark memd README.md
|
|
386
|
+
MEMD_THEME=dracula memd serve
|
|
387
|
+
NO_COLOR=1 memd README.md
|
|
388
|
+
MEMD_SERVE_RENDER_TIMEOUT_MS=60000 memd serve
|
|
389
|
+
```
|
|
390
|
+
|
|
366
391
|
## Author
|
|
367
392
|
|
|
368
393
|
[ktrysmt](https://github.com/ktrysmt)
|
package/main.js
CHANGED
|
@@ -566,6 +566,7 @@ async function main() {
|
|
|
566
566
|
.name('memd')
|
|
567
567
|
.version(packageJson.version, '-v, --version', 'output the version number')
|
|
568
568
|
.description('Render markdown with mermaid diagrams')
|
|
569
|
+
.enablePositionalOptions()
|
|
569
570
|
.argument('[files...]', 'markdown file(s) to render')
|
|
570
571
|
.option('--no-pager', 'disable pager (less)')
|
|
571
572
|
.option('--no-mouse', 'disable mouse scroll in pager')
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memd-cli",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "main.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"memd": "main.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"test": "vitest run"
|
|
10
|
+
"test": "vitest run --maxConcurrency=20"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"beautiful-mermaid": "^1.1.3",
|
package/test/memd.test.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
|
2
2
|
import { launchTerminal } from 'tuistory'
|
|
3
|
-
import { execSync, spawn } from 'child_process'
|
|
3
|
+
import { execSync, execFile, execFileSync, spawn } from 'child_process'
|
|
4
4
|
import fs from 'fs'
|
|
5
5
|
import path from 'path'
|
|
6
6
|
import { fileURLToPath } from 'url'
|
|
@@ -11,30 +11,25 @@ const MAIN = path.join(__dirname, '..', 'main.js')
|
|
|
11
11
|
// Strip MEMD_THEME from env so tests always get the built-in default (nord)
|
|
12
12
|
delete process.env.MEMD_THEME
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
waitForData: false,
|
|
21
|
-
})
|
|
22
|
-
const waitText = waitFor ?? (t => t.trim().length > 0)
|
|
23
|
-
const output = await session.text({ waitFor: waitText, timeout: 8000 })
|
|
24
|
-
return output.trim()
|
|
14
|
+
function run(args) {
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
execFile('node', [MAIN, ...args], { encoding: 'utf-8', timeout: 15000 }, (err, stdout, stderr) => {
|
|
17
|
+
resolve(((stdout ?? '') + (stderr ?? '')).trim())
|
|
18
|
+
})
|
|
19
|
+
})
|
|
25
20
|
}
|
|
26
21
|
|
|
27
22
|
function runSync(args) {
|
|
28
|
-
return
|
|
23
|
+
return execFileSync('node', [MAIN, ...args], { encoding: 'utf-8', timeout: 15000 })
|
|
29
24
|
}
|
|
30
25
|
|
|
31
26
|
describe('memd CLI', () => {
|
|
32
|
-
it('--version', async () => {
|
|
27
|
+
it.concurrent('--version', async () => {
|
|
33
28
|
const output = await run(['-v'])
|
|
34
|
-
expect(output).toContain('3.0.
|
|
29
|
+
expect(output).toContain('3.0.2')
|
|
35
30
|
})
|
|
36
31
|
|
|
37
|
-
it('--help', async () => {
|
|
32
|
+
it.concurrent('--help', async () => {
|
|
38
33
|
const output = await run(['--help'])
|
|
39
34
|
expect(output).toContain('Usage: memd')
|
|
40
35
|
expect(output).toContain('--no-pager')
|
|
@@ -47,7 +42,6 @@ describe('memd CLI', () => {
|
|
|
47
42
|
it('renders test1.md (basic markdown + mermaid)', async () => {
|
|
48
43
|
const output = await run(
|
|
49
44
|
['--no-pager', '--no-color', '--width', '80', 'test/test1.md'],
|
|
50
|
-
{ waitFor: t => t.includes('More text.') },
|
|
51
45
|
)
|
|
52
46
|
expect(output).toMatchInlineSnapshot(`
|
|
53
47
|
"# Hello
|
|
@@ -63,10 +57,9 @@ describe('memd CLI', () => {
|
|
|
63
57
|
`)
|
|
64
58
|
})
|
|
65
59
|
|
|
66
|
-
it('renders test2.md (complex mermaid diagram)', async () => {
|
|
60
|
+
it.concurrent('renders test2.md (complex mermaid diagram)', async () => {
|
|
67
61
|
const output = await run(
|
|
68
62
|
['--no-pager', '--no-color', '--width', '80', 'test/test2.md'],
|
|
69
|
-
{ waitFor: t => t.includes('More text after the diagram.') },
|
|
70
63
|
)
|
|
71
64
|
expect(output).toContain('Start')
|
|
72
65
|
expect(output).toContain('Decision?')
|
|
@@ -75,10 +68,9 @@ describe('memd CLI', () => {
|
|
|
75
68
|
expect(output).toContain('More text after the diagram.')
|
|
76
69
|
})
|
|
77
70
|
|
|
78
|
-
it('--ascii renders ASCII-only diagram', async () => {
|
|
71
|
+
it.concurrent('--ascii renders ASCII-only diagram', async () => {
|
|
79
72
|
const output = await run(
|
|
80
73
|
['--no-pager', '--no-color', '--width', '80', '--ascii', 'test/test1.md'],
|
|
81
|
-
{ waitFor: t => t.includes('More text.') },
|
|
82
74
|
)
|
|
83
75
|
expect(output).toContain('+---+')
|
|
84
76
|
expect(output).toContain('---->')
|
|
@@ -86,35 +78,23 @@ describe('memd CLI', () => {
|
|
|
86
78
|
expect(output).not.toContain('►')
|
|
87
79
|
})
|
|
88
80
|
|
|
89
|
-
it('--no-color strips ANSI escape codes', async () => {
|
|
81
|
+
it.concurrent('--no-color strips ANSI escape codes', async () => {
|
|
90
82
|
const output = await run(
|
|
91
83
|
['--no-pager', '--no-color', '--width', '80', 'test/test1.md'],
|
|
92
|
-
{ waitFor: t => t.includes('More text.') },
|
|
93
84
|
)
|
|
94
85
|
// eslint-disable-next-line no-control-regex
|
|
95
86
|
expect(output).not.toMatch(/\x1b\[[\d;]*m/)
|
|
96
87
|
})
|
|
97
88
|
|
|
98
|
-
it('error on missing file', async () => {
|
|
99
|
-
const
|
|
100
|
-
command: 'node',
|
|
101
|
-
args: [MAIN, '--no-pager', 'test/nonexistent.md'],
|
|
102
|
-
cols: 80,
|
|
103
|
-
rows: 10,
|
|
104
|
-
waitForData: false,
|
|
105
|
-
})
|
|
106
|
-
const output = (await session.text({
|
|
107
|
-
waitFor: t => t.includes('Error'),
|
|
108
|
-
timeout: 8000,
|
|
109
|
-
})).trim()
|
|
89
|
+
it.concurrent('error on missing file', async () => {
|
|
90
|
+
const output = await run(['--no-pager', 'test/nonexistent.md'])
|
|
110
91
|
expect(output).toContain('Error reading file')
|
|
111
92
|
expect(output).toContain('nonexistent.md')
|
|
112
93
|
})
|
|
113
94
|
|
|
114
|
-
it('renders test-br.md (<br> tag line breaks)', async () => {
|
|
95
|
+
it.concurrent('renders test-br.md (<br> tag line breaks)', async () => {
|
|
115
96
|
const output = await run(
|
|
116
97
|
['--no-pager', '--no-color', '--width', '80', 'test/test-br.md'],
|
|
117
|
-
{ waitFor: t => t.includes('All three variants') },
|
|
118
98
|
)
|
|
119
99
|
// Each node should have its label split across two lines
|
|
120
100
|
expect(output).toContain('Line1')
|
|
@@ -130,10 +110,9 @@ describe('memd CLI', () => {
|
|
|
130
110
|
expect(diagramSection).not.toMatch(/<br\s*\/?>/)
|
|
131
111
|
})
|
|
132
112
|
|
|
133
|
-
it('renders test-cjk.md (Japanese labels)', async () => {
|
|
113
|
+
it.concurrent('renders test-cjk.md (Japanese labels)', async () => {
|
|
134
114
|
const output = await run(
|
|
135
115
|
['--no-pager', '--no-color', '--width', '80', 'test/test-cjk.md'],
|
|
136
|
-
{ waitFor: t => t.includes('Japanese labels') },
|
|
137
116
|
)
|
|
138
117
|
expect(output).toContain('開始')
|
|
139
118
|
expect(output).toContain('判定')
|
|
@@ -143,25 +122,18 @@ describe('memd CLI', () => {
|
|
|
143
122
|
expect(output).toContain('いいえ')
|
|
144
123
|
})
|
|
145
124
|
|
|
146
|
-
it('reads markdown from stdin via shell',
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
rows: 10,
|
|
152
|
-
waitForData: false,
|
|
153
|
-
})
|
|
154
|
-
const output = (await session.text({
|
|
155
|
-
waitFor: t => t.includes('stdin test'),
|
|
156
|
-
timeout: 8000,
|
|
157
|
-
})).trim()
|
|
125
|
+
it('reads markdown from stdin via shell', () => {
|
|
126
|
+
const output = execSync(
|
|
127
|
+
`echo '# stdin test' | node ${MAIN} --no-pager --no-color`,
|
|
128
|
+
{ encoding: 'utf-8', timeout: 15000 },
|
|
129
|
+
).trim()
|
|
158
130
|
expect(output).toContain('stdin test')
|
|
159
131
|
})
|
|
160
132
|
|
|
161
133
|
// --html output tests
|
|
162
134
|
describe('--html output', () => {
|
|
163
135
|
it('--html + file -> stdout', () => {
|
|
164
|
-
const output = runSync('--html test/test1.md')
|
|
136
|
+
const output = runSync(['--html', 'test/test1.md'])
|
|
165
137
|
expect(output).toContain('<!DOCTYPE html>')
|
|
166
138
|
expect(output).toContain('<svg')
|
|
167
139
|
// Contains theme CSS colors (nord default: bg=#2e3440, fg=#d8dee9)
|
|
@@ -184,7 +156,7 @@ describe('memd CLI', () => {
|
|
|
184
156
|
})
|
|
185
157
|
|
|
186
158
|
it('--html + multiple Mermaid blocks have unique marker IDs', () => {
|
|
187
|
-
const output = runSync('--html test/test3.md')
|
|
159
|
+
const output = runSync(['--html', 'test/test3.md'])
|
|
188
160
|
expect(output).toContain('<svg')
|
|
189
161
|
// Check for prefixed marker IDs (m0-, m1-, etc.)
|
|
190
162
|
expect(output).toMatch(/id="m0-/)
|
|
@@ -192,7 +164,7 @@ describe('memd CLI', () => {
|
|
|
192
164
|
})
|
|
193
165
|
|
|
194
166
|
it('--html + multiple files -> combined single HTML', () => {
|
|
195
|
-
const output = runSync('--html test/test1.md test/test2.md')
|
|
167
|
+
const output = runSync(['--html', 'test/test1.md', 'test/test2.md'])
|
|
196
168
|
expect(output).toContain('<!DOCTYPE html>')
|
|
197
169
|
// Content from both files
|
|
198
170
|
expect(output).toContain('Hello')
|
|
@@ -206,13 +178,13 @@ describe('memd CLI', () => {
|
|
|
206
178
|
// Theme tests (HTML path)
|
|
207
179
|
describe('theme (HTML path)', () => {
|
|
208
180
|
it('--html --theme dracula uses dracula colors', () => {
|
|
209
|
-
const output = runSync('--html --theme dracula test/test1.md')
|
|
181
|
+
const output = runSync(['--html', '--theme', 'dracula', 'test/test1.md'])
|
|
210
182
|
expect(output).toContain('#282a36') // bg
|
|
211
183
|
expect(output).toContain('#f8f8f2') // fg
|
|
212
184
|
})
|
|
213
185
|
|
|
214
186
|
it('--html --theme tokyo-night uses tokyo-night colors', () => {
|
|
215
|
-
const output = runSync('--html --theme tokyo-night test/test1.md')
|
|
187
|
+
const output = runSync(['--html', '--theme', 'tokyo-night', 'test/test1.md'])
|
|
216
188
|
expect(output).toContain('#1a1b26') // bg
|
|
217
189
|
expect(output).toContain('#a9b1d6') // fg
|
|
218
190
|
})
|
|
@@ -224,7 +196,7 @@ describe('memd CLI', () => {
|
|
|
224
196
|
})
|
|
225
197
|
|
|
226
198
|
it('--html --no-color outputs full color HTML (silently ignored)', () => {
|
|
227
|
-
const output = runSync('--html --no-color test/test1.md')
|
|
199
|
+
const output = runSync(['--html', '--no-color', 'test/test1.md'])
|
|
228
200
|
expect(output).toContain('<!DOCTYPE html>')
|
|
229
201
|
expect(output).toContain('#2e3440')
|
|
230
202
|
})
|
|
@@ -232,53 +204,39 @@ describe('memd CLI', () => {
|
|
|
232
204
|
|
|
233
205
|
// Theme tests (terminal path)
|
|
234
206
|
describe('theme (terminal path)', () => {
|
|
235
|
-
it('--theme dracula renders terminal output', async () => {
|
|
207
|
+
it.concurrent('--theme dracula renders terminal output', async () => {
|
|
236
208
|
const output = await run(
|
|
237
209
|
['--no-pager', '--no-color', '--theme', 'dracula', 'test/test1.md'],
|
|
238
|
-
{ waitFor: t => t.includes('More text.') },
|
|
239
210
|
)
|
|
240
211
|
expect(output).toContain('Hello')
|
|
241
212
|
expect(output).toContain('More text.')
|
|
242
213
|
})
|
|
243
214
|
|
|
244
|
-
it('--theme tokyo-night (no highlight) renders terminal output', async () => {
|
|
215
|
+
it.concurrent('--theme tokyo-night (no highlight) renders terminal output', async () => {
|
|
245
216
|
const output = await run(
|
|
246
217
|
['--no-pager', '--no-color', '--theme', 'tokyo-night', 'test/test1.md'],
|
|
247
|
-
{ waitFor: t => t.includes('More text.') },
|
|
248
218
|
)
|
|
249
219
|
expect(output).toContain('Hello')
|
|
250
220
|
expect(output).toContain('More text.')
|
|
251
221
|
})
|
|
252
222
|
|
|
253
|
-
it('--theme one-dark renders terminal output', async () => {
|
|
223
|
+
it.concurrent('--theme one-dark renders terminal output', async () => {
|
|
254
224
|
const output = await run(
|
|
255
225
|
['--no-pager', '--no-color', '--theme', 'one-dark', 'test/test1.md'],
|
|
256
|
-
{ waitFor: t => t.includes('More text.') },
|
|
257
226
|
)
|
|
258
227
|
expect(output).toContain('Hello')
|
|
259
228
|
expect(output).toContain('More text.')
|
|
260
229
|
})
|
|
261
230
|
|
|
262
|
-
it('--theme nonexistent exits with error', async () => {
|
|
263
|
-
const
|
|
264
|
-
command: 'node',
|
|
265
|
-
args: [MAIN, '--no-pager', '--no-color', '--theme', 'nonexistent', 'test/test1.md'],
|
|
266
|
-
cols: 80,
|
|
267
|
-
rows: 10,
|
|
268
|
-
waitForData: false,
|
|
269
|
-
})
|
|
270
|
-
const output = (await session.text({
|
|
271
|
-
waitFor: t => t.includes('Unknown theme'),
|
|
272
|
-
timeout: 8000,
|
|
273
|
-
})).trim()
|
|
231
|
+
it.concurrent('--theme nonexistent exits with error', async () => {
|
|
232
|
+
const output = await run(['--no-pager', '--no-color', '--theme', 'nonexistent', 'test/test1.md'])
|
|
274
233
|
expect(output).toContain('Unknown theme')
|
|
275
234
|
expect(output).toContain('Available themes')
|
|
276
235
|
})
|
|
277
236
|
|
|
278
|
-
it('default theme is nord', async () => {
|
|
237
|
+
it.concurrent('default theme is nord', async () => {
|
|
279
238
|
const output = await run(
|
|
280
239
|
['--no-pager', '--no-color', 'test/test1.md'],
|
|
281
|
-
{ waitFor: t => t.includes('More text.') },
|
|
282
240
|
)
|
|
283
241
|
expect(output).toContain('Hello')
|
|
284
242
|
expect(output).toContain('More text.')
|
|
@@ -317,7 +275,7 @@ describe('memd CLI', () => {
|
|
|
317
275
|
})
|
|
318
276
|
|
|
319
277
|
// chalk.level = 0 + Shiki: verify no ANSI codes for all themes
|
|
320
|
-
describe('--no-color strips ANSI from all themes', () => {
|
|
278
|
+
describe.concurrent('--no-color strips ANSI from all themes', () => {
|
|
321
279
|
const themes = [
|
|
322
280
|
'nord', 'dracula', 'one-dark', 'github-dark', 'github-light',
|
|
323
281
|
'solarized-dark', 'solarized-light', 'catppuccin-mocha', 'catppuccin-latte',
|
|
@@ -329,7 +287,6 @@ describe('memd CLI', () => {
|
|
|
329
287
|
it(`--theme ${theme} --no-color has no ANSI codes`, async () => {
|
|
330
288
|
const output = await run(
|
|
331
289
|
['--no-pager', '--no-color', '--theme', theme, 'test/test-highlight.md'],
|
|
332
|
-
{ waitFor: t => t.includes('TypeScript') },
|
|
333
290
|
)
|
|
334
291
|
// eslint-disable-next-line no-control-regex
|
|
335
292
|
expect(output).not.toMatch(/\x1b\[[\d;]*m/)
|
|
@@ -347,37 +304,72 @@ describe('memd CLI', () => {
|
|
|
347
304
|
expect(output).toMatch(/\x1b\[38;2;\d+;\d+;\d+m/)
|
|
348
305
|
})
|
|
349
306
|
|
|
350
|
-
it('unknown language falls back to plain text without errors',
|
|
307
|
+
it('unknown language falls back to plain text without errors', () => {
|
|
308
|
+
const output = execSync(`node ${MAIN} --no-pager --no-color`, {
|
|
309
|
+
encoding: 'utf-8',
|
|
310
|
+
timeout: 15000,
|
|
311
|
+
input: '# Test\n\n```unknownlang\nsome code\n```',
|
|
312
|
+
}).trim()
|
|
313
|
+
expect(output).toContain('some code')
|
|
314
|
+
expect(output).not.toContain('Error')
|
|
315
|
+
expect(output).not.toContain('Could not find the language')
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
it('cli-highlight is not invoked (no highlight.js errors in output)', () => {
|
|
319
|
+
const output = execSync(`node ${MAIN} --no-pager --no-color`, {
|
|
320
|
+
encoding: 'utf-8',
|
|
321
|
+
timeout: 15000,
|
|
322
|
+
input: '# Test\n\n```rust\nfn main() {}\n```',
|
|
323
|
+
}).trim()
|
|
324
|
+
expect(output).toContain('fn main')
|
|
325
|
+
expect(output).not.toContain('Could not find the language')
|
|
326
|
+
})
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
describe('TTY behavior (PTY required)', () => {
|
|
330
|
+
it('auto-detects color in TTY (no --no-color)', async () => {
|
|
331
|
+
const session = await launchTerminal({
|
|
332
|
+
command: 'node',
|
|
333
|
+
args: [MAIN, '--no-pager', '--width', '80', 'test/test-highlight.md'],
|
|
334
|
+
cols: 80,
|
|
335
|
+
rows: 30,
|
|
336
|
+
waitForData: false,
|
|
337
|
+
})
|
|
338
|
+
await session.text({ waitFor: t => t.includes('TypeScript'), timeout: 8000 })
|
|
339
|
+
const data = await session.getTerminalData()
|
|
340
|
+
const hasColor = data.lines.some(line =>
|
|
341
|
+
line.spans.some(span => span.fg !== null)
|
|
342
|
+
)
|
|
343
|
+
expect(hasColor).toBe(true)
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
it('pager activates for long output in TTY (no --no-pager)', async () => {
|
|
347
|
+
// test3.md is long enough to exceed terminal rows
|
|
351
348
|
const session = await launchTerminal({
|
|
352
|
-
command: '
|
|
353
|
-
args: ['-
|
|
349
|
+
command: 'node',
|
|
350
|
+
args: [MAIN, '--no-color', '--width', '80', 'test/test3.md'],
|
|
354
351
|
cols: 80,
|
|
355
352
|
rows: 10,
|
|
356
353
|
waitForData: false,
|
|
357
354
|
})
|
|
358
|
-
const output =
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
expect(output).toContain('some code')
|
|
363
|
-
expect(output).not.toContain('Error')
|
|
364
|
-
expect(output).not.toContain('Could not find the language')
|
|
355
|
+
const output = await session.text({ timeout: 8000 })
|
|
356
|
+
// less shows ':' or '(END)' prompt; partial output means pager is holding
|
|
357
|
+
expect(output).not.toContain('Error Handling Example')
|
|
358
|
+
await session.write('q')
|
|
365
359
|
})
|
|
366
360
|
|
|
367
|
-
it('
|
|
361
|
+
it('pager quit with q exits cleanly', async () => {
|
|
368
362
|
const session = await launchTerminal({
|
|
369
|
-
command: '
|
|
370
|
-
args: ['-
|
|
363
|
+
command: 'node',
|
|
364
|
+
args: [MAIN, '--no-color', '--width', '80', 'test/test3.md'],
|
|
371
365
|
cols: 80,
|
|
372
366
|
rows: 10,
|
|
373
367
|
waitForData: false,
|
|
374
368
|
})
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
expect(output).toContain('fn main')
|
|
380
|
-
expect(output).not.toContain('Could not find the language')
|
|
369
|
+
await session.text({ timeout: 8000 })
|
|
370
|
+
await session.write('q')
|
|
371
|
+
// After quitting, session should end without error
|
|
372
|
+
await session.waitIdle({ timeout: 3000 })
|
|
381
373
|
})
|
|
382
374
|
})
|
|
383
375
|
|
|
@@ -414,7 +406,7 @@ describe('memd serve', () => {
|
|
|
414
406
|
})
|
|
415
407
|
|
|
416
408
|
it('serve --help shows options', () => {
|
|
417
|
-
const output = runSync('serve --help')
|
|
409
|
+
const output = runSync(['serve', '--help'])
|
|
418
410
|
expect(output).toContain('-d, --dir')
|
|
419
411
|
expect(output).toContain('--port')
|
|
420
412
|
expect(output).toContain('--host')
|