@yancyyu/openhermit 1.6.13 → 1.6.15

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.
@@ -1,8 +1,8 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import path from 'node:path';
3
- import { pathToFileURL } from 'node:url';
3
+ import { fileURLToPath, pathToFileURL } from 'node:url';
4
4
 
5
- const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..');
5
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
6
6
 
7
7
  const ALIASES = [
8
8
  ['@features/', 'src/features/'],
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 { closeSync, existsSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
19
+ import { appendFileSync, 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';
@@ -55,7 +55,7 @@ Usage:
55
55
 
56
56
  Options:
57
57
  --port <number> HTTP server port (default: 5680)
58
- --no-cc-connect Do not auto-start bundled cc-connect
58
+ --no-cc-connect Do not auto-start bundled runtime service
59
59
  --daemon Run openHermit in the background
60
60
  --version Show current version
61
61
  --help Show this help message
@@ -90,6 +90,7 @@ const daemonRequested = args.includes('--daemon');
90
90
  const daemonChild = process.env.HERMIT_DAEMON_CHILD === '1';
91
91
  const daemonPidPath = path.join(hermitHome, 'openhermit.pid');
92
92
  const daemonLogPath = path.join(hermitHome, 'logs', 'openhermit.log');
93
+ const serverLogPath = path.join(hermitHome, 'logs', 'openhermit-server.log');
93
94
  const ccConnectConfigPath =
94
95
  process.env.HERMIT_CC_CONNECT_CONFIG ||
95
96
  process.env.CC_CONNECT_CONFIG ||
@@ -517,10 +518,10 @@ if (!skipCcConnect) {
517
518
  const ccBaseUrl = process.env.CC_CONNECT_BASE_URL || 'http://127.0.0.1:9820';
518
519
  const alreadyRunning = await waitForCcConnect(ccBaseUrl, ccTokens.managementToken, 1_000);
519
520
  if (alreadyRunning) {
520
- console.log(`[openHermit] cc-connect already running: ${ccBaseUrl}`);
521
+ console.log(`[openHermit] Runtime service already running: ${ccBaseUrl}`);
521
522
  } else {
522
- console.log('[openHermit] Starting bundled cc-connect...');
523
- console.log(`[openHermit] cc-connect config: ${ccConnectConfigPath}`);
523
+ console.log('[openHermit] Starting bundled runtime service...');
524
+ console.log(`[openHermit] Runtime config: ${ccConnectConfigPath}`);
524
525
  ccConnectProcess = spawn(process.execPath, [resolveCcConnectRunner(), '-config', ccConnectConfigPath], {
525
526
  cwd: repoRoot,
526
527
  detached: true,
@@ -534,7 +535,7 @@ if (!skipCcConnect) {
534
535
  });
535
536
  const ready = await waitForCcConnect(ccBaseUrl, ccTokens.managementToken, 30_000);
536
537
  if (!ready) {
537
- console.warn('[openHermit] cc-connect did not become ready within 30s; openHermit will keep trying via API.');
538
+ console.warn('[openHermit] Runtime service did not become ready within 30s; openHermit will keep trying via API.');
538
539
  }
539
540
  }
540
541
  }
@@ -576,6 +577,29 @@ if (!existsSync(distRenderererDir) || !existsSync(path.join(distRenderererDir, '
576
577
  // Start the server
577
578
  console.log('[openHermit] Launching server...\n');
578
579
 
580
+ function appendServerLog(chunk) {
581
+ try {
582
+ mkdirSync(path.dirname(serverLogPath), { recursive: true });
583
+ appendFileSync(serverLogPath, chunk);
584
+ } catch {
585
+ // Logging must never block startup.
586
+ }
587
+ }
588
+
589
+ function printServerLogTail() {
590
+ try {
591
+ const content = readFileSync(serverLogPath, 'utf-8');
592
+ const lines = content.trimEnd().split(/\r?\n/).slice(-60);
593
+ if (lines.length > 0) {
594
+ console.error(`[openHermit] Server log: ${serverLogPath}`);
595
+ console.error('[openHermit] Last server log lines:');
596
+ console.error(lines.join('\n'));
597
+ }
598
+ } catch {
599
+ console.error(`[openHermit] Server log: ${serverLogPath}`);
600
+ }
601
+ }
602
+
579
603
  const serverProcess = spawn(process.execPath, ['--import', resolveAliasLoaderRegister(), '--import', resolveTsxLoader(), 'src/main/server.ts'], {
580
604
  cwd: repoRoot,
581
605
  detached: true,
@@ -590,7 +614,17 @@ const serverProcess = spawn(process.execPath, ['--import', resolveAliasLoaderReg
590
614
  CC_CONNECT_BRIDGE_TOKEN: ccTokens.bridgeToken,
591
615
  CC_CONNECT_CONFIG: ccConnectConfigPath,
592
616
  },
593
- stdio: 'inherit',
617
+ stdio: ['ignore', 'pipe', 'pipe'],
618
+ });
619
+
620
+ serverProcess.stdout?.on('data', (chunk) => {
621
+ process.stdout.write(chunk);
622
+ appendServerLog(chunk);
623
+ });
624
+
625
+ serverProcess.stderr?.on('data', (chunk) => {
626
+ process.stderr.write(chunk);
627
+ appendServerLog(chunk);
594
628
  });
595
629
 
596
630
  serverProcess.on('exit', (code) => {
@@ -598,6 +632,7 @@ serverProcess.on('exit', (code) => {
598
632
  signalDaemon(ccConnectProcess?.pid, 'SIGTERM');
599
633
  if (code !== 0) {
600
634
  console.error(`[openHermit] Server exited with code ${code}`);
635
+ printServerLogTail();
601
636
  process.exit(code ?? 1);
602
637
  }
603
638
  });
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync } from 'node:fs';
4
+ import { createRequire } from 'node:module';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const require = createRequire(import.meta.url);
10
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
11
+
12
+ let version = 'unknown';
13
+ try {
14
+ version = JSON.parse(readFileSync(path.join(packageRoot, 'package.json'), 'utf-8')).version;
15
+ } catch {
16
+ // Keep install non-blocking.
17
+ }
18
+
19
+ let runtimeVersion = 'bundled';
20
+ try {
21
+ runtimeVersion = JSON.parse(readFileSync(require.resolve('cc-connect/package.json'), 'utf-8')).version;
22
+ } catch {
23
+ // Keep install non-blocking.
24
+ }
25
+
26
+ const hermitHome = process.env.HERMIT_HOME || path.join(os.homedir(), '.hermit');
27
+
28
+ console.log(`[openHermit] Installed ${version}`);
29
+ console.log(`[openHermit] Bundled runtime service: ${runtimeVersion}`);
30
+ console.log(`[openHermit] Data directory: ${hermitHome}`);
31
+ console.log('[openHermit] Start with: openhermit');
32
+ console.log('[openHermit] Background mode: openhermit --daemon');
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@yancyyu/openhermit",
3
3
  "type": "module",
4
- "version": "1.6.13",
4
+ "version": "1.6.15",
5
5
  "description": "openHermit: team-oriented agent management workbench atop cc-connect.",
6
6
  "license": "AGPL-3.0",
7
7
  "author": {
@@ -34,6 +34,7 @@
34
34
  "dev:server": "tsx watch src/main/server.ts",
35
35
  "dev:web": "vite --config vite.web.config.ts",
36
36
  "build:web": "vite build --config vite.web.config.ts --outDir ../../dist-renderer --emptyOutDir",
37
+ "postinstall": "node ./bin/postinstall.mjs",
37
38
  "prepack": "pnpm build:web",
38
39
  "start": "node bin/hermit.mjs",
39
40
  "typecheck": "tsc --noEmit",
@@ -50,6 +50,16 @@ import { TaskDispatchService } from './services/teams-mvp/TaskDispatchService';
50
50
  import type { TaskBusConfig } from '@shared/types/team';
51
51
  import { UpdateService } from './services/UpdateService';
52
52
 
53
+ process.on('uncaughtException', (err) => {
54
+ console.error('[openHermit:server] uncaughtException', err);
55
+ process.exit(1);
56
+ });
57
+
58
+ process.on('unhandledRejection', (reason) => {
59
+ console.error('[openHermit:server] unhandledRejection', reason);
60
+ process.exit(1);
61
+ });
62
+
53
63
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
54
64
  const pkg = JSON.parse(readFileSync(path.join(__dirname, '../../package.json'), 'utf-8'));
55
65
  const REPO_ROOT = path.resolve(__dirname, '..', '..');
@@ -1033,11 +1043,10 @@ app.delete<{ Params: { name: string }; Querystring: { deleteFiles?: string } }>(
1033
1043
  async (request, reply) => {
1034
1044
  const teamName = request.params.name;
1035
1045
  try {
1046
+ let restartRequired = false;
1036
1047
  try {
1037
1048
  const result = await cc.deleteProject(teamName);
1038
- if (result.restart_required) {
1039
- await cc.restart();
1040
- }
1049
+ restartRequired = result.restart_required === true;
1041
1050
  } catch (err) {
1042
1051
  request.log.warn({ err, teamName }, 'delete cc-connect project failed or project missing');
1043
1052
  }
@@ -1048,7 +1057,7 @@ app.delete<{ Params: { name: string }; Querystring: { deleteFiles?: string } }>(
1048
1057
  request.log.warn({ err, teamName }, 'delete local team metadata failed or already missing');
1049
1058
  }
1050
1059
 
1051
- return { ok: true, restartRequired: false };
1060
+ return { ok: true, restartRequired };
1052
1061
  } catch (err) {
1053
1062
  return reply.code(500).send(reply500(err));
1054
1063
  }
@@ -7,6 +7,8 @@
7
7
 
8
8
  import { useCallback, useEffect, useState } from 'react';
9
9
 
10
+ import { api } from '@renderer/api';
11
+ import { confirm } from '@renderer/components/common/ConfirmDialog';
10
12
  import { Button } from '@renderer/components/ui/button';
11
13
  import {
12
14
  Dialog,
@@ -198,14 +200,20 @@ async function addPlatform(
198
200
  projectName: string,
199
201
  type: string,
200
202
  options: Record<string, string>
201
- ): Promise<void> {
203
+ ): Promise<{ restartRequired: boolean }> {
202
204
  const res = await fetch(`/api/v1/projects/${encodeURIComponent(projectName)}/add-platform`, {
203
205
  method: 'POST',
204
206
  headers: { 'Content-Type': 'application/json' },
205
207
  body: JSON.stringify({ type, options }),
206
208
  });
207
- const json = (await res.json()) as { ok: boolean; error?: string };
209
+ const json = (await res.json()) as {
210
+ ok: boolean;
211
+ error?: string;
212
+ data?: { restart_required?: boolean };
213
+ restart_required?: boolean;
214
+ };
208
215
  if (!json.ok) throw new Error(json.error ?? '添加失败');
216
+ return { restartRequired: json.data?.restart_required === true || json.restart_required === true };
209
217
  }
210
218
 
211
219
  // ---------------------------------------------------------------------------
@@ -482,8 +490,19 @@ function AddPlatformDialog({
482
490
  else if (v === 'false') ccOptions[k] = false;
483
491
  else ccOptions[k] = v;
484
492
  }
485
- await addPlatform(projectName, platformType, ccOptions as Record<string, string>);
493
+ const result = await addPlatform(projectName, platformType, ccOptions as Record<string, string>);
486
494
  onAdded(projectName);
495
+ if (result.restartRequired) {
496
+ const shouldRestart = await confirm({
497
+ title: '重启 cc-connect',
498
+ message: '渠道已添加。需要重启 cc-connect 才会生效。',
499
+ confirmLabel: '立即重启',
500
+ cancelLabel: '稍后重启',
501
+ });
502
+ if (shouldRestart) {
503
+ await api.ccSettings.restart();
504
+ }
505
+ }
487
506
  } catch (e) {
488
507
  setError(e instanceof Error ? e.message : '添加失败');
489
508
  } finally {
@@ -1830,7 +1830,19 @@ export const TeamDetailView = ({
1830
1830
  setDeleteConfirmOpen(false);
1831
1831
  void (async () => {
1832
1832
  try {
1833
- await deleteTeam(teamName);
1833
+ const result = await deleteTeam(teamName);
1834
+ if (result.restartRequired) {
1835
+ const shouldRestart = await confirm({
1836
+ title: '重启 cc-connect',
1837
+ message: '团队已从配置中删除。需要重启 cc-connect 才会停止对应运行时。',
1838
+ confirmLabel: '立即重启',
1839
+ cancelLabel: '稍后重启',
1840
+ variant: 'danger',
1841
+ });
1842
+ if (shouldRestart) {
1843
+ await api.ccSettings.restart();
1844
+ }
1845
+ }
1834
1846
  if (tabId) closeTab(tabId);
1835
1847
  openTeamsTab();
1836
1848
  } catch {
@@ -539,7 +539,19 @@ export const TeamListView = (): React.JSX.Element => {
539
539
  });
540
540
  if (confirmed) {
541
541
  try {
542
- await deleteTeam(teamName);
542
+ const result = await deleteTeam(teamName);
543
+ if (result.restartRequired) {
544
+ const shouldRestart = await confirm({
545
+ title: '重启 cc-connect',
546
+ message: '团队已从配置中删除。需要重启 cc-connect 才会停止对应运行时。',
547
+ confirmLabel: '立即重启',
548
+ cancelLabel: '稍后重启',
549
+ variant: 'danger',
550
+ });
551
+ if (shouldRestart) {
552
+ await api.ccSettings.restart();
553
+ }
554
+ }
543
555
  } catch {
544
556
  // error via store
545
557
  }