@helping-ai-workflow/md2doc 1.0.1 → 1.0.3

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
@@ -6,11 +6,39 @@ Two global CLIs (`md2html`, `md2pdf`) you can call from any directory.
6
6
 
7
7
  ## Install
8
8
 
9
+ Requires Node.js 18 or higher. The first install pulls puppeteer (≈ 170 MB Chromium download); subsequent installs reuse it.
10
+
11
+ ### Recommended: install via nvm
12
+
13
+ If you do not yet have Node.js — or your system Node lives under `/usr/local` and `npm install -g` fails with `EACCES` — install Node through [nvm](https://github.com/nvm-sh/nvm) first. nvm puts Node under `~/.nvm`, so global packages never need `sudo`.
14
+
9
15
  ```bash
16
+ sudo apt install -y curl # Debian / Ubuntu only; skip if curl is already installed
17
+ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash
18
+ source ~/.zshrc # or: source ~/.bashrc
19
+ nvm install --lts
20
+ nvm use --lts
10
21
  npm install -g @helping-ai-workflow/md2doc
11
22
  ```
12
23
 
13
- Requires Node.js 18 or higher. The first install pulls puppeteer (≈ 170 MB Chromium download); subsequent installs reuse it.
24
+ ### Already have Node.js
25
+
26
+ ```bash
27
+ npm install -g @helping-ai-workflow/md2doc
28
+ ```
29
+
30
+ ### Troubleshooting
31
+
32
+ **`EACCES: permission denied, mkdir '/usr/local/lib/node_modules'`**
33
+ Your system Node is owned by root. Do **not** run `sudo npm install -g` — puppeteer's postinstall would download Chromium as root and break later runs. Instead, switch to nvm using the steps above.
34
+
35
+ **`Failed to set up chrome ...! Set "PUPPETEER_SKIP_DOWNLOAD" env variable to skip download.`**
36
+ An earlier install left a half-finished Chromium download in `~/.cache/puppeteer`. md2doc ≥ 1.0.3 cleans this automatically; on older versions, clear the cache and retry:
37
+
38
+ ```bash
39
+ rm -rf ~/.cache/puppeteer
40
+ npm install -g @helping-ai-workflow/md2doc
41
+ ```
14
42
 
15
43
  ## Usage
16
44
 
package/bin/md2html.js CHANGED
@@ -14,7 +14,7 @@ function printHelp() {
14
14
  `md2${FORMAT} — render Markdown to ${FORMAT.toUpperCase()} (WaveDrom / Mermaid / Graphviz supported)`,
15
15
  '',
16
16
  'Usage:',
17
- ` md2${FORMAT} <input.md> [<input2.md> ...] # render each to <stem>.${FORMAT} next to source`,
17
+ ` md2${FORMAT} <input.md> [<input2.md> ...] # render each to <stem>_gen.${FORMAT} next to source`,
18
18
  ` md2${FORMAT} <input.md> --out <path.${FORMAT}> # explicit output (single-file mode)`,
19
19
  ` md2${FORMAT} <input.md> --open # render then open viewer`,
20
20
  ` md2${FORMAT} <input.md> --quiet # suppress progress output`,
@@ -79,12 +79,23 @@ function parseArgs(argv) {
79
79
  }
80
80
 
81
81
  function deriveOutput(input, format) {
82
+ // Default suffix matches the workspace Makefile convention: <stem>_gen.<format>.
82
83
  const stem = input.replace(/\.md$/i, '');
83
84
  if (stem === input) {
84
- // No .md extension — append .<format>.
85
- return input + '.' + format;
85
+ // No .md extension — append _gen.<format>.
86
+ return input + '_gen.' + format;
87
+ }
88
+ return stem + '_gen.' + format;
89
+ }
90
+
91
+ function isWSL() {
92
+ if (process.platform !== 'linux') return false;
93
+ if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) return true;
94
+ try {
95
+ return fs.existsSync('/proc/sys/fs/binfmt_misc/WSLInterop');
96
+ } catch (_) {
97
+ return false;
86
98
  }
87
- return stem + '.' + format;
88
99
  }
89
100
 
90
101
  function openViewer(filePath) {
@@ -94,12 +105,23 @@ function openViewer(filePath) {
94
105
  cmd = 'open'; args = [filePath];
95
106
  } else if (platform === 'win32') {
96
107
  cmd = 'cmd'; args = ['/c', 'start', '""', filePath];
108
+ } else if (isWSL()) {
109
+ // Convert the WSL Linux path to a Windows-side path then launch via explorer.exe,
110
+ // which uses Windows file associations (.html → default browser, .pdf → default reader).
111
+ const r = spawnSync('wslpath', ['-w', filePath], { encoding: 'utf8' });
112
+ if (r.status === 0 && r.stdout) {
113
+ cmd = 'explorer.exe'; args = [r.stdout.trim()];
114
+ } else {
115
+ cmd = 'xdg-open'; args = [filePath];
116
+ }
97
117
  } else {
98
118
  cmd = 'xdg-open'; args = [filePath];
99
119
  }
100
120
  const r = spawnSync(cmd, args, { stdio: 'ignore' });
101
- if (r.error || r.status !== 0) {
102
- process.stderr.write(`warning: could not launch viewer for ${filePath}\n`);
121
+ // explorer.exe returns exit code 1 on success (Windows quirk via WSL interop),
122
+ // so a non-zero exit is not necessarily a failure when launching via explorer.
123
+ if (r.error) {
124
+ process.stderr.write(`warning: could not launch viewer for ${filePath}: ${r.error.message}\n`);
103
125
  }
104
126
  }
105
127
 
@@ -113,10 +135,10 @@ function main() {
113
135
  process.exit(1);
114
136
  }
115
137
  const output = (args.out !== null) ? args.out : deriveOutput(input, FORMAT);
116
- if (!args.quiet) {
117
- process.stdout.write(`[${FORMAT.toUpperCase()}] ${input} ${output}\n`);
118
- }
119
- const r = spawnSync(process.execPath, [LIB, input, output], { stdio: 'inherit' });
138
+ // lib/md2doc.js prints its own "[FORMAT] input → output" line; redirect its stdout
139
+ // when --quiet so neither layer emits progress.
140
+ const stdio = args.quiet ? ['inherit', 'ignore', 'inherit'] : 'inherit';
141
+ const r = spawnSync(process.execPath, [LIB, input, output], { stdio });
120
142
  if (r.status !== 0) {
121
143
  process.stderr.write(`error: render failed for ${input} (exit ${r.status})\n`);
122
144
  process.exit(r.status || 1);
package/bin/md2pdf.js CHANGED
@@ -14,7 +14,7 @@ function printHelp() {
14
14
  `md2${FORMAT} — render Markdown to ${FORMAT.toUpperCase()} (WaveDrom / Mermaid / Graphviz supported)`,
15
15
  '',
16
16
  'Usage:',
17
- ` md2${FORMAT} <input.md> [<input2.md> ...] # render each to <stem>.${FORMAT} next to source`,
17
+ ` md2${FORMAT} <input.md> [<input2.md> ...] # render each to <stem>_gen.${FORMAT} next to source`,
18
18
  ` md2${FORMAT} <input.md> --out <path.${FORMAT}> # explicit output (single-file mode)`,
19
19
  ` md2${FORMAT} <input.md> --open # render then open viewer`,
20
20
  ` md2${FORMAT} <input.md> --quiet # suppress progress output`,
@@ -79,11 +79,22 @@ function parseArgs(argv) {
79
79
  }
80
80
 
81
81
  function deriveOutput(input, format) {
82
+ // Default suffix matches the workspace Makefile convention: <stem>_gen.<format>.
82
83
  const stem = input.replace(/\.md$/i, '');
83
84
  if (stem === input) {
84
- return input + '.' + format;
85
+ return input + '_gen.' + format;
86
+ }
87
+ return stem + '_gen.' + format;
88
+ }
89
+
90
+ function isWSL() {
91
+ if (process.platform !== 'linux') return false;
92
+ if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) return true;
93
+ try {
94
+ return fs.existsSync('/proc/sys/fs/binfmt_misc/WSLInterop');
95
+ } catch (_) {
96
+ return false;
85
97
  }
86
- return stem + '.' + format;
87
98
  }
88
99
 
89
100
  function openViewer(filePath) {
@@ -93,12 +104,21 @@ function openViewer(filePath) {
93
104
  cmd = 'open'; args = [filePath];
94
105
  } else if (platform === 'win32') {
95
106
  cmd = 'cmd'; args = ['/c', 'start', '""', filePath];
107
+ } else if (isWSL()) {
108
+ // Convert the WSL Linux path to a Windows-side path then launch via explorer.exe,
109
+ // which uses Windows file associations (.html → default browser, .pdf → default reader).
110
+ const r = spawnSync('wslpath', ['-w', filePath], { encoding: 'utf8' });
111
+ if (r.status === 0 && r.stdout) {
112
+ cmd = 'explorer.exe'; args = [r.stdout.trim()];
113
+ } else {
114
+ cmd = 'xdg-open'; args = [filePath];
115
+ }
96
116
  } else {
97
117
  cmd = 'xdg-open'; args = [filePath];
98
118
  }
99
119
  const r = spawnSync(cmd, args, { stdio: 'ignore' });
100
- if (r.error || r.status !== 0) {
101
- process.stderr.write(`warning: could not launch viewer for ${filePath}\n`);
120
+ if (r.error) {
121
+ process.stderr.write(`warning: could not launch viewer for ${filePath}: ${r.error.message}\n`);
102
122
  }
103
123
  }
104
124
 
@@ -112,10 +132,10 @@ function main() {
112
132
  process.exit(1);
113
133
  }
114
134
  const output = (args.out !== null) ? args.out : deriveOutput(input, FORMAT);
115
- if (!args.quiet) {
116
- process.stdout.write(`[${FORMAT.toUpperCase()}] ${input} ${output}\n`);
117
- }
118
- const r = spawnSync(process.execPath, [LIB, input, output], { stdio: 'inherit' });
135
+ // lib/md2doc.js prints its own "[FORMAT] input → output" line; redirect its stdout
136
+ // when --quiet so neither layer emits progress.
137
+ const stdio = args.quiet ? ['inherit', 'ignore', 'inherit'] : 'inherit';
138
+ const r = spawnSync(process.execPath, [LIB, input, output], { stdio });
119
139
  if (r.status !== 0) {
120
140
  process.stderr.write(`error: render failed for ${input} (exit ${r.status})\n`);
121
141
  process.exit(r.status || 1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helping-ai-workflow/md2doc",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Markdown → HTML / PDF renderer with WaveDrom, Mermaid, and Graphviz support",
5
5
  "keywords": ["markdown", "html", "pdf", "renderer", "wavedrom", "mermaid", "graphviz"],
6
6
  "main": "lib/md2doc.js",
@@ -11,6 +11,7 @@
11
11
  "files": [
12
12
  "lib/",
13
13
  "bin/",
14
+ "scripts/",
14
15
  "README.md",
15
16
  "LICENSE"
16
17
  ],
@@ -22,6 +23,7 @@
22
23
  "puppeteer": "^24.15.0"
23
24
  },
24
25
  "scripts": {
26
+ "preinstall": "node scripts/preinstall.js",
25
27
  "test": "node test/md2doc.test.js"
26
28
  },
27
29
  "repository": {
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // Remove stale puppeteer browser-cache folders that exist but are missing
5
+ // the actual binary. An interrupted prior download leaves @puppeteer/browsers
6
+ // in a state where it sees the version folder and refuses to redownload,
7
+ // failing puppeteer's postinstall with:
8
+ // The browser folder (.../chrome/<platform>-<ver>) exists but the
9
+ // executable (.../chrome-<platform>/chrome) is missing
10
+ // We run before puppeteer's postinstall and clean those husks so the
11
+ // download proceeds fresh.
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const os = require('os');
16
+
17
+ const BINARY_NAMES = new Set([
18
+ 'chrome',
19
+ 'chrome.exe',
20
+ 'chrome-headless-shell',
21
+ 'chrome-headless-shell.exe',
22
+ 'Google Chrome for Testing',
23
+ ]);
24
+ const MIN_BINARY_BYTES = 1_000_000;
25
+ const MAX_DEPTH = 4;
26
+
27
+ function hasBinary(rootDir) {
28
+ const stack = [[rootDir, 0]];
29
+ while (stack.length) {
30
+ const [dir, depth] = stack.pop();
31
+ if (depth > MAX_DEPTH) continue;
32
+ let entries;
33
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
34
+ catch { continue; }
35
+ for (const ent of entries) {
36
+ const full = path.join(dir, ent.name);
37
+ if (ent.isFile() && BINARY_NAMES.has(ent.name)) {
38
+ try {
39
+ if (fs.statSync(full).size >= MIN_BINARY_BYTES) return true;
40
+ } catch { /* ignore */ }
41
+ } else if (ent.isDirectory()) {
42
+ stack.push([full, depth + 1]);
43
+ }
44
+ }
45
+ }
46
+ return false;
47
+ }
48
+
49
+ function cleanStaleVersionDirs(parentDir) {
50
+ if (!fs.existsSync(parentDir)) return;
51
+ let entries;
52
+ try { entries = fs.readdirSync(parentDir, { withFileTypes: true }); }
53
+ catch { return; }
54
+ for (const ent of entries) {
55
+ if (!ent.isDirectory()) continue;
56
+ const versionDir = path.join(parentDir, ent.name);
57
+ if (hasBinary(versionDir)) continue;
58
+ console.log(`[md2doc preinstall] removing stale puppeteer cache: ${versionDir}`);
59
+ try { fs.rmSync(versionDir, { recursive: true, force: true }); }
60
+ catch (e) {
61
+ console.log(`[md2doc preinstall] failed to remove ${versionDir}: ${e.message}`);
62
+ }
63
+ }
64
+ }
65
+
66
+ const cacheRoot = process.env.PUPPETEER_CACHE_DIR
67
+ || path.join(os.homedir(), '.cache', 'puppeteer');
68
+
69
+ cleanStaleVersionDirs(path.join(cacheRoot, 'chrome'));
70
+ cleanStaleVersionDirs(path.join(cacheRoot, 'chrome-headless-shell'));