@astlw/peekmd 1.0.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/README.md +102 -0
- package/bin/peekmd.js +372 -0
- package/package.json +43 -0
- package/src/config.js +143 -0
- package/src/public/app.js +811 -0
- package/src/public/index.html +329 -0
- package/src/public/style.css +1107 -0
- package/src/server.js +297 -0
- package/src/watcher.js +89 -0
package/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# PeekMD
|
|
2
|
+
|
|
3
|
+
A CLI tool that renders Markdown files in the browser and live-reloads on every file save.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g peekmd
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires Node.js 18+.
|
|
12
|
+
|
|
13
|
+
## CLI Commands
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Server
|
|
17
|
+
peekmd start # start server (daemon)
|
|
18
|
+
peekmd stop # stop server
|
|
19
|
+
peekmd status # check server status
|
|
20
|
+
PORT=3000 peekmd start # custom port (default: 4000)
|
|
21
|
+
|
|
22
|
+
# Folder management
|
|
23
|
+
peekmd link <dir> [dir2] ... # persist folders to config
|
|
24
|
+
peekmd unlink <dir> [dir2] ... # remove folders from config
|
|
25
|
+
peekmd list # show linked folders
|
|
26
|
+
|
|
27
|
+
# Ignore patterns (glob syntax)
|
|
28
|
+
peekmd ignore <pattern> ... # ignore folders/files by glob pattern
|
|
29
|
+
peekmd unignore <pattern> ... # remove an ignore pattern
|
|
30
|
+
peekmd ignored # show all active ignore patterns
|
|
31
|
+
|
|
32
|
+
# Structured Output - making it easy to integrate with scripts or other tools.
|
|
33
|
+
peekmd list --json # linked folders as JSON
|
|
34
|
+
peekmd ignored --json # ignore patterns as JSON
|
|
35
|
+
peekmd status --json # server status as JSON
|
|
36
|
+
peekmd search <query> # search files and content (JSON)
|
|
37
|
+
peekmd files # list all markdown files (JSON)
|
|
38
|
+
|
|
39
|
+
peekmd --help # print help
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Ignore Patterns
|
|
43
|
+
|
|
44
|
+
Ignore patterns let you exclude folders or files from the file tree, search,
|
|
45
|
+
and live-reload. Patterns are stored in `~/.peekmd.json` and apply globally.
|
|
46
|
+
All patterns use **glob syntax** (picomatch).
|
|
47
|
+
|
|
48
|
+
**Examples:**
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
peekmd ignore "**/node_modules/**" # ignores all node_modules folders
|
|
52
|
+
peekmd ignore "**/dist/**" "**/build/**" # ignore multiple patterns
|
|
53
|
+
peekmd ignore "**/*.draft.md" # ignores all .draft.md files
|
|
54
|
+
peekmd ignore "docs/private/**" # ignores everything under docs/private
|
|
55
|
+
peekmd ignore "*.tmp" # ignores .tmp files at any depth
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Default patterns** (applied automatically on first use):
|
|
59
|
+
|
|
60
|
+
- `**/node_modules/**`
|
|
61
|
+
- `**/.git/**`
|
|
62
|
+
- `**/dist/**`
|
|
63
|
+
- `**/build/**`
|
|
64
|
+
- `**/.next/**`
|
|
65
|
+
- `**/__pycache__/**`
|
|
66
|
+
|
|
67
|
+
## Features
|
|
68
|
+
|
|
69
|
+
| Feature | Summary | Why it matters |
|
|
70
|
+
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------: | ------------------------------------------------------------------- |
|
|
71
|
+
| Live reload | Real-time file events over WebSocket; open file preview refreshes on `add`/`change`/`unlink`. | Instant feedback while editing files. |
|
|
72
|
+
| Folder groups (multi-folder) | Sidebar shows each linked folder as a separate group; when folder basenames collide a compact path is shown to disambiguate. | Keep multiple projects side-by-side without confusion. |
|
|
73
|
+
| File tree | Per-folder collapsible file tree with relative paths; click a file to render Markdown in the preview. | Fast navigation and focused previews. |
|
|
74
|
+
| Ignore patterns (glob) | Glob-only patterns (picomatch) applied to watcher, search, and tree. | Simple, consistent filtering across the app. Quote globs in shells. |
|
|
75
|
+
| Search | Lightweight full-text substring search across filenames and file contents. | Fast, low-overhead lookup for most small/medium projects. |
|
|
76
|
+
| Markdown + Mermaid | Renders GFM and client-side Mermaid diagrams inside `mermaid` fences. | Rich previews without server-side rendering. |
|
|
77
|
+
| Daemon & CLI | Start/stop/status plus `--json` machine-readable output for scripts and agents. | Integrates with workflows and automation. |
|
|
78
|
+
| Theming & UX | Light/dark themes, instant floating tooltips appended to `body`, compact path badges for long names. | Tooltips avoid clipping; UI scales to longer path. |
|
|
79
|
+
|
|
80
|
+
### Limitations
|
|
81
|
+
|
|
82
|
+
- Ignore patterns are glob-only (picomatch). Regex/exact-name modes are not supported.
|
|
83
|
+
- Search is intentionally simple (substring per-line) for clarity and low resource usage.
|
|
84
|
+
- Initial scan of very large directories may take noticeable time; prefer linking smaller subfolders if needed.
|
|
85
|
+
|
|
86
|
+
## Config
|
|
87
|
+
|
|
88
|
+
Linked folders and ignore patterns are stored in:
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
~/.peekmd.json
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"folders": ["/Users/you/docs", "/Users/you/notes"],
|
|
98
|
+
"ignore": ["**/node_modules/**", "**/dist/**", "**/*.draft.md"]
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Created automatically on first use. Can be edited manually.
|
package/bin/peekmd.js
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
const fs = require("node:fs");
|
|
7
|
+
const os = require("node:os");
|
|
8
|
+
const config = require("../src/config");
|
|
9
|
+
const { createServer } = require("../src/server");
|
|
10
|
+
|
|
11
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
12
|
+
const PID_FILE = path.join(os.tmpdir(), "peekmd.pid");
|
|
13
|
+
|
|
14
|
+
/* ── PID management ────────────────────────────────────────────────── */
|
|
15
|
+
|
|
16
|
+
const pid = {
|
|
17
|
+
read() {
|
|
18
|
+
try {
|
|
19
|
+
return parseInt(fs.readFileSync(PID_FILE, "utf-8"), 10);
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
write(id) {
|
|
25
|
+
fs.writeFileSync(PID_FILE, String(id));
|
|
26
|
+
},
|
|
27
|
+
clear() {
|
|
28
|
+
try {
|
|
29
|
+
fs.unlinkSync(PID_FILE);
|
|
30
|
+
} catch {}
|
|
31
|
+
},
|
|
32
|
+
alive(id) {
|
|
33
|
+
try {
|
|
34
|
+
process.kill(id, 0);
|
|
35
|
+
return true;
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/* ── Output ────────────────────────────────────────────────────────── */
|
|
43
|
+
|
|
44
|
+
const BANNER = `
|
|
45
|
+
┌─────────────────────┐
|
|
46
|
+
│ │
|
|
47
|
+
│ p e e k M D │
|
|
48
|
+
│ ───────────── │
|
|
49
|
+
│ │ │ │ │ │ │ │
|
|
50
|
+
│ │
|
|
51
|
+
└─────────────────────┘
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
function printTable(headers, rows) {
|
|
55
|
+
const widths = headers.map(
|
|
56
|
+
(h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)) + 2,
|
|
57
|
+
);
|
|
58
|
+
const line = " +" + widths.map((w) => "-".repeat(w)).join("+") + "+";
|
|
59
|
+
|
|
60
|
+
console.log(line);
|
|
61
|
+
console.log(
|
|
62
|
+
" |" +
|
|
63
|
+
headers.map((h, i) => (" " + h).padEnd(widths[i])).join("|") +
|
|
64
|
+
"|",
|
|
65
|
+
);
|
|
66
|
+
console.log(line);
|
|
67
|
+
for (const row of rows)
|
|
68
|
+
console.log(
|
|
69
|
+
" |" +
|
|
70
|
+
row.map((c, i) => (" " + c).padEnd(widths[i])).join("|") +
|
|
71
|
+
"|",
|
|
72
|
+
);
|
|
73
|
+
console.log(line);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const HELP = `
|
|
77
|
+
peekmd — Preview Markdown files in the browser
|
|
78
|
+
|
|
79
|
+
Server:
|
|
80
|
+
peekmd start [dir] [dir2] ... Start server (daemon)
|
|
81
|
+
peekmd stop Stop server
|
|
82
|
+
peekmd status Check if server is running
|
|
83
|
+
|
|
84
|
+
Folders:
|
|
85
|
+
peekmd link <dir> [dir2] ... Link folders for persistent tracking
|
|
86
|
+
peekmd unlink <dir> [dir2] ... Remove folders from tracking
|
|
87
|
+
peekmd list Show all linked folders
|
|
88
|
+
|
|
89
|
+
Ignore:
|
|
90
|
+
peekmd ignore <pattern> ... Ignore folders/files by glob pattern
|
|
91
|
+
peekmd unignore <pattern> ... Remove an ignore pattern
|
|
92
|
+
peekmd ignored Show all active ignore patterns
|
|
93
|
+
|
|
94
|
+
Glob Pattern Examples:
|
|
95
|
+
**/node_modules/** Ignore all node_modules folders
|
|
96
|
+
**/*.draft.md Ignore all .draft.md files
|
|
97
|
+
docs/private/** Ignore everything under docs/private
|
|
98
|
+
*.tmp Ignore .tmp files at any depth
|
|
99
|
+
|
|
100
|
+
AI / Machine-readable output:
|
|
101
|
+
peekmd list --json Linked folders as JSON
|
|
102
|
+
peekmd ignored --json Ignore patterns as JSON
|
|
103
|
+
peekmd status --json Server status as JSON
|
|
104
|
+
peekmd search <query> Search files and content (JSON)
|
|
105
|
+
peekmd files List all markdown files (JSON)
|
|
106
|
+
|
|
107
|
+
Other:
|
|
108
|
+
peekmd --help Show this help
|
|
109
|
+
|
|
110
|
+
Environment:
|
|
111
|
+
PORT=<number> Server port (default: 4000)
|
|
112
|
+
|
|
113
|
+
Config file: ~/.peekmd.json
|
|
114
|
+
`;
|
|
115
|
+
|
|
116
|
+
/* ── Helpers ───────────────────────────────────────────────────────── */
|
|
117
|
+
|
|
118
|
+
const resolveDirs = (dirs) => dirs.map((d) => path.resolve(d));
|
|
119
|
+
const getPort = () => Number(process.env.PORT) || 4000;
|
|
120
|
+
|
|
121
|
+
function openBrowser(url) {
|
|
122
|
+
const open =
|
|
123
|
+
process.platform === "darwin"
|
|
124
|
+
? "open"
|
|
125
|
+
: process.platform === "win32"
|
|
126
|
+
? "start"
|
|
127
|
+
: "xdg-open";
|
|
128
|
+
require("node:child_process").exec(`${open} ${url}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* ── Commands ──────────────────────────────────────────────────────── */
|
|
132
|
+
|
|
133
|
+
switch (cmd) {
|
|
134
|
+
case "--help":
|
|
135
|
+
case "-h":
|
|
136
|
+
console.log(HELP);
|
|
137
|
+
break;
|
|
138
|
+
|
|
139
|
+
case "link": {
|
|
140
|
+
if (!rest.length) {
|
|
141
|
+
console.error(" Usage: peekmd link <dir> [dir2] ...");
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
for (const dir of rest) {
|
|
145
|
+
const r = config.linkFolder(dir);
|
|
146
|
+
if (r.error) console.error(" ✗ %s — %s", r.path, r.error);
|
|
147
|
+
else if (r.added) console.log(" ✓ Linked %s", r.path);
|
|
148
|
+
else console.log(" · Already linked %s", r.path);
|
|
149
|
+
}
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
case "unlink": {
|
|
154
|
+
if (!rest.length) {
|
|
155
|
+
console.error(" Usage: peekmd unlink <dir> [dir2] ...");
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
for (const dir of rest) {
|
|
159
|
+
const r = config.unlinkFolder(dir);
|
|
160
|
+
console.log(
|
|
161
|
+
r.removed ? " ✓ Unlinked %s" : " · Not linked %s",
|
|
162
|
+
r.path,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
case "list": {
|
|
169
|
+
const folders = config.getFolders();
|
|
170
|
+
if (rest.includes("--json")) {
|
|
171
|
+
const names = config.getDisplayNames(folders);
|
|
172
|
+
console.log(
|
|
173
|
+
JSON.stringify(
|
|
174
|
+
folders.map((f, i) => ({ name: names[i], path: f })),
|
|
175
|
+
null,
|
|
176
|
+
2,
|
|
177
|
+
),
|
|
178
|
+
);
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
if (!folders.length) {
|
|
182
|
+
console.log(" No folders linked. Run: peekmd link <dir>");
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
const names = config.getDisplayNames(folders);
|
|
186
|
+
console.log("\n Linked folders:\n");
|
|
187
|
+
printTable(
|
|
188
|
+
["Name", "Path"],
|
|
189
|
+
folders.map((f, i) => [names[i], f]),
|
|
190
|
+
);
|
|
191
|
+
console.log();
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
case "start": {
|
|
196
|
+
const existing = pid.read();
|
|
197
|
+
if (existing && pid.alive(existing)) {
|
|
198
|
+
console.log(" Server already running (PID: %d)", existing);
|
|
199
|
+
process.exit(0);
|
|
200
|
+
}
|
|
201
|
+
pid.clear();
|
|
202
|
+
|
|
203
|
+
const child = require("node:child_process").spawn(
|
|
204
|
+
process.execPath,
|
|
205
|
+
[__filename, "__serve__", ...rest],
|
|
206
|
+
{
|
|
207
|
+
stdio: "ignore",
|
|
208
|
+
detached: true,
|
|
209
|
+
env: { ...process.env, PORT: String(getPort()) },
|
|
210
|
+
},
|
|
211
|
+
);
|
|
212
|
+
child.unref();
|
|
213
|
+
pid.write(child.pid);
|
|
214
|
+
|
|
215
|
+
const url = `http://localhost:${getPort()}`;
|
|
216
|
+
console.log(BANNER);
|
|
217
|
+
console.log(" %s\n", url);
|
|
218
|
+
console.log(" To stop: peekmd stop\n");
|
|
219
|
+
openBrowser(url);
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
case "stop": {
|
|
224
|
+
const id = pid.read();
|
|
225
|
+
if (!id || !pid.alive(id)) {
|
|
226
|
+
console.log(
|
|
227
|
+
id
|
|
228
|
+
? " PID %d is not running"
|
|
229
|
+
: " No background server running",
|
|
230
|
+
id,
|
|
231
|
+
);
|
|
232
|
+
pid.clear();
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
process.kill(id, "SIGTERM");
|
|
237
|
+
} catch (e) {
|
|
238
|
+
console.error(" ✗ Failed to stop: %s", e.message);
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
pid.clear();
|
|
242
|
+
console.log(" ✓ Server stopped");
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
case "status": {
|
|
247
|
+
const id = pid.read();
|
|
248
|
+
const running = !!(id && pid.alive(id));
|
|
249
|
+
if (!running && id) pid.clear();
|
|
250
|
+
if (rest.includes("--json")) {
|
|
251
|
+
console.log(
|
|
252
|
+
JSON.stringify({
|
|
253
|
+
running,
|
|
254
|
+
pid: running ? id : null,
|
|
255
|
+
port: Number(process.env.PORT) || 4000,
|
|
256
|
+
}),
|
|
257
|
+
);
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
if (running) {
|
|
261
|
+
console.log(
|
|
262
|
+
" ✓ Server running (PID: %d, port: %s)",
|
|
263
|
+
id,
|
|
264
|
+
process.env.PORT || 4000,
|
|
265
|
+
);
|
|
266
|
+
} else {
|
|
267
|
+
console.log(" Server not running");
|
|
268
|
+
}
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
case "ignore": {
|
|
273
|
+
if (!rest.length) {
|
|
274
|
+
console.error(" Usage: peekmd ignore <pattern> [pattern2] ...");
|
|
275
|
+
console.error(
|
|
276
|
+
" Examples: peekmd ignore '**/node_modules/**' '**/dist/**'",
|
|
277
|
+
);
|
|
278
|
+
console.error(" peekmd ignore '**/*.draft.md'");
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
281
|
+
for (const p of rest) {
|
|
282
|
+
const r = config.addIgnorePattern(p);
|
|
283
|
+
console.log(
|
|
284
|
+
r.added
|
|
285
|
+
? " \u2713 Ignoring %s"
|
|
286
|
+
: " \u00b7 Already ignored %s",
|
|
287
|
+
p,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
case "unignore": {
|
|
294
|
+
if (!rest.length) {
|
|
295
|
+
console.error(" Usage: peekmd unignore <pattern> [pattern2] ...");
|
|
296
|
+
process.exit(1);
|
|
297
|
+
}
|
|
298
|
+
for (const p of rest) {
|
|
299
|
+
const r = config.removeIgnorePattern(p);
|
|
300
|
+
console.log(
|
|
301
|
+
r.removed
|
|
302
|
+
? " \u2713 Removed %s"
|
|
303
|
+
: " \u00b7 Not in ignore list %s",
|
|
304
|
+
p,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
case "ignored": {
|
|
311
|
+
const patterns = config.getIgnorePatterns();
|
|
312
|
+
if (rest.includes("--json")) {
|
|
313
|
+
console.log(
|
|
314
|
+
JSON.stringify(
|
|
315
|
+
patterns.map((p) => ({ pattern: p })),
|
|
316
|
+
null,
|
|
317
|
+
2,
|
|
318
|
+
),
|
|
319
|
+
);
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
if (!patterns.length) {
|
|
323
|
+
console.log(
|
|
324
|
+
" No ignore patterns set. Run: peekmd ignore <pattern>",
|
|
325
|
+
);
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
console.log("\n Ignore patterns (glob):\n");
|
|
329
|
+
for (const p of patterns) {
|
|
330
|
+
console.log(" %s", p);
|
|
331
|
+
}
|
|
332
|
+
console.log();
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
case "search": {
|
|
337
|
+
/* Search across linked folders — outputs JSON for AI agents */
|
|
338
|
+
const query = rest.filter((a) => !a.startsWith("--")).join(" ");
|
|
339
|
+
if (!query) {
|
|
340
|
+
console.error(" Usage: peekmd search <query>");
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
const { searchCli } = require("../src/server");
|
|
344
|
+
searchCli(config.getFolders(), query)
|
|
345
|
+
.then((r) => console.log(JSON.stringify(r, null, 2)))
|
|
346
|
+
.catch(() => process.exit(1));
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
case "files": {
|
|
351
|
+
/* List all markdown files across linked folders — JSON output */
|
|
352
|
+
const { listFilesCli } = require("../src/server");
|
|
353
|
+
listFilesCli(config.getFolders())
|
|
354
|
+
.then((r) => console.log(JSON.stringify(r, null, 2)))
|
|
355
|
+
.catch(() => process.exit(1));
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
case "__serve__": {
|
|
360
|
+
/* Internal: spawned by 'start' as a detached daemon */
|
|
361
|
+
const extra = rest.length ? resolveDirs(rest) : [];
|
|
362
|
+
const port = getPort();
|
|
363
|
+
|
|
364
|
+
createServer({ port, extraDirs: extra }).catch(() => process.exit(1));
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
default: {
|
|
369
|
+
console.log(HELP);
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@astlw/peekmd",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A CLI tool that renders Markdown files in the browser and live-reloads on every file save.",
|
|
5
|
+
"author": "AST-LW",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"private": false,
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/AST-LW/peekmd.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/AST-LW/peekmd#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/AST-LW/peekmd/issues"
|
|
15
|
+
},
|
|
16
|
+
"bin": {
|
|
17
|
+
"peekmd": "./bin/peekmd.js"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"bin",
|
|
21
|
+
"src",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"markdown",
|
|
29
|
+
"markdown-server",
|
|
30
|
+
"markdown-preview",
|
|
31
|
+
"mermaid",
|
|
32
|
+
"cli",
|
|
33
|
+
"viewer",
|
|
34
|
+
"documentation",
|
|
35
|
+
"live-reload"
|
|
36
|
+
],
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"chokidar": "^3.6.0",
|
|
39
|
+
"express": "^4.21.0",
|
|
40
|
+
"picomatch": "^4.0.3",
|
|
41
|
+
"ws": "^8.18.0"
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const picomatch = require("picomatch");
|
|
6
|
+
|
|
7
|
+
const CONFIG_PATH = path.join(require("node:os").homedir(), ".peekmd.json");
|
|
8
|
+
|
|
9
|
+
function read() {
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
|
|
12
|
+
} catch {
|
|
13
|
+
return { folders: [] };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function write(data) {
|
|
18
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(data, null, 2) + "\n");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getFolders() {
|
|
22
|
+
return read().folders;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function linkFolder(dir) {
|
|
26
|
+
const abs = path.resolve(dir);
|
|
27
|
+
if (!fs.existsSync(abs))
|
|
28
|
+
return { added: false, path: abs, error: "path does not exist" };
|
|
29
|
+
if (!fs.statSync(abs).isDirectory())
|
|
30
|
+
return { added: false, path: abs, error: "not a directory" };
|
|
31
|
+
|
|
32
|
+
const data = read();
|
|
33
|
+
if (data.folders.includes(abs)) return { added: false, path: abs };
|
|
34
|
+
data.folders.push(abs);
|
|
35
|
+
write(data);
|
|
36
|
+
return { added: true, path: abs };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function unlinkFolder(dir) {
|
|
40
|
+
const abs = path.resolve(dir);
|
|
41
|
+
const data = read();
|
|
42
|
+
const idx = data.folders.indexOf(abs);
|
|
43
|
+
if (idx === -1) return { removed: false, path: abs };
|
|
44
|
+
data.folders.splice(idx, 1);
|
|
45
|
+
write(data);
|
|
46
|
+
return { removed: true, path: abs };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getDisplayNames(folders) {
|
|
50
|
+
const counts = {};
|
|
51
|
+
for (const f of folders) {
|
|
52
|
+
const b = path.basename(f);
|
|
53
|
+
counts[b] = (counts[b] || 0) + 1;
|
|
54
|
+
}
|
|
55
|
+
return folders.map((f) => {
|
|
56
|
+
const b = path.basename(f);
|
|
57
|
+
return counts[b] > 1 ? `${path.basename(path.dirname(f))}/${b}` : b;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* ── Ignore patterns ───────────────────────────────────────────────── */
|
|
62
|
+
|
|
63
|
+
const DEFAULT_IGNORE = [
|
|
64
|
+
"**/node_modules/**",
|
|
65
|
+
"**/.git/**",
|
|
66
|
+
"**/dist/**",
|
|
67
|
+
"**/build/**",
|
|
68
|
+
"**/.next/**",
|
|
69
|
+
"**/__pycache__/**",
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
function ensureDefaults() {
|
|
73
|
+
const data = read();
|
|
74
|
+
if (!data.ignore) {
|
|
75
|
+
data.ignore = [...DEFAULT_IGNORE];
|
|
76
|
+
write(data);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getIgnorePatterns() {
|
|
81
|
+
return read().ignore || DEFAULT_IGNORE;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function addIgnorePattern(pattern) {
|
|
85
|
+
const data = read();
|
|
86
|
+
if (!data.ignore) data.ignore = [];
|
|
87
|
+
if (data.ignore.includes(pattern)) return { added: false, pattern };
|
|
88
|
+
data.ignore.push(pattern);
|
|
89
|
+
write(data);
|
|
90
|
+
clearGlobCache();
|
|
91
|
+
return { added: true, pattern };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function removeIgnorePattern(pattern) {
|
|
95
|
+
const data = read();
|
|
96
|
+
if (!data.ignore) return { removed: false, pattern };
|
|
97
|
+
const idx = data.ignore.indexOf(pattern);
|
|
98
|
+
if (idx === -1) return { removed: false, pattern };
|
|
99
|
+
data.ignore.splice(idx, 1);
|
|
100
|
+
write(data);
|
|
101
|
+
clearGlobCache();
|
|
102
|
+
return { removed: true, pattern };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* pre-compiled glob cache so we don't re-parse every call */
|
|
106
|
+
const _globCache = new Map();
|
|
107
|
+
|
|
108
|
+
function clearGlobCache() {
|
|
109
|
+
_globCache.clear();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function getGlobMatcher(pattern) {
|
|
113
|
+
if (!_globCache.has(pattern)) {
|
|
114
|
+
_globCache.set(pattern, picomatch(pattern, { dot: true }));
|
|
115
|
+
}
|
|
116
|
+
return _globCache.get(pattern);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Test whether a relative path should be ignored.
|
|
121
|
+
* All patterns are treated as glob patterns (picomatch syntax).
|
|
122
|
+
*/
|
|
123
|
+
function isIgnored(relPath) {
|
|
124
|
+
const patterns = getIgnorePatterns();
|
|
125
|
+
for (const p of patterns) {
|
|
126
|
+
if (getGlobMatcher(p)(relPath)) return true;
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = {
|
|
132
|
+
getFolders,
|
|
133
|
+
linkFolder,
|
|
134
|
+
unlinkFolder,
|
|
135
|
+
getDisplayNames,
|
|
136
|
+
ensureDefaults,
|
|
137
|
+
getIgnorePatterns,
|
|
138
|
+
addIgnorePattern,
|
|
139
|
+
removeIgnorePattern,
|
|
140
|
+
isIgnored,
|
|
141
|
+
clearGlobCache,
|
|
142
|
+
CONFIG_PATH,
|
|
143
|
+
};
|