@yancyyu/openhermit 1.6.24 → 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 +83 -132
- 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 });
|
|
@@ -435,22 +464,46 @@ function ensureClaudeCodeCliIfNeeded(raw) {
|
|
|
435
464
|
console.log('[openHermit] Claude Code CLI installed and available in PATH.');
|
|
436
465
|
}
|
|
437
466
|
|
|
438
|
-
function
|
|
467
|
+
function ensureCcConnectConfig() {
|
|
439
468
|
mkdirSync(path.dirname(ccConnectConfigPath), { recursive: true });
|
|
440
469
|
if (!existsSync(ccConnectConfigPath)) {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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');
|
|
448
492
|
}
|
|
449
493
|
|
|
450
|
-
|
|
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
|
+
}
|
|
451
505
|
|
|
452
506
|
return {
|
|
453
|
-
configExists: true,
|
|
454
507
|
managementToken:
|
|
455
508
|
process.env.CC_CONNECT_TOKEN ||
|
|
456
509
|
process.env.CC_CONNECT_MANAGEMENT_TOKEN ||
|
|
@@ -459,8 +512,6 @@ function readCcConnectConfigState() {
|
|
|
459
512
|
process.env.CC_CONNECT_BRIDGE_TOKEN ||
|
|
460
513
|
process.env.CC_CONNECT_TOKEN ||
|
|
461
514
|
parseTomlToken(raw, 'bridge'),
|
|
462
|
-
hasProjects: hasProjectEntries(raw),
|
|
463
|
-
isStarterConfig: isStarterProjectConfig(raw),
|
|
464
515
|
raw,
|
|
465
516
|
};
|
|
466
517
|
}
|
|
@@ -543,124 +594,25 @@ function resolveAliasLoaderRegister() {
|
|
|
543
594
|
return `data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register(${JSON.stringify(aliasLoaderUrl)}, pathToFileURL("./"));`;
|
|
544
595
|
}
|
|
545
596
|
|
|
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
|
-
|
|
591
597
|
let ccConnectProcess = null;
|
|
592
598
|
let ccTokens = {
|
|
593
599
|
managementToken: process.env.CC_CONNECT_TOKEN || process.env.CC_CONNECT_MANAGEMENT_TOKEN || '',
|
|
594
600
|
bridgeToken: process.env.CC_CONNECT_BRIDGE_TOKEN || process.env.CC_CONNECT_TOKEN || '',
|
|
595
601
|
};
|
|
596
|
-
let runtimeSetupMode = false;
|
|
597
|
-
|
|
598
|
-
await assertWebPortAvailable();
|
|
599
602
|
|
|
600
603
|
if (!skipCcConnect) {
|
|
601
|
-
|
|
602
|
-
ccTokens = readCcConnectConfigState();
|
|
604
|
+
ccTokens = ensureCcConnectConfig();
|
|
603
605
|
const ccBaseUrl = process.env.CC_CONNECT_BASE_URL || 'http://127.0.0.1:9820';
|
|
604
606
|
const alreadyRunning = await waitForCcConnect(ccBaseUrl, ccTokens.managementToken, 1_000);
|
|
605
607
|
if (alreadyRunning) {
|
|
606
608
|
console.log(`[openHermit] Runtime service already running: ${ccBaseUrl}`);
|
|
607
|
-
} else
|
|
608
|
-
console.log('[openHermit] Initializing runtime config with bundled runtime service...');
|
|
609
|
-
console.log(`[openHermit] Runtime config: ${ccConnectConfigPath}`);
|
|
610
|
-
const initProcess = spawn(process.execPath, [resolveCcConnectRunner(), '-config', ccConnectConfigPath], {
|
|
611
|
-
cwd: repoRoot,
|
|
612
|
-
env: {
|
|
613
|
-
...process.env,
|
|
614
|
-
CC_CONNECT_TOKEN: ccTokens.managementToken,
|
|
615
|
-
CC_CONNECT_MANAGEMENT_TOKEN: ccTokens.managementToken,
|
|
616
|
-
CC_CONNECT_BRIDGE_TOKEN: ccTokens.bridgeToken,
|
|
617
|
-
},
|
|
618
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
619
|
-
});
|
|
620
|
-
initProcess.stdout?.on('data', (chunk) => {
|
|
621
|
-
process.stdout.write(chunk);
|
|
622
|
-
appendLog(runtimeLogPath, chunk);
|
|
623
|
-
});
|
|
624
|
-
initProcess.stderr?.on('data', (chunk) => {
|
|
625
|
-
process.stderr.write(chunk);
|
|
626
|
-
appendLog(runtimeLogPath, chunk);
|
|
627
|
-
});
|
|
628
|
-
|
|
629
|
-
const initCode = await new Promise((resolve) => {
|
|
630
|
-
initProcess.on('exit', (code) => resolve(code ?? 1));
|
|
631
|
-
initProcess.on('error', () => resolve(1));
|
|
632
|
-
});
|
|
633
|
-
|
|
634
|
-
ccTokens = readCcConnectConfigState();
|
|
635
|
-
if (initCode === 0 && ccTokens.configExists) {
|
|
636
|
-
console.log('[openHermit] Runtime starter config created.');
|
|
637
|
-
try {
|
|
638
|
-
ensureClaudeCodeCliIfNeeded(ccTokens.raw);
|
|
639
|
-
} catch {
|
|
640
|
-
printLogTail('Runtime', runtimeLogPath);
|
|
641
|
-
process.exit(1);
|
|
642
|
-
}
|
|
643
|
-
shouldStartRuntime = true;
|
|
644
|
-
} else {
|
|
645
|
-
console.error(`[openHermit] Runtime config initialization failed (code ${initCode}).`);
|
|
646
|
-
printLogTail('Runtime', runtimeLogPath);
|
|
647
|
-
process.exit(1);
|
|
648
|
-
}
|
|
649
|
-
} else if (ccTokens.hasProjects) {
|
|
609
|
+
} else {
|
|
650
610
|
try {
|
|
651
611
|
ensureClaudeCodeCliIfNeeded(ccTokens.raw);
|
|
652
612
|
} catch {
|
|
653
613
|
printLogTail('Runtime', runtimeLogPath);
|
|
654
614
|
process.exit(1);
|
|
655
615
|
}
|
|
656
|
-
shouldStartRuntime = true;
|
|
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) {
|
|
664
616
|
console.log('[openHermit] Starting bundled runtime service...');
|
|
665
617
|
console.log(`[openHermit] Runtime config: ${ccConnectConfigPath}`);
|
|
666
618
|
ccConnectProcess = spawn(process.execPath, [resolveCcConnectRunner(), '-config', ccConnectConfigPath], {
|
|
@@ -748,7 +700,6 @@ const serverProcess = spawn(process.execPath, ['--import', resolveAliasLoaderReg
|
|
|
748
700
|
HOST: process.env.HOST || '127.0.0.1',
|
|
749
701
|
NODE_ENV: 'production',
|
|
750
702
|
HERMIT_HOME: hermitHome,
|
|
751
|
-
HERMIT_RUNTIME_SETUP_MODE: runtimeSetupMode ? '1' : '0',
|
|
752
703
|
CC_CONNECT_TOKEN: ccTokens.managementToken,
|
|
753
704
|
CC_CONNECT_MANAGEMENT_TOKEN: ccTokens.managementToken,
|
|
754
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
|
}
|