memd-cli 3.2.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CLAUDE.md CHANGED
@@ -41,7 +41,8 @@ node main.js test/test1.md
41
41
  node main.js test/test2.md
42
42
  node main.js test/complex.md
43
43
  node main.js --html test/test1.md # HTML output to stdout
44
- node main.js serve --dir test --port 3000 # HTTP serve mode
44
+ node main.js serve test --port 3000 # HTTP serve mode (directory)
45
+ node main.js serve test/test1.md # HTTP serve mode (single file)
45
46
  ```
46
47
 
47
48
  ## Key CLI flags
@@ -70,4 +71,5 @@ nord-light, zinc-dark, zinc-light
70
71
  - `render-shared.js` converts Mermaid fenced blocks to SVG via `@ktrysmt/beautiful-mermaid`, then renders full HTML with `marked`
71
72
  - `render-worker.js` runs `renderToHTML` in a worker thread for non-blocking serve mode
72
73
  - `render-utils.js` provides theme color resolution and HTML escaping (shared by main.js and render-shared.js)
73
- - Serve mode supports: directory listing, ETag/304 caching, gzip, static file serving (images/css), sidebar navigation, `--watch` with SSE live reload, CSP nonce
74
+ - Serve mode supports: directory or single-file serving, directory listing, ETag/304 caching, gzip, static file serving (images/css), sidebar navigation, `--watch` with SSE live reload, CSP nonce
75
+ - Single-file mode (`serve foo.md`): sets baseDir to the file's parent directory, root `/` redirects to `/<filename>`
package/main.js CHANGED
@@ -707,13 +707,13 @@ async function main() {
707
707
  program
708
708
  .command('serve')
709
709
  .description('Start HTTP server to serve .md files as HTML')
710
- .option('-d, --dir <path>', 'directory to serve', '.')
710
+ .argument('[path]', 'directory or .md file to serve', '.')
711
711
  .option('-p, --port <number>', 'port number (0-65535)', Number, 8888)
712
712
  .option('--host <string>', 'host to bind', '127.0.0.1')
713
713
  .option('--workers <number>', 'number of render workers (default: min(cpus-1, 4))', Number)
714
714
  .option('--watch', 'watch for file changes and live-reload')
715
715
  .option('--theme <name>', `color theme (env: MEMD_THEME)\n${THEME_NAMES.join(', ')}`, process.env.MEMD_THEME || 'nord')
716
- .action(async (options) => {
716
+ .action(async (servePath, options) => {
717
717
  if (!(options.theme in THEME_MAP)) {
718
718
  const names = Object.keys(THEME_MAP).join(', ');
719
719
  console.error(`Unknown theme: ${options.theme}\nAvailable themes: ${names}`);
@@ -732,14 +732,26 @@ async function main() {
732
732
  }
733
733
 
734
734
  let baseDir;
735
+ let singleFile = null; // basename of .md file in single-file mode
736
+ let resolvedServePath;
735
737
  try {
736
- baseDir = fs.realpathSync(path.resolve(options.dir));
738
+ resolvedServePath = fs.realpathSync(path.resolve(servePath));
737
739
  } catch {
738
- console.error(`Directory not found: ${options.dir}`);
740
+ console.error(`Path not found: ${servePath}`);
739
741
  process.exit(1);
740
742
  }
741
- if (!fs.statSync(baseDir).isDirectory()) {
742
- console.error(`Not a directory: ${options.dir}`);
743
+ const serveStat = fs.statSync(resolvedServePath);
744
+ if (serveStat.isFile()) {
745
+ if (!resolvedServePath.endsWith('.md')) {
746
+ console.error(`Not a .md file: ${servePath}`);
747
+ process.exit(1);
748
+ }
749
+ singleFile = path.basename(resolvedServePath);
750
+ baseDir = path.dirname(resolvedServePath);
751
+ } else if (serveStat.isDirectory()) {
752
+ baseDir = resolvedServePath;
753
+ } else {
754
+ console.error(`Not a file or directory: ${servePath}`);
743
755
  process.exit(1);
744
756
  }
745
757
  if (baseDir === '/') {
@@ -1069,6 +1081,14 @@ body:has(.memd-layout) { max-width: none; margin: 0; padding: 0; }
1069
1081
  return;
1070
1082
  }
1071
1083
 
1084
+ // Single-file mode: redirect root to the served file
1085
+ if (singleFile && urlPath === '/') {
1086
+ const target = '/' + encodeURIComponent(singleFile) + parsedUrl.search;
1087
+ res.writeHead(302, { Location: target });
1088
+ res.end();
1089
+ return;
1090
+ }
1091
+
1072
1092
  if (isDotPath(urlPath)) {
1073
1093
  res.writeHead(403, { 'Content-Type': 'text/plain' });
1074
1094
  res.end('Forbidden');
@@ -1277,7 +1297,11 @@ body:has(.memd-layout) { max-width: none; margin: 0; padding: 0; }
1277
1297
  let displayHost = options.host === '0.0.0.0' || options.host === '::' ? 'localhost' : options.host;
1278
1298
  if (displayHost.includes(':')) displayHost = `[${displayHost}]`;
1279
1299
  console.log(`memd serve`);
1280
- console.log(` Directory: ${baseDir}`);
1300
+ if (singleFile) {
1301
+ console.log(` File: ${path.join(baseDir, singleFile)}`);
1302
+ } else {
1303
+ console.log(` Directory: ${baseDir}`);
1304
+ }
1281
1305
  console.log(` Theme: ${options.theme}`);
1282
1306
  if (options.watch) console.log(' Watch: enabled');
1283
1307
  console.log(` URL: http://${displayHost}:${addr.port}/`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memd-cli",
3
- "version": "3.2.0",
3
+ "version": "3.3.0",
4
4
  "type": "module",
5
5
  "main": "main.js",
6
6
  "bin": {
@@ -10,7 +10,7 @@
10
10
  "test": "vitest run --maxConcurrency=20"
11
11
  },
12
12
  "dependencies": {
13
- "@ktrysmt/beautiful-mermaid": "1.4.0",
13
+ "@ktrysmt/beautiful-mermaid": "1.4.1",
14
14
  "chalk": "^5.6.2",
15
15
  "commander": "^14.0.3",
16
16
  "marked": "^17.0.4",
package/test/memd.test.js CHANGED
@@ -26,7 +26,7 @@ function runSync(args) {
26
26
  describe('memd CLI', () => {
27
27
  it.concurrent('--version', async () => {
28
28
  const output = await run(['-v'])
29
- expect(output).toContain('3.2.0')
29
+ expect(output).toContain('3.2.1')
30
30
  })
31
31
 
32
32
  it.concurrent('--help', async () => {
@@ -416,7 +416,7 @@ describe('memd serve', () => {
416
416
  let serverProcess
417
417
 
418
418
  beforeAll(async () => {
419
- serverProcess = spawn('node', [MAIN, 'serve', '--port', String(PORT), '--dir', __dirname, '--host', '127.0.0.1'], {
419
+ serverProcess = spawn('node', [MAIN, 'serve', __dirname, '--port', String(PORT), '--host', '127.0.0.1'], {
420
420
  stdio: ['pipe', 'pipe', 'pipe'],
421
421
  })
422
422
  await new Promise((resolve, reject) => {
@@ -444,7 +444,7 @@ describe('memd serve', () => {
444
444
 
445
445
  it('serve --help shows options', () => {
446
446
  const output = runSync(['serve', '--help'])
447
- expect(output).toContain('-d, --dir')
447
+ expect(output).toContain('[path]')
448
448
  expect(output).toContain('--port')
449
449
  expect(output).toContain('--host')
450
450
  expect(output).toContain('--watch')
@@ -655,7 +655,7 @@ describe('memd serve --watch', () => {
655
655
  let watchProcess
656
656
 
657
657
  beforeAll(async () => {
658
- watchProcess = spawn('node', [MAIN, 'serve', '--port', String(WATCH_PORT), '--dir', __dirname, '--host', '127.0.0.1', '--watch'], {
658
+ watchProcess = spawn('node', [MAIN, 'serve', __dirname, '--port', String(WATCH_PORT), '--host', '127.0.0.1', '--watch'], {
659
659
  stdio: ['pipe', 'pipe', 'pipe'],
660
660
  })
661
661
  await new Promise((resolve, reject) => {
@@ -0,0 +1,26 @@
1
+ ```mermaid
2
+ graph TD
3
+ subgraph WS1["WS1: Blog Publication"]
4
+ B1["Blog #1-6<br>FISC 13th Analysis"]
5
+ B7["Blog #7<br>Cross-Mapping<br>Methods and Challenges"]
6
+ end
7
+
8
+ subgraph WS3["WS3: CODE BLUE CFP"]
9
+ IR["NIST IR 8477<br>Methodology Notes<br>(internal research)"]
10
+ CE["3.3 CIS-NIST-ISO<br>Prior Art Review"]
11
+ AB["3.4 CFP Abstract<br>Draft"]
12
+ SUB["3.12 CFP Submission<br>Jul 10"]
13
+ end
14
+
15
+ subgraph WS4["WS4: C3a SaaS"]
16
+ OSCAL["OSCAL Mapping Model<br>Data Format"]
17
+ end
18
+
19
+ B1 -->|"Establishes<br>domain expertise"| B7
20
+ B7 -->|"Demonstrates<br>practical experience"| AB
21
+ IR -->|"Provides<br>theoretical framework"| AB
22
+ CE -->|"Positions<br>novelty"| AB
23
+ AB --> SUB
24
+ IR -->|"OSCAL compatibility<br>requirement"| OSCAL
25
+ B7 -.->|"Blog #7 references<br>IR 8477 methodology"| IR
26
+ ```
@@ -1,23 +0,0 @@
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
- "WebFetch(domain:registry.npmjs.org)",
19
- "WebFetch(domain:api.github.com)",
20
- "Bash(mkdir:*)"
21
- ]
22
- }
23
- }