@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 +108 -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,43 @@ 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; 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
|
-
|
|
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
|
-
|
|
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 (
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
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
|
}
|