@yancyyu/openhermit 1.6.11 → 1.6.13
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/hermit.mjs
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import { spawn, execSync } from 'node:child_process';
|
|
18
18
|
import crypto from 'node:crypto';
|
|
19
|
-
import { mkdirSync,
|
|
19
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
20
20
|
import { createRequire } from 'node:module';
|
|
21
21
|
import os from 'node:os';
|
|
22
22
|
import path from 'node:path';
|
|
@@ -56,14 +56,20 @@ 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 openHermit in the background
|
|
59
60
|
--version Show current version
|
|
60
61
|
--help Show this help message
|
|
62
|
+
status Show background service status
|
|
63
|
+
stop Stop background service
|
|
61
64
|
update Check and install updates
|
|
62
65
|
|
|
63
66
|
Examples:
|
|
64
67
|
npx @yancyyu/openhermit # Run without installing
|
|
65
68
|
npx @yancyyu/openhermit --port 8080
|
|
66
69
|
openhermit # After global install
|
|
70
|
+
openhermit --daemon
|
|
71
|
+
openhermit status
|
|
72
|
+
openhermit stop
|
|
67
73
|
openhermit --version
|
|
68
74
|
openhermit update
|
|
69
75
|
`);
|
|
@@ -80,11 +86,16 @@ const portIndex = args.indexOf('--port');
|
|
|
80
86
|
const port = portIndex !== -1 && args[portIndex + 1] ? args[portIndex + 1] : '5680';
|
|
81
87
|
const skipCcConnect = args.includes('--no-cc-connect') || process.env.HERMIT_NO_CC_CONNECT === '1';
|
|
82
88
|
const hermitHome = process.env.HERMIT_HOME || path.join(os.homedir(), '.hermit');
|
|
89
|
+
const daemonRequested = args.includes('--daemon');
|
|
90
|
+
const daemonChild = process.env.HERMIT_DAEMON_CHILD === '1';
|
|
91
|
+
const daemonPidPath = path.join(hermitHome, 'openhermit.pid');
|
|
92
|
+
const daemonLogPath = path.join(hermitHome, 'logs', 'openhermit.log');
|
|
83
93
|
const ccConnectConfigPath =
|
|
84
94
|
process.env.HERMIT_CC_CONNECT_CONFIG ||
|
|
85
95
|
process.env.CC_CONNECT_CONFIG ||
|
|
86
96
|
path.join(hermitHome, 'cc-connect', 'config.toml');
|
|
87
|
-
const bootstrapProjectName = '
|
|
97
|
+
const bootstrapProjectName = 'default';
|
|
98
|
+
const legacyBootstrapProjectName = '__openhermit_bootstrap__';
|
|
88
99
|
|
|
89
100
|
// ---------------------------------------------------------------------------
|
|
90
101
|
// Update command
|
|
@@ -141,6 +152,171 @@ async function runUpdate() {
|
|
|
141
152
|
}
|
|
142
153
|
}
|
|
143
154
|
|
|
155
|
+
function readDaemonPid() {
|
|
156
|
+
try {
|
|
157
|
+
const raw = readFileSync(daemonPidPath, 'utf-8').trim();
|
|
158
|
+
const pid = Number.parseInt(raw, 10);
|
|
159
|
+
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
160
|
+
} catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function isPidRunning(pid) {
|
|
166
|
+
try {
|
|
167
|
+
process.kill(pid, 0);
|
|
168
|
+
return true;
|
|
169
|
+
} catch {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function removeDaemonPidFile() {
|
|
175
|
+
try {
|
|
176
|
+
unlinkSync(daemonPidPath);
|
|
177
|
+
} catch {
|
|
178
|
+
// Already gone.
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function signalDaemon(pid, signal) {
|
|
183
|
+
if (!pid) return false;
|
|
184
|
+
try {
|
|
185
|
+
process.kill(-pid, signal);
|
|
186
|
+
return true;
|
|
187
|
+
} catch {
|
|
188
|
+
// Fall back to direct process signal.
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
process.kill(pid, signal);
|
|
192
|
+
return true;
|
|
193
|
+
} catch {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function collectFallbackPids() {
|
|
199
|
+
const pids = new Set();
|
|
200
|
+
const commands = [
|
|
201
|
+
`lsof -tiTCP:${port} -sTCP:LISTEN 2>/dev/null || true`,
|
|
202
|
+
'lsof -tiTCP:9810 -sTCP:LISTEN 2>/dev/null || true',
|
|
203
|
+
'lsof -tiTCP:9820 -sTCP:LISTEN 2>/dev/null || true',
|
|
204
|
+
"pgrep -f '@yancyyu/openhermit|openhermit/bin/hermit\\.mjs|src/main/server\\.ts|cc-connect' 2>/dev/null || true",
|
|
205
|
+
];
|
|
206
|
+
|
|
207
|
+
for (const command of commands) {
|
|
208
|
+
try {
|
|
209
|
+
const out = execSync(command, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
210
|
+
for (const line of out.split(/\s+/)) {
|
|
211
|
+
const pid = Number.parseInt(line, 10);
|
|
212
|
+
if (Number.isFinite(pid) && pid > 0 && pid !== process.pid) {
|
|
213
|
+
pids.add(pid);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
} catch {
|
|
217
|
+
// Ignore missing lsof/pgrep or races with exiting processes.
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return [...pids];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function stopFallbackProcesses() {
|
|
225
|
+
const pids = collectFallbackPids();
|
|
226
|
+
if (pids.length === 0) return false;
|
|
227
|
+
|
|
228
|
+
for (const pid of pids) {
|
|
229
|
+
signalDaemon(pid, 'SIGTERM');
|
|
230
|
+
}
|
|
231
|
+
await new Promise((resolve) => setTimeout(resolve, 2_000));
|
|
232
|
+
for (const pid of pids) {
|
|
233
|
+
if (isPidRunning(pid)) {
|
|
234
|
+
signalDaemon(pid, 'SIGKILL');
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function printDaemonStatus() {
|
|
241
|
+
const pid = readDaemonPid();
|
|
242
|
+
if (pid && isPidRunning(pid)) {
|
|
243
|
+
console.log(`[openHermit] Running in background (pid ${pid})`);
|
|
244
|
+
console.log(`[openHermit] Log: ${daemonLogPath}`);
|
|
245
|
+
process.exit(0);
|
|
246
|
+
}
|
|
247
|
+
if (pid) removeDaemonPidFile();
|
|
248
|
+
const fallbackPids = collectFallbackPids();
|
|
249
|
+
if (fallbackPids.length > 0) {
|
|
250
|
+
console.log(`[openHermit] Running without daemon pidfile (pids ${fallbackPids.join(', ')})`);
|
|
251
|
+
process.exit(0);
|
|
252
|
+
}
|
|
253
|
+
console.log('[openHermit] Not running');
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function stopDaemon() {
|
|
258
|
+
const pid = readDaemonPid();
|
|
259
|
+
if (!pid || !isPidRunning(pid)) {
|
|
260
|
+
if (pid) removeDaemonPidFile();
|
|
261
|
+
const stoppedFallback = await stopFallbackProcesses();
|
|
262
|
+
console.log(stoppedFallback ? '[openHermit] Stopped orphaned service processes' : '[openHermit] Not running');
|
|
263
|
+
process.exit(0);
|
|
264
|
+
}
|
|
265
|
+
console.log(`[openHermit] Stopping background service (pid ${pid})...`);
|
|
266
|
+
signalDaemon(pid, 'SIGTERM');
|
|
267
|
+
await new Promise((resolve) => setTimeout(resolve, 2_000));
|
|
268
|
+
if (isPidRunning(pid)) {
|
|
269
|
+
signalDaemon(pid, 'SIGKILL');
|
|
270
|
+
}
|
|
271
|
+
removeDaemonPidFile();
|
|
272
|
+
console.log('[openHermit] Stopped');
|
|
273
|
+
process.exit(0);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function startDaemon() {
|
|
277
|
+
const existingPid = readDaemonPid();
|
|
278
|
+
if (existingPid && isPidRunning(existingPid)) {
|
|
279
|
+
console.log(`[openHermit] Already running in background (pid ${existingPid})`);
|
|
280
|
+
console.log(`[openHermit] Log: ${daemonLogPath}`);
|
|
281
|
+
process.exit(0);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
mkdirSync(path.dirname(daemonPidPath), { recursive: true });
|
|
285
|
+
mkdirSync(path.dirname(daemonLogPath), { recursive: true });
|
|
286
|
+
const out = openSync(daemonLogPath, 'a');
|
|
287
|
+
const err = openSync(daemonLogPath, 'a');
|
|
288
|
+
const childArgs = process.argv.slice(2).filter((arg) => arg !== '--daemon');
|
|
289
|
+
const child = spawn(process.execPath, [fileURLToPath(import.meta.url), ...childArgs], {
|
|
290
|
+
cwd: repoRoot,
|
|
291
|
+
detached: true,
|
|
292
|
+
env: {
|
|
293
|
+
...process.env,
|
|
294
|
+
HERMIT_DAEMON_CHILD: '1',
|
|
295
|
+
},
|
|
296
|
+
stdio: ['ignore', out, err],
|
|
297
|
+
});
|
|
298
|
+
child.unref();
|
|
299
|
+
closeSync(out);
|
|
300
|
+
closeSync(err);
|
|
301
|
+
writeFileSync(daemonPidPath, String(child.pid), 'utf-8');
|
|
302
|
+
console.log(`[openHermit] Started in background (pid ${child.pid})`);
|
|
303
|
+
console.log(`[openHermit] URL: http://127.0.0.1:${port}`);
|
|
304
|
+
console.log(`[openHermit] Log: ${daemonLogPath}`);
|
|
305
|
+
process.exit(0);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (args.includes('status')) {
|
|
309
|
+
printDaemonStatus();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (args.includes('stop')) {
|
|
313
|
+
await stopDaemon();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (daemonRequested && !daemonChild) {
|
|
317
|
+
startDaemon();
|
|
318
|
+
}
|
|
319
|
+
|
|
144
320
|
// ---------------------------------------------------------------------------
|
|
145
321
|
// Check dependencies
|
|
146
322
|
// ---------------------------------------------------------------------------
|
|
@@ -216,21 +392,37 @@ callback_path = "/openhermit-bootstrap"
|
|
|
216
392
|
`;
|
|
217
393
|
}
|
|
218
394
|
|
|
219
|
-
function
|
|
220
|
-
|
|
395
|
+
function escapeRegExp(value) {
|
|
396
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function findProjectBlock(raw, name) {
|
|
400
|
+
const projectPattern = new RegExp(
|
|
401
|
+
`\\[\\[projects\\]\\]\\nname\\s*=\\s*"${escapeRegExp(name)}"[\\s\\S]*?(?=\\n\\[\\[projects\\]\\]|\\s*$)`
|
|
402
|
+
);
|
|
221
403
|
const match = raw.match(projectPattern);
|
|
222
|
-
|
|
404
|
+
return match ? { pattern: projectPattern, match } : null;
|
|
405
|
+
}
|
|
223
406
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
block.includes('
|
|
227
|
-
block.includes('app_id = "placeholder"')
|
|
228
|
-
|
|
407
|
+
function isManagedBootstrapBlock(block) {
|
|
408
|
+
return (
|
|
409
|
+
block.includes('disabled_commands = ["*"]') &&
|
|
410
|
+
(block.includes('app_id = "placeholder"') ||
|
|
411
|
+
block.includes('channel_token = "openhermit-bootstrap"') ||
|
|
412
|
+
block.includes('callback_path = "/openhermit-bootstrap"'))
|
|
413
|
+
);
|
|
414
|
+
}
|
|
229
415
|
|
|
230
|
-
|
|
416
|
+
function migrateManagedBootstrapProject(raw) {
|
|
417
|
+
const legacyBlock = findProjectBlock(raw, legacyBootstrapProjectName);
|
|
418
|
+
if (!legacyBlock || !isManagedBootstrapBlock(legacyBlock.match[0])) return raw;
|
|
231
419
|
|
|
232
|
-
const
|
|
233
|
-
|
|
420
|
+
const withoutLegacy = raw.replace(legacyBlock.pattern, '').replace(/\n{3,}/g, '\n\n').trimEnd();
|
|
421
|
+
if (findProjectBlock(withoutLegacy, bootstrapProjectName)) {
|
|
422
|
+
return `${withoutLegacy}\n`;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return `${withoutLegacy}\n${buildBootstrapProjectToml()}`;
|
|
234
426
|
}
|
|
235
427
|
|
|
236
428
|
function ensureCcConnectConfig() {
|
|
@@ -331,6 +523,7 @@ if (!skipCcConnect) {
|
|
|
331
523
|
console.log(`[openHermit] cc-connect config: ${ccConnectConfigPath}`);
|
|
332
524
|
ccConnectProcess = spawn(process.execPath, [resolveCcConnectRunner(), '-config', ccConnectConfigPath], {
|
|
333
525
|
cwd: repoRoot,
|
|
526
|
+
detached: true,
|
|
334
527
|
env: {
|
|
335
528
|
...process.env,
|
|
336
529
|
CC_CONNECT_TOKEN: ccTokens.managementToken,
|
|
@@ -385,6 +578,7 @@ console.log('[openHermit] Launching server...\n');
|
|
|
385
578
|
|
|
386
579
|
const serverProcess = spawn(process.execPath, ['--import', resolveAliasLoaderRegister(), '--import', resolveTsxLoader(), 'src/main/server.ts'], {
|
|
387
580
|
cwd: repoRoot,
|
|
581
|
+
detached: true,
|
|
388
582
|
env: {
|
|
389
583
|
...process.env,
|
|
390
584
|
PORT: port,
|
|
@@ -400,22 +594,34 @@ const serverProcess = spawn(process.execPath, ['--import', resolveAliasLoaderReg
|
|
|
400
594
|
});
|
|
401
595
|
|
|
402
596
|
serverProcess.on('exit', (code) => {
|
|
597
|
+
if (shuttingDown) return;
|
|
598
|
+
signalDaemon(ccConnectProcess?.pid, 'SIGTERM');
|
|
403
599
|
if (code !== 0) {
|
|
404
600
|
console.error(`[openHermit] Server exited with code ${code}`);
|
|
405
601
|
process.exit(code ?? 1);
|
|
406
602
|
}
|
|
407
603
|
});
|
|
408
604
|
|
|
409
|
-
|
|
605
|
+
let shuttingDown = false;
|
|
606
|
+
function shutdown(exitCode = 0) {
|
|
607
|
+
if (shuttingDown) return;
|
|
608
|
+
shuttingDown = true;
|
|
410
609
|
console.log('\n[openHermit] Shutting down...');
|
|
411
|
-
serverProcess
|
|
412
|
-
ccConnectProcess?.
|
|
610
|
+
signalDaemon(serverProcess?.pid, 'SIGTERM');
|
|
611
|
+
signalDaemon(ccConnectProcess?.pid, 'SIGTERM');
|
|
612
|
+
setTimeout(() => {
|
|
613
|
+
signalDaemon(serverProcess?.pid, 'SIGKILL');
|
|
614
|
+
signalDaemon(ccConnectProcess?.pid, 'SIGKILL');
|
|
615
|
+
process.exit(exitCode);
|
|
616
|
+
}, 2_000).unref();
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
process.on('SIGINT', () => {
|
|
620
|
+
shutdown(0);
|
|
413
621
|
});
|
|
414
622
|
|
|
415
623
|
process.on('SIGTERM', () => {
|
|
416
|
-
|
|
417
|
-
serverProcess.kill('SIGTERM');
|
|
418
|
-
ccConnectProcess?.kill('SIGTERM');
|
|
624
|
+
shutdown(0);
|
|
419
625
|
});
|
|
420
626
|
|
|
421
627
|
console.log(`[openHermit] Server starting on http://127.0.0.1:${port}`);
|
package/package.json
CHANGED
package/src/main/server.ts
CHANGED
|
@@ -1033,30 +1033,22 @@ app.delete<{ Params: { name: string }; Querystring: { deleteFiles?: string } }>(
|
|
|
1033
1033
|
async (request, reply) => {
|
|
1034
1034
|
const teamName = request.params.name;
|
|
1035
1035
|
try {
|
|
1036
|
-
let restartRequired = false;
|
|
1037
1036
|
try {
|
|
1038
1037
|
const result = await cc.deleteProject(teamName);
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
restartRequired,
|
|
1043
|
-
});
|
|
1038
|
+
if (result.restart_required) {
|
|
1039
|
+
await cc.restart();
|
|
1040
|
+
}
|
|
1044
1041
|
} catch (err) {
|
|
1045
1042
|
request.log.warn({ err, teamName }, 'delete cc-connect project failed or project missing');
|
|
1046
|
-
try {
|
|
1047
|
-
await svc.updateTeam(teamName, {
|
|
1048
|
-
pendingDelete: true,
|
|
1049
|
-
restartRequired: true,
|
|
1050
|
-
});
|
|
1051
|
-
restartRequired = true;
|
|
1052
|
-
} catch {
|
|
1053
|
-
// Local metadata may already be gone; keep deletion best-effort.
|
|
1054
|
-
}
|
|
1055
1043
|
}
|
|
1056
|
-
|
|
1057
|
-
|
|
1044
|
+
|
|
1045
|
+
try {
|
|
1046
|
+
await svc.deleteTeam(teamName, { deleteFiles: request.query.deleteFiles === 'true' });
|
|
1047
|
+
} catch (err) {
|
|
1048
|
+
request.log.warn({ err, teamName }, 'delete local team metadata failed or already missing');
|
|
1058
1049
|
}
|
|
1059
|
-
|
|
1050
|
+
|
|
1051
|
+
return { ok: true, restartRequired: false };
|
|
1060
1052
|
} catch (err) {
|
|
1061
1053
|
return reply.code(500).send(reply500(err));
|
|
1062
1054
|
}
|
|
@@ -1830,20 +1830,7 @@ export const TeamDetailView = ({
|
|
|
1830
1830
|
setDeleteConfirmOpen(false);
|
|
1831
1831
|
void (async () => {
|
|
1832
1832
|
try {
|
|
1833
|
-
|
|
1834
|
-
if (result.restartRequired) {
|
|
1835
|
-
const shouldRestart = await confirm({
|
|
1836
|
-
title: '需要重启 cc-connect',
|
|
1837
|
-
message:
|
|
1838
|
-
'已调用 cc-connect 删除团队配置。cc-connect 需要重启后才会真正移除该团队并停止相关运行时。',
|
|
1839
|
-
confirmLabel: '立即重启',
|
|
1840
|
-
cancelLabel: '稍后重启',
|
|
1841
|
-
variant: 'danger',
|
|
1842
|
-
});
|
|
1843
|
-
if (shouldRestart) {
|
|
1844
|
-
await api.ccSettings.restart();
|
|
1845
|
-
}
|
|
1846
|
-
}
|
|
1833
|
+
await deleteTeam(teamName);
|
|
1847
1834
|
if (tabId) closeTab(tabId);
|
|
1848
1835
|
openTeamsTab();
|
|
1849
1836
|
} catch {
|
|
@@ -531,28 +531,15 @@ export const TeamListView = (): React.JSX.Element => {
|
|
|
531
531
|
return;
|
|
532
532
|
}
|
|
533
533
|
const confirmed = await confirm({
|
|
534
|
-
title: '
|
|
535
|
-
message:
|
|
536
|
-
confirmLabel: '
|
|
534
|
+
title: '删除团队',
|
|
535
|
+
message: `确定删除团队“${teamDisplayName}”吗?此操作会同步删除 cc-connect 项目并移除本地团队数据。`,
|
|
536
|
+
confirmLabel: '删除',
|
|
537
537
|
cancelLabel: '取消',
|
|
538
538
|
variant: 'danger',
|
|
539
539
|
});
|
|
540
540
|
if (confirmed) {
|
|
541
541
|
try {
|
|
542
|
-
|
|
543
|
-
if (result.restartRequired) {
|
|
544
|
-
const shouldRestart = await confirm({
|
|
545
|
-
title: '需要重启 cc-connect',
|
|
546
|
-
message:
|
|
547
|
-
'已调用 cc-connect 删除团队配置。cc-connect 需要重启后才会真正移除该团队并停止相关运行时。',
|
|
548
|
-
confirmLabel: '立即重启',
|
|
549
|
-
cancelLabel: '稍后重启',
|
|
550
|
-
variant: 'danger',
|
|
551
|
-
});
|
|
552
|
-
if (shouldRestart) {
|
|
553
|
-
await api.ccSettings.restart();
|
|
554
|
-
}
|
|
555
|
-
}
|
|
542
|
+
await deleteTeam(teamName);
|
|
556
543
|
} catch {
|
|
557
544
|
// error via store
|
|
558
545
|
}
|