@yancyyu/openhermit 1.6.12 → 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,6 +86,10 @@ 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 ||
|
|
@@ -142,6 +152,171 @@ async function runUpdate() {
|
|
|
142
152
|
}
|
|
143
153
|
}
|
|
144
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
|
+
|
|
145
320
|
// ---------------------------------------------------------------------------
|
|
146
321
|
// Check dependencies
|
|
147
322
|
// ---------------------------------------------------------------------------
|
|
@@ -348,6 +523,7 @@ if (!skipCcConnect) {
|
|
|
348
523
|
console.log(`[openHermit] cc-connect config: ${ccConnectConfigPath}`);
|
|
349
524
|
ccConnectProcess = spawn(process.execPath, [resolveCcConnectRunner(), '-config', ccConnectConfigPath], {
|
|
350
525
|
cwd: repoRoot,
|
|
526
|
+
detached: true,
|
|
351
527
|
env: {
|
|
352
528
|
...process.env,
|
|
353
529
|
CC_CONNECT_TOKEN: ccTokens.managementToken,
|
|
@@ -402,6 +578,7 @@ console.log('[openHermit] Launching server...\n');
|
|
|
402
578
|
|
|
403
579
|
const serverProcess = spawn(process.execPath, ['--import', resolveAliasLoaderRegister(), '--import', resolveTsxLoader(), 'src/main/server.ts'], {
|
|
404
580
|
cwd: repoRoot,
|
|
581
|
+
detached: true,
|
|
405
582
|
env: {
|
|
406
583
|
...process.env,
|
|
407
584
|
PORT: port,
|
|
@@ -417,22 +594,34 @@ const serverProcess = spawn(process.execPath, ['--import', resolveAliasLoaderReg
|
|
|
417
594
|
});
|
|
418
595
|
|
|
419
596
|
serverProcess.on('exit', (code) => {
|
|
597
|
+
if (shuttingDown) return;
|
|
598
|
+
signalDaemon(ccConnectProcess?.pid, 'SIGTERM');
|
|
420
599
|
if (code !== 0) {
|
|
421
600
|
console.error(`[openHermit] Server exited with code ${code}`);
|
|
422
601
|
process.exit(code ?? 1);
|
|
423
602
|
}
|
|
424
603
|
});
|
|
425
604
|
|
|
426
|
-
|
|
605
|
+
let shuttingDown = false;
|
|
606
|
+
function shutdown(exitCode = 0) {
|
|
607
|
+
if (shuttingDown) return;
|
|
608
|
+
shuttingDown = true;
|
|
427
609
|
console.log('\n[openHermit] Shutting down...');
|
|
428
|
-
serverProcess
|
|
429
|
-
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);
|
|
430
621
|
});
|
|
431
622
|
|
|
432
623
|
process.on('SIGTERM', () => {
|
|
433
|
-
|
|
434
|
-
serverProcess.kill('SIGTERM');
|
|
435
|
-
ccConnectProcess?.kill('SIGTERM');
|
|
624
|
+
shutdown(0);
|
|
436
625
|
});
|
|
437
626
|
|
|
438
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
|
}
|