@yancyyu/openhermit 1.6.16 → 1.6.18

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.
Files changed (2) hide show
  1. package/bin/hermit.mjs +100 -75
  2. package/package.json +1 -1
package/bin/hermit.mjs CHANGED
@@ -90,13 +90,13 @@ 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 runtimeLogPath = path.join(hermitHome, 'logs', 'openhermit-runtime.log');
93
94
  const serverLogPath = path.join(hermitHome, 'logs', 'openhermit-server.log');
94
95
  const ccConnectConfigPath =
95
96
  process.env.HERMIT_CC_CONNECT_CONFIG ||
96
97
  process.env.CC_CONNECT_CONFIG ||
97
98
  path.join(hermitHome, 'cc-connect', 'config.toml');
98
- const bootstrapProjectName = 'default';
99
- const legacyBootstrapProjectName = '__openhermit_bootstrap__';
99
+ const bootstrapProjectNames = new Set(['default', '__openhermit_bootstrap__']);
100
100
 
101
101
  // ---------------------------------------------------------------------------
102
102
  // Update command
@@ -363,36 +363,6 @@ function parseTomlToken(raw, section) {
363
363
  return match?.[1] || '';
364
364
  }
365
365
 
366
- function hasProjectEntries(raw) {
367
- return /^\s*\[\[projects\]\]/m.test(raw);
368
- }
369
-
370
- function buildBootstrapProjectToml() {
371
- return `
372
- # Internal bootstrap project used only so cc-connect can start with an otherwise empty config.
373
- # It is safe to keep this project; users can replace or delete it after creating real teams.
374
- [[projects]]
375
- name = "${bootstrapProjectName}"
376
- disabled_commands = ["*"]
377
-
378
- [projects.agent]
379
- type = "claudecode"
380
-
381
- [projects.agent.options]
382
- work_dir = "${escapeTomlPath(hermitHome)}"
383
- mode = "default"
384
-
385
- [[projects.platforms]]
386
- type = "line"
387
-
388
- [projects.platforms.options]
389
- channel_secret = "openhermit-bootstrap"
390
- channel_token = "openhermit-bootstrap"
391
- port = "0"
392
- callback_path = "/openhermit-bootstrap"
393
- `;
394
- }
395
-
396
366
  function escapeRegExp(value) {
397
367
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
398
368
  }
@@ -414,16 +384,26 @@ function isManagedBootstrapBlock(block) {
414
384
  );
415
385
  }
416
386
 
417
- function migrateManagedBootstrapProject(raw) {
418
- const legacyBlock = findProjectBlock(raw, legacyBootstrapProjectName);
419
- if (!legacyBlock || !isManagedBootstrapBlock(legacyBlock.match[0])) return raw;
420
-
421
- const withoutLegacy = raw.replace(legacyBlock.pattern, '').replace(/\n{3,}/g, '\n\n').trimEnd();
422
- if (findProjectBlock(withoutLegacy, bootstrapProjectName)) {
423
- return `${withoutLegacy}\n`;
387
+ function stripManagedBootstrapProjects(raw) {
388
+ let next = raw;
389
+ for (const name of bootstrapProjectNames) {
390
+ const block = findProjectBlock(next, name);
391
+ if (block && isManagedBootstrapBlock(block.match[0])) {
392
+ next = next.replace(block.pattern, '').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n';
393
+ }
424
394
  }
395
+ return next;
396
+ }
425
397
 
426
- return `${withoutLegacy}\n${buildBootstrapProjectToml()}`;
398
+ function hasRealProjectEntries(raw) {
399
+ const projectPattern = /\[\[projects\]\]\nname\s*=\s*"([^"]+)"[\s\S]*?(?=\n\[\[projects\]\]|\s*$)/g;
400
+ for (const match of raw.matchAll(projectPattern)) {
401
+ const name = match[1];
402
+ if (!bootstrapProjectNames.has(name) || !isManagedBootstrapBlock(match[0])) {
403
+ return true;
404
+ }
405
+ }
406
+ return false;
427
407
  }
428
408
 
429
409
  function ensureCcConnectConfig() {
@@ -449,20 +429,15 @@ path = "/bridge/ws"
449
429
 
450
430
  [log]
451
431
  level = "info"
452
- ${buildBootstrapProjectToml()}`;
432
+ `;
453
433
  writeFileSync(ccConnectConfigPath, config, 'utf-8');
454
434
  }
455
435
 
456
436
  let raw = readFileSync(ccConnectConfigPath, 'utf-8');
457
- if (!hasProjectEntries(raw)) {
458
- raw = `${raw.trimEnd()}\n${buildBootstrapProjectToml()}`;
437
+ const migrated = stripManagedBootstrapProjects(raw);
438
+ if (migrated !== raw) {
439
+ raw = migrated;
459
440
  writeFileSync(ccConnectConfigPath, raw, 'utf-8');
460
- } else {
461
- const migrated = migrateManagedBootstrapProject(raw);
462
- if (migrated !== raw) {
463
- raw = migrated;
464
- writeFileSync(ccConnectConfigPath, raw, 'utf-8');
465
- }
466
441
  }
467
442
 
468
443
  return {
@@ -474,6 +449,7 @@ ${buildBootstrapProjectToml()}`;
474
449
  process.env.CC_CONNECT_BRIDGE_TOKEN ||
475
450
  process.env.CC_CONNECT_TOKEN ||
476
451
  parseTomlToken(raw, 'bridge'),
452
+ hasRealProjects: hasRealProjectEntries(raw),
477
453
  };
478
454
  }
479
455
 
@@ -493,6 +469,54 @@ async function waitForCcConnect(baseUrl, token, timeoutMs = 15_000) {
493
469
  return false;
494
470
  }
495
471
 
472
+ function appendLog(filePath, chunk) {
473
+ try {
474
+ mkdirSync(path.dirname(filePath), { recursive: true });
475
+ appendFileSync(filePath, chunk);
476
+ } catch {
477
+ // Logging must never block startup.
478
+ }
479
+ }
480
+
481
+ function printLogTail(label, filePath) {
482
+ try {
483
+ const content = readFileSync(filePath, 'utf-8');
484
+ const lines = content.trimEnd().split(/\r?\n/).slice(-80);
485
+ console.error(`[openHermit] ${label} log: ${filePath}`);
486
+ if (lines.length > 0) {
487
+ console.error(`[openHermit] Last ${label} log lines:`);
488
+ console.error(lines.join('\n'));
489
+ }
490
+ } catch {
491
+ console.error(`[openHermit] ${label} log: ${filePath}`);
492
+ }
493
+ }
494
+
495
+ async function waitForRuntimeReady(baseUrl, token, child, timeoutMs = 30_000) {
496
+ let exitCode = null;
497
+ child.once('exit', (code) => {
498
+ exitCode = code ?? 1;
499
+ });
500
+
501
+ const startedAt = Date.now();
502
+ while (Date.now() - startedAt < timeoutMs) {
503
+ if (exitCode !== null) {
504
+ throw new Error(`Runtime service exited before becoming ready (code ${exitCode})`);
505
+ }
506
+ try {
507
+ const res = await fetch(`${baseUrl}/api/v1/status`, {
508
+ headers: token ? { Authorization: `Bearer ${token}` } : undefined,
509
+ });
510
+ if (res.ok) return;
511
+ } catch {
512
+ // keep polling
513
+ }
514
+ await new Promise((resolve) => setTimeout(resolve, 500));
515
+ }
516
+
517
+ throw new Error(`Runtime service did not become ready within ${Math.round(timeoutMs / 1000)}s`);
518
+ }
519
+
496
520
  function resolveCcConnectRunner() {
497
521
  const pkgPath = require.resolve('cc-connect/package.json');
498
522
  return path.join(path.dirname(pkgPath), 'run.js');
@@ -519,6 +543,9 @@ if (!skipCcConnect) {
519
543
  const alreadyRunning = await waitForCcConnect(ccBaseUrl, ccTokens.managementToken, 1_000);
520
544
  if (alreadyRunning) {
521
545
  console.log(`[openHermit] Runtime service already running: ${ccBaseUrl}`);
546
+ } else if (!ccTokens.hasRealProjects) {
547
+ console.log('[openHermit] No runtime projects configured; starting control panel only.');
548
+ console.log('[openHermit] Create a team in the UI to install/configure a runtime.');
522
549
  } else {
523
550
  console.log('[openHermit] Starting bundled runtime service...');
524
551
  console.log(`[openHermit] Runtime config: ${ccConnectConfigPath}`);
@@ -531,11 +558,28 @@ if (!skipCcConnect) {
531
558
  CC_CONNECT_MANAGEMENT_TOKEN: ccTokens.managementToken,
532
559
  CC_CONNECT_BRIDGE_TOKEN: ccTokens.bridgeToken,
533
560
  },
534
- stdio: 'inherit',
561
+ stdio: ['ignore', 'pipe', 'pipe'],
562
+ });
563
+
564
+ ccConnectProcess.stdout?.on('data', (chunk) => {
565
+ process.stdout.write(chunk);
566
+ appendLog(runtimeLogPath, chunk);
535
567
  });
536
- const ready = await waitForCcConnect(ccBaseUrl, ccTokens.managementToken, 30_000);
537
- if (!ready) {
538
- console.warn('[openHermit] Runtime service did not become ready within 30s; openHermit will keep trying via API.');
568
+ ccConnectProcess.stderr?.on('data', (chunk) => {
569
+ process.stderr.write(chunk);
570
+ appendLog(runtimeLogPath, chunk);
571
+ });
572
+
573
+ try {
574
+ await waitForRuntimeReady(ccBaseUrl, ccTokens.managementToken, ccConnectProcess, 30_000);
575
+ } catch (err) {
576
+ console.error(
577
+ `[openHermit] Runtime service failed to start: ${err instanceof Error ? err.message : String(err)}`
578
+ );
579
+ printLogTail('Runtime', runtimeLogPath);
580
+ signalDaemon(ccConnectProcess.pid, 'SIGTERM');
581
+ setTimeout(() => signalDaemon(ccConnectProcess?.pid, 'SIGKILL'), 2_000).unref();
582
+ process.exit(1);
539
583
  }
540
584
  }
541
585
  }
@@ -577,27 +621,8 @@ if (!existsSync(distRenderererDir) || !existsSync(path.join(distRenderererDir, '
577
621
  // Start the server
578
622
  console.log('[openHermit] Launching server...\n');
579
623
 
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
624
  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
- }
625
+ printLogTail('Server', serverLogPath);
601
626
  }
602
627
 
603
628
  const serverProcess = spawn(process.execPath, ['--import', resolveAliasLoaderRegister(), '--import', resolveTsxLoader(), 'src/main/server.ts'], {
@@ -619,12 +644,12 @@ const serverProcess = spawn(process.execPath, ['--import', resolveAliasLoaderReg
619
644
 
620
645
  serverProcess.stdout?.on('data', (chunk) => {
621
646
  process.stdout.write(chunk);
622
- appendServerLog(chunk);
647
+ appendLog(serverLogPath, chunk);
623
648
  });
624
649
 
625
650
  serverProcess.stderr?.on('data', (chunk) => {
626
651
  process.stderr.write(chunk);
627
- appendServerLog(chunk);
652
+ appendLog(serverLogPath, chunk);
628
653
  });
629
654
 
630
655
  serverProcess.on('exit', (code) => {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@yancyyu/openhermit",
3
3
  "type": "module",
4
- "version": "1.6.16",
4
+ "version": "1.6.18",
5
5
  "description": "openHermit: team-oriented agent management workbench atop cc-connect.",
6
6
  "license": "AGPL-3.0",
7
7
  "author": {