@justyork/repo-mind 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 +110 -0
- package/dist/ab-demo/arm-baseline.d.ts +8 -0
- package/dist/ab-demo/arm-baseline.js +40 -0
- package/dist/ab-demo/arm-repomind.d.ts +7 -0
- package/dist/ab-demo/arm-repomind.js +35 -0
- package/dist/ab-demo/estimate-tokens.d.ts +3 -0
- package/dist/ab-demo/estimate-tokens.js +10 -0
- package/dist/ab-demo/load-questions.d.ts +2 -0
- package/dist/ab-demo/load-questions.js +65 -0
- package/dist/ab-demo/paths.d.ts +5 -0
- package/dist/ab-demo/paths.js +31 -0
- package/dist/ab-demo/run-ab.d.ts +7 -0
- package/dist/ab-demo/run-ab.js +128 -0
- package/dist/ab-demo/run-arms.d.ts +3 -0
- package/dist/ab-demo/run-arms.js +67 -0
- package/dist/ab-demo/session-overhead.d.ts +3 -0
- package/dist/ab-demo/session-overhead.js +68 -0
- package/dist/ab-demo/types.d.ts +65 -0
- package/dist/ab-demo/types.js +1 -0
- package/dist/ab-demo/validate-corpus.d.ts +3 -0
- package/dist/ab-demo/validate-corpus.js +38 -0
- package/dist/check/collect-violations.d.ts +11 -0
- package/dist/check/collect-violations.js +127 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +147 -0
- package/dist/commands/check.d.ts +6 -0
- package/dist/commands/check.js +19 -0
- package/dist/commands/export.d.ts +8 -0
- package/dist/commands/export.js +80 -0
- package/dist/commands/init.d.ts +4 -0
- package/dist/commands/init.js +86 -0
- package/dist/commands/prepare.d.ts +7 -0
- package/dist/commands/prepare.js +61 -0
- package/dist/commands/setup.d.ts +11 -0
- package/dist/commands/setup.js +84 -0
- package/dist/commands/sync-links.d.ts +7 -0
- package/dist/commands/sync-links.js +41 -0
- package/dist/commands/ui.d.ts +5 -0
- package/dist/commands/ui.js +83 -0
- package/dist/index/asset-file.d.ts +4 -0
- package/dist/index/asset-file.js +26 -0
- package/dist/index/doc-index.d.ts +21 -0
- package/dist/index/doc-index.js +231 -0
- package/dist/index/knowledge-file.d.ts +4 -0
- package/dist/index/knowledge-file.js +17 -0
- package/dist/index/link-index.d.ts +41 -0
- package/dist/index/link-index.js +150 -0
- package/dist/index/path-inference.d.ts +9 -0
- package/dist/index/path-inference.js +33 -0
- package/dist/index/resolve-asset-href.d.ts +2 -0
- package/dist/index/resolve-asset-href.js +65 -0
- package/dist/index/resolve-md-href.d.ts +7 -0
- package/dist/index/resolve-md-href.js +116 -0
- package/dist/index/slug.d.ts +5 -0
- package/dist/index/slug.js +43 -0
- package/dist/index/types.d.ts +44 -0
- package/dist/index/types.js +71 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +155 -0
- package/dist/package-version.d.ts +2 -0
- package/dist/package-version.js +15 -0
- package/dist/prepare/auto-links.d.ts +22 -0
- package/dist/prepare/auto-links.js +124 -0
- package/dist/prepare/prepare-docs.d.ts +36 -0
- package/dist/prepare/prepare-docs.js +106 -0
- package/dist/tools/explore-graph.d.ts +25 -0
- package/dist/tools/explore-graph.js +84 -0
- package/dist/tools/get-doc.d.ts +11 -0
- package/dist/tools/get-doc.js +21 -0
- package/dist/tools/get-glossary-term.d.ts +9 -0
- package/dist/tools/get-glossary-term.js +41 -0
- package/dist/tools/list-docs.d.ts +19 -0
- package/dist/tools/list-docs.js +35 -0
- package/dist/tools/search-docs.d.ts +14 -0
- package/dist/tools/search-docs.js +80 -0
- package/dist/ui/api-handlers.d.ts +12 -0
- package/dist/ui/api-handlers.js +223 -0
- package/dist/ui/catalog-meta.d.ts +4 -0
- package/dist/ui/catalog-meta.js +47 -0
- package/dist/ui/db/drafts-db.d.ts +49 -0
- package/dist/ui/db/drafts-db.js +179 -0
- package/dist/ui/diff.d.ts +8 -0
- package/dist/ui/diff.js +58 -0
- package/dist/ui/docs-watcher.d.ts +13 -0
- package/dist/ui/docs-watcher.js +59 -0
- package/dist/ui/draft-api.d.ts +7 -0
- package/dist/ui/draft-api.js +413 -0
- package/dist/ui/fs-operations.d.ts +52 -0
- package/dist/ui/fs-operations.js +304 -0
- package/dist/ui/fs-tree.d.ts +28 -0
- package/dist/ui/fs-tree.js +148 -0
- package/dist/ui/graph-all.d.ts +5 -0
- package/dist/ui/graph-all.js +50 -0
- package/dist/ui/link-cascade.d.ts +9 -0
- package/dist/ui/link-cascade.js +113 -0
- package/dist/ui/parse-multipart.d.ts +12 -0
- package/dist/ui/parse-multipart.js +64 -0
- package/dist/ui/publish.d.ts +14 -0
- package/dist/ui/publish.js +83 -0
- package/dist/ui/safe-path.d.ts +6 -0
- package/dist/ui/safe-path.js +42 -0
- package/dist/ui/serve-asset.d.ts +4 -0
- package/dist/ui/serve-asset.js +49 -0
- package/dist/ui/server.d.ts +17 -0
- package/dist/ui/server.js +237 -0
- package/dist/ui/stats.d.ts +9 -0
- package/dist/ui/stats.js +23 -0
- package/dist/ui/templates.d.ts +12 -0
- package/dist/ui/templates.js +39 -0
- package/dist/ui/upload-asset.d.ts +11 -0
- package/dist/ui/upload-asset.js +61 -0
- package/package.json +55 -0
- package/templates/adr-example.md +27 -0
- package/templates/agent-instruction-example.md +20 -0
- package/templates/combat-system-example.md +27 -0
- package/templates/feature-spec-example.md +27 -0
- package/templates/glossary-term-example.md +15 -0
- package/templates/open-question-example.md +26 -0
- package/ui/dist/assets/arc-DhC0JPue.js +1 -0
- package/ui/dist/assets/architectureDiagram-3BPJPVTR-Cun_Ijrv.js +36 -0
- package/ui/dist/assets/blockDiagram-GPEHLZMM-CgiNAArN.js +132 -0
- package/ui/dist/assets/c4Diagram-AAUBKEIU-BIwHcwcH.js +10 -0
- package/ui/dist/assets/channel-CNwAp9ic.js +1 -0
- package/ui/dist/assets/chunk-2J33WTMH-DXRgHPpp.js +1 -0
- package/ui/dist/assets/chunk-4BX2VUAB-BTb70kIb.js +1 -0
- package/ui/dist/assets/chunk-55IACEB6-BrAelyhX.js +1 -0
- package/ui/dist/assets/chunk-727SXJPM-BlYnlPdj.js +206 -0
- package/ui/dist/assets/chunk-AQP2D5EJ-DSPgdKZ8.js +231 -0
- package/ui/dist/assets/chunk-FMBD7UC4-BhH8ir2K.js +15 -0
- package/ui/dist/assets/chunk-ND2GUHAM-DCAuTSxB.js +1 -0
- package/ui/dist/assets/chunk-QZHKN3VN-DtYEkbYr.js +1 -0
- package/ui/dist/assets/classDiagram-4FO5ZUOK-DnHeGLmR.js +1 -0
- package/ui/dist/assets/classDiagram-v2-Q7XG4LA2-DnHeGLmR.js +1 -0
- package/ui/dist/assets/cose-bilkent-S5V4N54A-CAM4jLYo.js +1 -0
- package/ui/dist/assets/cytoscape.esm-DTSO7Bv0.js +331 -0
- package/ui/dist/assets/dagre-BM42HDAG-CISbgani.js +4 -0
- package/ui/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/ui/dist/assets/diagram-2AECGRRQ-BmXargwF.js +43 -0
- package/ui/dist/assets/diagram-5GNKFQAL-COlrLu0O.js +10 -0
- package/ui/dist/assets/diagram-KO2AKTUF-B-kUxuHX.js +3 -0
- package/ui/dist/assets/diagram-LMA3HP47-C3AVVxcm.js +24 -0
- package/ui/dist/assets/diagram-OG6HWLK6-JHeftSsO.js +24 -0
- package/ui/dist/assets/erDiagram-TEJ5UH35-BSWwMysi.js +85 -0
- package/ui/dist/assets/flowDiagram-I6XJVG4X-D-q1cK69.js +162 -0
- package/ui/dist/assets/ganttDiagram-6RSMTGT7-DrYn1H_t.js +292 -0
- package/ui/dist/assets/gitGraphDiagram-PVQCEYII-vJByl99X.js +106 -0
- package/ui/dist/assets/graph-CAnANduQ.js +1 -0
- package/ui/dist/assets/graph-DwoitsWW.js +2 -0
- package/ui/dist/assets/infoDiagram-5YYISTIA-D6zhGTMj.js +2 -0
- package/ui/dist/assets/init-Gi6I4Gst.js +1 -0
- package/ui/dist/assets/ishikawaDiagram-YF4QCWOH-CY-U_l7l.js +70 -0
- package/ui/dist/assets/journeyDiagram-JHISSGLW-jKj4lBEJ.js +139 -0
- package/ui/dist/assets/kanban-definition-UN3LZRKU-PZ-5AYw2.js +89 -0
- package/ui/dist/assets/katex-C5jXJg4s.js +257 -0
- package/ui/dist/assets/layout-DGIYPm2g.js +1 -0
- package/ui/dist/assets/linear-COY9pyF4.js +1 -0
- package/ui/dist/assets/main-BBzCq-49.js +308 -0
- package/ui/dist/assets/mermaid.core-Bddhr0ku.js +309 -0
- package/ui/dist/assets/mindmap-definition-RKZ34NQL-CPY2Fdu_.js +96 -0
- package/ui/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/ui/dist/assets/pieDiagram-4H26LBE5-C7GJ49et.js +30 -0
- package/ui/dist/assets/quadrantDiagram-W4KKPZXB-DQyQN5K7.js +7 -0
- package/ui/dist/assets/requirementDiagram-4Y6WPE33-CDrkwz1t.js +84 -0
- package/ui/dist/assets/sankeyDiagram-5OEKKPKP-BrYb9Eql.js +40 -0
- package/ui/dist/assets/sequenceDiagram-3UESZ5HK-B8If_JZp.js +162 -0
- package/ui/dist/assets/stateDiagram-AJRCARHV-BbpTp9VX.js +1 -0
- package/ui/dist/assets/stateDiagram-v2-BHNVJYJU-BT4PvMFS.js +1 -0
- package/ui/dist/assets/theme-DV7vqTnV.js +1 -0
- package/ui/dist/assets/theme-SpsWsRN5.css +1 -0
- package/ui/dist/assets/timeline-definition-PNZ67QCA-DhUg6aIV.js +120 -0
- package/ui/dist/assets/transform-BwXaE9hv.js +1 -0
- package/ui/dist/assets/vennDiagram-CIIHVFJN-DpQVNNzF.js +34 -0
- package/ui/dist/assets/wardley-L42UT6IY-CyaxzHGP.js +173 -0
- package/ui/dist/assets/wardleyDiagram-YWT4CUSO-Bm0mA7wm.js +78 -0
- package/ui/dist/assets/xychartDiagram-2RQKCTM6-OJbmgDx6.js +7 -0
- package/ui/dist/graph.html +27 -0
- package/ui/dist/index.html +37 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import matter from 'gray-matter';
|
|
4
|
+
import { getBacklinksForSlug } from '../index/link-index.js';
|
|
5
|
+
import { inferTypeFromRelative, resolveDomain } from '../index/path-inference.js';
|
|
6
|
+
import { slugFromRelativePath } from '../index/slug.js';
|
|
7
|
+
import { buildLinkIndexForDocs } from '../tools/explore-graph.js';
|
|
8
|
+
import { readPageTemplate } from './templates.js';
|
|
9
|
+
import { isValidFsName, joinRelativePath, normalizeRelativePath, parentRelativePath, resolveRelativeMdPath, resolveUnderKnowledgeRoot, } from './safe-path.js';
|
|
10
|
+
import { cascadeSlugDelete, cascadeSlugRename, toRelativePaths } from './link-cascade.js';
|
|
11
|
+
function inferTypeFromParent(parentPath) {
|
|
12
|
+
return inferTypeFromRelative(`${parentPath}/page.md`);
|
|
13
|
+
}
|
|
14
|
+
export function createFolder(index, parentPath, name) {
|
|
15
|
+
const knowledgeRoot = index.getKnowledgeRoot();
|
|
16
|
+
if (!knowledgeRoot) {
|
|
17
|
+
throw new Error('no docs/ directory found');
|
|
18
|
+
}
|
|
19
|
+
if (!isValidFsName(name)) {
|
|
20
|
+
throw new Error(`invalid folder name: ${name}`);
|
|
21
|
+
}
|
|
22
|
+
const relativePath = joinRelativePath(parentPath, name);
|
|
23
|
+
const absolutePath = resolveUnderKnowledgeRoot(knowledgeRoot, relativePath);
|
|
24
|
+
if (!absolutePath) {
|
|
25
|
+
throw new Error('path escapes docs/');
|
|
26
|
+
}
|
|
27
|
+
if (fs.existsSync(absolutePath)) {
|
|
28
|
+
throw new Error(`already exists: ${relativePath}`);
|
|
29
|
+
}
|
|
30
|
+
fs.mkdirSync(absolutePath, { recursive: true });
|
|
31
|
+
return { relativePath: normalizeRelativePath(relativePath), absolutePath };
|
|
32
|
+
}
|
|
33
|
+
export function createPageFile(index, parentPath, name, options = {}) {
|
|
34
|
+
const knowledgeRoot = index.getKnowledgeRoot();
|
|
35
|
+
if (!knowledgeRoot) {
|
|
36
|
+
throw new Error('no docs/ directory found');
|
|
37
|
+
}
|
|
38
|
+
const baseName = name.endsWith('.md') ? name.slice(0, -3) : name;
|
|
39
|
+
if (!isValidFsName(baseName)) {
|
|
40
|
+
throw new Error(`invalid page name: ${name}`);
|
|
41
|
+
}
|
|
42
|
+
const relativePath = joinRelativePath(parentPath, `${baseName}.md`);
|
|
43
|
+
const absolutePath = resolveRelativeMdPath(knowledgeRoot, relativePath);
|
|
44
|
+
if (!absolutePath) {
|
|
45
|
+
throw new Error('path escapes docs/');
|
|
46
|
+
}
|
|
47
|
+
if (fs.existsSync(absolutePath)) {
|
|
48
|
+
throw new Error(`already exists: ${relativePath}`);
|
|
49
|
+
}
|
|
50
|
+
const relativeNorm = normalizeRelativePath(relativePath);
|
|
51
|
+
const type = inferTypeFromParent(parentPath);
|
|
52
|
+
const slug = slugFromRelativePath(relativeNorm);
|
|
53
|
+
const domain = resolveDomain(relativeNorm, undefined);
|
|
54
|
+
const pageTitle = options.title?.trim() || baseName.replace(/[-_]/g, ' ');
|
|
55
|
+
let body = `# ${pageTitle}\n\n`;
|
|
56
|
+
if (options.templateId) {
|
|
57
|
+
const template = readPageTemplate(options.templateId);
|
|
58
|
+
body = template.body || body;
|
|
59
|
+
if (!options.title?.trim() && template.title) {
|
|
60
|
+
options.title = template.title;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const resolvedTitle = options.title?.trim() || pageTitle;
|
|
64
|
+
const frontmatter = {
|
|
65
|
+
type,
|
|
66
|
+
slug,
|
|
67
|
+
status: 'draft',
|
|
68
|
+
domain,
|
|
69
|
+
title: resolvedTitle,
|
|
70
|
+
tags: [],
|
|
71
|
+
related: [],
|
|
72
|
+
updated: new Date().toISOString().slice(0, 10),
|
|
73
|
+
};
|
|
74
|
+
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
75
|
+
fs.writeFileSync(absolutePath, matter.stringify(body, frontmatter), 'utf8');
|
|
76
|
+
index.refresh();
|
|
77
|
+
return {
|
|
78
|
+
relativePath: normalizeRelativePath(relativePath),
|
|
79
|
+
absolutePath,
|
|
80
|
+
slug,
|
|
81
|
+
type,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function readPageSlug(absolutePath) {
|
|
85
|
+
const raw = fs.readFileSync(absolutePath, 'utf8');
|
|
86
|
+
const parsed = matter(raw);
|
|
87
|
+
const slug = parsed.data.slug;
|
|
88
|
+
if (typeof slug === 'string' && slug.trim()) {
|
|
89
|
+
return slug.trim();
|
|
90
|
+
}
|
|
91
|
+
const relative = path.basename(absolutePath, '.md');
|
|
92
|
+
return slugFromRelativePath(`${relative}.md`);
|
|
93
|
+
}
|
|
94
|
+
function collectInboundWarnings(index, slug) {
|
|
95
|
+
const docs = index.refresh();
|
|
96
|
+
const snapshot = buildLinkIndexForDocs(index);
|
|
97
|
+
const docsBySlug = new Map(docs.map((doc) => [doc.slug, doc]));
|
|
98
|
+
return getBacklinksForSlug(snapshot, slug, docsBySlug).map((entry) => ({
|
|
99
|
+
slug: entry.slug,
|
|
100
|
+
title: entry.title,
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
function rewritePageFrontmatter(absolutePath, newRelativePath) {
|
|
104
|
+
const raw = fs.readFileSync(absolutePath, 'utf8');
|
|
105
|
+
const parsed = matter(raw);
|
|
106
|
+
const parentPath = parentRelativePath(newRelativePath);
|
|
107
|
+
const newSlug = slugFromRelativePath(newRelativePath);
|
|
108
|
+
const newType = inferTypeFromParent(parentPath);
|
|
109
|
+
const nextData = {
|
|
110
|
+
...parsed.data,
|
|
111
|
+
slug: newSlug,
|
|
112
|
+
type: newType,
|
|
113
|
+
updated: new Date().toISOString().slice(0, 10),
|
|
114
|
+
};
|
|
115
|
+
fs.writeFileSync(absolutePath, matter.stringify(parsed.content, nextData), 'utf8');
|
|
116
|
+
return { newSlug, newType };
|
|
117
|
+
}
|
|
118
|
+
export function movePageFile(index, fromPath, toDir) {
|
|
119
|
+
const knowledgeRoot = index.getKnowledgeRoot();
|
|
120
|
+
if (!knowledgeRoot) {
|
|
121
|
+
throw new Error('no docs/ directory found');
|
|
122
|
+
}
|
|
123
|
+
const normalizedFrom = normalizeRelativePath(fromPath);
|
|
124
|
+
const sourceAbsolute = resolveRelativeMdPath(knowledgeRoot, normalizedFrom);
|
|
125
|
+
if (!sourceAbsolute || !fs.existsSync(sourceAbsolute)) {
|
|
126
|
+
throw new Error(`page not found: ${fromPath}`);
|
|
127
|
+
}
|
|
128
|
+
const destDirRelative = normalizeRelativePath(toDir);
|
|
129
|
+
const destDirAbsolute = resolveUnderKnowledgeRoot(knowledgeRoot, destDirRelative);
|
|
130
|
+
if (!destDirAbsolute || !fs.existsSync(destDirAbsolute)) {
|
|
131
|
+
throw new Error(`destination folder not found: ${toDir}`);
|
|
132
|
+
}
|
|
133
|
+
if (!fs.statSync(destDirAbsolute).isDirectory()) {
|
|
134
|
+
throw new Error(`destination is not a folder: ${toDir}`);
|
|
135
|
+
}
|
|
136
|
+
const fileName = path.basename(sourceAbsolute);
|
|
137
|
+
const destRelative = joinRelativePath(destDirRelative, fileName);
|
|
138
|
+
const destAbsolute = resolveRelativeMdPath(knowledgeRoot, destRelative);
|
|
139
|
+
if (!destAbsolute) {
|
|
140
|
+
throw new Error('path escapes docs/');
|
|
141
|
+
}
|
|
142
|
+
if (fs.existsSync(destAbsolute)) {
|
|
143
|
+
throw new Error(`already exists: ${destRelative}`);
|
|
144
|
+
}
|
|
145
|
+
const previousSlug = readPageSlug(sourceAbsolute);
|
|
146
|
+
const inboundWarnings = collectInboundWarnings(index, previousSlug);
|
|
147
|
+
fs.mkdirSync(path.dirname(destAbsolute), { recursive: true });
|
|
148
|
+
fs.renameSync(sourceAbsolute, destAbsolute);
|
|
149
|
+
const { newSlug } = rewritePageFrontmatter(destAbsolute, destRelative);
|
|
150
|
+
let cascadeUpdated = [];
|
|
151
|
+
if (newSlug !== previousSlug) {
|
|
152
|
+
cascadeUpdated = toRelativePaths(knowledgeRoot, cascadeSlugRename(knowledgeRoot, previousSlug, newSlug, {
|
|
153
|
+
excludeAbsolutePath: destAbsolute,
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
index.refresh();
|
|
157
|
+
return {
|
|
158
|
+
relativePath: normalizeRelativePath(destRelative),
|
|
159
|
+
absolutePath: destAbsolute,
|
|
160
|
+
slug: newSlug,
|
|
161
|
+
previousSlug,
|
|
162
|
+
slugChanged: newSlug !== previousSlug,
|
|
163
|
+
inboundWarnings: previousSlug !== newSlug ? inboundWarnings : [],
|
|
164
|
+
cascadeUpdated,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
export function renamePageFile(index, pagePath, newName) {
|
|
168
|
+
const knowledgeRoot = index.getKnowledgeRoot();
|
|
169
|
+
if (!knowledgeRoot) {
|
|
170
|
+
throw new Error('no docs/ directory found');
|
|
171
|
+
}
|
|
172
|
+
const baseName = newName.endsWith('.md') ? newName.slice(0, -3) : newName;
|
|
173
|
+
if (!isValidFsName(baseName)) {
|
|
174
|
+
throw new Error(`invalid page name: ${newName}`);
|
|
175
|
+
}
|
|
176
|
+
const normalizedFrom = normalizeRelativePath(pagePath);
|
|
177
|
+
const sourceAbsolute = resolveRelativeMdPath(knowledgeRoot, normalizedFrom);
|
|
178
|
+
if (!sourceAbsolute || !fs.existsSync(sourceAbsolute)) {
|
|
179
|
+
throw new Error(`page not found: ${pagePath}`);
|
|
180
|
+
}
|
|
181
|
+
const parentPath = parentRelativePath(normalizedFrom);
|
|
182
|
+
const destRelative = joinRelativePath(parentPath, `${baseName}.md`);
|
|
183
|
+
const destAbsolute = resolveRelativeMdPath(knowledgeRoot, destRelative);
|
|
184
|
+
if (!destAbsolute) {
|
|
185
|
+
throw new Error('path escapes docs/');
|
|
186
|
+
}
|
|
187
|
+
if (destAbsolute === sourceAbsolute) {
|
|
188
|
+
throw new Error('new name matches current name');
|
|
189
|
+
}
|
|
190
|
+
if (fs.existsSync(destAbsolute)) {
|
|
191
|
+
throw new Error(`already exists: ${destRelative}`);
|
|
192
|
+
}
|
|
193
|
+
const previousSlug = readPageSlug(sourceAbsolute);
|
|
194
|
+
const inboundWarnings = collectInboundWarnings(index, previousSlug);
|
|
195
|
+
fs.renameSync(sourceAbsolute, destAbsolute);
|
|
196
|
+
const { newSlug } = rewritePageFrontmatter(destAbsolute, destRelative);
|
|
197
|
+
let cascadeUpdated = [];
|
|
198
|
+
if (newSlug !== previousSlug) {
|
|
199
|
+
cascadeUpdated = toRelativePaths(knowledgeRoot, cascadeSlugRename(knowledgeRoot, previousSlug, newSlug, {
|
|
200
|
+
excludeAbsolutePath: destAbsolute,
|
|
201
|
+
}));
|
|
202
|
+
}
|
|
203
|
+
index.refresh();
|
|
204
|
+
return {
|
|
205
|
+
relativePath: normalizeRelativePath(destRelative),
|
|
206
|
+
absolutePath: destAbsolute,
|
|
207
|
+
slug: newSlug,
|
|
208
|
+
previousSlug,
|
|
209
|
+
slugChanged: newSlug !== previousSlug,
|
|
210
|
+
inboundWarnings: previousSlug !== newSlug ? inboundWarnings : [],
|
|
211
|
+
cascadeUpdated,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function listMarkdownFilesRecursive(absoluteDir) {
|
|
215
|
+
const files = [];
|
|
216
|
+
if (!fs.existsSync(absoluteDir)) {
|
|
217
|
+
return files;
|
|
218
|
+
}
|
|
219
|
+
for (const entry of fs.readdirSync(absoluteDir, { withFileTypes: true })) {
|
|
220
|
+
if (entry.name === '.repo-mind' || entry.name === '.worktrees') {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const fullPath = path.join(absoluteDir, entry.name);
|
|
224
|
+
if (entry.isDirectory()) {
|
|
225
|
+
files.push(...listMarkdownFilesRecursive(fullPath));
|
|
226
|
+
}
|
|
227
|
+
else if (entry.isFile() && entry.name.toLowerCase().endsWith('.md')) {
|
|
228
|
+
files.push(fullPath);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return files;
|
|
232
|
+
}
|
|
233
|
+
function mergeInboundWarnings(lists) {
|
|
234
|
+
const seen = new Set();
|
|
235
|
+
const merged = [];
|
|
236
|
+
for (const list of lists) {
|
|
237
|
+
for (const item of list) {
|
|
238
|
+
if (seen.has(item.slug)) {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
seen.add(item.slug);
|
|
242
|
+
merged.push(item);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return merged;
|
|
246
|
+
}
|
|
247
|
+
export function deletePageFile(index, pagePath) {
|
|
248
|
+
const knowledgeRoot = index.getKnowledgeRoot();
|
|
249
|
+
if (!knowledgeRoot) {
|
|
250
|
+
throw new Error('no docs/ directory found');
|
|
251
|
+
}
|
|
252
|
+
const normalized = normalizeRelativePath(pagePath);
|
|
253
|
+
const absolutePath = resolveRelativeMdPath(knowledgeRoot, normalized);
|
|
254
|
+
if (!absolutePath || !fs.existsSync(absolutePath)) {
|
|
255
|
+
throw new Error(`page not found: ${pagePath}`);
|
|
256
|
+
}
|
|
257
|
+
const slug = readPageSlug(absolutePath);
|
|
258
|
+
const inboundWarnings = collectInboundWarnings(index, slug);
|
|
259
|
+
const cascadeUpdated = toRelativePaths(knowledgeRoot, cascadeSlugDelete(knowledgeRoot, slug, { excludeAbsolutePaths: [absolutePath] }));
|
|
260
|
+
fs.unlinkSync(absolutePath);
|
|
261
|
+
index.refresh();
|
|
262
|
+
return {
|
|
263
|
+
relativePath: normalized,
|
|
264
|
+
slug,
|
|
265
|
+
inboundWarnings,
|
|
266
|
+
cascadeUpdated,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
export function deleteFolder(index, folderPath) {
|
|
270
|
+
const knowledgeRoot = index.getKnowledgeRoot();
|
|
271
|
+
if (!knowledgeRoot) {
|
|
272
|
+
throw new Error('no docs/ directory found');
|
|
273
|
+
}
|
|
274
|
+
const normalized = normalizeRelativePath(folderPath);
|
|
275
|
+
if (!normalized) {
|
|
276
|
+
throw new Error('cannot delete docs root');
|
|
277
|
+
}
|
|
278
|
+
const absolutePath = resolveUnderKnowledgeRoot(knowledgeRoot, normalized);
|
|
279
|
+
if (!absolutePath || !fs.existsSync(absolutePath)) {
|
|
280
|
+
throw new Error(`folder not found: ${folderPath}`);
|
|
281
|
+
}
|
|
282
|
+
if (!fs.statSync(absolutePath).isDirectory()) {
|
|
283
|
+
throw new Error(`not a folder: ${folderPath}`);
|
|
284
|
+
}
|
|
285
|
+
const markdownFiles = listMarkdownFilesRecursive(absolutePath);
|
|
286
|
+
const deletedSlugs = markdownFiles.map((filePath) => readPageSlug(filePath));
|
|
287
|
+
const inboundWarnings = mergeInboundWarnings(deletedSlugs.map((slug) => collectInboundWarnings(index, slug)));
|
|
288
|
+
const cascadeUpdated = new Set();
|
|
289
|
+
for (const deletedSlug of deletedSlugs) {
|
|
290
|
+
for (const updatedPath of cascadeSlugDelete(knowledgeRoot, deletedSlug, {
|
|
291
|
+
excludeAbsolutePaths: markdownFiles,
|
|
292
|
+
})) {
|
|
293
|
+
cascadeUpdated.add(updatedPath);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
fs.rmSync(absolutePath, { recursive: true, force: true });
|
|
297
|
+
index.refresh();
|
|
298
|
+
return {
|
|
299
|
+
relativePath: normalized,
|
|
300
|
+
deletedSlugs,
|
|
301
|
+
inboundWarnings,
|
|
302
|
+
cascadeUpdated: toRelativePaths(knowledgeRoot, [...cascadeUpdated]),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { DocIndex } from '../index/doc-index.js';
|
|
2
|
+
import type { LinkEdge } from '../index/link-index.js';
|
|
3
|
+
export interface TreePageNode {
|
|
4
|
+
kind: 'page';
|
|
5
|
+
name: string;
|
|
6
|
+
relativePath: string;
|
|
7
|
+
slug: string;
|
|
8
|
+
title: string;
|
|
9
|
+
status: string;
|
|
10
|
+
type: string;
|
|
11
|
+
contentKind: 'markdown' | 'yaml' | 'json';
|
|
12
|
+
}
|
|
13
|
+
export interface TreeFolderNode {
|
|
14
|
+
kind: 'folder';
|
|
15
|
+
name: string;
|
|
16
|
+
relativePath: string;
|
|
17
|
+
emoji: string | null;
|
|
18
|
+
/** Slug of README.md index page for this folder. */
|
|
19
|
+
indexPageSlug: string | null;
|
|
20
|
+
children: TreeNode[];
|
|
21
|
+
}
|
|
22
|
+
export type TreeNode = TreePageNode | TreeFolderNode;
|
|
23
|
+
export declare function readmeIndexRelativePath(folderRelativePath: string): string;
|
|
24
|
+
/** Human label for domain folders at `docs/{domain}/`; otherwise basename. */
|
|
25
|
+
export declare function folderDisplayName(relativePath: string): string;
|
|
26
|
+
export declare function buildDocsTree(index: DocIndex): TreeFolderNode | null;
|
|
27
|
+
export declare function findTreePageSlug(node: TreeFolderNode, relativePath: string): string | null;
|
|
28
|
+
export declare function collectParentOfEdges(tree: TreeFolderNode | null): LinkEdge[];
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { isKnowledgeFileName } from '../index/knowledge-file.js';
|
|
4
|
+
import { DOC_DOMAINS, DOMAIN_LABELS } from '../index/types.js';
|
|
5
|
+
import { catalogEmoji, readCatalogMeta } from './catalog-meta.js';
|
|
6
|
+
import { joinRelativePath, normalizeRelativePath } from './safe-path.js';
|
|
7
|
+
const IGNORED_DIRS = new Set(['.repo-mind', '.worktrees']);
|
|
8
|
+
function knowledgeFileDisplayName(fileName) {
|
|
9
|
+
return fileName.replace(/\.(md|ya?ml|json)$/i, '');
|
|
10
|
+
}
|
|
11
|
+
function listDirEntries(dirPath) {
|
|
12
|
+
if (!fs.existsSync(dirPath)) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
return fs
|
|
16
|
+
.readdirSync(dirPath, { withFileTypes: true })
|
|
17
|
+
.filter((entry) => !entry.name.startsWith('.'))
|
|
18
|
+
.map((entry) => ({ name: entry.name, isDirectory: entry.isDirectory() }));
|
|
19
|
+
}
|
|
20
|
+
export function readmeIndexRelativePath(folderRelativePath) {
|
|
21
|
+
const base = normalizeRelativePath(folderRelativePath);
|
|
22
|
+
return base ? `${base}/README.md` : 'README.md';
|
|
23
|
+
}
|
|
24
|
+
function isDocDomain(name) {
|
|
25
|
+
return DOC_DOMAINS.includes(name);
|
|
26
|
+
}
|
|
27
|
+
/** Human label for domain folders at `docs/{domain}/`; otherwise basename. */
|
|
28
|
+
export function folderDisplayName(relativePath) {
|
|
29
|
+
if (!relativePath) {
|
|
30
|
+
return 'Knowledge';
|
|
31
|
+
}
|
|
32
|
+
const segments = normalizeRelativePath(relativePath).split('/').filter(Boolean);
|
|
33
|
+
if (segments.length === 1 && isDocDomain(segments[0])) {
|
|
34
|
+
return DOMAIN_LABELS[segments[0]];
|
|
35
|
+
}
|
|
36
|
+
return path.basename(relativePath);
|
|
37
|
+
}
|
|
38
|
+
function buildFolder(relativePath, absPath, ctx) {
|
|
39
|
+
const entries = listDirEntries(absPath);
|
|
40
|
+
const children = [];
|
|
41
|
+
const readmeRel = readmeIndexRelativePath(relativePath);
|
|
42
|
+
let indexPageSlug = null;
|
|
43
|
+
const readmeDoc = ctx.docsByRelative.get(readmeRel);
|
|
44
|
+
if (readmeDoc) {
|
|
45
|
+
indexPageSlug = readmeDoc.slug;
|
|
46
|
+
}
|
|
47
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
48
|
+
if (entry.isDirectory) {
|
|
49
|
+
if (IGNORED_DIRS.has(entry.name)) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const childRel = joinRelativePath(relativePath, entry.name);
|
|
53
|
+
children.push(buildFolder(childRel, path.join(absPath, entry.name), ctx));
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (!isKnowledgeFileName(entry.name)) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const fileRel = joinRelativePath(relativePath, entry.name);
|
|
60
|
+
if (fileRel === readmeRel && indexPageSlug) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const doc = ctx.docsByRelative.get(fileRel);
|
|
64
|
+
if (!doc) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
children.push({
|
|
68
|
+
kind: 'page',
|
|
69
|
+
name: knowledgeFileDisplayName(entry.name),
|
|
70
|
+
relativePath: fileRel,
|
|
71
|
+
slug: doc.slug,
|
|
72
|
+
title: doc.title,
|
|
73
|
+
status: doc.status,
|
|
74
|
+
type: doc.type,
|
|
75
|
+
contentKind: doc.contentKind,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
kind: 'folder',
|
|
80
|
+
name: folderDisplayName(relativePath),
|
|
81
|
+
relativePath,
|
|
82
|
+
emoji: catalogEmoji(ctx.meta, relativePath),
|
|
83
|
+
indexPageSlug,
|
|
84
|
+
children,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export function buildDocsTree(index) {
|
|
88
|
+
const knowledgeRoot = index.getKnowledgeRoot();
|
|
89
|
+
if (!knowledgeRoot) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const docsByRelative = new Map();
|
|
93
|
+
for (const doc of index.refresh()) {
|
|
94
|
+
docsByRelative.set(doc.relativePath, {
|
|
95
|
+
slug: doc.slug,
|
|
96
|
+
title: doc.title,
|
|
97
|
+
status: doc.status,
|
|
98
|
+
type: doc.type,
|
|
99
|
+
contentKind: doc.contentKind,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
const ctx = {
|
|
103
|
+
docsByRelative,
|
|
104
|
+
meta: readCatalogMeta(knowledgeRoot),
|
|
105
|
+
};
|
|
106
|
+
return buildFolder('', knowledgeRoot, ctx);
|
|
107
|
+
}
|
|
108
|
+
export function findTreePageSlug(node, relativePath) {
|
|
109
|
+
const normalized = normalizeRelativePath(relativePath);
|
|
110
|
+
for (const child of node.children) {
|
|
111
|
+
if (child.kind === 'page' && child.relativePath === normalized) {
|
|
112
|
+
return child.slug;
|
|
113
|
+
}
|
|
114
|
+
if (child.kind === 'folder') {
|
|
115
|
+
const found = findTreePageSlug(child, normalized);
|
|
116
|
+
if (found) {
|
|
117
|
+
return found;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
export function collectParentOfEdges(tree) {
|
|
124
|
+
if (!tree) {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
const edges = [];
|
|
128
|
+
function walk(folder) {
|
|
129
|
+
if (folder.indexPageSlug) {
|
|
130
|
+
for (const child of folder.children ?? []) {
|
|
131
|
+
if (child.kind === 'page' && child.slug !== folder.indexPageSlug) {
|
|
132
|
+
edges.push({
|
|
133
|
+
from: folder.indexPageSlug,
|
|
134
|
+
to: child.slug,
|
|
135
|
+
kind: 'parent_of',
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
for (const child of folder.children ?? []) {
|
|
141
|
+
if (child.kind === 'folder') {
|
|
142
|
+
walk(child);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
walk(tree);
|
|
147
|
+
return edges;
|
|
148
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { DocIndex } from '../index/doc-index.js';
|
|
2
|
+
import { type ExploreGraphResult } from '../tools/explore-graph.js';
|
|
3
|
+
export declare const ALL_GRAPH_SLUG = "__all__";
|
|
4
|
+
export declare const MAX_ALL_GRAPH_NODES = 200;
|
|
5
|
+
export declare function exploreGraphAll(index: DocIndex): ExploreGraphResult;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { buildLinkIndexForDocs } from '../tools/explore-graph.js';
|
|
2
|
+
export const ALL_GRAPH_SLUG = '__all__';
|
|
3
|
+
export const MAX_ALL_GRAPH_NODES = 200;
|
|
4
|
+
function edgesForLimitedSlugs(linkIndex, limitedSlugs) {
|
|
5
|
+
const edgeKeys = new Set();
|
|
6
|
+
const edges = [];
|
|
7
|
+
for (const edge of linkIndex.edges) {
|
|
8
|
+
if (!limitedSlugs.has(edge.from) || !limitedSlugs.has(edge.to)) {
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
const key = `${edge.from}\0${edge.to}`;
|
|
12
|
+
if (edgeKeys.has(key)) {
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
edgeKeys.add(key);
|
|
16
|
+
edges.push({ from: edge.from, to: edge.to });
|
|
17
|
+
}
|
|
18
|
+
return edges;
|
|
19
|
+
}
|
|
20
|
+
export function exploreGraphAll(index) {
|
|
21
|
+
const empty = {
|
|
22
|
+
nodes: [],
|
|
23
|
+
edges: [],
|
|
24
|
+
maxDepthReached: 0,
|
|
25
|
+
truncated: false,
|
|
26
|
+
broken_links: [],
|
|
27
|
+
};
|
|
28
|
+
if (!index.getKnowledgeRoot()) {
|
|
29
|
+
return empty;
|
|
30
|
+
}
|
|
31
|
+
const docs = index.refresh();
|
|
32
|
+
const linkIndex = buildLinkIndexForDocs(index);
|
|
33
|
+
let truncated = false;
|
|
34
|
+
const limitedDocs = docs.length > MAX_ALL_GRAPH_NODES ? docs.slice(0, MAX_ALL_GRAPH_NODES) : docs;
|
|
35
|
+
if (docs.length > MAX_ALL_GRAPH_NODES) {
|
|
36
|
+
truncated = true;
|
|
37
|
+
}
|
|
38
|
+
const limitedSlugs = new Set(limitedDocs.map((doc) => doc.slug));
|
|
39
|
+
return {
|
|
40
|
+
nodes: limitedDocs.map((doc) => ({
|
|
41
|
+
slug: doc.slug,
|
|
42
|
+
type: doc.type,
|
|
43
|
+
title: doc.title,
|
|
44
|
+
})),
|
|
45
|
+
edges: edgesForLimitedSlugs(linkIndex, limitedSlugs),
|
|
46
|
+
maxDepthReached: 0,
|
|
47
|
+
truncated,
|
|
48
|
+
broken_links: [...linkIndex.brokenTargets],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Rewrites inbound wikilinks and related frontmatter after a slug rename. */
|
|
2
|
+
export declare function cascadeSlugRename(knowledgeRoot: string, fromSlug: string, toSlug: string, options?: {
|
|
3
|
+
excludeAbsolutePath?: string;
|
|
4
|
+
}): string[];
|
|
5
|
+
/** Removes inbound references to a deleted slug. */
|
|
6
|
+
export declare function cascadeSlugDelete(knowledgeRoot: string, deletedSlug: string, options?: {
|
|
7
|
+
excludeAbsolutePaths?: string[];
|
|
8
|
+
}): string[];
|
|
9
|
+
export declare function toRelativePaths(knowledgeRoot: string, absolutePaths: string[]): string[];
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import matter from 'gray-matter';
|
|
3
|
+
import { listMarkdownFiles } from '../index/doc-index.js';
|
|
4
|
+
const WIKILINK_PATTERN = /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g;
|
|
5
|
+
function normalizeStringArray(value) {
|
|
6
|
+
if (!Array.isArray(value)) {
|
|
7
|
+
return [];
|
|
8
|
+
}
|
|
9
|
+
return value.filter((item) => typeof item === 'string');
|
|
10
|
+
}
|
|
11
|
+
function replaceWikilinksInBody(body, fromSlug, toSlug) {
|
|
12
|
+
return body.replace(WIKILINK_PATTERN, (full, display, slugPart) => {
|
|
13
|
+
const target = (slugPart ?? display).trim();
|
|
14
|
+
if (target !== fromSlug) {
|
|
15
|
+
return full;
|
|
16
|
+
}
|
|
17
|
+
const label = display.trim();
|
|
18
|
+
if (slugPart) {
|
|
19
|
+
return `[[${label}|${toSlug}]]`;
|
|
20
|
+
}
|
|
21
|
+
if (label === fromSlug) {
|
|
22
|
+
return `[[${toSlug}]]`;
|
|
23
|
+
}
|
|
24
|
+
return `[[${label}|${toSlug}]]`;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function removeWikilinksInBody(body, slug) {
|
|
28
|
+
return body.replace(WIKILINK_PATTERN, (full, display, slugPart) => {
|
|
29
|
+
const target = (slugPart ?? display).trim();
|
|
30
|
+
if (target !== slug) {
|
|
31
|
+
return full;
|
|
32
|
+
}
|
|
33
|
+
const label = display.trim();
|
|
34
|
+
return label || slug;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function replaceInRelated(related, fromSlug, toSlug) {
|
|
38
|
+
const seen = new Set();
|
|
39
|
+
const result = [];
|
|
40
|
+
for (const entry of related) {
|
|
41
|
+
const next = entry === fromSlug ? toSlug : entry;
|
|
42
|
+
if (seen.has(next)) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
seen.add(next);
|
|
46
|
+
result.push(next);
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
function removeFromRelated(related, slug) {
|
|
51
|
+
return related.filter((entry) => entry !== slug);
|
|
52
|
+
}
|
|
53
|
+
function updateMarkdownFile(absolutePath, mutate) {
|
|
54
|
+
const raw = fs.readFileSync(absolutePath, 'utf8');
|
|
55
|
+
const parsed = matter(raw);
|
|
56
|
+
const data = parsed.data;
|
|
57
|
+
const related = normalizeStringArray(data.related);
|
|
58
|
+
const next = mutate(parsed.content, related);
|
|
59
|
+
const bodyChanged = next.body !== parsed.content;
|
|
60
|
+
const relatedChanged = next.related.length !== related.length || next.related.some((slug, index) => slug !== related[index]);
|
|
61
|
+
if (!bodyChanged && !relatedChanged) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
fs.writeFileSync(absolutePath, matter.stringify(next.body, {
|
|
65
|
+
...data,
|
|
66
|
+
related: next.related,
|
|
67
|
+
}), 'utf8');
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
/** Rewrites inbound wikilinks and related frontmatter after a slug rename. */
|
|
71
|
+
export function cascadeSlugRename(knowledgeRoot, fromSlug, toSlug, options = {}) {
|
|
72
|
+
if (fromSlug === toSlug) {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
const updated = [];
|
|
76
|
+
for (const absolutePath of listMarkdownFiles(knowledgeRoot)) {
|
|
77
|
+
if (options.excludeAbsolutePath && absolutePath === options.excludeAbsolutePath) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const changed = updateMarkdownFile(absolutePath, (body, related) => ({
|
|
81
|
+
body: replaceWikilinksInBody(body, fromSlug, toSlug),
|
|
82
|
+
related: replaceInRelated(related, fromSlug, toSlug),
|
|
83
|
+
}));
|
|
84
|
+
if (changed) {
|
|
85
|
+
updated.push(absolutePath);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return updated;
|
|
89
|
+
}
|
|
90
|
+
/** Removes inbound references to a deleted slug. */
|
|
91
|
+
export function cascadeSlugDelete(knowledgeRoot, deletedSlug, options = {}) {
|
|
92
|
+
const excluded = new Set(options.excludeAbsolutePaths ?? []);
|
|
93
|
+
const updated = [];
|
|
94
|
+
for (const absolutePath of listMarkdownFiles(knowledgeRoot)) {
|
|
95
|
+
if (excluded.has(absolutePath)) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const changed = updateMarkdownFile(absolutePath, (body, related) => ({
|
|
99
|
+
body: removeWikilinksInBody(body, deletedSlug),
|
|
100
|
+
related: removeFromRelated(related, deletedSlug),
|
|
101
|
+
}));
|
|
102
|
+
if (changed) {
|
|
103
|
+
updated.push(absolutePath);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return updated;
|
|
107
|
+
}
|
|
108
|
+
export function toRelativePaths(knowledgeRoot, absolutePaths) {
|
|
109
|
+
const rootWithSep = knowledgeRoot.endsWith('/') ? knowledgeRoot : `${knowledgeRoot}/`;
|
|
110
|
+
return absolutePaths.map((absolutePath) => absolutePath.startsWith(rootWithSep)
|
|
111
|
+
? absolutePath.slice(rootWithSep.length).replace(/\\/g, '/')
|
|
112
|
+
: absolutePath);
|
|
113
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface MultipartFilePart {
|
|
2
|
+
fieldName: string;
|
|
3
|
+
fileName: string;
|
|
4
|
+
mimeType: string;
|
|
5
|
+
data: Buffer;
|
|
6
|
+
}
|
|
7
|
+
export interface ParsedMultipart {
|
|
8
|
+
fields: Map<string, string>;
|
|
9
|
+
files: Map<string, MultipartFilePart>;
|
|
10
|
+
}
|
|
11
|
+
/** Minimal multipart/form-data parser for single-file uploads. */
|
|
12
|
+
export declare function parseMultipartFormData(body: Buffer, contentType: string): ParsedMultipart;
|