@yancyyu/openhermit 1.6.20 → 1.6.24

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
@@ -17,6 +17,7 @@
17
17
  import { spawn, execSync } from 'node:child_process';
18
18
  import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
19
19
  import { createRequire } from 'node:module';
20
+ import net from 'node:net';
20
21
  import os from 'node:os';
21
22
  import path from 'node:path';
22
23
  import { fileURLToPath, pathToFileURL } from 'node:url';
@@ -392,14 +393,46 @@ function isStarterProjectConfig(raw) {
392
393
  );
393
394
  }
394
395
 
395
- function hasRunnableProjectEntries(raw) {
396
+ function configRequiresClaudeCode(raw) {
397
+ return /type\s*=\s*"claudecode"/.test(raw);
398
+ }
399
+
400
+ function hasProjectEntries(raw) {
396
401
  const projectPattern = /\[\[projects\]\]\nname\s*=\s*"([^"]+)"[\s\S]*?(?=\n\[\[projects\]\]|\s*$)/g;
397
- for (const match of raw.matchAll(projectPattern)) {
398
- if (!isStarterProjectConfig(match[0]) && !isManagedBootstrapBlock(match[0])) {
399
- return true;
400
- }
402
+ return [...raw.matchAll(projectPattern)].some((match) => !isManagedBootstrapBlock(match[0]));
403
+ }
404
+
405
+ function commandExists(command) {
406
+ try {
407
+ execSync(`${command} --version`, { stdio: 'ignore', shell: true });
408
+ return true;
409
+ } catch {
410
+ return false;
401
411
  }
402
- return false;
412
+ }
413
+
414
+ function ensureClaudeCodeCliIfNeeded(raw) {
415
+ if (!configRequiresClaudeCode(raw) || commandExists('claude')) return;
416
+
417
+ console.log('[openHermit] Claude Code CLI not found.');
418
+ console.log('[openHermit] Installing @anthropic-ai/claude-code globally. This may take a few minutes...');
419
+ console.log('[openHermit] Running: npm install -g @anthropic-ai/claude-code@latest --prefer-online');
420
+ try {
421
+ execSync('npm install -g @anthropic-ai/claude-code@latest --prefer-online', {
422
+ stdio: 'inherit',
423
+ shell: true,
424
+ });
425
+ } catch (err) {
426
+ console.error('[openHermit] Claude Code CLI install command failed.');
427
+ console.error('[openHermit] Failed to install Claude Code CLI automatically.');
428
+ console.error('[openHermit] Please install it manually: npm install -g @anthropic-ai/claude-code@latest');
429
+ throw err;
430
+ }
431
+
432
+ if (!commandExists('claude')) {
433
+ throw new Error('Claude Code CLI was installed but `claude` is still not available in PATH');
434
+ }
435
+ console.log('[openHermit] Claude Code CLI installed and available in PATH.');
403
436
  }
404
437
 
405
438
  function readCcConnectConfigState() {
@@ -426,8 +459,9 @@ function readCcConnectConfigState() {
426
459
  process.env.CC_CONNECT_BRIDGE_TOKEN ||
427
460
  process.env.CC_CONNECT_TOKEN ||
428
461
  parseTomlToken(raw, 'bridge'),
429
- hasRunnableProjects: hasRunnableProjectEntries(raw),
462
+ hasProjects: hasProjectEntries(raw),
430
463
  isStarterConfig: isStarterProjectConfig(raw),
464
+ raw,
431
465
  };
432
466
  }
433
467
 
@@ -509,13 +543,62 @@ function resolveAliasLoaderRegister() {
509
543
  return `data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register(${JSON.stringify(aliasLoaderUrl)}, pathToFileURL("./"));`;
510
544
  }
511
545
 
546
+ async function checkExistingOpenHermitServer() {
547
+ const url = `http://127.0.0.1:${port}`;
548
+ try {
549
+ const res = await fetch(`${url}/api/version`, { signal: AbortSignal.timeout(1000) });
550
+ if (res.ok) {
551
+ const version = (await res.text()).trim() || 'unknown';
552
+ return { running: true, version, url };
553
+ }
554
+ } catch {
555
+ // Port may be unused or owned by another process.
556
+ }
557
+ return { running: false, version: '', url };
558
+ }
559
+
560
+ async function isTcpPortAvailable(portNumber) {
561
+ return new Promise((resolve) => {
562
+ const server = net.createServer();
563
+ server.once('error', () => resolve(false));
564
+ server.once('listening', () => {
565
+ server.close(() => resolve(true));
566
+ });
567
+ server.listen(portNumber, '127.0.0.1');
568
+ });
569
+ }
570
+
571
+ async function assertWebPortAvailable() {
572
+ const existingServer = await checkExistingOpenHermitServer();
573
+ if (existingServer.running) {
574
+ console.log(`[openHermit] Already running: ${existingServer.url}`);
575
+ console.log(`[openHermit] Version: ${existingServer.version}`);
576
+ console.log('[openHermit] Run `openhermit stop` first, or use `openhermit --port <port>` for another instance.');
577
+ process.exit(0);
578
+ }
579
+
580
+ const available = await isTcpPortAvailable(Number.parseInt(port, 10));
581
+ if (!available) {
582
+ console.error(`[openHermit] Port ${port} is already in use.`);
583
+ console.error('[openHermit] Stop the existing process first, or start with another port:');
584
+ console.error(` openhermit --port ${Number.parseInt(port, 10) + 1}`);
585
+ console.error('[openHermit] macOS/Linux: lsof -nP -iTCP:' + port + ' -sTCP:LISTEN');
586
+ console.error('[openHermit] Windows: netstat -ano | findstr :' + port);
587
+ process.exit(1);
588
+ }
589
+ }
590
+
512
591
  let ccConnectProcess = null;
513
592
  let ccTokens = {
514
593
  managementToken: process.env.CC_CONNECT_TOKEN || process.env.CC_CONNECT_MANAGEMENT_TOKEN || '',
515
594
  bridgeToken: process.env.CC_CONNECT_BRIDGE_TOKEN || process.env.CC_CONNECT_TOKEN || '',
516
595
  };
596
+ let runtimeSetupMode = false;
597
+
598
+ await assertWebPortAvailable();
517
599
 
518
600
  if (!skipCcConnect) {
601
+ let shouldStartRuntime = false;
519
602
  ccTokens = readCcConnectConfigState();
520
603
  const ccBaseUrl = process.env.CC_CONNECT_BASE_URL || 'http://127.0.0.1:9820';
521
604
  const alreadyRunning = await waitForCcConnect(ccBaseUrl, ccTokens.managementToken, 1_000);
@@ -551,19 +634,33 @@ if (!skipCcConnect) {
551
634
  ccTokens = readCcConnectConfigState();
552
635
  if (initCode === 0 && ccTokens.configExists) {
553
636
  console.log('[openHermit] Runtime starter config created.');
554
- console.log('[openHermit] Open the panel to finish setup before starting the runtime service.');
637
+ try {
638
+ ensureClaudeCodeCliIfNeeded(ccTokens.raw);
639
+ } catch {
640
+ printLogTail('Runtime', runtimeLogPath);
641
+ process.exit(1);
642
+ }
643
+ shouldStartRuntime = true;
555
644
  } else {
556
645
  console.error(`[openHermit] Runtime config initialization failed (code ${initCode}).`);
557
646
  printLogTail('Runtime', runtimeLogPath);
558
647
  process.exit(1);
559
648
  }
560
- } else if (!ccTokens.hasRunnableProjects) {
561
- console.log('[openHermit] Runtime config is a starter template; starting control panel only.');
562
- console.log(`[openHermit] Edit runtime config in the UI before starting the runtime service.`);
563
- if (ccTokens.isStarterConfig) {
564
- console.log(`[openHermit] Starter config: ${ccConnectConfigPath}`);
649
+ } else if (ccTokens.hasProjects) {
650
+ try {
651
+ ensureClaudeCodeCliIfNeeded(ccTokens.raw);
652
+ } catch {
653
+ printLogTail('Runtime', runtimeLogPath);
654
+ process.exit(1);
565
655
  }
656
+ shouldStartRuntime = true;
566
657
  } else {
658
+ console.error('[openHermit] Runtime config has no projects. Please edit the config and try again.');
659
+ console.error(`[openHermit] Runtime config: ${ccConnectConfigPath}`);
660
+ process.exit(1);
661
+ }
662
+
663
+ if (shouldStartRuntime) {
567
664
  console.log('[openHermit] Starting bundled runtime service...');
568
665
  console.log(`[openHermit] Runtime config: ${ccConnectConfigPath}`);
569
666
  ccConnectProcess = spawn(process.execPath, [resolveCcConnectRunner(), '-config', ccConnectConfigPath], {
@@ -651,6 +748,7 @@ const serverProcess = spawn(process.execPath, ['--import', resolveAliasLoaderReg
651
748
  HOST: process.env.HOST || '127.0.0.1',
652
749
  NODE_ENV: 'production',
653
750
  HERMIT_HOME: hermitHome,
751
+ HERMIT_RUNTIME_SETUP_MODE: runtimeSetupMode ? '1' : '0',
654
752
  CC_CONNECT_TOKEN: ccTokens.managementToken,
655
753
  CC_CONNECT_MANAGEMENT_TOKEN: ccTokens.managementToken,
656
754
  CC_CONNECT_BRIDGE_TOKEN: ccTokens.bridgeToken,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@yancyyu/openhermit",
3
3
  "type": "module",
4
- "version": "1.6.20",
4
+ "version": "1.6.24",
5
5
  "description": "openHermit: team-oriented agent management workbench atop cc-connect.",
6
6
  "license": "AGPL-3.0",
7
7
  "author": {
@@ -68,6 +68,7 @@ const HOST = process.env.HOST ?? '127.0.0.1';
68
68
  const PORT = Number.parseInt(process.env.PORT ?? '5680', 10);
69
69
  const STATIC_DIR = process.env.STATIC_DIR ?? path.resolve(REPO_ROOT, 'dist-renderer');
70
70
  const HARNESS_BRIDGE_CONNECT_TIMEOUT_MS = 10_000;
71
+ const RUNTIME_SETUP_MODE = process.env.HERMIT_RUNTIME_SETUP_MODE === '1';
71
72
 
72
73
  // ===========================================================================
73
74
  // Hermit runtime config — ~/.hermit/config.json
@@ -2183,6 +2184,11 @@ function isCronNotFoundError(error: unknown): boolean {
2183
2184
  return /(\b404\b|not found|no matching|does not exist|不存在)/i.test(message);
2184
2185
  }
2185
2186
 
2187
+ function isRuntimeUnavailableError(error: unknown): boolean {
2188
+ const message = error instanceof Error ? error.message : String(error);
2189
+ return /ECONNREFUSED|ECONNRESET|ENOTFOUND|ETIMEDOUT|fetch failed|cc-connect 不可达/i.test(message);
2190
+ }
2191
+
2186
2192
  app.get('/api/schedules', async () => {
2187
2193
  try {
2188
2194
  const jobs = await cc.listCronJobs();
@@ -2190,6 +2196,9 @@ app.get('/api/schedules', async () => {
2190
2196
  const workDirMap = await resolveTeamWorkDirs(jobs.map((job) => job.project));
2191
2197
  return jobs.map((job) => mapCronJobToSchedule(job, workDirMap.get(job.project) ?? ''));
2192
2198
  } catch (err) {
2199
+ if (RUNTIME_SETUP_MODE && isRuntimeUnavailableError(err)) {
2200
+ return [];
2201
+ }
2193
2202
  app.log.warn({ err }, 'list schedules from cc-connect failed');
2194
2203
  return [];
2195
2204
  }