backpack-viewer 0.6.0 → 0.7.1
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/bin/serve.js +159 -396
- package/dist/app/assets/index-D-H7agBH.js +12 -0
- package/dist/app/assets/index-DE73ngo-.css +1 -0
- package/dist/app/assets/index-Lvl7EMM_.js +6 -0
- package/dist/app/index.html +2 -2
- package/dist/bridge.d.ts +22 -0
- package/dist/bridge.js +41 -0
- package/dist/config.js +10 -0
- package/dist/copy-prompt.d.ts +17 -0
- package/dist/copy-prompt.js +81 -0
- package/dist/default-config.json +4 -0
- package/dist/dom-utils.d.ts +46 -0
- package/dist/dom-utils.js +57 -0
- package/dist/empty-state.js +63 -31
- package/dist/extensions/api.d.ts +15 -0
- package/dist/extensions/api.js +185 -0
- package/dist/extensions/chat/backpack-extension.json +23 -0
- package/dist/extensions/chat/src/index.js +32 -0
- package/dist/extensions/chat/src/panel.js +306 -0
- package/dist/extensions/chat/src/providers/anthropic.js +158 -0
- package/dist/extensions/chat/src/providers/types.js +15 -0
- package/dist/extensions/chat/src/tools.js +281 -0
- package/dist/extensions/chat/style.css +147 -0
- package/dist/extensions/event-bus.d.ts +12 -0
- package/dist/extensions/event-bus.js +30 -0
- package/dist/extensions/loader.d.ts +32 -0
- package/dist/extensions/loader.js +71 -0
- package/dist/extensions/manifest.d.ts +54 -0
- package/dist/extensions/manifest.js +116 -0
- package/dist/extensions/panel-mount.d.ts +26 -0
- package/dist/extensions/panel-mount.js +377 -0
- package/dist/extensions/share/backpack-extension.json +20 -0
- package/dist/extensions/share/src/index.js +357 -0
- package/dist/extensions/share/style.css +151 -0
- package/dist/extensions/taskbar.d.ts +29 -0
- package/dist/extensions/taskbar.js +64 -0
- package/dist/extensions/types.d.ts +182 -0
- package/dist/extensions/types.js +8 -0
- package/dist/info-panel.d.ts +2 -1
- package/dist/info-panel.js +78 -87
- package/dist/main.js +189 -29
- package/dist/search.js +1 -1
- package/dist/server-api-routes.d.ts +56 -0
- package/dist/server-api-routes.js +460 -0
- package/dist/server-extensions.d.ts +126 -0
- package/dist/server-extensions.js +272 -0
- package/dist/server-viewer-state.d.ts +18 -0
- package/dist/server-viewer-state.js +33 -0
- package/dist/sidebar.js +19 -7
- package/dist/style.css +356 -74
- package/dist/tools-pane.js +31 -14
- package/package.json +4 -3
- package/dist/app/assets/index-B3z5bBGl.css +0 -1
- package/dist/app/assets/index-BROJmzot.js +0 -35
package/bin/serve.js
CHANGED
|
@@ -83,17 +83,24 @@ async function getCachedVersionCheck(currentVersion) {
|
|
|
83
83
|
|
|
84
84
|
if (hasDistBuild) {
|
|
85
85
|
// --- Production: static file server + API (zero native deps) ---
|
|
86
|
-
const {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
RemoteRegistry,
|
|
90
|
-
listBackpacks,
|
|
91
|
-
getActiveBackpack,
|
|
92
|
-
setActiveBackpack,
|
|
93
|
-
registerBackpack,
|
|
94
|
-
unregisterBackpack,
|
|
95
|
-
} = await import("backpack-ontology");
|
|
86
|
+
const { JsonFileBackend, RemoteRegistry, getActiveBackpack } = await import(
|
|
87
|
+
"backpack-ontology"
|
|
88
|
+
);
|
|
96
89
|
const { loadViewerConfig } = await import("../dist/config.js");
|
|
90
|
+
const { writeViewerState, readViewerState } = await import(
|
|
91
|
+
"../dist/server-viewer-state.js"
|
|
92
|
+
);
|
|
93
|
+
const {
|
|
94
|
+
loadExtensions: loadServerExtensions,
|
|
95
|
+
findExtension,
|
|
96
|
+
publicExtensionInfo,
|
|
97
|
+
proxyExtensionFetch,
|
|
98
|
+
readExtensionSettings,
|
|
99
|
+
writeExtensionSetting,
|
|
100
|
+
deleteExtensionSetting,
|
|
101
|
+
resolveExtensionFile,
|
|
102
|
+
} = await import("../dist/server-extensions.js");
|
|
103
|
+
const { handleApiRequest } = await import("../dist/server-api-routes.js");
|
|
97
104
|
|
|
98
105
|
// Load our own version from package.json for the stale-version check
|
|
99
106
|
const pkgJson = JSON.parse(
|
|
@@ -105,15 +112,17 @@ if (hasDistBuild) {
|
|
|
105
112
|
// precedence for quick overrides. Default is 127.0.0.1 loopback —
|
|
106
113
|
// the viewer must never bind to all interfaces by default because
|
|
107
114
|
// its API exposes read/write access to the user's learning graphs.
|
|
108
|
-
const
|
|
109
|
-
const configuredHost =
|
|
110
|
-
const configuredPort =
|
|
115
|
+
const viewerConfig = loadViewerConfig();
|
|
116
|
+
const configuredHost = viewerConfig?.server?.host ?? "127.0.0.1";
|
|
117
|
+
const configuredPort = viewerConfig?.server?.port ?? 5173;
|
|
111
118
|
const bindHost = process.env.BACKPACK_VIEWER_HOST ?? configuredHost;
|
|
112
119
|
const port = parseInt(process.env.PORT || String(configuredPort), 10);
|
|
113
120
|
|
|
114
121
|
// Storage points at the active backpack. Wrapped in a mutable
|
|
115
122
|
// holder so a `/api/backpacks/switch` POST can swap it out in place
|
|
116
|
-
// without restarting the whole server.
|
|
123
|
+
// without restarting the whole server. The holder is shared with
|
|
124
|
+
// the API route handler so a swap inside one request is visible to
|
|
125
|
+
// the next request without re-plumbing.
|
|
117
126
|
async function makeBackend() {
|
|
118
127
|
const entry = await getActiveBackpack();
|
|
119
128
|
const backend = new JsonFileBackend(undefined, {
|
|
@@ -122,10 +131,36 @@ if (hasDistBuild) {
|
|
|
122
131
|
await backend.initialize();
|
|
123
132
|
return { backend, entry };
|
|
124
133
|
}
|
|
125
|
-
|
|
134
|
+
const initial = await makeBackend();
|
|
135
|
+
const storageHolder = { current: initial.backend, activeEntry: initial.entry };
|
|
136
|
+
|
|
126
137
|
const remoteRegistry = new RemoteRegistry();
|
|
127
138
|
await remoteRegistry.initialize();
|
|
128
|
-
|
|
139
|
+
|
|
140
|
+
// First-party extensions are bundled at dist/extensions/<name>/.
|
|
141
|
+
// External extensions come from the user's viewer config.
|
|
142
|
+
const firstPartyExtensionsDir = path.resolve(distDir, "..", "extensions");
|
|
143
|
+
const extConfig = viewerConfig.extensions ?? {};
|
|
144
|
+
const userExternalExtensions = Array.isArray(extConfig.external)
|
|
145
|
+
? extConfig.external.filter((e) => e && typeof e.name === "string" && typeof e.path === "string")
|
|
146
|
+
: [];
|
|
147
|
+
const disabledFirstParty = new Set(
|
|
148
|
+
Array.isArray(extConfig.disabled) ? extConfig.disabled : [],
|
|
149
|
+
);
|
|
150
|
+
const loadedExtensions = loadServerExtensions(
|
|
151
|
+
firstPartyExtensionsDir,
|
|
152
|
+
userExternalExtensions,
|
|
153
|
+
disabledFirstParty,
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// Context object passed to the shared API route handler.
|
|
157
|
+
const apiContext = {
|
|
158
|
+
storage: storageHolder,
|
|
159
|
+
remoteRegistry,
|
|
160
|
+
viewerConfig,
|
|
161
|
+
makeBackend,
|
|
162
|
+
versionCheck: () => getCachedVersionCheck(currentVersion),
|
|
163
|
+
};
|
|
129
164
|
|
|
130
165
|
const MIME_TYPES = {
|
|
131
166
|
".html": "text/html",
|
|
@@ -139,6 +174,10 @@ if (hasDistBuild) {
|
|
|
139
174
|
|
|
140
175
|
// Strict CSP — style-src 'self' means no inline styles allowed.
|
|
141
176
|
// Keep it that way; see CLAUDE.md for the rule.
|
|
177
|
+
//
|
|
178
|
+
// connect-src stays at 'self' — extensions never talk to external
|
|
179
|
+
// origins directly. They go through the per-extension proxy below
|
|
180
|
+
// which forwards server-side using env-var-injected secrets.
|
|
142
181
|
const CSP = [
|
|
143
182
|
"default-src 'self'",
|
|
144
183
|
"script-src 'self'",
|
|
@@ -158,143 +197,17 @@ if (hasDistBuild) {
|
|
|
158
197
|
|
|
159
198
|
const url = req.url?.replace(/\?.*$/, "") || "/";
|
|
160
199
|
|
|
161
|
-
// ---
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (url === "/api/version-check" && req.method === "GET") {
|
|
169
|
-
try {
|
|
170
|
-
const result = await getCachedVersionCheck(currentVersion);
|
|
171
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
172
|
-
res.end(JSON.stringify(result));
|
|
173
|
-
} catch {
|
|
174
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
175
|
-
res.end(
|
|
176
|
-
JSON.stringify({ current: currentVersion, latest: null, stale: false }),
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// --- Remote graph routes (read-only) ---
|
|
183
|
-
if (url === "/api/remotes" && req.method === "GET") {
|
|
184
|
-
try {
|
|
185
|
-
const remotes = await remoteRegistry.list();
|
|
186
|
-
const summaries = await Promise.all(
|
|
187
|
-
remotes.map(async (r) => {
|
|
188
|
-
try {
|
|
189
|
-
const data = await remoteRegistry.loadCached(r.name);
|
|
190
|
-
return {
|
|
191
|
-
name: r.name,
|
|
192
|
-
url: r.url,
|
|
193
|
-
source: r.source,
|
|
194
|
-
addedAt: r.addedAt,
|
|
195
|
-
lastFetched: r.lastFetched,
|
|
196
|
-
pinned: r.pinned,
|
|
197
|
-
sizeBytes: r.sizeBytes,
|
|
198
|
-
nodeCount: data.nodes.length,
|
|
199
|
-
edgeCount: data.edges.length,
|
|
200
|
-
};
|
|
201
|
-
} catch {
|
|
202
|
-
return {
|
|
203
|
-
name: r.name,
|
|
204
|
-
url: r.url,
|
|
205
|
-
source: r.source,
|
|
206
|
-
addedAt: r.addedAt,
|
|
207
|
-
lastFetched: r.lastFetched,
|
|
208
|
-
pinned: r.pinned,
|
|
209
|
-
sizeBytes: r.sizeBytes,
|
|
210
|
-
nodeCount: 0,
|
|
211
|
-
edgeCount: 0,
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
}),
|
|
215
|
-
);
|
|
216
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
217
|
-
res.end(JSON.stringify(summaries));
|
|
218
|
-
} catch (err) {
|
|
219
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
220
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
221
|
-
}
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const remoteItemMatch = url.match(/^\/api\/remotes\/(.+)$/);
|
|
226
|
-
if (remoteItemMatch && req.method === "GET") {
|
|
227
|
-
const remoteName = decodeURIComponent(remoteItemMatch[1]);
|
|
228
|
-
try {
|
|
229
|
-
const data = await remoteRegistry.loadCached(remoteName);
|
|
230
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
231
|
-
res.end(JSON.stringify(data));
|
|
232
|
-
} catch (err) {
|
|
233
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
234
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
235
|
-
}
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// --- Branch routes ---
|
|
240
|
-
const branchSwitchMatch = url.match(/^\/api\/graphs\/(.+)\/branches\/switch$/);
|
|
241
|
-
if (branchSwitchMatch && req.method === "POST") {
|
|
242
|
-
const graphName = decodeURIComponent(branchSwitchMatch[1]);
|
|
243
|
-
let body = "";
|
|
244
|
-
req.on("data", (chunk) => { body += chunk.toString(); });
|
|
245
|
-
req.on("end", async () => {
|
|
246
|
-
try {
|
|
247
|
-
const { name: branchName } = JSON.parse(body);
|
|
248
|
-
await storage.switchBranch(graphName, branchName);
|
|
249
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
250
|
-
res.end(JSON.stringify({ ok: true }));
|
|
251
|
-
} catch (err) {
|
|
252
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
253
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
254
|
-
}
|
|
255
|
-
});
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const deleteBranchMatch = url.match(/^\/api\/graphs\/(.+)\/branches\/(.+)$/);
|
|
260
|
-
if (deleteBranchMatch && req.method === "DELETE") {
|
|
261
|
-
const graphName = decodeURIComponent(deleteBranchMatch[1]);
|
|
262
|
-
const branchName = decodeURIComponent(deleteBranchMatch[2]);
|
|
263
|
-
try {
|
|
264
|
-
await storage.deleteBranch(graphName, branchName);
|
|
265
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
266
|
-
res.end(JSON.stringify({ ok: true }));
|
|
267
|
-
} catch (err) {
|
|
268
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
269
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
270
|
-
}
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
const branchMatch = url.match(/^\/api\/graphs\/(.+)\/branches$/);
|
|
275
|
-
if (branchMatch && req.method === "GET") {
|
|
276
|
-
const graphName = decodeURIComponent(branchMatch[1]);
|
|
277
|
-
try {
|
|
278
|
-
const branches = await storage.listBranches(graphName);
|
|
279
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
280
|
-
res.end(JSON.stringify(branches));
|
|
281
|
-
} catch (err) {
|
|
282
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
283
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
284
|
-
}
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
if (branchMatch && req.method === "POST") {
|
|
289
|
-
const graphName = decodeURIComponent(branchMatch[1]);
|
|
200
|
+
// --- Viewer state bridge ---
|
|
201
|
+
// Logic lives in src/server-viewer-state.ts so dev (Vite plugin)
|
|
202
|
+
// and prod share it. Only the HTTP wiring is here.
|
|
203
|
+
if (url === "/api/viewer-state" && req.method === "PUT") {
|
|
290
204
|
let body = "";
|
|
291
205
|
req.on("data", (chunk) => { body += chunk.toString(); });
|
|
292
206
|
req.on("end", async () => {
|
|
293
207
|
try {
|
|
294
|
-
|
|
295
|
-
await storage.createBranch(graphName, branchName, from);
|
|
208
|
+
await writeViewerState(body);
|
|
296
209
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
297
|
-
res.end(
|
|
210
|
+
res.end('{"ok":true}');
|
|
298
211
|
} catch (err) {
|
|
299
212
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
300
213
|
res.end(JSON.stringify({ error: err.message }));
|
|
@@ -303,171 +216,103 @@ if (hasDistBuild) {
|
|
|
303
216
|
return;
|
|
304
217
|
}
|
|
305
218
|
|
|
306
|
-
|
|
307
|
-
const snapshotMatch = url.match(/^\/api\/graphs\/(.+)\/snapshots$/);
|
|
308
|
-
if (snapshotMatch && req.method === "GET") {
|
|
309
|
-
const graphName = decodeURIComponent(snapshotMatch[1]);
|
|
219
|
+
if (url === "/api/viewer-state" && req.method === "GET") {
|
|
310
220
|
try {
|
|
311
|
-
const
|
|
221
|
+
const data = await readViewerState();
|
|
312
222
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
313
|
-
res.end(
|
|
314
|
-
} catch
|
|
315
|
-
res.writeHead(
|
|
316
|
-
res.end(
|
|
223
|
+
res.end(data);
|
|
224
|
+
} catch {
|
|
225
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
226
|
+
res.end('{"error":"no viewer state"}');
|
|
317
227
|
}
|
|
318
228
|
return;
|
|
319
229
|
}
|
|
320
230
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
req.on("data", (chunk) => { body += chunk.toString(); });
|
|
325
|
-
req.on("end", async () => {
|
|
326
|
-
try {
|
|
327
|
-
const { label } = JSON.parse(body);
|
|
328
|
-
await storage.createSnapshot(graphName, label);
|
|
329
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
330
|
-
res.end(JSON.stringify({ ok: true }));
|
|
331
|
-
} catch (err) {
|
|
332
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
333
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
334
|
-
}
|
|
335
|
-
});
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// --- Rollback route ---
|
|
340
|
-
const rollbackMatch = url.match(/^\/api\/graphs\/(.+)\/rollback$/);
|
|
341
|
-
if (rollbackMatch && req.method === "POST") {
|
|
342
|
-
const graphName = decodeURIComponent(rollbackMatch[1]);
|
|
343
|
-
let body = "";
|
|
344
|
-
req.on("data", (chunk) => { body += chunk.toString(); });
|
|
345
|
-
req.on("end", async () => {
|
|
346
|
-
try {
|
|
347
|
-
const { version } = JSON.parse(body);
|
|
348
|
-
await storage.rollback(graphName, version);
|
|
349
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
350
|
-
res.end(JSON.stringify({ ok: true }));
|
|
351
|
-
} catch (err) {
|
|
352
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
353
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
354
|
-
}
|
|
355
|
-
});
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
231
|
+
// --- Extension system ---
|
|
232
|
+
// Generic per-extension endpoints. Logic lives in
|
|
233
|
+
// src/server-extensions.ts so dev and prod share it.
|
|
358
234
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
const graphName = decodeURIComponent(diffMatch[1]);
|
|
363
|
-
const version = parseInt(diffMatch[2], 10);
|
|
364
|
-
try {
|
|
365
|
-
const current = await storage.loadOntology(graphName);
|
|
366
|
-
const snapshot = await storage.loadSnapshot(graphName, version);
|
|
367
|
-
const currentNodeIds = new Set(current.nodes.map(n => n.id));
|
|
368
|
-
const snapshotNodeIds = new Set(snapshot.nodes.map(n => n.id));
|
|
369
|
-
const currentEdgeIds = new Set(current.edges.map(e => e.id));
|
|
370
|
-
const snapshotEdgeIds = new Set(snapshot.edges.map(e => e.id));
|
|
371
|
-
const diff = {
|
|
372
|
-
nodesAdded: current.nodes.filter(n => !snapshotNodeIds.has(n.id)).length,
|
|
373
|
-
nodesRemoved: snapshot.nodes.filter(n => !currentNodeIds.has(n.id)).length,
|
|
374
|
-
edgesAdded: current.edges.filter(e => !snapshotEdgeIds.has(e.id)).length,
|
|
375
|
-
edgesRemoved: snapshot.edges.filter(e => !currentEdgeIds.has(e.id)).length,
|
|
376
|
-
};
|
|
377
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
378
|
-
res.end(JSON.stringify(diff));
|
|
379
|
-
} catch (err) {
|
|
380
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
381
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
382
|
-
}
|
|
235
|
+
if (url === "/api/extensions" && req.method === "GET") {
|
|
236
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
237
|
+
res.end(JSON.stringify(loadedExtensions.map(publicExtensionInfo)));
|
|
383
238
|
return;
|
|
384
239
|
}
|
|
385
240
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
const
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
const snippet = await storage.loadSnippet(graphName, snippetId);
|
|
393
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
394
|
-
res.end(JSON.stringify(snippet));
|
|
395
|
-
} catch {
|
|
241
|
+
const extFileMatch = url.match(/^\/extensions\/([^/]+)\/(.+)$/);
|
|
242
|
+
if (extFileMatch && req.method === "GET") {
|
|
243
|
+
const extName = decodeURIComponent(extFileMatch[1]);
|
|
244
|
+
const subPath = decodeURIComponent(extFileMatch[2]);
|
|
245
|
+
const filePath = resolveExtensionFile(loadedExtensions, extName, subPath);
|
|
246
|
+
if (!filePath) {
|
|
396
247
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
397
|
-
res.end(
|
|
398
|
-
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
if (snippetItemMatch && req.method === "DELETE") {
|
|
403
|
-
const graphName = decodeURIComponent(snippetItemMatch[1]);
|
|
404
|
-
const snippetId = decodeURIComponent(snippetItemMatch[2]);
|
|
405
|
-
try {
|
|
406
|
-
await storage.deleteSnippet(graphName, snippetId);
|
|
407
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
408
|
-
res.end(JSON.stringify({ ok: true }));
|
|
409
|
-
} catch (err) {
|
|
410
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
411
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
248
|
+
res.end('{"error":"extension file not found"}');
|
|
249
|
+
return;
|
|
412
250
|
}
|
|
413
|
-
return;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
const snippetMatch = url.match(/^\/api\/graphs\/(.+)\/snippets$/);
|
|
417
|
-
if (snippetMatch && req.method === "GET") {
|
|
418
|
-
const graphName = decodeURIComponent(snippetMatch[1]);
|
|
419
251
|
try {
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
|
|
252
|
+
const data = fs.readFileSync(filePath);
|
|
253
|
+
const ext = path.extname(filePath);
|
|
254
|
+
const mime = MIME_TYPES[ext] || "application/octet-stream";
|
|
255
|
+
res.writeHead(200, { "Content-Type": mime });
|
|
256
|
+
res.end(data);
|
|
423
257
|
} catch {
|
|
424
|
-
res.writeHead(
|
|
425
|
-
res.end("
|
|
258
|
+
res.writeHead(404);
|
|
259
|
+
res.end("Not found");
|
|
426
260
|
}
|
|
427
261
|
return;
|
|
428
262
|
}
|
|
429
263
|
|
|
430
|
-
|
|
431
|
-
|
|
264
|
+
const extFetchMatch = url.match(/^\/api\/extensions\/([^/]+)\/fetch$/);
|
|
265
|
+
if (extFetchMatch && req.method === "POST") {
|
|
266
|
+
const extName = decodeURIComponent(extFetchMatch[1]);
|
|
267
|
+
const ext = findExtension(loadedExtensions, extName);
|
|
268
|
+
if (!ext) {
|
|
269
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
270
|
+
res.end(JSON.stringify({ error: `unknown extension: ${extName}` }));
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
432
273
|
let body = "";
|
|
433
274
|
req.on("data", (chunk) => { body += chunk.toString(); });
|
|
434
275
|
req.on("end", async () => {
|
|
435
276
|
try {
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
277
|
+
const result = await proxyExtensionFetch(ext, body);
|
|
278
|
+
if (result.errorJson || !result.upstreamBody) {
|
|
279
|
+
res.writeHead(result.status, { "Content-Type": "application/json" });
|
|
280
|
+
res.end(result.errorJson ?? JSON.stringify({ error: "proxy failed" }));
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const upstreamCT =
|
|
284
|
+
result.upstreamHeaders?.get("content-type") ?? "application/octet-stream";
|
|
285
|
+
res.writeHead(result.status, {
|
|
286
|
+
"Content-Type": upstreamCT,
|
|
287
|
+
"Cache-Control": "no-cache",
|
|
288
|
+
Connection: "keep-alive",
|
|
289
|
+
});
|
|
290
|
+
const reader = result.upstreamBody.getReader();
|
|
291
|
+
while (true) {
|
|
292
|
+
const { done, value } = await reader.read();
|
|
293
|
+
if (done) break;
|
|
294
|
+
res.write(value);
|
|
295
|
+
}
|
|
296
|
+
res.end();
|
|
440
297
|
} catch (err) {
|
|
441
|
-
res.
|
|
442
|
-
|
|
298
|
+
if (!res.headersSent) {
|
|
299
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
300
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
301
|
+
} else {
|
|
302
|
+
res.end();
|
|
303
|
+
}
|
|
443
304
|
}
|
|
444
305
|
});
|
|
445
306
|
return;
|
|
446
307
|
}
|
|
447
308
|
|
|
448
|
-
|
|
449
|
-
if (
|
|
450
|
-
|
|
451
|
-
const list = await listBackpacks();
|
|
452
|
-
const active = await getActiveBackpack();
|
|
453
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
454
|
-
res.end(
|
|
455
|
-
JSON.stringify(
|
|
456
|
-
list.map((b) => ({ ...b, active: b.name === active.name })),
|
|
457
|
-
),
|
|
458
|
-
);
|
|
459
|
-
} catch (err) {
|
|
460
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
461
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
462
|
-
}
|
|
463
|
-
return;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
if (url === "/api/backpacks/active" && req.method === "GET") {
|
|
309
|
+
const extSettingsAllMatch = url.match(/^\/api\/extensions\/([^/]+)\/settings$/);
|
|
310
|
+
if (extSettingsAllMatch && req.method === "GET") {
|
|
311
|
+
const extName = decodeURIComponent(extSettingsAllMatch[1]);
|
|
467
312
|
try {
|
|
468
|
-
const
|
|
313
|
+
const all = await readExtensionSettings(extName);
|
|
469
314
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
470
|
-
res.end(JSON.stringify(
|
|
315
|
+
res.end(JSON.stringify(all));
|
|
471
316
|
} catch (err) {
|
|
472
317
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
473
318
|
res.end(JSON.stringify({ error: err.message }));
|
|
@@ -475,41 +320,34 @@ if (hasDistBuild) {
|
|
|
475
320
|
return;
|
|
476
321
|
}
|
|
477
322
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
323
|
+
const extSettingsKeyMatch = url.match(/^\/api\/extensions\/([^/]+)\/settings\/([^/]+)$/);
|
|
324
|
+
if (extSettingsKeyMatch && (req.method === "PUT" || req.method === "DELETE")) {
|
|
325
|
+
const extName = decodeURIComponent(extSettingsKeyMatch[1]);
|
|
326
|
+
const key = decodeURIComponent(extSettingsKeyMatch[2]);
|
|
327
|
+
if (req.method === "DELETE") {
|
|
482
328
|
try {
|
|
483
|
-
|
|
484
|
-
await setActiveBackpack(name);
|
|
485
|
-
const swapped = await makeBackend();
|
|
486
|
-
storage = swapped.backend;
|
|
487
|
-
activeEntry = swapped.entry;
|
|
329
|
+
await deleteExtensionSetting(extName, key);
|
|
488
330
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
489
|
-
res.end(
|
|
331
|
+
res.end('{"ok":true}');
|
|
490
332
|
} catch (err) {
|
|
491
|
-
res.writeHead(
|
|
333
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
492
334
|
res.end(JSON.stringify({ error: err.message }));
|
|
493
335
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
if (url === "/api/backpacks" && req.method === "POST") {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
499
338
|
let body = "";
|
|
500
339
|
req.on("data", (chunk) => { body += chunk.toString(); });
|
|
501
340
|
req.on("end", async () => {
|
|
502
341
|
try {
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
storage = swapped.backend;
|
|
509
|
-
activeEntry = swapped.entry;
|
|
342
|
+
const parsed = JSON.parse(body);
|
|
343
|
+
if (!("value" in parsed)) {
|
|
344
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
345
|
+
res.end('{"error":"body must include {value}"}');
|
|
346
|
+
return;
|
|
510
347
|
}
|
|
348
|
+
await writeExtensionSetting(extName, key, parsed.value);
|
|
511
349
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
512
|
-
res.end(
|
|
350
|
+
res.end('{"ok":true}');
|
|
513
351
|
} catch (err) {
|
|
514
352
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
515
353
|
res.end(JSON.stringify({ error: err.message }));
|
|
@@ -518,93 +356,11 @@ if (hasDistBuild) {
|
|
|
518
356
|
return;
|
|
519
357
|
}
|
|
520
358
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
// If we just removed the active one, the registry switched us;
|
|
527
|
-
// rebuild the backend to match.
|
|
528
|
-
if (activeEntry && activeEntry.name === name) {
|
|
529
|
-
const swapped = await makeBackend();
|
|
530
|
-
storage = swapped.backend;
|
|
531
|
-
activeEntry = swapped.entry;
|
|
532
|
-
}
|
|
533
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
534
|
-
res.end(JSON.stringify({ ok: true }));
|
|
535
|
-
} catch (err) {
|
|
536
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
537
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
538
|
-
}
|
|
539
|
-
return;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
// --- Lock heartbeat ---
|
|
543
|
-
if (url === "/api/locks" && req.method === "GET") {
|
|
544
|
-
// Batch endpoint: returns { graphName: lockInfo|null } for all graphs.
|
|
545
|
-
// One request instead of N on every sidebar refresh.
|
|
546
|
-
try {
|
|
547
|
-
const summaries = await storage.listOntologies();
|
|
548
|
-
const result = {};
|
|
549
|
-
if (typeof storage.readLock === "function") {
|
|
550
|
-
await Promise.all(
|
|
551
|
-
summaries.map(async (s) => {
|
|
552
|
-
try {
|
|
553
|
-
result[s.name] = await storage.readLock(s.name);
|
|
554
|
-
} catch {
|
|
555
|
-
result[s.name] = null;
|
|
556
|
-
}
|
|
557
|
-
}),
|
|
558
|
-
);
|
|
559
|
-
}
|
|
560
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
561
|
-
res.end(JSON.stringify(result));
|
|
562
|
-
} catch {
|
|
563
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
564
|
-
res.end("{}");
|
|
565
|
-
}
|
|
566
|
-
return;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
const lockMatch = url.match(/^\/api\/graphs\/(.+)\/lock$/);
|
|
570
|
-
if (lockMatch && req.method === "GET") {
|
|
571
|
-
const graphName = decodeURIComponent(lockMatch[1]);
|
|
572
|
-
try {
|
|
573
|
-
const lock =
|
|
574
|
-
typeof storage.readLock === "function"
|
|
575
|
-
? await storage.readLock(graphName)
|
|
576
|
-
: null;
|
|
577
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
578
|
-
res.end(JSON.stringify(lock));
|
|
579
|
-
} catch {
|
|
580
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
581
|
-
res.end("null");
|
|
582
|
-
}
|
|
583
|
-
return;
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
if (url === "/api/ontologies") {
|
|
587
|
-
try {
|
|
588
|
-
const summaries = await storage.listOntologies();
|
|
589
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
590
|
-
res.end(JSON.stringify(summaries));
|
|
591
|
-
} catch {
|
|
592
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
593
|
-
res.end("[]");
|
|
594
|
-
}
|
|
595
|
-
return;
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
if (url.startsWith("/api/ontologies/")) {
|
|
599
|
-
const name = decodeURIComponent(url.replace("/api/ontologies/", ""));
|
|
600
|
-
try {
|
|
601
|
-
const data = await storage.loadOntology(name);
|
|
602
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
603
|
-
res.end(JSON.stringify(data));
|
|
604
|
-
} catch {
|
|
605
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
606
|
-
res.end(JSON.stringify({ error: "Ontology not found" }));
|
|
607
|
-
}
|
|
359
|
+
// --- Shared API routes ---
|
|
360
|
+
// All the other API endpoints (config, version-check, ontologies,
|
|
361
|
+
// backpacks, branches, snapshots, snippets, locks, remotes, etc.)
|
|
362
|
+
// live in src/server-api-routes.ts so dev and prod share them.
|
|
363
|
+
if (await handleApiRequest(req, res, apiContext)) {
|
|
608
364
|
return;
|
|
609
365
|
}
|
|
610
366
|
|
|
@@ -638,6 +394,13 @@ if (hasDistBuild) {
|
|
|
638
394
|
server.listen(port, bindHost, () => {
|
|
639
395
|
const displayHost = bindHost === "0.0.0.0" || bindHost === "::" ? "localhost" : bindHost;
|
|
640
396
|
console.log(` Backpack Viewer v${currentVersion} running at http://${displayHost}:${port}/`);
|
|
397
|
+
if (loadedExtensions.length > 0) {
|
|
398
|
+
console.log(
|
|
399
|
+
` ${loadedExtensions.length} extension(s) loaded: ${loadedExtensions.map((e) => e.manifest.name).join(", ")}`,
|
|
400
|
+
);
|
|
401
|
+
} else {
|
|
402
|
+
console.log(" No extensions loaded");
|
|
403
|
+
}
|
|
641
404
|
const isLoopback =
|
|
642
405
|
bindHost === "127.0.0.1" ||
|
|
643
406
|
bindHost === "localhost" ||
|
|
@@ -654,7 +417,7 @@ if (hasDistBuild) {
|
|
|
654
417
|
fetchLatestVersion("backpack-viewer").then((latest) => {
|
|
655
418
|
if (latest && latest !== currentVersion) {
|
|
656
419
|
console.warn("");
|
|
657
|
-
console.warn(`
|
|
420
|
+
console.warn(` Backpack Viewer ${currentVersion} is out of date — latest is ${latest}`);
|
|
658
421
|
console.warn(` To update:`);
|
|
659
422
|
console.warn(` npm cache clean --force`);
|
|
660
423
|
console.warn(` npx backpack-viewer@latest`);
|
|
@@ -669,7 +432,7 @@ if (hasDistBuild) {
|
|
|
669
432
|
const server = await createServer({
|
|
670
433
|
root,
|
|
671
434
|
configFile: path.resolve(root, "vite.config.ts"),
|
|
672
|
-
server: { port, open: true },
|
|
435
|
+
server: { port: parseInt(process.env.PORT || "5173", 10), open: true },
|
|
673
436
|
});
|
|
674
437
|
|
|
675
438
|
await server.listen();
|