@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 +111 -13
- package/package.json +1 -1
- package/src/main/server.ts +9 -0
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
|
|
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
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
package/src/main/server.ts
CHANGED
|
@@ -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
|
}
|