@yancyyu/openhermit 1.5.9 → 1.5.11
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/alias-loader.mjs +51 -0
- package/bin/hermit.mjs +193 -29
- package/package.json +1 -1
- package/src/main/server.ts +9 -3
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..');
|
|
6
|
+
|
|
7
|
+
const ALIASES = [
|
|
8
|
+
['@features/', 'src/features/'],
|
|
9
|
+
['@main/', 'src/main/'],
|
|
10
|
+
['@renderer/', 'src/renderer/'],
|
|
11
|
+
['@shared/', 'src/shared/'],
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const EXACT_ALIASES = new Map([
|
|
15
|
+
['@shared/types', 'src/shared/types/index.ts'],
|
|
16
|
+
['@main/types', 'src/main/types/index.ts'],
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
function resolveAlias(specifier) {
|
|
20
|
+
const exactTarget = EXACT_ALIASES.get(specifier);
|
|
21
|
+
if (exactTarget) {
|
|
22
|
+
const absolutePath = path.join(repoRoot, exactTarget);
|
|
23
|
+
if (existsSync(absolutePath)) return pathToFileURL(absolutePath).href;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
for (const [prefix, target] of ALIASES) {
|
|
27
|
+
if (!specifier.startsWith(prefix)) continue;
|
|
28
|
+
const relativePath = specifier.slice(prefix.length);
|
|
29
|
+
const basePath = path.join(repoRoot, target, relativePath);
|
|
30
|
+
const candidates = [
|
|
31
|
+
basePath,
|
|
32
|
+
`${basePath}.ts`,
|
|
33
|
+
`${basePath}.tsx`,
|
|
34
|
+
`${basePath}.js`,
|
|
35
|
+
path.join(basePath, 'index.ts'),
|
|
36
|
+
path.join(basePath, 'index.tsx'),
|
|
37
|
+
path.join(basePath, 'index.js'),
|
|
38
|
+
];
|
|
39
|
+
const match = candidates.find((candidate) => existsSync(candidate));
|
|
40
|
+
if (match) return pathToFileURL(match).href;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function resolve(specifier, context, nextResolve) {
|
|
46
|
+
const aliasUrl = resolveAlias(specifier);
|
|
47
|
+
if (aliasUrl) {
|
|
48
|
+
return { url: aliasUrl, shortCircuit: true };
|
|
49
|
+
}
|
|
50
|
+
return nextResolve(specifier, context);
|
|
51
|
+
}
|
package/bin/hermit.mjs
CHANGED
|
@@ -16,11 +16,11 @@
|
|
|
16
16
|
|
|
17
17
|
import { spawn, execSync } from 'node:child_process';
|
|
18
18
|
import crypto from 'node:crypto';
|
|
19
|
-
import { mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
|
|
19
|
+
import { mkdirSync, writeFileSync, readFileSync, existsSync, openSync, closeSync, unlinkSync } from 'node:fs';
|
|
20
20
|
import { createRequire } from 'node:module';
|
|
21
21
|
import os from 'node:os';
|
|
22
22
|
import path from 'node:path';
|
|
23
|
-
import { fileURLToPath } from 'node:url';
|
|
23
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
24
24
|
|
|
25
25
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
26
26
|
const repoRoot = path.resolve(__dirname, '..');
|
|
@@ -56,12 +56,18 @@ Usage:
|
|
|
56
56
|
Options:
|
|
57
57
|
--port <number> HTTP server port (default: 5680)
|
|
58
58
|
--no-cc-connect Do not auto-start bundled cc-connect
|
|
59
|
+
--daemon Run in the background
|
|
59
60
|
--version Show current version
|
|
60
61
|
--help Show this help message
|
|
61
62
|
update Check and install updates
|
|
63
|
+
status Show background service status
|
|
64
|
+
stop Stop the background service
|
|
62
65
|
|
|
63
66
|
Examples:
|
|
64
67
|
openhermit # Start on port 5680
|
|
68
|
+
openhermit --daemon # Start in background
|
|
69
|
+
openhermit status # Show background status
|
|
70
|
+
openhermit stop # Stop background service
|
|
65
71
|
openhermit --port 8080 # Start on port 8080
|
|
66
72
|
openhermit --no-cc-connect # Start only openHermit
|
|
67
73
|
openhermit --version # Show version
|
|
@@ -81,11 +87,131 @@ const portIndex = args.indexOf('--port');
|
|
|
81
87
|
const port = portIndex !== -1 && args[portIndex + 1] ? args[portIndex + 1] : '5680';
|
|
82
88
|
const skipCcConnect = args.includes('--no-cc-connect') || process.env.HERMIT_NO_CC_CONNECT === '1';
|
|
83
89
|
const hermitHome = process.env.HERMIT_HOME || path.join(os.homedir(), '.hermit');
|
|
90
|
+
const daemonRequested = args.includes('--daemon');
|
|
91
|
+
const daemonChild = process.env.HERMIT_DAEMON_CHILD === '1';
|
|
92
|
+
const daemonPidPath = path.join(hermitHome, 'openhermit.pid');
|
|
93
|
+
const daemonLogPath = path.join(hermitHome, 'logs', 'openhermit.log');
|
|
84
94
|
const ccConnectConfigPath =
|
|
85
95
|
process.env.HERMIT_CC_CONNECT_CONFIG ||
|
|
86
96
|
process.env.CC_CONNECT_CONFIG ||
|
|
87
97
|
path.join(hermitHome, 'cc-connect', 'config.toml');
|
|
88
98
|
|
|
99
|
+
function readDaemonPid() {
|
|
100
|
+
try {
|
|
101
|
+
const raw = readFileSync(daemonPidPath, 'utf-8').trim();
|
|
102
|
+
const pid = Number.parseInt(raw, 10);
|
|
103
|
+
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
104
|
+
} catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isPidRunning(pid) {
|
|
110
|
+
try {
|
|
111
|
+
process.kill(pid, 0);
|
|
112
|
+
return true;
|
|
113
|
+
} catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function removeDaemonPidFile() {
|
|
119
|
+
try {
|
|
120
|
+
unlinkSync(daemonPidPath);
|
|
121
|
+
} catch {
|
|
122
|
+
// Already gone.
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function signalDaemon(pid, signal) {
|
|
127
|
+
try {
|
|
128
|
+
process.kill(-pid, signal);
|
|
129
|
+
return true;
|
|
130
|
+
} catch {
|
|
131
|
+
// Fall back to direct process signal.
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
process.kill(pid, signal);
|
|
135
|
+
return true;
|
|
136
|
+
} catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function printDaemonStatus() {
|
|
142
|
+
const pid = readDaemonPid();
|
|
143
|
+
if (pid && isPidRunning(pid)) {
|
|
144
|
+
console.log(`[openHermit] Running in background (pid ${pid})`);
|
|
145
|
+
console.log(`[openHermit] Log: ${daemonLogPath}`);
|
|
146
|
+
process.exit(0);
|
|
147
|
+
}
|
|
148
|
+
if (pid) removeDaemonPidFile();
|
|
149
|
+
console.log('[openHermit] Not running');
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function stopDaemon() {
|
|
154
|
+
const pid = readDaemonPid();
|
|
155
|
+
if (!pid || !isPidRunning(pid)) {
|
|
156
|
+
if (pid) removeDaemonPidFile();
|
|
157
|
+
console.log('[openHermit] Not running');
|
|
158
|
+
process.exit(0);
|
|
159
|
+
}
|
|
160
|
+
console.log(`[openHermit] Stopping background service (pid ${pid})...`);
|
|
161
|
+
signalDaemon(pid, 'SIGTERM');
|
|
162
|
+
await new Promise((resolve) => setTimeout(resolve, 2_000));
|
|
163
|
+
if (isPidRunning(pid)) {
|
|
164
|
+
signalDaemon(pid, 'SIGKILL');
|
|
165
|
+
}
|
|
166
|
+
removeDaemonPidFile();
|
|
167
|
+
console.log('[openHermit] Stopped');
|
|
168
|
+
process.exit(0);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function startDaemon() {
|
|
172
|
+
const existingPid = readDaemonPid();
|
|
173
|
+
if (existingPid && isPidRunning(existingPid)) {
|
|
174
|
+
console.log(`[openHermit] Already running in background (pid ${existingPid})`);
|
|
175
|
+
console.log(`[openHermit] Log: ${daemonLogPath}`);
|
|
176
|
+
process.exit(0);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
mkdirSync(path.dirname(daemonPidPath), { recursive: true });
|
|
180
|
+
mkdirSync(path.dirname(daemonLogPath), { recursive: true });
|
|
181
|
+
const out = openSync(daemonLogPath, 'a');
|
|
182
|
+
const err = openSync(daemonLogPath, 'a');
|
|
183
|
+
const childArgs = process.argv.slice(2).filter((arg) => arg !== '--daemon');
|
|
184
|
+
const child = spawn(process.execPath, [fileURLToPath(import.meta.url), ...childArgs], {
|
|
185
|
+
cwd: repoRoot,
|
|
186
|
+
detached: true,
|
|
187
|
+
env: {
|
|
188
|
+
...process.env,
|
|
189
|
+
HERMIT_DAEMON_CHILD: '1',
|
|
190
|
+
},
|
|
191
|
+
stdio: ['ignore', out, err],
|
|
192
|
+
});
|
|
193
|
+
child.unref();
|
|
194
|
+
closeSync(out);
|
|
195
|
+
closeSync(err);
|
|
196
|
+
writeFileSync(daemonPidPath, String(child.pid), 'utf-8');
|
|
197
|
+
console.log(`[openHermit] Started in background (pid ${child.pid})`);
|
|
198
|
+
console.log(`[openHermit] URL: http://127.0.0.1:${port}`);
|
|
199
|
+
console.log(`[openHermit] Log: ${daemonLogPath}`);
|
|
200
|
+
process.exit(0);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (args.includes('status')) {
|
|
204
|
+
printDaemonStatus();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (args.includes('stop')) {
|
|
208
|
+
await stopDaemon();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (daemonRequested && !daemonChild) {
|
|
212
|
+
startDaemon();
|
|
213
|
+
}
|
|
214
|
+
|
|
89
215
|
// ---------------------------------------------------------------------------
|
|
90
216
|
// Update command
|
|
91
217
|
// ---------------------------------------------------------------------------
|
|
@@ -252,8 +378,42 @@ function resolveCcConnectRunner() {
|
|
|
252
378
|
return path.join(path.dirname(pkgPath), 'run.js');
|
|
253
379
|
}
|
|
254
380
|
|
|
255
|
-
function
|
|
256
|
-
return require.resolve('tsx
|
|
381
|
+
function resolveTsxLoader() {
|
|
382
|
+
return require.resolve('tsx');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function resolveAliasLoaderRegister() {
|
|
386
|
+
const aliasLoaderUrl = pathToFileURL(path.join(__dirname, 'alias-loader.mjs')).href;
|
|
387
|
+
return `data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register(${JSON.stringify(aliasLoaderUrl)}, pathToFileURL("./"));`;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function terminateProcessGroup(child, signal = 'SIGTERM') {
|
|
391
|
+
if (!child?.pid) return;
|
|
392
|
+
try {
|
|
393
|
+
process.kill(-child.pid, signal);
|
|
394
|
+
return;
|
|
395
|
+
} catch {
|
|
396
|
+
// Fall back to the direct child if it was not started as a process group.
|
|
397
|
+
}
|
|
398
|
+
try {
|
|
399
|
+
child.kill(signal);
|
|
400
|
+
} catch {
|
|
401
|
+
// Already gone.
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
let shuttingDown = false;
|
|
406
|
+
function shutdown(exitCode = 0) {
|
|
407
|
+
if (shuttingDown) return;
|
|
408
|
+
shuttingDown = true;
|
|
409
|
+
console.log('\n[openHermit] Shutting down...');
|
|
410
|
+
terminateProcessGroup(serverProcess, 'SIGTERM');
|
|
411
|
+
terminateProcessGroup(ccConnectProcess, 'SIGTERM');
|
|
412
|
+
setTimeout(() => {
|
|
413
|
+
terminateProcessGroup(serverProcess, 'SIGKILL');
|
|
414
|
+
terminateProcessGroup(ccConnectProcess, 'SIGKILL');
|
|
415
|
+
process.exit(exitCode);
|
|
416
|
+
}, 2_000).unref();
|
|
257
417
|
}
|
|
258
418
|
|
|
259
419
|
let ccConnectProcess = null;
|
|
@@ -273,6 +433,7 @@ if (!skipCcConnect) {
|
|
|
273
433
|
console.log(`[openHermit] cc-connect config: ${ccConnectConfigPath}`);
|
|
274
434
|
ccConnectProcess = spawn(process.execPath, [resolveCcConnectRunner(), '-config', ccConnectConfigPath], {
|
|
275
435
|
cwd: repoRoot,
|
|
436
|
+
detached: true,
|
|
276
437
|
env: {
|
|
277
438
|
...process.env,
|
|
278
439
|
CC_CONNECT_TOKEN: ccTokens.managementToken,
|
|
@@ -325,39 +486,42 @@ if (!existsSync(distRenderererDir) || !existsSync(path.join(distRenderererDir, '
|
|
|
325
486
|
// Start the server
|
|
326
487
|
console.log('[openHermit] Launching server...\n');
|
|
327
488
|
|
|
328
|
-
const serverProcess = spawn(
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
489
|
+
const serverProcess = spawn(
|
|
490
|
+
process.execPath,
|
|
491
|
+
['--import', resolveAliasLoaderRegister(), '--import', resolveTsxLoader(), 'src/main/server.ts'],
|
|
492
|
+
{
|
|
493
|
+
cwd: repoRoot,
|
|
494
|
+
detached: true,
|
|
495
|
+
env: {
|
|
496
|
+
...process.env,
|
|
497
|
+
PORT: port,
|
|
498
|
+
HOST: process.env.HOST || '127.0.0.1',
|
|
499
|
+
NODE_ENV: 'production',
|
|
500
|
+
HERMIT_HOME: hermitHome,
|
|
501
|
+
CC_CONNECT_TOKEN: ccTokens.managementToken,
|
|
502
|
+
CC_CONNECT_MANAGEMENT_TOKEN: ccTokens.managementToken,
|
|
503
|
+
CC_CONNECT_BRIDGE_TOKEN: ccTokens.bridgeToken,
|
|
504
|
+
CC_CONNECT_CONFIG: ccConnectConfigPath,
|
|
505
|
+
},
|
|
506
|
+
stdio: 'inherit',
|
|
507
|
+
}
|
|
508
|
+
);
|
|
343
509
|
|
|
344
510
|
serverProcess.on('exit', (code) => {
|
|
511
|
+
if (shuttingDown) return;
|
|
512
|
+
terminateProcessGroup(ccConnectProcess, 'SIGTERM');
|
|
345
513
|
if (code !== 0) {
|
|
346
514
|
console.error(`[openHermit] Server exited with code ${code}`);
|
|
347
515
|
process.exit(code ?? 1);
|
|
348
516
|
}
|
|
517
|
+
process.exit(0);
|
|
349
518
|
});
|
|
350
519
|
|
|
351
|
-
process.on('SIGINT', () =>
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
process.on('SIGTERM', () => {
|
|
358
|
-
console.log('\n[openHermit] Shutting down...');
|
|
359
|
-
serverProcess.kill('SIGTERM');
|
|
360
|
-
ccConnectProcess?.kill('SIGTERM');
|
|
520
|
+
process.on('SIGINT', () => shutdown(0));
|
|
521
|
+
process.on('SIGTERM', () => shutdown(0));
|
|
522
|
+
process.on('exit', () => {
|
|
523
|
+
terminateProcessGroup(serverProcess, 'SIGTERM');
|
|
524
|
+
terminateProcessGroup(ccConnectProcess, 'SIGTERM');
|
|
361
525
|
});
|
|
362
526
|
|
|
363
527
|
console.log(`[openHermit] Server starting on http://127.0.0.1:${port}`);
|
package/package.json
CHANGED
package/src/main/server.ts
CHANGED
|
@@ -299,7 +299,10 @@ function resolveTeamFromSessionKey(sessionKey: string): string | null {
|
|
|
299
299
|
return sessionKey;
|
|
300
300
|
}
|
|
301
301
|
|
|
302
|
-
const app = Fastify({
|
|
302
|
+
const app = Fastify({
|
|
303
|
+
logger: { level: process.env.HERMIT_LOG_LEVEL ?? 'warn' },
|
|
304
|
+
disableRequestLogging: true,
|
|
305
|
+
});
|
|
303
306
|
|
|
304
307
|
// ===========================================================================
|
|
305
308
|
// Plugins
|
|
@@ -3839,14 +3842,17 @@ app.get('/api/events', (request, reply) => {
|
|
|
3839
3842
|
|
|
3840
3843
|
const SSE_FALLBACK_RE = /^\/api\/(.*\/(events|stream|notifications\/stream))$/;
|
|
3841
3844
|
|
|
3845
|
+
app.get('/api/extensions/mcp/browse', async () => ({
|
|
3846
|
+
servers: [],
|
|
3847
|
+
items: [],
|
|
3848
|
+
}));
|
|
3849
|
+
|
|
3842
3850
|
app.setNotFoundHandler((request, reply) => {
|
|
3843
3851
|
const u = request.url;
|
|
3844
3852
|
if (!u.startsWith('/api/')) {
|
|
3845
3853
|
return reply.code(404).type('text/plain').send('not found');
|
|
3846
3854
|
}
|
|
3847
3855
|
|
|
3848
|
-
request.log.info({ method: request.method, url: u }, '[stub]');
|
|
3849
|
-
|
|
3850
3856
|
if (request.method === 'GET' && SSE_FALLBACK_RE.test(u)) {
|
|
3851
3857
|
reply.raw.writeHead(200, {
|
|
3852
3858
|
'Content-Type': 'text/event-stream; charset=utf-8',
|