@yancyyu/openhermit 1.6.20 → 1.6.23

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,43 @@ 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; installing @anthropic-ai/claude-code...');
418
+ try {
419
+ execSync('npm install -g @anthropic-ai/claude-code@latest --prefer-online', {
420
+ stdio: 'inherit',
421
+ shell: true,
422
+ });
423
+ } catch (err) {
424
+ console.error('[openHermit] Failed to install Claude Code CLI automatically.');
425
+ console.error('[openHermit] Please install it manually: npm install -g @anthropic-ai/claude-code@latest');
426
+ throw err;
427
+ }
428
+
429
+ if (!commandExists('claude')) {
430
+ throw new Error('Claude Code CLI was installed but `claude` is still not available in PATH');
431
+ }
432
+ console.log('[openHermit] Claude Code CLI installed.');
403
433
  }
404
434
 
405
435
  function readCcConnectConfigState() {
@@ -426,8 +456,9 @@ function readCcConnectConfigState() {
426
456
  process.env.CC_CONNECT_BRIDGE_TOKEN ||
427
457
  process.env.CC_CONNECT_TOKEN ||
428
458
  parseTomlToken(raw, 'bridge'),
429
- hasRunnableProjects: hasRunnableProjectEntries(raw),
459
+ hasProjects: hasProjectEntries(raw),
430
460
  isStarterConfig: isStarterProjectConfig(raw),
461
+ raw,
431
462
  };
432
463
  }
433
464
 
@@ -509,13 +540,62 @@ function resolveAliasLoaderRegister() {
509
540
  return `data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register(${JSON.stringify(aliasLoaderUrl)}, pathToFileURL("./"));`;
510
541
  }
511
542
 
543
+ async function checkExistingOpenHermitServer() {
544
+ const url = `http://127.0.0.1:${port}`;
545
+ try {
546
+ const res = await fetch(`${url}/api/version`, { signal: AbortSignal.timeout(1000) });
547
+ if (res.ok) {
548
+ const version = (await res.text()).trim() || 'unknown';
549
+ return { running: true, version, url };
550
+ }
551
+ } catch {
552
+ // Port may be unused or owned by another process.
553
+ }
554
+ return { running: false, version: '', url };
555
+ }
556
+
557
+ async function isTcpPortAvailable(portNumber) {
558
+ return new Promise((resolve) => {
559
+ const server = net.createServer();
560
+ server.once('error', () => resolve(false));
561
+ server.once('listening', () => {
562
+ server.close(() => resolve(true));
563
+ });
564
+ server.listen(portNumber, '127.0.0.1');
565
+ });
566
+ }
567
+
568
+ async function assertWebPortAvailable() {
569
+ const existingServer = await checkExistingOpenHermitServer();
570
+ if (existingServer.running) {
571
+ console.log(`[openHermit] Already running: ${existingServer.url}`);
572
+ console.log(`[openHermit] Version: ${existingServer.version}`);
573
+ console.log('[openHermit] Run `openhermit stop` first, or use `openhermit --port <port>` for another instance.');
574
+ process.exit(0);
575
+ }
576
+
577
+ const available = await isTcpPortAvailable(Number.parseInt(port, 10));
578
+ if (!available) {
579
+ console.error(`[openHermit] Port ${port} is already in use.`);
580
+ console.error('[openHermit] Stop the existing process first, or start with another port:');
581
+ console.error(` openhermit --port ${Number.parseInt(port, 10) + 1}`);
582
+ console.error('[openHermit] macOS/Linux: lsof -nP -iTCP:' + port + ' -sTCP:LISTEN');
583
+ console.error('[openHermit] Windows: netstat -ano | findstr :' + port);
584
+ process.exit(1);
585
+ }
586
+ }
587
+
512
588
  let ccConnectProcess = null;
513
589
  let ccTokens = {
514
590
  managementToken: process.env.CC_CONNECT_TOKEN || process.env.CC_CONNECT_MANAGEMENT_TOKEN || '',
515
591
  bridgeToken: process.env.CC_CONNECT_BRIDGE_TOKEN || process.env.CC_CONNECT_TOKEN || '',
516
592
  };
593
+ let runtimeSetupMode = false;
594
+
595
+ await assertWebPortAvailable();
517
596
 
518
597
  if (!skipCcConnect) {
598
+ let shouldStartRuntime = false;
519
599
  ccTokens = readCcConnectConfigState();
520
600
  const ccBaseUrl = process.env.CC_CONNECT_BASE_URL || 'http://127.0.0.1:9820';
521
601
  const alreadyRunning = await waitForCcConnect(ccBaseUrl, ccTokens.managementToken, 1_000);
@@ -551,19 +631,33 @@ if (!skipCcConnect) {
551
631
  ccTokens = readCcConnectConfigState();
552
632
  if (initCode === 0 && ccTokens.configExists) {
553
633
  console.log('[openHermit] Runtime starter config created.');
554
- console.log('[openHermit] Open the panel to finish setup before starting the runtime service.');
634
+ try {
635
+ ensureClaudeCodeCliIfNeeded(ccTokens.raw);
636
+ } catch {
637
+ printLogTail('Runtime', runtimeLogPath);
638
+ process.exit(1);
639
+ }
640
+ shouldStartRuntime = true;
555
641
  } else {
556
642
  console.error(`[openHermit] Runtime config initialization failed (code ${initCode}).`);
557
643
  printLogTail('Runtime', runtimeLogPath);
558
644
  process.exit(1);
559
645
  }
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}`);
646
+ } else if (ccTokens.hasProjects) {
647
+ try {
648
+ ensureClaudeCodeCliIfNeeded(ccTokens.raw);
649
+ } catch {
650
+ printLogTail('Runtime', runtimeLogPath);
651
+ process.exit(1);
565
652
  }
653
+ shouldStartRuntime = true;
566
654
  } else {
655
+ console.error('[openHermit] Runtime config has no projects. Please edit the config and try again.');
656
+ console.error(`[openHermit] Runtime config: ${ccConnectConfigPath}`);
657
+ process.exit(1);
658
+ }
659
+
660
+ if (shouldStartRuntime) {
567
661
  console.log('[openHermit] Starting bundled runtime service...');
568
662
  console.log(`[openHermit] Runtime config: ${ccConnectConfigPath}`);
569
663
  ccConnectProcess = spawn(process.execPath, [resolveCcConnectRunner(), '-config', ccConnectConfigPath], {
@@ -651,6 +745,7 @@ const serverProcess = spawn(process.execPath, ['--import', resolveAliasLoaderReg
651
745
  HOST: process.env.HOST || '127.0.0.1',
652
746
  NODE_ENV: 'production',
653
747
  HERMIT_HOME: hermitHome,
748
+ HERMIT_RUNTIME_SETUP_MODE: runtimeSetupMode ? '1' : '0',
654
749
  CC_CONNECT_TOKEN: ccTokens.managementToken,
655
750
  CC_CONNECT_MANAGEMENT_TOKEN: ccTokens.managementToken,
656
751
  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.23",
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
  }