@hardikg/mdview 0.10.0 → 0.11.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 +63 -0
- package/README.md +52 -10
- package/bin/chunk-RBYD2HWV.mjs +196 -0
- package/bin/chunk-WWM6JNAS.mjs +209 -0
- package/bin/chunk-XFRQFZZ4.mjs +20 -0
- package/bin/mdview.mjs +521 -1323
- package/bin/regex-worker.mjs +52 -0
- package/bin/server-KALIU7HM.mjs +1854 -0
- package/bin/shiki-5SFVA32N.mjs +13 -0
- package/dist/client/assets/23I4SOAX-BN9xvzna.js +2 -0
- package/dist/client/assets/26H2HVXD-Cu1OTMxT.js +2 -0
- package/dist/client/assets/2IZ5MNOH-DMjMO3_9.js +1 -0
- package/dist/client/assets/2P3CZEXH-D-4BP7dg.js +2 -0
- package/dist/client/assets/3E5HRNYT-DUYblpdl.js +5 -0
- package/dist/client/assets/3JIRLDUO-C1CwW1Ha.js +1 -0
- package/dist/client/assets/5MJ3UKSG-DtT76qKq.js +2 -0
- package/dist/client/assets/5THPTBWS-Ce_hS451.js +1 -0
- package/dist/client/assets/7WLXJQOR-D6QVYUu9.js +1 -0
- package/dist/client/assets/A255FCTW-woaH7Mj_.js +1 -0
- package/dist/client/assets/AM43MQ64-CsWC_OQH.js +2 -0
- package/dist/client/assets/AOZTNTMC-BIehUQHD.js +5 -0
- package/dist/client/assets/AQNLM6KG-DIUuJgPm.js +4 -0
- package/dist/client/assets/BNU2RHQB-DeXpwU5k.js +2 -0
- package/dist/client/assets/C5HTQT3I-7U13x70S.js +4 -0
- package/dist/client/assets/CPKRWH5G-CRxaqyOs.js +12 -0
- package/dist/client/assets/CUVCMPQX-JTGMOfG2.js +1 -0
- package/dist/client/assets/D4V2MWCI-CytzCr6g.js +1 -0
- package/dist/client/assets/DHKBVIA3-CVnlqiEq.js +1 -0
- package/dist/client/assets/DKTLEL5E-DciMwFJ1.js +1 -0
- package/dist/client/assets/DUZNPW7S-D3pifVNb.js +2 -0
- package/dist/client/assets/E3FWOBN4-D5m5o2g0.js +4 -0
- package/dist/client/assets/E65FQMJL-Bg5F0Zo8.js +1 -0
- package/dist/client/assets/FTXIJQGB-CggtIYAM.js +1 -0
- package/dist/client/assets/G3MDBVQH-BZmnMkFw.js +1 -0
- package/dist/client/assets/G3OATVW2-VpVvT5i2.js +8 -0
- package/dist/client/assets/IV52FOAS-DgPxxRI4.js +5 -0
- package/dist/client/assets/JTJEEBDN-Bl0iUT_r.js +2 -0
- package/dist/client/assets/K7FUICZ3-DgoihkiR.js +2 -0
- package/dist/client/assets/LHH2HHRB-BXHTnBD0.js +2 -0
- package/dist/client/assets/MSPQPZZY-DQg7F6a8.js +4 -0
- package/dist/client/assets/OB6ZFVKF-UHmC8m-y.js +2 -0
- package/dist/client/assets/PLTZDMI2-C1ZAoc-e.js +1 -0
- package/dist/client/assets/R7EPXMMD-ChI1SqjJ.js +5 -0
- package/dist/client/assets/RVARI57O-CgA1WoRv.js +1 -0
- package/dist/client/assets/SPBWZ4VS-EqfCsJ5V.js +1 -0
- package/dist/client/assets/SR7OOV3Q-CFnFs0kL.js +4 -0
- package/dist/client/assets/STAXBNH4-D4c13Xlj.js +2 -0
- package/dist/client/assets/SXFARSFO-CkFSkgdi.js +1 -0
- package/dist/client/assets/TC77ISAL--WP0GBo0.js +2 -0
- package/dist/client/assets/TYIQQHVC-B67pCpxM.js +13 -0
- package/dist/client/assets/TZUXETIT-BFD9W5hl.js +2 -0
- package/dist/client/assets/U7N72BGE-Do3829la.js +6 -0
- package/dist/client/assets/UFPIB6EZ-DTs_MyQv.js +3 -0
- package/dist/client/assets/UO52YEYL-DN1qbGwF.js +3 -0
- package/dist/client/assets/VIK3ONSK-DC8mR2sZ.js +2 -0
- package/dist/client/assets/VVVO4ZVP-DAtGLbxR.js +2 -0
- package/dist/client/assets/WRGXZO53-D30IuREw.js +3 -0
- package/dist/client/assets/X5SG7MGC-CdjdoL_s.js +2 -0
- package/dist/client/assets/XLHPVVFU-BFP9ID9p.js +2 -0
- package/dist/client/assets/XXYPNCD5-DqCewu_S.js +5 -0
- package/dist/client/assets/Z6WRYFJC-BEt1KCoU.js +7 -0
- package/dist/client/assets/ZI2QNEGF-VzC4uHcW.js +8 -0
- package/dist/client/assets/index-B96jWJTi.css +1 -0
- package/dist/client/assets/index-DCU09WmS.js +2 -0
- package/dist/client/assets/katex-C0xaXAD4.js +257 -0
- package/dist/client/assets/katex.min-3X1qOa03.js +1 -0
- package/dist/client/assets/katex.min-DMey5pMQ.css +1 -0
- package/dist/client/assets/lazy-DaMZI1F7.js +45 -0
- package/dist/client/index.html +2 -2
- package/package.json +32 -27
- package/dist/client/assets/2DXBBFLN-BxPqHNSN.js +0 -5
- package/dist/client/assets/46IJ22JX-u_jvQJNg.js +0 -2
- package/dist/client/assets/4TTDE5I4-BgbNAVNj.js +0 -3
- package/dist/client/assets/4UF3HTLV-DNbPxlOE.js +0 -2
- package/dist/client/assets/5THPTBWS-D57irc_4.js +0 -1
- package/dist/client/assets/7WLXJQOR-CHhKreq0.js +0 -1
- package/dist/client/assets/AAXQQPKT-CsAJqrOC.js +0 -5
- package/dist/client/assets/AKE5O7KZ-MSuBOjCG.js +0 -2
- package/dist/client/assets/BNMLP6AG-Ca3zyKQY.js +0 -3
- package/dist/client/assets/DKTLEL5E-B7ILCVTH.js +0 -1
- package/dist/client/assets/DR6PKOQU-Bw7cHvYp.js +0 -2
- package/dist/client/assets/E4RJ7UJB-CYB5C4K7.js +0 -2
- package/dist/client/assets/ELSXBIJH-DG0MW5dC.js +0 -1
- package/dist/client/assets/ERTIBQVS-BMWumWfk.js +0 -2
- package/dist/client/assets/F653G4FO-FI8M9Mc0.js +0 -12
- package/dist/client/assets/FKCDD66M-BPFkyrwI.js +0 -1
- package/dist/client/assets/FP7KDXVC-BLEBNkCO.js +0 -2
- package/dist/client/assets/FT5I3RHX-j5YoMicq.js +0 -6
- package/dist/client/assets/FTXIJQGB-nrHdCfp7.js +0 -1
- package/dist/client/assets/G3MDBVQH-EkdejvdW.js +0 -1
- package/dist/client/assets/GY35JXOZ-3ovTGxgz.js +0 -2
- package/dist/client/assets/H5EEJPP2-CNVQBEB3.js +0 -1
- package/dist/client/assets/HWDLEDTA-C_JcdBMO.js +0 -1
- package/dist/client/assets/JZWSX6AR-OwVPptYv.js +0 -1
- package/dist/client/assets/KQOEJWJY-DKTXya68.js +0 -2
- package/dist/client/assets/KS3BB3KE-QkpqVdmA.js +0 -1
- package/dist/client/assets/KXJRRZHI-BQRjeGWj.js +0 -2
- package/dist/client/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/client/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/client/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/client/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/client/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/client/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/client/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/client/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/client/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/client/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/client/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/client/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/client/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/client/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/client/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/client/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/client/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/client/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/client/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/client/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/client/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/client/assets/MFVI5XKI-DrRaRE9X.js +0 -1
- package/dist/client/assets/MWY2J62P-AbEgJnzK.js +0 -12
- package/dist/client/assets/MXGNCBL2-DG3CZR8g.js +0 -2
- package/dist/client/assets/N6DAJLIZ-BsZQKTyT.js +0 -3
- package/dist/client/assets/NU5M23IW-CvtpIovv.js +0 -2
- package/dist/client/assets/NV4XNBIS-nS8wAx3h.js +0 -8
- package/dist/client/assets/OGLU4SIR-DoTUWhkz.js +0 -2
- package/dist/client/assets/OUNS5N76-BwVLonT4.js +0 -8
- package/dist/client/assets/PU4ISKXV-BYX7eA_r.js +0 -2
- package/dist/client/assets/PUZGS23X-n6IKOr7S.js +0 -1
- package/dist/client/assets/Q3IYVLG5-hWt1rVyh.js +0 -4
- package/dist/client/assets/QYHQDLKZ-CjGeJM7R.js +0 -4
- package/dist/client/assets/RVARI57O-CXmrUIiS.js +0 -1
- package/dist/client/assets/RZSVICHU-BiwjruZZ.js +0 -4
- package/dist/client/assets/UEXXS2NB-Dkz_tz-N.js +0 -2
- package/dist/client/assets/UHTQJXKL-lvl-7QeT.js +0 -5
- package/dist/client/assets/URA2L2QI-B8pYKxOk.js +0 -1
- package/dist/client/assets/UXCHDUKG-BGy3ZRLM.js +0 -2
- package/dist/client/assets/VKNV3BUX-DZnoX4IV.js +0 -5
- package/dist/client/assets/VMV7XPHP-6QkisQwm.js +0 -4
- package/dist/client/assets/WEVWDWVA-nohBBhTA.js +0 -2
- package/dist/client/assets/XRRJ35LF-BE7gSN1k.js +0 -2
- package/dist/client/assets/YFKBLVVN-CVTPpei6.js +0 -6
- package/dist/client/assets/YHRE5QGZ-Bs7dUKjj.js +0 -4
- package/dist/client/assets/ZF3OVT4B-6LaC6iLs.js +0 -3
- package/dist/client/assets/index-392ivruk.js +0 -2
- package/dist/client/assets/index-CGQuEJ_w.css +0 -1
- package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/dist/client/assets/jetbrains-mono-cyrillic-600-normal-8K4wrrwR.woff +0 -0
- package/dist/client/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/dist/client/assets/jetbrains-mono-greek-600-normal-mc2nkWzM.woff +0 -0
- package/dist/client/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/dist/client/assets/jetbrains-mono-latin-600-normal-BfsvjouI.woff +0 -0
- package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/dist/client/assets/jetbrains-mono-latin-ext-600-normal-DObL3zCW.woff +0 -0
- package/dist/client/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/dist/client/assets/jetbrains-mono-vietnamese-600-normal-OWROknRo.woff +0 -0
- package/dist/client/assets/katex-C5jXJg4s.js +0 -257
- package/dist/client/assets/katex.min-BYZTj3zW.css +0 -1
- package/dist/client/assets/katex.min-DXaO1tdm.js +0 -1
- package/dist/client/assets/lazy-CKqR1774.js +0 -43
package/bin/mdview.mjs
CHANGED
|
@@ -1,159 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_IGNORED_DIRS,
|
|
4
|
+
deriveRootId,
|
|
5
|
+
globalConfigPath,
|
|
6
|
+
isValidRootId,
|
|
7
|
+
loadGlobalConfig
|
|
8
|
+
} from "./chunk-WWM6JNAS.mjs";
|
|
9
|
+
import {
|
|
10
|
+
PALETTES
|
|
11
|
+
} from "./chunk-XFRQFZZ4.mjs";
|
|
2
12
|
|
|
3
13
|
// src/cli.ts
|
|
4
14
|
import dns from "dns/promises";
|
|
5
|
-
import { existsSync as existsSync2,
|
|
6
|
-
import
|
|
7
|
-
import { fileURLToPath } from "url";
|
|
8
|
-
import chokidar3 from "chokidar";
|
|
9
|
-
import openBrowser from "open";
|
|
15
|
+
import { existsSync as existsSync2, realpathSync as realpathSync2, statSync as statSync5 } from "fs";
|
|
16
|
+
import path6 from "path";
|
|
17
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
10
18
|
|
|
11
|
-
// src/server/config.ts
|
|
12
|
-
import { readFile } from "fs/promises";
|
|
13
|
-
import os from "os";
|
|
19
|
+
// src/server/config-write.ts
|
|
20
|
+
import { chmod, mkdir, readFile, writeFile } from "fs/promises";
|
|
14
21
|
import path from "path";
|
|
15
|
-
|
|
16
|
-
// src/shared/types.ts
|
|
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
|
-
];
|
|
28
|
-
|
|
29
|
-
// src/server/config.ts
|
|
30
|
-
var CONFIG_FILENAME = ".mdview.json";
|
|
31
|
-
var GLOBAL_CONFIG_FILENAME = "config.json";
|
|
32
|
-
var FONT_FAMILIES = ["serif", "sans", "mono"];
|
|
33
22
|
var IGNORE_ENTRY_PATTERN = /^[A-Za-z0-9_.\-+]{1,64}$/;
|
|
34
|
-
function isObject(v) {
|
|
35
|
-
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
36
|
-
}
|
|
37
|
-
function globalConfigPath() {
|
|
38
|
-
const xdg = process.env.XDG_CONFIG_HOME;
|
|
39
|
-
const base = xdg && xdg.length > 0 ? xdg : path.join(os.homedir(), ".config");
|
|
40
|
-
return path.join(base, "mdview", GLOBAL_CONFIG_FILENAME);
|
|
41
|
-
}
|
|
42
|
-
function validateConfig(raw, source = CONFIG_FILENAME) {
|
|
43
|
-
if (!isObject(raw)) return null;
|
|
44
|
-
const out = {};
|
|
45
|
-
if ("palette" in raw) {
|
|
46
|
-
const p = raw.palette;
|
|
47
|
-
if (typeof p === "string" && PALETTES.includes(p)) {
|
|
48
|
-
out.palette = p;
|
|
49
|
-
} else {
|
|
50
|
-
console.warn(`[mdview] ${source}: ignoring invalid palette: ${String(p)}`);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
if ("fontFamily" in raw) {
|
|
54
|
-
const f = raw.fontFamily;
|
|
55
|
-
if (typeof f === "string" && FONT_FAMILIES.includes(f)) {
|
|
56
|
-
out.fontFamily = f;
|
|
57
|
-
} else {
|
|
58
|
-
console.warn(`[mdview] ${source}: ignoring invalid fontFamily: ${String(f)}`);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
if ("lineWidth" in raw) {
|
|
62
|
-
const w = raw.lineWidth;
|
|
63
|
-
if (typeof w === "string" && w.length > 0 && w.length <= 32 && /^[\w%.-]+$/.test(w)) {
|
|
64
|
-
out.lineWidth = w;
|
|
65
|
-
} else {
|
|
66
|
-
console.warn(`[mdview] ${source}: ignoring invalid lineWidth: ${String(w)}`);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
if ("defaultCollapsed" in raw && isObject(raw.defaultCollapsed)) {
|
|
70
|
-
const dc = raw.defaultCollapsed;
|
|
71
|
-
const collapsed = {};
|
|
72
|
-
if (typeof dc.tree === "boolean") collapsed.tree = dc.tree;
|
|
73
|
-
if (typeof dc.outline === "boolean") collapsed.outline = dc.outline;
|
|
74
|
-
if (collapsed.tree !== void 0 || collapsed.outline !== void 0) {
|
|
75
|
-
out.defaultCollapsed = collapsed;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
if ("ignore" in raw) {
|
|
79
|
-
const ig = raw.ignore;
|
|
80
|
-
if (Array.isArray(ig)) {
|
|
81
|
-
const cleaned = [];
|
|
82
|
-
for (const entry of ig) {
|
|
83
|
-
if (typeof entry === "string" && entry !== "." && entry !== ".." && IGNORE_ENTRY_PATTERN.test(entry)) {
|
|
84
|
-
cleaned.push(entry);
|
|
85
|
-
} else {
|
|
86
|
-
console.warn(`[mdview] ${source}: ignoring invalid ignore entry: ${String(entry)}`);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
if (cleaned.length > 0) out.ignore = cleaned;
|
|
90
|
-
} else {
|
|
91
|
-
console.warn(`[mdview] ${source}: ignoring invalid ignore (expected array of strings)`);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
return out;
|
|
95
|
-
}
|
|
96
|
-
async function loadConfigFromPath(filePath, source) {
|
|
97
|
-
let raw;
|
|
98
|
-
try {
|
|
99
|
-
raw = await readFile(filePath, "utf8");
|
|
100
|
-
} catch (err) {
|
|
101
|
-
if (err.code === "ENOENT") return null;
|
|
102
|
-
console.warn(`[mdview] failed to read ${source}: ${err.message}`);
|
|
103
|
-
return null;
|
|
104
|
-
}
|
|
105
|
-
let parsed;
|
|
106
|
-
try {
|
|
107
|
-
parsed = JSON.parse(raw);
|
|
108
|
-
} catch (err) {
|
|
109
|
-
console.warn(`[mdview] ${source} is not valid JSON: ${err.message}`);
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
return validateConfig(parsed, source);
|
|
113
|
-
}
|
|
114
|
-
async function loadProjectConfig(rootAbsPath) {
|
|
115
|
-
return loadConfigFromPath(path.join(rootAbsPath, CONFIG_FILENAME), CONFIG_FILENAME);
|
|
116
|
-
}
|
|
117
|
-
async function loadGlobalConfig() {
|
|
118
|
-
return loadConfigFromPath(globalConfigPath(), `~/.config/mdview/${GLOBAL_CONFIG_FILENAME}`);
|
|
119
|
-
}
|
|
120
|
-
function mergeConfigs(global, project) {
|
|
121
|
-
if (!global && !project) return null;
|
|
122
|
-
const out = {};
|
|
123
|
-
if (global?.palette !== void 0) out.palette = global.palette;
|
|
124
|
-
if (project?.palette !== void 0) out.palette = project.palette;
|
|
125
|
-
if (global?.fontFamily !== void 0) out.fontFamily = global.fontFamily;
|
|
126
|
-
if (project?.fontFamily !== void 0) out.fontFamily = project.fontFamily;
|
|
127
|
-
if (global?.lineWidth !== void 0) out.lineWidth = global.lineWidth;
|
|
128
|
-
if (project?.lineWidth !== void 0) out.lineWidth = project.lineWidth;
|
|
129
|
-
if (global?.defaultCollapsed || project?.defaultCollapsed) {
|
|
130
|
-
out.defaultCollapsed = { ...global?.defaultCollapsed, ...project?.defaultCollapsed };
|
|
131
|
-
}
|
|
132
|
-
const ig = [...global?.ignore ?? [], ...project?.ignore ?? []];
|
|
133
|
-
if (ig.length > 0) {
|
|
134
|
-
out.ignore = Array.from(new Set(ig));
|
|
135
|
-
}
|
|
136
|
-
return out;
|
|
137
|
-
}
|
|
138
|
-
async function loadEffectiveConfig(rootAbsPath) {
|
|
139
|
-
const [global, project] = await Promise.all([
|
|
140
|
-
loadGlobalConfig(),
|
|
141
|
-
loadProjectConfig(rootAbsPath)
|
|
142
|
-
]);
|
|
143
|
-
return mergeConfigs(global, project);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// src/server/config-write.ts
|
|
147
|
-
import { mkdir, readFile as readFile2, writeFile } from "fs/promises";
|
|
148
|
-
import path2 from "path";
|
|
149
|
-
var IGNORE_ENTRY_PATTERN2 = /^[A-Za-z0-9_.\-+]{1,64}$/;
|
|
150
23
|
function isValidIgnoreEntry(s) {
|
|
151
|
-
return s !== "." && s !== ".." &&
|
|
24
|
+
return s !== "." && s !== ".." && IGNORE_ENTRY_PATTERN.test(s);
|
|
152
25
|
}
|
|
153
26
|
async function readGlobalRaw(filePath) {
|
|
154
27
|
let raw;
|
|
155
28
|
try {
|
|
156
|
-
raw = await
|
|
29
|
+
raw = await readFile(filePath, "utf8");
|
|
157
30
|
} catch (err) {
|
|
158
31
|
if (err.code === "ENOENT") return {};
|
|
159
32
|
throw err;
|
|
@@ -170,9 +43,12 @@ async function readGlobalRaw(filePath) {
|
|
|
170
43
|
return parsed;
|
|
171
44
|
}
|
|
172
45
|
async function writeGlobalRaw(filePath, data) {
|
|
173
|
-
|
|
46
|
+
const dir = path.dirname(filePath);
|
|
47
|
+
await mkdir(dir, { recursive: true, mode: 448 });
|
|
48
|
+
await chmod(dir, 448);
|
|
174
49
|
const json = JSON.stringify(data, null, 2) + "\n";
|
|
175
|
-
await writeFile(filePath, json, "utf8");
|
|
50
|
+
await writeFile(filePath, json, { encoding: "utf8", mode: 384 });
|
|
51
|
+
await chmod(filePath, 384);
|
|
176
52
|
}
|
|
177
53
|
function readIgnoreArray(cfg) {
|
|
178
54
|
const v = cfg.ignore;
|
|
@@ -213,22 +89,18 @@ async function listUserIgnoreEntries(filePath = globalConfigPath()) {
|
|
|
213
89
|
const cfg = await readGlobalRaw(filePath);
|
|
214
90
|
return readIgnoreArray(cfg);
|
|
215
91
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
"Pods",
|
|
229
|
-
"DerivedData",
|
|
230
|
-
"_build"
|
|
231
|
-
];
|
|
92
|
+
async function readUpdateCheck(filePath = globalConfigPath()) {
|
|
93
|
+
const cfg = await readGlobalRaw(filePath);
|
|
94
|
+
return cfg.updateCheck === true;
|
|
95
|
+
}
|
|
96
|
+
async function setUpdateCheck(enabled, filePath = globalConfigPath()) {
|
|
97
|
+
const cfg = await readGlobalRaw(filePath);
|
|
98
|
+
const before = cfg.updateCheck === true;
|
|
99
|
+
if (enabled) cfg.updateCheck = true;
|
|
100
|
+
else delete cfg.updateCheck;
|
|
101
|
+
await writeGlobalRaw(filePath, cfg);
|
|
102
|
+
return { before, after: enabled, path: filePath };
|
|
103
|
+
}
|
|
232
104
|
|
|
233
105
|
// src/cli-config.ts
|
|
234
106
|
function printConfigUsage() {
|
|
@@ -241,12 +113,18 @@ Subcommands:
|
|
|
241
113
|
ignore list Show the built-in skips plus user-added ones
|
|
242
114
|
ignore add <name\u2026> Append basename(s) to the ignore list
|
|
243
115
|
ignore rm <name\u2026> Remove basename(s) from the ignore list
|
|
116
|
+
update-check [on|off] Show, or turn on/off, the daily check for a newer release
|
|
244
117
|
|
|
245
118
|
Examples:
|
|
246
119
|
mdview config path
|
|
247
120
|
mdview config ignore add deps _site
|
|
248
121
|
mdview config ignore list
|
|
249
122
|
|
|
123
|
+
The update check is off by default. When on, the background server asks
|
|
124
|
+
registry.npmjs.org once a day for the latest version and sends nothing that
|
|
125
|
+
identifies you. It only ever prints a one-line notice; 'mdview update' shows
|
|
126
|
+
the install command.
|
|
127
|
+
|
|
250
128
|
After editing the ignore list, restart any running mdview instance \u2014 the file
|
|
251
129
|
watcher caches its skip set at startup.
|
|
252
130
|
`.trim()
|
|
@@ -267,6 +145,8 @@ async function runConfigSubcommand(argv) {
|
|
|
267
145
|
return 0;
|
|
268
146
|
case "ignore":
|
|
269
147
|
return runIgnoreSubcommand(rest);
|
|
148
|
+
case "update-check":
|
|
149
|
+
return runUpdateCheckSubcommand(rest);
|
|
270
150
|
default:
|
|
271
151
|
console.error(`Unknown subcommand: config ${sub}`);
|
|
272
152
|
printConfigUsage();
|
|
@@ -340,31 +220,293 @@ async function runIgnoreSubcommand(argv) {
|
|
|
340
220
|
return 2;
|
|
341
221
|
}
|
|
342
222
|
}
|
|
223
|
+
async function runUpdateCheckSubcommand(argv) {
|
|
224
|
+
const [action, ...extra] = argv;
|
|
225
|
+
if (extra.length > 0 || action !== void 0 && action !== "on" && action !== "off") {
|
|
226
|
+
console.error("Usage: mdview config update-check [on|off]");
|
|
227
|
+
return 2;
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
if (action === void 0) {
|
|
231
|
+
console.log(`update-check: ${await readUpdateCheck() ? "on" : "off"}`);
|
|
232
|
+
return 0;
|
|
233
|
+
}
|
|
234
|
+
const result = await setUpdateCheck(action === "on");
|
|
235
|
+
if (result.before === result.after) {
|
|
236
|
+
console.log(`update-check is already ${action}.`);
|
|
237
|
+
} else if (result.after) {
|
|
238
|
+
console.log(`update-check on (${result.path}).`);
|
|
239
|
+
console.log(
|
|
240
|
+
"The background server checks registry.npmjs.org about once a day; a running one notices this within an hour."
|
|
241
|
+
);
|
|
242
|
+
} else {
|
|
243
|
+
console.log(`update-check off (${result.path}).`);
|
|
244
|
+
}
|
|
245
|
+
return 0;
|
|
246
|
+
} catch (err) {
|
|
247
|
+
console.error(err.message);
|
|
248
|
+
return 2;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// src/cli-update.ts
|
|
253
|
+
import { statSync } from "fs";
|
|
254
|
+
import path3 from "path";
|
|
255
|
+
|
|
256
|
+
// src/update/semver.ts
|
|
257
|
+
var PRERELEASE_ID = "(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*)";
|
|
258
|
+
var SEMVER_PATTERN = new RegExp(
|
|
259
|
+
`^(0|[1-9]\\d{0,8})\\.(0|[1-9]\\d{0,8})\\.(0|[1-9]\\d{0,8})(?:-(${PRERELEASE_ID}(?:\\.${PRERELEASE_ID})*))?$`
|
|
260
|
+
);
|
|
261
|
+
var MAX_VERSION_LENGTH = 48;
|
|
262
|
+
function parseVersion(v) {
|
|
263
|
+
if (typeof v !== "string" || v.length > MAX_VERSION_LENGTH) return null;
|
|
264
|
+
const m = SEMVER_PATTERN.exec(v);
|
|
265
|
+
if (!m) return null;
|
|
266
|
+
return {
|
|
267
|
+
core: [Number(m[1]), Number(m[2]), Number(m[3])],
|
|
268
|
+
pre: m[4] === void 0 ? [] : m[4].split(".")
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function isValidVersion(v) {
|
|
272
|
+
return parseVersion(v) !== null;
|
|
273
|
+
}
|
|
274
|
+
var NUMERIC = /^\d+$/;
|
|
275
|
+
function comparePreId(a, b) {
|
|
276
|
+
const an = NUMERIC.test(a);
|
|
277
|
+
const bn = NUMERIC.test(b);
|
|
278
|
+
if (an && bn) return a.length - b.length || (a < b ? -1 : a > b ? 1 : 0);
|
|
279
|
+
if (an) return -1;
|
|
280
|
+
if (bn) return 1;
|
|
281
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
282
|
+
}
|
|
283
|
+
function compareVersions(a, b) {
|
|
284
|
+
for (let i = 0; i < 3; i++) {
|
|
285
|
+
const d = a.core[i] - b.core[i];
|
|
286
|
+
if (d !== 0) return d;
|
|
287
|
+
}
|
|
288
|
+
if (a.pre.length === 0 && b.pre.length === 0) return 0;
|
|
289
|
+
if (a.pre.length === 0) return 1;
|
|
290
|
+
if (b.pre.length === 0) return -1;
|
|
291
|
+
const n = Math.min(a.pre.length, b.pre.length);
|
|
292
|
+
for (let i = 0; i < n; i++) {
|
|
293
|
+
const d = comparePreId(a.pre[i], b.pre[i]);
|
|
294
|
+
if (d !== 0) return d;
|
|
295
|
+
}
|
|
296
|
+
return a.pre.length - b.pre.length;
|
|
297
|
+
}
|
|
298
|
+
function isNewerVersion(candidate, current) {
|
|
299
|
+
const c = parseVersion(candidate);
|
|
300
|
+
const k = parseVersion(current);
|
|
301
|
+
return c !== null && k !== null && compareVersions(c, k) > 0;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// src/update/latest.ts
|
|
305
|
+
var DIST_TAGS_URL = "https://registry.npmjs.org/-/package/@hardikg%2fmdview/dist-tags";
|
|
306
|
+
var TIMEOUT_MS = 5e3;
|
|
307
|
+
var MAX_BODY_BYTES = 64 * 1024;
|
|
308
|
+
var UpdateCheckError = class extends Error {
|
|
309
|
+
};
|
|
310
|
+
async function readCapped(res, maxBytes, abort) {
|
|
311
|
+
const declared = Number(res.headers.get("content-length"));
|
|
312
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
313
|
+
abort.abort();
|
|
314
|
+
throw new UpdateCheckError("the registry answer was too large");
|
|
315
|
+
}
|
|
316
|
+
if (!res.body) throw new UpdateCheckError("the registry answer was empty");
|
|
317
|
+
const reader = res.body.getReader();
|
|
318
|
+
const chunks = [];
|
|
319
|
+
let total = 0;
|
|
320
|
+
for (; ; ) {
|
|
321
|
+
const { done, value } = await reader.read();
|
|
322
|
+
if (done) break;
|
|
323
|
+
total += value.byteLength;
|
|
324
|
+
if (total > maxBytes) {
|
|
325
|
+
abort.abort();
|
|
326
|
+
await reader.cancel().catch(() => void 0);
|
|
327
|
+
throw new UpdateCheckError("the registry answer was too large");
|
|
328
|
+
}
|
|
329
|
+
chunks.push(value);
|
|
330
|
+
}
|
|
331
|
+
return new TextDecoder().decode(Buffer.concat(chunks));
|
|
332
|
+
}
|
|
333
|
+
async function fetchLatestVersion(options = {}) {
|
|
334
|
+
const fetchFn = options.fetchFn ?? fetch;
|
|
335
|
+
const abort = new AbortController();
|
|
336
|
+
const timeout = AbortSignal.timeout(options.timeoutMs ?? TIMEOUT_MS);
|
|
337
|
+
const signal = AbortSignal.any([abort.signal, timeout]);
|
|
338
|
+
let body;
|
|
339
|
+
try {
|
|
340
|
+
const res = await fetchFn(DIST_TAGS_URL, { redirect: "error", signal });
|
|
341
|
+
if (!res.ok) throw new UpdateCheckError(`the registry answered HTTP ${res.status}`);
|
|
342
|
+
body = await readCapped(res, options.maxBytes ?? MAX_BODY_BYTES, abort);
|
|
343
|
+
} catch (err) {
|
|
344
|
+
if (err instanceof UpdateCheckError) throw err;
|
|
345
|
+
if (timeout.aborted) throw new UpdateCheckError("the registry did not answer in time", { cause: err });
|
|
346
|
+
throw new UpdateCheckError("could not reach the registry", { cause: err });
|
|
347
|
+
}
|
|
348
|
+
let parsed;
|
|
349
|
+
try {
|
|
350
|
+
parsed = JSON.parse(body);
|
|
351
|
+
} catch {
|
|
352
|
+
throw new UpdateCheckError("the registry answer was not valid JSON");
|
|
353
|
+
}
|
|
354
|
+
const latest = typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed.latest : void 0;
|
|
355
|
+
if (!isValidVersion(latest)) {
|
|
356
|
+
throw new UpdateCheckError("the registry answer did not hold a valid version");
|
|
357
|
+
}
|
|
358
|
+
return latest;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// src/version.ts
|
|
362
|
+
import { readFileSync } from "fs";
|
|
363
|
+
import path2 from "path";
|
|
364
|
+
import { fileURLToPath } from "url";
|
|
365
|
+
function readVersion() {
|
|
366
|
+
const here = path2.dirname(fileURLToPath(import.meta.url));
|
|
367
|
+
const candidates = [
|
|
368
|
+
path2.resolve(here, "../package.json"),
|
|
369
|
+
path2.resolve(here, "../../package.json")
|
|
370
|
+
];
|
|
371
|
+
for (const p of candidates) {
|
|
372
|
+
try {
|
|
373
|
+
const raw = readFileSync(p, "utf8");
|
|
374
|
+
const v = JSON.parse(raw).version;
|
|
375
|
+
if (typeof v === "string") return v;
|
|
376
|
+
} catch {
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return "unknown";
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// src/cli-update.ts
|
|
383
|
+
var POSIX_SAFE = /^[A-Za-z0-9_@%+=:,./-]+$/;
|
|
384
|
+
var WINDOWS_UNSAFE = /["%^&|<>!]/;
|
|
385
|
+
function hasControlCharacter(s) {
|
|
386
|
+
for (let i = 0; i < s.length; i++) {
|
|
387
|
+
const c = s.charCodeAt(i);
|
|
388
|
+
if (c < 32 || c === 127) return true;
|
|
389
|
+
}
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
function isFileOnDisk(file) {
|
|
393
|
+
try {
|
|
394
|
+
return statSync(file).isFile();
|
|
395
|
+
} catch {
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
function npmInstallCommand(version, env) {
|
|
400
|
+
const windows = env.platform === "win32";
|
|
401
|
+
const p = windows ? path3.win32 : path3.posix;
|
|
402
|
+
const npm = p.join(p.dirname(env.execPath), windows ? "npm.cmd" : "npm");
|
|
403
|
+
const args = `install -g @hardikg/mdview@${version}`;
|
|
404
|
+
if (!env.isFile(npm) || hasControlCharacter(npm)) return `npm ${args}`;
|
|
405
|
+
if (windows) {
|
|
406
|
+
if (WINDOWS_UNSAFE.test(npm)) return `npm ${args}`;
|
|
407
|
+
return /^[A-Za-z0-9_@+=:,.\\/-]+$/.test(npm) ? `${npm} ${args}` : `"${npm}" ${args}`;
|
|
408
|
+
}
|
|
409
|
+
const quoted = POSIX_SAFE.test(npm) ? npm : `'${npm.replace(/'/g, `'\\''`)}'`;
|
|
410
|
+
return `${quoted} ${args}`;
|
|
411
|
+
}
|
|
412
|
+
async function runUpdateCommand(deps = {}) {
|
|
413
|
+
const log = deps.log ?? ((line) => console.log(line));
|
|
414
|
+
const logError = deps.logError ?? ((line) => console.error(line));
|
|
415
|
+
const current = deps.currentVersion ?? readVersion();
|
|
416
|
+
const currentParsed = parseVersion(current);
|
|
417
|
+
if (!currentParsed) {
|
|
418
|
+
logError("mdview: could not read the version of this install, so it cannot compare.");
|
|
419
|
+
return 1;
|
|
420
|
+
}
|
|
421
|
+
let latest;
|
|
422
|
+
try {
|
|
423
|
+
latest = await fetchLatestVersion({
|
|
424
|
+
...deps.fetchFn ? { fetchFn: deps.fetchFn } : {},
|
|
425
|
+
...deps.timeoutMs === void 0 ? {} : { timeoutMs: deps.timeoutMs }
|
|
426
|
+
});
|
|
427
|
+
} catch (err) {
|
|
428
|
+
if (err instanceof UpdateCheckError) {
|
|
429
|
+
logError(`mdview: could not check for updates: ${err.message}.`);
|
|
430
|
+
if (process.env.MDVIEW_DEBUG === "1" && err.cause instanceof Error && err.cause.stack) {
|
|
431
|
+
logError(err.cause.stack);
|
|
432
|
+
}
|
|
433
|
+
return 1;
|
|
434
|
+
}
|
|
435
|
+
throw err;
|
|
436
|
+
}
|
|
437
|
+
const order = compareVersions(parseVersion(latest), currentParsed);
|
|
438
|
+
if (order > 0) {
|
|
439
|
+
const command = npmInstallCommand(latest, {
|
|
440
|
+
execPath: deps.install?.execPath ?? process.execPath,
|
|
441
|
+
platform: deps.install?.platform ?? process.platform,
|
|
442
|
+
isFile: deps.install?.isFile ?? isFileOnDisk
|
|
443
|
+
});
|
|
444
|
+
log(`mdview ${current} \u2192 ${latest} is available.`);
|
|
445
|
+
log("Install it with:");
|
|
446
|
+
log("");
|
|
447
|
+
log(` ${command}`);
|
|
448
|
+
log("");
|
|
449
|
+
log("The background server restarts itself on the next `mdview` run.");
|
|
450
|
+
} else if (order === 0) {
|
|
451
|
+
log(`mdview ${current} is up to date.`);
|
|
452
|
+
} else {
|
|
453
|
+
log(`mdview ${current} is newer than the latest release (${latest}).`);
|
|
454
|
+
}
|
|
455
|
+
return 0;
|
|
456
|
+
}
|
|
343
457
|
|
|
344
458
|
// src/daemon/client.ts
|
|
345
459
|
import { spawn } from "child_process";
|
|
346
|
-
import { closeSync
|
|
460
|
+
import { closeSync as closeSync2, statSync as statSync3, truncateSync } from "fs";
|
|
347
461
|
|
|
348
462
|
// src/daemon/state.ts
|
|
349
|
-
import {
|
|
350
|
-
|
|
463
|
+
import {
|
|
464
|
+
chmodSync,
|
|
465
|
+
closeSync,
|
|
466
|
+
fchmodSync,
|
|
467
|
+
mkdirSync,
|
|
468
|
+
openSync,
|
|
469
|
+
readFileSync as readFileSync2,
|
|
470
|
+
renameSync,
|
|
471
|
+
rmSync,
|
|
472
|
+
statSync as statSync2,
|
|
473
|
+
writeFileSync
|
|
474
|
+
} from "fs";
|
|
475
|
+
import path4 from "path";
|
|
351
476
|
function stateDir() {
|
|
352
|
-
return
|
|
477
|
+
return path4.dirname(globalConfigPath());
|
|
353
478
|
}
|
|
354
479
|
function daemonStatePath() {
|
|
355
|
-
return
|
|
480
|
+
return path4.join(stateDir(), "daemon.json");
|
|
356
481
|
}
|
|
357
482
|
function workspaceFilePath() {
|
|
358
|
-
return
|
|
483
|
+
return path4.join(stateDir(), "workspace.json");
|
|
359
484
|
}
|
|
360
485
|
function daemonLogPath() {
|
|
361
|
-
return
|
|
486
|
+
return path4.join(stateDir(), "daemon.log");
|
|
362
487
|
}
|
|
363
488
|
function daemonLockPath() {
|
|
364
|
-
return
|
|
489
|
+
return path4.join(stateDir(), "daemon.lock");
|
|
490
|
+
}
|
|
491
|
+
function updateCheckPath() {
|
|
492
|
+
return path4.join(stateDir(), "update-check.json");
|
|
493
|
+
}
|
|
494
|
+
function ensurePrivateDir(dir) {
|
|
495
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
496
|
+
chmodSync(dir, 448);
|
|
497
|
+
}
|
|
498
|
+
function openPrivateAppend(file) {
|
|
499
|
+
const fd = openSync(file, "a", 384);
|
|
500
|
+
try {
|
|
501
|
+
fchmodSync(fd, 384);
|
|
502
|
+
} catch (err) {
|
|
503
|
+
closeSync(fd);
|
|
504
|
+
throw err;
|
|
505
|
+
}
|
|
506
|
+
return fd;
|
|
365
507
|
}
|
|
366
508
|
function writeJsonAtomic(file, value) {
|
|
367
|
-
|
|
509
|
+
ensurePrivateDir(path4.dirname(file));
|
|
368
510
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
369
511
|
writeFileSync(tmp, `${JSON.stringify(value, null, 2)}
|
|
370
512
|
`, { mode: 384 });
|
|
@@ -372,7 +514,7 @@ function writeJsonAtomic(file, value) {
|
|
|
372
514
|
}
|
|
373
515
|
function readJsonSafe(file) {
|
|
374
516
|
try {
|
|
375
|
-
return JSON.parse(
|
|
517
|
+
return JSON.parse(readFileSync2(file, "utf8"));
|
|
376
518
|
} catch {
|
|
377
519
|
return null;
|
|
378
520
|
}
|
|
@@ -395,18 +537,18 @@ function clearDaemonState() {
|
|
|
395
537
|
rmSync(daemonStatePath(), { force: true });
|
|
396
538
|
}
|
|
397
539
|
function tryTakeSpawnLock(staleAfterMs = 1e4) {
|
|
398
|
-
|
|
540
|
+
ensurePrivateDir(stateDir());
|
|
399
541
|
const file = daemonLockPath();
|
|
400
542
|
try {
|
|
401
|
-
openSync(file, "wx");
|
|
543
|
+
closeSync(openSync(file, "wx", 384));
|
|
402
544
|
return true;
|
|
403
545
|
} catch (err) {
|
|
404
546
|
if (err.code !== "EEXIST") throw err;
|
|
405
547
|
}
|
|
406
548
|
try {
|
|
407
|
-
if (Date.now() -
|
|
549
|
+
if (Date.now() - statSync2(file).mtimeMs < staleAfterMs) return false;
|
|
408
550
|
rmSync(file, { force: true });
|
|
409
|
-
openSync(file, "wx");
|
|
551
|
+
closeSync(openSync(file, "wx", 384));
|
|
410
552
|
return true;
|
|
411
553
|
} catch {
|
|
412
554
|
return false;
|
|
@@ -435,6 +577,8 @@ async function probeHealth(port) {
|
|
|
435
577
|
return {
|
|
436
578
|
pid: body.pid,
|
|
437
579
|
startedAt: body.startedAt,
|
|
580
|
+
// Validated because it is printed to the terminal when it differs.
|
|
581
|
+
...isValidVersion(body.version) ? { version: body.version } : {},
|
|
438
582
|
roots: Array.isArray(body.roots) ? body.roots.filter((r) => typeof r === "string") : []
|
|
439
583
|
};
|
|
440
584
|
} catch {
|
|
@@ -451,12 +595,12 @@ async function findLiveDaemon() {
|
|
|
451
595
|
}
|
|
452
596
|
function spawnDaemon(cliEntry) {
|
|
453
597
|
try {
|
|
454
|
-
if (
|
|
598
|
+
if (statSync3(daemonLogPath()).size > LOG_TRUNCATE_BYTES) {
|
|
455
599
|
truncateSync(daemonLogPath(), 0);
|
|
456
600
|
}
|
|
457
601
|
} catch {
|
|
458
602
|
}
|
|
459
|
-
const log =
|
|
603
|
+
const log = openPrivateAppend(daemonLogPath());
|
|
460
604
|
try {
|
|
461
605
|
const child = spawn(process.execPath, [cliEntry, "--daemon"], {
|
|
462
606
|
detached: true,
|
|
@@ -465,28 +609,47 @@ function spawnDaemon(cliEntry) {
|
|
|
465
609
|
stdio: ["ignore", log, log]
|
|
466
610
|
});
|
|
467
611
|
child.unref();
|
|
468
|
-
return child;
|
|
469
612
|
} finally {
|
|
470
|
-
|
|
613
|
+
closeSync2(log);
|
|
471
614
|
}
|
|
472
615
|
}
|
|
473
|
-
|
|
616
|
+
function runsOtherVersion(live, version) {
|
|
617
|
+
return isValidVersion(version) && live.health.version !== version;
|
|
618
|
+
}
|
|
619
|
+
function sameDaemon(a, b) {
|
|
620
|
+
return a.state.pid === b.state.pid && a.state.startedAt === b.state.startedAt;
|
|
621
|
+
}
|
|
622
|
+
async function ensureDaemon(cliEntry, options = {}) {
|
|
623
|
+
const version = options.version ?? readVersion();
|
|
624
|
+
const timeoutMs = options.startTimeoutMs ?? START_TIMEOUT_MS;
|
|
625
|
+
const spawnFn = options.spawnFn ?? spawnDaemon;
|
|
474
626
|
const existing = await findLiveDaemon();
|
|
475
|
-
if (existing) return existing.state;
|
|
627
|
+
if (existing && !runsOtherVersion(existing, version)) return existing.state;
|
|
476
628
|
if (!tryTakeSpawnLock()) {
|
|
477
|
-
const found = await waitFor(() =>
|
|
629
|
+
const found = await waitFor(async () => {
|
|
630
|
+
const live = await findLiveDaemon();
|
|
631
|
+
if (!live) return null;
|
|
632
|
+
if (!existing || !sameDaemon(live, existing)) return live;
|
|
633
|
+
return runsOtherVersion(live, version) ? null : live;
|
|
634
|
+
}, timeoutMs);
|
|
478
635
|
if (found) return found.state;
|
|
636
|
+
const fallback = await findLiveDaemon();
|
|
637
|
+
if (fallback) return fallback.state;
|
|
479
638
|
throw new Error("Timed out waiting for the mdview background server to start.");
|
|
480
639
|
}
|
|
481
640
|
try {
|
|
641
|
+
const current = await findLiveDaemon();
|
|
642
|
+
if (current && !runsOtherVersion(current, version)) return current.state;
|
|
643
|
+
if (current) await stopDaemon();
|
|
482
644
|
clearDaemonState();
|
|
483
|
-
|
|
484
|
-
const found = await waitFor(() => findLiveDaemon(),
|
|
645
|
+
spawnFn(cliEntry);
|
|
646
|
+
const found = await waitFor(() => findLiveDaemon(), timeoutMs);
|
|
485
647
|
if (!found) {
|
|
486
648
|
throw new Error(
|
|
487
649
|
`The mdview background server did not start. See ${daemonLogPath()} for why.`
|
|
488
650
|
);
|
|
489
651
|
}
|
|
652
|
+
if (current) options.onRestart?.(current.health.version ?? "unknown", version);
|
|
490
653
|
return found.state;
|
|
491
654
|
} finally {
|
|
492
655
|
releaseSpawnLock();
|
|
@@ -532,51 +695,8 @@ async function stopDaemon() {
|
|
|
532
695
|
}
|
|
533
696
|
|
|
534
697
|
// src/daemon/workspace-file.ts
|
|
535
|
-
import { existsSync, statSync as
|
|
698
|
+
import { existsSync, lstatSync, realpathSync, statSync as statSync4 } from "fs";
|
|
536
699
|
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
700
|
var MAX_ROOTS = 8;
|
|
581
701
|
function validateRecord(raw) {
|
|
582
702
|
if (!raw || typeof raw !== "object") return null;
|
|
@@ -623,13 +743,19 @@ function toRootSpecs(record) {
|
|
|
623
743
|
function resolveTarget(target) {
|
|
624
744
|
const abs = path5.resolve(target);
|
|
625
745
|
if (!existsSync(abs)) throw new Error(`Path does not exist: ${abs}`);
|
|
626
|
-
const st =
|
|
746
|
+
const st = statSync4(abs);
|
|
627
747
|
if (st.isDirectory()) {
|
|
628
748
|
return { absPath: abs, kind: "dir", filePath: "", name: path5.basename(abs) };
|
|
629
749
|
}
|
|
630
750
|
if (st.isFile()) {
|
|
631
|
-
const
|
|
632
|
-
|
|
751
|
+
const onDisk = realpathSync.native(abs);
|
|
752
|
+
const real = lstatSync(abs).isSymbolicLink() ? onDisk : path5.join(path5.dirname(abs), path5.basename(onDisk));
|
|
753
|
+
return {
|
|
754
|
+
absPath: path5.dirname(real),
|
|
755
|
+
kind: "file",
|
|
756
|
+
filePath: path5.basename(real),
|
|
757
|
+
name: path5.basename(abs)
|
|
758
|
+
};
|
|
633
759
|
}
|
|
634
760
|
throw new Error(`Unsupported path type: ${abs}`);
|
|
635
761
|
}
|
|
@@ -718,6 +844,12 @@ Workspace is full (${MAX_ROOTS}). Remove one with \`mdview rm <name>\`.`);
|
|
|
718
844
|
if (live) {
|
|
719
845
|
console.log(`server: http://mdview.localhost:${live.state.port}/ (pid ${live.state.pid})`);
|
|
720
846
|
console.log(`logs: ${daemonLogPath()}`);
|
|
847
|
+
const own = readVersion();
|
|
848
|
+
if (isValidVersion(own) && live.health.version !== own) {
|
|
849
|
+
console.log(
|
|
850
|
+
`note: it runs ${live.health.version ?? "a version that reports none"}, this is ${own}; the next \`mdview <path>\` restarts it`
|
|
851
|
+
);
|
|
852
|
+
}
|
|
721
853
|
} else {
|
|
722
854
|
console.log("server: not running \u2014 the next `mdview <path>` will start it");
|
|
723
855
|
}
|
|
@@ -758,1086 +890,100 @@ async function runStopCommand() {
|
|
|
758
890
|
}
|
|
759
891
|
}
|
|
760
892
|
|
|
761
|
-
// src/
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
// src/server/fs/ignore.ts
|
|
767
|
-
import path6 from "path";
|
|
768
|
-
var DEFAULT_IGNORED_DIRS2 = new Set(DEFAULT_IGNORED_DIRS);
|
|
769
|
-
function buildIgnoreSet(extra = []) {
|
|
770
|
-
return /* @__PURE__ */ new Set([...DEFAULT_IGNORED_DIRS2, ...extra]);
|
|
771
|
-
}
|
|
772
|
-
function isDirIgnored(name, set) {
|
|
773
|
-
return name.startsWith(".") || set.has(name);
|
|
774
|
-
}
|
|
775
|
-
function isPathIgnored(abs, root, set) {
|
|
776
|
-
const rel = path6.relative(root, abs);
|
|
777
|
-
if (rel === "" || rel.startsWith("..")) return false;
|
|
778
|
-
for (const seg of rel.split(path6.sep)) {
|
|
779
|
-
if (!seg) continue;
|
|
780
|
-
if (seg.startsWith(".")) return true;
|
|
781
|
-
if (set.has(seg)) return true;
|
|
782
|
-
}
|
|
783
|
-
return false;
|
|
784
|
-
}
|
|
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
|
-
|
|
806
|
-
// src/server/routes/api-asset.ts
|
|
807
|
-
import { createReadStream } from "fs";
|
|
808
|
-
import { stat } from "fs/promises";
|
|
809
|
-
import path8 from "path";
|
|
810
|
-
|
|
811
|
-
// src/server/fs/resolve.ts
|
|
812
|
-
import path7 from "path";
|
|
813
|
-
function resolveSafePath(root, relPath) {
|
|
814
|
-
if (path7.isAbsolute(relPath)) {
|
|
815
|
-
throw new Error(`Refusing absolute path: ${relPath}`);
|
|
816
|
-
}
|
|
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)) {
|
|
821
|
-
throw new Error(`Path resolves outside root: ${relPath}`);
|
|
822
|
-
}
|
|
823
|
-
return candidate;
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
// src/server/routes/api-asset.ts
|
|
827
|
-
var MIME = {
|
|
828
|
-
".png": "image/png",
|
|
829
|
-
".jpg": "image/jpeg",
|
|
830
|
-
".jpeg": "image/jpeg",
|
|
831
|
-
".gif": "image/gif",
|
|
832
|
-
".webp": "image/webp",
|
|
833
|
-
".svg": "image/svg+xml",
|
|
834
|
-
".avif": "image/avif",
|
|
835
|
-
".bmp": "image/bmp",
|
|
836
|
-
".ico": "image/x-icon",
|
|
837
|
-
".mp4": "video/mp4",
|
|
838
|
-
".webm": "video/webm",
|
|
839
|
-
".mov": "video/quicktime",
|
|
840
|
-
".mp3": "audio/mpeg",
|
|
841
|
-
".wav": "audio/wav",
|
|
842
|
-
".pdf": "application/pdf",
|
|
843
|
-
".txt": "text/plain; charset=utf-8"
|
|
844
|
-
};
|
|
845
|
-
function registerApiAsset(app, roots) {
|
|
846
|
-
app.get("/__asset/*", async (req, reply) => {
|
|
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
|
-
}
|
|
852
|
-
let abs;
|
|
853
|
-
try {
|
|
854
|
-
abs = resolveSafePath(resolved.state.absPath, resolved.rel);
|
|
855
|
-
} catch (err) {
|
|
856
|
-
return reply.code(400).send({ error: err.message });
|
|
857
|
-
}
|
|
858
|
-
let st;
|
|
859
|
-
try {
|
|
860
|
-
st = await stat(abs);
|
|
861
|
-
} catch {
|
|
862
|
-
return reply.code(404).send({ error: "Asset not found" });
|
|
863
|
-
}
|
|
864
|
-
if (!st.isFile()) {
|
|
865
|
-
return reply.code(404).send({ error: "Not a file" });
|
|
866
|
-
}
|
|
867
|
-
const ext = path8.extname(abs).toLowerCase();
|
|
868
|
-
const mime = MIME[ext];
|
|
869
|
-
if (!mime) {
|
|
870
|
-
return reply.code(404).send({ error: "Asset type not allowed" });
|
|
871
|
-
}
|
|
872
|
-
reply.header("content-type", mime);
|
|
873
|
-
reply.header("content-length", st.size);
|
|
874
|
-
reply.header("cache-control", "no-cache");
|
|
875
|
-
return reply.send(createReadStream(abs));
|
|
876
|
-
});
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
// src/server/routes/api-file.ts
|
|
880
|
-
import { readFile as readFile3, stat as stat2 } from "fs/promises";
|
|
881
|
-
|
|
882
|
-
// src/render/frontmatter.ts
|
|
883
|
-
import { load } from "js-yaml";
|
|
884
|
-
var MAX_NODES = 1e4;
|
|
885
|
-
function unparsed(raw) {
|
|
886
|
-
return { data: null, body: raw, bodyStartLine: 0 };
|
|
887
|
-
}
|
|
888
|
-
function parseFrontmatter(raw) {
|
|
889
|
-
const block = locateBlock(raw);
|
|
890
|
-
if (!block) return unparsed(raw);
|
|
891
|
-
let loaded;
|
|
892
|
-
try {
|
|
893
|
-
loaded = load(block.yaml);
|
|
894
|
-
} catch {
|
|
895
|
-
return unparsed(raw);
|
|
896
|
-
}
|
|
897
|
-
if (!withinBudget(loaded)) return unparsed(raw);
|
|
898
|
-
return {
|
|
899
|
-
// An empty or comment-only block loads as `undefined`; it is still a
|
|
900
|
-
// front matter block with no keys.
|
|
901
|
-
data: loaded === void 0 ? {} : isPlainObject(loaded) ? loaded : null,
|
|
902
|
-
// CRLF-safe strip so the body and the bodyStartLine offset stay aligned for
|
|
903
|
-
// both LF- and CRLF-authored sources.
|
|
904
|
-
body: block.body.replace(/^(?:\r?\n)+/, ""),
|
|
905
|
-
bodyStartLine: block.bodyStartLine
|
|
906
|
-
};
|
|
907
|
-
}
|
|
908
|
-
function locateBlock(raw) {
|
|
909
|
-
const parts = raw.split(/(\r?\n)/);
|
|
910
|
-
const lineAt = (i) => parts[i * 2];
|
|
911
|
-
const lineCount = Math.ceil(parts.length / 2);
|
|
912
|
-
if (lineAt(0) !== "---") return null;
|
|
913
|
-
let closingIdx = -1;
|
|
914
|
-
for (let i = 1; i < lineCount; i++) {
|
|
915
|
-
if (lineAt(i) === "---") {
|
|
916
|
-
closingIdx = i;
|
|
917
|
-
break;
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
if (closingIdx < 0) return null;
|
|
921
|
-
let bodyStartLine = closingIdx + 1;
|
|
922
|
-
while (bodyStartLine < lineCount && lineAt(bodyStartLine) === "") {
|
|
923
|
-
bodyStartLine++;
|
|
924
|
-
}
|
|
925
|
-
const yaml = [];
|
|
926
|
-
for (let i = 1; i < closingIdx; i++) yaml.push(lineAt(i) ?? "");
|
|
927
|
-
return {
|
|
928
|
-
yaml: yaml.join("\n"),
|
|
929
|
-
body: parts.slice((closingIdx + 1) * 2).join(""),
|
|
930
|
-
bodyStartLine
|
|
931
|
-
};
|
|
932
|
-
}
|
|
933
|
-
function withinBudget(root) {
|
|
934
|
-
const stack = [root];
|
|
935
|
-
let visited = 0;
|
|
936
|
-
while (stack.length > 0) {
|
|
937
|
-
const node = stack.pop();
|
|
938
|
-
if (++visited > MAX_NODES) return false;
|
|
939
|
-
if (Array.isArray(node)) {
|
|
940
|
-
for (const child of node) stack.push(child);
|
|
941
|
-
} else if (isPlainObject(node)) {
|
|
942
|
-
for (const child of Object.values(node)) stack.push(child);
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
return true;
|
|
946
|
-
}
|
|
947
|
-
function isPlainObject(value) {
|
|
948
|
-
if (typeof value !== "object" || value === null) return false;
|
|
949
|
-
const proto = Object.getPrototypeOf(value);
|
|
950
|
-
return proto === Object.prototype || proto === null;
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
// src/render/links.ts
|
|
954
|
-
import path9 from "path/posix";
|
|
955
|
-
var MD_EXT_RE = /\.(md|markdown|mdx)$/i;
|
|
956
|
-
var HREF_RE = /<a\s+([^>]*?)href="([^"#?]*)(\?[^"#]*)?(#[^"]*)?"([^>]*)>/gi;
|
|
957
|
-
var IMG_SRC_RE = /<img\s+([^>]*?)src="([^"]+)"([^>]*)>/gi;
|
|
958
|
-
function tagInternalLinks(html, currentRelPath) {
|
|
959
|
-
const currentDir = path9.dirname(currentRelPath);
|
|
960
|
-
return html.replace(HREF_RE, (full, pre, target, query, hash, post) => {
|
|
961
|
-
if (!target || /^https?:\/\//i.test(target) || target.startsWith("mailto:")) {
|
|
962
|
-
return full;
|
|
963
|
-
}
|
|
964
|
-
if (!MD_EXT_RE.test(target)) return full;
|
|
965
|
-
const resolved = path9.normalize(path9.join(currentDir, target));
|
|
966
|
-
let decoded;
|
|
967
|
-
try {
|
|
968
|
-
decoded = decodeURIComponent(resolved);
|
|
969
|
-
} catch {
|
|
970
|
-
decoded = resolved;
|
|
971
|
-
}
|
|
972
|
-
const href = `?file=${encodeURIComponent(decoded)}${hash ?? ""}`;
|
|
973
|
-
const internal = `${decoded}${query ?? ""}${hash ?? ""}`;
|
|
974
|
-
return `<a ${pre}href="${href}" data-internal-link="${internal}"${post}>`;
|
|
975
|
-
});
|
|
976
|
-
}
|
|
977
|
-
function rewriteImageSrc(html, currentRelPath) {
|
|
978
|
-
const currentDir = path9.dirname(currentRelPath);
|
|
979
|
-
return html.replace(IMG_SRC_RE, (full, pre, src, post) => {
|
|
980
|
-
if (!src) return full;
|
|
981
|
-
if (/^(https?:\/\/|data:|\/__asset\/)/i.test(src)) return full;
|
|
982
|
-
const trimmed = src.replace(/^\/+/, "");
|
|
983
|
-
const resolved = path9.normalize(path9.join(currentDir, trimmed));
|
|
984
|
-
if (resolved.startsWith("..")) return full;
|
|
985
|
-
return `<img ${pre}src="/__asset/${resolved}"${post}>`;
|
|
986
|
-
});
|
|
893
|
+
// src/update/cache.ts
|
|
894
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
895
|
+
function isTimestamp(v) {
|
|
896
|
+
return typeof v === "number" && Number.isInteger(v) && v > 0;
|
|
987
897
|
}
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
function blockMathHtml(latex) {
|
|
998
|
-
const encoded = encodeURIComponent(latex.trim());
|
|
999
|
-
return `<div class="math-block" data-source="${encoded}"></div>
|
|
1000
|
-
`;
|
|
1001
|
-
}
|
|
1002
|
-
function inlineMathHtml(latex) {
|
|
1003
|
-
const encoded = encodeURIComponent(latex);
|
|
1004
|
-
return `<span class="math-inline" data-source="${encoded}"></span>`;
|
|
1005
|
-
}
|
|
1006
|
-
function convertBlockMath(tokens) {
|
|
1007
|
-
for (let i = 0; i < tokens.length - 2; i++) {
|
|
1008
|
-
if (tokens[i].type === "paragraph_open" && tokens[i + 1].type === "inline" && tokens[i + 2].type === "paragraph_close") {
|
|
1009
|
-
const inline = tokens[i + 1];
|
|
1010
|
-
const trimmed = inline.content.trim();
|
|
1011
|
-
const m = trimmed.match(BLOCK_RE);
|
|
1012
|
-
if (m) {
|
|
1013
|
-
const replacement = new tokens[i].constructor(
|
|
1014
|
-
"html_block",
|
|
1015
|
-
"",
|
|
1016
|
-
0
|
|
1017
|
-
);
|
|
1018
|
-
replacement.content = blockMathHtml(m[1]);
|
|
1019
|
-
tokens.splice(i, 3, replacement);
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
898
|
+
function readUpdateCache(file = updateCheckPath()) {
|
|
899
|
+
const raw = readJsonSafe(file);
|
|
900
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
901
|
+
const r = raw;
|
|
902
|
+
if (!isTimestamp(r.checkedAt) || !isValidVersion(r.latest)) return null;
|
|
903
|
+
const cache = { checkedAt: r.checkedAt, latest: r.latest };
|
|
904
|
+
if (isValidVersion(r.notifiedFor) && isTimestamp(r.notifiedAt)) {
|
|
905
|
+
cache.notifiedFor = r.notifiedFor;
|
|
906
|
+
cache.notifiedAt = r.notifiedAt;
|
|
1022
907
|
}
|
|
908
|
+
return cache;
|
|
1023
909
|
}
|
|
1024
|
-
function
|
|
1025
|
-
const
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
for (const child of top.children) {
|
|
1031
|
-
if (child.type !== "text") {
|
|
1032
|
-
out.push(child);
|
|
1033
|
-
continue;
|
|
1034
|
-
}
|
|
1035
|
-
const text = child.content;
|
|
1036
|
-
INLINE_RE.lastIndex = 0;
|
|
1037
|
-
let last = 0;
|
|
1038
|
-
let m;
|
|
1039
|
-
let found = false;
|
|
1040
|
-
while ((m = INLINE_RE.exec(text)) !== null) {
|
|
1041
|
-
found = true;
|
|
1042
|
-
if (m.index > last) {
|
|
1043
|
-
const t = new TokenCtor("text", "", 0);
|
|
1044
|
-
t.content = text.slice(last, m.index);
|
|
1045
|
-
out.push(t);
|
|
1046
|
-
}
|
|
1047
|
-
const mathTok = new TokenCtor("html_inline", "", 0);
|
|
1048
|
-
mathTok.content = inlineMathHtml(m[1]);
|
|
1049
|
-
out.push(mathTok);
|
|
1050
|
-
last = m.index + m[0].length;
|
|
1051
|
-
}
|
|
1052
|
-
if (!found) {
|
|
1053
|
-
out.push(child);
|
|
1054
|
-
} else if (last < text.length) {
|
|
1055
|
-
const t = new TokenCtor("text", "", 0);
|
|
1056
|
-
t.content = text.slice(last);
|
|
1057
|
-
out.push(t);
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
top.children = out;
|
|
910
|
+
function recordCheck(latest, now, file = updateCheckPath()) {
|
|
911
|
+
const previous = readUpdateCache(file);
|
|
912
|
+
const next = { checkedAt: now, latest };
|
|
913
|
+
if (previous?.notifiedFor !== void 0 && previous.notifiedAt !== void 0) {
|
|
914
|
+
next.notifiedFor = previous.notifiedFor;
|
|
915
|
+
next.notifiedAt = previous.notifiedAt;
|
|
1061
916
|
}
|
|
917
|
+
writeJsonAtomic(file, next);
|
|
1062
918
|
}
|
|
1063
|
-
function
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
});
|
|
919
|
+
function recordNotified(version, now, file = updateCheckPath()) {
|
|
920
|
+
const current = readUpdateCache(file);
|
|
921
|
+
if (!current) return;
|
|
922
|
+
writeJsonAtomic(file, { ...current, notifiedFor: version, notifiedAt: now });
|
|
1068
923
|
}
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
var COMMON_LANGUAGES = [
|
|
1073
|
-
"ts",
|
|
1074
|
-
"tsx",
|
|
1075
|
-
"js",
|
|
1076
|
-
"jsx",
|
|
1077
|
-
"python",
|
|
1078
|
-
"go",
|
|
1079
|
-
"rust",
|
|
1080
|
-
"java",
|
|
1081
|
-
"kotlin",
|
|
1082
|
-
"css",
|
|
1083
|
-
"scss",
|
|
1084
|
-
"html",
|
|
1085
|
-
"json",
|
|
1086
|
-
"yaml",
|
|
1087
|
-
"toml",
|
|
1088
|
-
"bash",
|
|
1089
|
-
"shell",
|
|
1090
|
-
"sql",
|
|
1091
|
-
"md",
|
|
1092
|
-
"diff",
|
|
1093
|
-
"dockerfile"
|
|
1094
|
-
];
|
|
1095
|
-
var PALETTE_THEMES = {
|
|
1096
|
-
// Flexoki has no upstream TextMate theme. `min-light` and `vesper` are the
|
|
1097
|
-
// closest bundled stand-ins: both are deliberately low-chroma, which is the
|
|
1098
|
-
// one property of Flexoki that a louder theme would contradict.
|
|
1099
|
-
flexoki: { light: "min-light", dark: "vesper" },
|
|
1100
|
-
paper: { light: "min-light", dark: "vitesse-dark" },
|
|
1101
|
-
solarized: { light: "solarized-light", dark: "solarized-dark" },
|
|
1102
|
-
everforest: { light: "everforest-light", dark: "everforest-dark" },
|
|
1103
|
-
"rose-pine": { light: "rose-pine-dawn", dark: "rose-pine" },
|
|
1104
|
-
kanagawa: { light: "kanagawa-lotus", dark: "kanagawa-wave" },
|
|
1105
|
-
catppuccin: { light: "catppuccin-latte", dark: "catppuccin-mocha" },
|
|
1106
|
-
"high-contrast": { light: "github-light-high-contrast", dark: "github-dark-high-contrast" }
|
|
1107
|
-
};
|
|
1108
|
-
var UNDERLYING_THEMES = Array.from(
|
|
1109
|
-
new Set(Object.values(PALETTE_THEMES).flatMap((t) => [t.light, t.dark]))
|
|
1110
|
-
);
|
|
1111
|
-
var highlighterPromise = null;
|
|
1112
|
-
function getHighlighter() {
|
|
1113
|
-
if (!highlighterPromise) {
|
|
1114
|
-
highlighterPromise = createHighlighter({
|
|
1115
|
-
themes: UNDERLYING_THEMES,
|
|
1116
|
-
langs: COMMON_LANGUAGES
|
|
1117
|
-
});
|
|
1118
|
-
}
|
|
1119
|
-
return highlighterPromise;
|
|
924
|
+
function isCheckDue(cache, now) {
|
|
925
|
+
if (!cache) return true;
|
|
926
|
+
return cache.checkedAt > now || now - cache.checkedAt >= DAY_MS;
|
|
1120
927
|
}
|
|
1121
|
-
function
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
async function highlightCode(code, lang, palette = DEFAULT_PALETTE) {
|
|
1125
|
-
const highlighter = await getHighlighter();
|
|
1126
|
-
const loaded = highlighter.getLoadedLanguages();
|
|
1127
|
-
let effectiveLang = lang;
|
|
1128
|
-
if (lang && !loaded.includes(lang) && !["text", "plain", ""].includes(lang)) {
|
|
1129
|
-
try {
|
|
1130
|
-
await highlighter.loadLanguage(lang);
|
|
1131
|
-
} catch {
|
|
1132
|
-
effectiveLang = "text";
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
try {
|
|
1136
|
-
return highlighter.codeToHtml(code, {
|
|
1137
|
-
lang: effectiveLang || "text",
|
|
1138
|
-
themes: PALETTE_THEMES[palette],
|
|
1139
|
-
defaultColor: false
|
|
1140
|
-
});
|
|
1141
|
-
} catch {
|
|
1142
|
-
return `<pre class="shiki shiki-fallback"><code>${escapeHtml(code)}</code></pre>`;
|
|
1143
|
-
}
|
|
928
|
+
function wasRecentlyNotified(cache, now) {
|
|
929
|
+
if (cache.notifiedFor !== cache.latest || cache.notifiedAt === void 0) return false;
|
|
930
|
+
return cache.notifiedAt <= now && now - cache.notifiedAt < DAY_MS;
|
|
1144
931
|
}
|
|
1145
932
|
|
|
1146
|
-
// src/
|
|
1147
|
-
var
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
});
|
|
1154
|
-
md.use(anchor, {
|
|
1155
|
-
permalink: false,
|
|
1156
|
-
slugify: (s) => s.toLowerCase().trim().replace(/[^\wÀ-ɏ\s-]/g, "").replace(/\s+/g, "-")
|
|
1157
|
-
});
|
|
1158
|
-
md.use(taskLists, { enabled: false, label: false });
|
|
1159
|
-
md.use(mathPlugin);
|
|
1160
|
-
async function renderMarkdown(source, bodyStartLine = 0, palette = DEFAULT_PALETTE) {
|
|
1161
|
-
const tokens = md.parse(source, {});
|
|
1162
|
-
for (const token of tokens) {
|
|
1163
|
-
if (token.type === "heading_open" && token.map) {
|
|
1164
|
-
token.attrSet("data-source-line", String(token.map[0] + bodyStartLine));
|
|
1165
|
-
}
|
|
1166
|
-
if (token.type === "fence") {
|
|
1167
|
-
const lang = token.info.trim().split(/\s+/)[0] || "text";
|
|
1168
|
-
if (lang === "mermaid") {
|
|
1169
|
-
token.type = "html_block";
|
|
1170
|
-
token.content = `<div class="mermaid-block" data-source="${encodeURIComponent(token.content)}"></div>
|
|
1171
|
-
`;
|
|
1172
|
-
} else {
|
|
1173
|
-
const highlighted = await highlightCode(token.content, lang, palette);
|
|
1174
|
-
token.type = "html_block";
|
|
1175
|
-
token.content = highlighted + "\n";
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
|
-
const html = md.renderer.render(tokens, md.options, {});
|
|
1180
|
-
return { html, tokens };
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
// src/render/outline.ts
|
|
1184
|
-
function extractOutline(tokens) {
|
|
1185
|
-
const flat = [];
|
|
1186
|
-
for (let i = 0; i < tokens.length; i++) {
|
|
1187
|
-
const t = tokens[i];
|
|
1188
|
-
if (t.type !== "heading_open") continue;
|
|
1189
|
-
const level = Number(t.tag.slice(1));
|
|
1190
|
-
const inline = tokens[i + 1];
|
|
1191
|
-
const id = (t.attrGet("id") ?? "").toString();
|
|
1192
|
-
const text = inline?.content?.trim() ?? "";
|
|
1193
|
-
if (!id || !text) continue;
|
|
1194
|
-
flat.push({ id, text, level, children: [] });
|
|
1195
|
-
}
|
|
1196
|
-
return nest(flat);
|
|
1197
|
-
}
|
|
1198
|
-
function nest(flat) {
|
|
1199
|
-
const root = [];
|
|
1200
|
-
const stack = [];
|
|
1201
|
-
for (const node of flat) {
|
|
1202
|
-
while (stack.length && stack[stack.length - 1].level >= node.level) {
|
|
1203
|
-
stack.pop();
|
|
1204
|
-
}
|
|
1205
|
-
if (stack.length === 0) {
|
|
1206
|
-
root.push(node);
|
|
1207
|
-
} else {
|
|
1208
|
-
stack[stack.length - 1].children.push(node);
|
|
1209
|
-
}
|
|
1210
|
-
stack.push(node);
|
|
1211
|
-
}
|
|
1212
|
-
return root;
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
// src/server/routes/api-file.ts
|
|
1216
|
-
function registerApiFile(app, roots) {
|
|
1217
|
-
app.get(
|
|
1218
|
-
"/api/file",
|
|
1219
|
-
async (req, reply) => {
|
|
1220
|
-
const requested = req.query.path?.trim() ?? "";
|
|
1221
|
-
const rawPalette = req.query.palette?.trim() ?? "";
|
|
1222
|
-
const palette = PALETTES.includes(rawPalette) ? rawPalette : DEFAULT_PALETTE;
|
|
1223
|
-
const primary = roots[0];
|
|
1224
|
-
if (!primary) {
|
|
1225
|
-
return reply.code(404).send({ error: "No folders are open" });
|
|
1226
|
-
}
|
|
1227
|
-
let wsPath = requested;
|
|
1228
|
-
if (!wsPath) {
|
|
1229
|
-
if (primary.root.kind !== "file") {
|
|
1230
|
-
return reply.code(400).send({ error: "Missing ?path" });
|
|
1231
|
-
}
|
|
1232
|
-
wsPath = toWorkspacePath(primary.root.id, primary.root.filePath);
|
|
1233
|
-
}
|
|
1234
|
-
const resolved = parseWorkspacePath(roots, wsPath);
|
|
1235
|
-
if (!resolved) {
|
|
1236
|
-
return reply.code(404).send({ error: "File not found" });
|
|
1237
|
-
}
|
|
1238
|
-
const { state, rel } = resolved;
|
|
1239
|
-
if (state.root.kind === "file" && rel !== state.root.filePath) {
|
|
1240
|
-
return reply.code(404).send({ error: "File not found in single-file mode" });
|
|
1241
|
-
}
|
|
1242
|
-
if (!/\.(md|markdown|mdx)$/i.test(rel)) {
|
|
1243
|
-
return reply.code(400).send({ error: "Only markdown files are supported" });
|
|
1244
|
-
}
|
|
1245
|
-
let absPath;
|
|
1246
|
-
try {
|
|
1247
|
-
absPath = resolveSafePath(state.absPath, rel);
|
|
1248
|
-
} catch (err) {
|
|
1249
|
-
return reply.code(400).send({ error: err.message });
|
|
1250
|
-
}
|
|
1251
|
-
let raw;
|
|
1252
|
-
let mtimeMs;
|
|
1253
|
-
try {
|
|
1254
|
-
const [content, st] = await Promise.all([
|
|
1255
|
-
readFile3(absPath, "utf8"),
|
|
1256
|
-
stat2(absPath)
|
|
1257
|
-
]);
|
|
1258
|
-
raw = content;
|
|
1259
|
-
mtimeMs = st.mtimeMs;
|
|
1260
|
-
} catch {
|
|
1261
|
-
return reply.code(404).send({ error: "File not found" });
|
|
1262
|
-
}
|
|
1263
|
-
const { data, body, bodyStartLine } = parseFrontmatter(raw);
|
|
1264
|
-
const { html: rawHtml, tokens } = await renderMarkdown(body, bodyStartLine, palette);
|
|
1265
|
-
const wsRel = toWorkspacePath(state.root.id, rel);
|
|
1266
|
-
const html = rewriteImageSrc(tagInternalLinks(rawHtml, wsRel), wsRel);
|
|
1267
|
-
const outline = extractOutline(tokens);
|
|
1268
|
-
const title = (typeof data?.title === "string" ? data.title : null) ?? outline[0]?.text ?? null;
|
|
1269
|
-
const result = {
|
|
1270
|
-
relPath: wsRel,
|
|
1271
|
-
html,
|
|
1272
|
-
outline,
|
|
1273
|
-
frontmatter: data,
|
|
1274
|
-
title,
|
|
1275
|
-
lastModified: mtimeMs
|
|
1276
|
-
};
|
|
1277
|
-
return reply.send(result);
|
|
1278
|
-
}
|
|
1279
|
-
);
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
// src/server/fs/grep.ts
|
|
1283
|
-
import { readFile as readFile4 } from "fs/promises";
|
|
1284
|
-
|
|
1285
|
-
// src/shared/search-pattern.ts
|
|
1286
|
-
function escapeRegex(s) {
|
|
1287
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1288
|
-
}
|
|
1289
|
-
function compilePattern(query, opts) {
|
|
1290
|
-
if (!query) {
|
|
1291
|
-
return {
|
|
1292
|
-
test: () => null,
|
|
1293
|
-
matchAll: () => [],
|
|
1294
|
-
matchAllWithBudget: () => ({ matches: [], truncated: false }),
|
|
1295
|
-
query,
|
|
1296
|
-
valid: true
|
|
1297
|
-
};
|
|
1298
|
-
}
|
|
1299
|
-
let re;
|
|
1300
|
-
if (opts.regex) {
|
|
933
|
+
// src/daemon/update-check.ts
|
|
934
|
+
var POLL_MS = 60 * 60 * 1e3;
|
|
935
|
+
function createUpdateChecker(deps) {
|
|
936
|
+
let running = false;
|
|
937
|
+
const tick = async () => {
|
|
938
|
+
if (running) return "busy";
|
|
939
|
+
running = true;
|
|
1301
940
|
try {
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
return
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
re = new RegExp(pat, opts.caseSensitive ? "g" : "gi");
|
|
1316
|
-
}
|
|
1317
|
-
return {
|
|
1318
|
-
query,
|
|
1319
|
-
valid: true,
|
|
1320
|
-
test(text) {
|
|
1321
|
-
re.lastIndex = 0;
|
|
1322
|
-
return re.exec(text);
|
|
1323
|
-
},
|
|
1324
|
-
matchAll(text) {
|
|
1325
|
-
re.lastIndex = 0;
|
|
1326
|
-
const out = [];
|
|
1327
|
-
let m;
|
|
1328
|
-
while ((m = re.exec(text)) !== null) {
|
|
1329
|
-
if (m[0].length === 0) {
|
|
1330
|
-
re.lastIndex++;
|
|
1331
|
-
continue;
|
|
1332
|
-
}
|
|
1333
|
-
out.push({ index: m.index, length: m[0].length });
|
|
1334
|
-
}
|
|
1335
|
-
return out;
|
|
1336
|
-
},
|
|
1337
|
-
matchAllWithBudget(text, budgetMs) {
|
|
1338
|
-
re.lastIndex = 0;
|
|
1339
|
-
const matches = [];
|
|
1340
|
-
const deadline = performance.now() + budgetMs;
|
|
1341
|
-
let m;
|
|
1342
|
-
while ((m = re.exec(text)) !== null) {
|
|
1343
|
-
if (m[0].length === 0) {
|
|
1344
|
-
re.lastIndex++;
|
|
1345
|
-
continue;
|
|
1346
|
-
}
|
|
1347
|
-
matches.push({ index: m.index, length: m[0].length });
|
|
1348
|
-
if (performance.now() > deadline) {
|
|
1349
|
-
return { matches, truncated: true };
|
|
1350
|
-
}
|
|
1351
|
-
}
|
|
1352
|
-
return { matches, truncated: false };
|
|
941
|
+
if (!await deps.isEnabled()) return "disabled";
|
|
942
|
+
const now = deps.now();
|
|
943
|
+
if (!isCheckDue(readUpdateCache(deps.cacheFile), now)) return "fresh";
|
|
944
|
+
const latest = await deps.fetchLatest();
|
|
945
|
+
recordCheck(latest, now, deps.cacheFile);
|
|
946
|
+
return "checked";
|
|
947
|
+
} catch (err) {
|
|
948
|
+
deps.log(
|
|
949
|
+
`update check failed: ${err instanceof UpdateCheckError ? err.message : "unexpected error"}`
|
|
950
|
+
);
|
|
951
|
+
return "failed";
|
|
952
|
+
} finally {
|
|
953
|
+
running = false;
|
|
1353
954
|
}
|
|
1354
955
|
};
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
// src/shared/tree-utils.ts
|
|
1358
|
-
var MD_EXT = /* @__PURE__ */ new Set([".md", ".markdown", ".mdx"]);
|
|
1359
|
-
function flattenMdRelPaths(nodes, out = []) {
|
|
1360
|
-
for (const n of nodes) {
|
|
1361
|
-
if (n.type === "file" && n.isMarkdown) out.push(n.relPath);
|
|
1362
|
-
else if (n.type === "dir" && n.children) flattenMdRelPaths(n.children, out);
|
|
1363
|
-
}
|
|
1364
|
-
return out;
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
// src/server/fs/tree.ts
|
|
1368
|
-
import { readdir } from "fs/promises";
|
|
1369
|
-
import path10 from "path";
|
|
1370
|
-
async function walkFolder(root, opts = {}) {
|
|
1371
|
-
const ignore = opts.ignore ?? DEFAULT_IGNORED_DIRS2;
|
|
1372
|
-
return walkInner(root, opts.relBase ?? "", ignore);
|
|
1373
|
-
}
|
|
1374
|
-
async function walkInner(absDir, relBase, ignore) {
|
|
1375
|
-
const entries = await readdir(absDir, { withFileTypes: true });
|
|
1376
|
-
const out = [];
|
|
1377
|
-
for (const entry of entries) {
|
|
1378
|
-
if (entry.isDirectory()) {
|
|
1379
|
-
if (isDirIgnored(entry.name, ignore)) continue;
|
|
1380
|
-
} else if (entry.name.startsWith(".")) {
|
|
1381
|
-
continue;
|
|
1382
|
-
}
|
|
1383
|
-
const childRel = relBase ? `${relBase}/${entry.name}` : entry.name;
|
|
1384
|
-
if (entry.isDirectory()) {
|
|
1385
|
-
const children = await walkInner(path10.join(absDir, entry.name), childRel, ignore);
|
|
1386
|
-
out.push({
|
|
1387
|
-
name: entry.name,
|
|
1388
|
-
relPath: childRel,
|
|
1389
|
-
type: "dir",
|
|
1390
|
-
children
|
|
1391
|
-
});
|
|
1392
|
-
} else if (entry.isFile()) {
|
|
1393
|
-
const ext = path10.extname(entry.name).toLowerCase();
|
|
1394
|
-
out.push({
|
|
1395
|
-
name: entry.name,
|
|
1396
|
-
relPath: childRel,
|
|
1397
|
-
type: "file",
|
|
1398
|
-
isMarkdown: MD_EXT.has(ext)
|
|
1399
|
-
});
|
|
1400
|
-
}
|
|
1401
|
-
}
|
|
1402
|
-
out.sort((a, b) => {
|
|
1403
|
-
if (a.type !== b.type) return a.type === "dir" ? -1 : 1;
|
|
1404
|
-
return a.name.localeCompare(b.name);
|
|
1405
|
-
});
|
|
1406
|
-
return out;
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
|
-
// src/server/fs/grep.ts
|
|
1410
|
-
function buildSnippet(line, start, end, radius) {
|
|
1411
|
-
const from = Math.max(0, start - radius);
|
|
1412
|
-
const to = Math.min(line.length, end + radius);
|
|
1413
|
-
const prefix = from > 0 ? "\u2026" : "";
|
|
1414
|
-
const suffix = to < line.length ? "\u2026" : "";
|
|
1415
|
-
const sliced = line.slice(from, to);
|
|
1416
|
-
const hlStart = prefix.length + (start - from);
|
|
1417
|
-
const hlEnd = hlStart + (end - start);
|
|
956
|
+
let timer;
|
|
1418
957
|
return {
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
const regexMode = opts.regex ?? false;
|
|
1430
|
-
if (!query || query.length === 0) {
|
|
1431
|
-
return { query, results: [], truncated: false };
|
|
1432
|
-
}
|
|
1433
|
-
const pattern = compilePattern(query, {
|
|
1434
|
-
caseSensitive: opts.caseSensitive ?? false,
|
|
1435
|
-
wholeWord: opts.wholeWord ?? false,
|
|
1436
|
-
regex: opts.regex ?? false
|
|
1437
|
-
});
|
|
1438
|
-
if (!pattern.valid) return { query, results: [], truncated: false };
|
|
1439
|
-
const results = [];
|
|
1440
|
-
let totalHits = 0;
|
|
1441
|
-
let truncated = false;
|
|
1442
|
-
for (const root of roots) {
|
|
1443
|
-
if (totalHits >= globalCap) {
|
|
1444
|
-
truncated = true;
|
|
1445
|
-
break;
|
|
1446
|
-
}
|
|
1447
|
-
const tree = await walkFolder(root.absPath, { ignore: root.ignore ?? opts.ignore });
|
|
1448
|
-
const files = flattenMdRelPaths(tree);
|
|
1449
|
-
for (const relPath of files) {
|
|
1450
|
-
if (totalHits >= globalCap) {
|
|
1451
|
-
truncated = true;
|
|
1452
|
-
break;
|
|
1453
|
-
}
|
|
1454
|
-
let absPath;
|
|
1455
|
-
try {
|
|
1456
|
-
absPath = resolveSafePath(root.absPath, relPath);
|
|
1457
|
-
} catch {
|
|
1458
|
-
continue;
|
|
1459
|
-
}
|
|
1460
|
-
let raw;
|
|
1461
|
-
try {
|
|
1462
|
-
raw = await readFile4(absPath, "utf8");
|
|
1463
|
-
} catch {
|
|
1464
|
-
continue;
|
|
1465
|
-
}
|
|
1466
|
-
const { body } = parseFrontmatter(raw);
|
|
1467
|
-
const lines = body.split(/\r?\n/);
|
|
1468
|
-
const fileHits = [];
|
|
1469
|
-
let fileTotal = 0;
|
|
1470
|
-
let fileTruncated = false;
|
|
1471
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1472
|
-
const line = lines[i];
|
|
1473
|
-
if (regexMode && line.length > maxLineLenForRegex) {
|
|
1474
|
-
fileTruncated = true;
|
|
1475
|
-
continue;
|
|
1476
|
-
}
|
|
1477
|
-
const { matches, truncated: lineTruncated } = regexMode ? pattern.matchAllWithBudget(line, perLineBudgetMs) : { matches: pattern.matchAll(line), truncated: false };
|
|
1478
|
-
if (lineTruncated) fileTruncated = true;
|
|
1479
|
-
for (const m of matches) {
|
|
1480
|
-
fileTotal++;
|
|
1481
|
-
if (fileHits.length < perFileCap) {
|
|
1482
|
-
const { snippet, highlight } = buildSnippet(
|
|
1483
|
-
line,
|
|
1484
|
-
m.index,
|
|
1485
|
-
m.index + m.length,
|
|
1486
|
-
snippetRadius
|
|
1487
|
-
);
|
|
1488
|
-
fileHits.push({ line: i + 1, col: m.index, snippet, highlight });
|
|
1489
|
-
totalHits++;
|
|
1490
|
-
} else {
|
|
1491
|
-
fileTruncated = true;
|
|
1492
|
-
}
|
|
1493
|
-
if (totalHits >= globalCap) {
|
|
1494
|
-
truncated = true;
|
|
1495
|
-
break;
|
|
1496
|
-
}
|
|
1497
|
-
}
|
|
1498
|
-
if (totalHits >= globalCap) break;
|
|
1499
|
-
}
|
|
1500
|
-
if (fileHits.length > 0) {
|
|
1501
|
-
results.push({
|
|
1502
|
-
relPath: `${root.id}/${relPath}`,
|
|
1503
|
-
hits: fileHits,
|
|
1504
|
-
total: fileTotal,
|
|
1505
|
-
truncated: fileTruncated
|
|
1506
|
-
});
|
|
1507
|
-
}
|
|
1508
|
-
}
|
|
1509
|
-
}
|
|
1510
|
-
return { query, results, truncated };
|
|
1511
|
-
}
|
|
1512
|
-
|
|
1513
|
-
// src/server/routes/api-search.ts
|
|
1514
|
-
var MAX_QUERY_LEN = 200;
|
|
1515
|
-
function isTrue(v) {
|
|
1516
|
-
return v === "1" || v === "true";
|
|
1517
|
-
}
|
|
1518
|
-
function registerApiSearch(app, roots) {
|
|
1519
|
-
app.get("/api/search", async (req, reply) => {
|
|
1520
|
-
const q = (req.query.q ?? "").trim();
|
|
1521
|
-
if (q.length === 0) {
|
|
1522
|
-
return reply.send({ query: "", results: [], truncated: false });
|
|
1523
|
-
}
|
|
1524
|
-
if (q.length > MAX_QUERY_LEN) {
|
|
1525
|
-
return reply.code(400).send({ error: "Query too long" });
|
|
1526
|
-
}
|
|
1527
|
-
const searchable = roots.filter((r) => r.root.kind === "dir").map((r) => ({ absPath: r.absPath, id: r.root.id, ignore: r.ignoreSet }));
|
|
1528
|
-
const out = await grepFiles(searchable, q, {
|
|
1529
|
-
caseSensitive: isTrue(req.query.case),
|
|
1530
|
-
wholeWord: isTrue(req.query.word),
|
|
1531
|
-
regex: isTrue(req.query.regex)
|
|
1532
|
-
});
|
|
1533
|
-
return reply.send(out);
|
|
1534
|
-
});
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
|
-
// src/server/routes/api-tree.ts
|
|
1538
|
-
import path11 from "path";
|
|
1539
|
-
function applyOverrides(config, paletteOverride) {
|
|
1540
|
-
if (!paletteOverride) return config;
|
|
1541
|
-
return { ...config ?? {}, palette: paletteOverride };
|
|
1542
|
-
}
|
|
1543
|
-
async function rootChildren(state) {
|
|
1544
|
-
const { id, kind, filePath } = state.root;
|
|
1545
|
-
if (kind === "file") {
|
|
1546
|
-
return [
|
|
1547
|
-
{
|
|
1548
|
-
name: path11.basename(filePath),
|
|
1549
|
-
relPath: toWorkspacePath(id, filePath),
|
|
1550
|
-
type: "file",
|
|
1551
|
-
isMarkdown: true
|
|
1552
|
-
}
|
|
1553
|
-
];
|
|
1554
|
-
}
|
|
1555
|
-
return walkFolder(state.absPath, { ignore: state.ignoreSet, relBase: id });
|
|
1556
|
-
}
|
|
1557
|
-
function registerApiTree(app, roots, paletteOverride) {
|
|
1558
|
-
app.get("/api/tree", async (_req, reply) => {
|
|
1559
|
-
const config = applyOverrides(roots[0]?.config ?? null, paletteOverride);
|
|
1560
|
-
if (roots.length === 1) {
|
|
1561
|
-
const only = roots[0];
|
|
1562
|
-
return reply.send({
|
|
1563
|
-
roots: [only.root],
|
|
1564
|
-
tree: await rootChildren(only),
|
|
1565
|
-
config
|
|
1566
|
-
});
|
|
1567
|
-
}
|
|
1568
|
-
const tree = [];
|
|
1569
|
-
for (const state of roots) {
|
|
1570
|
-
tree.push({
|
|
1571
|
-
name: state.root.name,
|
|
1572
|
-
relPath: state.root.id,
|
|
1573
|
-
type: "dir",
|
|
1574
|
-
children: await rootChildren(state)
|
|
1575
|
-
});
|
|
1576
|
-
}
|
|
1577
|
-
return reply.send({ roots: roots.map((r) => r.root), tree, config });
|
|
1578
|
-
});
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1581
|
-
// src/server/routes/health.ts
|
|
1582
|
-
function registerHealth(app, identity, roots) {
|
|
1583
|
-
app.get(
|
|
1584
|
-
"/api/health",
|
|
1585
|
-
async (_req, reply) => reply.send({
|
|
1586
|
-
ok: true,
|
|
1587
|
-
pid: identity.pid,
|
|
1588
|
-
startedAt: identity.startedAt,
|
|
1589
|
-
roots: roots.map((r) => r.root.id)
|
|
1590
|
-
})
|
|
1591
|
-
);
|
|
1592
|
-
}
|
|
1593
|
-
|
|
1594
|
-
// src/server/routes/sse.ts
|
|
1595
|
-
function registerSse(app, hub, hooks = {}) {
|
|
1596
|
-
let clients = 0;
|
|
1597
|
-
app.get("/api/watch", (req, reply) => {
|
|
1598
|
-
reply.raw.writeHead(200, {
|
|
1599
|
-
"Content-Type": "text/event-stream",
|
|
1600
|
-
"Cache-Control": "no-cache, no-transform",
|
|
1601
|
-
Connection: "keep-alive",
|
|
1602
|
-
"X-Accel-Buffering": "no"
|
|
1603
|
-
});
|
|
1604
|
-
reply.raw.write(": connected\n\n");
|
|
1605
|
-
let closed = false;
|
|
1606
|
-
clients++;
|
|
1607
|
-
hooks.onClientsChanged?.(clients);
|
|
1608
|
-
const cleanup = () => {
|
|
1609
|
-
if (closed) return;
|
|
1610
|
-
closed = true;
|
|
1611
|
-
clearInterval(heartbeat);
|
|
1612
|
-
hub.off("event", send);
|
|
1613
|
-
clients--;
|
|
1614
|
-
hooks.onClientsChanged?.(clients);
|
|
1615
|
-
try {
|
|
1616
|
-
reply.raw.end();
|
|
1617
|
-
} catch {
|
|
1618
|
-
}
|
|
1619
|
-
};
|
|
1620
|
-
const send = (e) => {
|
|
1621
|
-
if (closed) return;
|
|
1622
|
-
try {
|
|
1623
|
-
reply.raw.write(`event: ${e.kind}
|
|
1624
|
-
`);
|
|
1625
|
-
reply.raw.write(`data: ${JSON.stringify(e)}
|
|
1626
|
-
|
|
1627
|
-
`);
|
|
1628
|
-
} catch {
|
|
1629
|
-
cleanup();
|
|
1630
|
-
}
|
|
1631
|
-
};
|
|
1632
|
-
const heartbeat = setInterval(() => {
|
|
1633
|
-
if (closed) return;
|
|
1634
|
-
try {
|
|
1635
|
-
reply.raw.write(": hb\n\n");
|
|
1636
|
-
} catch {
|
|
1637
|
-
cleanup();
|
|
1638
|
-
}
|
|
1639
|
-
}, 15e3);
|
|
1640
|
-
hub.on("event", send);
|
|
1641
|
-
req.raw.on("close", cleanup);
|
|
1642
|
-
req.raw.on("error", cleanup);
|
|
1643
|
-
reply.raw.on("error", cleanup);
|
|
1644
|
-
reply.raw.on("close", cleanup);
|
|
1645
|
-
});
|
|
1646
|
-
}
|
|
1647
|
-
|
|
1648
|
-
// src/server/watcher.ts
|
|
1649
|
-
import { EventEmitter } from "events";
|
|
1650
|
-
import path12 from "path";
|
|
1651
|
-
import chokidar from "chokidar";
|
|
1652
|
-
function createEventHub() {
|
|
1653
|
-
const emitter = new EventEmitter();
|
|
1654
|
-
emitter.setMaxListeners(0);
|
|
1655
|
-
return {
|
|
1656
|
-
on: (event, listener) => emitter.on(event, listener),
|
|
1657
|
-
off: (event, listener) => emitter.off(event, listener),
|
|
1658
|
-
emitSynthetic: (e) => emitter.emit("event", e),
|
|
1659
|
-
close: async () => {
|
|
1660
|
-
emitter.removeAllListeners();
|
|
1661
|
-
}
|
|
1662
|
-
};
|
|
1663
|
-
}
|
|
1664
|
-
function createWatcher(rootAbsPath, opts = {}) {
|
|
1665
|
-
const emitter = new EventEmitter();
|
|
1666
|
-
emitter.setMaxListeners(0);
|
|
1667
|
-
const ignore = opts.ignore ?? DEFAULT_IGNORED_DIRS2;
|
|
1668
|
-
const prefix = opts.prefix ?? "";
|
|
1669
|
-
const watcher = chokidar.watch(rootAbsPath, {
|
|
1670
|
-
ignoreInitial: true,
|
|
1671
|
-
ignored: (p) => isPathIgnored(p, rootAbsPath, ignore),
|
|
1672
|
-
persistent: true,
|
|
1673
|
-
awaitWriteFinish: { stabilityThreshold: 60, pollInterval: 30 }
|
|
1674
|
-
});
|
|
1675
|
-
watcher.on("error", (err) => {
|
|
1676
|
-
const e = err;
|
|
1677
|
-
if (e?.code === "EMFILE" || e?.code === "ENOSPC") {
|
|
1678
|
-
console.error(
|
|
1679
|
-
`[mdview] file watcher hit ${e.code} while watching ${rootAbsPath}
|
|
1680
|
-
Path: ${e.path ?? "(unknown)"}
|
|
1681
|
-
Likely cause: a heavy directory (build output, deps) wasn't ignored.
|
|
1682
|
-
Fix: add the directory name to "ignore" in ~/.config/mdview/config.json, e.g.
|
|
1683
|
-
{ "ignore": ["deps", "_site"] }
|
|
1684
|
-
Built-in skips already cover ${[...DEFAULT_IGNORED_DIRS2].join(", ")}.`
|
|
1685
|
-
);
|
|
1686
|
-
} else {
|
|
1687
|
-
console.error(`[mdview] watcher error: ${e?.message ?? String(err)}`);
|
|
958
|
+
tick,
|
|
959
|
+
start: () => {
|
|
960
|
+
if (timer) return;
|
|
961
|
+
void tick();
|
|
962
|
+
timer = setInterval(() => void tick(), deps.pollMs ?? POLL_MS);
|
|
963
|
+
timer.unref();
|
|
964
|
+
},
|
|
965
|
+
stop: () => {
|
|
966
|
+
if (timer) clearInterval(timer);
|
|
967
|
+
timer = void 0;
|
|
1688
968
|
}
|
|
1689
|
-
});
|
|
1690
|
-
function emit(kind, abs) {
|
|
1691
|
-
const rel = path12.relative(rootAbsPath, abs).split(path12.sep).join("/");
|
|
1692
|
-
if (!rel) return;
|
|
1693
|
-
const relPath = prefix ? `${prefix}/${rel}` : rel;
|
|
1694
|
-
emitter.emit("event", { kind, relPath });
|
|
1695
|
-
}
|
|
1696
|
-
watcher.on("change", (p) => emit("change", p));
|
|
1697
|
-
watcher.on("add", (p) => emit("add", p));
|
|
1698
|
-
watcher.on("unlink", (p) => emit("unlink", p));
|
|
1699
|
-
return {
|
|
1700
|
-
on: (event, listener) => emitter.on(event, listener),
|
|
1701
|
-
off: (event, listener) => emitter.off(event, listener),
|
|
1702
|
-
emitSynthetic: (e) => emitter.emit("event", e),
|
|
1703
|
-
close: () => watcher.close()
|
|
1704
969
|
};
|
|
1705
970
|
}
|
|
1706
971
|
|
|
1707
|
-
// src/
|
|
1708
|
-
function
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
"base-uri 'none'",
|
|
1719
|
-
"form-action 'none'"
|
|
1720
|
-
];
|
|
1721
|
-
if (!embedMode) directives.push("frame-ancestors 'none'");
|
|
1722
|
-
return directives.join("; ");
|
|
1723
|
-
}
|
|
1724
|
-
async function createServer(opts) {
|
|
1725
|
-
const app = Fastify({ logger: false });
|
|
1726
|
-
const cspHtml = buildCspHtml(opts.embedMode ?? false);
|
|
1727
|
-
app.addHook("onRequest", async (req, reply) => {
|
|
1728
|
-
if (isAllowedHost(req.headers.host)) return;
|
|
1729
|
-
return reply.code(403).type("text/plain; charset=utf-8").send("Forbidden");
|
|
1730
|
-
});
|
|
1731
|
-
app.addHook("onSend", async (_req, reply, payload) => {
|
|
1732
|
-
const ct = String(reply.getHeader("content-type") ?? "");
|
|
1733
|
-
if (ct.startsWith("text/html")) {
|
|
1734
|
-
reply.header("content-security-policy", cspHtml);
|
|
1735
|
-
reply.header("x-content-type-options", "nosniff");
|
|
1736
|
-
reply.header("referrer-policy", "no-referrer");
|
|
1737
|
-
}
|
|
1738
|
-
return payload;
|
|
1739
|
-
});
|
|
1740
|
-
const roots = [];
|
|
1741
|
-
const configWatchers = /* @__PURE__ */ new Map();
|
|
1742
|
-
const hub = createEventHub();
|
|
1743
|
-
async function openRoot(spec, id) {
|
|
1744
|
-
const config = await loadEffectiveConfig(spec.absPath);
|
|
1745
|
-
const ignoreSet = buildIgnoreSet(config?.ignore ?? []);
|
|
1746
|
-
const state = {
|
|
1747
|
-
root: describeRoot(spec, id),
|
|
1748
|
-
absPath: spec.absPath,
|
|
1749
|
-
ignoreSet,
|
|
1750
|
-
config,
|
|
1751
|
-
watcher: createWatcher(spec.absPath, { ignore: ignoreSet, prefix: id })
|
|
1752
|
-
};
|
|
1753
|
-
state.watcher.on("event", (e) => hub.emitSynthetic(e));
|
|
1754
|
-
const configWatcher = chokidar2.watch(path13.join(spec.absPath, CONFIG_FILENAME), {
|
|
1755
|
-
ignoreInitial: true,
|
|
1756
|
-
persistent: true,
|
|
1757
|
-
awaitWriteFinish: { stabilityThreshold: 60, pollInterval: 30 }
|
|
1758
|
-
});
|
|
1759
|
-
const reloadConfig = async () => {
|
|
1760
|
-
state.config = await loadEffectiveConfig(spec.absPath);
|
|
1761
|
-
const event = { kind: "config", relPath: `${id}/${CONFIG_FILENAME}` };
|
|
1762
|
-
hub.emitSynthetic(event);
|
|
1763
|
-
};
|
|
1764
|
-
configWatcher.on("add", () => void reloadConfig());
|
|
1765
|
-
configWatcher.on("change", () => void reloadConfig());
|
|
1766
|
-
configWatcher.on("unlink", () => void reloadConfig());
|
|
1767
|
-
configWatchers.set(id, configWatcher);
|
|
1768
|
-
return state;
|
|
1769
|
-
}
|
|
1770
|
-
async function closeRoot(state) {
|
|
1771
|
-
const cw = configWatchers.get(state.root.id);
|
|
1772
|
-
configWatchers.delete(state.root.id);
|
|
1773
|
-
await Promise.all([state.watcher.close(), cw?.close()]);
|
|
1774
|
-
}
|
|
1775
|
-
async function setRoots(specs) {
|
|
1776
|
-
const taken = /* @__PURE__ */ new Set();
|
|
1777
|
-
const wanted = specs.map((spec) => {
|
|
1778
|
-
const id = resolveRootId(spec, taken);
|
|
1779
|
-
taken.add(id);
|
|
1780
|
-
return { id, spec };
|
|
1781
|
-
});
|
|
1782
|
-
const keep = new Map(
|
|
1783
|
-
roots.filter((r) => wanted.some((w) => w.id === r.root.id && w.spec.absPath === r.absPath)).map((r) => [r.root.id, r])
|
|
972
|
+
// src/update/notice.ts
|
|
973
|
+
async function printUpdateNotice(deps) {
|
|
974
|
+
try {
|
|
975
|
+
if (!deps.interactive()) return;
|
|
976
|
+
if (!await deps.isEnabled()) return;
|
|
977
|
+
const cache = readUpdateCache(deps.cacheFile);
|
|
978
|
+
if (!cache || !isNewerVersion(cache.latest, deps.version)) return;
|
|
979
|
+
const now = deps.now();
|
|
980
|
+
if (wasRecentlyNotified(cache, now)) return;
|
|
981
|
+
deps.print(
|
|
982
|
+
`mdview ${cache.latest} is available \u2014 run \`mdview update\` for the install command.`
|
|
1784
983
|
);
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
for (const { id, spec } of wanted) {
|
|
1788
|
-
const existing = keep.get(id);
|
|
1789
|
-
next.push(existing ?? await openRoot(spec, id));
|
|
1790
|
-
}
|
|
1791
|
-
roots.length = 0;
|
|
1792
|
-
roots.push(...next);
|
|
1793
|
-
await Promise.all(dropped.map(closeRoot));
|
|
1794
|
-
hub.emitSynthetic({ kind: "workspace" });
|
|
1795
|
-
}
|
|
1796
|
-
{
|
|
1797
|
-
const taken = /* @__PURE__ */ new Set();
|
|
1798
|
-
for (const spec of opts.roots) {
|
|
1799
|
-
const id = resolveRootId(spec, taken);
|
|
1800
|
-
taken.add(id);
|
|
1801
|
-
roots.push(await openRoot(spec, id));
|
|
1802
|
-
}
|
|
984
|
+
recordNotified(cache.latest, now, deps.cacheFile);
|
|
985
|
+
} catch {
|
|
1803
986
|
}
|
|
1804
|
-
registerApiFile(app, roots);
|
|
1805
|
-
registerApiTree(app, roots, opts.paletteOverride);
|
|
1806
|
-
registerApiAsset(app, roots);
|
|
1807
|
-
registerApiSearch(app, roots);
|
|
1808
|
-
registerSse(app, hub, { ...opts.onSseClientsChanged ? { onClientsChanged: opts.onSseClientsChanged } : {} });
|
|
1809
|
-
if (opts.identity) registerHealth(app, opts.identity, roots);
|
|
1810
|
-
await app.register(import("@fastify/static"), {
|
|
1811
|
-
root: opts.clientDir,
|
|
1812
|
-
prefix: "/"
|
|
1813
|
-
});
|
|
1814
|
-
app.setNotFoundHandler(async (req, reply) => {
|
|
1815
|
-
if (req.url.startsWith("/api/") || req.url.startsWith("/__asset/")) {
|
|
1816
|
-
reply.code(404).send({ error: "Not found" });
|
|
1817
|
-
return;
|
|
1818
|
-
}
|
|
1819
|
-
const qIndex = req.url.indexOf("?");
|
|
1820
|
-
const rawPath = qIndex >= 0 ? req.url.slice(0, qIndex) : req.url;
|
|
1821
|
-
if (/[\r\n]/.test(rawPath)) {
|
|
1822
|
-
reply.code(404).send({ error: "Not found" });
|
|
1823
|
-
return;
|
|
1824
|
-
}
|
|
1825
|
-
if (/\.(md|markdown|mdx)$/i.test(rawPath)) {
|
|
1826
|
-
const relPath = rawPath.replace(/^\/+/, "");
|
|
1827
|
-
const location = `/?file=${encodeURIComponent(relPath)}`;
|
|
1828
|
-
reply.code(302).header("Location", location).send();
|
|
1829
|
-
return;
|
|
1830
|
-
}
|
|
1831
|
-
return reply.sendFile("index.html");
|
|
1832
|
-
});
|
|
1833
|
-
app.addHook("onClose", async () => {
|
|
1834
|
-
await Promise.all([
|
|
1835
|
-
...roots.map((r) => r.watcher.close()),
|
|
1836
|
-
...[...configWatchers.values()].map((w) => w.close())
|
|
1837
|
-
]);
|
|
1838
|
-
await hub.close();
|
|
1839
|
-
});
|
|
1840
|
-
return { app, setRoots };
|
|
1841
987
|
}
|
|
1842
988
|
|
|
1843
989
|
// src/cli.ts
|
|
@@ -1850,6 +996,7 @@ Usage:
|
|
|
1850
996
|
mdview ls List open folders and the background server
|
|
1851
997
|
mdview rm <name|path>\u2026 Stop serving a folder
|
|
1852
998
|
mdview stop Stop the background server
|
|
999
|
+
mdview update Check for a newer release and print the install command
|
|
1853
1000
|
mdview config <\u2026> Read or edit the global config (run 'mdview config -h')
|
|
1854
1001
|
|
|
1855
1002
|
One background server serves every folder you open, so 'mdview .' in a second
|
|
@@ -1877,24 +1024,9 @@ Examples:
|
|
|
1877
1024
|
mdview --port 9000 ./docs One-off server on a specific port
|
|
1878
1025
|
mdview config ignore add deps _site Hide extra build dirs globally
|
|
1879
1026
|
mdview config path Print global config path
|
|
1027
|
+
mdview update Is there a newer mdview?
|
|
1880
1028
|
`.trim());
|
|
1881
1029
|
}
|
|
1882
|
-
function readVersion() {
|
|
1883
|
-
const here = path14.dirname(fileURLToPath(import.meta.url));
|
|
1884
|
-
const candidates = [
|
|
1885
|
-
path14.resolve(here, "../package.json"),
|
|
1886
|
-
path14.resolve(here, "../../package.json")
|
|
1887
|
-
];
|
|
1888
|
-
for (const p of candidates) {
|
|
1889
|
-
try {
|
|
1890
|
-
const raw = readFileSync2(p, "utf8");
|
|
1891
|
-
const v = JSON.parse(raw).version;
|
|
1892
|
-
if (typeof v === "string") return v;
|
|
1893
|
-
} catch {
|
|
1894
|
-
}
|
|
1895
|
-
}
|
|
1896
|
-
return "unknown";
|
|
1897
|
-
}
|
|
1898
1030
|
var DEFAULT_PORT = 7331;
|
|
1899
1031
|
var FRIENDLY_HOST = "mdview.localhost";
|
|
1900
1032
|
function isLoopbackAddress(addr) {
|
|
@@ -1975,24 +1107,38 @@ function parseArgs(argv, defaultPort = DEFAULT_PORT) {
|
|
|
1975
1107
|
return { kind: "run", args };
|
|
1976
1108
|
}
|
|
1977
1109
|
function detectRoot(target) {
|
|
1978
|
-
const abs =
|
|
1110
|
+
const abs = path6.resolve(target);
|
|
1979
1111
|
if (!existsSync2(abs)) throw new Error(`Path does not exist: ${abs}`);
|
|
1980
|
-
const st =
|
|
1112
|
+
const st = statSync5(abs);
|
|
1981
1113
|
if (st.isDirectory()) {
|
|
1982
1114
|
return { absPath: abs, id: deriveRootId(abs, /* @__PURE__ */ new Set()), kind: "dir" };
|
|
1983
1115
|
}
|
|
1984
1116
|
if (st.isFile()) {
|
|
1985
|
-
const
|
|
1117
|
+
const target2 = realpathSync2.native(abs);
|
|
1118
|
+
const linked = path6.dirname(target2) !== realpathSync2.native(path6.dirname(abs));
|
|
1119
|
+
const dir = linked ? path6.dirname(target2) : path6.dirname(abs);
|
|
1986
1120
|
return {
|
|
1987
1121
|
absPath: dir,
|
|
1988
1122
|
id: deriveRootId(dir, /* @__PURE__ */ new Set()),
|
|
1989
|
-
name:
|
|
1123
|
+
name: path6.basename(abs),
|
|
1990
1124
|
kind: "file",
|
|
1991
|
-
filePath:
|
|
1125
|
+
filePath: path6.basename(target2)
|
|
1992
1126
|
};
|
|
1993
1127
|
}
|
|
1994
1128
|
throw new Error(`Unsupported path type: ${abs}`);
|
|
1995
1129
|
}
|
|
1130
|
+
var EMBED_TOKEN_ENV = "MDVIEW_EMBED_TOKEN";
|
|
1131
|
+
var EMBED_TOKEN_PATTERN = /^[A-Za-z0-9_-]{32,256}$/;
|
|
1132
|
+
function takeEmbedToken() {
|
|
1133
|
+
const token = process.env[EMBED_TOKEN_ENV];
|
|
1134
|
+
delete process.env[EMBED_TOKEN_ENV];
|
|
1135
|
+
if (token === void 0 || !EMBED_TOKEN_PATTERN.test(token)) {
|
|
1136
|
+
throw new Error(
|
|
1137
|
+
`Embed mode (--vscode) needs ${EMBED_TOKEN_ENV} set to a random base64url token of at least 32 characters. --vscode is meant to be started by the mdview VS Code extension; use --foreground to run a server yourself.`
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
return token;
|
|
1141
|
+
}
|
|
1996
1142
|
function portFinderHint(port) {
|
|
1997
1143
|
switch (process.platform) {
|
|
1998
1144
|
case "darwin":
|
|
@@ -2038,10 +1184,10 @@ Find the process with: ${portFinderHint(port)}`
|
|
|
2038
1184
|
throw new Error("Could not bind a port (tried 10 in a row).");
|
|
2039
1185
|
}
|
|
2040
1186
|
function requireClientDir() {
|
|
2041
|
-
const here =
|
|
1187
|
+
const here = path6.dirname(fileURLToPath2(import.meta.url));
|
|
2042
1188
|
const candidates = [
|
|
2043
|
-
|
|
2044
|
-
|
|
1189
|
+
path6.resolve(here, "../dist/client"),
|
|
1190
|
+
path6.resolve(here, "../../dist/client")
|
|
2045
1191
|
];
|
|
2046
1192
|
const dir = candidates.find(existsSync2);
|
|
2047
1193
|
if (!dir) throw new Error("Client bundle not found. Run `npm run build:client` first.");
|
|
@@ -2054,14 +1200,29 @@ function idleTimeoutMs() {
|
|
|
2054
1200
|
if (!Number.isFinite(n) || n < 0) return 0;
|
|
2055
1201
|
return Math.round(n * 6e4);
|
|
2056
1202
|
}
|
|
1203
|
+
function loadServer() {
|
|
1204
|
+
return import("./server-KALIU7HM.mjs");
|
|
1205
|
+
}
|
|
1206
|
+
async function openBrowser(url) {
|
|
1207
|
+
const { default: open } = await import("open");
|
|
1208
|
+
await open(url);
|
|
1209
|
+
}
|
|
1210
|
+
var WARM_UP_DELAY_MS = 100;
|
|
1211
|
+
function warmHighlighterInBackground() {
|
|
1212
|
+
setTimeout(() => {
|
|
1213
|
+
void import("./shiki-5SFVA32N.mjs").then((m) => m.warmHighlighter()).catch(() => void 0);
|
|
1214
|
+
}, WARM_UP_DELAY_MS).unref();
|
|
1215
|
+
}
|
|
2057
1216
|
async function runForeground(args) {
|
|
1217
|
+
const embedToken = args.embedMode ? takeEmbedToken() : void 0;
|
|
2058
1218
|
const root = detectRoot(args.target);
|
|
2059
1219
|
const clientDir = requireClientDir();
|
|
1220
|
+
const { createServer } = await loadServer();
|
|
2060
1221
|
const { app } = await createServer({
|
|
2061
1222
|
roots: [root],
|
|
2062
1223
|
clientDir,
|
|
2063
1224
|
...args.palette === void 0 ? {} : { paletteOverride: args.palette },
|
|
2064
|
-
|
|
1225
|
+
...embedToken === void 0 ? {} : { embedToken }
|
|
2065
1226
|
});
|
|
2066
1227
|
const boundPort = await listen(app, args.port, args.portExplicit);
|
|
2067
1228
|
const host = args.embedMode ? "127.0.0.1" : await resolveDisplayHost();
|
|
@@ -2071,8 +1232,11 @@ async function runForeground(args) {
|
|
|
2071
1232
|
`);
|
|
2072
1233
|
} else {
|
|
2073
1234
|
console.log(`mdview \u2192 ${url}`);
|
|
2074
|
-
console.log(
|
|
1235
|
+
console.log(
|
|
1236
|
+
`watching: ${root.kind === "file" ? path6.join(root.absPath, root.filePath ?? "") : root.absPath}`
|
|
1237
|
+
);
|
|
2075
1238
|
}
|
|
1239
|
+
warmHighlighterInBackground();
|
|
2076
1240
|
if (args.open) await openBrowser(url);
|
|
2077
1241
|
let shuttingDown = false;
|
|
2078
1242
|
const shutdown = async () => {
|
|
@@ -2105,10 +1269,11 @@ async function runDaemon(args) {
|
|
|
2105
1269
|
const clientDir = requireClientDir();
|
|
2106
1270
|
const startedAt = Date.now();
|
|
2107
1271
|
let idleTimer;
|
|
1272
|
+
const { createServer } = await loadServer();
|
|
2108
1273
|
const server = await createServer({
|
|
2109
1274
|
roots: toRootSpecs(readWorkspace()),
|
|
2110
1275
|
clientDir,
|
|
2111
|
-
identity: { pid: process.pid, startedAt },
|
|
1276
|
+
identity: { pid: process.pid, startedAt, version: readVersion() },
|
|
2112
1277
|
onSseClientsChanged: (count) => {
|
|
2113
1278
|
if (count > 0) {
|
|
2114
1279
|
if (idleTimer) clearTimeout(idleTimer);
|
|
@@ -2121,9 +1286,19 @@ async function runDaemon(args) {
|
|
|
2121
1286
|
const boundPort = await listen(server.app, args.port, args.portExplicit);
|
|
2122
1287
|
writeDaemonState({ pid: process.pid, port: boundPort, startedAt });
|
|
2123
1288
|
console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] mdview daemon pid ${process.pid} on port ${boundPort}`);
|
|
2124
|
-
const
|
|
1289
|
+
const updateChecker = createUpdateChecker({
|
|
1290
|
+
isEnabled: async () => (await loadGlobalConfig())?.updateCheck === true,
|
|
1291
|
+
fetchLatest: () => fetchLatestVersion(),
|
|
1292
|
+
now: Date.now,
|
|
1293
|
+
log: (message) => console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${message}`)
|
|
1294
|
+
});
|
|
1295
|
+
const { default: chokidar } = await import("chokidar");
|
|
1296
|
+
const wsWatcher = chokidar.watch(workspaceFilePath(), {
|
|
2125
1297
|
ignoreInitial: true,
|
|
2126
1298
|
persistent: true,
|
|
1299
|
+
// A workspace.json replaced by a link to a directory would otherwise be
|
|
1300
|
+
// walked and watched recursively.
|
|
1301
|
+
followSymlinks: false,
|
|
2127
1302
|
awaitWriteFinish: { stabilityThreshold: 60, pollInterval: 30 }
|
|
2128
1303
|
});
|
|
2129
1304
|
const reload = () => {
|
|
@@ -2140,6 +1315,7 @@ async function runDaemon(args) {
|
|
|
2140
1315
|
shuttingDown = true;
|
|
2141
1316
|
console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] mdview daemon stopping (${why})`);
|
|
2142
1317
|
clearDaemonState();
|
|
1318
|
+
updateChecker.stop();
|
|
2143
1319
|
const timeout = new Promise((resolve) => setTimeout(resolve, 1500));
|
|
2144
1320
|
await Promise.race([
|
|
2145
1321
|
Promise.all([server.app.close().catch(() => void 0), wsWatcher.close()]).then(
|
|
@@ -2159,16 +1335,29 @@ async function runDaemon(args) {
|
|
|
2159
1335
|
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
2160
1336
|
if (process.platform !== "win32") process.on("SIGHUP", () => void shutdown("SIGHUP"));
|
|
2161
1337
|
armIdle();
|
|
1338
|
+
warmHighlighterInBackground();
|
|
1339
|
+
updateChecker.start();
|
|
2162
1340
|
}
|
|
2163
1341
|
async function runAttach(args) {
|
|
2164
1342
|
const added = addRoot(args.target);
|
|
2165
|
-
const state = await ensureDaemon(
|
|
1343
|
+
const state = await ensureDaemon(fileURLToPath2(import.meta.url), {
|
|
1344
|
+
onRestart: (from, to) => console.error(`mdview: restarted the background server (${from} \u2192 ${to})`)
|
|
1345
|
+
});
|
|
2166
1346
|
await waitForRoot(state.port, added.root.id);
|
|
2167
1347
|
const host = await resolveDisplayHost();
|
|
2168
1348
|
const base = `http://${host}:${state.port}/`;
|
|
2169
1349
|
const url = added.filePath ? `${base}?file=${encodeURIComponent(added.filePath)}` : `${base}?root=${encodeURIComponent(added.focusPath)}`;
|
|
2170
1350
|
console.log(`mdview \u2192 ${url}`);
|
|
2171
1351
|
console.log(`${added.changed ? "added" : "open"}: ${added.root.path} (${added.root.id})`);
|
|
1352
|
+
await printUpdateNotice({
|
|
1353
|
+
version: readVersion(),
|
|
1354
|
+
now: Date.now,
|
|
1355
|
+
interactive: () => Boolean(process.stdout.isTTY && process.stderr.isTTY),
|
|
1356
|
+
// The raw key, not loadGlobalConfig: this short command must not start
|
|
1357
|
+
// warning about unrelated fields in a config it never read before.
|
|
1358
|
+
isEnabled: () => readUpdateCheck(),
|
|
1359
|
+
print: (line) => console.error(line)
|
|
1360
|
+
});
|
|
2172
1361
|
if (args.open) await openBrowser(url);
|
|
2173
1362
|
}
|
|
2174
1363
|
async function main() {
|
|
@@ -2178,6 +1367,13 @@ async function main() {
|
|
|
2178
1367
|
if (sub === "ls" || sub === "status") process.exit(await runListCommand());
|
|
2179
1368
|
if (sub === "rm") process.exit(await runRemoveCommand(argv.slice(1)));
|
|
2180
1369
|
if (sub === "stop") process.exit(await runStopCommand());
|
|
1370
|
+
if (sub === "update") {
|
|
1371
|
+
if (argv.length > 1) {
|
|
1372
|
+
console.error("Usage: mdview update");
|
|
1373
|
+
process.exit(2);
|
|
1374
|
+
}
|
|
1375
|
+
process.exit(await runUpdateCommand());
|
|
1376
|
+
}
|
|
2181
1377
|
let parsed;
|
|
2182
1378
|
try {
|
|
2183
1379
|
parsed = parseArgs(argv, envPort() ?? DEFAULT_PORT);
|
|
@@ -2216,6 +1412,7 @@ function formatError(err) {
|
|
|
2216
1412
|
"Unsupported path type",
|
|
2217
1413
|
"Could not bind a port",
|
|
2218
1414
|
"Unexpected argument",
|
|
1415
|
+
"Embed mode",
|
|
2219
1416
|
"Workspace is full",
|
|
2220
1417
|
"Timed out waiting for the mdview background server",
|
|
2221
1418
|
"The mdview background server did not start"
|
|
@@ -2229,7 +1426,7 @@ var isEntryPoint = (() => {
|
|
|
2229
1426
|
const argv1 = process.argv[1];
|
|
2230
1427
|
if (!argv1) return false;
|
|
2231
1428
|
try {
|
|
2232
|
-
return
|
|
1429
|
+
return realpathSync2(fileURLToPath2(import.meta.url)) === realpathSync2(argv1);
|
|
2233
1430
|
} catch {
|
|
2234
1431
|
return false;
|
|
2235
1432
|
}
|
|
@@ -2253,6 +1450,7 @@ if (isEntryPoint) {
|
|
|
2253
1450
|
}
|
|
2254
1451
|
export {
|
|
2255
1452
|
DEFAULT_PORT,
|
|
1453
|
+
detectRoot,
|
|
2256
1454
|
idleTimeoutMs,
|
|
2257
1455
|
parseArgs
|
|
2258
1456
|
};
|