@karmaniverous/jeeves-server 3.4.2 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.tsbuildinfo +1 -1
- package/CHANGELOG.md +38 -1
- package/README.md +18 -17
- package/client/package.json +19 -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 +5 -40
- 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 +46 -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 +5 -45
- 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 +55 -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
package/src/config/index.ts
CHANGED
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
* @packageDocumentation
|
|
3
3
|
*
|
|
4
4
|
* Config loading and singleton management.
|
|
5
|
-
* Loads config
|
|
5
|
+
* Loads config from a JSON file, validates with Zod, applies env var substitution,
|
|
6
6
|
* resolves runtime types via resolve.ts, and exposes getConfig()/resetConfig().
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import fs from 'node:fs';
|
|
9
10
|
import path from 'node:path';
|
|
10
11
|
import { fileURLToPath } from 'node:url';
|
|
11
12
|
|
|
12
|
-
import {
|
|
13
|
-
|
|
13
|
+
import { migrateConfigPath } from './migration.js';
|
|
14
14
|
import { buildRuntimeConfig } from './resolve.js';
|
|
15
15
|
import { jeevesConfigSchema } from './schema.js';
|
|
16
16
|
import { substituteEnvVars } from './substituteEnvVars.js';
|
|
@@ -19,59 +19,72 @@ import type { RuntimeConfig } from './types.js';
|
|
|
19
19
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
20
20
|
const rootDir = path.resolve(__dirname, '../../..');
|
|
21
21
|
|
|
22
|
-
const MODULE_NAME = 'jeeves-server';
|
|
23
|
-
|
|
24
22
|
/**
|
|
25
|
-
* Load and validate jeeves-server configuration
|
|
26
|
-
*
|
|
27
|
-
* Searches for `jeeves-server.config.{json,yaml,yml,js,ts,cjs,mjs}`
|
|
28
|
-
* or `.jeeves-serverrc` in the package root and parent directories.
|
|
23
|
+
* Load and validate jeeves-server configuration from a JSON file.
|
|
29
24
|
*
|
|
30
25
|
* @param configPath - Optional explicit path to a config file.
|
|
31
26
|
* @returns Resolved runtime configuration.
|
|
32
27
|
*/
|
|
33
|
-
export
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
28
|
+
export function loadConfig(configPath?: string): RuntimeConfig {
|
|
29
|
+
const resolvedPath = configPath
|
|
30
|
+
? migrateConfigPath(configPath)
|
|
31
|
+
: findDefaultConfig();
|
|
32
|
+
|
|
33
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`Configuration file not found: ${resolvedPath}\n` +
|
|
36
|
+
`Create a jeeves-server config.json file or pass --config <path>.`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Reject non-JSON config files
|
|
41
|
+
const ext = path.extname(resolvedPath).toLowerCase();
|
|
42
|
+
if (ext && ext !== '.json') {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`Unsupported config file format: ${ext}\n` +
|
|
45
|
+
`Only JSON configuration files are supported. ` +
|
|
46
|
+
`Please convert your config to JSON format.`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const rawContent = fs.readFileSync(resolvedPath, 'utf8');
|
|
51
|
+
let rawConfig: Record<string, unknown>;
|
|
52
|
+
try {
|
|
53
|
+
rawConfig = JSON.parse(rawContent) as Record<string, unknown>;
|
|
54
|
+
} catch (err) {
|
|
56
55
|
throw new Error(
|
|
57
|
-
`
|
|
58
|
-
|
|
56
|
+
`Failed to parse config file ${resolvedPath}: ${err instanceof Error ? err.message : String(err)}`,
|
|
57
|
+
{ cause: err },
|
|
59
58
|
);
|
|
60
59
|
}
|
|
61
60
|
|
|
62
|
-
const substituted = substituteEnvVars(
|
|
63
|
-
result.config as Record<string, unknown>,
|
|
64
|
-
);
|
|
61
|
+
const substituted = substituteEnvVars(rawConfig);
|
|
65
62
|
|
|
66
63
|
const parseResult = jeevesConfigSchema.safeParse(substituted);
|
|
67
64
|
if (!parseResult.success) {
|
|
68
65
|
const issues = parseResult.error.issues
|
|
69
66
|
.map((i) => ` - ${i.path.join('.')}: ${i.message}`)
|
|
70
67
|
.join('\n');
|
|
71
|
-
throw new Error(`Invalid configuration in ${
|
|
68
|
+
throw new Error(`Invalid configuration in ${resolvedPath}:\n${issues}`);
|
|
72
69
|
}
|
|
73
70
|
|
|
74
|
-
return buildRuntimeConfig(parseResult.data, rootDir,
|
|
71
|
+
return buildRuntimeConfig(parseResult.data, rootDir, resolvedPath);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Find the default config file in the package root directory.
|
|
76
|
+
*/
|
|
77
|
+
function findDefaultConfig(): string {
|
|
78
|
+
// Try new convention first
|
|
79
|
+
const newPath = path.join(rootDir, 'jeeves-server', 'config.json');
|
|
80
|
+
if (fs.existsSync(newPath)) return newPath;
|
|
81
|
+
|
|
82
|
+
// Fall back to old convention
|
|
83
|
+
const oldPath = path.join(rootDir, 'jeeves-server.config.json');
|
|
84
|
+
if (fs.existsSync(oldPath)) return oldPath;
|
|
85
|
+
|
|
86
|
+
// Return new path for error message
|
|
87
|
+
return newPath;
|
|
75
88
|
}
|
|
76
89
|
|
|
77
90
|
let configInstance: RuntimeConfig | null = null;
|
|
@@ -101,9 +114,9 @@ export function getConfig(): RuntimeConfig {
|
|
|
101
114
|
* Initialize the config singleton. Must be called once at startup.
|
|
102
115
|
* @param configPath - Optional explicit path to a config file.
|
|
103
116
|
*/
|
|
104
|
-
export
|
|
117
|
+
export function initConfig(configPath?: string): RuntimeConfig {
|
|
105
118
|
lastConfigPath = configPath;
|
|
106
|
-
configInstance =
|
|
119
|
+
configInstance = loadConfig(configPath);
|
|
107
120
|
return configInstance;
|
|
108
121
|
}
|
|
109
122
|
|
|
@@ -111,8 +124,8 @@ export async function initConfig(configPath?: string): Promise<RuntimeConfig> {
|
|
|
111
124
|
* Reload the config singleton from the last-used config path.
|
|
112
125
|
* Call after mutating state that affects resolved config (e.g., key rotation).
|
|
113
126
|
*/
|
|
114
|
-
export
|
|
115
|
-
configInstance =
|
|
127
|
+
export function resetConfig(): void {
|
|
128
|
+
configInstance = loadConfig(lastConfigPath);
|
|
116
129
|
}
|
|
117
130
|
|
|
118
131
|
/**
|
|
@@ -36,28 +36,26 @@ describe('loadConfig', () => {
|
|
|
36
36
|
clearConfig();
|
|
37
37
|
});
|
|
38
38
|
|
|
39
|
-
it('loads a valid JSON config file',
|
|
39
|
+
it('loads a valid JSON config file', () => {
|
|
40
40
|
const configPath = writeConfig(tmpDir, VALID_CONFIG);
|
|
41
|
-
const config =
|
|
41
|
+
const config = loadConfig(configPath);
|
|
42
42
|
expect(config.port).toBe(9999);
|
|
43
43
|
expect(config.chromePath).toBe('/usr/bin/chromium');
|
|
44
|
-
|
|
44
|
+
// Migration moves jeeves-server.config.json → jeeves-server/config.json
|
|
45
|
+
const expectedPath = path.join(tmpDir, 'jeeves-server', 'config.json');
|
|
46
|
+
expect(config.configPath).toBe(expectedPath);
|
|
45
47
|
});
|
|
46
48
|
|
|
47
|
-
it('throws on missing config',
|
|
48
|
-
|
|
49
|
-
loadConfig(path.join(tmpDir, 'nonexistent.json')),
|
|
50
|
-
).rejects.toThrow();
|
|
49
|
+
it('throws on missing config', () => {
|
|
50
|
+
expect(() => loadConfig(path.join(tmpDir, 'nonexistent.json'))).toThrow();
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
-
it('throws on invalid config (missing auth)',
|
|
53
|
+
it('throws on invalid config (missing auth)', () => {
|
|
54
54
|
const configPath = writeConfig(tmpDir, { port: 1234 });
|
|
55
|
-
|
|
56
|
-
'Invalid configuration',
|
|
57
|
-
);
|
|
55
|
+
expect(() => loadConfig(configPath)).toThrow('Invalid configuration');
|
|
58
56
|
});
|
|
59
57
|
|
|
60
|
-
it('applies env var substitution',
|
|
58
|
+
it('applies env var substitution', () => {
|
|
61
59
|
const original = process.env['TEST_CHROME_PATH'];
|
|
62
60
|
process.env['TEST_CHROME_PATH'] = '/custom/chrome';
|
|
63
61
|
try {
|
|
@@ -65,7 +63,7 @@ describe('loadConfig', () => {
|
|
|
65
63
|
...VALID_CONFIG,
|
|
66
64
|
chromePath: '${TEST_CHROME_PATH}',
|
|
67
65
|
});
|
|
68
|
-
const config =
|
|
66
|
+
const config = loadConfig(configPath);
|
|
69
67
|
expect(config.chromePath).toBe('/custom/chrome');
|
|
70
68
|
} finally {
|
|
71
69
|
if (original === undefined) delete process.env['TEST_CHROME_PATH'];
|
|
@@ -73,15 +71,15 @@ describe('loadConfig', () => {
|
|
|
73
71
|
}
|
|
74
72
|
});
|
|
75
73
|
|
|
76
|
-
it('applies default port when omitted',
|
|
74
|
+
it('applies default port when omitted', () => {
|
|
77
75
|
const noPort = { ...VALID_CONFIG };
|
|
78
76
|
delete (noPort as Record<string, unknown>).port;
|
|
79
77
|
const configPath = writeConfig(tmpDir, noPort);
|
|
80
|
-
const config =
|
|
78
|
+
const config = loadConfig(configPath);
|
|
81
79
|
expect(config.port).toBe(1934);
|
|
82
80
|
});
|
|
83
81
|
|
|
84
|
-
it('rejects _plugin key with scopes',
|
|
82
|
+
it('rejects _plugin key with scopes', () => {
|
|
85
83
|
const configPath = writeConfig(tmpDir, {
|
|
86
84
|
...VALID_CONFIG,
|
|
87
85
|
keys: {
|
|
@@ -89,12 +87,12 @@ describe('loadConfig', () => {
|
|
|
89
87
|
_plugin: { key: 'c'.repeat(64), scopes: ['/restricted'] },
|
|
90
88
|
},
|
|
91
89
|
});
|
|
92
|
-
|
|
90
|
+
expect(() => loadConfig(configPath)).toThrow(
|
|
93
91
|
'_plugin key must not have scopes',
|
|
94
92
|
);
|
|
95
93
|
});
|
|
96
94
|
|
|
97
|
-
it('rejects _internal key with scopes',
|
|
95
|
+
it('rejects _internal key with scopes', () => {
|
|
98
96
|
const configPath = writeConfig(tmpDir, {
|
|
99
97
|
...VALID_CONFIG,
|
|
100
98
|
keys: {
|
|
@@ -102,12 +100,12 @@ describe('loadConfig', () => {
|
|
|
102
100
|
_internal: { key: 'b'.repeat(64), scopes: ['/restricted'] },
|
|
103
101
|
},
|
|
104
102
|
});
|
|
105
|
-
|
|
103
|
+
expect(() => loadConfig(configPath)).toThrow(
|
|
106
104
|
'_internal key must not have scopes',
|
|
107
105
|
);
|
|
108
106
|
});
|
|
109
107
|
|
|
110
|
-
it('accepts _plugin key without scopes',
|
|
108
|
+
it('accepts _plugin key without scopes', () => {
|
|
111
109
|
const configPath = writeConfig(tmpDir, {
|
|
112
110
|
...VALID_CONFIG,
|
|
113
111
|
keys: {
|
|
@@ -115,13 +113,13 @@ describe('loadConfig', () => {
|
|
|
115
113
|
_plugin: 'c'.repeat(64),
|
|
116
114
|
},
|
|
117
115
|
});
|
|
118
|
-
const config =
|
|
116
|
+
const config = loadConfig(configPath);
|
|
119
117
|
expect(config.resolvedKeys.find((k) => k.name === '_plugin')?.seed).toBe(
|
|
120
118
|
'c'.repeat(64),
|
|
121
119
|
);
|
|
122
120
|
});
|
|
123
121
|
|
|
124
|
-
it('rejects undefined named scope references',
|
|
122
|
+
it('rejects undefined named scope references', () => {
|
|
125
123
|
const configPath = writeConfig(tmpDir, {
|
|
126
124
|
...VALID_CONFIG,
|
|
127
125
|
scopes: { restricted: { allow: ['/**'], deny: ['/secret'] } },
|
|
@@ -131,12 +129,12 @@ describe('loadConfig', () => {
|
|
|
131
129
|
},
|
|
132
130
|
});
|
|
133
131
|
|
|
134
|
-
|
|
132
|
+
expect(() => loadConfig(configPath)).toThrow(
|
|
135
133
|
'Scope "missing" is not defined',
|
|
136
134
|
);
|
|
137
135
|
});
|
|
138
136
|
|
|
139
|
-
it('does not treat path globs as named scope references',
|
|
137
|
+
it('does not treat path globs as named scope references', () => {
|
|
140
138
|
const configPath = writeConfig(tmpDir, {
|
|
141
139
|
...VALID_CONFIG,
|
|
142
140
|
insiders: {
|
|
@@ -144,7 +142,7 @@ describe('loadConfig', () => {
|
|
|
144
142
|
},
|
|
145
143
|
});
|
|
146
144
|
|
|
147
|
-
const config =
|
|
145
|
+
const config = loadConfig(configPath);
|
|
148
146
|
expect(
|
|
149
147
|
config.resolvedInsiders.find((i) => i.email === 'a@example.com')?.scopes,
|
|
150
148
|
).toEqual({
|
|
@@ -173,16 +171,16 @@ describe('config singleton', () => {
|
|
|
173
171
|
expect(() => getConfig()).toThrow('Config not initialized');
|
|
174
172
|
});
|
|
175
173
|
|
|
176
|
-
it('initConfig populates getConfig',
|
|
174
|
+
it('initConfig populates getConfig', () => {
|
|
177
175
|
const configPath = writeConfig(tmpDir, VALID_CONFIG);
|
|
178
|
-
|
|
176
|
+
initConfig(configPath);
|
|
179
177
|
const config = getConfig();
|
|
180
178
|
expect(config.port).toBe(9999);
|
|
181
179
|
});
|
|
182
180
|
|
|
183
|
-
it('clearConfig clears the singleton',
|
|
181
|
+
it('clearConfig clears the singleton', () => {
|
|
184
182
|
const configPath = writeConfig(tmpDir, VALID_CONFIG);
|
|
185
|
-
|
|
183
|
+
initConfig(configPath);
|
|
186
184
|
clearConfig();
|
|
187
185
|
expect(() => getConfig()).toThrow('Config not initialized');
|
|
188
186
|
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config path migration — handles old → new config path convention.
|
|
3
|
+
*
|
|
4
|
+
* Old convention: `<configDir>/jeeves-server.config.json`
|
|
5
|
+
* New convention: `<configDir>/jeeves-server/config.json`
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
|
|
11
|
+
const NON_JSON_EXTENSIONS = new Set([
|
|
12
|
+
'.ts',
|
|
13
|
+
'.yaml',
|
|
14
|
+
'.yml',
|
|
15
|
+
'.toml',
|
|
16
|
+
'.mjs',
|
|
17
|
+
'.cjs',
|
|
18
|
+
'.js',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Migrate config path from old convention to new convention if needed.
|
|
23
|
+
*
|
|
24
|
+
* If the passed path matches the old convention (jeeves-server.config.json)
|
|
25
|
+
* and the new path does not exist, migrates the file. If non-JSON config
|
|
26
|
+
* is found, rejects with a clear error.
|
|
27
|
+
*
|
|
28
|
+
* @param configPath - The config path passed via --config CLI flag.
|
|
29
|
+
* @returns The resolved config path (may be the new path after migration).
|
|
30
|
+
*/
|
|
31
|
+
export function migrateConfigPath(configPath: string): string {
|
|
32
|
+
const ext = path.extname(configPath).toLowerCase();
|
|
33
|
+
|
|
34
|
+
// Reject non-JSON config files
|
|
35
|
+
if (NON_JSON_EXTENSIONS.has(ext)) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Unsupported config file format: ${ext}\n` +
|
|
38
|
+
`Only JSON configuration files are supported. ` +
|
|
39
|
+
`Please convert your config to JSON format.`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const basename = path.basename(configPath);
|
|
44
|
+
const configDir = path.dirname(configPath);
|
|
45
|
+
|
|
46
|
+
// Check if this matches the old convention
|
|
47
|
+
if (basename !== 'jeeves-server.config.json') {
|
|
48
|
+
return configPath;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const newDir = path.join(configDir, 'jeeves-server');
|
|
52
|
+
const newPath = path.join(newDir, 'config.json');
|
|
53
|
+
|
|
54
|
+
// If new path already exists, use it
|
|
55
|
+
if (fs.existsSync(newPath)) {
|
|
56
|
+
console.log(
|
|
57
|
+
`[config-migration] Using new config path: ${newPath} ` +
|
|
58
|
+
`(old path ${configPath} is superseded)`,
|
|
59
|
+
);
|
|
60
|
+
return newPath;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// If old path exists, migrate it
|
|
64
|
+
if (fs.existsSync(configPath)) {
|
|
65
|
+
console.log(
|
|
66
|
+
`[config-migration] Migrating config: ${configPath} → ${newPath}`,
|
|
67
|
+
);
|
|
68
|
+
fs.mkdirSync(newDir, { recursive: true });
|
|
69
|
+
fs.renameSync(configPath, newPath);
|
|
70
|
+
console.log(`[config-migration] Migration complete.`);
|
|
71
|
+
return newPath;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Neither exists — return the passed path for downstream error handling
|
|
75
|
+
return configPath;
|
|
76
|
+
}
|
package/src/config/schema.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SERVER_PORT } from '@karmaniverous/jeeves';
|
|
2
|
+
import { z } from 'zod';
|
|
2
3
|
|
|
3
4
|
/** Supported authentication methods */
|
|
4
5
|
export const authModeSchema = z.enum(['google', 'keys']);
|
|
@@ -99,7 +100,7 @@ function getScopeRefs(scopes: unknown): string[] {
|
|
|
99
100
|
/** Top-level Jeeves Server configuration */
|
|
100
101
|
export const jeevesConfigSchema = z
|
|
101
102
|
.object({
|
|
102
|
-
port: z.number().int().positive().default(
|
|
103
|
+
port: z.number().int().positive().default(SERVER_PORT),
|
|
103
104
|
/**
|
|
104
105
|
* Network interface to bind the server to.
|
|
105
106
|
* Default: '0.0.0.0' (all interfaces — required for external access by insiders, share links, etc.)
|
|
@@ -126,7 +127,7 @@ export const jeevesConfigSchema = z
|
|
|
126
127
|
roots: z.record(z.string(), z.string()).optional(),
|
|
127
128
|
/**
|
|
128
129
|
* URL of the jeeves-runner API for process dashboard proxy.
|
|
129
|
-
* Default: 'http://127.0.0.1:
|
|
130
|
+
* Default: 'http://127.0.0.1:1937'
|
|
130
131
|
*/
|
|
131
132
|
runnerUrl: z.url().optional(),
|
|
132
133
|
/** @deprecated Mermaid is now bundled. This field is ignored but kept for backward compatibility. */
|
|
@@ -153,7 +154,7 @@ export const jeevesConfigSchema = z
|
|
|
153
154
|
diagramCachePath: z.string().optional(),
|
|
154
155
|
/**
|
|
155
156
|
* URL of the jeeves-watcher API for semantic search.
|
|
156
|
-
* When set, the search UI appears in the header. Example: 'http://
|
|
157
|
+
* When set, the search UI appears in the header. Example: 'http://127.0.0.1:1936'
|
|
157
158
|
*/
|
|
158
159
|
watcherUrl: z.url().optional(),
|
|
159
160
|
/**
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side JeevesComponentDescriptor for the jeeves-server component.
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
jeevesComponentDescriptorSchema,
|
|
12
|
+
SERVER_PORT,
|
|
13
|
+
} from '@karmaniverous/jeeves';
|
|
14
|
+
|
|
15
|
+
import { jeevesConfigSchema } from './config/schema.js';
|
|
16
|
+
import { packageVersion } from './util/packageVersion.js';
|
|
17
|
+
|
|
18
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
|
|
20
|
+
/** Absolute path to the start-server entry point (resolves correctly from any cwd). */
|
|
21
|
+
const startServerPath = path.resolve(__dirname, 'cli', 'start-server.js');
|
|
22
|
+
|
|
23
|
+
export const serverDescriptor = jeevesComponentDescriptorSchema.parse({
|
|
24
|
+
name: 'server',
|
|
25
|
+
version: packageVersion,
|
|
26
|
+
servicePackage: '@karmaniverous/jeeves-server',
|
|
27
|
+
pluginPackage: '@karmaniverous/jeeves-server-openclaw',
|
|
28
|
+
defaultPort: SERVER_PORT,
|
|
29
|
+
configSchema: jeevesConfigSchema,
|
|
30
|
+
configFileName: 'config.json',
|
|
31
|
+
initTemplate: () => ({
|
|
32
|
+
chromePath: 'CHANGE_ME_chromePath',
|
|
33
|
+
auth: {
|
|
34
|
+
modes: ['keys'],
|
|
35
|
+
sessionSecret: 'CHANGE_ME_sessionSecret',
|
|
36
|
+
},
|
|
37
|
+
keys: {
|
|
38
|
+
default: 'CHANGE_ME_defaultKey',
|
|
39
|
+
},
|
|
40
|
+
}),
|
|
41
|
+
onConfigApply: async () => {
|
|
42
|
+
const { resetConfig } = await import('./config/index.js');
|
|
43
|
+
resetConfig();
|
|
44
|
+
},
|
|
45
|
+
startCommand: (configPath: string) => [
|
|
46
|
+
'node',
|
|
47
|
+
startServerPath,
|
|
48
|
+
'--config',
|
|
49
|
+
configPath,
|
|
50
|
+
],
|
|
51
|
+
sectionId: 'Server',
|
|
52
|
+
refreshIntervalSeconds: 61,
|
|
53
|
+
generateToolsContent: () => '',
|
|
54
|
+
dependencies: { hard: [], soft: ['watcher', 'runner', 'meta'] },
|
|
55
|
+
});
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for diagram export route handlers.
|
|
3
|
+
*
|
|
4
|
+
* Validates the cache-first rendering pipeline shared by Mermaid and PlantUML
|
|
5
|
+
* handlers, and the path resolution + error handling for both routes.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import { tmpdir } from 'node:os';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
|
|
14
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
15
|
+
|
|
16
|
+
// Mock config before importing the module under test
|
|
17
|
+
const tmpDir = path.join(tmpdir(), `diagramExport-test-${String(Date.now())}`);
|
|
18
|
+
|
|
19
|
+
vi.mock('../../config/index.js', () => ({
|
|
20
|
+
getConfig: () => ({
|
|
21
|
+
roots: {},
|
|
22
|
+
}),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
vi.mock('../../services/diagramCache.js', () => {
|
|
26
|
+
let cache: Record<string, Buffer> = {};
|
|
27
|
+
return {
|
|
28
|
+
getCachedDiagramBuffer: (
|
|
29
|
+
engine: string,
|
|
30
|
+
source: string,
|
|
31
|
+
format: string,
|
|
32
|
+
) => {
|
|
33
|
+
const key = `${engine}:${source}:${format}`;
|
|
34
|
+
return cache[key] ?? null;
|
|
35
|
+
},
|
|
36
|
+
cacheDiagramBuffer: (
|
|
37
|
+
engine: string,
|
|
38
|
+
source: string,
|
|
39
|
+
buffer: Buffer,
|
|
40
|
+
format: string,
|
|
41
|
+
) => {
|
|
42
|
+
cache[`${engine}:${source}:${format}`] = buffer;
|
|
43
|
+
},
|
|
44
|
+
_reset: () => {
|
|
45
|
+
cache = {};
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
vi.mock('../../services/mermaid.js', () => ({
|
|
51
|
+
renderMermaidToFile: vi.fn(),
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
vi.mock('../../services/plantuml.js', () => ({
|
|
55
|
+
getPlantUmlFormats: () => ['svg', 'png'],
|
|
56
|
+
renderPlantUmlToBuffer: vi.fn(),
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
const { diagramExportRoutes } = await import('./diagramExport.js');
|
|
60
|
+
const { renderMermaidToFile } = await import('../../services/mermaid.js');
|
|
61
|
+
const { renderPlantUmlToBuffer } = await import('../../services/plantuml.js');
|
|
62
|
+
|
|
63
|
+
/** Minimal Fastify reply mock that captures sent data. */
|
|
64
|
+
function createReplyMock() {
|
|
65
|
+
const headers: Record<string, string> = {};
|
|
66
|
+
let sentData: unknown = null;
|
|
67
|
+
let statusCode = 200;
|
|
68
|
+
const reply = {
|
|
69
|
+
code: (c: number) => {
|
|
70
|
+
statusCode = c;
|
|
71
|
+
return reply;
|
|
72
|
+
},
|
|
73
|
+
header: (key: string, value: string) => {
|
|
74
|
+
headers[key.toLowerCase()] = value;
|
|
75
|
+
return reply;
|
|
76
|
+
},
|
|
77
|
+
send: (data: unknown) => {
|
|
78
|
+
sentData = data;
|
|
79
|
+
return reply;
|
|
80
|
+
},
|
|
81
|
+
get statusCode() {
|
|
82
|
+
return statusCode;
|
|
83
|
+
},
|
|
84
|
+
get headers() {
|
|
85
|
+
return headers;
|
|
86
|
+
},
|
|
87
|
+
get sentData() {
|
|
88
|
+
return sentData;
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
return reply;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
describe('diagramExportRoutes', () => {
|
|
95
|
+
const routes: Record<
|
|
96
|
+
string,
|
|
97
|
+
(
|
|
98
|
+
req: { params: Record<string, string>; query: Record<string, string> },
|
|
99
|
+
reply: ReturnType<typeof createReplyMock>,
|
|
100
|
+
) => Promise<void>
|
|
101
|
+
> = {};
|
|
102
|
+
|
|
103
|
+
beforeEach(async () => {
|
|
104
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
105
|
+
|
|
106
|
+
const fakeFastify = {
|
|
107
|
+
get: (
|
|
108
|
+
routePath: string,
|
|
109
|
+
handler: (req: unknown, reply: unknown) => Promise<void>,
|
|
110
|
+
) => {
|
|
111
|
+
routes[routePath] = handler as (typeof routes)[string];
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
await diagramExportRoutes(fakeFastify as never, {});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
afterEach(() => {
|
|
119
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
120
|
+
vi.restoreAllMocks();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('returns 400 when no path is provided for mermaid export', async () => {
|
|
124
|
+
const reply = createReplyMock();
|
|
125
|
+
await routes['/api/mermaid-export/*'](
|
|
126
|
+
{ params: { '*': '' }, query: {} },
|
|
127
|
+
reply,
|
|
128
|
+
);
|
|
129
|
+
expect(reply.statusCode).toBe(400);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('returns 404 for non-existent mermaid file', async () => {
|
|
133
|
+
const reply = createReplyMock();
|
|
134
|
+
await routes['/api/mermaid-export/*'](
|
|
135
|
+
{ params: { '*': 'nonexistent.mmd' }, query: {} },
|
|
136
|
+
reply,
|
|
137
|
+
);
|
|
138
|
+
expect(reply.statusCode).toBe(404);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('returns 400 when no path is provided for plantuml export', async () => {
|
|
142
|
+
const reply = createReplyMock();
|
|
143
|
+
await routes['/api/plantuml-export/*'](
|
|
144
|
+
{ params: { '*': '' }, query: {} },
|
|
145
|
+
reply,
|
|
146
|
+
);
|
|
147
|
+
expect(reply.statusCode).toBe(400);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('returns 404 for non-existent plantuml file', async () => {
|
|
151
|
+
const reply = createReplyMock();
|
|
152
|
+
await routes['/api/plantuml-export/*'](
|
|
153
|
+
{ params: { '*': 'nonexistent.puml' }, query: {} },
|
|
154
|
+
reply,
|
|
155
|
+
);
|
|
156
|
+
expect(reply.statusCode).toBe(404);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('sends 500 when mermaid render returns null', async () => {
|
|
160
|
+
// Create a real .mmd file
|
|
161
|
+
const mmdPath = path.join(tmpDir, 'test.mmd');
|
|
162
|
+
fs.writeFileSync(mmdPath, 'graph TD; A-->B');
|
|
163
|
+
|
|
164
|
+
// Mock urlPathToFs to resolve our temp path
|
|
165
|
+
const platformMod = await import('../../util/platform.js');
|
|
166
|
+
vi.spyOn(platformMod, 'urlPathToFs').mockReturnValue(mmdPath);
|
|
167
|
+
|
|
168
|
+
vi.mocked(renderMermaidToFile).mockResolvedValue(null);
|
|
169
|
+
|
|
170
|
+
const reply = createReplyMock();
|
|
171
|
+
await routes['/api/mermaid-export/*'](
|
|
172
|
+
{ params: { '*': 'test.mmd' }, query: { format: 'svg' } },
|
|
173
|
+
reply,
|
|
174
|
+
);
|
|
175
|
+
expect(reply.statusCode).toBe(500);
|
|
176
|
+
expect((reply.sentData as Record<string, string>).error).toContain(
|
|
177
|
+
'render failed',
|
|
178
|
+
);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('sends 500 when plantuml render returns null', async () => {
|
|
182
|
+
const pumlPath = path.join(tmpDir, 'test.puml');
|
|
183
|
+
fs.writeFileSync(pumlPath, '@startuml\nA -> B\n@enduml');
|
|
184
|
+
|
|
185
|
+
const platformMod = await import('../../util/platform.js');
|
|
186
|
+
vi.spyOn(platformMod, 'urlPathToFs').mockReturnValue(pumlPath);
|
|
187
|
+
|
|
188
|
+
vi.mocked(renderPlantUmlToBuffer).mockResolvedValue(null);
|
|
189
|
+
|
|
190
|
+
const reply = createReplyMock();
|
|
191
|
+
await routes['/api/plantuml-export/*'](
|
|
192
|
+
{ params: { '*': 'test.puml' }, query: { format: 'svg' } },
|
|
193
|
+
reply,
|
|
194
|
+
);
|
|
195
|
+
expect(reply.statusCode).toBe(500);
|
|
196
|
+
expect((reply.sentData as Record<string, string>).error).toContain(
|
|
197
|
+
'render failed',
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
});
|