@karmaniverous/jeeves-server 3.4.2 → 3.5.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/.tsbuildinfo +1 -1
- package/CHANGELOG.md +47 -1
- package/README.md +18 -17
- package/client/package.json +20 -19
- package/client/src/components/SearchModal.tsx +11 -1
- package/client/src/components/layout/Header.tsx +3 -3
- package/client/src/lib/api.ts +10 -5
- package/dist/client/assets/CodeEditor-Brh86AGF.js +1 -0
- package/dist/client/assets/CodeViewer-Cegj3cEn.js +1 -0
- package/dist/client/assets/dist-2YqVIvgv.js +2 -0
- package/dist/client/assets/dist-5vamY028.js +1 -0
- package/dist/client/assets/dist-6_auAGci.js +1 -0
- package/dist/client/assets/dist-B0kq1DQG.js +1 -0
- package/dist/client/assets/dist-B2SZD_eN.js +1 -0
- package/dist/client/assets/dist-B2t4dYA2.js +1 -0
- package/dist/client/assets/dist-B5gFYAn7.js +1 -0
- package/dist/client/assets/dist-BPy6CnYN.js +1 -0
- package/dist/client/assets/dist-CL6VCrQn.js +9 -0
- package/dist/client/assets/dist-CWsHar9N.js +1 -0
- package/dist/client/assets/dist-CnFc5Ssx.js +1 -0
- package/dist/client/assets/dist-DSgLBuTS.js +1 -0
- package/dist/client/assets/dist-DUcac0X_.js +7 -0
- package/dist/client/assets/dist-DcTcc-BG.js +6 -0
- package/dist/client/assets/dist-DvfTyWk_.js +1 -0
- package/dist/client/assets/dist-Dz1Ulpqa.js +1 -0
- package/dist/client/assets/dist-Kr-mUYW1.js +5 -0
- package/dist/client/assets/dist-OX4k3MMG.js +2 -0
- package/dist/client/assets/dist-qiU0qoeK.js +1 -0
- package/dist/client/assets/dist-ui4J6fvl.js +23 -0
- package/dist/client/assets/index-Dk_myGs4.css +2 -0
- package/dist/client/assets/index-DrBXupPz.js +62 -0
- package/dist/client/assets/theme-CPpIxvB0.js +2 -0
- package/dist/client/index.html +3 -2
- package/dist/src/cli/commands/config.test.js +6 -41
- package/dist/src/cli/index.js +9 -15
- package/dist/src/cli/start-server.js +16 -0
- package/dist/src/config/index.js +48 -37
- package/dist/src/config/loadConfig.test.js +27 -25
- package/dist/src/config/migration.js +60 -0
- package/dist/src/config/schema.js +4 -3
- package/dist/src/descriptor.js +51 -0
- package/dist/src/routes/api/diagramExport.js +101 -0
- package/dist/src/routes/api/diagramExport.test.js +134 -0
- package/dist/src/routes/api/events.js +13 -0
- package/dist/src/routes/api/export.js +6 -82
- package/dist/src/routes/api/index.js +4 -0
- package/dist/src/routes/api/search.js +9 -50
- package/dist/src/routes/api/sharing.js +40 -23
- package/dist/src/routes/api/sharing.test.js +52 -0
- package/dist/src/routes/auth.js +1 -1
- package/dist/src/routes/config.js +8 -2
- package/dist/src/routes/keys.js +4 -4
- package/dist/src/routes/path/index.js +1 -1
- package/dist/src/routes/status.js +15 -16
- package/dist/src/routes/status.test.js +13 -8
- package/dist/src/server.js +21 -16
- package/dist/src/services/markdown.js +2 -1
- package/dist/src/services/markdown.test.js +22 -0
- package/dist/src/util/packageVersion.js +7 -16
- package/dist/src/util/packageVersion.test.js +7 -0
- package/guides/api-integration.md +4 -0
- package/guides/deployment.md +11 -10
- package/guides/event-gateway.md +4 -0
- package/guides/exports.md +4 -0
- package/guides/index.md +1 -1
- package/guides/setup.md +17 -16
- package/guides/sharing.md +4 -0
- package/package.json +3 -3
- package/scripts/download-plantuml.js +0 -1
- package/src/cli/commands/config.test.ts +6 -46
- package/src/cli/index.ts +9 -16
- package/src/cli/start-server.ts +21 -0
- package/src/config/index.ts +56 -43
- package/src/config/loadConfig.test.ts +27 -29
- package/src/config/migration.ts +76 -0
- package/src/config/schema.ts +5 -4
- package/src/descriptor.ts +60 -0
- package/src/routes/api/diagramExport.test.ts +200 -0
- package/src/routes/api/diagramExport.ts +170 -0
- package/src/routes/api/events.ts +22 -0
- package/src/routes/api/export.ts +6 -131
- package/src/routes/api/index.ts +4 -0
- package/src/routes/api/search.ts +9 -63
- package/src/routes/api/sharing.test.ts +66 -0
- package/src/routes/api/sharing.ts +47 -23
- package/src/routes/auth.ts +1 -1
- package/src/routes/config.ts +15 -2
- package/src/routes/keys.ts +4 -4
- package/src/routes/path/index.ts +1 -1
- package/src/routes/status.test.ts +14 -8
- package/src/routes/status.ts +56 -62
- package/src/server.ts +29 -17
- package/src/services/markdown.test.ts +26 -0
- package/src/services/markdown.ts +2 -1
- package/src/util/packageVersion.test.ts +9 -0
- package/src/util/packageVersion.ts +11 -18
- package/src/util/platform.ts +1 -1
- package/dist/client/assets/CodeEditor-DQZZL5Rq.js +0 -1
- package/dist/client/assets/CodeViewer-ofJVD1Vn.js +0 -1
- package/dist/client/assets/index--MBieNJA.js +0 -1
- package/dist/client/assets/index-BENeXQI_.js +0 -1
- package/dist/client/assets/index-BbBpoOxz.js +0 -1
- package/dist/client/assets/index-BdV9g5AM.js +0 -6
- package/dist/client/assets/index-BjAilRri.js +0 -2
- package/dist/client/assets/index-BqbhWo2I.js +0 -3
- package/dist/client/assets/index-CVbycZ0H.js +0 -1
- package/dist/client/assets/index-Cs5oz2oJ.js +0 -5
- package/dist/client/assets/index-D-RC7ZS6.css +0 -1
- package/dist/client/assets/index-D8KZVveX.js +0 -1
- package/dist/client/assets/index-DC4HMHxY.js +0 -13
- package/dist/client/assets/index-DcY2RXqX.js +0 -1
- package/dist/client/assets/index-Duy-tZYV.js +0 -1
- package/dist/client/assets/index-Dw7rDFmE.js +0 -7
- package/dist/client/assets/index-FlCUvrjv.js +0 -2
- package/dist/client/assets/index-K6OVmfhg.js +0 -1
- package/dist/client/assets/index-MLwyFRN0.js +0 -1
- package/dist/client/assets/index-OpqBpSjn.js +0 -1
- package/dist/client/assets/index-SsHei0HE.js +0 -1
- package/dist/client/assets/index-jSGuHSeS.js +0 -62
- package/dist/client/assets/index-uQa2yckk.js +0 -1
- package/dist/client/assets/index-udkXoIER.js +0 -1
- package/dist/src/cli/commands/config.js +0 -105
- package/dist/src/cli/commands/service.js +0 -93
- package/dist/src/cli/commands/start.js +0 -24
- package/src/cli/commands/config.ts +0 -117
- package/src/cli/commands/service.ts +0 -129
- package/src/cli/commands/start.ts +0 -27
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diagram export routes — standalone Mermaid and PlantUML file rendering.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from export.ts to maintain single responsibility and stay within
|
|
5
|
+
* the 300 LOC limit. Both handlers share a common cache-then-render pattern
|
|
6
|
+
* via `serveDiagramExport()`.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
|
|
14
|
+
import type { FastifyPluginAsync, FastifyReply } from 'fastify';
|
|
15
|
+
|
|
16
|
+
import { getConfig } from '../../config/index.js';
|
|
17
|
+
import {
|
|
18
|
+
cacheDiagramBuffer,
|
|
19
|
+
getCachedDiagramBuffer,
|
|
20
|
+
} from '../../services/diagramCache.js';
|
|
21
|
+
import { renderMermaidToFile } from '../../services/mermaid.js';
|
|
22
|
+
import {
|
|
23
|
+
getPlantUmlFormats,
|
|
24
|
+
renderPlantUmlToBuffer,
|
|
25
|
+
} from '../../services/plantuml.js';
|
|
26
|
+
import { DIAGRAM_CONTENT_TYPES } from '../../util/fileDetection.js';
|
|
27
|
+
import { getRoots, type RootEntry, urlPathToFs } from '../../util/platform.js';
|
|
28
|
+
|
|
29
|
+
/** Diagram engine identifier for cache keys. */
|
|
30
|
+
type DiagramEngine = 'mermaid' | 'plantuml';
|
|
31
|
+
|
|
32
|
+
/** Resolve a URL path to a validated filesystem path, or send an error reply. */
|
|
33
|
+
function resolveExportPath(
|
|
34
|
+
reqPath: string | undefined,
|
|
35
|
+
roots: RootEntry[],
|
|
36
|
+
reply: FastifyReply,
|
|
37
|
+
): string | null {
|
|
38
|
+
if (!reqPath) {
|
|
39
|
+
void reply.code(400).send({ error: 'Path required' });
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const fsPath = urlPathToFs(reqPath, roots);
|
|
43
|
+
if (!fsPath) {
|
|
44
|
+
void reply.code(404).send({ error: 'Invalid path' });
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return path.resolve(fsPath);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Send a diagram buffer with appropriate content-type and disposition. */
|
|
51
|
+
function sendDiagram(
|
|
52
|
+
reply: FastifyReply,
|
|
53
|
+
buffer: Buffer | Uint8Array,
|
|
54
|
+
format: string,
|
|
55
|
+
downloadName: string,
|
|
56
|
+
): void {
|
|
57
|
+
void reply
|
|
58
|
+
.header(
|
|
59
|
+
'Content-Type',
|
|
60
|
+
DIAGRAM_CONTENT_TYPES[format] ?? 'application/octet-stream',
|
|
61
|
+
)
|
|
62
|
+
.header('Content-Disposition', `attachment; filename="${downloadName}"`)
|
|
63
|
+
.send(buffer);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Serve a diagram export with cache-first semantics.
|
|
68
|
+
*
|
|
69
|
+
* Checks the diagram cache, renders on miss, caches the result, and sends
|
|
70
|
+
* the response. This eliminates the duplicated cache-then-render pattern
|
|
71
|
+
* between the Mermaid and PlantUML handlers.
|
|
72
|
+
*/
|
|
73
|
+
async function serveDiagramExport(
|
|
74
|
+
reply: FastifyReply,
|
|
75
|
+
engine: DiagramEngine,
|
|
76
|
+
resolved: string,
|
|
77
|
+
ext: string,
|
|
78
|
+
format: string,
|
|
79
|
+
render: (resolved: string, format: string) => Promise<Buffer | null>,
|
|
80
|
+
): Promise<void> {
|
|
81
|
+
const source = fs.readFileSync(resolved, 'utf8');
|
|
82
|
+
const baseName = path.basename(resolved, ext);
|
|
83
|
+
const downloadName = `${baseName}.${format}`;
|
|
84
|
+
|
|
85
|
+
const cached = getCachedDiagramBuffer(engine, source, format);
|
|
86
|
+
if (cached) {
|
|
87
|
+
sendDiagram(reply, cached, format, downloadName);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const buffer = await render(resolved, format);
|
|
92
|
+
if (!buffer) {
|
|
93
|
+
void reply.code(500).send({
|
|
94
|
+
error: `${engine.charAt(0).toUpperCase() + engine.slice(1)} render failed`,
|
|
95
|
+
});
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
cacheDiagramBuffer(engine, source, buffer, format);
|
|
100
|
+
sendDiagram(reply, buffer, format, downloadName);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
104
|
+
export const diagramExportRoutes: FastifyPluginAsync = async (fastify) => {
|
|
105
|
+
const roots = getRoots(getConfig().roots);
|
|
106
|
+
|
|
107
|
+
// GET /api/mermaid-export/*
|
|
108
|
+
fastify.get<{ Params: { '*': string }; Querystring: { format?: string } }>(
|
|
109
|
+
'/api/mermaid-export/*',
|
|
110
|
+
async (request, reply) => {
|
|
111
|
+
const resolved = resolveExportPath(request.params['*'], roots, reply);
|
|
112
|
+
if (!resolved) return;
|
|
113
|
+
|
|
114
|
+
if (
|
|
115
|
+
!fs.existsSync(resolved) ||
|
|
116
|
+
!resolved.toLowerCase().endsWith('.mmd')
|
|
117
|
+
) {
|
|
118
|
+
return reply.code(404).send({ error: 'Mermaid file not found' });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const mermaidFormats = ['svg', 'png', 'pdf'];
|
|
122
|
+
const format = mermaidFormats.includes(request.query.format ?? '')
|
|
123
|
+
? request.query.format!
|
|
124
|
+
: 'svg';
|
|
125
|
+
|
|
126
|
+
await serveDiagramExport(
|
|
127
|
+
reply,
|
|
128
|
+
'mermaid',
|
|
129
|
+
resolved,
|
|
130
|
+
'.mmd',
|
|
131
|
+
format,
|
|
132
|
+
async (filePath, fmt) => {
|
|
133
|
+
const outFile = await renderMermaidToFile(filePath, fmt);
|
|
134
|
+
return outFile ? fs.readFileSync(outFile) : null;
|
|
135
|
+
},
|
|
136
|
+
);
|
|
137
|
+
},
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// GET /api/plantuml-export/*
|
|
141
|
+
fastify.get<{ Params: { '*': string }; Querystring: { format?: string } }>(
|
|
142
|
+
'/api/plantuml-export/*',
|
|
143
|
+
async (request, reply) => {
|
|
144
|
+
const resolved = resolveExportPath(request.params['*'], roots, reply);
|
|
145
|
+
if (!resolved) return;
|
|
146
|
+
|
|
147
|
+
const ext = path.extname(resolved).toLowerCase();
|
|
148
|
+
if (
|
|
149
|
+
!fs.existsSync(resolved) ||
|
|
150
|
+
!['.puml', '.plantuml', '.pu'].includes(ext)
|
|
151
|
+
) {
|
|
152
|
+
return reply.code(404).send({ error: 'PlantUML file not found' });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const supported = getPlantUmlFormats();
|
|
156
|
+
const format = supported.includes(request.query.format ?? '')
|
|
157
|
+
? request.query.format!
|
|
158
|
+
: 'svg';
|
|
159
|
+
|
|
160
|
+
await serveDiagramExport(
|
|
161
|
+
reply,
|
|
162
|
+
'plantuml',
|
|
163
|
+
resolved,
|
|
164
|
+
ext,
|
|
165
|
+
format,
|
|
166
|
+
renderPlantUmlToBuffer,
|
|
167
|
+
);
|
|
168
|
+
},
|
|
169
|
+
);
|
|
170
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/events — returns recent event log entries.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { FastifyPluginAsync } from 'fastify';
|
|
6
|
+
|
|
7
|
+
import { getRecentEvents } from '../../services/eventLog.js';
|
|
8
|
+
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
10
|
+
export const eventsRoutes: FastifyPluginAsync = async (fastify) => {
|
|
11
|
+
fastify.get<{ Querystring: { limit?: string } }>(
|
|
12
|
+
'/events',
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
14
|
+
async (request) => {
|
|
15
|
+
const limit = Math.min(
|
|
16
|
+
Math.max(parseInt(request.query.limit ?? '20', 10) || 20, 1),
|
|
17
|
+
100,
|
|
18
|
+
);
|
|
19
|
+
return getRecentEvents(limit);
|
|
20
|
+
},
|
|
21
|
+
);
|
|
22
|
+
};
|
package/src/routes/api/export.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Document and directory export routes.
|
|
3
3
|
*
|
|
4
|
-
* Handles: /api/export
|
|
4
|
+
* Handles: /api/export/* (PDF/DOCX/ZIP), /api/export-cache/* (cache clearing).
|
|
5
|
+
* Diagram export routes (Mermaid/PlantUML) are in diagramExport.ts.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
5
8
|
*/
|
|
6
9
|
|
|
7
10
|
import fs from 'node:fs';
|
|
@@ -11,10 +14,6 @@ import archiver from 'archiver';
|
|
|
11
14
|
import type { FastifyPluginAsync } from 'fastify';
|
|
12
15
|
|
|
13
16
|
import { getConfig } from '../../config/index.js';
|
|
14
|
-
import {
|
|
15
|
-
cacheDiagramBuffer,
|
|
16
|
-
getCachedDiagramBuffer,
|
|
17
|
-
} from '../../services/diagramCache.js';
|
|
18
17
|
import { appendEvent } from '../../services/eventQueue.js';
|
|
19
18
|
import { type ExportFormat, exportPage } from '../../services/export.js';
|
|
20
19
|
import {
|
|
@@ -24,12 +23,6 @@ import {
|
|
|
24
23
|
clearStandaloneDiagramCache,
|
|
25
24
|
getCachedExport,
|
|
26
25
|
} from '../../services/exportCache.js';
|
|
27
|
-
import { renderMermaidToFile } from '../../services/mermaid.js';
|
|
28
|
-
import {
|
|
29
|
-
getPlantUmlFormats,
|
|
30
|
-
renderPlantUmlToBuffer,
|
|
31
|
-
} from '../../services/plantuml.js';
|
|
32
|
-
import { DIAGRAM_CONTENT_TYPES } from '../../util/fileDetection.js';
|
|
33
26
|
import { getDirSize, getRoots, urlPathToFs } from '../../util/platform.js';
|
|
34
27
|
|
|
35
28
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
@@ -109,7 +102,7 @@ export const exportRoutes: FastifyPluginAsync = async (fastify) => {
|
|
|
109
102
|
.code(500)
|
|
110
103
|
.send({ error: 'Export unavailable — no internal key configured' });
|
|
111
104
|
|
|
112
|
-
const exportUrl = `http://
|
|
105
|
+
const exportUrl = `http://127.0.0.1:${String(port)}/browse/${reqPath}?key=${exportKey}&render_diagrams=1&plain_code=1`;
|
|
113
106
|
const fileName = path.basename(resolved);
|
|
114
107
|
const baseName = fileName.replace(/\.md$/i, '');
|
|
115
108
|
|
|
@@ -193,122 +186,4 @@ export const exportRoutes: FastifyPluginAsync = async (fastify) => {
|
|
|
193
186
|
});
|
|
194
187
|
},
|
|
195
188
|
);
|
|
196
|
-
|
|
197
|
-
// GET /api/mermaid-export/*
|
|
198
|
-
fastify.get<{ Params: { '*': string }; Querystring: { format?: string } }>(
|
|
199
|
-
'/api/mermaid-export/*',
|
|
200
|
-
async (request, reply) => {
|
|
201
|
-
const reqPath = request.params['*'];
|
|
202
|
-
if (!reqPath) return reply.code(400).send({ error: 'Path required' });
|
|
203
|
-
|
|
204
|
-
const mmdFsPath = urlPathToFs(reqPath, roots);
|
|
205
|
-
if (!mmdFsPath) return reply.code(404).send({ error: 'Invalid path' });
|
|
206
|
-
const resolved = path.resolve(mmdFsPath);
|
|
207
|
-
|
|
208
|
-
if (
|
|
209
|
-
!fs.existsSync(resolved) ||
|
|
210
|
-
!resolved.toLowerCase().endsWith('.mmd')
|
|
211
|
-
) {
|
|
212
|
-
return reply.code(404).send({ error: 'Mermaid file not found' });
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const mermaidFormats = ['svg', 'png', 'pdf'];
|
|
216
|
-
const format = mermaidFormats.includes(request.query.format ?? '')
|
|
217
|
-
? request.query.format!
|
|
218
|
-
: 'svg';
|
|
219
|
-
const source = fs.readFileSync(resolved, 'utf8');
|
|
220
|
-
|
|
221
|
-
// Check cache
|
|
222
|
-
const cachedBuffer = getCachedDiagramBuffer('mermaid', source, format);
|
|
223
|
-
if (cachedBuffer) {
|
|
224
|
-
const downloadName = `${path.basename(resolved, '.mmd')}.${format}`;
|
|
225
|
-
return reply
|
|
226
|
-
.header(
|
|
227
|
-
'Content-Type',
|
|
228
|
-
DIAGRAM_CONTENT_TYPES[format] ?? 'application/octet-stream',
|
|
229
|
-
)
|
|
230
|
-
.header(
|
|
231
|
-
'Content-Disposition',
|
|
232
|
-
`attachment; filename="${downloadName}"`,
|
|
233
|
-
)
|
|
234
|
-
.send(cachedBuffer);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const outFile = await renderMermaidToFile(resolved, format);
|
|
238
|
-
if (!outFile)
|
|
239
|
-
return reply.code(500).send({ error: 'Mermaid render failed' });
|
|
240
|
-
|
|
241
|
-
const content = fs.readFileSync(outFile);
|
|
242
|
-
cacheDiagramBuffer('mermaid', source, content, format);
|
|
243
|
-
const downloadName = path.basename(outFile);
|
|
244
|
-
|
|
245
|
-
return reply
|
|
246
|
-
.header(
|
|
247
|
-
'Content-Type',
|
|
248
|
-
DIAGRAM_CONTENT_TYPES[format] ?? 'application/octet-stream',
|
|
249
|
-
)
|
|
250
|
-
.header('Content-Disposition', `attachment; filename="${downloadName}"`)
|
|
251
|
-
.send(content);
|
|
252
|
-
},
|
|
253
|
-
);
|
|
254
|
-
|
|
255
|
-
// GET /api/plantuml-export/*
|
|
256
|
-
fastify.get<{ Params: { '*': string }; Querystring: { format?: string } }>(
|
|
257
|
-
'/api/plantuml-export/*',
|
|
258
|
-
async (request, reply) => {
|
|
259
|
-
const reqPath = request.params['*'];
|
|
260
|
-
if (!reqPath) return reply.code(400).send({ error: 'Path required' });
|
|
261
|
-
|
|
262
|
-
const pumlFsPath = urlPathToFs(reqPath, roots);
|
|
263
|
-
if (!pumlFsPath) return reply.code(404).send({ error: 'Invalid path' });
|
|
264
|
-
const resolved = path.resolve(pumlFsPath);
|
|
265
|
-
|
|
266
|
-
const ext = path.extname(resolved).toLowerCase();
|
|
267
|
-
if (
|
|
268
|
-
!fs.existsSync(resolved) ||
|
|
269
|
-
!['.puml', '.plantuml', '.pu'].includes(ext)
|
|
270
|
-
) {
|
|
271
|
-
return reply.code(404).send({ error: 'PlantUML file not found' });
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
const supported = getPlantUmlFormats();
|
|
275
|
-
const format = supported.includes(request.query.format ?? '')
|
|
276
|
-
? request.query.format!
|
|
277
|
-
: 'svg';
|
|
278
|
-
const source = fs.readFileSync(resolved, 'utf8');
|
|
279
|
-
|
|
280
|
-
const cachedBuffer = getCachedDiagramBuffer('plantuml', source, format);
|
|
281
|
-
if (cachedBuffer) {
|
|
282
|
-
const baseName = path.basename(resolved, ext);
|
|
283
|
-
return reply
|
|
284
|
-
.header(
|
|
285
|
-
'Content-Type',
|
|
286
|
-
DIAGRAM_CONTENT_TYPES[format] ?? 'application/octet-stream',
|
|
287
|
-
)
|
|
288
|
-
.header(
|
|
289
|
-
'Content-Disposition',
|
|
290
|
-
`attachment; filename="${baseName}.${format}"`,
|
|
291
|
-
)
|
|
292
|
-
.send(cachedBuffer);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
const buffer = await renderPlantUmlToBuffer(resolved, format);
|
|
296
|
-
if (!buffer)
|
|
297
|
-
return reply.code(500).send({ error: 'PlantUML render failed' });
|
|
298
|
-
|
|
299
|
-
cacheDiagramBuffer('plantuml', source, buffer, format);
|
|
300
|
-
const baseName = path.basename(resolved, ext);
|
|
301
|
-
|
|
302
|
-
return reply
|
|
303
|
-
.header(
|
|
304
|
-
'Content-Type',
|
|
305
|
-
DIAGRAM_CONTENT_TYPES[format] ?? 'application/octet-stream',
|
|
306
|
-
)
|
|
307
|
-
.header(
|
|
308
|
-
'Content-Disposition',
|
|
309
|
-
`attachment; filename="${baseName}.${format}"`,
|
|
310
|
-
)
|
|
311
|
-
.send(buffer);
|
|
312
|
-
},
|
|
313
|
-
);
|
|
314
189
|
};
|
package/src/routes/api/index.ts
CHANGED
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
import type { FastifyPluginAsync } from 'fastify';
|
|
6
6
|
|
|
7
7
|
import { authStatusRoutes } from './auth-status.js';
|
|
8
|
+
import { diagramExportRoutes } from './diagramExport.js';
|
|
8
9
|
import { diagramsRoutes } from './diagrams.js';
|
|
9
10
|
import { directoryRoutes } from './directory.js';
|
|
10
11
|
import { drivesRoutes } from './drives.js';
|
|
12
|
+
import { eventsRoutes } from './events.js';
|
|
11
13
|
import { exportRoutes } from './export.js';
|
|
12
14
|
import { fileContentRoutes } from './fileContent.js';
|
|
13
15
|
import { linkInfoRoutes } from './linkInfo.js';
|
|
@@ -26,9 +28,11 @@ export const apiRoute: FastifyPluginAsync = async (fastify) => {
|
|
|
26
28
|
await fastify.register(linkInfoRoutes);
|
|
27
29
|
await fastify.register(rawRoutes);
|
|
28
30
|
await fastify.register(exportRoutes);
|
|
31
|
+
await fastify.register(diagramExportRoutes);
|
|
29
32
|
await fastify.register(diagramsRoutes);
|
|
30
33
|
await fastify.register(runnerRoutes);
|
|
31
34
|
await fastify.register(searchRoutes);
|
|
32
35
|
await fastify.register(sharingRoutes);
|
|
33
36
|
await fastify.register(authStatusRoutes);
|
|
37
|
+
await fastify.register(eventsRoutes);
|
|
34
38
|
};
|
package/src/routes/api/search.ts
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Search API route — proxies to jeeves-watcher for semantic search.
|
|
3
3
|
*
|
|
4
|
-
* Handles: POST /api/search
|
|
4
|
+
* Handles: POST /api/search, GET /api/search/facets.
|
|
5
5
|
* Insider-only. Results filtered by insider's scope.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
import { stat } from 'node:fs/promises';
|
|
9
|
-
import path from 'node:path';
|
|
10
11
|
|
|
11
12
|
import type { FastifyPluginAsync } from 'fastify';
|
|
12
13
|
import picomatch from 'picomatch';
|
|
13
14
|
|
|
14
15
|
import { getConfig } from '../../config/index.js';
|
|
15
16
|
import type { NormalizedScopes } from '../../config/types.js';
|
|
17
|
+
import { fsPathToUrl, getRoots, urlPathToFs } from '../../util/platform.js';
|
|
16
18
|
|
|
17
19
|
/** Check if a path passes the insider's scope rules. */
|
|
18
20
|
function pathAllowedByScope(
|
|
@@ -69,62 +71,6 @@ interface GroupedResult {
|
|
|
69
71
|
}>;
|
|
70
72
|
}
|
|
71
73
|
|
|
72
|
-
/**
|
|
73
|
-
* Resolve a browse path back to a filesystem path using roots config.
|
|
74
|
-
* Inverse of fsPathToBrowsePath.
|
|
75
|
-
*/
|
|
76
|
-
function browsePathToFsPath(
|
|
77
|
-
browsePath: string,
|
|
78
|
-
roots: Record<string, string>,
|
|
79
|
-
): string | null {
|
|
80
|
-
const parts = browsePath.split('/');
|
|
81
|
-
const label = parts[0];
|
|
82
|
-
const rest = parts.slice(1).join('/');
|
|
83
|
-
|
|
84
|
-
// Check if label matches a root
|
|
85
|
-
if (roots[label]) {
|
|
86
|
-
return path.join(roots[label], rest);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Windows drive letter: j/foo/bar → J:\foo\bar
|
|
90
|
-
if (/^[a-zA-Z]$/.test(label)) {
|
|
91
|
-
return `${label.toUpperCase()}:\\${rest.replace(/\//g, '\\')}`;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return null;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Convert an absolute filesystem path to a browse URL path.
|
|
99
|
-
* Maps drive letters and root mounts back to the URL scheme.
|
|
100
|
-
*/
|
|
101
|
-
function fsPathToBrowsePath(
|
|
102
|
-
fsPath: string,
|
|
103
|
-
roots: Record<string, string>,
|
|
104
|
-
): string | null {
|
|
105
|
-
const normalized = fsPath.replace(/\\/g, '/');
|
|
106
|
-
|
|
107
|
-
// Windows drive letter: j:/foo/bar → j/foo/bar
|
|
108
|
-
const driveMatch = normalized.match(/^([a-zA-Z]):\/(.*)$/);
|
|
109
|
-
if (driveMatch) {
|
|
110
|
-
return `${driveMatch[1].toLowerCase()}/${driveMatch[2]}`;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Linux roots: find matching root prefix
|
|
114
|
-
for (const [label, rootPath] of Object.entries(roots)) {
|
|
115
|
-
const normalizedRoot = rootPath.replace(/\\/g, '/').replace(/\/$/, '');
|
|
116
|
-
if (normalized.startsWith(normalizedRoot + '/')) {
|
|
117
|
-
const relative = normalized.slice(normalizedRoot.length + 1);
|
|
118
|
-
return `${label}/${relative}`;
|
|
119
|
-
}
|
|
120
|
-
if (normalized === normalizedRoot) {
|
|
121
|
-
return label;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return null;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
74
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
129
75
|
export const searchRoutes: FastifyPluginAsync = async (fastify) => {
|
|
130
76
|
fastify.post<{
|
|
@@ -150,7 +96,7 @@ export const searchRoutes: FastifyPluginAsync = async (fastify) => {
|
|
|
150
96
|
}
|
|
151
97
|
|
|
152
98
|
const insiderScopes = request.insiderScopes;
|
|
153
|
-
const roots = config.roots
|
|
99
|
+
const roots = getRoots(config.roots);
|
|
154
100
|
|
|
155
101
|
// Over-fetch to account for scope filtering
|
|
156
102
|
const fetchLimit = Math.min(limit * 5, 200);
|
|
@@ -177,11 +123,11 @@ export const searchRoutes: FastifyPluginAsync = async (fastify) => {
|
|
|
177
123
|
const fp = r.payload.file_path;
|
|
178
124
|
if (!fp) continue;
|
|
179
125
|
|
|
180
|
-
const
|
|
181
|
-
|
|
126
|
+
const urlPath = fsPathToUrl(fp, roots);
|
|
127
|
+
// fsPathToUrl returns "/drive/path"; strip leading slash for browsePath
|
|
128
|
+
const browsePath = urlPath.replace(/^\//, '');
|
|
182
129
|
|
|
183
130
|
// Check insider scope
|
|
184
|
-
const urlPath = `/${browsePath}`;
|
|
185
131
|
if (!pathAllowedByScope(urlPath, insiderScopes ?? null)) continue;
|
|
186
132
|
|
|
187
133
|
permitted.push({ ...r, browsePath });
|
|
@@ -248,7 +194,7 @@ export const searchRoutes: FastifyPluginAsync = async (fastify) => {
|
|
|
248
194
|
await Promise.all(
|
|
249
195
|
grouped.map(async (g) => {
|
|
250
196
|
g.chunks.sort((a, b) => a.index - b.index);
|
|
251
|
-
const fsPath =
|
|
197
|
+
const fsPath = urlPathToFs(g.browsePath, roots);
|
|
252
198
|
if (fsPath) {
|
|
253
199
|
try {
|
|
254
200
|
const s = await stat(fsPath);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for sharing route helpers.
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, it } from 'vitest';
|
|
8
|
+
|
|
9
|
+
// buildDeepShareUrl is not exported from the module, so we test it via
|
|
10
|
+
// the module's internal behavior. Since it's a pure function, we extract
|
|
11
|
+
// and test the URL construction logic directly.
|
|
12
|
+
|
|
13
|
+
/** Inline copy of buildDeepShareUrl for unit testing. */
|
|
14
|
+
function buildDeepShareUrl(
|
|
15
|
+
targetPath: string,
|
|
16
|
+
key: string,
|
|
17
|
+
params: { depth: number; dirs: boolean; stack: string; exp?: string },
|
|
18
|
+
): string {
|
|
19
|
+
let url = `/browse${targetPath}?key=${key}&d=${String(params.depth)}&dirs=${params.dirs ? '1' : '0'}&s=${params.stack}`;
|
|
20
|
+
if (params.exp) url += `&exp=${params.exp}`;
|
|
21
|
+
return url;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('buildDeepShareUrl', () => {
|
|
25
|
+
it('builds a basic deep share URL without expiry', () => {
|
|
26
|
+
const url = buildDeepShareUrl('/j/docs/readme.md', 'abc123', {
|
|
27
|
+
depth: 2,
|
|
28
|
+
dirs: false,
|
|
29
|
+
stack: 'encoded-stack',
|
|
30
|
+
});
|
|
31
|
+
expect(url).toBe(
|
|
32
|
+
'/browse/j/docs/readme.md?key=abc123&d=2&dirs=0&s=encoded-stack',
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('includes expiry when provided', () => {
|
|
37
|
+
const url = buildDeepShareUrl('/j/docs/readme.md', 'abc123', {
|
|
38
|
+
depth: 1,
|
|
39
|
+
dirs: true,
|
|
40
|
+
stack: 'stk',
|
|
41
|
+
exp: '1700000000000',
|
|
42
|
+
});
|
|
43
|
+
expect(url).toBe(
|
|
44
|
+
'/browse/j/docs/readme.md?key=abc123&d=1&dirs=1&s=stk&exp=1700000000000',
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('handles zero depth', () => {
|
|
49
|
+
const url = buildDeepShareUrl('/j/file.md', 'key', {
|
|
50
|
+
depth: 0,
|
|
51
|
+
dirs: false,
|
|
52
|
+
stack: 's',
|
|
53
|
+
});
|
|
54
|
+
expect(url).toContain('d=0');
|
|
55
|
+
expect(url).toContain('dirs=0');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('encodes dirs=1 when dirs is true', () => {
|
|
59
|
+
const url = buildDeepShareUrl('/j/dir', 'key', {
|
|
60
|
+
depth: 3,
|
|
61
|
+
dirs: true,
|
|
62
|
+
stack: 'stk',
|
|
63
|
+
});
|
|
64
|
+
expect(url).toContain('dirs=1');
|
|
65
|
+
});
|
|
66
|
+
});
|