@hardikg/mdview 0.7.1 → 0.9.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/CHANGELOG.md +60 -0
- package/README.md +76 -9
- package/bin/mdview.mjs +986 -258
- package/dist/client/assets/abnfDiagram-VCTEODGH-d5-zUW3h.js +1 -0
- package/dist/client/assets/{arc-DYMLyL-_.js → arc-DIbSNhHb.js} +1 -1
- package/dist/client/assets/architectureDiagram-5GKGNRK7-Cq_KBCV9.js +36 -0
- package/dist/client/assets/blockDiagram-NRAW4CY4-CKgrJGzB.js +129 -0
- package/dist/client/assets/c4Diagram-UCG6FXSJ-CamsXU_i.js +38 -0
- package/dist/client/assets/channel-MJp4QxQY.js +1 -0
- package/dist/client/assets/{chunk-QZHKN3VN-Cd7CcXLC.js → chunk-2Q5K7J3B-DHB85OBA.js} +1 -1
- package/dist/client/assets/{chunk-FMBD7UC4-BYGwnUQ4.js → chunk-5VM5RSS4-C3v07bfZ.js} +1 -1
- package/dist/client/assets/{chunk-ND2GUHAM-TheCoD51.js → chunk-F27PBJKO-bDA5q_T9.js} +1 -1
- package/dist/client/assets/chunk-G27WJ6UU-DjS6mkGt.js +231 -0
- package/dist/client/assets/{chunk-4BX2VUAB-By-ok8D7.js → chunk-JWPE2WC7-ln94eTAD.js} +1 -1
- package/dist/client/assets/{chunk-727SXJPM-Bj1JFNtT.js → chunk-LCL6LL3I-SEXOnCPG.js} +4 -4
- package/dist/client/assets/chunk-POPQ4Y6H-Dpgmgmgq.js +1 -0
- package/dist/client/assets/chunk-SVP7TREG-C_qgSQCy.js +88 -0
- package/dist/client/assets/{chunk-55IACEB6-BDUDnCl9.js → chunk-XXDRQBXY-CguISEfl.js} +1 -1
- package/dist/client/assets/classDiagram-DTDB5LWJ-BlbBfPMZ.js +1 -0
- package/dist/client/assets/classDiagram-v2-JRS7N3AN-BlbBfPMZ.js +1 -0
- package/dist/client/assets/{cose-bilkent-S5V4N54A-Br-CySzr.js → cose-bilkent-JH36ORCC-CjtLn09K.js} +1 -1
- package/dist/client/assets/cynefin-OW5HDTMX-BgNmTgCR.js +178 -0
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-BFfuQ7YY.js +62 -0
- package/dist/client/assets/cytoscape.esm-BjAbmLdb.js +331 -0
- package/dist/client/assets/dagre-3AP2YEHR-BDLHOy1Z.js +4 -0
- package/dist/client/assets/diagram-S7CK7UJ4-CfzUQiTU.js +30 -0
- package/dist/client/assets/diagram-UQ7AKVKN-Ds3lTKuY.js +41 -0
- package/dist/client/assets/{diagram-KO2AKTUF-mwoLS2TK.js → diagram-VSXAHHWV-lwAuvia3.js} +3 -3
- package/dist/client/assets/diagram-VX7I27RA-CsU4EPwU.js +24 -0
- package/dist/client/assets/{diagram-LMA3HP47-4o7ohGjJ.js → diagram-Z3DM3KII-DFieIgnM.js} +2 -2
- package/dist/client/assets/ebnfDiagram-PWID7BFC-BzR_CBzj.js +1 -0
- package/dist/client/assets/erDiagram-SSCWMZ5O-joYlyDrM.js +99 -0
- package/dist/client/assets/flowDiagram-A5DVABFB-DVyMxsTJ.js +168 -0
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-Cf4FwZsI.js +292 -0
- package/dist/client/assets/{gitGraphDiagram-PVQCEYII-eN40bRnz.js → gitGraphDiagram-WWUBYQGX-9grceorS.js} +7 -7
- package/dist/client/assets/index-7zMmn_PY.js +2 -0
- package/dist/client/assets/index-CcKPW3Oc.css +1 -0
- package/dist/client/assets/infoDiagram-RXCK75RN-ja-7F4cO.js +2 -0
- package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-BDIzsJJZ.js → ishikawaDiagram-5VMMS53U-uRRSMmsi.js} +5 -5
- package/dist/client/assets/{journeyDiagram-JHISSGLW-BfWwpj1w.js → journeyDiagram-EYS64GPL-BdD_OYKc.js} +1 -1
- package/dist/client/assets/{kanban-definition-UN3LZRKU-Cuij1uoR.js → kanban-definition-3QL26DDD-3rMqnFFk.js} +8 -8
- package/dist/client/assets/katex-C5jXJg4s.js +257 -0
- package/dist/client/assets/katex.min-BYZTj3zW.css +1 -0
- package/dist/client/assets/katex.min-DXaO1tdm.js +1 -0
- package/dist/client/assets/layout-3dyJkKek.js +1 -0
- package/dist/client/assets/{linear-CZOgGL8m.js → linear-B49R1aTA.js} +1 -1
- package/dist/client/assets/mermaid.core-CVM43n3z.js +312 -0
- package/dist/client/assets/{mindmap-definition-RKZ34NQL-BbNc5LRE.js → mindmap-definition-FBJOCRG2-B8QesUuH.js} +1 -1
- package/dist/client/assets/pegDiagram-XKGWAZYB-BeO8X3By.js +1 -0
- package/dist/client/assets/pieDiagram-E7YTZNPT-DD3M93Je.js +39 -0
- package/dist/client/assets/{quadrantDiagram-W4KKPZXB-CmJGRE1L.js → quadrantDiagram-AXDQQJYC-LxJ8Qxzg.js} +3 -3
- package/dist/client/assets/railroadDiagram-O6MQD6OU-Cc6-wuTB.js +1 -0
- package/dist/client/assets/{requirementDiagram-4Y6WPE33-Dp-nBLhC.js → requirementDiagram-EFPCY7ZU-D-vaSng7.js} +5 -5
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-C-SrB4wj.js +40 -0
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-BpUTXTa1.js +162 -0
- package/dist/client/assets/sizeCapture-X5ZJPWSS-BG1rRkMg.js +1 -0
- package/dist/client/assets/stateDiagram-HBIQ2CUA-CAsJ9kP3.js +1 -0
- package/dist/client/assets/stateDiagram-v2-4QOOHH4V-ssPfmTD6.js +1 -0
- package/dist/client/assets/swimlanes-XN3QIQJK-DH5txxCi.js +1 -0
- package/dist/client/assets/swimlanesDiagram-VK2B7HYN-Cqi1auey.js +8 -0
- package/dist/client/assets/{timeline-definition-PNZ67QCA-uVQ-oiv8.js → timeline-definition-24CTP7MA-DPSR_k-5.js} +3 -3
- package/dist/client/assets/vennDiagram-4TSXK5OY-CRMSJqLq.js +34 -0
- package/dist/client/assets/{wardleyDiagram-YWT4CUSO-BQMj40Ao.js → wardleyDiagram-VM6X3IG4-DRCnYQxB.js} +4 -4
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-x5N3gqvJ.js +7 -0
- package/dist/client/bootstrap.js +13 -2
- package/dist/client/index.html +2 -2
- package/package.json +3 -3
- package/dist/client/assets/architectureDiagram-3BPJPVTR-iXijJkGs.js +0 -36
- package/dist/client/assets/blockDiagram-GPEHLZMM-Dmjj2Ajf.js +0 -132
- package/dist/client/assets/c4Diagram-AAUBKEIU-kJb6eQJJ.js +0 -10
- package/dist/client/assets/channel-DirF0LQF.js +0 -1
- package/dist/client/assets/chunk-2J33WTMH-T3vdO-20.js +0 -1
- package/dist/client/assets/chunk-AQP2D5EJ-BPZWXvxS.js +0 -231
- package/dist/client/assets/classDiagram-4FO5ZUOK-CdoMz3xi.js +0 -1
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-CdoMz3xi.js +0 -1
- package/dist/client/assets/cytoscape.esm-D_LviqZs.js +0 -331
- package/dist/client/assets/dagre-BM42HDAG-DcIxclFu.js +0 -4
- package/dist/client/assets/diagram-2AECGRRQ-C0xPdtMR.js +0 -43
- package/dist/client/assets/diagram-5GNKFQAL-B9i7DtoQ.js +0 -10
- package/dist/client/assets/diagram-OG6HWLK6-C6rhUBe2.js +0 -24
- package/dist/client/assets/erDiagram-TEJ5UH35-smNcu7Lp.js +0 -85
- package/dist/client/assets/flowDiagram-I6XJVG4X-CPuYTxJr.js +0 -162
- package/dist/client/assets/ganttDiagram-6RSMTGT7-xaV0Djok.js +0 -292
- package/dist/client/assets/graph-CAnANduQ.js +0 -1
- package/dist/client/assets/index-Cmcberpw.css +0 -1
- package/dist/client/assets/index-OFdfWE8x.js +0 -2
- package/dist/client/assets/infoDiagram-5YYISTIA-xm1BEpfJ.js +0 -2
- package/dist/client/assets/katex-DkKDou_j.js +0 -257
- package/dist/client/assets/katex.min-CfVKi3_s.css +0 -1
- package/dist/client/assets/katex.min-DBnXG_UH.js +0 -1
- package/dist/client/assets/layout-DGIYPm2g.js +0 -1
- package/dist/client/assets/mermaid.core-B1i-ZGkv.js +0 -309
- package/dist/client/assets/pieDiagram-4H26LBE5-oB2YhWwR.js +0 -30
- package/dist/client/assets/sankeyDiagram-5OEKKPKP-DB5dqk7n.js +0 -40
- package/dist/client/assets/sequenceDiagram-3UESZ5HK-jmqrpzCx.js +0 -162
- package/dist/client/assets/stateDiagram-AJRCARHV-U6m2A9Wk.js +0 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-00c0MW6J.js +0 -1
- package/dist/client/assets/vennDiagram-CIIHVFJN-By-tXl41.js +0 -34
- package/dist/client/assets/wardley-L42UT6IY-ByB1C5d8.js +0 -173
- package/dist/client/assets/xychartDiagram-2RQKCTM6-7czjL7-G.js +0 -7
package/bin/mdview.mjs
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import
|
|
5
|
-
import
|
|
4
|
+
import dns from "dns/promises";
|
|
5
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, realpathSync, statSync as statSync4 } from "fs";
|
|
6
|
+
import path14 from "path";
|
|
6
7
|
import { fileURLToPath } from "url";
|
|
8
|
+
import chokidar3 from "chokidar";
|
|
7
9
|
import openBrowser from "open";
|
|
8
10
|
|
|
9
11
|
// src/server/config.ts
|
|
@@ -12,7 +14,17 @@ import os from "os";
|
|
|
12
14
|
import path from "path";
|
|
13
15
|
|
|
14
16
|
// src/shared/types.ts
|
|
15
|
-
var
|
|
17
|
+
var DEFAULT_PALETTE = "flexoki";
|
|
18
|
+
var PALETTES = [
|
|
19
|
+
"flexoki",
|
|
20
|
+
"paper",
|
|
21
|
+
"solarized",
|
|
22
|
+
"everforest",
|
|
23
|
+
"rose-pine",
|
|
24
|
+
"kanagawa",
|
|
25
|
+
"catppuccin",
|
|
26
|
+
"high-contrast"
|
|
27
|
+
];
|
|
16
28
|
|
|
17
29
|
// src/server/config.ts
|
|
18
30
|
var CONFIG_FILENAME = ".mdview.json";
|
|
@@ -329,13 +341,430 @@ async function runIgnoreSubcommand(argv) {
|
|
|
329
341
|
}
|
|
330
342
|
}
|
|
331
343
|
|
|
344
|
+
// src/daemon/client.ts
|
|
345
|
+
import { spawn } from "child_process";
|
|
346
|
+
import { closeSync, openSync as openSync2, statSync as statSync2, truncateSync } from "fs";
|
|
347
|
+
|
|
348
|
+
// src/daemon/state.ts
|
|
349
|
+
import { mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "fs";
|
|
350
|
+
import path3 from "path";
|
|
351
|
+
function stateDir() {
|
|
352
|
+
return path3.dirname(globalConfigPath());
|
|
353
|
+
}
|
|
354
|
+
function daemonStatePath() {
|
|
355
|
+
return path3.join(stateDir(), "daemon.json");
|
|
356
|
+
}
|
|
357
|
+
function workspaceFilePath() {
|
|
358
|
+
return path3.join(stateDir(), "workspace.json");
|
|
359
|
+
}
|
|
360
|
+
function daemonLogPath() {
|
|
361
|
+
return path3.join(stateDir(), "daemon.log");
|
|
362
|
+
}
|
|
363
|
+
function daemonLockPath() {
|
|
364
|
+
return path3.join(stateDir(), "daemon.lock");
|
|
365
|
+
}
|
|
366
|
+
function writeJsonAtomic(file, value) {
|
|
367
|
+
mkdirSync(path3.dirname(file), { recursive: true, mode: 448 });
|
|
368
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
369
|
+
writeFileSync(tmp, `${JSON.stringify(value, null, 2)}
|
|
370
|
+
`, { mode: 384 });
|
|
371
|
+
renameSync(tmp, file);
|
|
372
|
+
}
|
|
373
|
+
function readJsonSafe(file) {
|
|
374
|
+
try {
|
|
375
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
376
|
+
} catch {
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
function isPositiveInt(v) {
|
|
381
|
+
return typeof v === "number" && Number.isInteger(v) && v > 0;
|
|
382
|
+
}
|
|
383
|
+
function readDaemonState() {
|
|
384
|
+
const raw = readJsonSafe(daemonStatePath());
|
|
385
|
+
if (!raw || typeof raw !== "object") return null;
|
|
386
|
+
const { pid, port, startedAt } = raw;
|
|
387
|
+
if (!isPositiveInt(pid) || !isPositiveInt(port) || !isPositiveInt(startedAt)) return null;
|
|
388
|
+
if (port > 65535) return null;
|
|
389
|
+
return { pid, port, startedAt };
|
|
390
|
+
}
|
|
391
|
+
function writeDaemonState(state) {
|
|
392
|
+
writeJsonAtomic(daemonStatePath(), state);
|
|
393
|
+
}
|
|
394
|
+
function clearDaemonState() {
|
|
395
|
+
rmSync(daemonStatePath(), { force: true });
|
|
396
|
+
}
|
|
397
|
+
function tryTakeSpawnLock(staleAfterMs = 1e4) {
|
|
398
|
+
mkdirSync(stateDir(), { recursive: true, mode: 448 });
|
|
399
|
+
const file = daemonLockPath();
|
|
400
|
+
try {
|
|
401
|
+
openSync(file, "wx");
|
|
402
|
+
return true;
|
|
403
|
+
} catch (err) {
|
|
404
|
+
if (err.code !== "EEXIST") throw err;
|
|
405
|
+
}
|
|
406
|
+
try {
|
|
407
|
+
if (Date.now() - statSync(file).mtimeMs < staleAfterMs) return false;
|
|
408
|
+
rmSync(file, { force: true });
|
|
409
|
+
openSync(file, "wx");
|
|
410
|
+
return true;
|
|
411
|
+
} catch {
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
function releaseSpawnLock() {
|
|
416
|
+
rmSync(daemonLockPath(), { force: true });
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// src/daemon/client.ts
|
|
420
|
+
var HEALTH_TIMEOUT_MS = 500;
|
|
421
|
+
var START_TIMEOUT_MS = 8e3;
|
|
422
|
+
var POLL_INTERVAL_MS = 50;
|
|
423
|
+
var LOG_TRUNCATE_BYTES = 1024 * 1024;
|
|
424
|
+
function sleep(ms) {
|
|
425
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
426
|
+
}
|
|
427
|
+
async function probeHealth(port) {
|
|
428
|
+
try {
|
|
429
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/health`, {
|
|
430
|
+
signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS)
|
|
431
|
+
});
|
|
432
|
+
if (!res.ok) return null;
|
|
433
|
+
const body = await res.json();
|
|
434
|
+
if (typeof body.pid !== "number" || typeof body.startedAt !== "number") return null;
|
|
435
|
+
return {
|
|
436
|
+
pid: body.pid,
|
|
437
|
+
startedAt: body.startedAt,
|
|
438
|
+
roots: Array.isArray(body.roots) ? body.roots.filter((r) => typeof r === "string") : []
|
|
439
|
+
};
|
|
440
|
+
} catch {
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
async function findLiveDaemon() {
|
|
445
|
+
const state = readDaemonState();
|
|
446
|
+
if (!state) return null;
|
|
447
|
+
const health = await probeHealth(state.port);
|
|
448
|
+
if (!health) return null;
|
|
449
|
+
if (health.pid !== state.pid || health.startedAt !== state.startedAt) return null;
|
|
450
|
+
return { state, health };
|
|
451
|
+
}
|
|
452
|
+
function spawnDaemon(cliEntry) {
|
|
453
|
+
try {
|
|
454
|
+
if (statSync2(daemonLogPath()).size > LOG_TRUNCATE_BYTES) {
|
|
455
|
+
truncateSync(daemonLogPath(), 0);
|
|
456
|
+
}
|
|
457
|
+
} catch {
|
|
458
|
+
}
|
|
459
|
+
const log = openSync2(daemonLogPath(), "a");
|
|
460
|
+
try {
|
|
461
|
+
const child = spawn(process.execPath, [cliEntry, "--daemon"], {
|
|
462
|
+
detached: true,
|
|
463
|
+
// Nothing may hold the terminal: stdout and stderr go to the log so the
|
|
464
|
+
// parent can exit while the daemon keeps running.
|
|
465
|
+
stdio: ["ignore", log, log]
|
|
466
|
+
});
|
|
467
|
+
child.unref();
|
|
468
|
+
return child;
|
|
469
|
+
} finally {
|
|
470
|
+
closeSync(log);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
async function ensureDaemon(cliEntry) {
|
|
474
|
+
const existing = await findLiveDaemon();
|
|
475
|
+
if (existing) return existing.state;
|
|
476
|
+
if (!tryTakeSpawnLock()) {
|
|
477
|
+
const found = await waitFor(() => findLiveDaemon(), START_TIMEOUT_MS);
|
|
478
|
+
if (found) return found.state;
|
|
479
|
+
throw new Error("Timed out waiting for the mdview background server to start.");
|
|
480
|
+
}
|
|
481
|
+
try {
|
|
482
|
+
clearDaemonState();
|
|
483
|
+
spawnDaemon(cliEntry);
|
|
484
|
+
const found = await waitFor(() => findLiveDaemon(), START_TIMEOUT_MS);
|
|
485
|
+
if (!found) {
|
|
486
|
+
throw new Error(
|
|
487
|
+
`The mdview background server did not start. See ${daemonLogPath()} for why.`
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
return found.state;
|
|
491
|
+
} finally {
|
|
492
|
+
releaseSpawnLock();
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
async function waitFor(attempt, timeoutMs) {
|
|
496
|
+
const deadline = Date.now() + timeoutMs;
|
|
497
|
+
for (; ; ) {
|
|
498
|
+
const result = await attempt();
|
|
499
|
+
if (result) return result;
|
|
500
|
+
if (Date.now() >= deadline) return null;
|
|
501
|
+
await sleep(POLL_INTERVAL_MS);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
async function waitForRoot(port, rootId, timeoutMs = 3e3) {
|
|
505
|
+
const found = await waitFor(async () => {
|
|
506
|
+
const health = await probeHealth(port);
|
|
507
|
+
return health?.roots.includes(rootId) ? health : null;
|
|
508
|
+
}, timeoutMs);
|
|
509
|
+
return found !== null;
|
|
510
|
+
}
|
|
511
|
+
async function stopDaemon() {
|
|
512
|
+
const state = readDaemonState();
|
|
513
|
+
if (!state) return { kind: "not-running" };
|
|
514
|
+
const live = await findLiveDaemon();
|
|
515
|
+
if (!live) {
|
|
516
|
+
clearDaemonState();
|
|
517
|
+
return { kind: "stale" };
|
|
518
|
+
}
|
|
519
|
+
try {
|
|
520
|
+
if (process.platform === "win32") {
|
|
521
|
+
process.kill(live.state.pid);
|
|
522
|
+
} else {
|
|
523
|
+
process.kill(live.state.pid, "SIGTERM");
|
|
524
|
+
}
|
|
525
|
+
} catch {
|
|
526
|
+
clearDaemonState();
|
|
527
|
+
return { kind: "stale" };
|
|
528
|
+
}
|
|
529
|
+
await waitFor(async () => await probeHealth(live.state.port) === null ? true : null, 3e3);
|
|
530
|
+
clearDaemonState();
|
|
531
|
+
return { kind: "stopped", pid: live.state.pid };
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// src/daemon/workspace-file.ts
|
|
535
|
+
import { existsSync, statSync as statSync3 } from "fs";
|
|
536
|
+
import path5 from "path";
|
|
537
|
+
|
|
538
|
+
// src/server/workspace.ts
|
|
539
|
+
import path4 from "path";
|
|
540
|
+
var ROOT_ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
541
|
+
function isValidRootId(id) {
|
|
542
|
+
return id !== "." && id !== ".." && ROOT_ID_PATTERN.test(id);
|
|
543
|
+
}
|
|
544
|
+
function deriveRootId(absPath, taken) {
|
|
545
|
+
const slug = path4.basename(absPath).toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[^a-z0-9]+/, "").replace(/[-.]+$/, "").slice(0, 64);
|
|
546
|
+
const stem = isValidRootId(slug) ? slug : "root";
|
|
547
|
+
if (!taken.has(stem)) return stem;
|
|
548
|
+
for (let n = 2; ; n++) {
|
|
549
|
+
const candidate = `${stem.slice(0, 60)}-${n}`;
|
|
550
|
+
if (!taken.has(candidate)) return candidate;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
function resolveRootId(spec, taken) {
|
|
554
|
+
if (spec.id && isValidRootId(spec.id) && !taken.has(spec.id)) return spec.id;
|
|
555
|
+
return deriveRootId(spec.absPath, taken);
|
|
556
|
+
}
|
|
557
|
+
function describeRoot(spec, id) {
|
|
558
|
+
return {
|
|
559
|
+
id,
|
|
560
|
+
name: spec.name ?? path4.basename(spec.absPath),
|
|
561
|
+
kind: spec.kind,
|
|
562
|
+
filePath: spec.kind === "file" ? spec.filePath ?? "" : ""
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
function parseWorkspacePath(roots, wsPath) {
|
|
566
|
+
const primary = roots[0];
|
|
567
|
+
if (!primary) return null;
|
|
568
|
+
const trimmed = wsPath.replace(/^\/+/, "");
|
|
569
|
+
const slash = trimmed.indexOf("/");
|
|
570
|
+
const head = slash === -1 ? trimmed : trimmed.slice(0, slash);
|
|
571
|
+
const match = roots.find((r) => r.root.id === head);
|
|
572
|
+
if (!match) return { state: primary, rel: trimmed };
|
|
573
|
+
return { state: match, rel: slash === -1 ? "" : trimmed.slice(slash + 1) };
|
|
574
|
+
}
|
|
575
|
+
function toWorkspacePath(rootId, rel) {
|
|
576
|
+
return rel ? `${rootId}/${rel}` : rootId;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// src/daemon/workspace-file.ts
|
|
580
|
+
var MAX_ROOTS = 8;
|
|
581
|
+
function validateRecord(raw) {
|
|
582
|
+
if (!raw || typeof raw !== "object") return null;
|
|
583
|
+
const r = raw;
|
|
584
|
+
if (typeof r.id !== "string" || !isValidRootId(r.id)) return null;
|
|
585
|
+
if (typeof r.path !== "string" || !path5.isAbsolute(r.path)) return null;
|
|
586
|
+
if (r.kind !== "file" && r.kind !== "dir") return null;
|
|
587
|
+
const out = { id: r.id, path: r.path, kind: r.kind };
|
|
588
|
+
if (typeof r.name === "string" && r.name.length > 0 && r.name.length <= 128) out.name = r.name;
|
|
589
|
+
if (r.kind === "file") {
|
|
590
|
+
if (typeof r.filePath !== "string" || r.filePath.length === 0) return null;
|
|
591
|
+
if (r.filePath.includes("/") || r.filePath.includes("\\")) return null;
|
|
592
|
+
out.filePath = r.filePath;
|
|
593
|
+
}
|
|
594
|
+
return out;
|
|
595
|
+
}
|
|
596
|
+
function readWorkspace() {
|
|
597
|
+
const raw = readJsonSafe(workspaceFilePath());
|
|
598
|
+
if (!raw || typeof raw !== "object") return { roots: [] };
|
|
599
|
+
const list = raw.roots;
|
|
600
|
+
if (!Array.isArray(list)) return { roots: [] };
|
|
601
|
+
const seen = /* @__PURE__ */ new Set();
|
|
602
|
+
const roots = [];
|
|
603
|
+
for (const entry of list.slice(0, MAX_ROOTS)) {
|
|
604
|
+
const rec = validateRecord(entry);
|
|
605
|
+
if (!rec || seen.has(rec.id)) continue;
|
|
606
|
+
seen.add(rec.id);
|
|
607
|
+
roots.push(rec);
|
|
608
|
+
}
|
|
609
|
+
return { roots };
|
|
610
|
+
}
|
|
611
|
+
function writeWorkspace(record) {
|
|
612
|
+
writeJsonAtomic(workspaceFilePath(), record);
|
|
613
|
+
}
|
|
614
|
+
function toRootSpecs(record) {
|
|
615
|
+
return record.roots.map((r) => ({
|
|
616
|
+
absPath: r.path,
|
|
617
|
+
id: r.id,
|
|
618
|
+
...r.name === void 0 ? {} : { name: r.name },
|
|
619
|
+
kind: r.kind,
|
|
620
|
+
...r.filePath === void 0 ? {} : { filePath: r.filePath }
|
|
621
|
+
}));
|
|
622
|
+
}
|
|
623
|
+
function resolveTarget(target) {
|
|
624
|
+
const abs = path5.resolve(target);
|
|
625
|
+
if (!existsSync(abs)) throw new Error(`Path does not exist: ${abs}`);
|
|
626
|
+
const st = statSync3(abs);
|
|
627
|
+
if (st.isDirectory()) {
|
|
628
|
+
return { absPath: abs, kind: "dir", filePath: "", name: path5.basename(abs) };
|
|
629
|
+
}
|
|
630
|
+
if (st.isFile()) {
|
|
631
|
+
const dir = path5.dirname(abs);
|
|
632
|
+
return { absPath: dir, kind: "file", filePath: path5.basename(abs), name: path5.basename(abs) };
|
|
633
|
+
}
|
|
634
|
+
throw new Error(`Unsupported path type: ${abs}`);
|
|
635
|
+
}
|
|
636
|
+
function findCovering(roots, abs) {
|
|
637
|
+
let best = null;
|
|
638
|
+
for (const root of roots) {
|
|
639
|
+
if (root.kind !== "dir" && root.path !== abs) continue;
|
|
640
|
+
const rel = path5.relative(root.path, abs);
|
|
641
|
+
if (rel.startsWith("..") || path5.isAbsolute(rel)) continue;
|
|
642
|
+
if (!best || root.path.length > best.root.path.length) {
|
|
643
|
+
best = { root, rel: rel.split(path5.sep).join("/") };
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
return best;
|
|
647
|
+
}
|
|
648
|
+
function addRoot(target) {
|
|
649
|
+
const resolved = resolveTarget(target);
|
|
650
|
+
const record = readWorkspace();
|
|
651
|
+
const fullPath = resolved.kind === "file" ? path5.join(resolved.absPath, resolved.filePath) : resolved.absPath;
|
|
652
|
+
const covering = findCovering(record.roots, fullPath);
|
|
653
|
+
if (covering) {
|
|
654
|
+
const wsPath = covering.rel ? `${covering.root.id}/${covering.rel}` : covering.root.id;
|
|
655
|
+
return {
|
|
656
|
+
root: covering.root,
|
|
657
|
+
filePath: resolved.kind === "file" ? wsPath : "",
|
|
658
|
+
focusPath: resolved.kind === "file" ? "" : wsPath,
|
|
659
|
+
changed: false
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
if (record.roots.length >= MAX_ROOTS) {
|
|
663
|
+
throw new Error(
|
|
664
|
+
`Workspace is full (${MAX_ROOTS} folders). Close one with 'mdview rm <name>' first.`
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
const id = deriveRootId(resolved.absPath, new Set(record.roots.map((r) => r.id)));
|
|
668
|
+
const root = {
|
|
669
|
+
id,
|
|
670
|
+
path: resolved.absPath,
|
|
671
|
+
kind: resolved.kind,
|
|
672
|
+
...resolved.kind === "file" ? { name: resolved.name, filePath: resolved.filePath } : {}
|
|
673
|
+
};
|
|
674
|
+
record.roots.push(root);
|
|
675
|
+
writeWorkspace(record);
|
|
676
|
+
return {
|
|
677
|
+
root,
|
|
678
|
+
filePath: resolved.kind === "file" ? `${id}/${resolved.filePath}` : "",
|
|
679
|
+
focusPath: resolved.kind === "file" ? "" : id,
|
|
680
|
+
changed: true
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
function removeRoot(idOrPath) {
|
|
684
|
+
const record = readWorkspace();
|
|
685
|
+
const abs = path5.resolve(idOrPath);
|
|
686
|
+
const idx = record.roots.findIndex((r) => r.id === idOrPath || r.path === abs);
|
|
687
|
+
if (idx === -1) return null;
|
|
688
|
+
const [removed] = record.roots.splice(idx, 1);
|
|
689
|
+
writeWorkspace(record);
|
|
690
|
+
return removed ?? null;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// src/cli-workspace.ts
|
|
694
|
+
function tilde(abs) {
|
|
695
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
696
|
+
return home && abs.startsWith(home) ? `~${abs.slice(home.length)}` : abs;
|
|
697
|
+
}
|
|
698
|
+
async function runListCommand() {
|
|
699
|
+
const { roots } = readWorkspace();
|
|
700
|
+
const live = await findLiveDaemon();
|
|
701
|
+
if (roots.length === 0) {
|
|
702
|
+
console.log("No folders open. Run `mdview <path>` to add one.");
|
|
703
|
+
} else {
|
|
704
|
+
const open = new Set(live?.health.roots ?? []);
|
|
705
|
+
const idWidth = Math.max(...roots.map((r) => r.id.length));
|
|
706
|
+
for (const [i, r] of roots.entries()) {
|
|
707
|
+
const mark = live ? open.has(r.id) ? "\u25CF" : "\u25CB" : " ";
|
|
708
|
+
const label = i === 0 ? " (primary)" : "";
|
|
709
|
+
const suffix = r.kind === "file" ? ` \u2192 ${r.filePath}` : "";
|
|
710
|
+
console.log(`${mark} ${r.id.padEnd(idWidth)} ${tilde(r.path)}${suffix}${label}`);
|
|
711
|
+
}
|
|
712
|
+
if (roots.length >= MAX_ROOTS) {
|
|
713
|
+
console.log(`
|
|
714
|
+
Workspace is full (${MAX_ROOTS}). Remove one with \`mdview rm <name>\`.`);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
console.log("");
|
|
718
|
+
if (live) {
|
|
719
|
+
console.log(`server: http://mdview.localhost:${live.state.port}/ (pid ${live.state.pid})`);
|
|
720
|
+
console.log(`logs: ${daemonLogPath()}`);
|
|
721
|
+
} else {
|
|
722
|
+
console.log("server: not running \u2014 the next `mdview <path>` will start it");
|
|
723
|
+
}
|
|
724
|
+
return 0;
|
|
725
|
+
}
|
|
726
|
+
async function runRemoveCommand(argv) {
|
|
727
|
+
const targets = argv.filter((a) => !a.startsWith("-"));
|
|
728
|
+
if (targets.length === 0) {
|
|
729
|
+
console.error("Usage: mdview rm <name|path>\u2026");
|
|
730
|
+
return 2;
|
|
731
|
+
}
|
|
732
|
+
let removedAny = false;
|
|
733
|
+
for (const target of targets) {
|
|
734
|
+
const removed = removeRoot(target);
|
|
735
|
+
if (removed) {
|
|
736
|
+
removedAny = true;
|
|
737
|
+
console.log(`Removed ${removed.id} (${tilde(removed.path)})`);
|
|
738
|
+
} else {
|
|
739
|
+
console.error(`Not open: ${target}`);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
if (!removedAny) return 2;
|
|
743
|
+
console.log("Open tabs will update on their own.");
|
|
744
|
+
return 0;
|
|
745
|
+
}
|
|
746
|
+
async function runStopCommand() {
|
|
747
|
+
const outcome = await stopDaemon();
|
|
748
|
+
switch (outcome.kind) {
|
|
749
|
+
case "stopped":
|
|
750
|
+
console.log(`mdview: stopped (pid ${outcome.pid})`);
|
|
751
|
+
return 0;
|
|
752
|
+
case "not-running":
|
|
753
|
+
console.log("mdview: not running");
|
|
754
|
+
return 0;
|
|
755
|
+
case "stale":
|
|
756
|
+
console.log("mdview: not running (cleared a stale record)");
|
|
757
|
+
return 0;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
332
761
|
// src/server/index.ts
|
|
333
|
-
import
|
|
762
|
+
import path13 from "path";
|
|
334
763
|
import chokidar2 from "chokidar";
|
|
335
764
|
import Fastify from "fastify";
|
|
336
765
|
|
|
337
766
|
// src/server/fs/ignore.ts
|
|
338
|
-
import
|
|
767
|
+
import path6 from "path";
|
|
339
768
|
var DEFAULT_IGNORED_DIRS2 = new Set(DEFAULT_IGNORED_DIRS);
|
|
340
769
|
function buildIgnoreSet(extra = []) {
|
|
341
770
|
return /* @__PURE__ */ new Set([...DEFAULT_IGNORED_DIRS2, ...extra]);
|
|
@@ -344,9 +773,9 @@ function isDirIgnored(name, set) {
|
|
|
344
773
|
return name.startsWith(".") || set.has(name);
|
|
345
774
|
}
|
|
346
775
|
function isPathIgnored(abs, root, set) {
|
|
347
|
-
const rel =
|
|
776
|
+
const rel = path6.relative(root, abs);
|
|
348
777
|
if (rel === "" || rel.startsWith("..")) return false;
|
|
349
|
-
for (const seg of rel.split(
|
|
778
|
+
for (const seg of rel.split(path6.sep)) {
|
|
350
779
|
if (!seg) continue;
|
|
351
780
|
if (seg.startsWith(".")) return true;
|
|
352
781
|
if (set.has(seg)) return true;
|
|
@@ -354,21 +783,41 @@ function isPathIgnored(abs, root, set) {
|
|
|
354
783
|
return false;
|
|
355
784
|
}
|
|
356
785
|
|
|
786
|
+
// src/server/hosts.ts
|
|
787
|
+
var LOOPBACK_NAMES = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
|
|
788
|
+
var MAX_HOST_LEN = 253;
|
|
789
|
+
function hostOnly(value) {
|
|
790
|
+
const h = value.trim().toLowerCase();
|
|
791
|
+
if (h.startsWith("[")) {
|
|
792
|
+
const end = h.indexOf("]");
|
|
793
|
+
return end === -1 ? h : h.slice(0, end + 1);
|
|
794
|
+
}
|
|
795
|
+
if (h.indexOf(":") !== h.lastIndexOf(":")) return h;
|
|
796
|
+
const colon = h.indexOf(":");
|
|
797
|
+
return colon === -1 ? h : h.slice(0, colon);
|
|
798
|
+
}
|
|
799
|
+
function isAllowedHost(value) {
|
|
800
|
+
if (!value || value.length > MAX_HOST_LEN) return false;
|
|
801
|
+
if (!/^[\x21-\x7e]+$/.test(value)) return false;
|
|
802
|
+
const host = hostOnly(value);
|
|
803
|
+
return LOOPBACK_NAMES.has(host) || /^[a-z0-9][a-z0-9.-]*\.localhost$/.test(host);
|
|
804
|
+
}
|
|
805
|
+
|
|
357
806
|
// src/server/routes/api-asset.ts
|
|
358
807
|
import { createReadStream } from "fs";
|
|
359
808
|
import { stat } from "fs/promises";
|
|
360
|
-
import
|
|
809
|
+
import path8 from "path";
|
|
361
810
|
|
|
362
811
|
// src/server/fs/resolve.ts
|
|
363
|
-
import
|
|
812
|
+
import path7 from "path";
|
|
364
813
|
function resolveSafePath(root, relPath) {
|
|
365
|
-
if (
|
|
814
|
+
if (path7.isAbsolute(relPath)) {
|
|
366
815
|
throw new Error(`Refusing absolute path: ${relPath}`);
|
|
367
816
|
}
|
|
368
|
-
const absRoot =
|
|
369
|
-
const candidate =
|
|
370
|
-
const rel =
|
|
371
|
-
if (rel.startsWith("..") ||
|
|
817
|
+
const absRoot = path7.resolve(root);
|
|
818
|
+
const candidate = path7.resolve(absRoot, relPath);
|
|
819
|
+
const rel = path7.relative(absRoot, candidate);
|
|
820
|
+
if (rel.startsWith("..") || path7.isAbsolute(rel)) {
|
|
372
821
|
throw new Error(`Path resolves outside root: ${relPath}`);
|
|
373
822
|
}
|
|
374
823
|
return candidate;
|
|
@@ -393,12 +842,16 @@ var MIME = {
|
|
|
393
842
|
".pdf": "application/pdf",
|
|
394
843
|
".txt": "text/plain; charset=utf-8"
|
|
395
844
|
};
|
|
396
|
-
function registerApiAsset(app,
|
|
845
|
+
function registerApiAsset(app, roots) {
|
|
397
846
|
app.get("/__asset/*", async (req, reply) => {
|
|
398
|
-
const
|
|
847
|
+
const wsPath = (req.params["*"] ?? "").replace(/^\/+/, "");
|
|
848
|
+
const resolved = parseWorkspacePath(roots, wsPath);
|
|
849
|
+
if (!resolved) {
|
|
850
|
+
return reply.code(404).send({ error: "Asset not found" });
|
|
851
|
+
}
|
|
399
852
|
let abs;
|
|
400
853
|
try {
|
|
401
|
-
abs = resolveSafePath(
|
|
854
|
+
abs = resolveSafePath(resolved.state.absPath, resolved.rel);
|
|
402
855
|
} catch (err) {
|
|
403
856
|
return reply.code(400).send({ error: err.message });
|
|
404
857
|
}
|
|
@@ -411,7 +864,7 @@ function registerApiAsset(app, rootAbsPath) {
|
|
|
411
864
|
if (!st.isFile()) {
|
|
412
865
|
return reply.code(404).send({ error: "Not a file" });
|
|
413
866
|
}
|
|
414
|
-
const ext =
|
|
867
|
+
const ext = path8.extname(abs).toLowerCase();
|
|
415
868
|
const mime = MIME[ext];
|
|
416
869
|
if (!mime) {
|
|
417
870
|
return reply.code(404).send({ error: "Asset type not allowed" });
|
|
@@ -464,18 +917,18 @@ function computeBodyStartLine(raw) {
|
|
|
464
917
|
}
|
|
465
918
|
|
|
466
919
|
// src/render/links.ts
|
|
467
|
-
import
|
|
920
|
+
import path9 from "path/posix";
|
|
468
921
|
var MD_EXT_RE = /\.(md|markdown|mdx)$/i;
|
|
469
922
|
var HREF_RE = /<a\s+([^>]*?)href="([^"#?]*)(\?[^"#]*)?(#[^"]*)?"([^>]*)>/gi;
|
|
470
923
|
var IMG_SRC_RE = /<img\s+([^>]*?)src="([^"]+)"([^>]*)>/gi;
|
|
471
924
|
function tagInternalLinks(html, currentRelPath) {
|
|
472
|
-
const currentDir =
|
|
925
|
+
const currentDir = path9.dirname(currentRelPath);
|
|
473
926
|
return html.replace(HREF_RE, (full, pre, target, query, hash, post) => {
|
|
474
927
|
if (!target || /^https?:\/\//i.test(target) || target.startsWith("mailto:")) {
|
|
475
928
|
return full;
|
|
476
929
|
}
|
|
477
930
|
if (!MD_EXT_RE.test(target)) return full;
|
|
478
|
-
const resolved =
|
|
931
|
+
const resolved = path9.normalize(path9.join(currentDir, target));
|
|
479
932
|
let decoded;
|
|
480
933
|
try {
|
|
481
934
|
decoded = decodeURIComponent(resolved);
|
|
@@ -488,12 +941,12 @@ function tagInternalLinks(html, currentRelPath) {
|
|
|
488
941
|
});
|
|
489
942
|
}
|
|
490
943
|
function rewriteImageSrc(html, currentRelPath) {
|
|
491
|
-
const currentDir =
|
|
944
|
+
const currentDir = path9.dirname(currentRelPath);
|
|
492
945
|
return html.replace(IMG_SRC_RE, (full, pre, src, post) => {
|
|
493
946
|
if (!src) return full;
|
|
494
947
|
if (/^(https?:\/\/|data:|\/__asset\/)/i.test(src)) return full;
|
|
495
948
|
const trimmed = src.replace(/^\/+/, "");
|
|
496
|
-
const resolved =
|
|
949
|
+
const resolved = path9.normalize(path9.join(currentDir, trimmed));
|
|
497
950
|
if (resolved.startsWith("..")) return full;
|
|
498
951
|
return `<img ${pre}src="/__asset/${resolved}"${post}>`;
|
|
499
952
|
});
|
|
@@ -605,20 +1058,21 @@ var COMMON_LANGUAGES = [
|
|
|
605
1058
|
"diff",
|
|
606
1059
|
"dockerfile"
|
|
607
1060
|
];
|
|
608
|
-
var
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
"
|
|
613
|
-
"
|
|
614
|
-
"
|
|
615
|
-
"
|
|
616
|
-
"
|
|
617
|
-
"
|
|
618
|
-
"
|
|
1061
|
+
var PALETTE_THEMES = {
|
|
1062
|
+
// Flexoki has no upstream TextMate theme. `min-light` and `vesper` are the
|
|
1063
|
+
// closest bundled stand-ins: both are deliberately low-chroma, which is the
|
|
1064
|
+
// one property of Flexoki that a louder theme would contradict.
|
|
1065
|
+
flexoki: { light: "min-light", dark: "vesper" },
|
|
1066
|
+
paper: { light: "min-light", dark: "vitesse-dark" },
|
|
1067
|
+
solarized: { light: "solarized-light", dark: "solarized-dark" },
|
|
1068
|
+
everforest: { light: "everforest-light", dark: "everforest-dark" },
|
|
1069
|
+
"rose-pine": { light: "rose-pine-dawn", dark: "rose-pine" },
|
|
1070
|
+
kanagawa: { light: "kanagawa-lotus", dark: "kanagawa-wave" },
|
|
1071
|
+
catppuccin: { light: "catppuccin-latte", dark: "catppuccin-mocha" },
|
|
1072
|
+
"high-contrast": { light: "github-light-high-contrast", dark: "github-dark-high-contrast" }
|
|
619
1073
|
};
|
|
620
1074
|
var UNDERLYING_THEMES = Array.from(
|
|
621
|
-
new Set(Object.values(
|
|
1075
|
+
new Set(Object.values(PALETTE_THEMES).flatMap((t) => [t.light, t.dark]))
|
|
622
1076
|
);
|
|
623
1077
|
var highlighterPromise = null;
|
|
624
1078
|
function getHighlighter() {
|
|
@@ -633,7 +1087,7 @@ function getHighlighter() {
|
|
|
633
1087
|
function escapeHtml(s) {
|
|
634
1088
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
635
1089
|
}
|
|
636
|
-
async function highlightCode(code, lang) {
|
|
1090
|
+
async function highlightCode(code, lang, palette = DEFAULT_PALETTE) {
|
|
637
1091
|
const highlighter = await getHighlighter();
|
|
638
1092
|
const loaded = highlighter.getLoadedLanguages();
|
|
639
1093
|
let effectiveLang = lang;
|
|
@@ -647,7 +1101,7 @@ async function highlightCode(code, lang) {
|
|
|
647
1101
|
try {
|
|
648
1102
|
return highlighter.codeToHtml(code, {
|
|
649
1103
|
lang: effectiveLang || "text",
|
|
650
|
-
themes:
|
|
1104
|
+
themes: PALETTE_THEMES[palette],
|
|
651
1105
|
defaultColor: false
|
|
652
1106
|
});
|
|
653
1107
|
} catch {
|
|
@@ -669,7 +1123,7 @@ md.use(anchor, {
|
|
|
669
1123
|
});
|
|
670
1124
|
md.use(taskLists, { enabled: false, label: false });
|
|
671
1125
|
md.use(mathPlugin);
|
|
672
|
-
async function renderMarkdown(source, bodyStartLine = 0) {
|
|
1126
|
+
async function renderMarkdown(source, bodyStartLine = 0, palette = DEFAULT_PALETTE) {
|
|
673
1127
|
const tokens = md.parse(source, {});
|
|
674
1128
|
for (const token of tokens) {
|
|
675
1129
|
if (token.type === "heading_open" && token.map) {
|
|
@@ -682,7 +1136,7 @@ async function renderMarkdown(source, bodyStartLine = 0) {
|
|
|
682
1136
|
token.content = `<div class="mermaid-block" data-source="${encodeURIComponent(token.content)}"></div>
|
|
683
1137
|
`;
|
|
684
1138
|
} else {
|
|
685
|
-
const highlighted = await highlightCode(token.content, lang);
|
|
1139
|
+
const highlighted = await highlightCode(token.content, lang, palette);
|
|
686
1140
|
token.type = "html_block";
|
|
687
1141
|
token.content = highlighted + "\n";
|
|
688
1142
|
}
|
|
@@ -725,57 +1179,70 @@ function nest(flat) {
|
|
|
725
1179
|
}
|
|
726
1180
|
|
|
727
1181
|
// src/server/routes/api-file.ts
|
|
728
|
-
function registerApiFile(app,
|
|
729
|
-
app.get(
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
1182
|
+
function registerApiFile(app, roots) {
|
|
1183
|
+
app.get(
|
|
1184
|
+
"/api/file",
|
|
1185
|
+
async (req, reply) => {
|
|
1186
|
+
const requested = req.query.path?.trim() ?? "";
|
|
1187
|
+
const rawPalette = req.query.palette?.trim() ?? "";
|
|
1188
|
+
const palette = PALETTES.includes(rawPalette) ? rawPalette : DEFAULT_PALETTE;
|
|
1189
|
+
const primary = roots[0];
|
|
1190
|
+
if (!primary) {
|
|
1191
|
+
return reply.code(404).send({ error: "No folders are open" });
|
|
1192
|
+
}
|
|
1193
|
+
let wsPath = requested;
|
|
1194
|
+
if (!wsPath) {
|
|
1195
|
+
if (primary.root.kind !== "file") {
|
|
1196
|
+
return reply.code(400).send({ error: "Missing ?path" });
|
|
1197
|
+
}
|
|
1198
|
+
wsPath = toWorkspacePath(primary.root.id, primary.root.filePath);
|
|
1199
|
+
}
|
|
1200
|
+
const resolved = parseWorkspacePath(roots, wsPath);
|
|
1201
|
+
if (!resolved) {
|
|
1202
|
+
return reply.code(404).send({ error: "File not found" });
|
|
1203
|
+
}
|
|
1204
|
+
const { state, rel } = resolved;
|
|
1205
|
+
if (state.root.kind === "file" && rel !== state.root.filePath) {
|
|
734
1206
|
return reply.code(404).send({ error: "File not found in single-file mode" });
|
|
735
1207
|
}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
if (!requested) {
|
|
739
|
-
return reply.code(400).send({ error: "Missing ?path" });
|
|
1208
|
+
if (!/\.(md|markdown|mdx)$/i.test(rel)) {
|
|
1209
|
+
return reply.code(400).send({ error: "Only markdown files are supported" });
|
|
740
1210
|
}
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
1211
|
+
let absPath;
|
|
1212
|
+
try {
|
|
1213
|
+
absPath = resolveSafePath(state.absPath, rel);
|
|
1214
|
+
} catch (err) {
|
|
1215
|
+
return reply.code(400).send({ error: err.message });
|
|
1216
|
+
}
|
|
1217
|
+
let raw;
|
|
1218
|
+
let mtimeMs;
|
|
1219
|
+
try {
|
|
1220
|
+
const [content, st] = await Promise.all([
|
|
1221
|
+
readFile3(absPath, "utf8"),
|
|
1222
|
+
stat2(absPath)
|
|
1223
|
+
]);
|
|
1224
|
+
raw = content;
|
|
1225
|
+
mtimeMs = st.mtimeMs;
|
|
1226
|
+
} catch {
|
|
1227
|
+
return reply.code(404).send({ error: "File not found" });
|
|
1228
|
+
}
|
|
1229
|
+
const { data, body, bodyStartLine } = parseFrontmatter(raw);
|
|
1230
|
+
const { html: rawHtml, tokens } = await renderMarkdown(body, bodyStartLine, palette);
|
|
1231
|
+
const wsRel = toWorkspacePath(state.root.id, rel);
|
|
1232
|
+
const html = rewriteImageSrc(tagInternalLinks(rawHtml, wsRel), wsRel);
|
|
1233
|
+
const outline = extractOutline(tokens);
|
|
1234
|
+
const title = (typeof data?.title === "string" ? data.title : null) ?? outline[0]?.text ?? null;
|
|
1235
|
+
const result = {
|
|
1236
|
+
relPath: wsRel,
|
|
1237
|
+
html,
|
|
1238
|
+
outline,
|
|
1239
|
+
frontmatter: data,
|
|
1240
|
+
title,
|
|
1241
|
+
lastModified: mtimeMs
|
|
1242
|
+
};
|
|
1243
|
+
return reply.send(result);
|
|
751
1244
|
}
|
|
752
|
-
|
|
753
|
-
let mtimeMs;
|
|
754
|
-
try {
|
|
755
|
-
const [content, st] = await Promise.all([
|
|
756
|
-
readFile3(absPath, "utf8"),
|
|
757
|
-
stat2(absPath)
|
|
758
|
-
]);
|
|
759
|
-
raw = content;
|
|
760
|
-
mtimeMs = st.mtimeMs;
|
|
761
|
-
} catch {
|
|
762
|
-
return reply.code(404).send({ error: "File not found" });
|
|
763
|
-
}
|
|
764
|
-
const { data, body, bodyStartLine } = parseFrontmatter(raw);
|
|
765
|
-
const { html: rawHtml, tokens } = await renderMarkdown(body, bodyStartLine);
|
|
766
|
-
const html = rewriteImageSrc(tagInternalLinks(rawHtml, relPath), relPath);
|
|
767
|
-
const outline = extractOutline(tokens);
|
|
768
|
-
const title = (typeof data?.title === "string" ? data.title : null) ?? outline[0]?.text ?? null;
|
|
769
|
-
const result = {
|
|
770
|
-
relPath,
|
|
771
|
-
html,
|
|
772
|
-
outline,
|
|
773
|
-
frontmatter: data,
|
|
774
|
-
title,
|
|
775
|
-
lastModified: mtimeMs
|
|
776
|
-
};
|
|
777
|
-
return reply.send(result);
|
|
778
|
-
});
|
|
1245
|
+
);
|
|
779
1246
|
}
|
|
780
1247
|
|
|
781
1248
|
// src/server/fs/grep.ts
|
|
@@ -865,13 +1332,12 @@ function flattenMdRelPaths(nodes, out = []) {
|
|
|
865
1332
|
|
|
866
1333
|
// src/server/fs/tree.ts
|
|
867
1334
|
import { readdir } from "fs/promises";
|
|
868
|
-
import
|
|
1335
|
+
import path10 from "path";
|
|
869
1336
|
async function walkFolder(root, opts = {}) {
|
|
870
1337
|
const ignore = opts.ignore ?? DEFAULT_IGNORED_DIRS2;
|
|
871
|
-
return walkInner(root, "", ignore);
|
|
1338
|
+
return walkInner(root, opts.relBase ?? "", ignore);
|
|
872
1339
|
}
|
|
873
|
-
async function walkInner(
|
|
874
|
-
const absDir = path7.join(root, relBase);
|
|
1340
|
+
async function walkInner(absDir, relBase, ignore) {
|
|
875
1341
|
const entries = await readdir(absDir, { withFileTypes: true });
|
|
876
1342
|
const out = [];
|
|
877
1343
|
for (const entry of entries) {
|
|
@@ -882,7 +1348,7 @@ async function walkInner(root, relBase, ignore) {
|
|
|
882
1348
|
}
|
|
883
1349
|
const childRel = relBase ? `${relBase}/${entry.name}` : entry.name;
|
|
884
1350
|
if (entry.isDirectory()) {
|
|
885
|
-
const children = await walkInner(
|
|
1351
|
+
const children = await walkInner(path10.join(absDir, entry.name), childRel, ignore);
|
|
886
1352
|
out.push({
|
|
887
1353
|
name: entry.name,
|
|
888
1354
|
relPath: childRel,
|
|
@@ -890,7 +1356,7 @@ async function walkInner(root, relBase, ignore) {
|
|
|
890
1356
|
children
|
|
891
1357
|
});
|
|
892
1358
|
} else if (entry.isFile()) {
|
|
893
|
-
const ext =
|
|
1359
|
+
const ext = path10.extname(entry.name).toLowerCase();
|
|
894
1360
|
out.push({
|
|
895
1361
|
name: entry.name,
|
|
896
1362
|
relPath: childRel,
|
|
@@ -920,7 +1386,7 @@ function buildSnippet(line, start, end, radius) {
|
|
|
920
1386
|
highlight: [hlStart, hlEnd]
|
|
921
1387
|
};
|
|
922
1388
|
}
|
|
923
|
-
async function grepFiles(
|
|
1389
|
+
async function grepFiles(roots, query, opts = {}) {
|
|
924
1390
|
const perFileCap = opts.perFileCap ?? 20;
|
|
925
1391
|
const globalCap = opts.globalCap ?? 200;
|
|
926
1392
|
const snippetRadius = opts.snippetRadius ?? 60;
|
|
@@ -936,64 +1402,75 @@ async function grepFiles(rootAbsPath, query, opts = {}) {
|
|
|
936
1402
|
regex: opts.regex ?? false
|
|
937
1403
|
});
|
|
938
1404
|
if (!pattern.valid) return { query, results: [], truncated: false };
|
|
939
|
-
const tree = await walkFolder(rootAbsPath, { ignore: opts.ignore });
|
|
940
|
-
const files = flattenMdRelPaths(tree);
|
|
941
1405
|
const results = [];
|
|
942
1406
|
let totalHits = 0;
|
|
943
1407
|
let truncated = false;
|
|
944
|
-
for (const
|
|
1408
|
+
for (const root of roots) {
|
|
945
1409
|
if (totalHits >= globalCap) {
|
|
946
1410
|
truncated = true;
|
|
947
1411
|
break;
|
|
948
1412
|
}
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
}
|
|
961
|
-
const { body } = parseFrontmatter(raw);
|
|
962
|
-
const lines = body.split(/\r?\n/);
|
|
963
|
-
const fileHits = [];
|
|
964
|
-
let fileTotal = 0;
|
|
965
|
-
let fileTruncated = false;
|
|
966
|
-
for (let i = 0; i < lines.length; i++) {
|
|
967
|
-
const line = lines[i];
|
|
968
|
-
if (regexMode && line.length > maxLineLenForRegex) {
|
|
969
|
-
fileTruncated = true;
|
|
1413
|
+
const tree = await walkFolder(root.absPath, { ignore: root.ignore ?? opts.ignore });
|
|
1414
|
+
const files = flattenMdRelPaths(tree);
|
|
1415
|
+
for (const relPath of files) {
|
|
1416
|
+
if (totalHits >= globalCap) {
|
|
1417
|
+
truncated = true;
|
|
1418
|
+
break;
|
|
1419
|
+
}
|
|
1420
|
+
let absPath;
|
|
1421
|
+
try {
|
|
1422
|
+
absPath = resolveSafePath(root.absPath, relPath);
|
|
1423
|
+
} catch {
|
|
970
1424
|
continue;
|
|
971
1425
|
}
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1426
|
+
let raw;
|
|
1427
|
+
try {
|
|
1428
|
+
raw = await readFile4(absPath, "utf8");
|
|
1429
|
+
} catch {
|
|
1430
|
+
continue;
|
|
1431
|
+
}
|
|
1432
|
+
const { body } = parseFrontmatter(raw);
|
|
1433
|
+
const lines = body.split(/\r?\n/);
|
|
1434
|
+
const fileHits = [];
|
|
1435
|
+
let fileTotal = 0;
|
|
1436
|
+
let fileTruncated = false;
|
|
1437
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1438
|
+
const line = lines[i];
|
|
1439
|
+
if (regexMode && line.length > maxLineLenForRegex) {
|
|
986
1440
|
fileTruncated = true;
|
|
1441
|
+
continue;
|
|
987
1442
|
}
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
1443
|
+
const { matches, truncated: lineTruncated } = regexMode ? pattern.matchAllWithBudget(line, perLineBudgetMs) : { matches: pattern.matchAll(line), truncated: false };
|
|
1444
|
+
if (lineTruncated) fileTruncated = true;
|
|
1445
|
+
for (const m of matches) {
|
|
1446
|
+
fileTotal++;
|
|
1447
|
+
if (fileHits.length < perFileCap) {
|
|
1448
|
+
const { snippet, highlight } = buildSnippet(
|
|
1449
|
+
line,
|
|
1450
|
+
m.index,
|
|
1451
|
+
m.index + m.length,
|
|
1452
|
+
snippetRadius
|
|
1453
|
+
);
|
|
1454
|
+
fileHits.push({ line: i + 1, col: m.index, snippet, highlight });
|
|
1455
|
+
totalHits++;
|
|
1456
|
+
} else {
|
|
1457
|
+
fileTruncated = true;
|
|
1458
|
+
}
|
|
1459
|
+
if (totalHits >= globalCap) {
|
|
1460
|
+
truncated = true;
|
|
1461
|
+
break;
|
|
1462
|
+
}
|
|
991
1463
|
}
|
|
1464
|
+
if (totalHits >= globalCap) break;
|
|
1465
|
+
}
|
|
1466
|
+
if (fileHits.length > 0) {
|
|
1467
|
+
results.push({
|
|
1468
|
+
relPath: `${root.id}/${relPath}`,
|
|
1469
|
+
hits: fileHits,
|
|
1470
|
+
total: fileTotal,
|
|
1471
|
+
truncated: fileTruncated
|
|
1472
|
+
});
|
|
992
1473
|
}
|
|
993
|
-
if (totalHits >= globalCap) break;
|
|
994
|
-
}
|
|
995
|
-
if (fileHits.length > 0) {
|
|
996
|
-
results.push({ relPath, hits: fileHits, total: fileTotal, truncated: fileTruncated });
|
|
997
1474
|
}
|
|
998
1475
|
}
|
|
999
1476
|
return { query, results, truncated };
|
|
@@ -1004,11 +1481,8 @@ var MAX_QUERY_LEN = 200;
|
|
|
1004
1481
|
function isTrue(v) {
|
|
1005
1482
|
return v === "1" || v === "true";
|
|
1006
1483
|
}
|
|
1007
|
-
function registerApiSearch(app,
|
|
1484
|
+
function registerApiSearch(app, roots) {
|
|
1008
1485
|
app.get("/api/search", async (req, reply) => {
|
|
1009
|
-
if (rootInfo.rootKind === "file") {
|
|
1010
|
-
return reply.send({ query: req.query.q ?? "", results: [], truncated: false });
|
|
1011
|
-
}
|
|
1012
1486
|
const q = (req.query.q ?? "").trim();
|
|
1013
1487
|
if (q.length === 0) {
|
|
1014
1488
|
return reply.send({ query: "", results: [], truncated: false });
|
|
@@ -1016,43 +1490,76 @@ function registerApiSearch(app, rootAbsPath, rootInfo, configState) {
|
|
|
1016
1490
|
if (q.length > MAX_QUERY_LEN) {
|
|
1017
1491
|
return reply.code(400).send({ error: "Query too long" });
|
|
1018
1492
|
}
|
|
1019
|
-
const
|
|
1493
|
+
const searchable = roots.filter((r) => r.root.kind === "dir").map((r) => ({ absPath: r.absPath, id: r.root.id, ignore: r.ignoreSet }));
|
|
1494
|
+
const out = await grepFiles(searchable, q, {
|
|
1020
1495
|
caseSensitive: isTrue(req.query.case),
|
|
1021
1496
|
wholeWord: isTrue(req.query.word),
|
|
1022
|
-
regex: isTrue(req.query.regex)
|
|
1023
|
-
ignore: configState.ignoreSet
|
|
1497
|
+
regex: isTrue(req.query.regex)
|
|
1024
1498
|
});
|
|
1025
1499
|
return reply.send(out);
|
|
1026
1500
|
});
|
|
1027
1501
|
}
|
|
1028
1502
|
|
|
1029
1503
|
// src/server/routes/api-tree.ts
|
|
1030
|
-
import
|
|
1504
|
+
import path11 from "path";
|
|
1031
1505
|
function applyOverrides(config, paletteOverride) {
|
|
1032
1506
|
if (!paletteOverride) return config;
|
|
1033
1507
|
return { ...config ?? {}, palette: paletteOverride };
|
|
1034
1508
|
}
|
|
1035
|
-
function
|
|
1509
|
+
async function rootChildren(state) {
|
|
1510
|
+
const { id, kind, filePath } = state.root;
|
|
1511
|
+
if (kind === "file") {
|
|
1512
|
+
return [
|
|
1513
|
+
{
|
|
1514
|
+
name: path11.basename(filePath),
|
|
1515
|
+
relPath: toWorkspacePath(id, filePath),
|
|
1516
|
+
type: "file",
|
|
1517
|
+
isMarkdown: true
|
|
1518
|
+
}
|
|
1519
|
+
];
|
|
1520
|
+
}
|
|
1521
|
+
return walkFolder(state.absPath, { ignore: state.ignoreSet, relBase: id });
|
|
1522
|
+
}
|
|
1523
|
+
function registerApiTree(app, roots, paletteOverride) {
|
|
1036
1524
|
app.get("/api/tree", async (_req, reply) => {
|
|
1037
|
-
const config = applyOverrides(
|
|
1038
|
-
if (
|
|
1039
|
-
const
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
}
|
|
1046
|
-
];
|
|
1047
|
-
return reply.send({ root: rootInfo, tree: single, config });
|
|
1525
|
+
const config = applyOverrides(roots[0]?.config ?? null, paletteOverride);
|
|
1526
|
+
if (roots.length === 1) {
|
|
1527
|
+
const only = roots[0];
|
|
1528
|
+
return reply.send({
|
|
1529
|
+
roots: [only.root],
|
|
1530
|
+
tree: await rootChildren(only),
|
|
1531
|
+
config
|
|
1532
|
+
});
|
|
1048
1533
|
}
|
|
1049
|
-
const tree =
|
|
1050
|
-
|
|
1534
|
+
const tree = [];
|
|
1535
|
+
for (const state of roots) {
|
|
1536
|
+
tree.push({
|
|
1537
|
+
name: state.root.name,
|
|
1538
|
+
relPath: state.root.id,
|
|
1539
|
+
type: "dir",
|
|
1540
|
+
children: await rootChildren(state)
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
return reply.send({ roots: roots.map((r) => r.root), tree, config });
|
|
1051
1544
|
});
|
|
1052
1545
|
}
|
|
1053
1546
|
|
|
1547
|
+
// src/server/routes/health.ts
|
|
1548
|
+
function registerHealth(app, identity, roots) {
|
|
1549
|
+
app.get(
|
|
1550
|
+
"/api/health",
|
|
1551
|
+
async (_req, reply) => reply.send({
|
|
1552
|
+
ok: true,
|
|
1553
|
+
pid: identity.pid,
|
|
1554
|
+
startedAt: identity.startedAt,
|
|
1555
|
+
roots: roots.map((r) => r.root.id)
|
|
1556
|
+
})
|
|
1557
|
+
);
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1054
1560
|
// src/server/routes/sse.ts
|
|
1055
|
-
function registerSse(app,
|
|
1561
|
+
function registerSse(app, hub, hooks = {}) {
|
|
1562
|
+
let clients = 0;
|
|
1056
1563
|
app.get("/api/watch", (req, reply) => {
|
|
1057
1564
|
reply.raw.writeHead(200, {
|
|
1058
1565
|
"Content-Type": "text/event-stream",
|
|
@@ -1062,11 +1569,15 @@ function registerSse(app, watcher) {
|
|
|
1062
1569
|
});
|
|
1063
1570
|
reply.raw.write(": connected\n\n");
|
|
1064
1571
|
let closed = false;
|
|
1572
|
+
clients++;
|
|
1573
|
+
hooks.onClientsChanged?.(clients);
|
|
1065
1574
|
const cleanup = () => {
|
|
1066
1575
|
if (closed) return;
|
|
1067
1576
|
closed = true;
|
|
1068
1577
|
clearInterval(heartbeat);
|
|
1069
|
-
|
|
1578
|
+
hub.off("event", send);
|
|
1579
|
+
clients--;
|
|
1580
|
+
hooks.onClientsChanged?.(clients);
|
|
1070
1581
|
try {
|
|
1071
1582
|
reply.raw.end();
|
|
1072
1583
|
} catch {
|
|
@@ -1092,7 +1603,7 @@ function registerSse(app, watcher) {
|
|
|
1092
1603
|
cleanup();
|
|
1093
1604
|
}
|
|
1094
1605
|
}, 15e3);
|
|
1095
|
-
|
|
1606
|
+
hub.on("event", send);
|
|
1096
1607
|
req.raw.on("close", cleanup);
|
|
1097
1608
|
req.raw.on("error", cleanup);
|
|
1098
1609
|
reply.raw.on("error", cleanup);
|
|
@@ -1102,12 +1613,25 @@ function registerSse(app, watcher) {
|
|
|
1102
1613
|
|
|
1103
1614
|
// src/server/watcher.ts
|
|
1104
1615
|
import { EventEmitter } from "events";
|
|
1105
|
-
import
|
|
1616
|
+
import path12 from "path";
|
|
1106
1617
|
import chokidar from "chokidar";
|
|
1618
|
+
function createEventHub() {
|
|
1619
|
+
const emitter = new EventEmitter();
|
|
1620
|
+
emitter.setMaxListeners(0);
|
|
1621
|
+
return {
|
|
1622
|
+
on: (event, listener) => emitter.on(event, listener),
|
|
1623
|
+
off: (event, listener) => emitter.off(event, listener),
|
|
1624
|
+
emitSynthetic: (e) => emitter.emit("event", e),
|
|
1625
|
+
close: async () => {
|
|
1626
|
+
emitter.removeAllListeners();
|
|
1627
|
+
}
|
|
1628
|
+
};
|
|
1629
|
+
}
|
|
1107
1630
|
function createWatcher(rootAbsPath, opts = {}) {
|
|
1108
1631
|
const emitter = new EventEmitter();
|
|
1109
1632
|
emitter.setMaxListeners(0);
|
|
1110
1633
|
const ignore = opts.ignore ?? DEFAULT_IGNORED_DIRS2;
|
|
1634
|
+
const prefix = opts.prefix ?? "";
|
|
1111
1635
|
const watcher = chokidar.watch(rootAbsPath, {
|
|
1112
1636
|
ignoreInitial: true,
|
|
1113
1637
|
ignored: (p) => isPathIgnored(p, rootAbsPath, ignore),
|
|
@@ -1130,9 +1654,10 @@ function createWatcher(rootAbsPath, opts = {}) {
|
|
|
1130
1654
|
}
|
|
1131
1655
|
});
|
|
1132
1656
|
function emit(kind, abs) {
|
|
1133
|
-
const rel =
|
|
1657
|
+
const rel = path12.relative(rootAbsPath, abs).split(path12.sep).join("/");
|
|
1134
1658
|
if (!rel) return;
|
|
1135
|
-
|
|
1659
|
+
const relPath = prefix ? `${prefix}/${rel}` : rel;
|
|
1660
|
+
emitter.emit("event", { kind, relPath });
|
|
1136
1661
|
}
|
|
1137
1662
|
watcher.on("change", (p) => emit("change", p));
|
|
1138
1663
|
watcher.on("add", (p) => emit("add", p));
|
|
@@ -1165,6 +1690,10 @@ function buildCspHtml(embedMode) {
|
|
|
1165
1690
|
async function createServer(opts) {
|
|
1166
1691
|
const app = Fastify({ logger: false });
|
|
1167
1692
|
const cspHtml = buildCspHtml(opts.embedMode ?? false);
|
|
1693
|
+
app.addHook("onRequest", async (req, reply) => {
|
|
1694
|
+
if (isAllowedHost(req.headers.host)) return;
|
|
1695
|
+
return reply.code(403).type("text/plain; charset=utf-8").send("Forbidden");
|
|
1696
|
+
});
|
|
1168
1697
|
app.addHook("onSend", async (_req, reply, payload) => {
|
|
1169
1698
|
const ct = String(reply.getHeader("content-type") ?? "");
|
|
1170
1699
|
if (ct.startsWith("text/html")) {
|
|
@@ -1174,33 +1703,76 @@ async function createServer(opts) {
|
|
|
1174
1703
|
}
|
|
1175
1704
|
return payload;
|
|
1176
1705
|
});
|
|
1177
|
-
const
|
|
1178
|
-
const
|
|
1179
|
-
const
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1706
|
+
const roots = [];
|
|
1707
|
+
const configWatchers = /* @__PURE__ */ new Map();
|
|
1708
|
+
const hub = createEventHub();
|
|
1709
|
+
async function openRoot(spec, id) {
|
|
1710
|
+
const config = await loadEffectiveConfig(spec.absPath);
|
|
1711
|
+
const ignoreSet = buildIgnoreSet(config?.ignore ?? []);
|
|
1712
|
+
const state = {
|
|
1713
|
+
root: describeRoot(spec, id),
|
|
1714
|
+
absPath: spec.absPath,
|
|
1715
|
+
ignoreSet,
|
|
1716
|
+
config,
|
|
1717
|
+
watcher: createWatcher(spec.absPath, { ignore: ignoreSet, prefix: id })
|
|
1718
|
+
};
|
|
1719
|
+
state.watcher.on("event", (e) => hub.emitSynthetic(e));
|
|
1720
|
+
const configWatcher = chokidar2.watch(path13.join(spec.absPath, CONFIG_FILENAME), {
|
|
1721
|
+
ignoreInitial: true,
|
|
1722
|
+
persistent: true,
|
|
1723
|
+
awaitWriteFinish: { stabilityThreshold: 60, pollInterval: 30 }
|
|
1724
|
+
});
|
|
1725
|
+
const reloadConfig = async () => {
|
|
1726
|
+
state.config = await loadEffectiveConfig(spec.absPath);
|
|
1727
|
+
const event = { kind: "config", relPath: `${id}/${CONFIG_FILENAME}` };
|
|
1728
|
+
hub.emitSynthetic(event);
|
|
1729
|
+
};
|
|
1730
|
+
configWatcher.on("add", () => void reloadConfig());
|
|
1731
|
+
configWatcher.on("change", () => void reloadConfig());
|
|
1732
|
+
configWatcher.on("unlink", () => void reloadConfig());
|
|
1733
|
+
configWatchers.set(id, configWatcher);
|
|
1734
|
+
return state;
|
|
1735
|
+
}
|
|
1736
|
+
async function closeRoot(state) {
|
|
1737
|
+
const cw = configWatchers.get(state.root.id);
|
|
1738
|
+
configWatchers.delete(state.root.id);
|
|
1739
|
+
await Promise.all([state.watcher.close(), cw?.close()]);
|
|
1740
|
+
}
|
|
1741
|
+
async function setRoots(specs) {
|
|
1742
|
+
const taken = /* @__PURE__ */ new Set();
|
|
1743
|
+
const wanted = specs.map((spec) => {
|
|
1744
|
+
const id = resolveRootId(spec, taken);
|
|
1745
|
+
taken.add(id);
|
|
1746
|
+
return { id, spec };
|
|
1747
|
+
});
|
|
1748
|
+
const keep = new Map(
|
|
1749
|
+
roots.filter((r) => wanted.some((w) => w.id === r.root.id && w.spec.absPath === r.absPath)).map((r) => [r.root.id, r])
|
|
1750
|
+
);
|
|
1751
|
+
const dropped = roots.filter((r) => !keep.has(r.root.id));
|
|
1752
|
+
const next = [];
|
|
1753
|
+
for (const { id, spec } of wanted) {
|
|
1754
|
+
const existing = keep.get(id);
|
|
1755
|
+
next.push(existing ?? await openRoot(spec, id));
|
|
1756
|
+
}
|
|
1757
|
+
roots.length = 0;
|
|
1758
|
+
roots.push(...next);
|
|
1759
|
+
await Promise.all(dropped.map(closeRoot));
|
|
1760
|
+
hub.emitSynthetic({ kind: "workspace" });
|
|
1761
|
+
}
|
|
1762
|
+
{
|
|
1763
|
+
const taken = /* @__PURE__ */ new Set();
|
|
1764
|
+
for (const spec of opts.roots) {
|
|
1765
|
+
const id = resolveRootId(spec, taken);
|
|
1766
|
+
taken.add(id);
|
|
1767
|
+
roots.push(await openRoot(spec, id));
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
registerApiFile(app, roots);
|
|
1771
|
+
registerApiTree(app, roots, opts.paletteOverride);
|
|
1772
|
+
registerApiAsset(app, roots);
|
|
1773
|
+
registerApiSearch(app, roots);
|
|
1774
|
+
registerSse(app, hub, { ...opts.onSseClientsChanged ? { onClientsChanged: opts.onSseClientsChanged } : {} });
|
|
1775
|
+
if (opts.identity) registerHealth(app, opts.identity, roots);
|
|
1204
1776
|
await app.register(import("@fastify/static"), {
|
|
1205
1777
|
root: opts.clientDir,
|
|
1206
1778
|
prefix: "/"
|
|
@@ -1225,10 +1797,13 @@ async function createServer(opts) {
|
|
|
1225
1797
|
return reply.sendFile("index.html");
|
|
1226
1798
|
});
|
|
1227
1799
|
app.addHook("onClose", async () => {
|
|
1228
|
-
await
|
|
1229
|
-
|
|
1800
|
+
await Promise.all([
|
|
1801
|
+
...roots.map((r) => r.watcher.close()),
|
|
1802
|
+
...[...configWatchers.values()].map((w) => w.close())
|
|
1803
|
+
]);
|
|
1804
|
+
await hub.close();
|
|
1230
1805
|
});
|
|
1231
|
-
return app;
|
|
1806
|
+
return { app, setRoots };
|
|
1232
1807
|
}
|
|
1233
1808
|
|
|
1234
1809
|
// src/cli.ts
|
|
@@ -1237,33 +1812,48 @@ function printUsage() {
|
|
|
1237
1812
|
mdview \u2014 local markdown viewer
|
|
1238
1813
|
|
|
1239
1814
|
Usage:
|
|
1240
|
-
mdview [path]
|
|
1815
|
+
mdview [path] Open a file or folder and return the shell (default: cwd)
|
|
1816
|
+
mdview ls List open folders and the background server
|
|
1817
|
+
mdview rm <name|path>\u2026 Stop serving a folder
|
|
1818
|
+
mdview stop Stop the background server
|
|
1241
1819
|
mdview config <\u2026> Read or edit the global config (run 'mdview config -h')
|
|
1242
1820
|
|
|
1821
|
+
One background server serves every folder you open, so 'mdview .' in a second
|
|
1822
|
+
repo adds it alongside the first instead of starting a rival server.
|
|
1823
|
+
|
|
1243
1824
|
Options:
|
|
1244
|
-
--
|
|
1825
|
+
--foreground, -f Run a server in this terminal instead (Ctrl-C to stop)
|
|
1245
1826
|
--no-open Don't auto-launch the browser
|
|
1246
|
-
--
|
|
1827
|
+
--port <n> Port to listen on; implies --foreground
|
|
1828
|
+
--palette <name> Palette for this run; implies --foreground (one of: ${PALETTES.join(", ")})
|
|
1247
1829
|
--version, -v Print version and exit
|
|
1248
1830
|
--help, -h Show this help
|
|
1249
1831
|
|
|
1832
|
+
Environment:
|
|
1833
|
+
PORT Port for the background server (default: 7331)
|
|
1834
|
+
MDVIEW_IDLE_TIMEOUT Minutes with no open tab after which the background
|
|
1835
|
+
server exits (default: off \u2014 use 'mdview stop')
|
|
1836
|
+
MDVIEW_DEBUG=1 Print full stack traces on unexpected errors
|
|
1837
|
+
|
|
1250
1838
|
Examples:
|
|
1251
|
-
mdview ./docs Browse a folder
|
|
1839
|
+
mdview ./docs Browse a folder, shell stays free
|
|
1252
1840
|
mdview README.md View a single file
|
|
1253
|
-
mdview
|
|
1841
|
+
mdview ls What's open, and where
|
|
1842
|
+
mdview --foreground ./docs Keep it in this terminal
|
|
1843
|
+
mdview --port 9000 ./docs One-off server on a specific port
|
|
1254
1844
|
mdview config ignore add deps _site Hide extra build dirs globally
|
|
1255
1845
|
mdview config path Print global config path
|
|
1256
1846
|
`.trim());
|
|
1257
1847
|
}
|
|
1258
1848
|
function readVersion() {
|
|
1259
|
-
const here =
|
|
1849
|
+
const here = path14.dirname(fileURLToPath(import.meta.url));
|
|
1260
1850
|
const candidates = [
|
|
1261
|
-
|
|
1262
|
-
|
|
1851
|
+
path14.resolve(here, "../package.json"),
|
|
1852
|
+
path14.resolve(here, "../../package.json")
|
|
1263
1853
|
];
|
|
1264
1854
|
for (const p of candidates) {
|
|
1265
1855
|
try {
|
|
1266
|
-
const raw =
|
|
1856
|
+
const raw = readFileSync2(p, "utf8");
|
|
1267
1857
|
const v = JSON.parse(raw).version;
|
|
1268
1858
|
if (typeof v === "string") return v;
|
|
1269
1859
|
} catch {
|
|
@@ -1271,8 +1861,38 @@ function readVersion() {
|
|
|
1271
1861
|
}
|
|
1272
1862
|
return "unknown";
|
|
1273
1863
|
}
|
|
1274
|
-
|
|
1275
|
-
|
|
1864
|
+
var DEFAULT_PORT = 7331;
|
|
1865
|
+
var FRIENDLY_HOST = "mdview.localhost";
|
|
1866
|
+
function isLoopbackAddress(addr) {
|
|
1867
|
+
return addr === "::1" || addr.startsWith("127.");
|
|
1868
|
+
}
|
|
1869
|
+
async function resolveDisplayHost() {
|
|
1870
|
+
try {
|
|
1871
|
+
const addrs = await dns.lookup(FRIENDLY_HOST, { all: true });
|
|
1872
|
+
if (addrs.length > 0 && addrs.every((a) => isLoopbackAddress(a.address))) {
|
|
1873
|
+
return FRIENDLY_HOST;
|
|
1874
|
+
}
|
|
1875
|
+
} catch {
|
|
1876
|
+
}
|
|
1877
|
+
return "127.0.0.1";
|
|
1878
|
+
}
|
|
1879
|
+
function envPort() {
|
|
1880
|
+
const raw = process.env.PORT;
|
|
1881
|
+
if (!raw) return void 0;
|
|
1882
|
+
const n = Number(raw);
|
|
1883
|
+
if (!Number.isInteger(n) || n < 0 || n > 65535) return void 0;
|
|
1884
|
+
return n;
|
|
1885
|
+
}
|
|
1886
|
+
function parseArgs(argv, defaultPort = DEFAULT_PORT) {
|
|
1887
|
+
const args = {
|
|
1888
|
+
target: ".",
|
|
1889
|
+
port: defaultPort,
|
|
1890
|
+
portExplicit: false,
|
|
1891
|
+
open: true,
|
|
1892
|
+
embedMode: false,
|
|
1893
|
+
foreground: false,
|
|
1894
|
+
daemon: false
|
|
1895
|
+
};
|
|
1276
1896
|
let targetSet = false;
|
|
1277
1897
|
for (let i = 0; i < argv.length; i++) {
|
|
1278
1898
|
const a = argv[i];
|
|
@@ -1286,6 +1906,14 @@ function parseArgs(argv) {
|
|
|
1286
1906
|
args.embedMode = true;
|
|
1287
1907
|
continue;
|
|
1288
1908
|
}
|
|
1909
|
+
if (a === "-f" || a === "--foreground") {
|
|
1910
|
+
args.foreground = true;
|
|
1911
|
+
continue;
|
|
1912
|
+
}
|
|
1913
|
+
if (a === "--daemon") {
|
|
1914
|
+
args.daemon = true;
|
|
1915
|
+
continue;
|
|
1916
|
+
}
|
|
1289
1917
|
if (a === "--port") {
|
|
1290
1918
|
const v = argv[++i];
|
|
1291
1919
|
if (!v) throw new Error("--port requires a number");
|
|
@@ -1309,26 +1937,24 @@ function parseArgs(argv) {
|
|
|
1309
1937
|
targetSet = true;
|
|
1310
1938
|
}
|
|
1311
1939
|
if (args.embedMode) args.open = false;
|
|
1940
|
+
if (args.portExplicit || args.palette !== void 0 || args.embedMode) args.foreground = true;
|
|
1312
1941
|
return { kind: "run", args };
|
|
1313
1942
|
}
|
|
1314
1943
|
function detectRoot(target) {
|
|
1315
|
-
const abs =
|
|
1316
|
-
if (!
|
|
1317
|
-
const st =
|
|
1944
|
+
const abs = path14.resolve(target);
|
|
1945
|
+
if (!existsSync2(abs)) throw new Error(`Path does not exist: ${abs}`);
|
|
1946
|
+
const st = statSync4(abs);
|
|
1318
1947
|
if (st.isDirectory()) {
|
|
1319
|
-
return {
|
|
1320
|
-
rootAbsPath: abs,
|
|
1321
|
-
rootInfo: { rootKind: "dir", rootRelPath: "", rootName: path11.basename(abs) }
|
|
1322
|
-
};
|
|
1948
|
+
return { absPath: abs, id: deriveRootId(abs, /* @__PURE__ */ new Set()), kind: "dir" };
|
|
1323
1949
|
}
|
|
1324
1950
|
if (st.isFile()) {
|
|
1951
|
+
const dir = path14.dirname(abs);
|
|
1325
1952
|
return {
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
}
|
|
1953
|
+
absPath: dir,
|
|
1954
|
+
id: deriveRootId(dir, /* @__PURE__ */ new Set()),
|
|
1955
|
+
name: path14.basename(abs),
|
|
1956
|
+
kind: "file",
|
|
1957
|
+
filePath: path14.basename(abs)
|
|
1332
1958
|
};
|
|
1333
1959
|
}
|
|
1334
1960
|
throw new Error(`Unsupported path type: ${abs}`);
|
|
@@ -1377,48 +2003,41 @@ Find the process with: ${portFinderHint(port)}`
|
|
|
1377
2003
|
}
|
|
1378
2004
|
throw new Error("Could not bind a port (tried 10 in a row).");
|
|
1379
2005
|
}
|
|
1380
|
-
|
|
1381
|
-
const
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
}
|
|
1386
|
-
let parsed;
|
|
1387
|
-
try {
|
|
1388
|
-
parsed = parseArgs(argv);
|
|
1389
|
-
} catch (err) {
|
|
1390
|
-
console.error(err.message);
|
|
1391
|
-
printUsage();
|
|
1392
|
-
process.exit(2);
|
|
1393
|
-
}
|
|
1394
|
-
if (parsed.kind === "help") {
|
|
1395
|
-
printUsage();
|
|
1396
|
-
process.exit(0);
|
|
1397
|
-
}
|
|
1398
|
-
if (parsed.kind === "version") {
|
|
1399
|
-
console.log(readVersion());
|
|
1400
|
-
process.exit(0);
|
|
1401
|
-
}
|
|
1402
|
-
const args = parsed.args;
|
|
1403
|
-
const { rootAbsPath, rootInfo } = detectRoot(args.target);
|
|
1404
|
-
const here = path11.dirname(fileURLToPath(import.meta.url));
|
|
1405
|
-
const clientDirCandidates = [
|
|
1406
|
-
path11.resolve(here, "../dist/client"),
|
|
1407
|
-
path11.resolve(here, "../../dist/client")
|
|
2006
|
+
function requireClientDir() {
|
|
2007
|
+
const here = path14.dirname(fileURLToPath(import.meta.url));
|
|
2008
|
+
const candidates = [
|
|
2009
|
+
path14.resolve(here, "../dist/client"),
|
|
2010
|
+
path14.resolve(here, "../../dist/client")
|
|
1408
2011
|
];
|
|
1409
|
-
const
|
|
1410
|
-
if (!
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
const
|
|
2012
|
+
const dir = candidates.find(existsSync2);
|
|
2013
|
+
if (!dir) throw new Error("Client bundle not found. Run `npm run build:client` first.");
|
|
2014
|
+
return dir;
|
|
2015
|
+
}
|
|
2016
|
+
function idleTimeoutMs() {
|
|
2017
|
+
const raw = process.env.MDVIEW_IDLE_TIMEOUT;
|
|
2018
|
+
if (raw === void 0) return 0;
|
|
2019
|
+
const n = Number(raw);
|
|
2020
|
+
if (!Number.isFinite(n) || n < 0) return 0;
|
|
2021
|
+
return Math.round(n * 6e4);
|
|
2022
|
+
}
|
|
2023
|
+
async function runForeground(args) {
|
|
2024
|
+
const root = detectRoot(args.target);
|
|
2025
|
+
const clientDir = requireClientDir();
|
|
2026
|
+
const { app } = await createServer({
|
|
2027
|
+
roots: [root],
|
|
2028
|
+
clientDir,
|
|
2029
|
+
...args.palette === void 0 ? {} : { paletteOverride: args.palette },
|
|
2030
|
+
embedMode: args.embedMode
|
|
2031
|
+
});
|
|
1415
2032
|
const boundPort = await listen(app, args.port, args.portExplicit);
|
|
1416
|
-
const
|
|
2033
|
+
const host = args.embedMode ? "127.0.0.1" : await resolveDisplayHost();
|
|
2034
|
+
const url = root.kind === "file" ? `http://${host}:${boundPort}/?file=${encodeURIComponent(`${root.id}/${root.filePath}`)}` : `http://${host}:${boundPort}/`;
|
|
1417
2035
|
if (args.embedMode) {
|
|
1418
|
-
process.stdout.write(JSON.stringify({ event: "ready", url, port: boundPort })
|
|
2036
|
+
process.stdout.write(`${JSON.stringify({ event: "ready", url, port: boundPort })}
|
|
2037
|
+
`);
|
|
1419
2038
|
} else {
|
|
1420
2039
|
console.log(`mdview \u2192 ${url}`);
|
|
1421
|
-
console.log(`watching: ${
|
|
2040
|
+
console.log(`watching: ${root.absPath}`);
|
|
1422
2041
|
}
|
|
1423
2042
|
if (args.open) await openBrowser(url);
|
|
1424
2043
|
let shuttingDown = false;
|
|
@@ -1448,6 +2067,104 @@ async function main() {
|
|
|
1448
2067
|
process.stdin.resume();
|
|
1449
2068
|
}
|
|
1450
2069
|
}
|
|
2070
|
+
async function runDaemon(args) {
|
|
2071
|
+
const clientDir = requireClientDir();
|
|
2072
|
+
const startedAt = Date.now();
|
|
2073
|
+
let idleTimer;
|
|
2074
|
+
const server = await createServer({
|
|
2075
|
+
roots: toRootSpecs(readWorkspace()),
|
|
2076
|
+
clientDir,
|
|
2077
|
+
identity: { pid: process.pid, startedAt },
|
|
2078
|
+
onSseClientsChanged: (count) => {
|
|
2079
|
+
if (count > 0) {
|
|
2080
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
2081
|
+
idleTimer = void 0;
|
|
2082
|
+
} else {
|
|
2083
|
+
armIdle();
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
});
|
|
2087
|
+
const boundPort = await listen(server.app, args.port, args.portExplicit);
|
|
2088
|
+
writeDaemonState({ pid: process.pid, port: boundPort, startedAt });
|
|
2089
|
+
console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] mdview daemon pid ${process.pid} on port ${boundPort}`);
|
|
2090
|
+
const wsWatcher = chokidar3.watch(workspaceFilePath(), {
|
|
2091
|
+
ignoreInitial: true,
|
|
2092
|
+
persistent: true,
|
|
2093
|
+
awaitWriteFinish: { stabilityThreshold: 60, pollInterval: 30 }
|
|
2094
|
+
});
|
|
2095
|
+
const reload = () => {
|
|
2096
|
+
void server.setRoots(toRootSpecs(readWorkspace())).catch((err) => {
|
|
2097
|
+
console.error(`[mdview] failed to apply workspace change: ${String(err)}`);
|
|
2098
|
+
});
|
|
2099
|
+
};
|
|
2100
|
+
wsWatcher.on("add", reload);
|
|
2101
|
+
wsWatcher.on("change", reload);
|
|
2102
|
+
wsWatcher.on("unlink", reload);
|
|
2103
|
+
let shuttingDown = false;
|
|
2104
|
+
const shutdown = async (why) => {
|
|
2105
|
+
if (shuttingDown) process.exit(130);
|
|
2106
|
+
shuttingDown = true;
|
|
2107
|
+
console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] mdview daemon stopping (${why})`);
|
|
2108
|
+
clearDaemonState();
|
|
2109
|
+
const timeout = new Promise((resolve) => setTimeout(resolve, 1500));
|
|
2110
|
+
await Promise.race([
|
|
2111
|
+
Promise.all([server.app.close().catch(() => void 0), wsWatcher.close()]).then(
|
|
2112
|
+
() => void 0
|
|
2113
|
+
),
|
|
2114
|
+
timeout
|
|
2115
|
+
]);
|
|
2116
|
+
process.exit(0);
|
|
2117
|
+
};
|
|
2118
|
+
function armIdle() {
|
|
2119
|
+
const ms = idleTimeoutMs();
|
|
2120
|
+
if (ms <= 0) return;
|
|
2121
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
2122
|
+
idleTimer = setTimeout(() => void shutdown("idle"), ms);
|
|
2123
|
+
}
|
|
2124
|
+
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
2125
|
+
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
2126
|
+
if (process.platform !== "win32") process.on("SIGHUP", () => void shutdown("SIGHUP"));
|
|
2127
|
+
armIdle();
|
|
2128
|
+
}
|
|
2129
|
+
async function runAttach(args) {
|
|
2130
|
+
const added = addRoot(args.target);
|
|
2131
|
+
const state = await ensureDaemon(fileURLToPath(import.meta.url));
|
|
2132
|
+
await waitForRoot(state.port, added.root.id);
|
|
2133
|
+
const host = await resolveDisplayHost();
|
|
2134
|
+
const base = `http://${host}:${state.port}/`;
|
|
2135
|
+
const url = added.filePath ? `${base}?file=${encodeURIComponent(added.filePath)}` : `${base}?root=${encodeURIComponent(added.focusPath)}`;
|
|
2136
|
+
console.log(`mdview \u2192 ${url}`);
|
|
2137
|
+
console.log(`${added.changed ? "added" : "open"}: ${added.root.path} (${added.root.id})`);
|
|
2138
|
+
if (args.open) await openBrowser(url);
|
|
2139
|
+
}
|
|
2140
|
+
async function main() {
|
|
2141
|
+
const argv = process.argv.slice(2);
|
|
2142
|
+
const sub = argv[0];
|
|
2143
|
+
if (sub === "config") process.exit(await runConfigSubcommand(argv.slice(1)));
|
|
2144
|
+
if (sub === "ls" || sub === "status") process.exit(await runListCommand());
|
|
2145
|
+
if (sub === "rm") process.exit(await runRemoveCommand(argv.slice(1)));
|
|
2146
|
+
if (sub === "stop") process.exit(await runStopCommand());
|
|
2147
|
+
let parsed;
|
|
2148
|
+
try {
|
|
2149
|
+
parsed = parseArgs(argv, envPort() ?? DEFAULT_PORT);
|
|
2150
|
+
} catch (err) {
|
|
2151
|
+
console.error(err.message);
|
|
2152
|
+
printUsage();
|
|
2153
|
+
process.exit(2);
|
|
2154
|
+
}
|
|
2155
|
+
if (parsed.kind === "help") {
|
|
2156
|
+
printUsage();
|
|
2157
|
+
process.exit(0);
|
|
2158
|
+
}
|
|
2159
|
+
if (parsed.kind === "version") {
|
|
2160
|
+
console.log(readVersion());
|
|
2161
|
+
process.exit(0);
|
|
2162
|
+
}
|
|
2163
|
+
const args = parsed.args;
|
|
2164
|
+
if (args.daemon) return runDaemon(args);
|
|
2165
|
+
if (args.foreground) return runForeground(args);
|
|
2166
|
+
return runAttach(args);
|
|
2167
|
+
}
|
|
1451
2168
|
function formatError(err) {
|
|
1452
2169
|
if (!(err instanceof Error)) return null;
|
|
1453
2170
|
const errno = err;
|
|
@@ -1460,7 +2177,16 @@ function formatError(err) {
|
|
|
1460
2177
|
const msg = err.message;
|
|
1461
2178
|
if (msg.startsWith("Port ")) return { message: msg, code: 1 };
|
|
1462
2179
|
if (msg.startsWith("Client bundle not found")) return { message: `mdview: ${msg}`, code: 1 };
|
|
1463
|
-
|
|
2180
|
+
const EXPECTED = [
|
|
2181
|
+
"Path does not exist",
|
|
2182
|
+
"Unsupported path type",
|
|
2183
|
+
"Could not bind a port",
|
|
2184
|
+
"Unexpected argument",
|
|
2185
|
+
"Workspace is full",
|
|
2186
|
+
"Timed out waiting for the mdview background server",
|
|
2187
|
+
"The mdview background server did not start"
|
|
2188
|
+
];
|
|
2189
|
+
if (EXPECTED.some((prefix) => msg.startsWith(prefix))) {
|
|
1464
2190
|
return { message: `mdview: ${msg}`, code: 1 };
|
|
1465
2191
|
}
|
|
1466
2192
|
return null;
|
|
@@ -1492,6 +2218,8 @@ if (isEntryPoint) {
|
|
|
1492
2218
|
});
|
|
1493
2219
|
}
|
|
1494
2220
|
export {
|
|
2221
|
+
DEFAULT_PORT,
|
|
2222
|
+
idleTimeoutMs,
|
|
1495
2223
|
parseArgs
|
|
1496
2224
|
};
|
|
1497
2225
|
//# sourceMappingURL=mdview.mjs.map
|