contextqmd 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.
@@ -0,0 +1,183 @@
1
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync, } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ export function normalizeDocPath(docPath) {
4
+ const trimmed = docPath.replace(/\\/g, "/").replace(/^\/+/, "");
5
+ return trimmed.endsWith(".md") ? trimmed : `${trimmed}.md`;
6
+ }
7
+ function normalizePageUid(pageUid) {
8
+ const trimmed = pageUid.replace(/\\/g, "/").replace(/^\/+/, "");
9
+ if (!trimmed || trimmed.split("/").includes("..")) {
10
+ throw new Error(`Unsafe page_uid: ${pageUid}`);
11
+ }
12
+ return trimmed;
13
+ }
14
+ export class LocalCache {
15
+ cacheDir;
16
+ constructor(cacheDir) {
17
+ this.cacheDir = cacheDir;
18
+ mkdirSync(join(this.cacheDir, "docs"), { recursive: true });
19
+ mkdirSync(join(this.cacheDir, "state"), { recursive: true });
20
+ mkdirSync(join(this.cacheDir, "tmp"), { recursive: true });
21
+ }
22
+ docsDir(namespace, name, version) {
23
+ return join(this.cacheDir, "docs", namespace, name, version);
24
+ }
25
+ pagesDir(namespace, name, version) {
26
+ return join(this.docsDir(namespace, name, version), "pages");
27
+ }
28
+ saveManifest(namespace, name, version, manifest) {
29
+ const dir = this.docsDir(namespace, name, version);
30
+ mkdirSync(dir, { recursive: true });
31
+ writeFileSync(join(dir, "manifest.json"), JSON.stringify(manifest, null, 2));
32
+ }
33
+ savePageIndex(namespace, name, version, pageIndex) {
34
+ const dir = this.docsDir(namespace, name, version);
35
+ mkdirSync(dir, { recursive: true });
36
+ writeFileSync(join(dir, "page-index.json"), JSON.stringify(pageIndex, null, 2));
37
+ }
38
+ loadPageIndex(namespace, name, version) {
39
+ const path = join(this.docsDir(namespace, name, version), "page-index.json");
40
+ if (!existsSync(path))
41
+ return [];
42
+ return JSON.parse(readFileSync(path, "utf-8"));
43
+ }
44
+ findPageByUid(namespace, name, version, pageUid) {
45
+ return this.loadPageIndex(namespace, name, version).find(page => page.page_uid === pageUid) ?? null;
46
+ }
47
+ findPageByPath(namespace, name, version, docPath) {
48
+ const normalized = normalizeDocPath(docPath);
49
+ return this.loadPageIndex(namespace, name, version)
50
+ .find(page => normalizeDocPath(page.path) === normalized) ?? null;
51
+ }
52
+ savePage(namespace, name, version, pageUid, content) {
53
+ const path = this.pagePath(namespace, name, version, pageUid);
54
+ mkdirSync(dirname(path), { recursive: true });
55
+ writeFileSync(path, content);
56
+ }
57
+ readPage(namespace, name, version, pageUid) {
58
+ const path = this.pagePath(namespace, name, version, pageUid);
59
+ if (!existsSync(path))
60
+ return null;
61
+ return readFileSync(path, "utf-8");
62
+ }
63
+ hasManifest(namespace, name, version) {
64
+ return existsSync(join(this.docsDir(namespace, name, version), "manifest.json"));
65
+ }
66
+ countPages(namespace, name, version) {
67
+ const pageIndex = this.loadPageIndex(namespace, name, version);
68
+ if (pageIndex.length > 0)
69
+ return pageIndex.length;
70
+ const dir = this.pagesDir(namespace, name, version);
71
+ if (!existsSync(dir))
72
+ return 0;
73
+ return this.collectMarkdownFiles(dir).length;
74
+ }
75
+ listPageUids(namespace, name, version) {
76
+ const pageIndex = this.loadPageIndex(namespace, name, version);
77
+ if (pageIndex.length > 0)
78
+ return pageIndex.map(page => page.page_uid);
79
+ const dir = this.pagesDir(namespace, name, version);
80
+ if (!existsSync(dir))
81
+ return [];
82
+ return this.collectMarkdownFiles(dir).map(file => file.replace(/\.md$/, ""));
83
+ }
84
+ removeVersion(namespace, name, version) {
85
+ const dir = this.docsDir(namespace, name, version);
86
+ if (existsSync(dir)) {
87
+ rmSync(dir, { recursive: true, force: true });
88
+ }
89
+ }
90
+ createTempInstallDir(namespace, name, version) {
91
+ const prefix = `${namespace}-${name}-${version}-`.replace(/[^a-zA-Z0-9._-]/g, "-");
92
+ return mkdtempSync(join(this.cacheDir, "tmp", prefix));
93
+ }
94
+ createTempArchivePath(namespace, name, version, filename = "bundle.tar.gz") {
95
+ const dir = this.createTempInstallDir(namespace, name, version);
96
+ return join(dir, filename);
97
+ }
98
+ commitStagedVersion(namespace, name, version, stagedDocsDir) {
99
+ const finalDir = this.docsDir(namespace, name, version);
100
+ const parentDir = join(this.cacheDir, "docs", namespace, name);
101
+ mkdirSync(parentDir, { recursive: true });
102
+ if (existsSync(finalDir)) {
103
+ rmSync(finalDir, { recursive: true, force: true });
104
+ }
105
+ renameSync(stagedDocsDir, finalDir);
106
+ }
107
+ backupVersion(namespace, name, version) {
108
+ const finalDir = this.docsDir(namespace, name, version);
109
+ if (!existsSync(finalDir))
110
+ return null;
111
+ const backupRoot = this.createTempInstallDir(namespace, name, version);
112
+ const backupDir = join(backupRoot, "docs");
113
+ renameSync(finalDir, backupDir);
114
+ return backupDir;
115
+ }
116
+ restoreVersionFromBackup(namespace, name, version, backupDir) {
117
+ const finalDir = this.docsDir(namespace, name, version);
118
+ const parentDir = join(this.cacheDir, "docs", namespace, name);
119
+ if (existsSync(finalDir)) {
120
+ rmSync(finalDir, { recursive: true, force: true });
121
+ }
122
+ if (existsSync(backupDir)) {
123
+ mkdirSync(parentDir, { recursive: true });
124
+ renameSync(backupDir, finalDir);
125
+ }
126
+ this.cleanupTempPath(dirname(backupDir));
127
+ }
128
+ discardBackup(backupDir) {
129
+ this.cleanupTempPath(dirname(backupDir));
130
+ }
131
+ cleanupTempPath(path) {
132
+ if (existsSync(path)) {
133
+ rmSync(path, { recursive: true, force: true });
134
+ }
135
+ }
136
+ statePath() {
137
+ return join(this.cacheDir, "state", "installed.json");
138
+ }
139
+ loadState() {
140
+ const path = this.statePath();
141
+ if (!existsSync(path))
142
+ return { libraries: [] };
143
+ return JSON.parse(readFileSync(path, "utf-8"));
144
+ }
145
+ saveState(state) {
146
+ writeFileSync(this.statePath(), JSON.stringify(state, null, 2));
147
+ }
148
+ findInstalled(namespace, name, version) {
149
+ return this.loadState().libraries.find(lib => lib.namespace === namespace && lib.name === name && (!version || lib.version === version));
150
+ }
151
+ addInstalled(lib) {
152
+ const state = this.loadState();
153
+ state.libraries = state.libraries.filter(existing => !(existing.namespace === lib.namespace && existing.name === lib.name && existing.version === lib.version));
154
+ state.libraries.push(lib);
155
+ this.saveState(state);
156
+ }
157
+ removeInstalled(namespace, name, version) {
158
+ const state = this.loadState();
159
+ state.libraries = state.libraries.filter(lib => !(lib.namespace === namespace && lib.name === name && lib.version === version));
160
+ this.saveState(state);
161
+ }
162
+ listInstalled() {
163
+ return this.loadState().libraries;
164
+ }
165
+ pagePath(namespace, name, version, pageUid) {
166
+ return join(this.pagesDir(namespace, name, version), `${normalizePageUid(pageUid)}.md`);
167
+ }
168
+ collectMarkdownFiles(path, relativeDir = "") {
169
+ const files = [];
170
+ for (const entry of readdirSync(path, { withFileTypes: true })) {
171
+ const nextRelative = relativeDir ? join(relativeDir, entry.name) : entry.name;
172
+ const nextPath = join(path, entry.name);
173
+ if (entry.isDirectory()) {
174
+ files.push(...this.collectMarkdownFiles(nextPath, nextRelative));
175
+ continue;
176
+ }
177
+ if (entry.isFile() && entry.name.endsWith(".md")) {
178
+ files.push(nextRelative.replace(/\\/g, "/"));
179
+ }
180
+ }
181
+ return files;
182
+ }
183
+ }
@@ -0,0 +1,100 @@
1
+ export class RegistryClient {
2
+ registryUrl;
3
+ baseUrl;
4
+ token;
5
+ constructor(registryUrl, token) {
6
+ this.registryUrl = registryUrl.replace(/\/$/, "");
7
+ this.baseUrl = `${this.registryUrl}/api/v1`;
8
+ this.token = token;
9
+ }
10
+ async health() {
11
+ return this.get("health");
12
+ }
13
+ async capabilities() {
14
+ return this.get("capabilities");
15
+ }
16
+ async searchLibraries(query, cursor) {
17
+ const params = new URLSearchParams({ query });
18
+ if (cursor)
19
+ params.set("cursor", cursor);
20
+ return this.get(`libraries?${params}`);
21
+ }
22
+ async getLibrary(namespace, name) {
23
+ return this.get(`libraries/${namespace}/${name}`);
24
+ }
25
+ async getVersions(namespace, name, cursor) {
26
+ const params = cursor ? `?cursor=${cursor}` : "";
27
+ return this.get(`libraries/${namespace}/${name}/versions${params}`);
28
+ }
29
+ async getManifest(namespace, name, version) {
30
+ return this.get(`libraries/${namespace}/${name}/versions/${version}/manifest`);
31
+ }
32
+ async getPageIndex(namespace, name, version, cursor) {
33
+ const params = cursor ? `?cursor=${cursor}` : "";
34
+ return this.get(`libraries/${namespace}/${name}/versions/${version}/page-index${params}`);
35
+ }
36
+ async getAllPageIndex(namespace, name, version) {
37
+ const maxFetches = 100;
38
+ const allPages = [];
39
+ let cursor;
40
+ let iterations = 0;
41
+ do {
42
+ if (++iterations > maxFetches) {
43
+ break;
44
+ }
45
+ const response = await this.getPageIndex(namespace, name, version, cursor);
46
+ allPages.push(...response.data);
47
+ cursor = response.meta.cursor ?? undefined;
48
+ } while (cursor);
49
+ return allPages;
50
+ }
51
+ async getPageContent(namespace, name, version, pageUid) {
52
+ return this.get(`libraries/${namespace}/${name}/versions/${version}/pages/${pageUid}`);
53
+ }
54
+ async resolve(request) {
55
+ return this.post("resolve", request);
56
+ }
57
+ async downloadBundle(bundleUrl) {
58
+ const res = await fetch(this.resolveUrl(bundleUrl), { headers: this.authHeaders() });
59
+ if (!res.ok) {
60
+ throw new Error(`Registry error ${res.status}: ${await res.text()}`);
61
+ }
62
+ return Buffer.from(await res.arrayBuffer());
63
+ }
64
+ async get(path) {
65
+ const res = await fetch(this.resolveUrl(path), { headers: this.headers() });
66
+ if (!res.ok) {
67
+ throw new Error(`Registry error ${res.status}: ${await res.text()}`);
68
+ }
69
+ return res.json();
70
+ }
71
+ async post(path, body) {
72
+ const res = await fetch(this.resolveUrl(path), {
73
+ method: "POST",
74
+ headers: { ...this.headers(), "Content-Type": "application/json" },
75
+ body: JSON.stringify(body),
76
+ });
77
+ if (!res.ok) {
78
+ throw new Error(`Registry error ${res.status}: ${await res.text()}`);
79
+ }
80
+ return res.json();
81
+ }
82
+ resolveUrl(pathOrUrl) {
83
+ if (/^https?:\/\//.test(pathOrUrl)) {
84
+ return pathOrUrl;
85
+ }
86
+ if (pathOrUrl.startsWith("/")) {
87
+ return new URL(pathOrUrl, `${this.registryUrl}/`).toString();
88
+ }
89
+ return `${this.baseUrl}/${pathOrUrl.replace(/^\/+/, "")}`;
90
+ }
91
+ headers() {
92
+ const headers = { Accept: "application/json" };
93
+ if (this.token)
94
+ headers.Authorization = `Token ${this.token}`;
95
+ return headers;
96
+ }
97
+ authHeaders() {
98
+ return this.token ? { Authorization: `Token ${this.token}` } : {};
99
+ }
100
+ }