@karmaniverous/jeeves-server 3.5.2 → 3.6.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 +33 -1
- package/client/src/components/DirectoryRow.tsx +5 -1
- package/client/src/components/FileContentView.tsx +7 -0
- package/client/src/components/MarkdownView.tsx +63 -22
- package/client/src/components/TocSection.tsx +74 -0
- package/client/src/components/renderableUtils.ts +2 -2
- package/client/src/components/tocUtils.ts +65 -0
- package/client/src/index.css +36 -0
- package/client/src/lib/api.ts +2 -1
- package/client/src/lucide.d.ts +15 -0
- package/dist/client/assets/{CodeViewer-Cegj3cEn.js → CodeViewer-D5fJ1Z6_.js} +1 -1
- package/dist/client/assets/{index-DrBXupPz.js → index-BXy6kgl7.js} +2 -2
- package/dist/client/assets/index-Ch0vkF39.css +2 -0
- package/dist/client/index.html +2 -2
- package/dist/src/cli/index.js +2 -1
- package/dist/src/cli/start-server.js +12 -2
- package/dist/src/config/index.js +16 -2
- package/dist/src/config/loadConfig.test.js +66 -1
- package/dist/src/config/resolve.js +0 -4
- package/dist/src/config/resolve.test.js +0 -2
- package/dist/src/config/schema.js +7 -21
- package/dist/src/config/substituteEnvVars.js +2 -0
- package/dist/src/descriptor.js +9 -2
- package/dist/src/routes/api/auth-status.js +1 -1
- package/dist/src/routes/api/directory.js +46 -24
- package/dist/src/routes/api/directory.test.js +65 -0
- package/dist/src/routes/api/export.js +5 -1
- package/dist/src/routes/api/export.test.js +46 -0
- package/dist/src/routes/api/fileContent.js +26 -4
- package/dist/src/routes/api/runner.js +2 -3
- package/dist/src/routes/api/runner.test.js +29 -0
- package/dist/src/routes/api/search.js +4 -9
- package/dist/src/routes/api/search.test.js +28 -0
- package/dist/src/routes/config.test.js +0 -1
- package/dist/src/routes/event.js +1 -1
- package/dist/src/routes/status.js +4 -4
- package/dist/src/routes/status.test.js +10 -4
- package/dist/src/server.js +4 -2
- package/dist/src/services/csv.js +114 -0
- package/dist/src/services/csv.test.js +107 -0
- package/dist/src/services/markdown.js +21 -1
- package/dist/src/services/markdown.test.js +43 -0
- package/dist/src/util/packageVersion.js +3 -13
- package/guides/deployment.md +1 -1
- package/guides/setup.md +14 -10
- package/knip.json +2 -1
- package/package.json +18 -16
- package/src/cli/index.ts +3 -1
- package/src/cli/start-server.ts +17 -3
- package/src/config/index.ts +22 -2
- package/src/config/loadConfig.test.ts +77 -1
- package/src/config/resolve.test.ts +0 -2
- package/src/config/resolve.ts +0 -4
- package/src/config/schema.ts +8 -21
- package/src/config/substituteEnvVars.ts +2 -0
- package/src/config/types.ts +0 -4
- package/src/descriptor.ts +9 -1
- package/src/routes/api/auth-status.ts +1 -1
- package/src/routes/api/directory.test.ts +77 -0
- package/src/routes/api/directory.ts +59 -22
- package/src/routes/api/export.test.ts +56 -0
- package/src/routes/api/export.ts +5 -1
- package/src/routes/api/fileContent.ts +27 -3
- package/src/routes/api/runner.test.ts +39 -0
- package/src/routes/api/runner.ts +2 -5
- package/src/routes/api/search.test.ts +36 -0
- package/src/routes/api/search.ts +4 -9
- package/src/routes/config.test.ts +0 -1
- package/src/routes/event.test.ts +4 -4
- package/src/routes/event.ts +11 -6
- package/src/routes/status.test.ts +13 -4
- package/src/routes/status.ts +4 -4
- package/src/server.ts +4 -2
- package/src/services/csv.test.ts +127 -0
- package/src/services/csv.ts +115 -0
- package/src/services/markdown.test.ts +54 -0
- package/src/services/markdown.ts +21 -1
- package/src/types/puppeteer-core.d.ts +16 -0
- package/src/util/packageVersion.ts +3 -18
- package/dist/client/assets/index-Dk_myGs4.css +0 -2
- package/src/types/jsonmap.d.ts +0 -10
package/guides/setup.md
CHANGED
|
@@ -6,7 +6,7 @@ title: "Setup & Configuration"
|
|
|
6
6
|
|
|
7
7
|
## Prerequisites
|
|
8
8
|
|
|
9
|
-
- **Node.js** ≥
|
|
9
|
+
- **Node.js** ≥ 22
|
|
10
10
|
- **Chrome or Chromium** — required for PDF export (Puppeteer uses it headlessly)
|
|
11
11
|
|
|
12
12
|
## Installation
|
|
@@ -64,9 +64,7 @@ Legacy paths (`jeeves-server.config.json`) are auto-migrated to the new conventi
|
|
|
64
64
|
"scopes": ["/event"]
|
|
65
65
|
}
|
|
66
66
|
},
|
|
67
|
-
"events": {}
|
|
68
|
-
"watcherUrl": "http://localhost:1936",
|
|
69
|
-
"runnerUrl": "http://127.0.0.1:1937"
|
|
67
|
+
"events": {}
|
|
70
68
|
}
|
|
71
69
|
```
|
|
72
70
|
|
|
@@ -285,18 +283,22 @@ See the [Event Gateway](event-gateway.md) guide for full configuration and usage
|
|
|
285
283
|
|
|
286
284
|
### jeeves-watcher (Semantic Search)
|
|
287
285
|
|
|
288
|
-
|
|
286
|
+
The server proxies semantic search queries to [jeeves-watcher](https://github.com/karmaniverous/jeeves-watcher) and provides a search UI in the header, with filter facets and scope-aware result filtering. The watcher URL is resolved automatically via core service discovery (default port 1936).
|
|
287
|
+
|
|
288
|
+
To override the default URL, configure it in `{configRoot}/jeeves-core/config.json`:
|
|
289
289
|
|
|
290
290
|
```json
|
|
291
|
-
{ "
|
|
291
|
+
{ "services": { "watcher": { "url": "http://custom-host:1936" } } }
|
|
292
292
|
```
|
|
293
293
|
|
|
294
294
|
### jeeves-runner (Process Dashboard)
|
|
295
295
|
|
|
296
|
-
|
|
296
|
+
The server proxies runner API calls for the process dashboard UI. The runner URL is resolved automatically via core service discovery (default port 1937).
|
|
297
|
+
|
|
298
|
+
To override the default URL, configure it in `{configRoot}/jeeves-core/config.json`:
|
|
297
299
|
|
|
298
300
|
```json
|
|
299
|
-
{ "
|
|
301
|
+
{ "services": { "runner": { "url": "http://custom-host:1937" } } }
|
|
300
302
|
```
|
|
301
303
|
|
|
302
304
|
### PlantUML
|
|
@@ -336,8 +338,10 @@ If `plantuml` is omitted entirely, only the community server is used.
|
|
|
336
338
|
| `eventLogPurgeMs` | number | `2592000000` | Event log retention (default: 30 days) |
|
|
337
339
|
| `maxZipSizeMb` | number | `100` | Max directory size for ZIP export |
|
|
338
340
|
| `roots` | object | — | Linux filesystem roots (ignored on Windows) |
|
|
339
|
-
|
|
|
340
|
-
|
|
|
341
|
+
| ~~`watcherUrl`~~ | — | — | **Deprecated in v3.6.0.** Use core service discovery. |
|
|
342
|
+
| ~~`runnerUrl`~~ | — | — | **Deprecated in v3.6.0.** Use core service discovery. |
|
|
343
|
+
| ~~`host`~~ | — | — | **Deprecated in v3.6.0.** Use `getBindAddress()` via core. |
|
|
344
|
+
| ~~`metaUrl`~~ | — | — | **Deprecated in v3.6.0.** Use core service discovery. |
|
|
341
345
|
| `plantuml` | object | — | PlantUML rendering config |
|
|
342
346
|
| `diagramCachePath` | string | `.diagram-cache` | Cached rendered diagram directory |
|
|
343
347
|
| `outsiderPolicy` | scopes | — | Global outsider sharing constraints |
|
package/knip.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@karmaniverous/jeeves-server",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.1",
|
|
4
4
|
"description": "Secure file browser, markdown viewer, and webhook gateway with PDF/DOCX export and expiring share links",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fastify",
|
|
@@ -48,19 +48,18 @@
|
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@fastify/cookie": "^11.0.2",
|
|
50
50
|
"@fastify/static": "^8.3.0",
|
|
51
|
-
"@karmaniverous/jeeves": "^0.
|
|
52
|
-
"@karmaniverous/jsonmap": "^
|
|
51
|
+
"@karmaniverous/jeeves": "^0.5.3",
|
|
52
|
+
"@karmaniverous/jsonmap": "^2.1.1",
|
|
53
53
|
"@mermaid-js/mermaid-cli": "^11.12.0",
|
|
54
|
-
"@turbodocx/html-to-docx": "^1.1
|
|
55
|
-
"ajv": "^8.
|
|
54
|
+
"@turbodocx/html-to-docx": "^1.20.1",
|
|
55
|
+
"ajv": "^8.18.0",
|
|
56
56
|
"archiver": "^7.0.1",
|
|
57
57
|
"cheerio": "^1.2.0",
|
|
58
|
-
"fastify": "^5.
|
|
58
|
+
"fastify": "^5.8.4",
|
|
59
59
|
"lz-string": "^1.5.0",
|
|
60
|
-
"marked": "^17.0.
|
|
60
|
+
"marked": "^17.0.5",
|
|
61
61
|
"mime-types": "^3.0.2",
|
|
62
|
-
"
|
|
63
|
-
"picomatch": "^4.0.3",
|
|
62
|
+
"picomatch": "^4.0.4",
|
|
64
63
|
"plantuml-encoder": "^1.4.0",
|
|
65
64
|
"puppeteer": "^23.11.1",
|
|
66
65
|
"puppeteer-core": "^23.11.1",
|
|
@@ -68,18 +67,18 @@
|
|
|
68
67
|
"zod": "^4.3.6"
|
|
69
68
|
},
|
|
70
69
|
"devDependencies": {
|
|
71
|
-
"@dotenvx/dotenvx": "^1.
|
|
70
|
+
"@dotenvx/dotenvx": "^1.59.1",
|
|
72
71
|
"@types/archiver": "^7.0.0",
|
|
73
72
|
"@types/mime-types": "^3.0.1",
|
|
74
|
-
"@types/node": "^
|
|
75
|
-
"@types/picomatch": "^4.0.
|
|
76
|
-
"@vitest/coverage-v8": "^4.
|
|
73
|
+
"@types/node": "^25.5.2",
|
|
74
|
+
"@types/picomatch": "^4.0.3",
|
|
75
|
+
"@vitest/coverage-v8": "^4.1.2",
|
|
77
76
|
"auto-changelog": "^2.5.0",
|
|
78
77
|
"cross-env": "^10.1.0",
|
|
79
|
-
"happy-dom": "^20.
|
|
78
|
+
"happy-dom": "^20.8.9",
|
|
80
79
|
"release-it": "^19.2.4",
|
|
81
|
-
"rimraf": "^6.
|
|
82
|
-
"vitest": "^4.
|
|
80
|
+
"rimraf": "^6.1.3",
|
|
81
|
+
"vitest": "^4.1.2"
|
|
83
82
|
},
|
|
84
83
|
"auto-changelog": {
|
|
85
84
|
"output": "CHANGELOG.md",
|
|
@@ -119,6 +118,9 @@
|
|
|
119
118
|
"publish": true
|
|
120
119
|
}
|
|
121
120
|
},
|
|
121
|
+
"engines": {
|
|
122
|
+
"node": ">=22"
|
|
123
|
+
},
|
|
122
124
|
"bin": {
|
|
123
125
|
"jeeves-server": "./dist/src/cli/index.js"
|
|
124
126
|
}
|
package/src/cli/index.ts
CHANGED
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
* @packageDocumentation
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { createServiceCli } from '@karmaniverous/jeeves';
|
|
12
|
+
import { checkNodeVersion, createServiceCli } from '@karmaniverous/jeeves';
|
|
13
|
+
|
|
14
|
+
checkNodeVersion();
|
|
13
15
|
|
|
14
16
|
import { serverDescriptor } from '../descriptor.js';
|
|
15
17
|
|
package/src/cli/start-server.ts
CHANGED
|
@@ -9,12 +9,26 @@
|
|
|
9
9
|
* The CLI's `start` command uses this same logic in-process.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import { checkNodeVersion, init } from '@karmaniverous/jeeves';
|
|
13
|
+
|
|
14
|
+
checkNodeVersion();
|
|
15
|
+
|
|
12
16
|
import { initConfig } from '../config/index.js';
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
18
|
+
function parseArg(name: string): string | undefined {
|
|
19
|
+
const idx = process.argv.indexOf(name);
|
|
20
|
+
return idx !== -1 && process.argv[idx + 1]
|
|
21
|
+
? process.argv[idx + 1]
|
|
22
|
+
: undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const configPath = parseArg('--config');
|
|
26
|
+
const workspace =
|
|
27
|
+
parseArg('--workspace') ?? process.env['JEEVES_WORKSPACE'] ?? '.';
|
|
28
|
+
const configRoot =
|
|
29
|
+
parseArg('--config-root') ?? process.env['JEEVES_CONFIG_ROOT'] ?? './config';
|
|
17
30
|
|
|
31
|
+
init({ workspacePath: workspace, configRoot });
|
|
18
32
|
initConfig(configPath);
|
|
19
33
|
|
|
20
34
|
// Dynamic import to ensure config is initialized before server modules load
|
package/src/config/index.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
12
12
|
|
|
13
13
|
import { migrateConfigPath } from './migration.js';
|
|
14
14
|
import { buildRuntimeConfig } from './resolve.js';
|
|
15
|
-
import { jeevesConfigSchema } from './schema.js';
|
|
15
|
+
import { DEPRECATED_CONFIG_PROPS, jeevesConfigSchema } from './schema.js';
|
|
16
16
|
import { substituteEnvVars } from './substituteEnvVars.js';
|
|
17
17
|
import type { RuntimeConfig } from './types.js';
|
|
18
18
|
|
|
@@ -58,7 +58,27 @@ export function loadConfig(configPath?: string): RuntimeConfig {
|
|
|
58
58
|
);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
// Strip deprecated v3.6.0 properties before validation
|
|
62
|
+
const cleanedConfig: Record<string, unknown> = {};
|
|
63
|
+
for (const [key, value] of Object.entries(rawConfig)) {
|
|
64
|
+
if (
|
|
65
|
+
DEPRECATED_CONFIG_PROPS.includes(
|
|
66
|
+
key as (typeof DEPRECATED_CONFIG_PROPS)[number],
|
|
67
|
+
)
|
|
68
|
+
) {
|
|
69
|
+
console.warn(
|
|
70
|
+
`[jeeves-server] Deprecated config property "${key}" ignored. ` +
|
|
71
|
+
`Companion service URLs are now resolved via core config ` +
|
|
72
|
+
`({configRoot}/jeeves-core/config.json services.{name}.url). ` +
|
|
73
|
+
`Bind address is resolved via getBindAddress(). ` +
|
|
74
|
+
`Remove "${key}" from ${resolvedPath} to silence this warning.`,
|
|
75
|
+
);
|
|
76
|
+
} else {
|
|
77
|
+
cleanedConfig[key] = value;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const substituted = substituteEnvVars(cleanedConfig);
|
|
62
82
|
|
|
63
83
|
const parseResult = jeevesConfigSchema.safeParse(substituted);
|
|
64
84
|
if (!parseResult.success) {
|
|
@@ -2,7 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
|
|
5
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
6
|
|
|
7
7
|
import { clearConfig, getConfig, initConfig, loadConfig } from './index.js';
|
|
8
8
|
|
|
@@ -154,6 +154,82 @@ describe('loadConfig', () => {
|
|
|
154
154
|
});
|
|
155
155
|
});
|
|
156
156
|
|
|
157
|
+
describe('deprecated config property migration', () => {
|
|
158
|
+
let tmpDir: string;
|
|
159
|
+
|
|
160
|
+
beforeEach(() => {
|
|
161
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jeeves-config-'));
|
|
162
|
+
clearConfig();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
afterEach(() => {
|
|
166
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
167
|
+
clearConfig();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('loads config with deprecated host, watcherUrl, runnerUrl, metaUrl', () => {
|
|
171
|
+
const configWithDeprecated = {
|
|
172
|
+
...VALID_CONFIG,
|
|
173
|
+
host: '0.0.0.0',
|
|
174
|
+
watcherUrl: 'http://localhost:1936',
|
|
175
|
+
runnerUrl: 'http://localhost:1937',
|
|
176
|
+
metaUrl: 'http://localhost:1938',
|
|
177
|
+
};
|
|
178
|
+
const configPath = writeConfig(tmpDir, configWithDeprecated);
|
|
179
|
+
const config = loadConfig(configPath);
|
|
180
|
+
expect(config.port).toBe(9999);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('logs deprecation warnings for each deprecated property', () => {
|
|
184
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
185
|
+
try {
|
|
186
|
+
const configWithDeprecated = {
|
|
187
|
+
...VALID_CONFIG,
|
|
188
|
+
host: '0.0.0.0',
|
|
189
|
+
watcherUrl: 'http://localhost:1936',
|
|
190
|
+
runnerUrl: 'http://localhost:1937',
|
|
191
|
+
metaUrl: 'http://localhost:1938',
|
|
192
|
+
};
|
|
193
|
+
const configPath = writeConfig(tmpDir, configWithDeprecated);
|
|
194
|
+
loadConfig(configPath);
|
|
195
|
+
|
|
196
|
+
const deprecatedKeys = ['host', 'watcherUrl', 'runnerUrl', 'metaUrl'];
|
|
197
|
+
for (const key of deprecatedKeys) {
|
|
198
|
+
expect(
|
|
199
|
+
warnSpy.mock.calls.some(
|
|
200
|
+
(call) =>
|
|
201
|
+
typeof call[0] === 'string' && call[0].includes(`"${key}"`),
|
|
202
|
+
),
|
|
203
|
+
).toBe(true);
|
|
204
|
+
}
|
|
205
|
+
expect(warnSpy).toHaveBeenCalledTimes(4);
|
|
206
|
+
} finally {
|
|
207
|
+
warnSpy.mockRestore();
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('strips deprecated properties from the resulting RuntimeConfig', () => {
|
|
212
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
213
|
+
try {
|
|
214
|
+
const configWithDeprecated = {
|
|
215
|
+
...VALID_CONFIG,
|
|
216
|
+
host: '0.0.0.0',
|
|
217
|
+
watcherUrl: 'http://localhost:1936',
|
|
218
|
+
};
|
|
219
|
+
const configPath = writeConfig(tmpDir, configWithDeprecated);
|
|
220
|
+
const config = loadConfig(configPath);
|
|
221
|
+
|
|
222
|
+
const configAsRecord = config as unknown as Record<string, unknown>;
|
|
223
|
+
expect(configAsRecord['host']).toBeUndefined();
|
|
224
|
+
expect(configAsRecord['watcherUrl']).toBeUndefined();
|
|
225
|
+
expect(configAsRecord['runnerUrl']).toBeUndefined();
|
|
226
|
+
expect(configAsRecord['metaUrl']).toBeUndefined();
|
|
227
|
+
} finally {
|
|
228
|
+
warnSpy.mockRestore();
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
157
233
|
describe('config singleton', () => {
|
|
158
234
|
let tmpDir: string;
|
|
159
235
|
|
|
@@ -236,8 +236,6 @@ describe('buildRuntimeConfig', () => {
|
|
|
236
236
|
it('constructs correct path fields', () => {
|
|
237
237
|
const config = {
|
|
238
238
|
port: 1934,
|
|
239
|
-
host: '0.0.0.0',
|
|
240
|
-
metaUrl: 'http://127.0.0.1:1938',
|
|
241
239
|
eventTimeoutMs: 30000,
|
|
242
240
|
eventLogPurgeMs: 2592000000,
|
|
243
241
|
maxZipSizeMb: 100,
|
package/src/config/resolve.ts
CHANGED
|
@@ -250,7 +250,6 @@ export function buildRuntimeConfig(
|
|
|
250
250
|
|
|
251
251
|
return {
|
|
252
252
|
port: config.port,
|
|
253
|
-
host: config.host,
|
|
254
253
|
eventTimeoutMs: config.eventTimeoutMs,
|
|
255
254
|
eventLogPurgeMs: config.eventLogPurgeMs,
|
|
256
255
|
maxZipSizeMb: config.maxZipSizeMb,
|
|
@@ -284,9 +283,6 @@ export function buildRuntimeConfig(
|
|
|
284
283
|
googleAuth: config.auth.google ?? null,
|
|
285
284
|
sessionSecret: config.auth.sessionSecret ?? null,
|
|
286
285
|
internalInsiderKey: deriveInternalKey(resolvedKeys),
|
|
287
|
-
runnerUrl: config.runnerUrl,
|
|
288
|
-
watcherUrl: config.watcherUrl,
|
|
289
|
-
metaUrl: config.metaUrl,
|
|
290
286
|
diagramCachePath: config.diagramCachePath,
|
|
291
287
|
configPath,
|
|
292
288
|
eventsLog: path.join(rootDir, 'logs', 'webhook-events.jsonl'),
|
package/src/config/schema.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { SERVER_PORT } from '@karmaniverous/jeeves';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
|
|
4
|
+
/** Config properties deprecated in v3.6.0 (now resolved via core services). */
|
|
5
|
+
export const DEPRECATED_CONFIG_PROPS = [
|
|
6
|
+
'host',
|
|
7
|
+
'runnerUrl',
|
|
8
|
+
'watcherUrl',
|
|
9
|
+
'metaUrl',
|
|
10
|
+
] as const;
|
|
11
|
+
|
|
4
12
|
/** Supported authentication methods */
|
|
5
13
|
export const authModeSchema = z.enum(['google', 'keys']);
|
|
6
14
|
|
|
@@ -101,12 +109,6 @@ function getScopeRefs(scopes: unknown): string[] {
|
|
|
101
109
|
export const jeevesConfigSchema = z
|
|
102
110
|
.object({
|
|
103
111
|
port: z.number().int().positive().default(SERVER_PORT),
|
|
104
|
-
/**
|
|
105
|
-
* Network interface to bind the server to.
|
|
106
|
-
* Default: '0.0.0.0' (all interfaces — required for external access by insiders, share links, etc.)
|
|
107
|
-
* Set to '127.0.0.1' to restrict to loopback only.
|
|
108
|
-
*/
|
|
109
|
-
host: z.string().min(1).default('0.0.0.0'),
|
|
110
112
|
chromePath: z.string().min(1),
|
|
111
113
|
auth: authSchema,
|
|
112
114
|
/** Named scope definitions, referenced by insiders/keys/outsiderPolicy */
|
|
@@ -125,11 +127,6 @@ export const jeevesConfigSchema = z
|
|
|
125
127
|
* Default: \{ root: '/' \}
|
|
126
128
|
*/
|
|
127
129
|
roots: z.record(z.string(), z.string()).optional(),
|
|
128
|
-
/**
|
|
129
|
-
* URL of the jeeves-runner API for process dashboard proxy.
|
|
130
|
-
* Default: 'http://127.0.0.1:1937'
|
|
131
|
-
*/
|
|
132
|
-
runnerUrl: z.url().optional(),
|
|
133
130
|
/** @deprecated Mermaid is now bundled. This field is ignored but kept for backward compatibility. */
|
|
134
131
|
mermaidCliPath: z.string().optional(),
|
|
135
132
|
/**
|
|
@@ -152,16 +149,6 @@ export const jeevesConfigSchema = z
|
|
|
152
149
|
* Defaults to `.diagram-cache` in the server working directory.
|
|
153
150
|
*/
|
|
154
151
|
diagramCachePath: z.string().optional(),
|
|
155
|
-
/**
|
|
156
|
-
* URL of the jeeves-watcher API for semantic search.
|
|
157
|
-
* When set, the search UI appears in the header. Example: 'http://127.0.0.1:1936'
|
|
158
|
-
*/
|
|
159
|
-
watcherUrl: z.url().optional(),
|
|
160
|
-
/**
|
|
161
|
-
* URL of the jeeves-meta API for synthesis engine health checks.
|
|
162
|
-
* Default: 'http://127.0.0.1:1938'
|
|
163
|
-
*/
|
|
164
|
-
metaUrl: z.url().default('http://127.0.0.1:1938'),
|
|
165
152
|
/**
|
|
166
153
|
* Global outsider policy â€" constrains which paths are eligible for outsider sharing.
|
|
167
154
|
* Uses the same allow/deny model as insider scopes.
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Deep-walks config objects and replaces `${VAR_NAME}` patterns with environment variable values.
|
|
5
5
|
* Ported from jeeves-watcher — candidate for hoisting to shared `@karmaniverous/jeeves-config`.
|
|
6
|
+
*
|
|
7
|
+
* // TODO: replace with core utility when hoisted (see jeeves-core issue for substituteEnvVars hoist)
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
const ENV_PATTERN = /\$\{([^}]+)\}/g;
|
package/src/config/types.ts
CHANGED
|
@@ -50,7 +50,6 @@ export interface ResolvedInsider {
|
|
|
50
50
|
*/
|
|
51
51
|
export interface RuntimeConfig {
|
|
52
52
|
port: number;
|
|
53
|
-
host: string;
|
|
54
53
|
eventTimeoutMs: number;
|
|
55
54
|
eventLogPurgeMs: number;
|
|
56
55
|
maxZipSizeMb: number;
|
|
@@ -63,9 +62,6 @@ export interface RuntimeConfig {
|
|
|
63
62
|
servers: string[];
|
|
64
63
|
};
|
|
65
64
|
diagramCachePath?: string;
|
|
66
|
-
runnerUrl?: string;
|
|
67
|
-
watcherUrl?: string;
|
|
68
|
-
metaUrl?: string;
|
|
69
65
|
outsiderPolicy: NormalizedScopes | null;
|
|
70
66
|
events: JeevesConfig['events'];
|
|
71
67
|
authModes: AuthMode[];
|
package/src/descriptor.ts
CHANGED
|
@@ -8,6 +8,7 @@ import path from 'node:path';
|
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
|
+
init,
|
|
11
12
|
jeevesComponentDescriptorSchema,
|
|
12
13
|
SERVER_PORT,
|
|
13
14
|
} from '@karmaniverous/jeeves';
|
|
@@ -38,11 +39,18 @@ export const serverDescriptor = jeevesComponentDescriptorSchema.parse({
|
|
|
38
39
|
default: 'CHANGE_ME_defaultKey',
|
|
39
40
|
},
|
|
40
41
|
}),
|
|
41
|
-
onConfigApply: async () => {
|
|
42
|
+
onConfigApply: async (config: Record<string, unknown> | undefined) => {
|
|
43
|
+
void config; // config argument reserved for future use
|
|
42
44
|
const { resetConfig } = await import('./config/index.js');
|
|
43
45
|
resetConfig();
|
|
44
46
|
},
|
|
45
47
|
run: async (configPath: string) => {
|
|
48
|
+
// Ensure core init() runs before server modules that depend on
|
|
49
|
+
// getServiceUrl() / getBindAddress()
|
|
50
|
+
init({
|
|
51
|
+
workspacePath: process.env['JEEVES_WORKSPACE'] ?? '.',
|
|
52
|
+
configRoot: process.env['JEEVES_CONFIG_ROOT'] ?? './config',
|
|
53
|
+
});
|
|
46
54
|
const { initConfig } = await import('./config/index.js');
|
|
47
55
|
initConfig(configPath);
|
|
48
56
|
await import('./server.js');
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { mapDirectoryEntry } from './directory.js';
|
|
8
|
+
|
|
9
|
+
describe('directory entry mapping', () => {
|
|
10
|
+
let tmpDir: string;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jeeves-dir-'));
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('returns itemCount for directory entries', async () => {
|
|
21
|
+
const subDir = path.join(tmpDir, 'sub');
|
|
22
|
+
fs.mkdirSync(subDir);
|
|
23
|
+
fs.writeFileSync(path.join(subDir, 'a.txt'), 'a');
|
|
24
|
+
fs.writeFileSync(path.join(subDir, 'b.txt'), 'b');
|
|
25
|
+
|
|
26
|
+
const entries = fs.readdirSync(tmpDir, { withFileTypes: true });
|
|
27
|
+
const dirEntry = entries.find((e) => e.name === 'sub');
|
|
28
|
+
expect(dirEntry).toBeDefined();
|
|
29
|
+
const mapped = await mapDirectoryEntry(dirEntry!, tmpDir);
|
|
30
|
+
expect(mapped.type).toBe('directory');
|
|
31
|
+
expect(mapped.itemCount).toBe(2);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('returns null itemCount for file entries', async () => {
|
|
35
|
+
fs.writeFileSync(path.join(tmpDir, 'file.txt'), 'content');
|
|
36
|
+
const entries = fs.readdirSync(tmpDir, { withFileTypes: true });
|
|
37
|
+
const fileEntry = entries.find((e) => e.name === 'file.txt');
|
|
38
|
+
expect(fileEntry).toBeDefined();
|
|
39
|
+
const mapped = await mapDirectoryEntry(fileEntry!, tmpDir);
|
|
40
|
+
expect(mapped.type).toBe('file');
|
|
41
|
+
expect(mapped.itemCount).toBeNull();
|
|
42
|
+
expect(mapped.size).toBeGreaterThan(0);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('counts mixed contents (files + subdirectories)', async () => {
|
|
46
|
+
const subDir = path.join(tmpDir, 'mixed');
|
|
47
|
+
fs.mkdirSync(subDir);
|
|
48
|
+
fs.writeFileSync(path.join(subDir, 'file1.txt'), 'a');
|
|
49
|
+
fs.writeFileSync(path.join(subDir, 'file2.txt'), 'b');
|
|
50
|
+
fs.mkdirSync(path.join(subDir, 'nested'));
|
|
51
|
+
|
|
52
|
+
const entries = fs.readdirSync(tmpDir, { withFileTypes: true });
|
|
53
|
+
const dirEntry = entries.find((e) => e.name === 'mixed');
|
|
54
|
+
expect(dirEntry).toBeDefined();
|
|
55
|
+
const mapped = await mapDirectoryEntry(dirEntry!, tmpDir);
|
|
56
|
+
expect(mapped.itemCount).toBe(3);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('returns null itemCount when directory read fails', async () => {
|
|
60
|
+
const fakeDirent = {
|
|
61
|
+
name: 'nonexistent',
|
|
62
|
+
isDirectory: () => true,
|
|
63
|
+
isFile: () => false,
|
|
64
|
+
isBlockDevice: () => false,
|
|
65
|
+
isCharacterDevice: () => false,
|
|
66
|
+
isFIFO: () => false,
|
|
67
|
+
isSocket: () => false,
|
|
68
|
+
isSymbolicLink: () => false,
|
|
69
|
+
parentPath: tmpDir,
|
|
70
|
+
path: tmpDir,
|
|
71
|
+
} as fs.Dirent;
|
|
72
|
+
|
|
73
|
+
const mapped = await mapDirectoryEntry(fakeDirent, tmpDir);
|
|
74
|
+
expect(mapped.itemCount).toBeNull();
|
|
75
|
+
expect(mapped.mtime).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import fs from 'node:fs';
|
|
8
|
+
import fsp from 'node:fs/promises';
|
|
8
9
|
import path from 'node:path';
|
|
9
10
|
|
|
10
11
|
import type { FastifyPluginAsync } from 'fastify';
|
|
@@ -23,6 +24,57 @@ import {
|
|
|
23
24
|
urlPathToFs,
|
|
24
25
|
} from '../../util/platform.js';
|
|
25
26
|
|
|
27
|
+
/** Result shape returned by {@link mapDirectoryEntry}. */
|
|
28
|
+
export interface DirectoryEntryInfo {
|
|
29
|
+
name: string;
|
|
30
|
+
type: string;
|
|
31
|
+
ext: string;
|
|
32
|
+
size: number | null;
|
|
33
|
+
mtime: string | null;
|
|
34
|
+
itemCount: number | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Map a single directory entry to its API representation.
|
|
39
|
+
*
|
|
40
|
+
* Uses async fs operations so the event loop is not blocked
|
|
41
|
+
* when listing large directories.
|
|
42
|
+
*/
|
|
43
|
+
export async function mapDirectoryEntry(
|
|
44
|
+
entry: fs.Dirent,
|
|
45
|
+
parentDir: string,
|
|
46
|
+
): Promise<DirectoryEntryInfo> {
|
|
47
|
+
const entryPath = path.join(parentDir, entry.name);
|
|
48
|
+
let size: number | null = null;
|
|
49
|
+
let mtime: string | null = null;
|
|
50
|
+
let itemCount: number | null = null;
|
|
51
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
52
|
+
try {
|
|
53
|
+
const entryStats = await fsp.stat(entryPath);
|
|
54
|
+
mtime = entryStats.mtime.toISOString().split('T')[0];
|
|
55
|
+
if (entry.isDirectory()) {
|
|
56
|
+
try {
|
|
57
|
+
const children = await fsp.readdir(entryPath);
|
|
58
|
+
itemCount = children.length;
|
|
59
|
+
} catch {
|
|
60
|
+
/* permission denied, etc. */
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
size = entryStats.size;
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
/* ignore */
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
name: entry.name,
|
|
70
|
+
type: entry.isDirectory() ? 'directory' : 'file',
|
|
71
|
+
ext,
|
|
72
|
+
size,
|
|
73
|
+
mtime,
|
|
74
|
+
itemCount,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
26
78
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
27
79
|
export const directoryRoutes: FastifyPluginAsync = async (fastify) => {
|
|
28
80
|
const roots = getRoots(getConfig().roots);
|
|
@@ -41,7 +93,7 @@ export const directoryRoutes: FastifyPluginAsync = async (fastify) => {
|
|
|
41
93
|
return reply.code(404).send({ error: 'Not found', path: resolved });
|
|
42
94
|
}
|
|
43
95
|
|
|
44
|
-
const stats =
|
|
96
|
+
const stats = await fsp.stat(resolved);
|
|
45
97
|
if (!stats.isDirectory()) {
|
|
46
98
|
const ext = path.extname(resolved).toLowerCase();
|
|
47
99
|
return reply.send({
|
|
@@ -56,7 +108,9 @@ export const directoryRoutes: FastifyPluginAsync = async (fastify) => {
|
|
|
56
108
|
const isInsider = request.accessMode === 'insider';
|
|
57
109
|
const insiderScopes = request.insiderScopes ?? null;
|
|
58
110
|
|
|
59
|
-
const allEntries =
|
|
111
|
+
const allEntries = await fsp.readdir(resolved, {
|
|
112
|
+
withFileTypes: true,
|
|
113
|
+
});
|
|
60
114
|
|
|
61
115
|
const entries = insiderScopes
|
|
62
116
|
? allEntries.filter((entry) => {
|
|
@@ -82,26 +136,9 @@ export const directoryRoutes: FastifyPluginAsync = async (fastify) => {
|
|
|
82
136
|
return a.name.localeCompare(b.name);
|
|
83
137
|
});
|
|
84
138
|
|
|
85
|
-
const result =
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
let mtime: string | null = null;
|
|
89
|
-
const ext = path.extname(entry.name).toLowerCase();
|
|
90
|
-
try {
|
|
91
|
-
const entryStats = fs.statSync(entryPath);
|
|
92
|
-
mtime = entryStats.mtime.toISOString().split('T')[0];
|
|
93
|
-
if (!entry.isDirectory()) size = entryStats.size;
|
|
94
|
-
} catch {
|
|
95
|
-
/* ignore */
|
|
96
|
-
}
|
|
97
|
-
return {
|
|
98
|
-
name: entry.name,
|
|
99
|
-
type: entry.isDirectory() ? 'directory' : 'file',
|
|
100
|
-
ext,
|
|
101
|
-
size,
|
|
102
|
-
mtime,
|
|
103
|
-
};
|
|
104
|
-
});
|
|
139
|
+
const result = await Promise.all(
|
|
140
|
+
sorted.map((entry) => mapDirectoryEntry(entry, resolved)),
|
|
141
|
+
);
|
|
105
142
|
|
|
106
143
|
const breadcrumbs = breadcrumbParts(resolved, roots);
|
|
107
144
|
const matchedPath = request.authMatchedPath ?? null;
|