@rahmantech/knl-view 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +133 -32
- package/bin/knl-view.js +591 -58
- package/dist/assets/{abnfDiagram-WEYOIA3C-B1gaBaSI.js → abnfDiagram-WEYOIA3C-hoSbsMOz.js} +1 -1
- package/dist/assets/{arc-NZ9uRaCl.js → arc-g87Np6E5.js} +1 -1
- package/dist/assets/{architecture-WOLXFQ4H-8MalLwTd.js → architecture-WOLXFQ4H-DOIq-0IA.js} +1 -1
- package/dist/assets/{architectureDiagram-ZHVYKT53-eZ8i5cl4.js → architectureDiagram-ZHVYKT53-Dxwe8n7m.js} +1 -1
- package/dist/assets/{blockDiagram-RJAE7QA3-BavOE8BM.js → blockDiagram-RJAE7QA3-BnGng59Z.js} +1 -1
- package/dist/assets/{c4Diagram-XDPPPE54-DqMaTAzx.js → c4Diagram-XDPPPE54-C5LjqIIo.js} +1 -1
- package/dist/assets/channel-BVWkRFBw.js +1 -0
- package/dist/assets/{chunk-2E7WHVMD-B1Hj2BjJ.js → chunk-2E7WHVMD-BJ1dyGLq.js} +1 -1
- package/dist/assets/{chunk-4EMNKR2T-HDscrCiq.js → chunk-4EMNKR2T-DXF02Sgw.js} +1 -1
- package/dist/assets/{chunk-55XCU76K-QR_ARj8I.js → chunk-55XCU76K-CM1akAbu.js} +1 -1
- package/dist/assets/{chunk-7S6XSTFO-B0p3o547.js → chunk-7S6XSTFO-BOUjqldL.js} +1 -1
- package/dist/assets/{chunk-AQDTSKZB-CCxwNyyA.js → chunk-AQDTSKZB-C4isdNLA.js} +1 -1
- package/dist/assets/{chunk-DSNLSHJZ-BM5PBFmz.js → chunk-DSNLSHJZ-CbOTkSVM.js} +1 -1
- package/dist/assets/{chunk-HAGFOTED-UCDVFuN_.js → chunk-HAGFOTED-CRfnketR.js} +1 -1
- package/dist/assets/{chunk-HNE4Q36P-C_1sakp2.js → chunk-HNE4Q36P-rxlpGUU8.js} +1 -1
- package/dist/assets/{chunk-NQOKRVIF-b2D1Z4oK.js → chunk-NQOKRVIF-BOYCgTmV.js} +1 -1
- package/dist/assets/{chunk-UHW7PFBP-CJs0VwBY.js → chunk-UHW7PFBP-Cnogp1Fb.js} +1 -1
- package/dist/assets/{chunk-VQ5W62QI-DQxjgE4_.js → chunk-VQ5W62QI-Cu1WY5Uo.js} +1 -1
- package/dist/assets/{chunk-WQQL4CDK-DcjPeAfQ.js → chunk-WQQL4CDK-BJc3FPE2.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-glA0kB5s.js → chunk-XXDRQBXY-BlPj4H7s.js} +1 -1
- package/dist/assets/{chunk-YZO67HFG-DJqACL5V.js → chunk-YZO67HFG-V85H31x4.js} +1 -1
- package/dist/assets/{chunk-ZAUX2LTX-CWfaYtwb.js → chunk-ZAUX2LTX-C3XKXW09.js} +1 -1
- package/dist/assets/{chunk-ZCHMQSOR-BGX73Hho.js → chunk-ZCHMQSOR-B0mmpEgW.js} +1 -1
- package/dist/assets/{classDiagram-v2-4MYWR3UA-BpYXPz-e.js → classDiagram-v2-4MYWR3UA-oMd7YaHP.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-e1quKm0Q.js → cose-bilkent-JH36ORCC-J0kSVsDf.js} +1 -1
- package/dist/assets/{cynefin-EF2NZ3EQ-hb3oUmFO.js → cynefin-EF2NZ3EQ-DK2TNhel.js} +1 -1
- package/dist/assets/{cynefinDiagram-WF24UDRP-CnMQvLqi.js → cynefinDiagram-WF24UDRP-DDvKgieA.js} +1 -1
- package/dist/assets/{dagre-NK4GIZVX-BH_Y8y3F.js → dagre-NK4GIZVX-Ca2Fe-i-.js} +1 -1
- package/dist/assets/{diagram-CBK3MWTC-Dh79VetZ.js → diagram-CBK3MWTC-L6dmeQ2V.js} +1 -1
- package/dist/assets/{diagram-I5XIIV2D-D3u-o1RV.js → diagram-I5XIIV2D-DhG8R-v5.js} +1 -1
- package/dist/assets/{diagram-ODIL2UCW-Bfuo-OKS.js → diagram-ODIL2UCW-BpYYbYBT.js} +1 -1
- package/dist/assets/{diagram-PFBEJUXE-CwHasIbs.js → diagram-PFBEJUXE-DKQpHSsC.js} +1 -1
- package/dist/assets/{diagram-RJA3I4VV-D8ttmuE8.js → diagram-RJA3I4VV-DP7Dsnya.js} +1 -1
- package/dist/assets/{diagram-TYUNFSNR-CzC-8RZ0.js → diagram-TYUNFSNR-CfSFewUS.js} +1 -1
- package/dist/assets/{dist-CUJYGnnI.js → dist-CQNjnrld.js} +1 -1
- package/dist/assets/{ebnfDiagram-V4P3TTPT-9AAhU7In.js → ebnfDiagram-V4P3TTPT-DE3MZoJO.js} +1 -1
- package/dist/assets/{elk-QONTBCTQ-BgvnD1v_.js → elk-QONTBCTQ-p_aAQPHU.js} +1 -1
- package/dist/assets/{erDiagram-KYQIGQAA-RLtbiLlP.js → erDiagram-KYQIGQAA-DZO3vQrk.js} +1 -1
- package/dist/assets/{eventmodeling-K75KTNOO-DVY22vNZ.js → eventmodeling-K75KTNOO-BII9EPl5.js} +1 -1
- package/dist/assets/flowDiagram-IUGLLWBU-D9uPno39.js +1 -0
- package/dist/assets/{ganttDiagram-R7GPNTJY-hHiBGa9W.js → ganttDiagram-R7GPNTJY-BKiH3v2r.js} +1 -1
- package/dist/assets/{gitGraph-VSP46ZUC-Cvxay1wh.js → gitGraph-VSP46ZUC-oHL6cr73.js} +1 -1
- package/dist/assets/{gitGraphDiagram-PDYL7XHY-AOhlpSqg.js → gitGraphDiagram-PDYL7XHY-CqPVIEeo.js} +1 -1
- package/dist/assets/{index-CBLNQvK3.js → index-CSpirV16.js} +3 -3
- package/dist/assets/{info-OHQRW6UA-CJ8EUO1m.js → info-OHQRW6UA-Drrbnhpd.js} +1 -1
- package/dist/assets/{infoDiagram-MVDRKAX5-DOQu7oYI.js → infoDiagram-MVDRKAX5-DnQQFMAd.js} +1 -1
- package/dist/assets/{ishikawaDiagram-HPEHV2GE-DcVMvGmx.js → ishikawaDiagram-HPEHV2GE-DIpQlxRh.js} +1 -1
- package/dist/assets/{journeyDiagram-BOSHW7O7-luMeqEqz.js → journeyDiagram-BOSHW7O7-EBR9I5h1.js} +1 -1
- package/dist/assets/{kanban-definition-FNSA4HC5-KTZbmrW2.js → kanban-definition-FNSA4HC5-CdvNsjik.js} +1 -1
- package/dist/assets/{line-CmPHiOVm.js → line-CDLzgxDc.js} +1 -1
- package/dist/assets/{linear-DHYlKXBA.js → linear-DfXKql9u.js} +1 -1
- package/dist/assets/{mermaid-parser.core-BRDI-WhC.js → mermaid-parser.core-DnXGphT2.js} +3 -3
- package/dist/assets/{mermaid.core-DPTRaYLy.js → mermaid.core-h6fiCaSZ.js} +4 -4
- package/dist/assets/{mindmap-definition-6OY57UG3-70EN5Kw_.js → mindmap-definition-6OY57UG3-BiNfio-j.js} +1 -1
- package/dist/assets/{packet-JDAUHWVQ-CXWz83oS.js → packet-JDAUHWVQ-D76byv1S.js} +1 -1
- package/dist/assets/{pegDiagram-ECIUJNBX-z5UXKmp4.js → pegDiagram-ECIUJNBX-Drs8Pxdt.js} +1 -1
- package/dist/assets/{pie-XZMESJXO-rWI1HIQ7.js → pie-XZMESJXO-D_83z1lM.js} +1 -1
- package/dist/assets/{pieDiagram-GNMPY74T-Bh4Kv3NQ.js → pieDiagram-GNMPY74T-wvzIphSf.js} +1 -1
- package/dist/assets/{quadrantDiagram-D57K4MC3-wGM4Ckig.js → quadrantDiagram-D57K4MC3-B7EfMRWb.js} +1 -1
- package/dist/assets/{radar-ABXABTNO-CIJ92p5N.js → radar-ABXABTNO-Bs6dVdAR.js} +1 -1
- package/dist/assets/{railroad-I3PHUGI6-8bxua-Hf.js → railroad-I3PHUGI6-lKaN4miw.js} +1 -1
- package/dist/assets/{railroad-abnf-LNEFI6M7-B9KVI10m.js → railroad-abnf-LNEFI6M7-Bhh16b7Z.js} +1 -1
- package/dist/assets/{railroad-ebnf-DWYD2UWJ-Bt73rnyJ.js → railroad-ebnf-DWYD2UWJ-m6ybW-Y_.js} +1 -1
- package/dist/assets/{railroad-peg-JR7BVNR7-Cie82UPT.js → railroad-peg-JR7BVNR7-DNn72e7r.js} +1 -1
- package/dist/assets/{railroadDiagram-Y72NRBL7-C1nmi_tl.js → railroadDiagram-Y72NRBL7-DNeop1iZ.js} +1 -1
- package/dist/assets/{requirementDiagram-EQYSW5RA-BSianW40.js → requirementDiagram-EQYSW5RA-BVYZ2XYJ.js} +1 -1
- package/dist/assets/{sankeyDiagram-SY24JHO5-DIa11-h7.js → sankeyDiagram-SY24JHO5-B2MP51on.js} +1 -1
- package/dist/assets/{sequenceDiagram-BLEHNHQU-Df5rZKz2.js → sequenceDiagram-BLEHNHQU-BUvxstXG.js} +1 -1
- package/dist/assets/{src-dClg85lX.js → src-BrR0lqlG.js} +1 -1
- package/dist/assets/{stateDiagram-v2-A6IESKWZ-B15PTSvX.js → stateDiagram-v2-A6IESKWZ-Ba84OGKE.js} +1 -1
- package/dist/assets/{swimlanes-B3ENRKFQ-C9mgRCMi.js → swimlanes-B3ENRKFQ-BMSl2PNA.js} +1 -1
- package/dist/assets/swimlanesDiagram-AYQRV4AK-M1dmvq7m.js +8 -0
- package/dist/assets/{timeline-definition-JXYKGLGB-BDVf_mIv.js → timeline-definition-JXYKGLGB-DOdW-dxq.js} +1 -1
- package/dist/assets/{treeView-D4JQ5SDB-BbBK-25J.js → treeView-D4JQ5SDB-DZ9KQyn5.js} +1 -1
- package/dist/assets/{treemap-SAJKECNS-CEDX8apk.js → treemap-SAJKECNS-WEC5wtDV.js} +1 -1
- package/dist/assets/{usecaseDiagram-WPOLBNNX-D1QkwFF8.js → usecaseDiagram-WPOLBNNX-cldTztFL.js} +1 -1
- package/dist/assets/{vennDiagram-Q3Y3IDCW-5PeGZvEO.js → vennDiagram-Q3Y3IDCW-c41KySoY.js} +1 -1
- package/dist/assets/{wardley-7MLQ67FV-BatN94pO.js → wardley-7MLQ67FV-Bf9_aSSZ.js} +1 -1
- package/dist/assets/{wardleyDiagram-IVAIDKWA-DyCcDS3Y.js → wardleyDiagram-IVAIDKWA-BeKEvEBG.js} +1 -1
- package/dist/assets/{xychartDiagram-4URNHDU4-CbCXcRlg.js → xychartDiagram-4URNHDU4-tNkAxTxG.js} +1 -1
- package/dist/index.html +1 -1
- package/package.json +5 -1
- package/server/config.js +140 -14
- package/server/consensus.js +18 -13
- package/server/graph/modules_builder.js +290 -0
- package/server/graph/modules_catalog.js +288 -0
- package/server/graph/parser.js +3 -2
- package/server/graph/sources.js +165 -24
- package/server/graph/syntax.js +245 -28
- package/server/index.js +124 -61
- package/server/install.js +350 -10
- package/server/models_registry.js +34 -9
- package/server/scaffold.js +109 -9
- package/dist/assets/channel-BZ2_TTTJ.js +0 -1
- package/dist/assets/flowDiagram-IUGLLWBU-l7L5DTb5.js +0 -1
- package/dist/assets/swimlanesDiagram-AYQRV4AK-BGFq4t7O.js +0 -8
package/server/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import http from 'node:http'
|
|
2
|
-
import { promises as fs } from 'node:fs'
|
|
3
|
-
import { createReadStream } from 'node:fs'
|
|
2
|
+
import { promises as fs, createReadStream } from 'node:fs'
|
|
4
3
|
import path from 'node:path'
|
|
5
4
|
import { readMap, normalizeMap, decorateNode } from './map.js'
|
|
6
5
|
import { buildGraphFromRoot } from './graph/parser.js'
|
|
@@ -13,7 +12,7 @@ import {
|
|
|
13
12
|
inboundConsensus,
|
|
14
13
|
outboundConsensus
|
|
15
14
|
} from './consensus.js'
|
|
16
|
-
import { anchordbClientOptions, syncStatePath, DEFAULT_RELATIONS } from './config.js'
|
|
15
|
+
import { anchordbClientOptions, syncStatePath, DEFAULT_RELATIONS, createPathMatcher } from './config.js'
|
|
17
16
|
|
|
18
17
|
const MIME = {
|
|
19
18
|
'.html': 'text/html; charset=utf-8',
|
|
@@ -43,7 +42,7 @@ const MD_EXTS = new Set(['md', 'markdown', 'prd'])
|
|
|
43
42
|
const MAX_DOC_BYTES = 8 * 1024 * 1024
|
|
44
43
|
const TITLE_READ_BYTES = 8192
|
|
45
44
|
|
|
46
|
-
export function createKnowledgeServer({ root, dist = null, host = '127.0.0.1', port = 8085, anchordb = null, consensusModel = null, sourcesModel = null }) {
|
|
45
|
+
export function createKnowledgeServer({ root, dist = null, host = '127.0.0.1', port = 8085, anchordb = null, consensusModel = null, sourcesModel = null, include = null, exclude = null }) {
|
|
47
46
|
const rootAbs = path.resolve(root)
|
|
48
47
|
const distAbs = dist ? path.resolve(dist) : null
|
|
49
48
|
// Live AnchorDB connection status lives here, not a snapshot: attachAnchorDB
|
|
@@ -63,7 +62,7 @@ export function createKnowledgeServer({ root, dist = null, host = '127.0.0.1', p
|
|
|
63
62
|
}
|
|
64
63
|
|
|
65
64
|
const server = http.createServer((req, res) => {
|
|
66
|
-
route(req, res, rootAbs, distAbs, { host, port, anchordb: anchordbState }).catch((err) => {
|
|
65
|
+
route(req, res, rootAbs, distAbs, { host, port, anchordb: anchordbState, include, exclude }).catch((err) => {
|
|
67
66
|
if (!res.headersSent) sendJson(res, 500, { error: `Failed to handle request: ${err.message}` })
|
|
68
67
|
else res.end()
|
|
69
68
|
})
|
|
@@ -184,11 +183,16 @@ async function writeSyncState(model, edges) {
|
|
|
184
183
|
}
|
|
185
184
|
|
|
186
185
|
async function route(req, res, rootAbs, distAbs, listen) {
|
|
186
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
187
|
+
res.setHeader('Allow', 'GET, HEAD')
|
|
188
|
+
return sendJson(res, 405, { error: 'Only GET is supported.' })
|
|
189
|
+
}
|
|
190
|
+
|
|
187
191
|
const url = new URL(req.url, 'http://localhost')
|
|
188
192
|
const pathname = url.pathname
|
|
189
193
|
|
|
190
194
|
if (pathname === '/api/meta') return sendJson(res, 200, await buildMeta(rootAbs, listen))
|
|
191
|
-
if (pathname === '/api/tree') return sendJson(res, 200, await buildTree(rootAbs, '', await loadMapNorm(rootAbs)))
|
|
195
|
+
if (pathname === '/api/tree') return sendJson(res, 200, await buildTree(rootAbs, '', await loadMapNorm(rootAbs), listen))
|
|
192
196
|
if (pathname === '/api/doc') return sendDoc(res, rootAbs, url)
|
|
193
197
|
if (pathname === '/api/raw') return sendRaw(res, rootAbs, url)
|
|
194
198
|
if (pathname === '/api/graph') {
|
|
@@ -200,14 +204,15 @@ async function route(req, res, rootAbs, distAbs, listen) {
|
|
|
200
204
|
if (!target) {
|
|
201
205
|
return sendJson(res, 400, { error: 'Parameter "target" or "id" is required.' })
|
|
202
206
|
}
|
|
203
|
-
const
|
|
207
|
+
const rawHops = parseInt(url.searchParams.get('hops') || '2', 10)
|
|
208
|
+
const hops = Math.min(Math.max(1, Number.isInteger(rawHops) ? rawHops : 2), 10)
|
|
204
209
|
const rawDirection = url.searchParams.get('direction')
|
|
205
210
|
if (rawDirection !== null && !['in', 'out', 'both'].includes(rawDirection)) {
|
|
206
211
|
return sendJson(res, 400, { error: 'Parameter "direction" must be in, out, or both.' })
|
|
207
212
|
}
|
|
208
213
|
const direction = rawDirection || 'both'
|
|
209
214
|
const graph = await buildGraphFromRoot(rootAbs)
|
|
210
|
-
const subGraph = graph.extractSubGraph(target,
|
|
215
|
+
const subGraph = graph.extractSubGraph(target, hops, direction)
|
|
211
216
|
return sendJson(res, 200, subGraph)
|
|
212
217
|
}
|
|
213
218
|
if (pathname === '/api/anchordb/trace') {
|
|
@@ -223,11 +228,6 @@ async function route(req, res, rootAbs, distAbs, listen) {
|
|
|
223
228
|
return sendJson(res, 404, { error: `Unknown endpoint: ${pathname}` })
|
|
224
229
|
}
|
|
225
230
|
|
|
226
|
-
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
227
|
-
res.setHeader('Allow', 'GET, HEAD')
|
|
228
|
-
return sendJson(res, 405, { error: 'Only GET is supported.' })
|
|
229
|
-
}
|
|
230
|
-
|
|
231
231
|
return sendStatic(req, res, distAbs, pathname)
|
|
232
232
|
}
|
|
233
233
|
|
|
@@ -251,8 +251,10 @@ async function handleAnchorTrace(res, state, url) {
|
|
|
251
251
|
const relations = url.searchParams.get('relations')
|
|
252
252
|
? url.searchParams.get('relations').split(',').map((s) => s.trim()).filter(Boolean)
|
|
253
253
|
: (state.config.relations?.length ? state.config.relations : DEFAULT_RELATIONS)
|
|
254
|
-
const
|
|
255
|
-
const
|
|
254
|
+
const rawLimit = parseInt(url.searchParams.get('limit') || '20', 10)
|
|
255
|
+
const limit = Math.min(Math.max(1, Number.isInteger(rawLimit) ? rawLimit : 20), 100)
|
|
256
|
+
const rawHops = parseInt(url.searchParams.get('hops') || '2', 10)
|
|
257
|
+
const hops = Math.min(Math.max(1, Number.isInteger(rawHops) ? rawHops : 2), 10)
|
|
256
258
|
const rawDirection = url.searchParams.get('direction')
|
|
257
259
|
if (rawDirection !== null && !['in', 'out', 'both'].includes(rawDirection)) {
|
|
258
260
|
return sendJson(res, 400, { error: 'Parameter "direction" must be in, out, or both.' })
|
|
@@ -386,7 +388,7 @@ async function handleSourcesTrace(res, state, url) {
|
|
|
386
388
|
direction = rawDirection
|
|
387
389
|
}
|
|
388
390
|
const hops = Number.parseInt(url.searchParams.get('hops') || '2', 10)
|
|
389
|
-
const maxHops = Number.isInteger(hops)
|
|
391
|
+
const maxHops = Math.min(Math.max(1, Number.isInteger(hops) ? hops : 2), 10)
|
|
390
392
|
try {
|
|
391
393
|
const { out, in: inc } = await traceSources(state.sourcesClient, target, direction, maxHops)
|
|
392
394
|
return sendJson(res, 200, {
|
|
@@ -401,23 +403,35 @@ async function handleSourcesTrace(res, state, url) {
|
|
|
401
403
|
}
|
|
402
404
|
}
|
|
403
405
|
|
|
404
|
-
function safeJoin(base, relPath) {
|
|
405
|
-
if (!relPath) return null
|
|
406
|
-
|
|
406
|
+
async function safeJoin(base, relPath) {
|
|
407
|
+
if (!relPath || typeof relPath !== 'string') return null
|
|
408
|
+
if (relPath.includes('\0')) return null
|
|
409
|
+
const abs = path.resolve(base, relPath)
|
|
410
|
+
if (abs !== base && !abs.startsWith(base + path.sep)) return null
|
|
411
|
+
|
|
407
412
|
try {
|
|
408
|
-
|
|
409
|
-
|
|
413
|
+
const realBase = await fs.realpath(base)
|
|
414
|
+
const realAbs = await fs.realpath(abs)
|
|
415
|
+
if (realAbs !== realBase && !realAbs.startsWith(realBase + path.sep)) return null
|
|
416
|
+
return realAbs
|
|
417
|
+
} catch (err) {
|
|
418
|
+
if (err.code === 'ENOENT') {
|
|
419
|
+
try {
|
|
420
|
+
const realParent = await fs.realpath(path.dirname(abs))
|
|
421
|
+
const realBase = await fs.realpath(base)
|
|
422
|
+
if (realParent !== realBase && !realParent.startsWith(realBase + path.sep)) return null
|
|
423
|
+
return abs
|
|
424
|
+
} catch {
|
|
425
|
+
return null
|
|
426
|
+
}
|
|
427
|
+
}
|
|
410
428
|
return null
|
|
411
429
|
}
|
|
412
|
-
if (decoded.includes('\0')) return null
|
|
413
|
-
const abs = path.resolve(base, decoded)
|
|
414
|
-
if (abs !== base && !abs.startsWith(base + path.sep)) return null
|
|
415
|
-
return abs
|
|
416
430
|
}
|
|
417
431
|
|
|
418
432
|
async function buildMeta(rootAbs, listen = {}) {
|
|
419
433
|
const norm = await loadMapNorm(rootAbs)
|
|
420
|
-
const tree = await buildTree(rootAbs, '', norm)
|
|
434
|
+
const tree = await buildTree(rootAbs, '', norm, listen)
|
|
421
435
|
const counts = { files: 0, dirs: 0, newest: 0 }
|
|
422
436
|
walkStats(tree, counts)
|
|
423
437
|
const host = listen.host || '127.0.0.1'
|
|
@@ -453,7 +467,9 @@ function walkStats(nodes, acc) {
|
|
|
453
467
|
}
|
|
454
468
|
}
|
|
455
469
|
|
|
456
|
-
async function buildTree(dirAbs, rel, norm) {
|
|
470
|
+
async function buildTree(dirAbs, rel, norm, options = {}) {
|
|
471
|
+
const excludeFilter = options.exclude ? createPathMatcher(options.exclude) : null
|
|
472
|
+
const includeFilter = options.include ? createPathMatcher(options.include) : null
|
|
457
473
|
const entries = await fs.readdir(dirAbs, { withFileTypes: true })
|
|
458
474
|
const nodes = []
|
|
459
475
|
|
|
@@ -464,37 +480,52 @@ async function buildTree(dirAbs, rel, norm) {
|
|
|
464
480
|
// The map may hide entries (`ignore`). A hidden folder hides its whole
|
|
465
481
|
// contents with it: never walk into it at all.
|
|
466
482
|
if (norm?.entries?.get(childRel)?.ignore) continue
|
|
483
|
+
|
|
484
|
+
// 1. Hard exclude: knl-config exclude is truly excluded, always wins
|
|
485
|
+
if (excludeFilter && excludeFilter.matches(childRel, entry.name)) continue
|
|
486
|
+
|
|
487
|
+
// 2. Explicit include: if includeFilter is set, only included items or parent dirs are allowed
|
|
488
|
+
if (includeFilter) {
|
|
489
|
+
const isExplicitlyIncluded = includeFilter.matches(childRel, entry.name)
|
|
490
|
+
const isDirPrefixOfInclude = entry.isDirectory() && includeFilter.isPrefixOfAny(childRel)
|
|
491
|
+
if (!isExplicitlyIncluded && !isDirPrefixOfInclude) continue
|
|
492
|
+
}
|
|
493
|
+
|
|
467
494
|
const childAbs = path.join(dirAbs, entry.name)
|
|
468
495
|
|
|
469
|
-
|
|
496
|
+
try {
|
|
497
|
+
if (entry.isDirectory()) {
|
|
498
|
+
const stat = await fs.stat(childAbs)
|
|
499
|
+
const node = {
|
|
500
|
+
name: entry.name,
|
|
501
|
+
path: childRel,
|
|
502
|
+
type: 'dir',
|
|
503
|
+
mtime: stat.mtimeMs,
|
|
504
|
+
children: await buildTree(childAbs, childRel, norm, options)
|
|
505
|
+
}
|
|
506
|
+
nodes.push(decorateNode(node, norm))
|
|
507
|
+
continue
|
|
508
|
+
}
|
|
509
|
+
if (!entry.isFile()) continue
|
|
510
|
+
|
|
470
511
|
const stat = await fs.stat(childAbs)
|
|
512
|
+
const ext = path.extname(entry.name).slice(1).toLowerCase()
|
|
471
513
|
const node = {
|
|
472
514
|
name: entry.name,
|
|
473
515
|
path: childRel,
|
|
474
|
-
type: '
|
|
475
|
-
|
|
476
|
-
|
|
516
|
+
type: 'file',
|
|
517
|
+
ext,
|
|
518
|
+
size: stat.size,
|
|
519
|
+
mtime: stat.mtimeMs
|
|
520
|
+
}
|
|
521
|
+
if (MD_EXTS.has(ext)) {
|
|
522
|
+
const title = await readFirstHeading(childAbs, stat.size)
|
|
523
|
+
if (title) node.title = title
|
|
477
524
|
}
|
|
478
525
|
nodes.push(decorateNode(node, norm))
|
|
479
|
-
|
|
526
|
+
} catch {
|
|
527
|
+
// Skip unreadable or concurrently deleted entries
|
|
480
528
|
}
|
|
481
|
-
if (!entry.isFile()) continue
|
|
482
|
-
|
|
483
|
-
const stat = await fs.stat(childAbs)
|
|
484
|
-
const ext = path.extname(entry.name).slice(1).toLowerCase()
|
|
485
|
-
const node = {
|
|
486
|
-
name: entry.name,
|
|
487
|
-
path: childRel,
|
|
488
|
-
type: 'file',
|
|
489
|
-
ext,
|
|
490
|
-
size: stat.size,
|
|
491
|
-
mtime: stat.mtimeMs
|
|
492
|
-
}
|
|
493
|
-
if (MD_EXTS.has(ext)) {
|
|
494
|
-
const title = await readFirstHeading(childAbs, stat.size)
|
|
495
|
-
if (title) node.title = title
|
|
496
|
-
}
|
|
497
|
-
nodes.push(decorateNode(node, norm))
|
|
498
529
|
}
|
|
499
530
|
|
|
500
531
|
// One sort rule, same as orderForIndex in the views: documents first, then
|
|
@@ -505,15 +536,16 @@ async function buildTree(dirAbs, rel, norm) {
|
|
|
505
536
|
const bo = b.order ?? Number.POSITIVE_INFINITY
|
|
506
537
|
if (ao !== bo) return ao - bo
|
|
507
538
|
if (a.type !== b.type) return a.type === 'file' ? -1 : 1
|
|
508
|
-
return a.name.localeCompare(b.name, '
|
|
539
|
+
return a.name.localeCompare(b.name, 'en', { numeric: true, sensitivity: 'base' })
|
|
509
540
|
})
|
|
510
541
|
return nodes
|
|
511
542
|
}
|
|
512
543
|
|
|
513
544
|
async function readFirstHeading(abs, size) {
|
|
514
545
|
if (size === 0 || size > 512 * 1024) return null
|
|
515
|
-
|
|
546
|
+
let handle
|
|
516
547
|
try {
|
|
548
|
+
handle = await fs.open(abs, 'r')
|
|
517
549
|
const buffer = Buffer.alloc(TITLE_READ_BYTES)
|
|
518
550
|
const { bytesRead } = await handle.read(buffer, 0, TITLE_READ_BYTES, 0)
|
|
519
551
|
const text = buffer.subarray(0, bytesRead).toString('utf8')
|
|
@@ -521,8 +553,10 @@ async function readFirstHeading(abs, size) {
|
|
|
521
553
|
if (!match) return null
|
|
522
554
|
const title = cleanTitle(match[1])
|
|
523
555
|
return title || null
|
|
556
|
+
} catch {
|
|
557
|
+
return null
|
|
524
558
|
} finally {
|
|
525
|
-
await handle.close()
|
|
559
|
+
if (handle) await handle.close()
|
|
526
560
|
}
|
|
527
561
|
}
|
|
528
562
|
|
|
@@ -542,7 +576,7 @@ async function sendDoc(res, rootAbs, url) {
|
|
|
542
576
|
const rel = url.searchParams.get('path')
|
|
543
577
|
if (!rel) return sendJson(res, 400, { error: 'Parameter path is required.' })
|
|
544
578
|
|
|
545
|
-
const abs = safeJoin(rootAbs, rel)
|
|
579
|
+
const abs = await safeJoin(rootAbs, rel)
|
|
546
580
|
if (!abs) return sendJson(res, 400, { error: `Path outside the knowledge base: ${rel}` })
|
|
547
581
|
|
|
548
582
|
let stat
|
|
@@ -571,7 +605,7 @@ async function sendRaw(res, rootAbs, url) {
|
|
|
571
605
|
const rel = url.searchParams.get('path')
|
|
572
606
|
if (!rel) return sendJson(res, 400, { error: 'Parameter path is required.' })
|
|
573
607
|
|
|
574
|
-
const abs = safeJoin(rootAbs, rel)
|
|
608
|
+
const abs = await safeJoin(rootAbs, rel)
|
|
575
609
|
if (!abs) return sendJson(res, 400, { error: `Path outside the knowledge base: ${rel}` })
|
|
576
610
|
|
|
577
611
|
let stat
|
|
@@ -583,11 +617,23 @@ async function sendRaw(res, rootAbs, url) {
|
|
|
583
617
|
if (!stat.isFile()) return sendJson(res, 400, { error: `Not a file: ${rel}` })
|
|
584
618
|
|
|
585
619
|
const type = MIME[path.extname(abs).toLowerCase()] || 'application/octet-stream'
|
|
586
|
-
res.writeHead(200, {
|
|
620
|
+
res.writeHead(200, {
|
|
621
|
+
'Content-Type': type,
|
|
622
|
+
'Content-Length': stat.size,
|
|
623
|
+
'Cache-Control': 'no-store',
|
|
624
|
+
'X-Content-Type-Options': 'nosniff',
|
|
625
|
+
'X-Frame-Options': 'SAMEORIGIN'
|
|
626
|
+
})
|
|
587
627
|
if (res.req.method === 'HEAD') return res.end()
|
|
588
628
|
return new Promise((resolve, reject) => {
|
|
589
629
|
const stream = createReadStream(abs)
|
|
590
|
-
stream.on('error',
|
|
630
|
+
stream.on('error', (err) => {
|
|
631
|
+
res.destroy(err)
|
|
632
|
+
reject(err)
|
|
633
|
+
})
|
|
634
|
+
res.on('close', () => {
|
|
635
|
+
stream.destroy()
|
|
636
|
+
})
|
|
591
637
|
stream.on('end', resolve)
|
|
592
638
|
stream.pipe(res)
|
|
593
639
|
})
|
|
@@ -600,10 +646,17 @@ async function sendStatic(req, res, distAbs, pathname) {
|
|
|
600
646
|
return
|
|
601
647
|
}
|
|
602
648
|
|
|
603
|
-
let rel
|
|
649
|
+
let rel
|
|
650
|
+
try {
|
|
651
|
+
rel = decodeURIComponent(pathname)
|
|
652
|
+
} catch {
|
|
653
|
+
res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' })
|
|
654
|
+
res.end('Bad Request: Malformed URI path.')
|
|
655
|
+
return
|
|
656
|
+
}
|
|
604
657
|
if (rel.endsWith('/')) rel += 'index.html'
|
|
605
658
|
|
|
606
|
-
let abs = safeJoin(distAbs, rel.replace(/^\/+/, ''))
|
|
659
|
+
let abs = await safeJoin(distAbs, rel.replace(/^\/+/, ''))
|
|
607
660
|
if (!abs) {
|
|
608
661
|
res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' })
|
|
609
662
|
res.end('Access denied.')
|
|
@@ -636,12 +689,20 @@ async function sendStatic(req, res, distAbs, pathname) {
|
|
|
636
689
|
res.writeHead(200, {
|
|
637
690
|
'Content-Type': MIME[ext] || 'application/octet-stream',
|
|
638
691
|
'Content-Length': stat.size,
|
|
639
|
-
'Cache-Control': cache
|
|
692
|
+
'Cache-Control': cache,
|
|
693
|
+
'X-Content-Type-Options': 'nosniff',
|
|
694
|
+
'X-Frame-Options': 'SAMEORIGIN'
|
|
640
695
|
})
|
|
641
696
|
if (req.method === 'HEAD') return res.end()
|
|
642
697
|
return new Promise((resolve, reject) => {
|
|
643
698
|
const stream = createReadStream(abs)
|
|
644
|
-
stream.on('error',
|
|
699
|
+
stream.on('error', (err) => {
|
|
700
|
+
res.destroy(err)
|
|
701
|
+
reject(err)
|
|
702
|
+
})
|
|
703
|
+
res.on('close', () => {
|
|
704
|
+
stream.destroy()
|
|
705
|
+
})
|
|
645
706
|
stream.on('end', resolve)
|
|
646
707
|
stream.pipe(res)
|
|
647
708
|
})
|
|
@@ -660,7 +721,9 @@ function sendJson(res, status, body) {
|
|
|
660
721
|
res.writeHead(status, {
|
|
661
722
|
'Content-Type': 'application/json; charset=utf-8',
|
|
662
723
|
'Content-Length': Buffer.byteLength(payload),
|
|
663
|
-
'Cache-Control': 'no-store'
|
|
724
|
+
'Cache-Control': 'no-store',
|
|
725
|
+
'X-Content-Type-Options': 'nosniff',
|
|
726
|
+
'X-Frame-Options': 'SAMEORIGIN'
|
|
664
727
|
})
|
|
665
728
|
res.end(payload)
|
|
666
729
|
}
|