@yancyyu/openhermit 1.6.23 → 1.6.25
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 +88 -134
- package/package.json +1 -1
- package/src/main/server.ts +0 -9
package/bin/hermit.mjs
CHANGED
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { spawn, execSync } from 'node:child_process';
|
|
18
|
+
import crypto from 'node:crypto';
|
|
18
19
|
import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
19
20
|
import { createRequire } from 'node:module';
|
|
20
|
-
import net from 'node:net';
|
|
21
21
|
import os from 'node:os';
|
|
22
22
|
import path from 'node:path';
|
|
23
23
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
@@ -96,7 +96,8 @@ const ccConnectConfigPath =
|
|
|
96
96
|
process.env.HERMIT_CC_CONNECT_CONFIG ||
|
|
97
97
|
process.env.CC_CONNECT_CONFIG ||
|
|
98
98
|
path.join(hermitHome, 'cc-connect', 'config.toml');
|
|
99
|
-
const
|
|
99
|
+
const bootstrapProjectName = 'default';
|
|
100
|
+
const legacyBootstrapProjectName = '__openhermit_bootstrap__';
|
|
100
101
|
|
|
101
102
|
// ---------------------------------------------------------------------------
|
|
102
103
|
// Update command
|
|
@@ -350,6 +351,10 @@ Please install dependencies first:
|
|
|
350
351
|
// cc-connect sidecar
|
|
351
352
|
// ---------------------------------------------------------------------------
|
|
352
353
|
|
|
354
|
+
function randomToken() {
|
|
355
|
+
return crypto.randomBytes(16).toString('hex');
|
|
356
|
+
}
|
|
357
|
+
|
|
353
358
|
function escapeTomlPath(value) {
|
|
354
359
|
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
355
360
|
}
|
|
@@ -359,6 +364,36 @@ function parseTomlToken(raw, section) {
|
|
|
359
364
|
return match?.[1] || '';
|
|
360
365
|
}
|
|
361
366
|
|
|
367
|
+
function hasProjectEntries(raw) {
|
|
368
|
+
return /^\s*\[\[projects\]\]/m.test(raw);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function buildBootstrapProjectToml() {
|
|
372
|
+
return `
|
|
373
|
+
# Internal bootstrap project used only so cc-connect can start with an otherwise empty config.
|
|
374
|
+
# It is safe to keep this project; users can replace or delete it after creating real teams.
|
|
375
|
+
[[projects]]
|
|
376
|
+
name = "${bootstrapProjectName}"
|
|
377
|
+
disabled_commands = ["*"]
|
|
378
|
+
|
|
379
|
+
[projects.agent]
|
|
380
|
+
type = "claudecode"
|
|
381
|
+
|
|
382
|
+
[projects.agent.options]
|
|
383
|
+
work_dir = "${escapeTomlPath(hermitHome)}"
|
|
384
|
+
mode = "default"
|
|
385
|
+
|
|
386
|
+
[[projects.platforms]]
|
|
387
|
+
type = "line"
|
|
388
|
+
|
|
389
|
+
[projects.platforms.options]
|
|
390
|
+
channel_secret = "openhermit-bootstrap"
|
|
391
|
+
channel_token = "openhermit-bootstrap"
|
|
392
|
+
port = "0"
|
|
393
|
+
callback_path = "/openhermit-bootstrap"
|
|
394
|
+
`;
|
|
395
|
+
}
|
|
396
|
+
|
|
362
397
|
function escapeRegExp(value) {
|
|
363
398
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
364
399
|
}
|
|
@@ -380,28 +415,22 @@ function isManagedBootstrapBlock(block) {
|
|
|
380
415
|
);
|
|
381
416
|
}
|
|
382
417
|
|
|
383
|
-
function
|
|
384
|
-
const
|
|
385
|
-
if (!
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
);
|
|
418
|
+
function migrateManagedBootstrapProject(raw) {
|
|
419
|
+
const legacyBlock = findProjectBlock(raw, legacyBootstrapProjectName);
|
|
420
|
+
if (!legacyBlock || !isManagedBootstrapBlock(legacyBlock.match[0])) return raw;
|
|
421
|
+
|
|
422
|
+
const withoutLegacy = raw.replace(legacyBlock.pattern, '').replace(/\n{3,}/g, '\n\n').trimEnd();
|
|
423
|
+
if (findProjectBlock(withoutLegacy, bootstrapProjectName)) {
|
|
424
|
+
return `${withoutLegacy}\n`;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return `${withoutLegacy}\n${buildBootstrapProjectToml()}`;
|
|
394
428
|
}
|
|
395
429
|
|
|
396
430
|
function configRequiresClaudeCode(raw) {
|
|
397
431
|
return /type\s*=\s*"claudecode"/.test(raw);
|
|
398
432
|
}
|
|
399
433
|
|
|
400
|
-
function hasProjectEntries(raw) {
|
|
401
|
-
const projectPattern = /\[\[projects\]\]\nname\s*=\s*"([^"]+)"[\s\S]*?(?=\n\[\[projects\]\]|\s*$)/g;
|
|
402
|
-
return [...raw.matchAll(projectPattern)].some((match) => !isManagedBootstrapBlock(match[0]));
|
|
403
|
-
}
|
|
404
|
-
|
|
405
434
|
function commandExists(command) {
|
|
406
435
|
try {
|
|
407
436
|
execSync(`${command} --version`, { stdio: 'ignore', shell: true });
|
|
@@ -414,13 +443,16 @@ function commandExists(command) {
|
|
|
414
443
|
function ensureClaudeCodeCliIfNeeded(raw) {
|
|
415
444
|
if (!configRequiresClaudeCode(raw) || commandExists('claude')) return;
|
|
416
445
|
|
|
417
|
-
console.log('[openHermit] Claude Code CLI not found
|
|
446
|
+
console.log('[openHermit] Claude Code CLI not found.');
|
|
447
|
+
console.log('[openHermit] Installing @anthropic-ai/claude-code globally. This may take a few minutes...');
|
|
448
|
+
console.log('[openHermit] Running: npm install -g @anthropic-ai/claude-code@latest --prefer-online');
|
|
418
449
|
try {
|
|
419
450
|
execSync('npm install -g @anthropic-ai/claude-code@latest --prefer-online', {
|
|
420
451
|
stdio: 'inherit',
|
|
421
452
|
shell: true,
|
|
422
453
|
});
|
|
423
454
|
} catch (err) {
|
|
455
|
+
console.error('[openHermit] Claude Code CLI install command failed.');
|
|
424
456
|
console.error('[openHermit] Failed to install Claude Code CLI automatically.');
|
|
425
457
|
console.error('[openHermit] Please install it manually: npm install -g @anthropic-ai/claude-code@latest');
|
|
426
458
|
throw err;
|
|
@@ -429,25 +461,49 @@ function ensureClaudeCodeCliIfNeeded(raw) {
|
|
|
429
461
|
if (!commandExists('claude')) {
|
|
430
462
|
throw new Error('Claude Code CLI was installed but `claude` is still not available in PATH');
|
|
431
463
|
}
|
|
432
|
-
console.log('[openHermit] Claude Code CLI installed.');
|
|
464
|
+
console.log('[openHermit] Claude Code CLI installed and available in PATH.');
|
|
433
465
|
}
|
|
434
466
|
|
|
435
|
-
function
|
|
467
|
+
function ensureCcConnectConfig() {
|
|
436
468
|
mkdirSync(path.dirname(ccConnectConfigPath), { recursive: true });
|
|
437
469
|
if (!existsSync(ccConnectConfigPath)) {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
470
|
+
const managementToken = randomToken();
|
|
471
|
+
const bridgeToken = randomToken();
|
|
472
|
+
const config = `data_dir = "${escapeTomlPath(path.join(hermitHome, 'cc-connect', 'data'))}"
|
|
473
|
+
language = "zh"
|
|
474
|
+
|
|
475
|
+
[management]
|
|
476
|
+
enabled = true
|
|
477
|
+
host = "127.0.0.1"
|
|
478
|
+
port = 9820
|
|
479
|
+
token = "${managementToken}"
|
|
480
|
+
|
|
481
|
+
[bridge]
|
|
482
|
+
enabled = true
|
|
483
|
+
host = "127.0.0.1"
|
|
484
|
+
port = 9810
|
|
485
|
+
token = "${bridgeToken}"
|
|
486
|
+
path = "/bridge/ws"
|
|
487
|
+
|
|
488
|
+
[log]
|
|
489
|
+
level = "info"
|
|
490
|
+
${buildBootstrapProjectToml()}`;
|
|
491
|
+
writeFileSync(ccConnectConfigPath, config, 'utf-8');
|
|
445
492
|
}
|
|
446
493
|
|
|
447
|
-
|
|
494
|
+
let raw = readFileSync(ccConnectConfigPath, 'utf-8');
|
|
495
|
+
if (!hasProjectEntries(raw)) {
|
|
496
|
+
raw = `${raw.trimEnd()}\n${buildBootstrapProjectToml()}`;
|
|
497
|
+
writeFileSync(ccConnectConfigPath, raw, 'utf-8');
|
|
498
|
+
} else {
|
|
499
|
+
const migrated = migrateManagedBootstrapProject(raw);
|
|
500
|
+
if (migrated !== raw) {
|
|
501
|
+
raw = migrated;
|
|
502
|
+
writeFileSync(ccConnectConfigPath, raw, 'utf-8');
|
|
503
|
+
}
|
|
504
|
+
}
|
|
448
505
|
|
|
449
506
|
return {
|
|
450
|
-
configExists: true,
|
|
451
507
|
managementToken:
|
|
452
508
|
process.env.CC_CONNECT_TOKEN ||
|
|
453
509
|
process.env.CC_CONNECT_MANAGEMENT_TOKEN ||
|
|
@@ -456,8 +512,6 @@ function readCcConnectConfigState() {
|
|
|
456
512
|
process.env.CC_CONNECT_BRIDGE_TOKEN ||
|
|
457
513
|
process.env.CC_CONNECT_TOKEN ||
|
|
458
514
|
parseTomlToken(raw, 'bridge'),
|
|
459
|
-
hasProjects: hasProjectEntries(raw),
|
|
460
|
-
isStarterConfig: isStarterProjectConfig(raw),
|
|
461
515
|
raw,
|
|
462
516
|
};
|
|
463
517
|
}
|
|
@@ -540,124 +594,25 @@ function resolveAliasLoaderRegister() {
|
|
|
540
594
|
return `data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register(${JSON.stringify(aliasLoaderUrl)}, pathToFileURL("./"));`;
|
|
541
595
|
}
|
|
542
596
|
|
|
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
|
-
|
|
588
597
|
let ccConnectProcess = null;
|
|
589
598
|
let ccTokens = {
|
|
590
599
|
managementToken: process.env.CC_CONNECT_TOKEN || process.env.CC_CONNECT_MANAGEMENT_TOKEN || '',
|
|
591
600
|
bridgeToken: process.env.CC_CONNECT_BRIDGE_TOKEN || process.env.CC_CONNECT_TOKEN || '',
|
|
592
601
|
};
|
|
593
|
-
let runtimeSetupMode = false;
|
|
594
|
-
|
|
595
|
-
await assertWebPortAvailable();
|
|
596
602
|
|
|
597
603
|
if (!skipCcConnect) {
|
|
598
|
-
|
|
599
|
-
ccTokens = readCcConnectConfigState();
|
|
604
|
+
ccTokens = ensureCcConnectConfig();
|
|
600
605
|
const ccBaseUrl = process.env.CC_CONNECT_BASE_URL || 'http://127.0.0.1:9820';
|
|
601
606
|
const alreadyRunning = await waitForCcConnect(ccBaseUrl, ccTokens.managementToken, 1_000);
|
|
602
607
|
if (alreadyRunning) {
|
|
603
608
|
console.log(`[openHermit] Runtime service already running: ${ccBaseUrl}`);
|
|
604
|
-
} else
|
|
605
|
-
console.log('[openHermit] Initializing runtime config with bundled runtime service...');
|
|
606
|
-
console.log(`[openHermit] Runtime config: ${ccConnectConfigPath}`);
|
|
607
|
-
const initProcess = spawn(process.execPath, [resolveCcConnectRunner(), '-config', ccConnectConfigPath], {
|
|
608
|
-
cwd: repoRoot,
|
|
609
|
-
env: {
|
|
610
|
-
...process.env,
|
|
611
|
-
CC_CONNECT_TOKEN: ccTokens.managementToken,
|
|
612
|
-
CC_CONNECT_MANAGEMENT_TOKEN: ccTokens.managementToken,
|
|
613
|
-
CC_CONNECT_BRIDGE_TOKEN: ccTokens.bridgeToken,
|
|
614
|
-
},
|
|
615
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
616
|
-
});
|
|
617
|
-
initProcess.stdout?.on('data', (chunk) => {
|
|
618
|
-
process.stdout.write(chunk);
|
|
619
|
-
appendLog(runtimeLogPath, chunk);
|
|
620
|
-
});
|
|
621
|
-
initProcess.stderr?.on('data', (chunk) => {
|
|
622
|
-
process.stderr.write(chunk);
|
|
623
|
-
appendLog(runtimeLogPath, chunk);
|
|
624
|
-
});
|
|
625
|
-
|
|
626
|
-
const initCode = await new Promise((resolve) => {
|
|
627
|
-
initProcess.on('exit', (code) => resolve(code ?? 1));
|
|
628
|
-
initProcess.on('error', () => resolve(1));
|
|
629
|
-
});
|
|
630
|
-
|
|
631
|
-
ccTokens = readCcConnectConfigState();
|
|
632
|
-
if (initCode === 0 && ccTokens.configExists) {
|
|
633
|
-
console.log('[openHermit] Runtime starter config created.');
|
|
634
|
-
try {
|
|
635
|
-
ensureClaudeCodeCliIfNeeded(ccTokens.raw);
|
|
636
|
-
} catch {
|
|
637
|
-
printLogTail('Runtime', runtimeLogPath);
|
|
638
|
-
process.exit(1);
|
|
639
|
-
}
|
|
640
|
-
shouldStartRuntime = true;
|
|
641
|
-
} else {
|
|
642
|
-
console.error(`[openHermit] Runtime config initialization failed (code ${initCode}).`);
|
|
643
|
-
printLogTail('Runtime', runtimeLogPath);
|
|
644
|
-
process.exit(1);
|
|
645
|
-
}
|
|
646
|
-
} else if (ccTokens.hasProjects) {
|
|
609
|
+
} else {
|
|
647
610
|
try {
|
|
648
611
|
ensureClaudeCodeCliIfNeeded(ccTokens.raw);
|
|
649
612
|
} catch {
|
|
650
613
|
printLogTail('Runtime', runtimeLogPath);
|
|
651
614
|
process.exit(1);
|
|
652
615
|
}
|
|
653
|
-
shouldStartRuntime = true;
|
|
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) {
|
|
661
616
|
console.log('[openHermit] Starting bundled runtime service...');
|
|
662
617
|
console.log(`[openHermit] Runtime config: ${ccConnectConfigPath}`);
|
|
663
618
|
ccConnectProcess = spawn(process.execPath, [resolveCcConnectRunner(), '-config', ccConnectConfigPath], {
|
|
@@ -745,7 +700,6 @@ const serverProcess = spawn(process.execPath, ['--import', resolveAliasLoaderReg
|
|
|
745
700
|
HOST: process.env.HOST || '127.0.0.1',
|
|
746
701
|
NODE_ENV: 'production',
|
|
747
702
|
HERMIT_HOME: hermitHome,
|
|
748
|
-
HERMIT_RUNTIME_SETUP_MODE: runtimeSetupMode ? '1' : '0',
|
|
749
703
|
CC_CONNECT_TOKEN: ccTokens.managementToken,
|
|
750
704
|
CC_CONNECT_MANAGEMENT_TOKEN: ccTokens.managementToken,
|
|
751
705
|
CC_CONNECT_BRIDGE_TOKEN: ccTokens.bridgeToken,
|
package/package.json
CHANGED
package/src/main/server.ts
CHANGED
|
@@ -68,7 +68,6 @@ 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';
|
|
72
71
|
|
|
73
72
|
// ===========================================================================
|
|
74
73
|
// Hermit runtime config — ~/.hermit/config.json
|
|
@@ -2184,11 +2183,6 @@ function isCronNotFoundError(error: unknown): boolean {
|
|
|
2184
2183
|
return /(\b404\b|not found|no matching|does not exist|不存在)/i.test(message);
|
|
2185
2184
|
}
|
|
2186
2185
|
|
|
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
|
-
|
|
2192
2186
|
app.get('/api/schedules', async () => {
|
|
2193
2187
|
try {
|
|
2194
2188
|
const jobs = await cc.listCronJobs();
|
|
@@ -2196,9 +2190,6 @@ app.get('/api/schedules', async () => {
|
|
|
2196
2190
|
const workDirMap = await resolveTeamWorkDirs(jobs.map((job) => job.project));
|
|
2197
2191
|
return jobs.map((job) => mapCronJobToSchedule(job, workDirMap.get(job.project) ?? ''));
|
|
2198
2192
|
} catch (err) {
|
|
2199
|
-
if (RUNTIME_SETUP_MODE && isRuntimeUnavailableError(err)) {
|
|
2200
|
-
return [];
|
|
2201
|
-
}
|
|
2202
2193
|
app.log.warn({ err }, 'list schedules from cc-connect failed');
|
|
2203
2194
|
return [];
|
|
2204
2195
|
}
|