memd-cli 3.0.0 → 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/README.md CHANGED
@@ -11,8 +11,8 @@ npm install -g memd-cli
11
11
  ## Usage
12
12
 
13
13
 
14
- ```
15
- Usage: memd [options] [files...]
14
+ ```sh
15
+ Usage: memd [options] [command] [files...]
16
16
 
17
17
  Render markdown with mermaid diagrams
18
18
 
@@ -27,12 +27,13 @@ Options:
27
27
  --width <number> terminal width override
28
28
  --ascii use pure ASCII mode for diagrams (default: unicode)
29
29
  --html output as standalone HTML (mermaid diagrams rendered as inline SVG)
30
- --theme <name> color theme (default: "nord", env: MEMD_THEME)
31
- zinc-light, zinc-dark, tokyo-night, tokyo-night-storm,
32
- tokyo-night-light, catppuccin-mocha, catppuccin-latte,
33
- nord, nord-light, dracula, github-light, github-dark,
34
- solarized-light, solarized-dark, one-dark
30
+ --theme <name> color theme (env: MEMD_THEME)
31
+ nord, dracula, one-dark, github-dark, github-light, solarized-dark, solarized-light, catppuccin-mocha, catppuccin-latte, tokyo-night, tokyo-night-storm, tokyo-night-light, nord-light,
32
+ zinc-dark, zinc-light (default: "catppuccin-mocha")
35
33
  -h, --help display help for command
34
+
35
+ Commands:
36
+ serve [options] Start HTTP server to serve .md files as HTML
36
37
  ```
37
38
 
38
39
 
@@ -293,45 +294,6 @@ This is regular text between mermaid diagrams.
293
294
 
294
295
  ```
295
296
 
296
- ### Serve
297
-
298
- Start a local HTTP server that renders `.md` files as HTML on the fly.
299
-
300
- ```
301
- $ memd serve
302
- memd serve
303
- Directory: /home/ubuntu/docs
304
- Theme: nord
305
- URL: http://localhost:8888/
306
-
307
- $ memd serve --dir ./docs --port 3000 --theme dracula
308
- $ memd serve --workers 2
309
- $ memd serve --watch
310
- ```
311
-
312
- ```
313
- Usage: memd serve [options]
314
-
315
- Start HTTP server to serve .md files as HTML
316
-
317
- Options:
318
- -d, --dir <path> directory to serve (default: ".")
319
- -p, --port <number> port number (0-65535) (default: 8888)
320
- --host <string> host to bind (default: "127.0.0.1")
321
- --workers <number> number of render workers (default: min(cpus-1, 4))
322
- --watch watch for file changes and live-reload
323
- --theme <name> color theme (env: MEMD_THEME) (default: "nord")
324
- -h, --help display help for command
325
- ```
326
-
327
- > **Note:** `--host 0.0.0.0` を指定するとネットワーク上の全インターフェースにバインドされます。認証機構はないため、ディレクトリ内の `.md` ファイルがネットワーク上から閲覧可能になります。信頼されたネットワーク内でのみ使用してください。
328
- >
329
- > serve コマンドはパス検証とファイル読み取りの間にわずかなタイミング差 (TOCTOU) があります。信頼されたファイルシステム上で使用してください。
330
- >
331
- > serve は `.md` ファイル、画像 (png, jpg, gif, svg, webp, ico, avif)、CSS を配信します。JavaScript やその他のファイルは配信されません。
332
- >
333
- > 各ワーカーは独立した V8 isolate で Mermaid レンダリングライブラリをロードします。ワーカー1つあたり約 80-120 MB のメモリを消費します。デフォルトは `min(CPU数-1, 4)` ワーカーです。メモリが限られた環境では `--workers 1` を指定してください。推奨メモリ: 512 MB + (ワーカー数 x 120 MB)。
334
-
335
297
  ### HTML output
336
298
 
337
299
  HTML is written to stdout. Use shell redirection to save to a file.
@@ -355,28 +317,75 @@ $ echo '# Hello\n\n```mermaid\nflowchart LR\n A --> B\n```' | memd
355
317
  └───┘ └───┘
356
318
  ```
357
319
 
358
- ## Uninstall
320
+ ### Serve
359
321
 
360
- ```bash
361
- npm remove -g memd-cli
322
+ Start a local HTTP server that renders `.md` files as HTML on the fly.
323
+
324
+ ```sh
325
+ $ memd serve --help
326
+ Usage: memd serve [options]
327
+
328
+ Start HTTP server to serve .md files as HTML
329
+
330
+ Options:
331
+ -d, --dir <path> directory to serve (default: ".")
332
+ -p, --port <number> port number (0-65535) (default: 8888)
333
+ --host <string> host to bind (default: "127.0.0.1")
334
+ --workers <number> number of render workers (default: min(cpus-1, 4))
335
+ --watch watch for file changes and live-reload
336
+ --theme <name> color theme (env: MEMD_THEME)
337
+ nord, dracula, one-dark, github-dark, github-light, solarized-dark,
338
+ solarized-light, catppuccin-mocha, catppuccin-latte, tokyo-night,
339
+ tokyo-night-storm, tokyo-night-light, nord-light, zinc-dark, zinc-light (default:
340
+ "catppuccin-mocha")
341
+ -h, --help display help for command
362
342
  ```
363
343
 
364
- ## Development
344
+ Example:
365
345
 
366
- ```bash
367
- node main.js test/test1.md
368
346
  ```
347
+ $ memd serve
348
+ memd serve
349
+ Directory: /home/ubuntu/docs
350
+ Theme: nord
351
+ URL: http://localhost:8888/
352
+
353
+ $ memd serve --dir ./docs --port 3000 --theme dracula
354
+ $ memd serve --workers 2
355
+ $ memd serve --watch
356
+ ```
357
+
358
+ Note:
359
+
360
+ * Specifying `--host 0.0.0.0` binds the server to all network interfaces. Since there is no authentication mechanism, `.md` files in the directory will be accessible over the network. Use only within trusted networks.
361
+ * The serve command has a small timing gap (TOCTOU) between path validation and file reading. Use on trusted filesystems only.
362
+ * Serve serves `.md` files, images (png, jpg, gif, svg, webp, ico, avif), and CSS. JavaScript and other file types are not served.
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
+
365
+
366
+ ## Environment Variables
369
367
 
370
- ## Use specific version
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` |
371
382
 
372
383
  ```bash
373
- # tag
374
- npm install -g git+https://github.com/ktrysmt/memd.git#v2.0.0
375
- # branch
376
- npm install -g git+https://github.com/ktrysmt/memd.git#master
377
- npm install -g git+https://github.com/ktrysmt/memd.git#feature
378
- # hash
379
- npm install -g git+https://github.com/ktrysmt/memd.git#a52a596
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
380
389
  ```
381
390
 
382
391
  ## Author
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.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
- async function run(args, { waitFor = null } = {}) {
15
- const session = await launchTerminal({
16
- command: 'node',
17
- args: [MAIN, ...args],
18
- cols: 80,
19
- rows: 30,
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 execSync(`node ${MAIN} ${args}`, { encoding: 'utf-8', timeout: 15000 })
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.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 session = await launchTerminal({
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', async () => {
147
- const session = await launchTerminal({
148
- command: 'sh',
149
- args: ['-c', `echo '# stdin test' | node ${MAIN} --no-pager --no-color`],
150
- cols: 80,
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 session = await launchTerminal({
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', async () => {
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: 'sh',
353
- args: ['-c', `echo '# Test\n\n\`\`\`unknownlang\nsome code\n\`\`\`' | node ${MAIN} --no-pager --no-color`],
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 = (await session.text({
359
- waitFor: t => t.includes('some code'),
360
- timeout: 8000,
361
- })).trim()
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('cli-highlight is not invoked (no highlight.js errors in output)', async () => {
361
+ it('pager quit with q exits cleanly', async () => {
368
362
  const session = await launchTerminal({
369
- command: 'sh',
370
- args: ['-c', `echo '# Test\n\n\`\`\`rust\nfn main() {}\n\`\`\`' | node ${MAIN} --no-pager`],
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
- const output = (await session.text({
376
- waitFor: t => t.includes('fn main'),
377
- timeout: 8000,
378
- })).trim()
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')