@anokye-labs/kbexplorer-engine 0.1.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/LICENSE +21 -0
- package/README.md +77 -0
- package/dist/build-manifest-BT0sY84J.d.ts +239 -0
- package/dist/build-manifest-BkgZUL0E.d.cts +239 -0
- package/dist/chunk-ASLGNOYV.js +220 -0
- package/dist/chunk-BA526FDQ.js +16 -0
- package/dist/chunk-DJ2DBEEK.js +424 -0
- package/dist/chunk-FJ4GTN4U.js +973 -0
- package/dist/chunk-HXAS3BSD.js +187 -0
- package/dist/chunk-IWEURXYH.js +273 -0
- package/dist/chunk-JNQVSNLC.js +55 -0
- package/dist/chunk-MW4BLXVK.js +72 -0
- package/dist/chunk-NOGBKE7C.js +351 -0
- package/dist/chunk-XVI6CBSX.js +412 -0
- package/dist/chunk-YTMIERYM.js +67 -0
- package/dist/github-api-source-KV5HZARH.js +4 -0
- package/dist/index.cjs +6218 -0
- package/dist/index.d.cts +1396 -0
- package/dist/index.d.ts +1396 -0
- package/dist/index.js +2892 -0
- package/dist/node-wasm-VSXBOCPD.js +9 -0
- package/dist/plugin-loader-K4VU6ODV.js +2 -0
- package/dist/repo-data-0JvFdLGv.d.cts +461 -0
- package/dist/repo-data-0JvFdLGv.d.ts +461 -0
- package/dist/sources.cjs +1191 -0
- package/dist/sources.d.cts +194 -0
- package/dist/sources.d.ts +194 -0
- package/dist/sources.js +357 -0
- package/dist/sqlite-graph-store-NH2NHL2B.js +2 -0
- package/dist/sqlite-runtime-CysPjjzX.d.cts +96 -0
- package/dist/sqlite-runtime-CysPjjzX.d.ts +96 -0
- package/dist/store-orchestrator-337XWIXO.js +5 -0
- package/dist/store.cjs +1112 -0
- package/dist/store.d.cts +37 -0
- package/dist/store.d.ts +37 -0
- package/dist/store.js +7 -0
- package/package.json +70 -0
package/dist/sources.cjs
ADDED
|
@@ -0,0 +1,1191 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var kbexplorerCore = require('@anokye-labs/kbexplorer-core');
|
|
4
|
+
var marked = require('marked');
|
|
5
|
+
require('sanitize-html');
|
|
6
|
+
var yaml = require('yaml');
|
|
7
|
+
var promises = require('fs/promises');
|
|
8
|
+
var path = require('path');
|
|
9
|
+
|
|
10
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
11
|
+
|
|
12
|
+
var yaml__default = /*#__PURE__*/_interopDefault(yaml);
|
|
13
|
+
|
|
14
|
+
// src/glob.ts
|
|
15
|
+
function globToRegex(pattern) {
|
|
16
|
+
let re = "";
|
|
17
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
18
|
+
const c = pattern[i];
|
|
19
|
+
if (c === "*" && pattern[i + 1] === "*") {
|
|
20
|
+
re += ".*";
|
|
21
|
+
i += 1;
|
|
22
|
+
if (pattern[i + 1] === "/") i += 1;
|
|
23
|
+
} else if (c === "*") {
|
|
24
|
+
re += "[^/]*";
|
|
25
|
+
} else if (c === "?") {
|
|
26
|
+
re += "[^/]";
|
|
27
|
+
} else if (".+^${}()|[]\\".includes(c)) {
|
|
28
|
+
re += "\\" + c;
|
|
29
|
+
} else {
|
|
30
|
+
re += c;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return new RegExp(`^${re}$`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/sources/manifest-source.ts
|
|
37
|
+
var ManifestSource = class {
|
|
38
|
+
id = "manifest";
|
|
39
|
+
name = "Repo Manifest";
|
|
40
|
+
/** A manifest is a frozen snapshot — read is the only possible affordance. */
|
|
41
|
+
possibleAffordances = ["read"];
|
|
42
|
+
manifest;
|
|
43
|
+
config;
|
|
44
|
+
constructor(manifest, config) {
|
|
45
|
+
this.manifest = manifest;
|
|
46
|
+
this.config = config;
|
|
47
|
+
}
|
|
48
|
+
async getRepoData() {
|
|
49
|
+
const manifest = this.manifest;
|
|
50
|
+
const listFiles = async (pattern) => {
|
|
51
|
+
const regex = globToRegex(pattern);
|
|
52
|
+
return Object.keys(manifest.nodemapFiles ?? {}).filter((p) => regex.test(p));
|
|
53
|
+
};
|
|
54
|
+
const data = {
|
|
55
|
+
repo: this.config.source.repo,
|
|
56
|
+
tree: manifest.tree,
|
|
57
|
+
authoredContent: manifest.authoredContent,
|
|
58
|
+
nodemapRaw: manifest.nodemapRaw ?? null,
|
|
59
|
+
listFiles,
|
|
60
|
+
issues: manifest.issues,
|
|
61
|
+
pullRequests: manifest.pullRequests,
|
|
62
|
+
commits: manifest.commits,
|
|
63
|
+
branches: manifest.branches ?? [],
|
|
64
|
+
repoMetadata: manifest.repoMetadata ?? null,
|
|
65
|
+
releases: manifest.releases ?? [],
|
|
66
|
+
structuralFiles: manifest.structuralFiles ?? {},
|
|
67
|
+
structuredNodeMapRaw: manifest.structuredNodeMapRaw ?? null,
|
|
68
|
+
contentModel: manifest.contentModel ?? null,
|
|
69
|
+
readme: manifest.readme,
|
|
70
|
+
themeFileRaw: manifest.themeFileRaw ?? null
|
|
71
|
+
};
|
|
72
|
+
if (manifest.nodemapFiles !== void 0) data.nodemapFiles = manifest.nodemapFiles;
|
|
73
|
+
if (manifest.nodemapDirs !== void 0) {
|
|
74
|
+
data.nodemapDirs = manifest.nodemapDirs;
|
|
75
|
+
}
|
|
76
|
+
return data;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Read-only resource surface. Files (tree blobs/trees) and issues are
|
|
80
|
+
* retrievable; every resource is afforded `['read']` and links only to
|
|
81
|
+
* itself. No write/stage, no staging area.
|
|
82
|
+
*/
|
|
83
|
+
async retrieve(query) {
|
|
84
|
+
const kind = query.kind;
|
|
85
|
+
const out = [];
|
|
86
|
+
if (!kind || kind === "file" || kind === "tree") {
|
|
87
|
+
for (const item of this.manifest.tree) {
|
|
88
|
+
if (kind === "file" && item.type !== "blob") continue;
|
|
89
|
+
if (kind === "tree" && item.type !== "tree") continue;
|
|
90
|
+
out.push(this.fileResource(item));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (!kind || kind === "issue") {
|
|
94
|
+
for (const issue of this.manifest.issues) {
|
|
95
|
+
out.push(this.issueResource(issue.number, issue.title));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
async get(href) {
|
|
101
|
+
const all = await this.retrieve({});
|
|
102
|
+
return all.find((r) => r.href === href);
|
|
103
|
+
}
|
|
104
|
+
fileResource(item) {
|
|
105
|
+
return {
|
|
106
|
+
href: `git://${this.config.source.repo}/${item.path}`,
|
|
107
|
+
kind: item.type === "tree" ? "tree" : "file",
|
|
108
|
+
affordances: ["read"],
|
|
109
|
+
links: [{ rel: "self", href: `git://${this.config.source.repo}/${item.path}` }],
|
|
110
|
+
body: { path: item.path }
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
issueResource(number, title) {
|
|
114
|
+
return {
|
|
115
|
+
href: `github://${this.config.source.repo}/issues/${number}`,
|
|
116
|
+
kind: "issue",
|
|
117
|
+
affordances: ["read"],
|
|
118
|
+
links: [{ rel: "self", href: `github://${this.config.source.repo}/issues/${number}` }],
|
|
119
|
+
body: { number, title }
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// src/github-client.ts
|
|
125
|
+
var GITHUB_ENDPOINT_PATTERNS = [
|
|
126
|
+
"contents/",
|
|
127
|
+
"git/trees/",
|
|
128
|
+
"issues",
|
|
129
|
+
"pulls",
|
|
130
|
+
"commits",
|
|
131
|
+
"releases",
|
|
132
|
+
"branches",
|
|
133
|
+
"languages"
|
|
134
|
+
];
|
|
135
|
+
var DEFAULT_GH_API_BASE = "https://api.github.com";
|
|
136
|
+
function resolveApiBase(env) {
|
|
137
|
+
return env?.VITE_GH_API_BASE ?? DEFAULT_GH_API_BASE;
|
|
138
|
+
}
|
|
139
|
+
async function ghFetch(path, env, etag) {
|
|
140
|
+
const headers = {
|
|
141
|
+
Accept: "application/vnd.github.v3+json"
|
|
142
|
+
};
|
|
143
|
+
const token = env?.GITHUB_TOKEN ?? env?.GH_TOKEN;
|
|
144
|
+
if (token) {
|
|
145
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
146
|
+
}
|
|
147
|
+
const res = await fetch(`${resolveApiBase(env)}${path}`, { headers });
|
|
148
|
+
if (res.status === 304) {
|
|
149
|
+
throw new NotModifiedError();
|
|
150
|
+
}
|
|
151
|
+
if (res.status === 403 && res.headers.get("X-RateLimit-Remaining") === "0") {
|
|
152
|
+
const reset = res.headers.get("X-RateLimit-Reset");
|
|
153
|
+
throw new RateLimitError(reset ? new Date(Number(reset) * 1e3) : void 0);
|
|
154
|
+
}
|
|
155
|
+
if (!res.ok) {
|
|
156
|
+
throw new GitHubApiError(res.status, await res.text());
|
|
157
|
+
}
|
|
158
|
+
const data = await res.json();
|
|
159
|
+
const responseEtag = res.headers.get("ETag");
|
|
160
|
+
return responseEtag ? { data, etag: responseEtag } : { data };
|
|
161
|
+
}
|
|
162
|
+
var NotModifiedError = class extends Error {
|
|
163
|
+
constructor() {
|
|
164
|
+
super("Not modified");
|
|
165
|
+
this.name = "NotModifiedError";
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
var RateLimitError = class extends Error {
|
|
169
|
+
resetAt;
|
|
170
|
+
constructor(resetAt) {
|
|
171
|
+
super(`GitHub API rate limit exceeded${resetAt ? `. Resets at ${resetAt.toISOString()}` : ""}`);
|
|
172
|
+
this.name = "RateLimitError";
|
|
173
|
+
if (resetAt) this.resetAt = resetAt;
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
var GitHubApiError = class extends Error {
|
|
177
|
+
status;
|
|
178
|
+
constructor(status, body) {
|
|
179
|
+
super(`GitHub API error ${status}: ${body}`);
|
|
180
|
+
this.name = "GitHubApiError";
|
|
181
|
+
this.status = status;
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
async function fetchFile(source, path, env, cache) {
|
|
185
|
+
const branch = source.branch ?? "main";
|
|
186
|
+
const key = `file:${source.owner}/${source.repo}:${path}`;
|
|
187
|
+
const hit = cache?.get(key);
|
|
188
|
+
if (hit !== void 0) return hit;
|
|
189
|
+
const { data } = await ghFetch(
|
|
190
|
+
`/repos/${source.owner}/${source.repo}/contents/${path}?ref=${branch}`,
|
|
191
|
+
env
|
|
192
|
+
);
|
|
193
|
+
const binary = atob(data.content);
|
|
194
|
+
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
195
|
+
const decoded = new TextDecoder().decode(bytes);
|
|
196
|
+
cache?.set(key, decoded);
|
|
197
|
+
return decoded;
|
|
198
|
+
}
|
|
199
|
+
async function fetchTree(source, path, env, cache) {
|
|
200
|
+
const branch = source.branch ?? "main";
|
|
201
|
+
const key = `tree:${source.owner}/${source.repo}:${path ?? ""}`;
|
|
202
|
+
const hit = cache?.get(key);
|
|
203
|
+
if (hit !== void 0) return hit;
|
|
204
|
+
const { data } = await ghFetch(
|
|
205
|
+
`/repos/${source.owner}/${source.repo}/git/trees/${branch}?recursive=1`,
|
|
206
|
+
env
|
|
207
|
+
);
|
|
208
|
+
const items = path ? data.tree.filter((item) => item.path.startsWith(path + "/")) : data.tree;
|
|
209
|
+
cache?.set(key, items);
|
|
210
|
+
return items;
|
|
211
|
+
}
|
|
212
|
+
async function fetchIssues(source, env, cache) {
|
|
213
|
+
const key = `issues:${source.owner}/${source.repo}`;
|
|
214
|
+
const hit = cache?.get(key);
|
|
215
|
+
if (hit !== void 0) return hit;
|
|
216
|
+
const allIssues = [];
|
|
217
|
+
let page = 1;
|
|
218
|
+
const perPage = 100;
|
|
219
|
+
while (true) {
|
|
220
|
+
const { data } = await ghFetch(
|
|
221
|
+
`/repos/${source.owner}/${source.repo}/issues?state=all&per_page=${perPage}&page=${page}`,
|
|
222
|
+
env
|
|
223
|
+
);
|
|
224
|
+
const issues = data.filter((i) => !i.pull_request);
|
|
225
|
+
allIssues.push(...issues);
|
|
226
|
+
if (data.length < perPage) break;
|
|
227
|
+
page++;
|
|
228
|
+
}
|
|
229
|
+
cache?.set(key, allIssues);
|
|
230
|
+
return allIssues;
|
|
231
|
+
}
|
|
232
|
+
async function fetchPullRequests(source, env, cache) {
|
|
233
|
+
const key = `prs:${source.owner}/${source.repo}`;
|
|
234
|
+
const hit = cache?.get(key);
|
|
235
|
+
if (hit !== void 0) return hit;
|
|
236
|
+
const allPRs = [];
|
|
237
|
+
let page = 1;
|
|
238
|
+
const perPage = 100;
|
|
239
|
+
while (true) {
|
|
240
|
+
const { data } = await ghFetch(
|
|
241
|
+
`/repos/${source.owner}/${source.repo}/pulls?state=all&per_page=${perPage}&page=${page}`,
|
|
242
|
+
env
|
|
243
|
+
);
|
|
244
|
+
allPRs.push(...data);
|
|
245
|
+
if (data.length < perPage) break;
|
|
246
|
+
page++;
|
|
247
|
+
}
|
|
248
|
+
cache?.set(key, allPRs);
|
|
249
|
+
return allPRs;
|
|
250
|
+
}
|
|
251
|
+
async function fetchCommits(source, count = 30, env, cache) {
|
|
252
|
+
const key = `commits:${source.owner}/${source.repo}`;
|
|
253
|
+
const hit = cache?.get(key);
|
|
254
|
+
if (hit !== void 0) return hit;
|
|
255
|
+
const branch = source.branch ?? "main";
|
|
256
|
+
const { data } = await ghFetch(
|
|
257
|
+
`/repos/${source.owner}/${source.repo}/commits?sha=${branch}&per_page=${count}`,
|
|
258
|
+
env
|
|
259
|
+
);
|
|
260
|
+
cache?.set(key, data);
|
|
261
|
+
return data;
|
|
262
|
+
}
|
|
263
|
+
async function fetchReleases(source, limit = 30, env, cache) {
|
|
264
|
+
const key = `releases:${source.owner}/${source.repo}`;
|
|
265
|
+
const hit = cache?.get(key);
|
|
266
|
+
if (hit !== void 0) return hit;
|
|
267
|
+
const { data } = await ghFetch(`/repos/${source.owner}/${source.repo}/releases?per_page=${limit}`, env);
|
|
268
|
+
const releases = data.filter((r) => !r.draft).sort((a, b) => new Date(b.published_at ?? 0).getTime() - new Date(a.published_at ?? 0).getTime()).slice(0, limit).map((r) => ({
|
|
269
|
+
tag_name: r.tag_name ?? "",
|
|
270
|
+
name: r.name ?? r.tag_name ?? "",
|
|
271
|
+
body: r.body ?? "",
|
|
272
|
+
html_url: r.html_url ?? "",
|
|
273
|
+
published_at: r.published_at ?? "",
|
|
274
|
+
prerelease: r.prerelease ?? false
|
|
275
|
+
}));
|
|
276
|
+
cache?.set(key, releases);
|
|
277
|
+
return releases;
|
|
278
|
+
}
|
|
279
|
+
async function fetchFiles(source, paths, env, cache) {
|
|
280
|
+
const results = /* @__PURE__ */ new Map();
|
|
281
|
+
const settled = await Promise.allSettled(
|
|
282
|
+
paths.map(async (path) => {
|
|
283
|
+
const content = await fetchFile(source, path, env, cache);
|
|
284
|
+
return { path, content };
|
|
285
|
+
})
|
|
286
|
+
);
|
|
287
|
+
for (const result of settled) {
|
|
288
|
+
if (result.status === "fulfilled") {
|
|
289
|
+
results.set(result.value.path, result.value.content);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return results;
|
|
293
|
+
}
|
|
294
|
+
async function fetchBranches(source, env, cache) {
|
|
295
|
+
const key = `branches:${source.owner}/${source.repo}`;
|
|
296
|
+
const hit = cache?.get(key);
|
|
297
|
+
if (hit !== void 0) return hit;
|
|
298
|
+
const all = [];
|
|
299
|
+
let page = 1;
|
|
300
|
+
const perPage = 100;
|
|
301
|
+
while (true) {
|
|
302
|
+
const { data } = await ghFetch(
|
|
303
|
+
`/repos/${source.owner}/${source.repo}/branches?per_page=${perPage}&page=${page}`,
|
|
304
|
+
env
|
|
305
|
+
);
|
|
306
|
+
all.push(...data.map((b) => ({ name: b.name, protected: b.protected ?? false })));
|
|
307
|
+
if (data.length < perPage) break;
|
|
308
|
+
page++;
|
|
309
|
+
}
|
|
310
|
+
cache?.set(key, all);
|
|
311
|
+
return all;
|
|
312
|
+
}
|
|
313
|
+
async function fetchRepoMetadata(source, env, cache) {
|
|
314
|
+
const key = `repoMetadata:${source.owner}/${source.repo}`;
|
|
315
|
+
const hit = cache?.get(key);
|
|
316
|
+
if (hit !== void 0) return hit;
|
|
317
|
+
const [{ data: repo }, languagesMap] = await Promise.all([
|
|
318
|
+
ghFetch(`/repos/${source.owner}/${source.repo}`, env),
|
|
319
|
+
ghFetch(`/repos/${source.owner}/${source.repo}/languages`, env).then((r) => r.data).catch(() => ({}))
|
|
320
|
+
]);
|
|
321
|
+
const languages = Object.entries(languagesMap).sort((a, b) => b[1] - a[1]).map(([name, size]) => ({ name, size }));
|
|
322
|
+
const metadata = {
|
|
323
|
+
name: repo.name ?? "",
|
|
324
|
+
description: repo.description ?? "",
|
|
325
|
+
html_url: repo.html_url ?? "",
|
|
326
|
+
homepage: repo.homepage ?? "",
|
|
327
|
+
default_branch: repo.default_branch ?? "main",
|
|
328
|
+
stargazers_count: repo.stargazers_count ?? 0,
|
|
329
|
+
forks_count: repo.forks_count ?? 0,
|
|
330
|
+
private: repo.private ?? false,
|
|
331
|
+
topics: repo.topics ?? [],
|
|
332
|
+
primary_language: repo.language ?? "",
|
|
333
|
+
languages,
|
|
334
|
+
owner: {
|
|
335
|
+
login: repo.owner?.login ?? "",
|
|
336
|
+
avatar_url: repo.owner?.avatar_url ?? ""
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
cache?.set(key, metadata);
|
|
340
|
+
return metadata;
|
|
341
|
+
}
|
|
342
|
+
new marked.Marked();
|
|
343
|
+
kbexplorerCore.resolveAccessExclusion(kbexplorerCore.DEFAULT_ACCESS_EXCLUSION);
|
|
344
|
+
|
|
345
|
+
// src/default-config.ts
|
|
346
|
+
var DEFAULT_CONFIG = {
|
|
347
|
+
title: "kbexplorer",
|
|
348
|
+
subtitle: "Interactive Knowledge Base Explorer",
|
|
349
|
+
author: "Anokye Labs",
|
|
350
|
+
source: { owner: "anokye-labs", repo: "kbexplorer", path: "content", branch: "main" },
|
|
351
|
+
clusters: {
|
|
352
|
+
// Each cluster may also carry an optional `tokens` delta (Fluent token name
|
|
353
|
+
// → CSS value, same shape as theme.tokens) to shift only that cluster's
|
|
354
|
+
// scoped surfaces (cards/badges/reading header). Omitted here so defaults
|
|
355
|
+
// inherit the active global theme unchanged.
|
|
356
|
+
feature: { name: "Feature", color: "#4A9CC8" },
|
|
357
|
+
task: { name: "Task", color: "#8CB050" },
|
|
358
|
+
bug: { name: "Bug", color: "#C04040" },
|
|
359
|
+
epic: { name: "Epic", color: "#E8A838" },
|
|
360
|
+
code: { name: "Code", color: "#9A8A78" },
|
|
361
|
+
docs: { name: "Documentation", color: "#D4A050" },
|
|
362
|
+
"pull-request": { name: "Pull Request", color: "#A86FDF" },
|
|
363
|
+
commits: { name: "Commits", color: "#5A98A8" },
|
|
364
|
+
releases: { name: "Releases", color: "#F78166" }
|
|
365
|
+
},
|
|
366
|
+
visuals: {
|
|
367
|
+
mode: "emoji",
|
|
368
|
+
fallback: "emoji"
|
|
369
|
+
},
|
|
370
|
+
theme: {
|
|
371
|
+
default: "dark",
|
|
372
|
+
font: {
|
|
373
|
+
heading: "'Segoe UI Variable', 'Segoe UI', system-ui, sans-serif",
|
|
374
|
+
body: "'Segoe UI Variable', 'Segoe UI', system-ui, sans-serif",
|
|
375
|
+
mono: "'Cascadia Code', 'Cascadia Mono', Consolas, monospace"
|
|
376
|
+
}
|
|
377
|
+
// brand / tokens / themes are optional, additive overrides (see KBConfig.theme).
|
|
378
|
+
// Left unset by default so the built-in dark/light/sepia themes are unchanged.
|
|
379
|
+
// themesFile (also unset by default) may point at a dedicated theme file in the
|
|
380
|
+
// host repo (e.g. "content/themes/extra.yaml"); when set it is fetched at runtime
|
|
381
|
+
// like config.yaml and its named themes are merged into the THEME_MAP, overriding
|
|
382
|
+
// any inline theme.themes of the same name. Unset means no fetch, no behavior change.
|
|
383
|
+
// moduleUrl (also unset by default) is the most powerful escape hatch: a
|
|
384
|
+
// security-sensitive opt-in that dynamically import()s a host-provided ESM JS
|
|
385
|
+
// module exporting a Fluent Theme / BrandVariants and registers it into the
|
|
386
|
+
// THEME_MAP. Off by default, meaning no import, pure no-op. Only set it for a
|
|
387
|
+
// module you trust (ideally self-hosted in this repo) and tighten CSP accordingly
|
|
388
|
+
// — see the theming docs' CSP note.
|
|
389
|
+
},
|
|
390
|
+
graph: {
|
|
391
|
+
physics: true,
|
|
392
|
+
layout: "force-atlas-2"
|
|
393
|
+
},
|
|
394
|
+
features: {
|
|
395
|
+
hud: true,
|
|
396
|
+
minimap: true,
|
|
397
|
+
readingTools: true,
|
|
398
|
+
keyboardNav: true,
|
|
399
|
+
sparkAnimation: false,
|
|
400
|
+
search: true
|
|
401
|
+
}
|
|
402
|
+
// branding omitted by default — host repos may set branding.logo (a repo-relative
|
|
403
|
+
// image path) to render a logo on the HomePage hero and HUD header, and
|
|
404
|
+
// branding.favicon (a repo-relative image path) to swap the favicon at
|
|
405
|
+
// runtime, and branding.css (a repo-relative path or URL) to inject a raw CSS
|
|
406
|
+
// override sheet last in <head> for full control over --colorNeutral*/
|
|
407
|
+
// --colorBrand*/--kbe-* variables. Text title and the static /favicon.svg are
|
|
408
|
+
// used as graceful fallbacks; branding.css is unset by default so nothing is
|
|
409
|
+
// injected.
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
// src/parser.ts
|
|
413
|
+
async function loadConfig(source, env, cache) {
|
|
414
|
+
try {
|
|
415
|
+
const raw = await fetchFile(
|
|
416
|
+
source,
|
|
417
|
+
source.path ? `${source.path}/config.yaml` : "content/config.yaml",
|
|
418
|
+
env,
|
|
419
|
+
cache
|
|
420
|
+
);
|
|
421
|
+
const parsed = yaml__default.default.parse(raw);
|
|
422
|
+
return { ...DEFAULT_CONFIG, ...parsed, source };
|
|
423
|
+
} catch {
|
|
424
|
+
return { ...DEFAULT_CONFIG, source };
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// src/structured-content.ts
|
|
429
|
+
var DEFAULT_STRUCTURED_CONTENT_PATH = "content-model";
|
|
430
|
+
function normalizeRepoRelativeDir(raw) {
|
|
431
|
+
if (typeof raw !== "string") return null;
|
|
432
|
+
let value = raw.trim().replace(/\\/g, "/");
|
|
433
|
+
while (value.startsWith("./")) value = value.slice(2);
|
|
434
|
+
value = value.replace(/\/+$/g, "");
|
|
435
|
+
if (!value || /^[a-zA-Z]:\//.test(value) || value.startsWith("/") || value.includes("://") || value === ".." || value.startsWith("../") || value.split("/").some((segment) => segment === "" || segment === "." || segment === "..")) {
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
return value;
|
|
439
|
+
}
|
|
440
|
+
function resolveStructuredContentPath(config, env) {
|
|
441
|
+
const engineEnv = env ?? {};
|
|
442
|
+
const envValue = engineEnv.VITE_KB_STRUCTURED_CONTENT_PATH ?? engineEnv.VITE_KB_CONTENT_MODEL_PATH;
|
|
443
|
+
const normalizedEnv = normalizeRepoRelativeDir(envValue);
|
|
444
|
+
if (normalizedEnv) return normalizedEnv;
|
|
445
|
+
const cfg = config;
|
|
446
|
+
const configured = cfg.structuredContent?.path ?? cfg.contentModel?.path;
|
|
447
|
+
const normalizedConfig = normalizeRepoRelativeDir(configured);
|
|
448
|
+
return normalizedConfig ?? DEFAULT_STRUCTURED_CONTENT_PATH;
|
|
449
|
+
}
|
|
450
|
+
function hasExplicitStructuredContentPath(config, env) {
|
|
451
|
+
const engineEnv = env ?? {};
|
|
452
|
+
const envValue = engineEnv.VITE_KB_STRUCTURED_CONTENT_PATH ?? engineEnv.VITE_KB_CONTENT_MODEL_PATH;
|
|
453
|
+
if (normalizeRepoRelativeDir(envValue)) return true;
|
|
454
|
+
const cfg = config;
|
|
455
|
+
const configured = cfg.structuredContent?.path ?? cfg.contentModel?.path;
|
|
456
|
+
return normalizeRepoRelativeDir(configured) !== null;
|
|
457
|
+
}
|
|
458
|
+
function globToRegex2(pattern) {
|
|
459
|
+
let re = "";
|
|
460
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
461
|
+
const c = pattern[i];
|
|
462
|
+
if (c === "*" && pattern[i + 1] === "*") {
|
|
463
|
+
re += ".*";
|
|
464
|
+
i += 1;
|
|
465
|
+
if (pattern[i + 1] === "/") i += 1;
|
|
466
|
+
} else if (c === "*") {
|
|
467
|
+
re += "[^/]*";
|
|
468
|
+
} else if (c === "?") {
|
|
469
|
+
re += "[^/]";
|
|
470
|
+
} else if (".+^${}()|[]\\".includes(c)) {
|
|
471
|
+
re += "\\" + c;
|
|
472
|
+
} else {
|
|
473
|
+
re += c;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return new RegExp(`^${re}$`);
|
|
477
|
+
}
|
|
478
|
+
function matchesAnyGlob(path, patterns) {
|
|
479
|
+
return patterns.some((p) => globToRegex2(p).test(path));
|
|
480
|
+
}
|
|
481
|
+
async function collectNodemapData(nodemapRaw, tree, readFile2, listDirectory) {
|
|
482
|
+
let map;
|
|
483
|
+
try {
|
|
484
|
+
map = yaml__default.default.parse(nodemapRaw);
|
|
485
|
+
} catch {
|
|
486
|
+
return { nodemapFiles: {}, nodemapDirs: {} };
|
|
487
|
+
}
|
|
488
|
+
if (!map?.nodes?.length) return { nodemapFiles: {}, nodemapDirs: {} };
|
|
489
|
+
const nodemapFiles = {};
|
|
490
|
+
const nodemapDirs = {};
|
|
491
|
+
const expandGlob = (pattern, exclude) => {
|
|
492
|
+
const regex = globToRegex2(pattern);
|
|
493
|
+
let matches = tree.filter((e) => e.type === "blob" && regex.test(e.path)).map((e) => e.path);
|
|
494
|
+
if (exclude?.length) {
|
|
495
|
+
matches = matches.filter((p) => !matchesAnyGlob(p, exclude));
|
|
496
|
+
}
|
|
497
|
+
return matches;
|
|
498
|
+
};
|
|
499
|
+
for (const entry of map.nodes) {
|
|
500
|
+
if (entry.file) {
|
|
501
|
+
const content = await readFile2(entry.file);
|
|
502
|
+
if (content !== null) nodemapFiles[entry.file] = content;
|
|
503
|
+
}
|
|
504
|
+
if (entry.files) {
|
|
505
|
+
for (const f of entry.files) {
|
|
506
|
+
const content = await readFile2(f);
|
|
507
|
+
if (content !== null) nodemapFiles[f] = content;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
if (entry.glob && entry.each === "file") {
|
|
511
|
+
for (const match of expandGlob(entry.glob, entry.exclude)) {
|
|
512
|
+
const content = await readFile2(match);
|
|
513
|
+
if (content !== null) nodemapFiles[match] = content;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (entry.directory) {
|
|
517
|
+
nodemapDirs[entry.directory] = await listDirectory(entry.directory);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return { nodemapFiles, nodemapDirs };
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// src/sources/github-api-source.ts
|
|
524
|
+
function isStructuralPath(path) {
|
|
525
|
+
return path.startsWith(".github/") || /(^|\/)CODEOWNERS$/.test(path);
|
|
526
|
+
}
|
|
527
|
+
var MAX_STRUCTURAL_FILE_SIZE = 256 * 1024;
|
|
528
|
+
async function fetchStructuredNodeMap(source, env, cache) {
|
|
529
|
+
for (const name of ["structured-node-map.yaml", "structured-node-map.yml"]) {
|
|
530
|
+
try {
|
|
531
|
+
return await fetchFile(source, name, env, cache);
|
|
532
|
+
} catch {
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
async function fetchNodemapRaw(source, env, cache) {
|
|
538
|
+
for (const name of ["nodemap.yaml", "nodemap.yml"]) {
|
|
539
|
+
try {
|
|
540
|
+
return await fetchFile(source, name, env, cache);
|
|
541
|
+
} catch {
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
var GitHubApiSource = class {
|
|
547
|
+
id = "github-api";
|
|
548
|
+
name = "GitHub API";
|
|
549
|
+
/** Advisory universe; authoritative affordances live on each retrieval. */
|
|
550
|
+
possibleAffordances = ["read", "write", "stage", "comment", "close", "merge"];
|
|
551
|
+
fetchPromise = null;
|
|
552
|
+
source;
|
|
553
|
+
preset;
|
|
554
|
+
env;
|
|
555
|
+
cache;
|
|
556
|
+
constructor(source, preset = "standard", env, cache) {
|
|
557
|
+
this.source = source;
|
|
558
|
+
this.preset = preset;
|
|
559
|
+
this.env = env;
|
|
560
|
+
this.cache = cache;
|
|
561
|
+
}
|
|
562
|
+
/** The locator of this repo's staging area (git index). */
|
|
563
|
+
get stagingHref() {
|
|
564
|
+
return `git://${this.source.repo}/.git/index`;
|
|
565
|
+
}
|
|
566
|
+
/** Fetch (memoized) so `resolveConfig` + `getRepoData` share one round-trip. */
|
|
567
|
+
fetch() {
|
|
568
|
+
if (!this.fetchPromise) this.fetchPromise = this.fetchGitHubData();
|
|
569
|
+
return this.fetchPromise;
|
|
570
|
+
}
|
|
571
|
+
/** Resolve the config without re-fetching. */
|
|
572
|
+
async resolveConfig() {
|
|
573
|
+
return (await this.fetch()).config;
|
|
574
|
+
}
|
|
575
|
+
async resolveThemeFileRaw() {
|
|
576
|
+
return (await this.fetch()).themeFileRaw;
|
|
577
|
+
}
|
|
578
|
+
async getRepoData() {
|
|
579
|
+
const data = await this.fetch();
|
|
580
|
+
return {
|
|
581
|
+
repo: this.source.repo,
|
|
582
|
+
tree: data.tree,
|
|
583
|
+
authoredContent: data.authoredContent,
|
|
584
|
+
nodemapRaw: data.nodemapRaw,
|
|
585
|
+
// `nodemapFiles`/`nodemapDirs` are omitted (rather than assigned empty
|
|
586
|
+
// objects) when there's nothing to report — this package enables
|
|
587
|
+
// exactOptionalPropertyTypes, and the loader/manifest convention is to
|
|
588
|
+
// leave optional fields unset rather than emit noisy `{}`.
|
|
589
|
+
...Object.keys(data.nodemapFiles).length > 0 ? { nodemapFiles: data.nodemapFiles } : {},
|
|
590
|
+
...Object.keys(data.nodemapDirs).length > 0 ? { nodemapDirs: data.nodemapDirs } : {},
|
|
591
|
+
listFiles: async () => [],
|
|
592
|
+
issues: data.issues,
|
|
593
|
+
// Reproduce the former remote shaping: no head_branch (so WorkProvider
|
|
594
|
+
// emits no branch connections), user/assignees retained for people.
|
|
595
|
+
// (user/assignees are set conditionally because this package's config
|
|
596
|
+
// enables exactOptionalPropertyTypes — an explicit `undefined` value is
|
|
597
|
+
// not assignable to the optional target fields.)
|
|
598
|
+
pullRequests: data.pullRequests.map((pr) => {
|
|
599
|
+
const mapped = {
|
|
600
|
+
number: pr.number,
|
|
601
|
+
title: pr.title,
|
|
602
|
+
body: pr.body ?? "",
|
|
603
|
+
state: pr.state,
|
|
604
|
+
labels: pr.labels.map((l) => ({ name: l.name, color: l.color ?? "" })),
|
|
605
|
+
html_url: pr.html_url,
|
|
606
|
+
created_at: pr.created_at,
|
|
607
|
+
updated_at: pr.updated_at
|
|
608
|
+
};
|
|
609
|
+
if (pr.user) mapped.user = pr.user;
|
|
610
|
+
if (pr.assignees) mapped.assignees = pr.assignees;
|
|
611
|
+
return mapped;
|
|
612
|
+
}),
|
|
613
|
+
commits: data.commits,
|
|
614
|
+
branches: data.branches,
|
|
615
|
+
repoMetadata: data.repoMetadata,
|
|
616
|
+
releases: data.releases,
|
|
617
|
+
structuralFiles: data.structuralFiles,
|
|
618
|
+
structuredNodeMapRaw: data.structuredNodeMapRaw,
|
|
619
|
+
contentModel: data.contentModel ?? null,
|
|
620
|
+
readme: data.readme,
|
|
621
|
+
themeFileRaw: data.themeFileRaw
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
// ── Formal resource surface (Git ≠ GitHub, per-retrieval affordances) ─────
|
|
625
|
+
async retrieve(query) {
|
|
626
|
+
const data = await this.fetch();
|
|
627
|
+
const kind = query.kind;
|
|
628
|
+
const worktree = query.worktree === true;
|
|
629
|
+
const staged = query.staged === true;
|
|
630
|
+
const out = [];
|
|
631
|
+
if (kind === "staging-area") {
|
|
632
|
+
out.push(this.stagingAreaResource());
|
|
633
|
+
return out;
|
|
634
|
+
}
|
|
635
|
+
if (!kind || kind === "file" || kind === "tree") {
|
|
636
|
+
for (const item of data.tree) {
|
|
637
|
+
if (kind === "file" && item.type !== "blob") continue;
|
|
638
|
+
if (kind === "tree" && item.type !== "tree") continue;
|
|
639
|
+
out.push(this.gitFileResource(item, { worktree, staged }));
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
if (!kind || kind === "commit") {
|
|
643
|
+
for (const c of data.commits) out.push(this.commitResource(c));
|
|
644
|
+
}
|
|
645
|
+
if (!kind || kind === "issue") {
|
|
646
|
+
for (const issue of data.issues) out.push(this.issueResource(issue));
|
|
647
|
+
}
|
|
648
|
+
if (!kind || kind === "pull-request") {
|
|
649
|
+
for (const pr of data.pullRequests) out.push(this.pullRequestResource(pr));
|
|
650
|
+
}
|
|
651
|
+
if (!kind || kind === "release") {
|
|
652
|
+
for (const r of data.releases) out.push(this.releaseResource(r));
|
|
653
|
+
}
|
|
654
|
+
return out;
|
|
655
|
+
}
|
|
656
|
+
async get(href) {
|
|
657
|
+
if (href === this.stagingHref) return this.stagingAreaResource();
|
|
658
|
+
const all = await this.retrieve({});
|
|
659
|
+
return all.find((r) => r.href === href);
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* A git file/tree resource. Affordances are situational:
|
|
663
|
+
* - plain read → `['read']`
|
|
664
|
+
* - writable worktree → `['read','write','stage']`
|
|
665
|
+
* - staged → `['read','write','stage']` PLUS a `staging-area` link.
|
|
666
|
+
*/
|
|
667
|
+
gitFileResource(item, ctx) {
|
|
668
|
+
const href = `git://${this.source.repo}/${item.path}`;
|
|
669
|
+
const affordances = ctx.worktree || ctx.staged ? ["read", "write", "stage"] : ["read"];
|
|
670
|
+
const links = [{ rel: "self", href }];
|
|
671
|
+
if (ctx.staged) {
|
|
672
|
+
links.push({ rel: kbexplorerCore.STAGING_AREA_REL, href: this.stagingHref });
|
|
673
|
+
}
|
|
674
|
+
return { href, kind: item.type === "tree" ? "tree" : "file", affordances, links, body: { path: item.path } };
|
|
675
|
+
}
|
|
676
|
+
stagingAreaResource() {
|
|
677
|
+
return {
|
|
678
|
+
href: this.stagingHref,
|
|
679
|
+
kind: "staging-area",
|
|
680
|
+
affordances: ["read", "stage"],
|
|
681
|
+
links: [{ rel: "self", href: this.stagingHref }],
|
|
682
|
+
body: { repo: this.source.repo }
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
commitResource(c) {
|
|
686
|
+
const href = `git://${this.source.repo}/commit/${c.sha}`;
|
|
687
|
+
return {
|
|
688
|
+
href,
|
|
689
|
+
kind: "commit",
|
|
690
|
+
affordances: ["read"],
|
|
691
|
+
links: [{ rel: "self", href }],
|
|
692
|
+
body: { sha: c.sha, message: c.commit.message }
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
issueResource(issue) {
|
|
696
|
+
const href = `github://${this.source.repo}/issues/${issue.number}`;
|
|
697
|
+
return {
|
|
698
|
+
href,
|
|
699
|
+
kind: "issue",
|
|
700
|
+
affordances: ["read", "comment", "close"],
|
|
701
|
+
links: [{ rel: "self", href }],
|
|
702
|
+
body: { number: issue.number, title: issue.title, state: issue.state }
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
pullRequestResource(pr) {
|
|
706
|
+
const href = `github://${this.source.repo}/pull/${pr.number}`;
|
|
707
|
+
return {
|
|
708
|
+
href,
|
|
709
|
+
kind: "pull-request",
|
|
710
|
+
affordances: ["read", "comment", "close", "merge"],
|
|
711
|
+
links: [{ rel: "self", href }],
|
|
712
|
+
body: { number: pr.number, title: pr.title, state: pr.state }
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
releaseResource(r) {
|
|
716
|
+
const href = `github://${this.source.repo}/releases/${r.tag_name}`;
|
|
717
|
+
return {
|
|
718
|
+
href,
|
|
719
|
+
kind: "release",
|
|
720
|
+
affordances: ["read"],
|
|
721
|
+
links: [{ rel: "self", href }],
|
|
722
|
+
body: { tag: r.tag_name }
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
// ── Data path (moved verbatim from the former remote loader) ──────────────
|
|
726
|
+
async fetchGitHubData() {
|
|
727
|
+
const source = this.source;
|
|
728
|
+
const preset = this.preset;
|
|
729
|
+
const env = this.env;
|
|
730
|
+
const cache = this.cache;
|
|
731
|
+
const config = await loadConfig(source, env, cache);
|
|
732
|
+
const themeFilePromise = config.theme?.themesFile ? fetchFile(source, config.theme.themesFile, env, cache).catch(() => null) : Promise.resolve(null);
|
|
733
|
+
const [issues, readme, releases, themeFileRaw, branches, repoMetadata, nodemapRaw] = await Promise.all([
|
|
734
|
+
fetchIssues(source, env, cache).catch(() => []),
|
|
735
|
+
fetchFile(source, "README.md", env, cache).catch(() => null),
|
|
736
|
+
fetchReleases(source, 30, env, cache).catch(() => []),
|
|
737
|
+
themeFilePromise,
|
|
738
|
+
fetchBranches(source, env, cache).catch(() => []),
|
|
739
|
+
fetchRepoMetadata(source, env, cache).catch(() => null),
|
|
740
|
+
fetchNodemapRaw(source, env, cache)
|
|
741
|
+
]);
|
|
742
|
+
let tree = [];
|
|
743
|
+
let pullRequests = [];
|
|
744
|
+
let commits = [];
|
|
745
|
+
const authoredContent = {};
|
|
746
|
+
const structuralFiles = {};
|
|
747
|
+
let structuredNodeMapRaw = null;
|
|
748
|
+
let contentModel = null;
|
|
749
|
+
let nodemapFiles = {};
|
|
750
|
+
let nodemapDirs = {};
|
|
751
|
+
if (preset === "standard" || preset === "full") {
|
|
752
|
+
const [treeResult, prResult] = await Promise.all([
|
|
753
|
+
fetchTree(source, void 0, env, cache).catch(() => []),
|
|
754
|
+
fetchPullRequests(source, env, cache).catch(() => [])
|
|
755
|
+
]);
|
|
756
|
+
tree = treeResult;
|
|
757
|
+
pullRequests = prResult;
|
|
758
|
+
if (config.source.path) {
|
|
759
|
+
try {
|
|
760
|
+
const contentTree = await fetchTree(source, config.source.path, env, cache);
|
|
761
|
+
const mdFiles = contentTree.filter((item) => item.type === "blob" && item.path.endsWith(".md")).map((item) => item.path);
|
|
762
|
+
const files = await fetchFiles(source, mdFiles, env, cache);
|
|
763
|
+
for (const [path, content] of files) {
|
|
764
|
+
authoredContent[path] = content;
|
|
765
|
+
}
|
|
766
|
+
} catch {
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
contentModel = await this.fetchContentModel(config);
|
|
770
|
+
try {
|
|
771
|
+
const structuralPaths = tree.filter(
|
|
772
|
+
(item) => item.type === "blob" && isStructuralPath(item.path) && !(typeof item.size === "number" && item.size > MAX_STRUCTURAL_FILE_SIZE)
|
|
773
|
+
).map((item) => item.path);
|
|
774
|
+
if (structuralPaths.length > 0) {
|
|
775
|
+
const files = await fetchFiles(source, structuralPaths, env, cache);
|
|
776
|
+
for (const [path, content] of files) {
|
|
777
|
+
if (content.length > MAX_STRUCTURAL_FILE_SIZE) continue;
|
|
778
|
+
structuralFiles[path] = content;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
structuredNodeMapRaw = await fetchStructuredNodeMap(source, env, cache);
|
|
782
|
+
} catch {
|
|
783
|
+
}
|
|
784
|
+
if (nodemapRaw !== null) {
|
|
785
|
+
try {
|
|
786
|
+
const collected = await collectNodemapData(
|
|
787
|
+
nodemapRaw,
|
|
788
|
+
tree,
|
|
789
|
+
(path) => fetchFile(source, path, env, cache).catch(() => null),
|
|
790
|
+
(dir) => this.listNodemapDirFromTree(tree, dir)
|
|
791
|
+
);
|
|
792
|
+
nodemapFiles = collected.nodemapFiles;
|
|
793
|
+
nodemapDirs = collected.nodemapDirs;
|
|
794
|
+
} catch {
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
if (preset === "full") {
|
|
799
|
+
commits = await fetchCommits(source, 50, env, cache).catch(() => []);
|
|
800
|
+
}
|
|
801
|
+
return {
|
|
802
|
+
issues,
|
|
803
|
+
pullRequests,
|
|
804
|
+
tree,
|
|
805
|
+
readme,
|
|
806
|
+
commits,
|
|
807
|
+
releases,
|
|
808
|
+
branches,
|
|
809
|
+
repoMetadata,
|
|
810
|
+
authoredContent,
|
|
811
|
+
structuralFiles,
|
|
812
|
+
structuredNodeMapRaw,
|
|
813
|
+
contentModel,
|
|
814
|
+
config,
|
|
815
|
+
themeFileRaw: themeFileRaw ?? null,
|
|
816
|
+
nodemapRaw,
|
|
817
|
+
nodemapFiles,
|
|
818
|
+
nodemapDirs
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* Direct children of `dir` in an already-fetched git tree — a nodemap
|
|
823
|
+
* `directory:` entry's listing derived with no extra API call. Skips
|
|
824
|
+
* dotfile-named children, mirroring the local source's `listNodemapDir`.
|
|
825
|
+
*/
|
|
826
|
+
listNodemapDirFromTree(tree, dir) {
|
|
827
|
+
const prefix = `${dir}/`;
|
|
828
|
+
const out = tree.filter((item) => {
|
|
829
|
+
if (!item.path.startsWith(prefix)) return false;
|
|
830
|
+
const rest = item.path.slice(prefix.length);
|
|
831
|
+
if (rest === "" || rest.includes("/")) return false;
|
|
832
|
+
return !rest.startsWith(".");
|
|
833
|
+
});
|
|
834
|
+
return Promise.resolve(out);
|
|
835
|
+
}
|
|
836
|
+
async fetchContentModel(config) {
|
|
837
|
+
const env = this.env;
|
|
838
|
+
const cache = this.cache;
|
|
839
|
+
const root = resolveStructuredContentPath(config, env);
|
|
840
|
+
const explicitPath = hasExplicitStructuredContentPath(config, env);
|
|
841
|
+
try {
|
|
842
|
+
const contentModelTree = await fetchTree(this.source, root, env, cache);
|
|
843
|
+
const paths = contentModelTree.filter((item) => item.type === "blob").map((item) => item.path).filter((path) => !path.slice(root.length + 1).split("/").some((segment) => segment.startsWith(".")));
|
|
844
|
+
if (paths.length === 0) {
|
|
845
|
+
if (explicitPath) {
|
|
846
|
+
console.warn(`[github-api-source] No structured content files found at configured path ${root}`);
|
|
847
|
+
}
|
|
848
|
+
return null;
|
|
849
|
+
}
|
|
850
|
+
const files = await fetchFiles(this.source, paths, env, cache);
|
|
851
|
+
const sourceFiles = {};
|
|
852
|
+
for (const [path, content] of files) {
|
|
853
|
+
const relativePath = path.startsWith(`${root}/`) ? path.slice(root.length + 1) : path;
|
|
854
|
+
sourceFiles[relativePath] = content;
|
|
855
|
+
}
|
|
856
|
+
if (Object.keys(sourceFiles).length === 0) {
|
|
857
|
+
if (explicitPath) {
|
|
858
|
+
console.warn(`[github-api-source] Structured content path ${root} was listed but no files could be fetched`);
|
|
859
|
+
}
|
|
860
|
+
return null;
|
|
861
|
+
}
|
|
862
|
+
return { root, files: sourceFiles };
|
|
863
|
+
} catch (error) {
|
|
864
|
+
console.warn(`[github-api-source] Failed to fetch structured content from ${root}:`, error);
|
|
865
|
+
return null;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
};
|
|
869
|
+
|
|
870
|
+
// src/sources/build-manifest.ts
|
|
871
|
+
function toManifestPullRequest(pr) {
|
|
872
|
+
const mapped = {
|
|
873
|
+
number: pr.number,
|
|
874
|
+
title: pr.title,
|
|
875
|
+
body: pr.body,
|
|
876
|
+
state: pr.state,
|
|
877
|
+
labels: pr.labels,
|
|
878
|
+
html_url: pr.html_url,
|
|
879
|
+
created_at: pr.created_at,
|
|
880
|
+
updated_at: pr.updated_at
|
|
881
|
+
};
|
|
882
|
+
if (pr.head_branch !== void 0) mapped.head_branch = pr.head_branch;
|
|
883
|
+
return mapped;
|
|
884
|
+
}
|
|
885
|
+
async function buildManifest(source, options = {}) {
|
|
886
|
+
const data = await source.getRepoData();
|
|
887
|
+
const manifest = {
|
|
888
|
+
configRaw: options.configRaw ?? null,
|
|
889
|
+
authoredContent: data.authoredContent,
|
|
890
|
+
tree: data.tree,
|
|
891
|
+
readme: data.readme,
|
|
892
|
+
issues: data.issues,
|
|
893
|
+
pullRequests: data.pullRequests.map(toManifestPullRequest),
|
|
894
|
+
commits: data.commits,
|
|
895
|
+
generatedAt: options.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
896
|
+
};
|
|
897
|
+
if (data.branches.length > 0) manifest.branches = data.branches;
|
|
898
|
+
if (data.releases.length > 0) manifest.releases = data.releases;
|
|
899
|
+
if (data.repoMetadata) manifest.repoMetadata = data.repoMetadata;
|
|
900
|
+
if (data.nodemapRaw !== null) manifest.nodemapRaw = data.nodemapRaw;
|
|
901
|
+
if (data.nodemapFiles !== void 0) manifest.nodemapFiles = data.nodemapFiles;
|
|
902
|
+
if (data.nodemapDirs !== void 0) manifest.nodemapDirs = data.nodemapDirs;
|
|
903
|
+
if (data.structuredNodeMapRaw !== null) manifest.structuredNodeMapRaw = data.structuredNodeMapRaw;
|
|
904
|
+
if (Object.keys(data.structuralFiles).length > 0) manifest.structuralFiles = data.structuralFiles;
|
|
905
|
+
if (data.contentModel) manifest.contentModel = data.contentModel;
|
|
906
|
+
if (data.themeFileRaw) manifest.themeFileRaw = data.themeFileRaw;
|
|
907
|
+
if (options.augmentFrom) {
|
|
908
|
+
const live = await options.augmentFrom.getRepoData();
|
|
909
|
+
manifest.issues = live.issues;
|
|
910
|
+
manifest.pullRequests = live.pullRequests.map(toManifestPullRequest);
|
|
911
|
+
manifest.commits = live.commits;
|
|
912
|
+
if (live.repoMetadata !== null) manifest.repoMetadata = live.repoMetadata;
|
|
913
|
+
if (live.branches.length > 0) manifest.branches = live.branches;
|
|
914
|
+
if (live.releases.length > 0) manifest.releases = live.releases;
|
|
915
|
+
}
|
|
916
|
+
return manifest;
|
|
917
|
+
}
|
|
918
|
+
var ALWAYS_IGNORE = /* @__PURE__ */ new Set([".git", "node_modules", "dist"]);
|
|
919
|
+
var MAX_STRUCTURAL_FILE_SIZE2 = 256 * 1024;
|
|
920
|
+
function isStructuralPath2(path) {
|
|
921
|
+
return path.startsWith(".github/") || /(^|\/)CODEOWNERS$/.test(path);
|
|
922
|
+
}
|
|
923
|
+
var FileSystemSource = class {
|
|
924
|
+
id = "filesystem";
|
|
925
|
+
name = "File System";
|
|
926
|
+
/** A local snapshot read — read is the only possible affordance. */
|
|
927
|
+
possibleAffordances = ["read"];
|
|
928
|
+
rootDir;
|
|
929
|
+
repo;
|
|
930
|
+
contentPath;
|
|
931
|
+
contentModelPath;
|
|
932
|
+
ignore;
|
|
933
|
+
includeFileTree;
|
|
934
|
+
cache = null;
|
|
935
|
+
constructor(rootDir, options = {}) {
|
|
936
|
+
this.rootDir = path.resolve(rootDir);
|
|
937
|
+
this.repo = options.repo ?? path.basename(this.rootDir);
|
|
938
|
+
this.contentPath = options.contentPath ?? "content";
|
|
939
|
+
this.contentModelPath = options.contentModelPath ?? "content-model";
|
|
940
|
+
this.ignore = /* @__PURE__ */ new Set([...ALWAYS_IGNORE, ...options.ignore ?? []]);
|
|
941
|
+
this.includeFileTree = options.includeFileTree ?? true;
|
|
942
|
+
}
|
|
943
|
+
async getRepoData() {
|
|
944
|
+
if (!this.cache) this.cache = this.readRepoData();
|
|
945
|
+
return this.cache;
|
|
946
|
+
}
|
|
947
|
+
async readRepoData() {
|
|
948
|
+
const files = await this.walk();
|
|
949
|
+
const paths = files.map((f) => f.path);
|
|
950
|
+
const tree = this.includeFileTree ? files.map((f) => ({ path: f.path, type: f.type })) : [];
|
|
951
|
+
const authoredContent = {};
|
|
952
|
+
const structuralFiles = {};
|
|
953
|
+
const rootScan = this.contentPath === "" || this.contentPath === ".";
|
|
954
|
+
const contentPrefix = rootScan ? "" : `${this.contentPath}/`;
|
|
955
|
+
const contentModelPrefix = `${this.contentModelPath}/`;
|
|
956
|
+
const contentModelFiles = {};
|
|
957
|
+
let readme = null;
|
|
958
|
+
let structuredNodeMapRaw = null;
|
|
959
|
+
for (const file of files) {
|
|
960
|
+
if (file.type !== "blob") continue;
|
|
961
|
+
const isAuthoredMd = file.path.endsWith(".md") && file.path !== "README.md" && (rootScan ? !file.path.includes("/") : file.path.startsWith(contentPrefix));
|
|
962
|
+
if (isAuthoredMd) {
|
|
963
|
+
authoredContent[file.path] = await this.read(file.path);
|
|
964
|
+
continue;
|
|
965
|
+
}
|
|
966
|
+
if (isStructuralPath2(file.path) && (file.size ?? 0) <= MAX_STRUCTURAL_FILE_SIZE2) {
|
|
967
|
+
const content = await this.read(file.path);
|
|
968
|
+
if (content.length <= MAX_STRUCTURAL_FILE_SIZE2) structuralFiles[file.path] = content;
|
|
969
|
+
continue;
|
|
970
|
+
}
|
|
971
|
+
if (file.path.startsWith(contentModelPrefix)) {
|
|
972
|
+
const relativePath = file.path.slice(contentModelPrefix.length);
|
|
973
|
+
contentModelFiles[relativePath] = await this.read(file.path);
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
if (file.path === "README.md") {
|
|
977
|
+
readme = await this.read(file.path);
|
|
978
|
+
continue;
|
|
979
|
+
}
|
|
980
|
+
if (structuredNodeMapRaw === null && (file.path === "structured-node-map.yaml" || file.path === "structured-node-map.yml")) {
|
|
981
|
+
structuredNodeMapRaw = await this.read(file.path);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
const contentModel = Object.keys(contentModelFiles).length > 0 ? { root: this.contentModelPath, files: contentModelFiles } : null;
|
|
985
|
+
const listFiles = async (pattern) => {
|
|
986
|
+
const regex = globToRegex(pattern);
|
|
987
|
+
return paths.filter((p) => regex.test(p));
|
|
988
|
+
};
|
|
989
|
+
const nodemapRaw = await this.readNodemapRaw();
|
|
990
|
+
let nodemapFiles;
|
|
991
|
+
let nodemapDirs;
|
|
992
|
+
if (nodemapRaw !== null) {
|
|
993
|
+
const collected = await collectNodemapData(
|
|
994
|
+
nodemapRaw,
|
|
995
|
+
files,
|
|
996
|
+
(path) => this.readSafe(path),
|
|
997
|
+
(dir) => this.listNodemapDir(dir)
|
|
998
|
+
);
|
|
999
|
+
nodemapFiles = collected.nodemapFiles;
|
|
1000
|
+
nodemapDirs = collected.nodemapDirs;
|
|
1001
|
+
}
|
|
1002
|
+
const themeFileRaw = await this.resolveThemeFileRaw();
|
|
1003
|
+
return {
|
|
1004
|
+
repo: this.repo,
|
|
1005
|
+
tree,
|
|
1006
|
+
authoredContent,
|
|
1007
|
+
nodemapRaw,
|
|
1008
|
+
...nodemapFiles !== void 0 ? { nodemapFiles } : {},
|
|
1009
|
+
...nodemapDirs !== void 0 ? { nodemapDirs } : {},
|
|
1010
|
+
listFiles,
|
|
1011
|
+
issues: [],
|
|
1012
|
+
pullRequests: [],
|
|
1013
|
+
commits: [],
|
|
1014
|
+
// Legitimately empty/null in local mode — there is no live GitHub to
|
|
1015
|
+
// fetch branches/repo metadata from (anokye-labs/kbexplorer-engine#23).
|
|
1016
|
+
branches: [],
|
|
1017
|
+
repoMetadata: null,
|
|
1018
|
+
releases: [],
|
|
1019
|
+
structuralFiles,
|
|
1020
|
+
structuredNodeMapRaw,
|
|
1021
|
+
contentModel,
|
|
1022
|
+
readme,
|
|
1023
|
+
themeFileRaw
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
/** Read one repo-relative file as UTF-8. */
|
|
1027
|
+
read(repoRelative) {
|
|
1028
|
+
return promises.readFile(path.join(this.rootDir, repoRelative), "utf8");
|
|
1029
|
+
}
|
|
1030
|
+
/** Read one repo-relative file as UTF-8, returning `null` when missing/unreadable. */
|
|
1031
|
+
async readSafe(repoRelative) {
|
|
1032
|
+
try {
|
|
1033
|
+
return await promises.readFile(path.join(this.rootDir, repoRelative), "utf8");
|
|
1034
|
+
} catch {
|
|
1035
|
+
return null;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
/** Read `nodemap.yaml`/`nodemap.yml` from the repo root, or `null` if neither exists. */
|
|
1039
|
+
async readNodemapRaw() {
|
|
1040
|
+
for (const name of ["nodemap.yaml", "nodemap.yml"]) {
|
|
1041
|
+
const content = await this.readSafe(name);
|
|
1042
|
+
if (content !== null) return content;
|
|
1043
|
+
}
|
|
1044
|
+
return null;
|
|
1045
|
+
}
|
|
1046
|
+
/** Read `${contentPath}/config.yaml`(`.yml`), falling back to a root-level `config.yaml`. */
|
|
1047
|
+
async readConfigRaw() {
|
|
1048
|
+
const rootScan = this.contentPath === "" || this.contentPath === ".";
|
|
1049
|
+
const contentPrefix = rootScan ? "" : `${this.contentPath}/`;
|
|
1050
|
+
const candidates = [`${contentPrefix}config.yaml`, `${contentPrefix}config.yml`, "config.yaml"];
|
|
1051
|
+
for (const candidate of candidates) {
|
|
1052
|
+
const content = await this.readSafe(candidate);
|
|
1053
|
+
if (content !== null) return content;
|
|
1054
|
+
}
|
|
1055
|
+
return null;
|
|
1056
|
+
}
|
|
1057
|
+
/**
|
|
1058
|
+
* Read the raw contents of the dedicated theme file referenced by
|
|
1059
|
+
* `config.theme.themesFile`, mirroring the old generator's `readThemeFile`.
|
|
1060
|
+
* `null` when no `themesFile` is configured, the config can't be parsed, the
|
|
1061
|
+
* path escapes the repo root, or the file is missing — a safe no-op.
|
|
1062
|
+
*/
|
|
1063
|
+
async resolveThemeFileRaw() {
|
|
1064
|
+
const configRaw = await this.readConfigRaw();
|
|
1065
|
+
if (!configRaw) return null;
|
|
1066
|
+
let parsed;
|
|
1067
|
+
try {
|
|
1068
|
+
parsed = yaml__default.default.parse(configRaw);
|
|
1069
|
+
} catch {
|
|
1070
|
+
return null;
|
|
1071
|
+
}
|
|
1072
|
+
const themesFile = parsed?.theme?.themesFile;
|
|
1073
|
+
if (!themesFile || typeof themesFile !== "string") return null;
|
|
1074
|
+
if (path.isAbsolute(themesFile)) return null;
|
|
1075
|
+
const abs = path.resolve(this.rootDir, themesFile);
|
|
1076
|
+
const rel = path.relative(this.rootDir, abs);
|
|
1077
|
+
if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel)) return null;
|
|
1078
|
+
try {
|
|
1079
|
+
return await promises.readFile(abs, "utf8");
|
|
1080
|
+
} catch {
|
|
1081
|
+
return null;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
/**
|
|
1085
|
+
* One-level directory listing (with file sizes) for a nodemap `directory:`
|
|
1086
|
+
* entry — distinct from `walk()`, which is recursive and does not stat
|
|
1087
|
+
* files. Mirrors the old generator's `listDir`.
|
|
1088
|
+
*/
|
|
1089
|
+
async listNodemapDir(dirPath) {
|
|
1090
|
+
const abs = path.join(this.rootDir, dirPath);
|
|
1091
|
+
let entries;
|
|
1092
|
+
try {
|
|
1093
|
+
entries = await promises.readdir(abs, { withFileTypes: true });
|
|
1094
|
+
} catch {
|
|
1095
|
+
return [];
|
|
1096
|
+
}
|
|
1097
|
+
const out = [];
|
|
1098
|
+
for (const entry of entries) {
|
|
1099
|
+
if (entry.name.startsWith(".")) continue;
|
|
1100
|
+
const entryPath = dirPath ? `${dirPath}/${entry.name}` : entry.name;
|
|
1101
|
+
if (entry.isDirectory()) {
|
|
1102
|
+
out.push({ path: entryPath, type: "tree" });
|
|
1103
|
+
} else if (entry.isFile()) {
|
|
1104
|
+
try {
|
|
1105
|
+
const st = await promises.stat(path.join(abs, entry.name));
|
|
1106
|
+
out.push({ path: entryPath, type: "blob", size: st.size });
|
|
1107
|
+
} catch {
|
|
1108
|
+
out.push({ path: entryPath, type: "blob" });
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
return out;
|
|
1113
|
+
}
|
|
1114
|
+
/** Convert an absolute path under the root into a POSIX repo-relative path. */
|
|
1115
|
+
toRepoRelative(absolutePath) {
|
|
1116
|
+
return absolutePath.slice(this.rootDir.length + 1).split(path.sep).join("/");
|
|
1117
|
+
}
|
|
1118
|
+
/**
|
|
1119
|
+
* Recursively enumerate the root directory into blob/tree entries with
|
|
1120
|
+
* POSIX-separated, repo-relative paths (sorted for determinism). Skips
|
|
1121
|
+
* dotfiles/dot-directories (except `.github`, which the StructuralProvider
|
|
1122
|
+
* reads) in addition to the always-ignored directories — mirrors the old
|
|
1123
|
+
* generator's `walkFileSystem` (anokye-labs/kbexplorer-engine#23).
|
|
1124
|
+
*/
|
|
1125
|
+
async walk() {
|
|
1126
|
+
const out = [];
|
|
1127
|
+
const stack = [this.rootDir];
|
|
1128
|
+
while (stack.length > 0) {
|
|
1129
|
+
const dir = stack.pop();
|
|
1130
|
+
const entries = await promises.readdir(dir, { withFileTypes: true });
|
|
1131
|
+
for (const entry of entries) {
|
|
1132
|
+
if (entry.name.startsWith(".") && entry.name !== ".github") continue;
|
|
1133
|
+
if (entry.isDirectory() && this.ignore.has(entry.name)) continue;
|
|
1134
|
+
const absolutePath = path.join(dir, entry.name);
|
|
1135
|
+
const repoRelative = this.toRepoRelative(absolutePath);
|
|
1136
|
+
if (entry.isDirectory()) {
|
|
1137
|
+
out.push({ path: repoRelative, type: "tree" });
|
|
1138
|
+
stack.push(absolutePath);
|
|
1139
|
+
} else if (entry.isFile()) {
|
|
1140
|
+
out.push({ path: repoRelative, type: "blob" });
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
out.sort((a, b) => a.path.localeCompare(b.path));
|
|
1145
|
+
return out;
|
|
1146
|
+
}
|
|
1147
|
+
// ── Read-only resource surface (mirrors ManifestSource) ───────────────────
|
|
1148
|
+
async retrieve(query) {
|
|
1149
|
+
const kind = query.kind;
|
|
1150
|
+
const out = [];
|
|
1151
|
+
if (!kind || kind === "file" || kind === "tree") {
|
|
1152
|
+
const { tree } = await this.getRepoData();
|
|
1153
|
+
for (const item of tree) {
|
|
1154
|
+
if (kind === "file" && item.type !== "blob") continue;
|
|
1155
|
+
if (kind === "tree" && item.type !== "tree") continue;
|
|
1156
|
+
out.push(this.fileResource(item));
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
return out;
|
|
1160
|
+
}
|
|
1161
|
+
async get(href) {
|
|
1162
|
+
const all = await this.retrieve({});
|
|
1163
|
+
return all.find((r) => r.href === href);
|
|
1164
|
+
}
|
|
1165
|
+
fileResource(item) {
|
|
1166
|
+
const href = `file://${this.repo}/${item.path}`;
|
|
1167
|
+
return {
|
|
1168
|
+
href,
|
|
1169
|
+
kind: item.type === "tree" ? "tree" : "file",
|
|
1170
|
+
affordances: ["read"],
|
|
1171
|
+
links: [{ rel: "self", href }],
|
|
1172
|
+
body: { path: item.path }
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
};
|
|
1176
|
+
|
|
1177
|
+
exports.FileSystemSource = FileSystemSource;
|
|
1178
|
+
exports.GITHUB_ENDPOINT_PATTERNS = GITHUB_ENDPOINT_PATTERNS;
|
|
1179
|
+
exports.GitHubApiError = GitHubApiError;
|
|
1180
|
+
exports.GitHubApiSource = GitHubApiSource;
|
|
1181
|
+
exports.ManifestSource = ManifestSource;
|
|
1182
|
+
exports.NotModifiedError = NotModifiedError;
|
|
1183
|
+
exports.RateLimitError = RateLimitError;
|
|
1184
|
+
exports.buildManifest = buildManifest;
|
|
1185
|
+
exports.fetchCommits = fetchCommits;
|
|
1186
|
+
exports.fetchFile = fetchFile;
|
|
1187
|
+
exports.fetchFiles = fetchFiles;
|
|
1188
|
+
exports.fetchIssues = fetchIssues;
|
|
1189
|
+
exports.fetchPullRequests = fetchPullRequests;
|
|
1190
|
+
exports.fetchReleases = fetchReleases;
|
|
1191
|
+
exports.fetchTree = fetchTree;
|