dahrk-node 0.1.7 → 0.1.9
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/dist/main.js +2036 -458
- package/package.json +2 -1
package/dist/main.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/main.ts
|
|
4
|
-
import {
|
|
4
|
+
import { execFileSync as execFileSync8 } from "child_process";
|
|
5
|
+
import { existsSync as existsSync12, realpathSync as realpathSync4 } from "fs";
|
|
5
6
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
6
|
-
import { homedir as
|
|
7
|
-
import { basename, join as
|
|
7
|
+
import { homedir as homedir5, platform as osPlatform4 } from "os";
|
|
8
|
+
import { basename as basename2, join as join14 } from "path";
|
|
8
9
|
import { pathToFileURL } from "url";
|
|
9
10
|
|
|
10
11
|
// ../../packages/edge/src/ws-client.ts
|
|
@@ -1332,7 +1333,7 @@ var PROVIDER_BY_ENV = {
|
|
|
1332
1333
|
import { execFileSync } from "child_process";
|
|
1333
1334
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "fs";
|
|
1334
1335
|
import { homedir, tmpdir } from "os";
|
|
1335
|
-
import { dirname, isAbsolute, join as join2 } from "path";
|
|
1336
|
+
import { basename, dirname, isAbsolute, join as join2 } from "path";
|
|
1336
1337
|
var noopLogger = { info: () => {
|
|
1337
1338
|
}, warn: () => {
|
|
1338
1339
|
} };
|
|
@@ -1348,12 +1349,16 @@ function sanitizeBranchName(name) {
|
|
|
1348
1349
|
function resolveWorktreesDir(override) {
|
|
1349
1350
|
return override ?? process.env.DAHRK_WORKTREES_DIR ?? process.env.SKAKEL_WORKTREES_DIR ?? join2(homedir(), ".dahrk", "worktrees");
|
|
1350
1351
|
}
|
|
1352
|
+
function resolveMirrorsDir(override) {
|
|
1353
|
+
return override ?? process.env.DAHRK_MIRRORS_DIR ?? process.env.SKAKEL_MIRRORS_DIR ?? join2(homedir(), ".dahrk", "mirrors");
|
|
1354
|
+
}
|
|
1351
1355
|
function createGitService(opts = {}) {
|
|
1352
1356
|
const worktreesDir = resolveWorktreesDir(opts.worktreesDir);
|
|
1353
|
-
const mirrorsDir = opts.mirrorsDir
|
|
1357
|
+
const mirrorsDir = resolveMirrorsDir(opts.mirrorsDir);
|
|
1354
1358
|
const authorName = opts.authorName ?? process.env.DAHRK_GIT_AUTHOR_NAME ?? "Dahrk";
|
|
1355
1359
|
const authorEmail = opts.authorEmail ?? process.env.DAHRK_GIT_AUTHOR_EMAIL ?? "noreply@dahrk.ai";
|
|
1356
|
-
const
|
|
1360
|
+
const isBusy = opts.isBusy;
|
|
1361
|
+
const log = opts.logger ?? noopLogger;
|
|
1357
1362
|
const git = (cwd, args, env) => execFileSync("git", args, {
|
|
1358
1363
|
cwd,
|
|
1359
1364
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -1365,7 +1370,7 @@ function createGitService(opts = {}) {
|
|
|
1365
1370
|
encoding: "utf-8",
|
|
1366
1371
|
...env ? { env } : {}
|
|
1367
1372
|
});
|
|
1368
|
-
const
|
|
1373
|
+
const gitOk2 = (cwd, args) => {
|
|
1369
1374
|
try {
|
|
1370
1375
|
git(cwd, args);
|
|
1371
1376
|
return true;
|
|
@@ -1392,14 +1397,14 @@ function createGitService(opts = {}) {
|
|
|
1392
1397
|
writeFileSync(excludePath, `${existing}${sep3}${entry}
|
|
1393
1398
|
`);
|
|
1394
1399
|
} catch (e) {
|
|
1395
|
-
|
|
1400
|
+
log.warn(`could not set worktree scratch exclude at ${worktreePath}: ${e.message}`);
|
|
1396
1401
|
}
|
|
1397
1402
|
};
|
|
1398
1403
|
const commitPending = (worktreePath, message) => {
|
|
1399
1404
|
excludeScratchLocally(worktreePath);
|
|
1400
1405
|
git(worktreePath, ["rm", "-r", "--cached", "--ignore-unmatch", "--quiet", SCRATCH_DIR]);
|
|
1401
1406
|
git(worktreePath, ["add", "-A", "--", "."]);
|
|
1402
|
-
const dirty = !
|
|
1407
|
+
const dirty = !gitOk2(worktreePath, ["diff", "--cached", "--quiet"]);
|
|
1403
1408
|
if (dirty) {
|
|
1404
1409
|
git(worktreePath, [
|
|
1405
1410
|
"-c",
|
|
@@ -1425,22 +1430,108 @@ function createGitService(opts = {}) {
|
|
|
1425
1430
|
const netEnv = (authEnv) => authEnv ?? { ...process.env, GIT_TERMINAL_PROMPT: "0" };
|
|
1426
1431
|
const withTokenUser = (gitUrl) => /^https:\/\/[^@/]+@/.test(gitUrl) ? gitUrl : gitUrl.replace(/^https:\/\//, "https://x-access-token@");
|
|
1427
1432
|
const mirrorPathFor = (repoId) => join2(mirrorsDir, sanitizeBranchName(repoId));
|
|
1433
|
+
const listWorktrees = (mirror) => {
|
|
1434
|
+
let out;
|
|
1435
|
+
try {
|
|
1436
|
+
out = git(mirror, ["worktree", "list", "--porcelain"]);
|
|
1437
|
+
} catch {
|
|
1438
|
+
return [];
|
|
1439
|
+
}
|
|
1440
|
+
const entries = [];
|
|
1441
|
+
let path = "";
|
|
1442
|
+
for (const line of out.split("\n")) {
|
|
1443
|
+
if (line.startsWith("worktree ")) path = line.slice(9).trim();
|
|
1444
|
+
else if (line.startsWith("branch ")) {
|
|
1445
|
+
const branch = line.slice(7).trim().replace(/^refs\/heads\//, "");
|
|
1446
|
+
if (path) entries.push({ path, branch });
|
|
1447
|
+
} else if (line.trim() === "") path = "";
|
|
1448
|
+
}
|
|
1449
|
+
return entries;
|
|
1450
|
+
};
|
|
1451
|
+
const teardownWorktreePath = (mirror, worktreePath) => {
|
|
1452
|
+
try {
|
|
1453
|
+
git(mirror, ["worktree", "remove", "--force", worktreePath]);
|
|
1454
|
+
} catch (e) {
|
|
1455
|
+
log.warn(`git worktree remove failed for ${worktreePath}: ${e.message}`);
|
|
1456
|
+
}
|
|
1457
|
+
rmSync(worktreePath, { recursive: true, force: true });
|
|
1458
|
+
gitOk2(mirror, ["worktree", "prune"]);
|
|
1459
|
+
};
|
|
1460
|
+
const TRACKING_REFSPEC = "+refs/heads/*:refs/remotes/origin/*";
|
|
1461
|
+
const migrateMirrorConfig = (mirror, repoId) => {
|
|
1462
|
+
const isMirror = gitOk2(mirror, ["config", "--get", "remote.origin.mirror"]);
|
|
1463
|
+
const refspecs = (() => {
|
|
1464
|
+
try {
|
|
1465
|
+
return git(mirror, ["config", "--get-all", "remote.origin.fetch"]).split("\n").map((s) => s.trim());
|
|
1466
|
+
} catch {
|
|
1467
|
+
return [];
|
|
1468
|
+
}
|
|
1469
|
+
})();
|
|
1470
|
+
if (!isMirror && refspecs.length === 1 && refspecs[0] === TRACKING_REFSPEC) return;
|
|
1471
|
+
log.info(`mirror ${repoId}: migrating to the tracking-refspec layout (was mirror=${isMirror})`);
|
|
1472
|
+
for (const args of [
|
|
1473
|
+
["config", "--unset-all", "remote.origin.mirror"],
|
|
1474
|
+
["config", "--unset-all", "remote.origin.push"]
|
|
1475
|
+
]) {
|
|
1476
|
+
gitOk2(mirror, args);
|
|
1477
|
+
}
|
|
1478
|
+
git(mirror, ["config", "--replace-all", "remote.origin.fetch", TRACKING_REFSPEC]);
|
|
1479
|
+
};
|
|
1480
|
+
const salvageOrphanedTip = (mirror, branchName, start2) => {
|
|
1481
|
+
try {
|
|
1482
|
+
if (!gitOk2(mirror, ["rev-parse", "--verify", "-q", `refs/heads/${branchName}`])) return;
|
|
1483
|
+
if (gitOk2(mirror, ["merge-base", "--is-ancestor", `refs/heads/${branchName}`, start2])) return;
|
|
1484
|
+
const sha = git(mirror, ["rev-parse", `refs/heads/${branchName}`]).trim();
|
|
1485
|
+
const ref = `refs/dahrk/salvage/${branchName}/${sha.slice(0, 12)}`;
|
|
1486
|
+
git(mirror, ["update-ref", ref, sha]);
|
|
1487
|
+
log.warn(`parked orphaned tip of ${branchName} (${sha.slice(0, 8)}) at ${ref} before reset`);
|
|
1488
|
+
} catch (e) {
|
|
1489
|
+
log.warn(`could not salvage tip of ${branchName}: ${e.message}`);
|
|
1490
|
+
}
|
|
1491
|
+
};
|
|
1492
|
+
const gcShadowHeads = (mirror) => {
|
|
1493
|
+
let heads;
|
|
1494
|
+
try {
|
|
1495
|
+
heads = git(mirror, ["for-each-ref", "--format=%(refname:short)", "refs/heads/"]).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
1496
|
+
} catch {
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
const held = new Set(listWorktrees(mirror).map((w) => w.branch));
|
|
1500
|
+
const ownHead = (() => {
|
|
1501
|
+
try {
|
|
1502
|
+
return git(mirror, ["symbolic-ref", "--quiet", "--short", "HEAD"]).trim();
|
|
1503
|
+
} catch {
|
|
1504
|
+
return "";
|
|
1505
|
+
}
|
|
1506
|
+
})();
|
|
1507
|
+
for (const h of heads) {
|
|
1508
|
+
if (h === ownHead || held.has(h)) continue;
|
|
1509
|
+
if (!gitOk2(mirror, ["rev-parse", "--verify", "-q", `refs/remotes/origin/${h}`])) continue;
|
|
1510
|
+
if (!gitOk2(mirror, ["merge-base", "--is-ancestor", `refs/heads/${h}`, `refs/remotes/origin/${h}`])) continue;
|
|
1511
|
+
gitOk2(mirror, ["branch", "-D", h]);
|
|
1512
|
+
}
|
|
1513
|
+
};
|
|
1428
1514
|
const ensureMirror = (repoId, gitUrl, authEnv) => {
|
|
1429
1515
|
const mirror = mirrorPathFor(repoId);
|
|
1430
|
-
if (existsSync(mirror) &&
|
|
1431
|
-
|
|
1516
|
+
if (existsSync(mirror) && gitOk2(mirror, ["rev-parse", "--git-dir"])) {
|
|
1517
|
+
log.info(`refreshing mirror ${repoId}`);
|
|
1432
1518
|
try {
|
|
1433
|
-
|
|
1519
|
+
migrateMirrorConfig(mirror, repoId);
|
|
1520
|
+
git(mirror, ["fetch", "--prune", "origin"], netEnv(authEnv));
|
|
1521
|
+
gcShadowHeads(mirror);
|
|
1434
1522
|
return { mirror, refreshed: true };
|
|
1435
1523
|
} catch (e) {
|
|
1436
|
-
|
|
1524
|
+
log.warn(`mirror fetch failed for ${repoId}: ${e.message}`);
|
|
1437
1525
|
return { mirror, refreshed: false };
|
|
1438
1526
|
}
|
|
1439
1527
|
}
|
|
1440
|
-
mkdirSync(
|
|
1528
|
+
mkdirSync(mirror, { recursive: true });
|
|
1441
1529
|
const cloneUrl = authEnv ? withTokenUser(gitUrl) : gitUrl;
|
|
1442
|
-
|
|
1443
|
-
gitBare(["
|
|
1530
|
+
log.info(`cloning mirror ${repoId} from ${gitUrl}`);
|
|
1531
|
+
gitBare(["init", "--bare", "--quiet", mirror]);
|
|
1532
|
+
git(mirror, ["remote", "add", "origin", cloneUrl]);
|
|
1533
|
+
git(mirror, ["config", "--replace-all", "remote.origin.fetch", TRACKING_REFSPEC]);
|
|
1534
|
+
git(mirror, ["fetch", "--prune", "origin"], netEnv(authEnv));
|
|
1444
1535
|
return { mirror, refreshed: true };
|
|
1445
1536
|
};
|
|
1446
1537
|
const refFor = (spec, worktreePath) => ({
|
|
@@ -1458,28 +1549,42 @@ function createGitService(opts = {}) {
|
|
|
1458
1549
|
const branchName = sanitizeBranchName(spec.branch ?? `dahrk/${runId}`);
|
|
1459
1550
|
const worktreePath = join2(worktreesDir, runId);
|
|
1460
1551
|
mkdirSync(worktreesDir, { recursive: true });
|
|
1461
|
-
if (existsSync(worktreePath) &&
|
|
1462
|
-
|
|
1552
|
+
if (existsSync(worktreePath) && gitOk2(worktreePath, ["rev-parse", "--git-dir"])) {
|
|
1553
|
+
log.info(`reusing existing worktree at ${worktreePath}`);
|
|
1463
1554
|
mkdirSync(join2(worktreePath, ".skakel", "scratch"), { recursive: true });
|
|
1464
1555
|
return refFor(spec, worktreePath);
|
|
1465
1556
|
}
|
|
1466
1557
|
const auth = spec.credentialToken ? setupAuth(spec.credentialToken) : void 0;
|
|
1467
1558
|
try {
|
|
1468
1559
|
const { mirror, refreshed } = ensureMirror(repoId, gitUrl, auth?.env);
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1560
|
+
gitOk2(mirror, ["worktree", "prune"]);
|
|
1561
|
+
for (const w of listWorktrees(mirror)) {
|
|
1562
|
+
if (w.branch !== branchName || w.path === worktreePath) continue;
|
|
1563
|
+
const holder = basename(w.path);
|
|
1564
|
+
if (isBusy?.(holder)) {
|
|
1565
|
+
throw new Error(`branch ${branchName} is held by in-flight run ${holder} (${w.path})`);
|
|
1475
1566
|
}
|
|
1476
|
-
|
|
1477
|
-
|
|
1567
|
+
log.warn(`clearing stale worktree ${w.path} which still claims ${branchName}`);
|
|
1568
|
+
teardownWorktreePath(mirror, w.path);
|
|
1569
|
+
}
|
|
1570
|
+
const remoteBranch = `refs/remotes/origin/${branchName}`;
|
|
1571
|
+
const remoteBase = `refs/remotes/origin/${baseBranch}`;
|
|
1572
|
+
if (!refreshed) {
|
|
1573
|
+
log.info(`mirror refresh failed; fetching base ${baseBranch} before branching ${branchName}`);
|
|
1574
|
+
git(mirror, ["fetch", "origin", `+refs/heads/${baseBranch}:${remoteBase}`], netEnv(auth?.env));
|
|
1575
|
+
}
|
|
1576
|
+
const seed = spec.seedRef && gitOk2(mirror, ["rev-parse", "--verify", "-q", spec.seedRef]) ? spec.seedRef : void 0;
|
|
1577
|
+
const start2 = seed ?? (gitOk2(mirror, ["rev-parse", "--verify", "-q", remoteBranch]) ? remoteBranch : remoteBase);
|
|
1578
|
+
if (!gitOk2(mirror, ["rev-parse", "--verify", "-q", start2])) {
|
|
1579
|
+
throw new Error(`start point '${start2}' does not resolve in mirror ${repoId}`);
|
|
1478
1580
|
}
|
|
1581
|
+
salvageOrphanedTip(mirror, branchName, start2);
|
|
1582
|
+
log.info(`creating worktree at ${worktreePath} from ${start2} on ${branchName}`);
|
|
1583
|
+
git(mirror, ["worktree", "add", "--force", "-B", branchName, worktreePath, start2]);
|
|
1479
1584
|
} finally {
|
|
1480
1585
|
auth?.cleanup();
|
|
1481
1586
|
}
|
|
1482
|
-
if (!
|
|
1587
|
+
if (!gitOk2(worktreePath, ["rev-parse", "--verify", "-q", "HEAD"])) {
|
|
1483
1588
|
throw new Error(`base '${baseBranch}' did not materialise into ${worktreePath} (unborn HEAD)`);
|
|
1484
1589
|
}
|
|
1485
1590
|
mkdirSync(join2(worktreePath, ".skakel", "scratch"), { recursive: true });
|
|
@@ -1487,14 +1592,14 @@ function createGitService(opts = {}) {
|
|
|
1487
1592
|
},
|
|
1488
1593
|
async commitAndPush(ref, opts2) {
|
|
1489
1594
|
const { worktreePath } = ref;
|
|
1490
|
-
if (!existsSync(worktreePath) || !
|
|
1595
|
+
if (!existsSync(worktreePath) || !gitOk2(worktreePath, ["rev-parse", "--git-dir"])) {
|
|
1491
1596
|
throw new Error(`worktree missing for push: ${worktreePath}`);
|
|
1492
1597
|
}
|
|
1493
1598
|
const branch = sanitizeBranchName(opts2.branch);
|
|
1494
1599
|
const { headSha: committedSha, dirty } = commitPending(worktreePath, opts2.message);
|
|
1495
1600
|
let headSha = committedSha;
|
|
1496
1601
|
let commitsAhead = 0;
|
|
1497
|
-
for (const baseRef of [
|
|
1602
|
+
for (const baseRef of ["FETCH_HEAD", `origin/${opts2.base}`, opts2.base, `refs/heads/${opts2.base}`]) {
|
|
1498
1603
|
if (!baseRef) continue;
|
|
1499
1604
|
try {
|
|
1500
1605
|
commitsAhead = Number.parseInt(git(worktreePath, ["rev-list", "--count", `${baseRef}..HEAD`]).trim(), 10) || 0;
|
|
@@ -1513,15 +1618,15 @@ function createGitService(opts = {}) {
|
|
|
1513
1618
|
git(worktreePath, ["fetch", remote, opts2.base], netEnv(auth?.env));
|
|
1514
1619
|
fetched = true;
|
|
1515
1620
|
} catch (e) {
|
|
1516
|
-
|
|
1621
|
+
log.warn(`base fetch failed for ${opts2.base}; skipping push-time integration: ${e.message}`);
|
|
1517
1622
|
}
|
|
1518
1623
|
}
|
|
1519
1624
|
if (fetched) {
|
|
1520
|
-
if (!
|
|
1625
|
+
if (!gitOk2(worktreePath, ["merge-base", "HEAD", "FETCH_HEAD"])) {
|
|
1521
1626
|
return { headSha, pushed: false, nothingToCommit: !dirty, commitsAhead, integration: "diverged" };
|
|
1522
1627
|
}
|
|
1523
1628
|
const delta = git(worktreePath, ["diff", "--name-only", "FETCH_HEAD...HEAD"]).split("\n").map((l) => l.trim()).filter(Boolean);
|
|
1524
|
-
const isScratchPath = (p) => p === SCRATCH_DIR || p.startsWith(`${SCRATCH_DIR}/`) ||
|
|
1629
|
+
const isScratchPath = (p) => p === SCRATCH_DIR || p.startsWith(`${SCRATCH_DIR}/`) || gitOk2(worktreePath, ["check-ignore", "-q", "--no-index", "--", p]);
|
|
1525
1630
|
if (!delta.some((p) => !isScratchPath(p))) {
|
|
1526
1631
|
return { headSha, pushed: false, nothingToCommit: true, commitsAhead, integration: "noop" };
|
|
1527
1632
|
}
|
|
@@ -1538,7 +1643,7 @@ function createGitService(opts = {}) {
|
|
|
1538
1643
|
integration = "clean";
|
|
1539
1644
|
headSha = git(worktreePath, ["rev-parse", "HEAD"]).trim();
|
|
1540
1645
|
} catch (mergeErr) {
|
|
1541
|
-
const inMerge =
|
|
1646
|
+
const inMerge = gitOk2(worktreePath, ["rev-parse", "--verify", "-q", "MERGE_HEAD"]);
|
|
1542
1647
|
if (inMerge) {
|
|
1543
1648
|
const conflictFiles = git(worktreePath, ["diff", "--name-only", "--diff-filter=U"]).split("\n").map((l) => l.trim()).filter(Boolean);
|
|
1544
1649
|
git(worktreePath, ["merge", "--abort"]);
|
|
@@ -1560,7 +1665,7 @@ function createGitService(opts = {}) {
|
|
|
1560
1665
|
},
|
|
1561
1666
|
async backupPush(ref, opts2) {
|
|
1562
1667
|
const { worktreePath } = ref;
|
|
1563
|
-
if (!existsSync(worktreePath) || !
|
|
1668
|
+
if (!existsSync(worktreePath) || !gitOk2(worktreePath, ["rev-parse", "--git-dir"])) {
|
|
1564
1669
|
throw new Error(`worktree missing for backup push: ${worktreePath}`);
|
|
1565
1670
|
}
|
|
1566
1671
|
const wipRef = sanitizeBranchName(opts2.branch);
|
|
@@ -1617,18 +1722,145 @@ function createGitService(opts = {}) {
|
|
|
1617
1722
|
}
|
|
1618
1723
|
},
|
|
1619
1724
|
async teardownWorktree(ref) {
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1725
|
+
teardownWorktreePath(mirrorPathFor(ref.repoId), ref.worktreePath);
|
|
1726
|
+
}
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
// ../../packages/executor-worktree/src/worktree-reaper.ts
|
|
1731
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
1732
|
+
import { existsSync as existsSync2, readdirSync, realpathSync, rmSync as rmSync2, statSync } from "fs";
|
|
1733
|
+
import { join as join3 } from "path";
|
|
1734
|
+
var MINUTE = 6e4;
|
|
1735
|
+
var HOUR = 60 * MINUTE;
|
|
1736
|
+
var DEFAULTS = {
|
|
1737
|
+
// Deliberately non-optional defaults. "No policy configured" used to mean "never collect anything",
|
|
1738
|
+
// which is precisely how the disk reached 65 GB. Absent config must mean sane collection, not none.
|
|
1739
|
+
maxRuns: 20,
|
|
1740
|
+
maxIdleMs: 6 * HOUR,
|
|
1741
|
+
activityGraceMs: 30 * MINUTE
|
|
1742
|
+
};
|
|
1743
|
+
var noop = { info: () => {
|
|
1744
|
+
}, warn: () => {
|
|
1745
|
+
} };
|
|
1746
|
+
var gitOk = (cwd, args) => {
|
|
1747
|
+
try {
|
|
1748
|
+
execFileSync2("git", args, { cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
1749
|
+
return true;
|
|
1750
|
+
} catch {
|
|
1751
|
+
return false;
|
|
1752
|
+
}
|
|
1753
|
+
};
|
|
1754
|
+
var gitOut = (cwd, args) => execFileSync2("git", args, { cwd, stdio: ["pipe", "pipe", "pipe"], encoding: "utf-8" });
|
|
1755
|
+
var canonical = (p) => {
|
|
1756
|
+
try {
|
|
1757
|
+
return realpathSync(p);
|
|
1758
|
+
} catch {
|
|
1759
|
+
return p;
|
|
1760
|
+
}
|
|
1761
|
+
};
|
|
1762
|
+
function lastUsedMs(worktreePath) {
|
|
1763
|
+
const candidates = [join3(worktreePath, ".skakel", "scratch", "state.json"), worktreePath];
|
|
1764
|
+
let newest = 0;
|
|
1765
|
+
for (const p of candidates) {
|
|
1766
|
+
try {
|
|
1767
|
+
newest = Math.max(newest, statSync(p).mtimeMs);
|
|
1768
|
+
} catch {
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
return newest;
|
|
1772
|
+
}
|
|
1773
|
+
function registeredWorktrees(mirror) {
|
|
1774
|
+
let out;
|
|
1775
|
+
try {
|
|
1776
|
+
out = gitOut(mirror, ["worktree", "list", "--porcelain"]);
|
|
1777
|
+
} catch {
|
|
1778
|
+
return [];
|
|
1779
|
+
}
|
|
1780
|
+
return out.split("\n").filter((l) => l.startsWith("worktree ")).map((l) => canonical(l.slice(9).trim())).filter((p) => p && p !== canonical(mirror));
|
|
1781
|
+
}
|
|
1782
|
+
function createWorktreeReaper(opts) {
|
|
1783
|
+
const log = opts.logger ?? noop;
|
|
1784
|
+
const mirrors = () => {
|
|
1785
|
+
try {
|
|
1786
|
+
return readdirSync(opts.mirrorsDir).map((d) => join3(opts.mirrorsDir, d)).filter((m) => gitOk(m, ["rev-parse", "--git-dir"]));
|
|
1787
|
+
} catch {
|
|
1788
|
+
return [];
|
|
1789
|
+
}
|
|
1790
|
+
};
|
|
1791
|
+
const ownerOf = (worktreePath, all) => {
|
|
1792
|
+
for (const [mirror, paths] of all) if (paths.includes(worktreePath)) return mirror;
|
|
1793
|
+
return void 0;
|
|
1794
|
+
};
|
|
1795
|
+
return {
|
|
1796
|
+
async reap(policy = {}) {
|
|
1797
|
+
const maxRuns = policy.maxRuns ?? DEFAULTS.maxRuns;
|
|
1798
|
+
const maxIdleMs = policy.maxIdleMs ?? DEFAULTS.maxIdleMs;
|
|
1799
|
+
const graceMs = policy.activityGraceMs ?? DEFAULTS.activityGraceMs;
|
|
1800
|
+
const dryRun = policy.dryRun ?? false;
|
|
1801
|
+
const report = { scanned: 0, reaped: [], skipped: 0, errors: [] };
|
|
1802
|
+
const now = Date.now();
|
|
1803
|
+
const registered = /* @__PURE__ */ new Map();
|
|
1804
|
+
for (const m of mirrors()) {
|
|
1805
|
+
gitOk(m, ["worktree", "prune"]);
|
|
1806
|
+
registered.set(m, registeredWorktrees(m));
|
|
1626
1807
|
}
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1808
|
+
const onDisk = (() => {
|
|
1809
|
+
try {
|
|
1810
|
+
return readdirSync(opts.worktreesDir).map((d) => canonical(join3(opts.worktreesDir, d)));
|
|
1811
|
+
} catch {
|
|
1812
|
+
return [];
|
|
1813
|
+
}
|
|
1814
|
+
})();
|
|
1815
|
+
const candidates = [.../* @__PURE__ */ new Set([...onDisk, ...[...registered.values()].flat()])];
|
|
1816
|
+
const entries = [];
|
|
1817
|
+
for (const path of candidates) {
|
|
1818
|
+
report.scanned++;
|
|
1819
|
+
const runId = path.split("/").pop() ?? path;
|
|
1820
|
+
if (opts.isBusy?.(runId)) {
|
|
1821
|
+
report.skipped++;
|
|
1822
|
+
continue;
|
|
1823
|
+
}
|
|
1824
|
+
const idleMs = now - lastUsedMs(path);
|
|
1825
|
+
if (idleMs < graceMs) {
|
|
1826
|
+
report.skipped++;
|
|
1827
|
+
continue;
|
|
1828
|
+
}
|
|
1829
|
+
const broken = !existsSync2(path) || !gitOk(path, ["rev-parse", "--verify", "-q", "HEAD"]);
|
|
1830
|
+
entries.push({ path, runId, idleMs, broken, ...ownerOf(path, registered) ? { mirror: ownerOf(path, registered) } : {} });
|
|
1831
|
+
}
|
|
1832
|
+
entries.sort((a, b) => b.idleMs - a.idleMs);
|
|
1833
|
+
const keep = entries.filter((e) => !e.broken && e.idleMs <= maxIdleMs);
|
|
1834
|
+
const doomed = [];
|
|
1835
|
+
for (const e of entries) {
|
|
1836
|
+
if (e.broken) doomed.push({ ...e, reason: "broken" });
|
|
1837
|
+
else if (e.idleMs > maxIdleMs) doomed.push({ ...e, reason: "idle" });
|
|
1838
|
+
}
|
|
1839
|
+
const survivors = keep.filter((e) => !doomed.some((d) => d.path === e.path));
|
|
1840
|
+
for (const e of survivors.slice(0, Math.max(0, survivors.length - maxRuns))) {
|
|
1841
|
+
doomed.push({ ...e, reason: "over-count" });
|
|
1842
|
+
}
|
|
1843
|
+
for (const d of doomed) {
|
|
1844
|
+
if (dryRun) {
|
|
1845
|
+
log.info(`reaper (dry-run): would reap ${d.runId} (${d.reason}, idle ${Math.round(d.idleMs / MINUTE)}m)`);
|
|
1846
|
+
report.reaped.push({ runId: d.runId, path: d.path, reason: d.reason });
|
|
1847
|
+
continue;
|
|
1848
|
+
}
|
|
1849
|
+
try {
|
|
1850
|
+
if (d.mirror) {
|
|
1851
|
+
gitOk(d.mirror, ["worktree", "remove", "--force", d.path]);
|
|
1852
|
+
}
|
|
1853
|
+
rmSync2(d.path, { recursive: true, force: true });
|
|
1854
|
+
if (d.mirror) gitOk(d.mirror, ["worktree", "prune"]);
|
|
1855
|
+
report.reaped.push({ runId: d.runId, path: d.path, reason: d.reason });
|
|
1856
|
+
} catch (e) {
|
|
1857
|
+
report.errors.push(`${d.runId}: ${e.message}`);
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
if (report.reaped.length) {
|
|
1861
|
+
log.info(`reaper: reaped ${report.reaped.length} worktree(s), skipped ${report.skipped}`);
|
|
1631
1862
|
}
|
|
1863
|
+
return report;
|
|
1632
1864
|
}
|
|
1633
1865
|
};
|
|
1634
1866
|
}
|
|
@@ -1636,15 +1868,15 @@ function createGitService(opts = {}) {
|
|
|
1636
1868
|
// ../../packages/executor-worktree/src/trace-writer.ts
|
|
1637
1869
|
import { createHash } from "crypto";
|
|
1638
1870
|
import { appendFileSync, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1639
|
-
import { join as
|
|
1871
|
+
import { join as join4 } from "path";
|
|
1640
1872
|
var DEFAULT_SPILL_BYTES = 8192;
|
|
1641
1873
|
function createTraceWriter(scratchPath, meta, opts = {}) {
|
|
1642
1874
|
const spillBytes = opts.spillBytes ?? DEFAULT_SPILL_BYTES;
|
|
1643
|
-
const dir =
|
|
1644
|
-
mkdirSync2(
|
|
1645
|
-
mkdirSync2(
|
|
1646
|
-
const tracePath =
|
|
1647
|
-
const metaPath =
|
|
1875
|
+
const dir = join4(scratchPath, "traces", meta.stageId, `attempt-${meta.attempt}`);
|
|
1876
|
+
mkdirSync2(join4(dir, "blobs"), { recursive: true });
|
|
1877
|
+
mkdirSync2(join4(dir, "raw"), { recursive: true });
|
|
1878
|
+
const tracePath = join4(dir, "trace.jsonl");
|
|
1879
|
+
const metaPath = join4(dir, "meta.json");
|
|
1648
1880
|
let current = { ...meta };
|
|
1649
1881
|
let nextSeq = 0;
|
|
1650
1882
|
let rawCount = 0;
|
|
@@ -1656,8 +1888,8 @@ function createTraceWriter(scratchPath, meta, opts = {}) {
|
|
|
1656
1888
|
const spillValue = (value) => {
|
|
1657
1889
|
const data = typeof value === "string" ? value : JSON.stringify(value);
|
|
1658
1890
|
const sha = createHash("sha256").update(data).digest("hex");
|
|
1659
|
-
writeFileSync2(
|
|
1660
|
-
return
|
|
1891
|
+
writeFileSync2(join4(dir, "blobs", sha), data);
|
|
1892
|
+
return join4("blobs", sha);
|
|
1661
1893
|
};
|
|
1662
1894
|
const spill = (event) => {
|
|
1663
1895
|
if (event.type === "thought" && event.text !== void 0 && tooBig(event.text)) {
|
|
@@ -1687,8 +1919,8 @@ function createTraceWriter(scratchPath, meta, opts = {}) {
|
|
|
1687
1919
|
return written;
|
|
1688
1920
|
},
|
|
1689
1921
|
writeRaw(record) {
|
|
1690
|
-
const rel =
|
|
1691
|
-
writeFileSync2(
|
|
1922
|
+
const rel = join4("raw", `${rawCount++}.json`);
|
|
1923
|
+
writeFileSync2(join4(dir, rel), JSON.stringify(record, null, 2));
|
|
1692
1924
|
return rel;
|
|
1693
1925
|
},
|
|
1694
1926
|
finalise(patch = {}) {
|
|
@@ -1703,13 +1935,13 @@ function createTraceWriter(scratchPath, meta, opts = {}) {
|
|
|
1703
1935
|
|
|
1704
1936
|
// ../../packages/executor-worktree/src/pack-cache.ts
|
|
1705
1937
|
import { createHash as createHash2 } from "crypto";
|
|
1706
|
-
import { cpSync, existsSync as
|
|
1707
|
-
import { dirname as dirname2, join as
|
|
1938
|
+
import { cpSync, existsSync as existsSync3, mkdirSync as mkdirSync3, mkdtempSync as mkdtempSync2, readdirSync as readdirSync2, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
1939
|
+
import { dirname as dirname2, join as join5, relative, sep } from "path";
|
|
1708
1940
|
function readManifestFiles(dir) {
|
|
1709
1941
|
const out = [];
|
|
1710
1942
|
const walk = (cur) => {
|
|
1711
|
-
for (const entry of
|
|
1712
|
-
const abs =
|
|
1943
|
+
for (const entry of readdirSync2(cur, { withFileTypes: true })) {
|
|
1944
|
+
const abs = join5(cur, entry.name);
|
|
1713
1945
|
if (entry.isDirectory()) walk(abs);
|
|
1714
1946
|
else out.push(relative(dir, abs).split(sep).join("/"));
|
|
1715
1947
|
}
|
|
@@ -1719,8 +1951,8 @@ function readManifestFiles(dir) {
|
|
|
1719
1951
|
}
|
|
1720
1952
|
|
|
1721
1953
|
// ../../packages/executor-worktree/src/overlay.ts
|
|
1722
|
-
import { existsSync as
|
|
1723
|
-
import { dirname as dirname3, join as
|
|
1954
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
1955
|
+
import { dirname as dirname3, join as join6 } from "path";
|
|
1724
1956
|
function sameBytes(dest, bytes) {
|
|
1725
1957
|
try {
|
|
1726
1958
|
return readFileSync3(dest).equals(bytes);
|
|
@@ -1740,10 +1972,10 @@ async function overlayComponents(opts) {
|
|
|
1740
1972
|
}
|
|
1741
1973
|
const { dir } = await cache.materialise(ref);
|
|
1742
1974
|
for (const relPath of readManifestFiles(dir)) {
|
|
1743
|
-
const src =
|
|
1744
|
-
const dest =
|
|
1975
|
+
const src = join6(dir, relPath);
|
|
1976
|
+
const dest = join6(worktreePath, relPath);
|
|
1745
1977
|
const bytes = readFileSync3(src);
|
|
1746
|
-
if (
|
|
1978
|
+
if (existsSync4(dest)) {
|
|
1747
1979
|
if (sameBytes(dest, bytes)) continue;
|
|
1748
1980
|
result.skippedRepoLocal.push(relPath);
|
|
1749
1981
|
continue;
|
|
@@ -1772,6 +2004,230 @@ function makeRunner(runtime) {
|
|
|
1772
2004
|
return runtime === "codex" ? createCodexRunner() : createClaudeRunner();
|
|
1773
2005
|
}
|
|
1774
2006
|
|
|
2007
|
+
// ../../packages/edge/src/logger.ts
|
|
2008
|
+
import { closeSync, existsSync as existsSync5, mkdirSync as mkdirSync5, openSync, renameSync, rmSync as rmSync4, statSync as statSync2, writeSync } from "fs";
|
|
2009
|
+
import { join as join7 } from "path";
|
|
2010
|
+
import pino from "pino";
|
|
2011
|
+
|
|
2012
|
+
// ../../packages/edge/src/redact.ts
|
|
2013
|
+
var SENSITIVE_KEY_PATTERNS = [
|
|
2014
|
+
"token",
|
|
2015
|
+
"secret",
|
|
2016
|
+
"password",
|
|
2017
|
+
"passwd",
|
|
2018
|
+
"apikey",
|
|
2019
|
+
"api_key",
|
|
2020
|
+
"authorization",
|
|
2021
|
+
"auth_header",
|
|
2022
|
+
"cookie",
|
|
2023
|
+
"private_key",
|
|
2024
|
+
"privatekey",
|
|
2025
|
+
"client_secret",
|
|
2026
|
+
"clientsecret",
|
|
2027
|
+
"refresh_token",
|
|
2028
|
+
"access_token",
|
|
2029
|
+
"bearer",
|
|
2030
|
+
"credential",
|
|
2031
|
+
"enroltoken",
|
|
2032
|
+
"enrol_token"
|
|
2033
|
+
];
|
|
2034
|
+
var REDACTED = "[REDACTED]";
|
|
2035
|
+
var MAX_DEPTH = 8;
|
|
2036
|
+
var KEY_ALLOWLIST = /* @__PURE__ */ new Set(["credentialmode"]);
|
|
2037
|
+
function isSensitiveKey(key) {
|
|
2038
|
+
const lower = key.toLowerCase();
|
|
2039
|
+
if (KEY_ALLOWLIST.has(lower)) return false;
|
|
2040
|
+
return SENSITIVE_KEY_PATTERNS.some((p) => lower.includes(p));
|
|
2041
|
+
}
|
|
2042
|
+
var INLINE_TOKEN_RE = /\b(github_pat_[A-Za-z0-9_]{20,}|gh[pousr]_[A-Za-z0-9]{20,}|glpat-[A-Za-z0-9_-]{16,}|xox[abprs]-[A-Za-z0-9-]{10,}|lin_(?:api|oauth)_[A-Za-z0-9]{20,}|sk-ant-[A-Za-z0-9_-]{20,}|sk-[A-Za-z0-9]{20,}|AKIA[A-Z0-9]{16})\b/g;
|
|
2043
|
+
var URL_CREDENTIAL_RE = /(\b[a-z][a-z0-9+.-]*:\/\/)([^/\s:@]+):([^/\s@]+)@/gi;
|
|
2044
|
+
var JWT_RE = /\b[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g;
|
|
2045
|
+
function scrubString(s) {
|
|
2046
|
+
return s.replace(URL_CREDENTIAL_RE, `$1$2:${REDACTED}@`).replace(INLINE_TOKEN_RE, REDACTED).replace(JWT_RE, REDACTED).replace(/Bearer\s+[A-Za-z0-9._\-+/=]{16,}/gi, `Bearer ${REDACTED}`).replace(/([?&](?:token|access_token|api_key|key|secret)=)[^&\s"']+/gi, `$1${REDACTED}`);
|
|
2047
|
+
}
|
|
2048
|
+
function looksLikeWholeToken(s) {
|
|
2049
|
+
if (s.length < 20 || /\s/.test(s)) return false;
|
|
2050
|
+
return /^(gh[pousr]|github_pat)_/i.test(s) || /^xox[abprs]-/i.test(s) || /^glpat-/i.test(s) || /^lin_(api|oauth)_/i.test(s) || /^sk-/i.test(s);
|
|
2051
|
+
}
|
|
2052
|
+
function scrubValue(value, depth = 0) {
|
|
2053
|
+
if (depth > MAX_DEPTH) return REDACTED;
|
|
2054
|
+
if (value === null || value === void 0) return value;
|
|
2055
|
+
if (typeof value === "string") {
|
|
2056
|
+
return looksLikeWholeToken(value) ? REDACTED : scrubString(value);
|
|
2057
|
+
}
|
|
2058
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return value;
|
|
2059
|
+
if (value instanceof Error) {
|
|
2060
|
+
const out = new Error(scrubString(value.message));
|
|
2061
|
+
out.name = value.name;
|
|
2062
|
+
if (value.stack) out.stack = scrubString(value.stack);
|
|
2063
|
+
return out;
|
|
2064
|
+
}
|
|
2065
|
+
if (Array.isArray(value)) return value.map((v) => scrubValue(v, depth + 1));
|
|
2066
|
+
if (typeof value === "object") {
|
|
2067
|
+
const out = {};
|
|
2068
|
+
for (const [k, v] of Object.entries(value)) {
|
|
2069
|
+
out[k] = isSensitiveKey(k) ? REDACTED : scrubValue(v, depth + 1);
|
|
2070
|
+
}
|
|
2071
|
+
return out;
|
|
2072
|
+
}
|
|
2073
|
+
return value;
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
// ../../packages/edge/src/logger.ts
|
|
2077
|
+
var LEVELS = ["trace", "debug", "info", "warn", "error", "fatal", "silent"];
|
|
2078
|
+
var isLevel = (v) => v !== void 0 && LEVELS.includes(v);
|
|
2079
|
+
function levelFromEnv(env) {
|
|
2080
|
+
const v = env.DAHRK_LOG_LEVEL?.toLowerCase();
|
|
2081
|
+
return isLevel(v) ? v : "info";
|
|
2082
|
+
}
|
|
2083
|
+
function fileLevelFromEnv(env) {
|
|
2084
|
+
const off = env.DAHRK_LOG_FILE?.toLowerCase();
|
|
2085
|
+
if (off === "0" || off === "off" || off === "false") return "off";
|
|
2086
|
+
const v = env.DAHRK_LOG_FILE_LEVEL?.toLowerCase();
|
|
2087
|
+
return isLevel(v) ? v : "debug";
|
|
2088
|
+
}
|
|
2089
|
+
var RotatingFile = class {
|
|
2090
|
+
constructor(path, maxBytes, maxFiles) {
|
|
2091
|
+
this.path = path;
|
|
2092
|
+
this.maxBytes = maxBytes;
|
|
2093
|
+
this.maxFiles = maxFiles;
|
|
2094
|
+
this.open();
|
|
2095
|
+
}
|
|
2096
|
+
path;
|
|
2097
|
+
maxBytes;
|
|
2098
|
+
maxFiles;
|
|
2099
|
+
fd;
|
|
2100
|
+
size = 0;
|
|
2101
|
+
failed = false;
|
|
2102
|
+
open() {
|
|
2103
|
+
try {
|
|
2104
|
+
this.fd = openSync(this.path, "a");
|
|
2105
|
+
this.size = existsSync5(this.path) ? statSync2(this.path).size : 0;
|
|
2106
|
+
} catch (e) {
|
|
2107
|
+
this.disable(e);
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
/** Report once, then go quiet. A logger that spams stderr about being unable to log is worse than one
|
|
2111
|
+
* that silently degrades. */
|
|
2112
|
+
disable(e) {
|
|
2113
|
+
if (!this.failed) {
|
|
2114
|
+
this.failed = true;
|
|
2115
|
+
process.stderr.write(`dahrk: file logging disabled (${e.message})
|
|
2116
|
+
`);
|
|
2117
|
+
}
|
|
2118
|
+
if (this.fd !== void 0) {
|
|
2119
|
+
try {
|
|
2120
|
+
closeSync(this.fd);
|
|
2121
|
+
} catch {
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
this.fd = void 0;
|
|
2125
|
+
}
|
|
2126
|
+
/** node.jsonl -> node.jsonl.1, .1 -> .2, ... dropping the oldest. */
|
|
2127
|
+
rotate() {
|
|
2128
|
+
if (this.fd === void 0) return;
|
|
2129
|
+
try {
|
|
2130
|
+
closeSync(this.fd);
|
|
2131
|
+
this.fd = void 0;
|
|
2132
|
+
const oldest = `${this.path}.${this.maxFiles}`;
|
|
2133
|
+
if (existsSync5(oldest)) rmSync4(oldest, { force: true });
|
|
2134
|
+
for (let i = this.maxFiles - 1; i >= 1; i--) {
|
|
2135
|
+
const from = `${this.path}.${i}`;
|
|
2136
|
+
if (existsSync5(from)) renameSync(from, `${this.path}.${i + 1}`);
|
|
2137
|
+
}
|
|
2138
|
+
if (existsSync5(this.path)) renameSync(this.path, `${this.path}.1`);
|
|
2139
|
+
this.open();
|
|
2140
|
+
} catch (e) {
|
|
2141
|
+
this.disable(e);
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
write(line) {
|
|
2145
|
+
if (this.failed) return;
|
|
2146
|
+
if (this.fd === void 0) return;
|
|
2147
|
+
try {
|
|
2148
|
+
const buf = Buffer.from(line, "utf8");
|
|
2149
|
+
if (this.size > 0 && this.size + buf.length > this.maxBytes) {
|
|
2150
|
+
this.rotate();
|
|
2151
|
+
if (this.fd === void 0) return;
|
|
2152
|
+
}
|
|
2153
|
+
writeSync(this.fd, buf);
|
|
2154
|
+
this.size += buf.length;
|
|
2155
|
+
} catch (e) {
|
|
2156
|
+
this.disable(e);
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
};
|
|
2160
|
+
var humanSink = {
|
|
2161
|
+
write(chunk) {
|
|
2162
|
+
let msg;
|
|
2163
|
+
try {
|
|
2164
|
+
msg = JSON.parse(chunk).msg ?? "";
|
|
2165
|
+
} catch {
|
|
2166
|
+
msg = chunk.trimEnd();
|
|
2167
|
+
}
|
|
2168
|
+
if (!msg) return;
|
|
2169
|
+
try {
|
|
2170
|
+
process.stdout.write(`${msg}
|
|
2171
|
+
`);
|
|
2172
|
+
} catch {
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
};
|
|
2176
|
+
var pipeGuardInstalled = false;
|
|
2177
|
+
function guardBrokenPipe() {
|
|
2178
|
+
if (pipeGuardInstalled) return;
|
|
2179
|
+
pipeGuardInstalled = true;
|
|
2180
|
+
const ignoreEpipe = (err) => {
|
|
2181
|
+
if (err.code !== "EPIPE") throw err;
|
|
2182
|
+
};
|
|
2183
|
+
process.stdout.on("error", ignoreEpipe);
|
|
2184
|
+
process.stderr.on("error", ignoreEpipe);
|
|
2185
|
+
}
|
|
2186
|
+
function lowest(a, b) {
|
|
2187
|
+
const rank = (l) => LEVELS.indexOf(l);
|
|
2188
|
+
return rank(a) <= rank(b) ? a : b;
|
|
2189
|
+
}
|
|
2190
|
+
function createNodeLogger(opts = {}) {
|
|
2191
|
+
guardBrokenPipe();
|
|
2192
|
+
const level = opts.level ?? "info";
|
|
2193
|
+
const fileLevel = opts.fileLevel ?? "debug";
|
|
2194
|
+
const streams = [];
|
|
2195
|
+
if (level !== "silent") streams.push({ level, stream: humanSink });
|
|
2196
|
+
if (opts.dir && fileLevel !== "silent") {
|
|
2197
|
+
try {
|
|
2198
|
+
mkdirSync5(opts.dir, { recursive: true, mode: 448 });
|
|
2199
|
+
const file = new RotatingFile(join7(opts.dir, "node.jsonl"), opts.maxBytes ?? 10 * 1024 * 1024, opts.maxFiles ?? 5);
|
|
2200
|
+
streams.push({ level: fileLevel, stream: file });
|
|
2201
|
+
} catch (e) {
|
|
2202
|
+
process.stderr.write(`dahrk: file logging disabled (${e.message})
|
|
2203
|
+
`);
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
const base = streams.length === 0 ? "silent" : opts.dir ? lowest(level, fileLevel) : level;
|
|
2207
|
+
return pino(
|
|
2208
|
+
{
|
|
2209
|
+
level: base,
|
|
2210
|
+
base: opts.base ?? {},
|
|
2211
|
+
// ISO time reads better than epoch millis in a support bundle a human is scanning.
|
|
2212
|
+
timestamp: pino.stdTimeFunctions.isoTime,
|
|
2213
|
+
hooks: {
|
|
2214
|
+
logMethod(args, method) {
|
|
2215
|
+
method.apply(this, scrubValue(args));
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
},
|
|
2219
|
+
pino.multistream(streams)
|
|
2220
|
+
);
|
|
2221
|
+
}
|
|
2222
|
+
function createNodeLoggerFromEnv(env, dir, base) {
|
|
2223
|
+
const fileLevel = fileLevelFromEnv(env);
|
|
2224
|
+
return createNodeLogger({
|
|
2225
|
+
level: levelFromEnv(env),
|
|
2226
|
+
...fileLevel === "off" ? {} : { dir, fileLevel },
|
|
2227
|
+
...base ? { base } : {}
|
|
2228
|
+
});
|
|
2229
|
+
}
|
|
2230
|
+
|
|
1775
2231
|
// ../../packages/edge/src/policy.ts
|
|
1776
2232
|
var ALLOW = { verdict: "allow", policy: "none" };
|
|
1777
2233
|
function evaluatePolicies(event, rules) {
|
|
@@ -1794,15 +2250,15 @@ function denyToolRule(tool3) {
|
|
|
1794
2250
|
}
|
|
1795
2251
|
|
|
1796
2252
|
// ../../packages/edge/src/stage-runner.ts
|
|
1797
|
-
import { execFileSync as
|
|
2253
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
1798
2254
|
import { createHash as createHash3 } from "crypto";
|
|
1799
|
-
import { mkdirSync as
|
|
2255
|
+
import { mkdirSync as mkdirSync6, readdirSync as readdirSync3, readFileSync as readFileSync4, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "fs";
|
|
1800
2256
|
import { tmpdir as tmpdir2 } from "os";
|
|
1801
|
-
import { isAbsolute as isAbsolute2, join as
|
|
2257
|
+
import { isAbsolute as isAbsolute2, join as join8, relative as relative2, resolve, sep as sep2 } from "path";
|
|
1802
2258
|
import { attachedDocBasename as attachedDocBasename2 } from "@dahrk/contracts";
|
|
1803
2259
|
|
|
1804
2260
|
// ../../packages/edge/src/builtins.ts
|
|
1805
|
-
import { execFileSync as
|
|
2261
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
1806
2262
|
var WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
1807
2263
|
"Write",
|
|
1808
2264
|
"Edit",
|
|
@@ -1844,7 +2300,7 @@ function isDangerousRm(cmd) {
|
|
|
1844
2300
|
}
|
|
1845
2301
|
function currentBranch(worktreePath) {
|
|
1846
2302
|
try {
|
|
1847
|
-
return
|
|
2303
|
+
return execFileSync3("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: worktreePath }).toString().trim();
|
|
1848
2304
|
} catch {
|
|
1849
2305
|
return "";
|
|
1850
2306
|
}
|
|
@@ -2019,7 +2475,7 @@ var attemptOf = (jobId) => {
|
|
|
2019
2475
|
};
|
|
2020
2476
|
var digest = (value) => `sha256:${createHash3("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 16)}`;
|
|
2021
2477
|
function writeScratchState(ref, job, attempt, status) {
|
|
2022
|
-
const statePath =
|
|
2478
|
+
const statePath = join8(ref.scratchPath, "state.json");
|
|
2023
2479
|
let state;
|
|
2024
2480
|
try {
|
|
2025
2481
|
state = JSON.parse(readFileSync4(statePath, "utf8"));
|
|
@@ -2027,24 +2483,24 @@ function writeScratchState(ref, job, attempt, status) {
|
|
|
2027
2483
|
state = { runId: job.runId, tenantId: job.tenantId, stages: {} };
|
|
2028
2484
|
}
|
|
2029
2485
|
state.stages[job.stageId] = { currentAttempt: attempt, status };
|
|
2030
|
-
|
|
2486
|
+
mkdirSync6(ref.scratchPath, { recursive: true });
|
|
2031
2487
|
writeFileSync5(statePath, JSON.stringify(state, null, 2));
|
|
2032
2488
|
}
|
|
2033
2489
|
function writeIssueContext(ref, issueContext) {
|
|
2034
2490
|
if (issueContext === void 0) return;
|
|
2035
2491
|
try {
|
|
2036
|
-
|
|
2037
|
-
writeFileSync5(
|
|
2492
|
+
mkdirSync6(ref.scratchPath, { recursive: true });
|
|
2493
|
+
writeFileSync5(join8(ref.scratchPath, "issue.md"), issueContext);
|
|
2038
2494
|
} catch {
|
|
2039
2495
|
}
|
|
2040
2496
|
}
|
|
2041
2497
|
function writeAttachedDocuments(ref, docs) {
|
|
2042
2498
|
if (!docs || docs.length === 0) return;
|
|
2043
2499
|
try {
|
|
2044
|
-
const dir =
|
|
2045
|
-
|
|
2500
|
+
const dir = join8(ref.scratchPath, "docs");
|
|
2501
|
+
mkdirSync6(dir, { recursive: true });
|
|
2046
2502
|
for (const doc of docs) {
|
|
2047
|
-
writeFileSync5(
|
|
2503
|
+
writeFileSync5(join8(dir, `${attachedDocBasename2(doc)}.md`), doc.content);
|
|
2048
2504
|
}
|
|
2049
2505
|
} catch {
|
|
2050
2506
|
}
|
|
@@ -2062,8 +2518,8 @@ ${lines.join("\n")}
|
|
|
2062
2518
|
function writeGuidance(ref, guidance) {
|
|
2063
2519
|
if (!guidance || guidance.length === 0) return;
|
|
2064
2520
|
try {
|
|
2065
|
-
|
|
2066
|
-
writeFileSync5(
|
|
2521
|
+
mkdirSync6(ref.scratchPath, { recursive: true });
|
|
2522
|
+
writeFileSync5(join8(ref.scratchPath, "guidance.md"), renderGuidanceMarkdown(guidance));
|
|
2067
2523
|
} catch {
|
|
2068
2524
|
}
|
|
2069
2525
|
}
|
|
@@ -2097,13 +2553,13 @@ function readEmittedArtifact(ref, relPath) {
|
|
|
2097
2553
|
}
|
|
2098
2554
|
function scanScratchOutput(ref, preferRel) {
|
|
2099
2555
|
try {
|
|
2100
|
-
const names =
|
|
2556
|
+
const names = readdirSync3(join8(ref.worktreePath, SCRATCH_OUTPUT_DIR)).filter(
|
|
2101
2557
|
(n) => n.toLowerCase().endsWith(".md")
|
|
2102
2558
|
);
|
|
2103
2559
|
if (names.length === 0) return void 0;
|
|
2104
2560
|
const preferBase = preferRel?.split("/").pop();
|
|
2105
2561
|
const pick = preferBase && names.includes(preferBase) ? preferBase : names[0];
|
|
2106
|
-
const raw = readFileSync4(
|
|
2562
|
+
const raw = readFileSync4(join8(ref.worktreePath, SCRATCH_OUTPUT_DIR, pick), "utf8");
|
|
2107
2563
|
if (raw.trim().length === 0) return void 0;
|
|
2108
2564
|
return { path: `${SCRATCH_OUTPUT_DIR}/${pick}`, content: capContent(raw) };
|
|
2109
2565
|
} catch {
|
|
@@ -2113,7 +2569,7 @@ function scanScratchOutput(ref, preferRel) {
|
|
|
2113
2569
|
function scanChangedMarkdown(ref) {
|
|
2114
2570
|
const git = (args) => {
|
|
2115
2571
|
try {
|
|
2116
|
-
return
|
|
2572
|
+
return execFileSync4("git", ["-C", ref.worktreePath, ...args], { encoding: "utf8" }).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
2117
2573
|
} catch {
|
|
2118
2574
|
return [];
|
|
2119
2575
|
}
|
|
@@ -2123,7 +2579,7 @@ function scanChangedMarkdown(ref) {
|
|
|
2123
2579
|
);
|
|
2124
2580
|
for (const rel of rels) {
|
|
2125
2581
|
try {
|
|
2126
|
-
const raw = readFileSync4(
|
|
2582
|
+
const raw = readFileSync4(join8(ref.worktreePath, rel), "utf8");
|
|
2127
2583
|
if (raw.trim().length > 0) return { path: rel, content: capContent(raw) };
|
|
2128
2584
|
} catch {
|
|
2129
2585
|
}
|
|
@@ -2146,22 +2602,34 @@ function resolveStageArtifact(ref, emitArtifact, handedBack) {
|
|
|
2146
2602
|
}
|
|
2147
2603
|
function createStageRunner(deps) {
|
|
2148
2604
|
const worktrees = /* @__PURE__ */ new Map();
|
|
2605
|
+
const log = deps.logger ?? createNodeLogger({ level: "silent" });
|
|
2149
2606
|
const active = /* @__PURE__ */ new Map();
|
|
2150
2607
|
const turnQueues = /* @__PURE__ */ new Map();
|
|
2151
2608
|
const runToolCalls = /* @__PURE__ */ new Map();
|
|
2152
2609
|
const lastUsed = /* @__PURE__ */ new Map();
|
|
2153
2610
|
const inFlight = /* @__PURE__ */ new Map();
|
|
2611
|
+
const isBusy = (runId) => (inFlight.get(runId) ?? 0) > 0;
|
|
2612
|
+
const reaperDryRun = process.env.DAHRK_REAPER_DRY_RUN === "1";
|
|
2613
|
+
const reaper = createWorktreeReaper({
|
|
2614
|
+
worktreesDir: deps.gitService.worktreesDir,
|
|
2615
|
+
mirrorsDir: resolveMirrorsDir(),
|
|
2616
|
+
isBusy,
|
|
2617
|
+
logger: { info: (m) => console.log(`REAPER ${m}`), warn: (m) => console.warn(`REAPER ${m}`) }
|
|
2618
|
+
});
|
|
2154
2619
|
const scratchOnly = /* @__PURE__ */ new Set();
|
|
2155
2620
|
const teardownRun = async (runId) => {
|
|
2156
2621
|
const ref = worktrees.get(runId);
|
|
2157
2622
|
if (ref) {
|
|
2158
2623
|
if (scratchOnly.has(runId)) {
|
|
2159
2624
|
try {
|
|
2160
|
-
|
|
2161
|
-
} catch {
|
|
2625
|
+
rmSync5(ref.worktreePath, { recursive: true, force: true });
|
|
2626
|
+
} catch (e) {
|
|
2627
|
+
log.warn({ err: e, runId, path: ref.worktreePath }, "teardown: could not remove scratch dir");
|
|
2162
2628
|
}
|
|
2163
2629
|
} else {
|
|
2164
|
-
await deps.gitService.teardownWorktree(ref).catch(() =>
|
|
2630
|
+
await deps.gitService.teardownWorktree(ref).catch((e) => {
|
|
2631
|
+
log.warn({ err: e, runId, path: ref.worktreePath }, "teardown: could not remove worktree");
|
|
2632
|
+
});
|
|
2165
2633
|
}
|
|
2166
2634
|
}
|
|
2167
2635
|
worktrees.delete(runId);
|
|
@@ -2171,6 +2639,11 @@ function createStageRunner(deps) {
|
|
|
2171
2639
|
};
|
|
2172
2640
|
const applyRetention = async (keepRunId) => {
|
|
2173
2641
|
const policy = deps.retention;
|
|
2642
|
+
await reaper.reap({
|
|
2643
|
+
...policy?.maxRuns !== void 0 ? { maxRuns: policy.maxRuns } : {},
|
|
2644
|
+
...policy?.maxAgeMs !== void 0 ? { maxIdleMs: policy.maxAgeMs } : {},
|
|
2645
|
+
...reaperDryRun ? { dryRun: true } : {}
|
|
2646
|
+
}).catch(() => void 0);
|
|
2174
2647
|
if (!policy) return;
|
|
2175
2648
|
const prunable = (id) => id !== keepRunId && (inFlight.get(id) ?? 0) === 0;
|
|
2176
2649
|
const now = Date.now();
|
|
@@ -2190,6 +2663,10 @@ function createStageRunner(deps) {
|
|
|
2190
2663
|
}
|
|
2191
2664
|
};
|
|
2192
2665
|
return {
|
|
2666
|
+
isBusy,
|
|
2667
|
+
async reapWorktrees() {
|
|
2668
|
+
return reaper.reap(reaperDryRun ? { dryRun: true } : {});
|
|
2669
|
+
},
|
|
2193
2670
|
async runJob(job) {
|
|
2194
2671
|
const { stageId, jobId, runId, agentConfig } = job;
|
|
2195
2672
|
const attempt = attemptOf(jobId);
|
|
@@ -2205,299 +2682,307 @@ function createStageRunner(deps) {
|
|
|
2205
2682
|
return { jobId, status: "fail", summary: `edge does not serve repo "${job.workspaceRef.repoId}"` };
|
|
2206
2683
|
}
|
|
2207
2684
|
inFlight.set(runId, (inFlight.get(runId) ?? 0) + 1);
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
if (
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2685
|
+
try {
|
|
2686
|
+
lastUsed.set(runId, Date.now());
|
|
2687
|
+
let ref = worktrees.get(runId);
|
|
2688
|
+
if (!ref) {
|
|
2689
|
+
if (job.workspaceRef) {
|
|
2690
|
+
ref = await deps.gitService.createWorktree({
|
|
2691
|
+
repoId: job.workspaceRef.repoId,
|
|
2692
|
+
gitUrl: job.workspaceRef.gitUrl,
|
|
2693
|
+
baseBranch: job.workspaceRef.baseBranch,
|
|
2694
|
+
runId,
|
|
2695
|
+
repo: job.workspaceRef.repo,
|
|
2696
|
+
// Stable per-issue branch (continuation); absent = legacy dahrk/<runId>.
|
|
2697
|
+
...job.workspaceRef.branch ? { branch: job.workspaceRef.branch } : {},
|
|
2698
|
+
// Re-enter from work a prior backup push preserved, rather than starting over from the base
|
|
2699
|
+
// (DHK-264). Carried on the contract but never plumbed through to the git service until now.
|
|
2700
|
+
...job.workspaceRef.seedRef ? { seedRef: job.workspaceRef.seedRef } : {},
|
|
2701
|
+
// Brokered git credential for this job; absent on ambient nodes (host creds used).
|
|
2702
|
+
...job.workspaceRef.credentialToken ? { credentialToken: job.workspaceRef.credentialToken } : {}
|
|
2703
|
+
});
|
|
2704
|
+
} else {
|
|
2705
|
+
const base = deps.scratchRoot ?? join8(tmpdir2(), "dahrk", "scratch");
|
|
2706
|
+
const worktreePath = join8(base, runId);
|
|
2707
|
+
const scratchPath = join8(worktreePath, ".skakel", "scratch");
|
|
2708
|
+
mkdirSync6(scratchPath, { recursive: true });
|
|
2709
|
+
ref = { repoId: "", gitUrl: "", repo: "", baseBranch: "", worktreePath, scratchPath };
|
|
2710
|
+
scratchOnly.add(runId);
|
|
2711
|
+
}
|
|
2712
|
+
worktrees.set(runId, ref);
|
|
2230
2713
|
}
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2714
|
+
writeScratchState(ref, job, attempt, "in-flight");
|
|
2715
|
+
writeIssueContext(ref, job.issueContext);
|
|
2716
|
+
writeGuidance(ref, job.guidance);
|
|
2717
|
+
writeAttachedDocuments(ref, job.attachedDocuments);
|
|
2718
|
+
if (job.agentConfig.emitArtifact) {
|
|
2719
|
+
const artifactDir = resolveWorktreeRelativePath(ref, job.agentConfig.emitArtifact);
|
|
2720
|
+
const slash = job.agentConfig.emitArtifact.lastIndexOf("/");
|
|
2721
|
+
if (artifactDir && slash > 0) {
|
|
2722
|
+
try {
|
|
2723
|
+
mkdirSync6(join8(ref.worktreePath, job.agentConfig.emitArtifact.slice(0, slash)), { recursive: true });
|
|
2724
|
+
} catch {
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
let gateway;
|
|
2729
|
+
const meta = {
|
|
2730
|
+
tenantId: job.tenantId,
|
|
2731
|
+
runId,
|
|
2732
|
+
stageId,
|
|
2733
|
+
jobId,
|
|
2734
|
+
attempt,
|
|
2735
|
+
runtime: agentConfig.runtime,
|
|
2736
|
+
model: agentConfig.model,
|
|
2737
|
+
sessionId: job.sessionId,
|
|
2738
|
+
configDigest: digest(agentConfig),
|
|
2739
|
+
startedAt: nowIso2()
|
|
2740
|
+
};
|
|
2741
|
+
const writer = createTraceWriter(ref.scratchPath, meta);
|
|
2742
|
+
const streamEvent = (e) => deps.trace?.event({ runId, stageId, attempt, tenantId: job.tenantId, event: e });
|
|
2743
|
+
streamEvent(
|
|
2744
|
+
writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: agentConfig.runtime, event: "attempt-start" })
|
|
2745
|
+
);
|
|
2746
|
+
const shipFinalTrace = async (finalMeta) => {
|
|
2747
|
+
const sink = deps.trace;
|
|
2748
|
+
if (!sink) return;
|
|
2749
|
+
const base = { tenantId: job.tenantId, runId, stageId, attempt };
|
|
2241
2750
|
try {
|
|
2242
|
-
|
|
2751
|
+
for (const name of readdirSync3(join8(writer.dir, "blobs"))) {
|
|
2752
|
+
const bytes = readFileSync4(join8(writer.dir, "blobs", name));
|
|
2753
|
+
const { url } = await sink.requestBlobUrl({
|
|
2754
|
+
...base,
|
|
2755
|
+
sha256: name,
|
|
2756
|
+
size: bytes.length,
|
|
2757
|
+
contentType: "application/octet-stream",
|
|
2758
|
+
slot: "blob"
|
|
2759
|
+
});
|
|
2760
|
+
if (url) await putBytes(url, bytes, "application/octet-stream");
|
|
2761
|
+
}
|
|
2243
2762
|
} catch {
|
|
2244
2763
|
}
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
runId,
|
|
2251
|
-
stageId,
|
|
2252
|
-
jobId,
|
|
2253
|
-
attempt,
|
|
2254
|
-
runtime: agentConfig.runtime,
|
|
2255
|
-
model: agentConfig.model,
|
|
2256
|
-
sessionId: job.sessionId,
|
|
2257
|
-
configDigest: digest(agentConfig),
|
|
2258
|
-
startedAt: nowIso2()
|
|
2259
|
-
};
|
|
2260
|
-
const writer = createTraceWriter(ref.scratchPath, meta);
|
|
2261
|
-
const streamEvent = (e) => deps.trace?.event({ runId, stageId, attempt, tenantId: job.tenantId, event: e });
|
|
2262
|
-
streamEvent(
|
|
2263
|
-
writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: agentConfig.runtime, event: "attempt-start" })
|
|
2264
|
-
);
|
|
2265
|
-
const shipFinalTrace = async (finalMeta) => {
|
|
2266
|
-
const sink = deps.trace;
|
|
2267
|
-
if (!sink) return;
|
|
2268
|
-
const base = { tenantId: job.tenantId, runId, stageId, attempt };
|
|
2269
|
-
try {
|
|
2270
|
-
for (const name of readdirSync2(join6(writer.dir, "blobs"))) {
|
|
2271
|
-
const bytes = readFileSync4(join6(writer.dir, "blobs", name));
|
|
2272
|
-
const { url } = await sink.requestBlobUrl({
|
|
2764
|
+
let archiveKey;
|
|
2765
|
+
try {
|
|
2766
|
+
const bytes = readFileSync4(join8(writer.dir, "trace.jsonl"));
|
|
2767
|
+
const sha = createHash3("sha256").update(bytes).digest("hex");
|
|
2768
|
+
const { key, url } = await sink.requestBlobUrl({
|
|
2273
2769
|
...base,
|
|
2274
|
-
sha256:
|
|
2770
|
+
sha256: sha,
|
|
2275
2771
|
size: bytes.length,
|
|
2276
|
-
contentType: "application/
|
|
2277
|
-
slot: "
|
|
2772
|
+
contentType: "application/x-ndjson",
|
|
2773
|
+
slot: "archive"
|
|
2278
2774
|
});
|
|
2279
|
-
if (url) await putBytes(url, bytes, "application/
|
|
2775
|
+
if (url) await putBytes(url, bytes, "application/x-ndjson");
|
|
2776
|
+
archiveKey = key;
|
|
2777
|
+
} catch {
|
|
2280
2778
|
}
|
|
2281
|
-
|
|
2282
|
-
}
|
|
2283
|
-
let archiveKey;
|
|
2284
|
-
try {
|
|
2285
|
-
const bytes = readFileSync4(join6(writer.dir, "trace.jsonl"));
|
|
2286
|
-
const sha = createHash3("sha256").update(bytes).digest("hex");
|
|
2287
|
-
const { key, url } = await sink.requestBlobUrl({
|
|
2288
|
-
...base,
|
|
2289
|
-
sha256: sha,
|
|
2290
|
-
size: bytes.length,
|
|
2291
|
-
contentType: "application/x-ndjson",
|
|
2292
|
-
slot: "archive"
|
|
2293
|
-
});
|
|
2294
|
-
if (url) await putBytes(url, bytes, "application/x-ndjson");
|
|
2295
|
-
archiveKey = key;
|
|
2296
|
-
} catch {
|
|
2297
|
-
}
|
|
2298
|
-
sink.finalised({ ...base, meta: finalMeta, eventCount: writer.count(), ...archiveKey ? { archiveKey } : {} });
|
|
2299
|
-
};
|
|
2300
|
-
const finish = async (status2, summary2, sessionId, costUsd, handedBackDoc) => {
|
|
2301
|
-
active.delete(jobId);
|
|
2302
|
-
turnQueues.delete(jobId);
|
|
2303
|
-
await gateway?.stop().catch(() => void 0);
|
|
2304
|
-
gateway = void 0;
|
|
2305
|
-
streamEvent(
|
|
2306
|
-
writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: agentConfig.runtime, event: "stage-exit", status: status2 })
|
|
2307
|
-
);
|
|
2308
|
-
const endedAt = nowIso2();
|
|
2309
|
-
writer.finalise({ status: status2, endedAt, ...sessionId ? { sessionId } : {} });
|
|
2310
|
-
writeScratchState(ref, job, attempt, status2);
|
|
2311
|
-
const finalMeta = {
|
|
2312
|
-
...meta,
|
|
2313
|
-
status: status2,
|
|
2314
|
-
endedAt,
|
|
2315
|
-
...sessionId ? { sessionId } : {},
|
|
2316
|
-
...costUsd !== void 0 ? { costUsd } : {}
|
|
2779
|
+
sink.finalised({ ...base, meta: finalMeta, eventCount: writer.count(), ...archiveKey ? { archiveKey } : {} });
|
|
2317
2780
|
};
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
const resolved = status2 === "ok" && wantsArtifact ? resolveStageArtifact(ref, agentConfig.emitArtifact, handedBackDoc) : void 0;
|
|
2324
|
-
if (status2 === "ok" && wantsArtifact) {
|
|
2325
|
-
const detail = resolved ? `source=${resolved.source} path=${resolved.artifact.path} bytes=${resolved.artifact.content.length}` : "no document resolved (declared path, tool handoff, and scratch/changed-file scans all empty)";
|
|
2781
|
+
const finish = async (status2, summary2, sessionId, costUsd, handedBackDoc) => {
|
|
2782
|
+
active.delete(jobId);
|
|
2783
|
+
turnQueues.delete(jobId);
|
|
2784
|
+
await gateway?.stop().catch((e) => log.warn({ err: e, jobId }, "mcp gateway: stop failed"));
|
|
2785
|
+
gateway = void 0;
|
|
2326
2786
|
streamEvent(
|
|
2327
|
-
writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: agentConfig.runtime, event: "
|
|
2787
|
+
writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: agentConfig.runtime, event: "stage-exit", status: status2 })
|
|
2328
2788
|
);
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
const overlay = await overlayComponents({
|
|
2342
|
-
worktreePath: ref.worktreePath,
|
|
2343
|
-
runtime: agentConfig.runtime,
|
|
2344
|
-
components: job.provision,
|
|
2345
|
-
cache: deps.packCache
|
|
2346
|
-
});
|
|
2347
|
-
const detail = `provision: ${overlay.written.length} written, ${overlay.skippedRepoLocal.length} repo-local, ${overlay.warnings.length} warning(s)`;
|
|
2348
|
-
streamEvent(
|
|
2349
|
-
writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: agentConfig.runtime, event: "provision", detail })
|
|
2789
|
+
const endedAt = nowIso2();
|
|
2790
|
+
writer.finalise({ status: status2, endedAt, ...sessionId ? { sessionId } : {} });
|
|
2791
|
+
writeScratchState(ref, job, attempt, status2);
|
|
2792
|
+
const finalMeta = {
|
|
2793
|
+
...meta,
|
|
2794
|
+
status: status2,
|
|
2795
|
+
endedAt,
|
|
2796
|
+
...sessionId ? { sessionId } : {},
|
|
2797
|
+
...costUsd !== void 0 ? { costUsd } : {}
|
|
2798
|
+
};
|
|
2799
|
+
await shipFinalTrace(finalMeta).catch(
|
|
2800
|
+
(e) => log.error({ err: e, runId, stageId: job.stageId, jobId, attempt }, "trace: failed to ship the final trace to the hub")
|
|
2350
2801
|
);
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
const
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2802
|
+
lastUsed.set(runId, Date.now());
|
|
2803
|
+
await applyRetention(runId).catch((e) => log.warn({ err: e, runId }, "retention: pass failed"));
|
|
2804
|
+
const wantsArtifact = agentConfig.emitArtifact !== void 0 || handedBackDoc !== void 0;
|
|
2805
|
+
const resolved = status2 === "ok" && wantsArtifact ? resolveStageArtifact(ref, agentConfig.emitArtifact, handedBackDoc) : void 0;
|
|
2806
|
+
if (status2 === "ok" && wantsArtifact) {
|
|
2807
|
+
const detail = resolved ? `source=${resolved.source} path=${resolved.artifact.path} bytes=${resolved.artifact.content.length}` : "no document resolved (declared path, tool handoff, and scratch/changed-file scans all empty)";
|
|
2808
|
+
streamEvent(
|
|
2809
|
+
writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: agentConfig.runtime, event: "artifact", detail })
|
|
2810
|
+
);
|
|
2811
|
+
}
|
|
2812
|
+
return {
|
|
2813
|
+
jobId,
|
|
2814
|
+
status: status2,
|
|
2815
|
+
summary: summary2,
|
|
2816
|
+
...sessionId ? { sessionId } : {},
|
|
2817
|
+
...costUsd !== void 0 ? { costUsd } : {},
|
|
2818
|
+
...resolved ? { artifact: resolved.artifact } : {}
|
|
2819
|
+
};
|
|
2820
|
+
};
|
|
2821
|
+
if (deps.packCache && job.provision && job.provision.length > 0) {
|
|
2822
|
+
try {
|
|
2823
|
+
const overlay = await overlayComponents({
|
|
2824
|
+
worktreePath: ref.worktreePath,
|
|
2825
|
+
runtime: agentConfig.runtime,
|
|
2826
|
+
components: job.provision,
|
|
2827
|
+
cache: deps.packCache
|
|
2828
|
+
});
|
|
2829
|
+
const detail = `provision: ${overlay.written.length} written, ${overlay.skippedRepoLocal.length} repo-local, ${overlay.warnings.length} warning(s)`;
|
|
2830
|
+
streamEvent(
|
|
2831
|
+
writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: agentConfig.runtime, event: "provision", detail })
|
|
2832
|
+
);
|
|
2833
|
+
const noteText = overlay.warnings.length > 0 ? `${detail}; ${overlay.warnings.join("; ")}` : detail;
|
|
2834
|
+
deps.sendProgress({ jobId, kind: "observation", ts: nowIso2(), text: noteText });
|
|
2835
|
+
} catch (e) {
|
|
2836
|
+
const msg = `component provisioning failed: ${e.message}`;
|
|
2837
|
+
writer.append({ seq: 0, ts: nowIso2(), type: "error", runtime: agentConfig.runtime, kind: "provision-failed", message: msg });
|
|
2838
|
+
deps.sendProgress({ jobId, kind: "error", ts: nowIso2(), text: msg });
|
|
2839
|
+
return finish("fail", `${stageId}: ${msg}`, job.sessionId);
|
|
2840
|
+
}
|
|
2384
2841
|
}
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
const reason = policyReason(verdict);
|
|
2390
|
-
if (toolUseId) {
|
|
2391
|
-
streamEvent(writer.append({ seq: 0, ts: nowIso2(), type: "observation", runtime, toolUseId, isError: true, output: { error: reason } }));
|
|
2842
|
+
let counter = runToolCalls.get(runId);
|
|
2843
|
+
if (!counter) {
|
|
2844
|
+
counter = { count: 0 };
|
|
2845
|
+
runToolCalls.set(runId, counter);
|
|
2392
2846
|
}
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2847
|
+
const jobRules = buildRules(job.policies ?? [], {
|
|
2848
|
+
worktreePath: ref.worktreePath,
|
|
2849
|
+
repoName: job.workspaceRef?.repo ?? "",
|
|
2850
|
+
runToolCalls: counter
|
|
2851
|
+
});
|
|
2852
|
+
const rules = [...jobRules, ...deps.rules];
|
|
2853
|
+
const entry = evaluatePolicies({ kind: "stage-entry", stageId }, rules);
|
|
2854
|
+
if (entry.verdict === "deny") {
|
|
2855
|
+
writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: agentConfig.runtime, event: "policy-deny", detail: entry.reason });
|
|
2856
|
+
return finish("fail", `${stageId}: denied at stage entry (${entry.policy})`, job.sessionId);
|
|
2402
2857
|
}
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2858
|
+
let denied = false;
|
|
2859
|
+
const authorisedActions = [];
|
|
2860
|
+
const runtime = agentConfig.runtime;
|
|
2861
|
+
const actionKey = (tool3, input) => {
|
|
2862
|
+
try {
|
|
2863
|
+
return `${tool3}\0${JSON.stringify(input)}`;
|
|
2864
|
+
} catch {
|
|
2865
|
+
return `${tool3}\0`;
|
|
2866
|
+
}
|
|
2867
|
+
};
|
|
2868
|
+
const policyReason = (verdict) => verdict.reason ?? `tool action denied by ${verdict.policy}`;
|
|
2869
|
+
const recordDeny = (verdict, toolUseId) => {
|
|
2870
|
+
denied = true;
|
|
2871
|
+
const reason = policyReason(verdict);
|
|
2872
|
+
if (toolUseId) {
|
|
2873
|
+
streamEvent(writer.append({ seq: 0, ts: nowIso2(), type: "observation", runtime, toolUseId, isError: true, output: { error: reason } }));
|
|
2874
|
+
}
|
|
2875
|
+
streamEvent(writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime, event: "policy-deny", detail: reason }));
|
|
2876
|
+
deps.sendProgress({ jobId, kind: "error", ts: nowIso2(), text: reason });
|
|
2877
|
+
};
|
|
2878
|
+
const authorizeToolUse = (tool3, input) => {
|
|
2879
|
+
const verdict = evaluatePolicies({ kind: "action", stageId, tool: tool3, input }, rules);
|
|
2880
|
+
if (verdict.verdict === "deny") {
|
|
2881
|
+
recordDeny(verdict);
|
|
2411
2882
|
} else {
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2883
|
+
authorisedActions.push(actionKey(tool3, input));
|
|
2884
|
+
}
|
|
2885
|
+
return verdict;
|
|
2886
|
+
};
|
|
2887
|
+
const onTrace = (event) => {
|
|
2888
|
+
if (event.type === "action") {
|
|
2889
|
+
const key = actionKey(event.tool, event.input);
|
|
2890
|
+
const authorised = authorisedActions.indexOf(key);
|
|
2891
|
+
if (authorised >= 0) {
|
|
2892
|
+
authorisedActions.splice(authorised, 1);
|
|
2893
|
+
} else {
|
|
2894
|
+
const verdict = evaluatePolicies(
|
|
2895
|
+
{ kind: "action", stageId, tool: event.tool, input: event.input },
|
|
2896
|
+
rules
|
|
2897
|
+
);
|
|
2898
|
+
if (verdict.verdict === "deny") {
|
|
2899
|
+
streamEvent(writer.append(event));
|
|
2900
|
+
recordDeny(verdict, event.toolUseId);
|
|
2901
|
+
return;
|
|
2902
|
+
}
|
|
2420
2903
|
}
|
|
2421
2904
|
}
|
|
2905
|
+
streamEvent(writer.append(event));
|
|
2906
|
+
if (event.type !== "state") deps.sendProgress({ jobId, kind: event.type, ts: event.ts, ...previewOf(event) });
|
|
2907
|
+
};
|
|
2908
|
+
const mcpServers = agentConfig.mcpServers;
|
|
2909
|
+
if (mcpServers && mcpServers.length > 0 && runtime === "claude-code") {
|
|
2910
|
+
gateway = await startMcpGateway({ servers: mcpServers, creds: job.brokeredCreds ?? {} });
|
|
2422
2911
|
}
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
turnQueues.set(jobId, mailbox);
|
|
2473
|
-
result = await runner.runInteractive(ctx, mailbox, onTrace);
|
|
2474
|
-
} else {
|
|
2475
|
-
result = await runner.runBatch(ctx, onTrace);
|
|
2912
|
+
const runner = deps.makeRunner(runtime);
|
|
2913
|
+
active.set(jobId, runner);
|
|
2914
|
+
const ctx = {
|
|
2915
|
+
config: agentConfig,
|
|
2916
|
+
workspace: ref,
|
|
2917
|
+
sessionId: job.sessionId,
|
|
2918
|
+
...job.issueContext !== void 0 ? { issueContext: job.issueContext } : {},
|
|
2919
|
+
...job.guidance !== void 0 ? { guidance: job.guidance } : {},
|
|
2920
|
+
...job.gateFeedback !== void 0 ? { gateFeedback: job.gateFeedback } : {},
|
|
2921
|
+
...job.attachedDocuments !== void 0 ? { attachedDocuments: job.attachedDocuments } : {},
|
|
2922
|
+
...gateway ? { mcpProxyBaseUrl: gateway.baseUrl } : {},
|
|
2923
|
+
// brokered inference env for a managed node (no operator login). The runtime adapter
|
|
2924
|
+
// (Pi) / container executor apply it as the inference process env, so the raw key is
|
|
2925
|
+
// never surfaced to the agent's own tool calls. Absent on ambient nodes; inert for the Claude/
|
|
2926
|
+
// Codex adapters, which use ambient inference.
|
|
2927
|
+
...job.runtimeEnv ? { runtimeEnv: job.runtimeEnv } : {},
|
|
2928
|
+
// The adapter persists each runtime-native record under the attempt's raw/ sidecar
|
|
2929
|
+
// and stamps the rawRef onto the emitted event.
|
|
2930
|
+
writeRaw: writer.writeRaw,
|
|
2931
|
+
authorizeToolUse,
|
|
2932
|
+
// Wire the interactive AskUserQuestion elicitation seam (DHK-344): the adapter calls this when
|
|
2933
|
+
// the agent asks a structured question, and the edge relays it to the hub as an `elicit` frame.
|
|
2934
|
+
...deps.sendElicit ? {
|
|
2935
|
+
emitElicit: (question) => deps.sendElicit({
|
|
2936
|
+
jobId,
|
|
2937
|
+
prompt: question.prompt,
|
|
2938
|
+
options: question.options,
|
|
2939
|
+
...question.multiSelect ? { multiSelect: true } : {}
|
|
2940
|
+
})
|
|
2941
|
+
} : {}
|
|
2942
|
+
};
|
|
2943
|
+
const interactive = agentConfig.interaction === "interactive";
|
|
2944
|
+
let result;
|
|
2945
|
+
let timedOut = false;
|
|
2946
|
+
const killMs = Math.max(0, Math.floor((job.timeout ?? 0) * 1e3));
|
|
2947
|
+
const killTimer = killMs > 0 ? setTimeout(() => {
|
|
2948
|
+
timedOut = true;
|
|
2949
|
+
void runner.cancel();
|
|
2950
|
+
}, killMs) : void 0;
|
|
2951
|
+
try {
|
|
2952
|
+
if (interactive) {
|
|
2953
|
+
const mailbox = new ManagedMailbox();
|
|
2954
|
+
turnQueues.set(jobId, mailbox);
|
|
2955
|
+
result = await runner.runInteractive(ctx, mailbox, onTrace);
|
|
2956
|
+
} else {
|
|
2957
|
+
result = await runner.runBatch(ctx, onTrace);
|
|
2958
|
+
}
|
|
2959
|
+
} finally {
|
|
2960
|
+
if (killTimer) clearTimeout(killTimer);
|
|
2476
2961
|
}
|
|
2477
|
-
|
|
2478
|
-
if (
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
writer.append({ seq: 0, ts: nowIso2(), type: "error", runtime, kind: "hook-failed", message: `hook "${cmd}" failed: ${e.message}` });
|
|
2488
|
-
break;
|
|
2962
|
+
let status = timedOut ? "timeout" : result.status;
|
|
2963
|
+
if (status === "ok" && job.hooks && job.hooks.length > 0) {
|
|
2964
|
+
for (const cmd of job.hooks) {
|
|
2965
|
+
try {
|
|
2966
|
+
execFileSync4("sh", ["-c", cmd], { cwd: ref.worktreePath, stdio: ["pipe", "pipe", "pipe"] });
|
|
2967
|
+
} catch (e) {
|
|
2968
|
+
status = "fail";
|
|
2969
|
+
writer.append({ seq: 0, ts: nowIso2(), type: "error", runtime, kind: "hook-failed", message: `hook "${cmd}" failed: ${e.message}` });
|
|
2970
|
+
break;
|
|
2971
|
+
}
|
|
2489
2972
|
}
|
|
2490
2973
|
}
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2974
|
+
let summary = result.summary ?? `${stageId}: ${status}`;
|
|
2975
|
+
if (!interactive && status === "ok") {
|
|
2976
|
+
try {
|
|
2977
|
+
summary = await runner.summarise(ctx);
|
|
2978
|
+
} catch {
|
|
2979
|
+
}
|
|
2497
2980
|
}
|
|
2981
|
+
if (denied) summary += "\n\n(note: one or more tool actions were blocked by a deny-only policy guard.)";
|
|
2982
|
+
return finish(status, summary, result.sessionId ?? job.sessionId, result.costUsd, result.artifact);
|
|
2983
|
+
} finally {
|
|
2984
|
+
inFlight.set(runId, Math.max(0, (inFlight.get(runId) ?? 1) - 1));
|
|
2498
2985
|
}
|
|
2499
|
-
if (denied) summary += "\n\n(note: one or more tool actions were blocked by a deny-only policy guard.)";
|
|
2500
|
-
return finish(status, summary, result.sessionId ?? job.sessionId, result.costUsd, result.artifact);
|
|
2501
2986
|
},
|
|
2502
2987
|
async runPush(job) {
|
|
2503
2988
|
const { runId, jobId } = job;
|
|
@@ -2640,19 +3125,27 @@ function createStageRunner(deps) {
|
|
|
2640
3125
|
|
|
2641
3126
|
// ../../packages/edge/src/ws-client.ts
|
|
2642
3127
|
var ENROLMENT_REJECTED_EXIT_CODE = 78;
|
|
2643
|
-
var log = (line) => void process.stdout.write(`${line}
|
|
2644
|
-
`);
|
|
2645
3128
|
async function startEdgeNode(opts) {
|
|
3129
|
+
const log = opts.logger ?? createNodeLogger({ level: levelFromEnv(process.env) });
|
|
2646
3130
|
const rules = opts.denyTool ? [denyToolRule(opts.denyTool)] : [];
|
|
3131
|
+
const gitLog = log.child({ component: "git" });
|
|
3132
|
+
const gitLogger = {
|
|
3133
|
+
info: (msg) => gitLog.debug(msg),
|
|
3134
|
+
warn: (msg) => gitLog.warn(msg)
|
|
3135
|
+
};
|
|
3136
|
+
let stageRunnerRef;
|
|
2647
3137
|
const gitService = createGitService({
|
|
2648
3138
|
worktreesDir: opts.worktreesDir,
|
|
2649
|
-
mirrorsDir: opts.mirrorsDir
|
|
3139
|
+
mirrorsDir: opts.mirrorsDir,
|
|
3140
|
+
isBusy: (runId) => stageRunnerRef?.isBusy(runId) ?? false,
|
|
3141
|
+
logger: gitLogger
|
|
2650
3142
|
});
|
|
2651
3143
|
let ws;
|
|
2652
3144
|
let heartbeat;
|
|
2653
3145
|
let reconnectTimer;
|
|
2654
3146
|
let lastPongAt = 0;
|
|
2655
3147
|
let shuttingDown = false;
|
|
3148
|
+
let connectCount = 0;
|
|
2656
3149
|
let onFatal;
|
|
2657
3150
|
const send = (msg) => {
|
|
2658
3151
|
if (ws && ws.readyState === WebSocket.OPEN) ws.send(encode(msg));
|
|
@@ -2662,8 +3155,9 @@ async function startEdgeNode(opts) {
|
|
|
2662
3155
|
if (heartbeat) clearInterval(heartbeat);
|
|
2663
3156
|
lastPongAt = Date.now();
|
|
2664
3157
|
heartbeat = setInterval(() => {
|
|
2665
|
-
|
|
2666
|
-
|
|
3158
|
+
const sincePongMs = Date.now() - lastPongAt;
|
|
3159
|
+
if (sincePongMs > MISSED_PONGS_BEFORE_DEAD * intervalMs) {
|
|
3160
|
+
log.warn({ sincePongMs, intervalMs }, `EDGE_STALE:${sincePongMs}ms without a pong, terminating socket`);
|
|
2667
3161
|
sock.terminate();
|
|
2668
3162
|
return;
|
|
2669
3163
|
}
|
|
@@ -2698,6 +3192,7 @@ async function startEdgeNode(opts) {
|
|
|
2698
3192
|
const stageDeps = {
|
|
2699
3193
|
gitService,
|
|
2700
3194
|
makeRunner,
|
|
3195
|
+
logger: log,
|
|
2701
3196
|
...opts.servesRepoIds ? { servesRepoIds: opts.servesRepoIds } : {},
|
|
2702
3197
|
...opts.tenantId ? { tenantId: opts.tenantId } : {},
|
|
2703
3198
|
rules,
|
|
@@ -2709,6 +3204,16 @@ async function startEdgeNode(opts) {
|
|
|
2709
3204
|
...opts.retention ? { retention: opts.retention } : {}
|
|
2710
3205
|
};
|
|
2711
3206
|
const stageRunner = createStageRunner(stageDeps);
|
|
3207
|
+
stageRunnerRef = stageRunner;
|
|
3208
|
+
void stageRunner.reapWorktrees().then((r) => {
|
|
3209
|
+
if (r.reaped.length) {
|
|
3210
|
+
log.info(
|
|
3211
|
+
{ reaped: r.reaped, scanned: r.scanned, skipped: r.skipped },
|
|
3212
|
+
`EDGE_REAPED:${r.reaped.length} worktrees (scanned ${r.scanned}, skipped ${r.skipped})`
|
|
3213
|
+
);
|
|
3214
|
+
}
|
|
3215
|
+
for (const e of r.errors) log.warn({ reapError: e }, `EDGE_REAP_ERROR:${e}`);
|
|
3216
|
+
}).catch((e) => log.warn({ err: e }, `EDGE_REAP_ERROR:${e.message}`));
|
|
2712
3217
|
const nodeId = opts.nodeId ?? randomUUID();
|
|
2713
3218
|
const running = /* @__PURE__ */ new Set();
|
|
2714
3219
|
const onMessage = async (raw) => {
|
|
@@ -2719,7 +3224,19 @@ async function startEdgeNode(opts) {
|
|
|
2719
3224
|
if (opts.heartbeatMs === void 0 && msg.heartbeatMs > 0 && ws) {
|
|
2720
3225
|
startHeartbeat(ws, msg.heartbeatMs);
|
|
2721
3226
|
}
|
|
2722
|
-
log(
|
|
3227
|
+
log.info(
|
|
3228
|
+
{ name: msg.name, tenantId: msg.tenantId, credentialMode: msg.credentialMode, heartbeatMs: msg.heartbeatMs },
|
|
3229
|
+
`EDGE_WELCOMED:${msg.name} tenant=${msg.tenantId} credentialMode=${msg.credentialMode}`
|
|
3230
|
+
);
|
|
3231
|
+
try {
|
|
3232
|
+
opts.onEnrolled?.({
|
|
3233
|
+
name: msg.name,
|
|
3234
|
+
tenantId: msg.tenantId,
|
|
3235
|
+
credentialMode: msg.credentialMode
|
|
3236
|
+
});
|
|
3237
|
+
} catch (e) {
|
|
3238
|
+
log.warn({ err: e }, `EDGE_ENROL_PERSIST_FAILED ${e.message}`);
|
|
3239
|
+
}
|
|
2723
3240
|
return;
|
|
2724
3241
|
}
|
|
2725
3242
|
if (msg.type === "blob-put-url") {
|
|
@@ -2731,7 +3248,7 @@ async function startEdgeNode(opts) {
|
|
|
2731
3248
|
return;
|
|
2732
3249
|
}
|
|
2733
3250
|
if (msg.type === "cancel") {
|
|
2734
|
-
log(`JOB_CANCEL:${msg.jobId}`);
|
|
3251
|
+
log.info({ jobId: msg.jobId }, `JOB_CANCEL:${msg.jobId}`);
|
|
2735
3252
|
stageRunner.cancel(msg.jobId);
|
|
2736
3253
|
return;
|
|
2737
3254
|
}
|
|
@@ -2739,29 +3256,34 @@ async function startEdgeNode(opts) {
|
|
|
2739
3256
|
if (msg.end === "cancel") stageRunner.cancel(msg.jobId);
|
|
2740
3257
|
else if (msg.end === "complete") stageRunner.endTurns(msg.jobId);
|
|
2741
3258
|
else if (msg.turn) stageRunner.enqueueTurn(msg.jobId, msg.turn);
|
|
2742
|
-
log(`JOB_TURN:${msg.jobId}${msg.end ? `:${msg.end}` : ""}`);
|
|
3259
|
+
log.info({ jobId: msg.jobId, ...msg.end ? { end: msg.end } : {} }, `JOB_TURN:${msg.jobId}${msg.end ? `:${msg.end}` : ""}`);
|
|
2743
3260
|
return;
|
|
2744
3261
|
}
|
|
2745
3262
|
if (msg.type === "push") {
|
|
2746
3263
|
const { job: job2 } = msg;
|
|
3264
|
+
const pushLog = log.child({ runId: job2.runId, jobId: job2.jobId, branch: job2.branch });
|
|
2747
3265
|
if (running.has(job2.jobId)) {
|
|
2748
|
-
|
|
3266
|
+
pushLog.warn({}, `PUSH_DUPLICATE:${job2.runId} ${job2.jobId}`);
|
|
2749
3267
|
return;
|
|
2750
3268
|
}
|
|
2751
3269
|
const cachedPush = lastResults.get(job2.jobId);
|
|
2752
3270
|
if (cachedPush) {
|
|
2753
3271
|
send(cachedPush);
|
|
2754
|
-
|
|
3272
|
+
pushLog.info({}, `PUSH_REPLAY:${job2.runId} ${job2.jobId}`);
|
|
2755
3273
|
return;
|
|
2756
3274
|
}
|
|
2757
3275
|
running.add(job2.jobId);
|
|
2758
|
-
|
|
3276
|
+
const pushStartedAt = Date.now();
|
|
3277
|
+
pushLog.info({}, `PUSH_STARTED:${job2.runId} ${job2.branch}`);
|
|
2759
3278
|
try {
|
|
2760
3279
|
const result = await stageRunner.runPush(job2);
|
|
2761
3280
|
const frame = { type: "push-result", awakeableId: job2.awakeableId, result };
|
|
2762
3281
|
rememberResult(job2.jobId, frame);
|
|
2763
3282
|
send(frame);
|
|
2764
|
-
|
|
3283
|
+
pushLog.info(
|
|
3284
|
+
{ status: result.status, summary: result.summary, durationMs: Date.now() - pushStartedAt },
|
|
3285
|
+
`PUSH_DONE:${job2.runId} ${result.status}${result.status !== "ok" ? ` - ${result.summary}` : ""}`
|
|
3286
|
+
);
|
|
2765
3287
|
} catch (e) {
|
|
2766
3288
|
const frame = {
|
|
2767
3289
|
type: "push-result",
|
|
@@ -2770,7 +3292,7 @@ async function startEdgeNode(opts) {
|
|
|
2770
3292
|
};
|
|
2771
3293
|
rememberResult(job2.jobId, frame);
|
|
2772
3294
|
send(frame);
|
|
2773
|
-
|
|
3295
|
+
pushLog.error({ err: e, durationMs: Date.now() - pushStartedAt }, `PUSH_ERROR:${job2.runId} ${e.message}`);
|
|
2774
3296
|
} finally {
|
|
2775
3297
|
running.delete(job2.jobId);
|
|
2776
3298
|
}
|
|
@@ -2778,24 +3300,36 @@ async function startEdgeNode(opts) {
|
|
|
2778
3300
|
}
|
|
2779
3301
|
if (msg.type !== "job") return;
|
|
2780
3302
|
const { job } = msg;
|
|
3303
|
+
const jobLog = log.child({
|
|
3304
|
+
runId: job.runId,
|
|
3305
|
+
stageId: job.stageId,
|
|
3306
|
+
jobId: job.jobId,
|
|
3307
|
+
...job.tenantId ? { tenantId: job.tenantId } : {},
|
|
3308
|
+
...job.agentConfig?.runtime ? { runtime: job.agentConfig.runtime } : {},
|
|
3309
|
+
...job.agentConfig?.model ? { model: job.agentConfig.model } : {}
|
|
3310
|
+
});
|
|
2781
3311
|
if (running.has(job.jobId)) {
|
|
2782
|
-
|
|
3312
|
+
jobLog.warn({}, `JOB_DUPLICATE:${job.stageId} ${job.jobId}`);
|
|
2783
3313
|
return;
|
|
2784
3314
|
}
|
|
2785
3315
|
const cachedJob = lastResults.get(job.jobId);
|
|
2786
3316
|
if (cachedJob) {
|
|
2787
3317
|
send(cachedJob);
|
|
2788
|
-
|
|
3318
|
+
jobLog.info({}, `JOB_REPLAY:${job.stageId} ${job.jobId}`);
|
|
2789
3319
|
return;
|
|
2790
3320
|
}
|
|
2791
3321
|
running.add(job.jobId);
|
|
2792
|
-
|
|
3322
|
+
const startedAt = Date.now();
|
|
3323
|
+
jobLog.info({}, `JOB_STARTED:${job.stageId} ${job.jobId}`);
|
|
2793
3324
|
try {
|
|
2794
3325
|
const result = await stageRunner.runJob(job);
|
|
2795
3326
|
const frame = { type: "result", awakeableId: job.awakeableId, result };
|
|
2796
3327
|
rememberResult(job.jobId, frame);
|
|
2797
3328
|
send(frame);
|
|
2798
|
-
|
|
3329
|
+
jobLog.info(
|
|
3330
|
+
{ status: result.status, costUsd: result.costUsd, summary: result.summary, durationMs: Date.now() - startedAt },
|
|
3331
|
+
`JOB_DONE:${job.stageId} ${result.status}`
|
|
3332
|
+
);
|
|
2799
3333
|
} catch (e) {
|
|
2800
3334
|
const frame = {
|
|
2801
3335
|
type: "result",
|
|
@@ -2804,7 +3338,7 @@ async function startEdgeNode(opts) {
|
|
|
2804
3338
|
};
|
|
2805
3339
|
rememberResult(job.jobId, frame);
|
|
2806
3340
|
send(frame);
|
|
2807
|
-
|
|
3341
|
+
jobLog.error({ err: e, durationMs: Date.now() - startedAt }, `JOB_ERROR:${job.stageId} ${e.message}`);
|
|
2808
3342
|
} finally {
|
|
2809
3343
|
running.delete(job.jobId);
|
|
2810
3344
|
}
|
|
@@ -2813,7 +3347,8 @@ async function startEdgeNode(opts) {
|
|
|
2813
3347
|
const sock = new WebSocket(opts.hubUrl);
|
|
2814
3348
|
ws = sock;
|
|
2815
3349
|
sock.on("open", () => {
|
|
2816
|
-
|
|
3350
|
+
connectCount++;
|
|
3351
|
+
log.info({ hubUrl: opts.hubUrl, nodeId, connectCount }, "EDGE_CONNECTED");
|
|
2817
3352
|
send({
|
|
2818
3353
|
type: "hello",
|
|
2819
3354
|
enrolToken: opts.enrolToken ?? "",
|
|
@@ -2837,19 +3372,22 @@ async function startEdgeNode(opts) {
|
|
|
2837
3372
|
sock.on("pong", () => {
|
|
2838
3373
|
lastPongAt = Date.now();
|
|
2839
3374
|
});
|
|
2840
|
-
sock.on("error", (e) => log(`EDGE_ERROR ${e.message}`));
|
|
3375
|
+
sock.on("error", (e) => log.error({ err: e, hubUrl: opts.hubUrl }, `EDGE_ERROR ${e.message}`));
|
|
2841
3376
|
sock.on("close", (code, reason) => {
|
|
2842
3377
|
if (heartbeat) clearInterval(heartbeat);
|
|
2843
3378
|
heartbeat = void 0;
|
|
2844
3379
|
if (isEnrolmentRejection(code)) {
|
|
2845
3380
|
shuttingDown = true;
|
|
2846
3381
|
const detail = reason.toString() || "enrolment rejected";
|
|
2847
|
-
log(`EDGE_REJECTED:${code} ${detail}`);
|
|
3382
|
+
log.error({ closeCode: code, detail, fatal: true }, `EDGE_REJECTED:${code} ${detail}`);
|
|
2848
3383
|
process.exitCode = ENROLMENT_REJECTED_EXIT_CODE;
|
|
2849
3384
|
onFatal?.(new Error(`hub rejected edge enrolment (${code}): ${detail}`));
|
|
2850
3385
|
return;
|
|
2851
3386
|
}
|
|
2852
|
-
if (!shuttingDown)
|
|
3387
|
+
if (!shuttingDown) {
|
|
3388
|
+
log.warn({ closeCode: code, reason: reason.toString(), connectCount }, `EDGE_DISCONNECTED:${code} reconnecting in 500ms`);
|
|
3389
|
+
reconnectTimer = setTimeout(connect, 500);
|
|
3390
|
+
}
|
|
2853
3391
|
});
|
|
2854
3392
|
};
|
|
2855
3393
|
opts.signal?.addEventListener(
|
|
@@ -3009,7 +3547,19 @@ function probeHub(opts) {
|
|
|
3009
3547
|
|
|
3010
3548
|
// src/cli.ts
|
|
3011
3549
|
import { parseArgs } from "util";
|
|
3012
|
-
var
|
|
3550
|
+
var DEFAULT_LOG_LINES = 200;
|
|
3551
|
+
var COMMANDS = /* @__PURE__ */ new Set([
|
|
3552
|
+
"start",
|
|
3553
|
+
"stop",
|
|
3554
|
+
"restart",
|
|
3555
|
+
"logs",
|
|
3556
|
+
"diagnose",
|
|
3557
|
+
"run",
|
|
3558
|
+
"service",
|
|
3559
|
+
"doctor",
|
|
3560
|
+
"status",
|
|
3561
|
+
"update"
|
|
3562
|
+
]);
|
|
3013
3563
|
var isCommand = (s) => COMMANDS.has(s);
|
|
3014
3564
|
function parseCli(argv) {
|
|
3015
3565
|
const [first, ...rest] = argv;
|
|
@@ -3030,6 +3580,8 @@ function parseCli(argv) {
|
|
|
3030
3580
|
if (command === "run") return parseRun(flagArgs);
|
|
3031
3581
|
if (command === "service") return parseService(flagArgs);
|
|
3032
3582
|
if (command === "update") return parseUpdate(flagArgs);
|
|
3583
|
+
if (command === "logs") return parseLogs(flagArgs);
|
|
3584
|
+
if (command === "diagnose") return parseDiagnose(flagArgs);
|
|
3033
3585
|
let values;
|
|
3034
3586
|
try {
|
|
3035
3587
|
({ values } = parseArgs({
|
|
@@ -3039,6 +3591,7 @@ function parseCli(argv) {
|
|
|
3039
3591
|
name: { type: "string" },
|
|
3040
3592
|
"hub-url": { type: "string" },
|
|
3041
3593
|
ephemeral: { type: "boolean", default: false },
|
|
3594
|
+
foreground: { type: "boolean", default: false },
|
|
3042
3595
|
help: { type: "boolean", default: false }
|
|
3043
3596
|
},
|
|
3044
3597
|
allowPositionals: false
|
|
@@ -3047,14 +3600,74 @@ function parseCli(argv) {
|
|
|
3047
3600
|
return { kind: "error", message: e.message };
|
|
3048
3601
|
}
|
|
3049
3602
|
if (values.help) return { kind: "help", command };
|
|
3603
|
+
const ephemeral = values.ephemeral ?? false;
|
|
3050
3604
|
const flags = {
|
|
3051
3605
|
...values.token ? { token: values.token } : {},
|
|
3052
3606
|
...values.name ? { name: values.name } : {},
|
|
3053
3607
|
...values["hub-url"] ? { hubUrl: values["hub-url"] } : {},
|
|
3054
|
-
ephemeral
|
|
3608
|
+
ephemeral,
|
|
3609
|
+
// An ephemeral node mints a throwaway id and persists nothing, so there is nothing coherent to hand to
|
|
3610
|
+
// a supervisor that restarts it on boot. It is a foreground node by definition.
|
|
3611
|
+
foreground: (values.foreground ?? false) || ephemeral
|
|
3055
3612
|
};
|
|
3613
|
+
if (command === "stop") return { kind: "stop" };
|
|
3056
3614
|
return { kind: command, flags };
|
|
3057
3615
|
}
|
|
3616
|
+
var LOG_LEVELS = ["trace", "debug", "info", "warn", "error", "fatal"];
|
|
3617
|
+
function parseLogs(flagArgs) {
|
|
3618
|
+
let values;
|
|
3619
|
+
try {
|
|
3620
|
+
({ values } = parseArgs({
|
|
3621
|
+
args: flagArgs,
|
|
3622
|
+
options: {
|
|
3623
|
+
follow: { type: "boolean", short: "f", default: false },
|
|
3624
|
+
lines: { type: "string", short: "n" },
|
|
3625
|
+
level: { type: "string" },
|
|
3626
|
+
run: { type: "string" },
|
|
3627
|
+
json: { type: "boolean", default: false },
|
|
3628
|
+
help: { type: "boolean", default: false }
|
|
3629
|
+
},
|
|
3630
|
+
allowPositionals: false
|
|
3631
|
+
}));
|
|
3632
|
+
} catch (e) {
|
|
3633
|
+
return { kind: "error", message: e.message };
|
|
3634
|
+
}
|
|
3635
|
+
if (values.help) return { kind: "help", command: "logs" };
|
|
3636
|
+
const lines = values.lines === void 0 ? DEFAULT_LOG_LINES : Number(values.lines);
|
|
3637
|
+
if (!Number.isInteger(lines) || lines < 0) {
|
|
3638
|
+
return { kind: "error", message: `logs: --lines must be a non-negative whole number (got "${values.lines}")` };
|
|
3639
|
+
}
|
|
3640
|
+
if (values.level !== void 0 && !LOG_LEVELS.includes(values.level)) {
|
|
3641
|
+
return { kind: "error", message: `logs: --level must be one of ${LOG_LEVELS.join(", ")} (got "${values.level}")` };
|
|
3642
|
+
}
|
|
3643
|
+
return {
|
|
3644
|
+
kind: "logs",
|
|
3645
|
+
flags: {
|
|
3646
|
+
lines,
|
|
3647
|
+
follow: values.follow ?? false,
|
|
3648
|
+
...values.level ? { level: values.level } : {},
|
|
3649
|
+
...values.run ? { run: values.run } : {},
|
|
3650
|
+
json: values.json ?? false
|
|
3651
|
+
}
|
|
3652
|
+
};
|
|
3653
|
+
}
|
|
3654
|
+
function parseDiagnose(flagArgs) {
|
|
3655
|
+
let values;
|
|
3656
|
+
try {
|
|
3657
|
+
({ values } = parseArgs({
|
|
3658
|
+
args: flagArgs,
|
|
3659
|
+
options: {
|
|
3660
|
+
out: { type: "string" },
|
|
3661
|
+
help: { type: "boolean", default: false }
|
|
3662
|
+
},
|
|
3663
|
+
allowPositionals: false
|
|
3664
|
+
}));
|
|
3665
|
+
} catch (e) {
|
|
3666
|
+
return { kind: "error", message: e.message };
|
|
3667
|
+
}
|
|
3668
|
+
if (values.help) return { kind: "help", command: "diagnose" };
|
|
3669
|
+
return { kind: "diagnose", flags: { ...values.out ? { out: values.out } : {} } };
|
|
3670
|
+
}
|
|
3058
3671
|
function parseRun(flagArgs) {
|
|
3059
3672
|
let values;
|
|
3060
3673
|
let positionals;
|
|
@@ -3145,15 +3758,98 @@ function parseUpdate(flagArgs) {
|
|
|
3145
3758
|
function usage(bin, command) {
|
|
3146
3759
|
if (command === "start") {
|
|
3147
3760
|
return [
|
|
3148
|
-
`Usage: ${bin} start --token <token> [options]`,
|
|
3761
|
+
`Usage: ${bin} start [--token <token>] [options]`,
|
|
3762
|
+
"",
|
|
3763
|
+
"Make this node run, and keep running: install it as an always-on service (launchd / systemd), start",
|
|
3764
|
+
"it, and return. It restarts on failure and comes back after a reboot. Running it again is a no-op if",
|
|
3765
|
+
"the node is already up, so it is safe to repeat.",
|
|
3766
|
+
"",
|
|
3767
|
+
"A token is needed only to enrol. Once the hub accepts it, it is cached in ~/.dahrk/node.json and",
|
|
3768
|
+
"every later `start` re-attaches without one. Pass --token again to re-enrol (rotated token, new pool).",
|
|
3149
3769
|
"",
|
|
3150
|
-
"
|
|
3770
|
+
"Once it is running:",
|
|
3771
|
+
` ${bin} logs -f Watch what it is doing.`,
|
|
3772
|
+
` ${bin} status Is it enrolled, and is it up?`,
|
|
3773
|
+
` ${bin} stop Stop it (it stays stopped until the next \`start\`).`,
|
|
3151
3774
|
"",
|
|
3152
3775
|
"Options:",
|
|
3153
|
-
" --token <token> Enrolment token (
|
|
3776
|
+
" --token <token> Enrolment token (first run only; or set DAHRK_ENROL_TOKEN).",
|
|
3154
3777
|
" --hub-url <url> Hub WebSocket URL (or set DAHRK_HUB_URL).",
|
|
3155
3778
|
" --name <name> Display-name override (else the hub assigns one).",
|
|
3156
|
-
" --
|
|
3779
|
+
" --foreground Run the node in THIS terminal and block, instead of as a service. For",
|
|
3780
|
+
" containers, pm2, CI, or just watching one work. Ctrl-C stops it. Same thing as",
|
|
3781
|
+
" setting DAHRK_FOREGROUND=1, which is easier to set in a Dockerfile or pm2 config.",
|
|
3782
|
+
" --ephemeral Do not persist (or read) node id and token; mint a throwaway id (CI / one-shot).",
|
|
3783
|
+
" Implies --foreground: a node with no persistent identity has nothing to daemonise.",
|
|
3784
|
+
"",
|
|
3785
|
+
"Only one node may run at a time on a host - they would share this machine's node id and race each",
|
|
3786
|
+
"other for Jobs - so a second `start` refuses rather than dialling the hub twice."
|
|
3787
|
+
].join("\n");
|
|
3788
|
+
}
|
|
3789
|
+
if (command === "stop") {
|
|
3790
|
+
return [
|
|
3791
|
+
`Usage: ${bin} stop`,
|
|
3792
|
+
"",
|
|
3793
|
+
"Stop the node. It stays stopped across reboots until you run `dahrk start` again - a stop that",
|
|
3794
|
+
"quietly undid itself at the next boot would not be a stop.",
|
|
3795
|
+
"",
|
|
3796
|
+
"The service stays installed, so starting it again is instant. To remove it entirely, use",
|
|
3797
|
+
"`dahrk service uninstall`. To stop a node running in a terminal (--foreground), press Ctrl-C."
|
|
3798
|
+
].join("\n");
|
|
3799
|
+
}
|
|
3800
|
+
if (command === "restart") {
|
|
3801
|
+
return [
|
|
3802
|
+
`Usage: ${bin} restart [options]`,
|
|
3803
|
+
"",
|
|
3804
|
+
"Stop the node and start it again. Picks up a new client version, a rotated token, or a runtime you",
|
|
3805
|
+
"have just installed (the node detects runtimes at boot, so a fresh `claude` on PATH needs a restart).",
|
|
3806
|
+
"",
|
|
3807
|
+
"Options:",
|
|
3808
|
+
" --token <token> Re-enrol with a new token (or set DAHRK_ENROL_TOKEN).",
|
|
3809
|
+
" --hub-url <url> Hub WebSocket URL (or set DAHRK_HUB_URL).",
|
|
3810
|
+
" --name <name> Display-name override (else the hub assigns one)."
|
|
3811
|
+
].join("\n");
|
|
3812
|
+
}
|
|
3813
|
+
if (command === "logs") {
|
|
3814
|
+
return [
|
|
3815
|
+
`Usage: ${bin} logs [-f] [-n <lines>] [--level <l>] [--run <id>] [--json]`,
|
|
3816
|
+
"",
|
|
3817
|
+
"Show what the node has been doing: its connect / welcome handshake, the Jobs it has run, and anything",
|
|
3818
|
+
"it has crashed on.",
|
|
3819
|
+
"",
|
|
3820
|
+
"There are two logs, and this reads whichever you ask for. On its own it tails the transcript the",
|
|
3821
|
+
"service captured (~/.dahrk/logs/node.out.log and node.err.log) - the same lines the node printed.",
|
|
3822
|
+
"Pass --level, --run or --json and it reads node.jsonl instead: the node's own structured log, which",
|
|
3823
|
+
"is the one carrying levels, timestamps, correlation ids and full error stacks. It is written at",
|
|
3824
|
+
"debug even when the terminal is not, so the detail for an incident is already there afterwards.",
|
|
3825
|
+
"",
|
|
3826
|
+
"Options:",
|
|
3827
|
+
" -f, --follow Keep watching as new lines arrive (Ctrl-C to stop).",
|
|
3828
|
+
" -n, --lines <n> Lines of history to show first (default 200).",
|
|
3829
|
+
" --level <level> Only this level and above: trace, debug, info, warn, error, fatal.",
|
|
3830
|
+
" --run <runId> Only this run. The runId is the hub's - so `logs --run <id>` and the hub's",
|
|
3831
|
+
" view of the same run describe the same thing from both ends.",
|
|
3832
|
+
" --json Print the raw JSON records (for jq) rather than a rendered line.",
|
|
3833
|
+
"",
|
|
3834
|
+
"A node run with --foreground prints to its terminal, but it still writes node.jsonl - so --run and",
|
|
3835
|
+
"--level work for it too."
|
|
3836
|
+
].join("\n");
|
|
3837
|
+
}
|
|
3838
|
+
if (command === "diagnose") {
|
|
3839
|
+
return [
|
|
3840
|
+
`Usage: ${bin} diagnose [--out <path>]`,
|
|
3841
|
+
"",
|
|
3842
|
+
"Write a support bundle: everything needed to debug this node, in one file you can read.",
|
|
3843
|
+
"",
|
|
3844
|
+
"It collects this node's id, name, tenant, version and host; the doctor's verdict; the tail of the",
|
|
3845
|
+
"structured log; and every crash record. Secrets are stripped and the enrolment token is not",
|
|
3846
|
+
"included. Your source code, prompts and issue content are not included.",
|
|
3847
|
+
"",
|
|
3848
|
+
"Nothing is uploaded. There is no flag to upload it. The file is written locally so that you can open",
|
|
3849
|
+
"it, read every line, and decide for yourself whether to send it on.",
|
|
3850
|
+
"",
|
|
3851
|
+
"Options:",
|
|
3852
|
+
" --out <path> Write the bundle here (default: ./dahrk-diagnose-<timestamp>.json)."
|
|
3157
3853
|
].join("\n");
|
|
3158
3854
|
}
|
|
3159
3855
|
if (command === "run") {
|
|
@@ -3201,6 +3897,18 @@ function usage(bin, command) {
|
|
|
3201
3897
|
" --hub-url <url> Hub WebSocket URL to reach (or set DAHRK_HUB_URL)."
|
|
3202
3898
|
].join("\n");
|
|
3203
3899
|
}
|
|
3900
|
+
if (command === "status") {
|
|
3901
|
+
return [
|
|
3902
|
+
`Usage: ${bin} status`,
|
|
3903
|
+
"",
|
|
3904
|
+
"Report this node's local state: whether it is enrolled (and as whom), its node id, the runtimes",
|
|
3905
|
+
"it can serve, and whether the always-on service is installed and actually running.",
|
|
3906
|
+
"",
|
|
3907
|
+
"Local only - it dials nothing, so it is instant and works offline. Use `doctor` to check the hub",
|
|
3908
|
+
"is reachable and the token still valid. Exits non-zero only when the service is installed but",
|
|
3909
|
+
"not running, so it works as a health check in a script."
|
|
3910
|
+
].join("\n");
|
|
3911
|
+
}
|
|
3204
3912
|
if (command === "update") {
|
|
3205
3913
|
return [
|
|
3206
3914
|
`Usage: ${bin} update [--check]`,
|
|
@@ -3217,18 +3925,139 @@ function usage(bin, command) {
|
|
|
3217
3925
|
`Usage: ${bin} <command> [options]`,
|
|
3218
3926
|
"",
|
|
3219
3927
|
"Commands:",
|
|
3220
|
-
" start Run the
|
|
3928
|
+
" start Run the node, and keep it running (installs the service). --token to enrol, once.",
|
|
3929
|
+
" stop Stop the node. It stays stopped until the next `start`.",
|
|
3930
|
+
" restart Stop it and start it again.",
|
|
3931
|
+
" logs Show what the node is doing (-f to follow, --run <id> to narrow to one run).",
|
|
3932
|
+
" diagnose Write a support bundle you can read, and send on if you choose. Uploads nothing.",
|
|
3933
|
+
" status Is this node enrolled, and is it up? (local, dials nothing)",
|
|
3221
3934
|
" run Run a workflow locally (engine-backed), e.g. `run preflight`.",
|
|
3222
|
-
" service Install/uninstall the node as an always-on service (launchd/systemd).",
|
|
3223
3935
|
" doctor Preflight checks: Node, runtimes, hub reachability, token validity.",
|
|
3936
|
+
" service Install/uninstall the always-on service by hand (`start` does this for you).",
|
|
3224
3937
|
" update Update the client to the latest release (or print how for your channel).",
|
|
3225
3938
|
" version Print the client version.",
|
|
3226
3939
|
" help Show this help, or `help <command>` for a command's options.",
|
|
3227
3940
|
"",
|
|
3228
|
-
`Run \`${bin} help <command>\` for command-specific options
|
|
3941
|
+
`Run \`${bin} help <command>\` for command-specific options.`,
|
|
3942
|
+
`To run a node in this terminal instead of as a service: \`${bin} start --foreground\`.`
|
|
3229
3943
|
].join("\n");
|
|
3230
3944
|
}
|
|
3231
3945
|
|
|
3946
|
+
// src/diagnose.ts
|
|
3947
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync7, readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync3, writeFileSync as writeFileSync6 } from "fs";
|
|
3948
|
+
import { join as join9 } from "path";
|
|
3949
|
+
var BUNDLE_LOG_LINES = 2e3;
|
|
3950
|
+
function tailJsonl(raw, n) {
|
|
3951
|
+
const records = [];
|
|
3952
|
+
for (const line of raw.split("\n")) {
|
|
3953
|
+
if (!line.trim()) continue;
|
|
3954
|
+
try {
|
|
3955
|
+
records.push(JSON.parse(line));
|
|
3956
|
+
} catch {
|
|
3957
|
+
}
|
|
3958
|
+
}
|
|
3959
|
+
return n > 0 ? records.slice(-n) : records;
|
|
3960
|
+
}
|
|
3961
|
+
async function buildBundle(deps) {
|
|
3962
|
+
const warnings = [];
|
|
3963
|
+
let state = {};
|
|
3964
|
+
if (deps.exists(deps.stateFile)) {
|
|
3965
|
+
try {
|
|
3966
|
+
const parsed = JSON.parse(deps.readFile(deps.stateFile));
|
|
3967
|
+
const { enrolToken: _dropped, ...rest } = parsed;
|
|
3968
|
+
state = rest;
|
|
3969
|
+
} catch (e) {
|
|
3970
|
+
warnings.push(`could not read node state (${e.message})`);
|
|
3971
|
+
}
|
|
3972
|
+
} else {
|
|
3973
|
+
warnings.push("no node.json - this node has never enrolled");
|
|
3974
|
+
}
|
|
3975
|
+
let log = [];
|
|
3976
|
+
if (deps.exists(deps.jsonlFile)) {
|
|
3977
|
+
try {
|
|
3978
|
+
log = tailJsonl(deps.readFile(deps.jsonlFile), BUNDLE_LOG_LINES);
|
|
3979
|
+
} catch (e) {
|
|
3980
|
+
warnings.push(`could not read node.jsonl (${e.message})`);
|
|
3981
|
+
}
|
|
3982
|
+
} else {
|
|
3983
|
+
warnings.push("no node.jsonl - the node has not run since structured logging was added");
|
|
3984
|
+
}
|
|
3985
|
+
const crashes = [];
|
|
3986
|
+
if (deps.exists(deps.crashDir)) {
|
|
3987
|
+
for (const name of deps.listDir(deps.crashDir).filter((f) => f.endsWith(".json")).sort()) {
|
|
3988
|
+
try {
|
|
3989
|
+
crashes.push(JSON.parse(deps.readFile(join9(deps.crashDir, name))));
|
|
3990
|
+
} catch (e) {
|
|
3991
|
+
warnings.push(`could not read crash record ${name} (${e.message})`);
|
|
3992
|
+
}
|
|
3993
|
+
}
|
|
3994
|
+
}
|
|
3995
|
+
let doctor;
|
|
3996
|
+
if (deps.doctor) {
|
|
3997
|
+
try {
|
|
3998
|
+
doctor = await deps.doctor();
|
|
3999
|
+
} catch (e) {
|
|
4000
|
+
warnings.push(`doctor failed (${e.message})`);
|
|
4001
|
+
}
|
|
4002
|
+
}
|
|
4003
|
+
const bundle = {
|
|
4004
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4005
|
+
clientVersion: deps.clientVersion,
|
|
4006
|
+
node: {
|
|
4007
|
+
state,
|
|
4008
|
+
platform: process.platform,
|
|
4009
|
+
arch: process.arch,
|
|
4010
|
+
nodeVersion: process.version
|
|
4011
|
+
},
|
|
4012
|
+
...doctor ? { doctor } : {},
|
|
4013
|
+
log,
|
|
4014
|
+
crashes,
|
|
4015
|
+
warnings
|
|
4016
|
+
};
|
|
4017
|
+
return scrubValue(bundle);
|
|
4018
|
+
}
|
|
4019
|
+
async function runDiagnose(deps) {
|
|
4020
|
+
const bundle = await buildBundle(deps);
|
|
4021
|
+
try {
|
|
4022
|
+
deps.writeFile(deps.outFile, `${JSON.stringify(bundle, null, 2)}
|
|
4023
|
+
`);
|
|
4024
|
+
} catch (e) {
|
|
4025
|
+
deps.out(`Could not write the bundle: ${e.message}`);
|
|
4026
|
+
return 1;
|
|
4027
|
+
}
|
|
4028
|
+
deps.out(`Wrote a support bundle to ${deps.outFile}`);
|
|
4029
|
+
deps.out("");
|
|
4030
|
+
deps.out(`It contains: this node's id, name and tenant; its version and host; ${bundle.log.length} log`);
|
|
4031
|
+
deps.out(`lines; and ${bundle.crashes.length} crash record(s). Secrets are stripped and the enrolment`);
|
|
4032
|
+
deps.out("token is not in it. Your source code, prompts and issue content are not in it.");
|
|
4033
|
+
deps.out("");
|
|
4034
|
+
deps.out("Nothing has been sent anywhere. Open the file, read it, and send it on only if you are happy to.");
|
|
4035
|
+
if (bundle.warnings.length > 0) {
|
|
4036
|
+
deps.out("");
|
|
4037
|
+
deps.out("Note:");
|
|
4038
|
+
for (const w of bundle.warnings) deps.out(` - ${w}`);
|
|
4039
|
+
}
|
|
4040
|
+
return 0;
|
|
4041
|
+
}
|
|
4042
|
+
var defaultDiagnoseDeps = (paths, clientVersion, doctor) => ({
|
|
4043
|
+
...paths,
|
|
4044
|
+
clientVersion,
|
|
4045
|
+
...doctor ? { doctor } : {},
|
|
4046
|
+
readFile: (p) => readFileSync5(p, "utf8"),
|
|
4047
|
+
listDir: (p) => readdirSync4(p),
|
|
4048
|
+
exists: (p) => existsSync6(p),
|
|
4049
|
+
writeFile: (p, content) => {
|
|
4050
|
+
const dir = join9(p, "..");
|
|
4051
|
+
if (!existsSync6(dir)) mkdirSync7(dir, { recursive: true });
|
|
4052
|
+
writeFileSync6(p, content, { mode: 384 });
|
|
4053
|
+
},
|
|
4054
|
+
out: (line) => void process.stdout.write(`${line}
|
|
4055
|
+
`)
|
|
4056
|
+
});
|
|
4057
|
+
function defaultBundlePath(cwd, now) {
|
|
4058
|
+
return join9(cwd, `dahrk-diagnose-${now.toISOString().replace(/[:.]/g, "-")}.json`);
|
|
4059
|
+
}
|
|
4060
|
+
|
|
3232
4061
|
// src/doctor.ts
|
|
3233
4062
|
var MIN_NODE_MAJOR = 22;
|
|
3234
4063
|
var TAG = { pass: "[PASS]", warn: "[WARN]", fail: "[FAIL]" };
|
|
@@ -3360,12 +4189,218 @@ async function runDoctor(inputs, deps = {}) {
|
|
|
3360
4189
|
return checks.some((c) => c.status === "fail") ? 1 : 0;
|
|
3361
4190
|
}
|
|
3362
4191
|
|
|
4192
|
+
// src/lock.ts
|
|
4193
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync8, readFileSync as readFileSync6, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "fs";
|
|
4194
|
+
import { dirname as dirname4 } from "path";
|
|
4195
|
+
function parseLock(content) {
|
|
4196
|
+
if (!content) return void 0;
|
|
4197
|
+
const pid = Number(content.trim());
|
|
4198
|
+
return Number.isInteger(pid) && pid > 0 ? pid : void 0;
|
|
4199
|
+
}
|
|
4200
|
+
function acquireLock(deps) {
|
|
4201
|
+
const held = parseLock(deps.readFile(deps.file));
|
|
4202
|
+
if (held !== void 0 && held !== deps.pid && deps.isAlive(held)) {
|
|
4203
|
+
return { ok: false, heldBy: held };
|
|
4204
|
+
}
|
|
4205
|
+
deps.writeFile(deps.file, `${deps.pid}
|
|
4206
|
+
`);
|
|
4207
|
+
return { ok: true, release: () => deps.removeFile(deps.file) };
|
|
4208
|
+
}
|
|
4209
|
+
function isAlive(pid) {
|
|
4210
|
+
try {
|
|
4211
|
+
process.kill(pid, 0);
|
|
4212
|
+
return true;
|
|
4213
|
+
} catch (e) {
|
|
4214
|
+
return e.code === "EPERM";
|
|
4215
|
+
}
|
|
4216
|
+
}
|
|
4217
|
+
var defaultLockDeps = (file) => ({
|
|
4218
|
+
file,
|
|
4219
|
+
pid: process.pid,
|
|
4220
|
+
readFile: (path) => {
|
|
4221
|
+
try {
|
|
4222
|
+
return readFileSync6(path, "utf8");
|
|
4223
|
+
} catch {
|
|
4224
|
+
return void 0;
|
|
4225
|
+
}
|
|
4226
|
+
},
|
|
4227
|
+
writeFile: (path, content) => {
|
|
4228
|
+
if (!existsSync7(dirname4(path))) mkdirSync8(dirname4(path), { recursive: true, mode: 448 });
|
|
4229
|
+
writeFileSync7(path, content);
|
|
4230
|
+
},
|
|
4231
|
+
removeFile: (path) => rmSync6(path, { force: true }),
|
|
4232
|
+
isAlive
|
|
4233
|
+
});
|
|
4234
|
+
|
|
4235
|
+
// src/logs.ts
|
|
4236
|
+
import { spawn } from "child_process";
|
|
4237
|
+
import { copyFileSync, existsSync as existsSync8, readFileSync as readFileSync7, statSync as statSync4, truncateSync } from "fs";
|
|
4238
|
+
var MAX_LOG_BYTES = 10 * 1024 * 1024;
|
|
4239
|
+
function logsCommand(inputs) {
|
|
4240
|
+
return [
|
|
4241
|
+
"tail",
|
|
4242
|
+
"-n",
|
|
4243
|
+
String(inputs.lines),
|
|
4244
|
+
...inputs.follow ? ["-f"] : [],
|
|
4245
|
+
...inputs.files
|
|
4246
|
+
];
|
|
4247
|
+
}
|
|
4248
|
+
var LEVEL_RANK = { trace: 10, debug: 20, info: 30, warn: 40, error: 50, fatal: 60 };
|
|
4249
|
+
var levelName = (n) => Object.entries(LEVEL_RANK).find(([, rank]) => rank === n)?.[0] ?? String(n);
|
|
4250
|
+
function parseRecords(raw) {
|
|
4251
|
+
const out = [];
|
|
4252
|
+
for (const line of raw.split("\n")) {
|
|
4253
|
+
if (!line.trim()) continue;
|
|
4254
|
+
try {
|
|
4255
|
+
out.push(JSON.parse(line));
|
|
4256
|
+
} catch {
|
|
4257
|
+
}
|
|
4258
|
+
}
|
|
4259
|
+
return out;
|
|
4260
|
+
}
|
|
4261
|
+
function filterRecords(records, q) {
|
|
4262
|
+
const min = q.level ? LEVEL_RANK[q.level] ?? 0 : 0;
|
|
4263
|
+
const matched = records.filter((r) => r.level >= min && (q.run === void 0 || r.runId === q.run));
|
|
4264
|
+
return q.lines > 0 ? matched.slice(-q.lines) : matched;
|
|
4265
|
+
}
|
|
4266
|
+
function renderRecord(r) {
|
|
4267
|
+
const where = [r.runId, r.stageId].filter(Boolean).join("/");
|
|
4268
|
+
const scope = where ? ` [${where}]` : r.component ? ` [${r.component}]` : "";
|
|
4269
|
+
const head = `${r.time} ${levelName(r.level).toUpperCase().padEnd(5)}${scope} ${r.msg}`;
|
|
4270
|
+
const lines = [head];
|
|
4271
|
+
if (r.err?.stack) lines.push(...r.err.stack.split("\n").map((l) => ` ${l}`));
|
|
4272
|
+
return lines;
|
|
4273
|
+
}
|
|
4274
|
+
async function runLogs(inputs, deps) {
|
|
4275
|
+
if (inputs.level !== void 0 || inputs.run !== void 0 || inputs.json) {
|
|
4276
|
+
return runStructuredLogs(inputs, deps);
|
|
4277
|
+
}
|
|
4278
|
+
const files = [deps.files.out, deps.files.err].filter((f) => deps.fileExists(f));
|
|
4279
|
+
if (files.length === 0) {
|
|
4280
|
+
deps.out("No logs yet - this node has not run under the service.");
|
|
4281
|
+
deps.out("Start it with `dahrk start`, or check what it thinks it is doing with `dahrk status`.");
|
|
4282
|
+
deps.out("");
|
|
4283
|
+
deps.out("(A node run in a terminal with `--foreground` logs to that terminal, not to a file.)");
|
|
4284
|
+
return 0;
|
|
4285
|
+
}
|
|
4286
|
+
return deps.run(logsCommand({ files, lines: inputs.lines, follow: inputs.follow }));
|
|
4287
|
+
}
|
|
4288
|
+
async function runStructuredLogs(inputs, deps) {
|
|
4289
|
+
if (!deps.fileExists(deps.jsonlFile)) {
|
|
4290
|
+
deps.out("No structured log yet - this node has not run since structured logging was added.");
|
|
4291
|
+
deps.out("Start it with `dahrk start`; it writes ~/.dahrk/logs/node.jsonl from the first boot.");
|
|
4292
|
+
return 0;
|
|
4293
|
+
}
|
|
4294
|
+
if (inputs.follow) {
|
|
4295
|
+
deps.out("(--follow on the structured log streams it unfiltered; pipe it through jq to narrow it:)");
|
|
4296
|
+
deps.out(` tail -f ${deps.jsonlFile} | jq -c 'select(.runId == "<runId>")'`);
|
|
4297
|
+
deps.out("");
|
|
4298
|
+
return deps.run(["tail", "-n", String(inputs.lines), "-f", deps.jsonlFile]);
|
|
4299
|
+
}
|
|
4300
|
+
const records = filterRecords(parseRecords(deps.readFile(deps.jsonlFile)), {
|
|
4301
|
+
level: inputs.level,
|
|
4302
|
+
run: inputs.run,
|
|
4303
|
+
lines: inputs.lines
|
|
4304
|
+
});
|
|
4305
|
+
if (records.length === 0) {
|
|
4306
|
+
const narrowed = [inputs.run ? `run ${inputs.run}` : void 0, inputs.level ? `level ${inputs.level}+` : void 0].filter(Boolean).join(" at ");
|
|
4307
|
+
deps.out(narrowed ? `No records for ${narrowed}.` : "No records.");
|
|
4308
|
+
return 0;
|
|
4309
|
+
}
|
|
4310
|
+
for (const r of records) {
|
|
4311
|
+
if (inputs.json) deps.out(JSON.stringify(r));
|
|
4312
|
+
else for (const line of renderRecord(r)) deps.out(line);
|
|
4313
|
+
}
|
|
4314
|
+
return 0;
|
|
4315
|
+
}
|
|
4316
|
+
function rotateIfLarge(file, maxBytes = MAX_LOG_BYTES) {
|
|
4317
|
+
try {
|
|
4318
|
+
if (!existsSync8(file) || statSync4(file).size <= maxBytes) return;
|
|
4319
|
+
copyFileSync(file, `${file}.1`);
|
|
4320
|
+
truncateSync(file, 0);
|
|
4321
|
+
} catch {
|
|
4322
|
+
}
|
|
4323
|
+
}
|
|
4324
|
+
var defaultLogsDeps = (files, jsonlFile) => ({
|
|
4325
|
+
files,
|
|
4326
|
+
jsonlFile,
|
|
4327
|
+
fileExists: (path) => existsSync8(path),
|
|
4328
|
+
readFile: (path) => readFileSync7(path, "utf8"),
|
|
4329
|
+
run: (argv) => new Promise((resolve2) => {
|
|
4330
|
+
const [cmd, ...args] = argv;
|
|
4331
|
+
const child = spawn(cmd, args, { stdio: "inherit" });
|
|
4332
|
+
child.on("error", () => resolve2(1));
|
|
4333
|
+
child.on("close", (code) => resolve2(code ?? 0));
|
|
4334
|
+
}),
|
|
4335
|
+
out: (line) => void process.stdout.write(`${line}
|
|
4336
|
+
`)
|
|
4337
|
+
});
|
|
4338
|
+
|
|
4339
|
+
// src/process-safety.ts
|
|
4340
|
+
import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync8 } from "fs";
|
|
4341
|
+
import { join as join10 } from "path";
|
|
4342
|
+
function writeCrashRecord(dir, record) {
|
|
4343
|
+
try {
|
|
4344
|
+
mkdirSync9(dir, { recursive: true, mode: 448 });
|
|
4345
|
+
const file = join10(dir, `${record.at.replace(/[:.]/g, "-")}.json`);
|
|
4346
|
+
writeFileSync8(file, `${JSON.stringify(record, null, 2)}
|
|
4347
|
+
`, { mode: 384 });
|
|
4348
|
+
return file;
|
|
4349
|
+
} catch {
|
|
4350
|
+
return void 0;
|
|
4351
|
+
}
|
|
4352
|
+
}
|
|
4353
|
+
function toRecord(kind, reason, opts) {
|
|
4354
|
+
const err = reason instanceof Error ? reason : void 0;
|
|
4355
|
+
return {
|
|
4356
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4357
|
+
kind,
|
|
4358
|
+
name: err?.name ?? typeof reason,
|
|
4359
|
+
message: err?.message ?? String(reason),
|
|
4360
|
+
...err?.stack ? { stack: err.stack } : {},
|
|
4361
|
+
clientVersion: opts.clientVersion,
|
|
4362
|
+
nodeVersion: process.version,
|
|
4363
|
+
platform: process.platform,
|
|
4364
|
+
arch: process.arch,
|
|
4365
|
+
uptimeSec: Math.round(process.uptime()),
|
|
4366
|
+
...opts.activeJobIds ? { activeJobIds: opts.activeJobIds() } : {}
|
|
4367
|
+
};
|
|
4368
|
+
}
|
|
4369
|
+
function makeCrashHandlers(opts) {
|
|
4370
|
+
const handle = (kind, reason) => {
|
|
4371
|
+
const record = toRecord(kind, reason, opts);
|
|
4372
|
+
const file = opts.crashDir ? writeCrashRecord(opts.crashDir, record) : void 0;
|
|
4373
|
+
opts.logger.error(
|
|
4374
|
+
{ err: reason, kind, ...file ? { crashFile: file } : {}, activeJobIds: record.activeJobIds },
|
|
4375
|
+
`NODE_CRASH:${kind} ${record.name}: ${record.message}`
|
|
4376
|
+
);
|
|
4377
|
+
};
|
|
4378
|
+
return {
|
|
4379
|
+
onUnhandledRejection: (reason) => handle("unhandledRejection", reason),
|
|
4380
|
+
onUncaughtException: (err) => {
|
|
4381
|
+
handle("uncaughtException", err);
|
|
4382
|
+
if (opts.exitOnCrash) process.exit(1);
|
|
4383
|
+
}
|
|
4384
|
+
};
|
|
4385
|
+
}
|
|
4386
|
+
function installProcessSafetyNet(opts) {
|
|
4387
|
+
const { onUnhandledRejection, onUncaughtException } = makeCrashHandlers(opts);
|
|
4388
|
+
process.on("unhandledRejection", onUnhandledRejection);
|
|
4389
|
+
process.on("uncaughtException", onUncaughtException);
|
|
4390
|
+
return {
|
|
4391
|
+
uninstall: () => {
|
|
4392
|
+
process.off("unhandledRejection", onUnhandledRejection);
|
|
4393
|
+
process.off("uncaughtException", onUncaughtException);
|
|
4394
|
+
}
|
|
4395
|
+
};
|
|
4396
|
+
}
|
|
4397
|
+
|
|
3363
4398
|
// src/preflight.ts
|
|
3364
|
-
import { execFileSync as
|
|
4399
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
3365
4400
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
3366
|
-
import { accessSync, constants as fsConstants, existsSync as
|
|
4401
|
+
import { accessSync, constants as fsConstants, existsSync as existsSync9, readdirSync as readdirSync5, statfsSync } from "fs";
|
|
3367
4402
|
import { homedir as homedir2 } from "os";
|
|
3368
|
-
import { join as
|
|
4403
|
+
import { join as join11 } from "path";
|
|
3369
4404
|
var REPORT_BASE_URL = "https://app.dahrk.ai/r";
|
|
3370
4405
|
var LOW_DISK_BYTES = 512 * 1024 * 1024;
|
|
3371
4406
|
var PREFLIGHT_STAGES = [
|
|
@@ -3494,7 +4529,7 @@ var defaultDeps2 = () => ({
|
|
|
3494
4529
|
});
|
|
3495
4530
|
function commandPresent(cmd) {
|
|
3496
4531
|
try {
|
|
3497
|
-
|
|
4532
|
+
execFileSync5("sh", ["-c", `command -v ${cmd}`], { stdio: "ignore" });
|
|
3498
4533
|
return true;
|
|
3499
4534
|
} catch {
|
|
3500
4535
|
return false;
|
|
@@ -3502,12 +4537,12 @@ function commandPresent(cmd) {
|
|
|
3502
4537
|
}
|
|
3503
4538
|
function sshKeyPresent() {
|
|
3504
4539
|
try {
|
|
3505
|
-
const dir =
|
|
3506
|
-
if (
|
|
4540
|
+
const dir = join11(homedir2(), ".ssh");
|
|
4541
|
+
if (existsSync9(dir) && readdirSync5(dir).some((f) => f.endsWith(".pub"))) return true;
|
|
3507
4542
|
} catch {
|
|
3508
4543
|
}
|
|
3509
4544
|
try {
|
|
3510
|
-
|
|
4545
|
+
execFileSync5("ssh-add", ["-l"], { stdio: "ignore" });
|
|
3511
4546
|
return true;
|
|
3512
4547
|
} catch {
|
|
3513
4548
|
return false;
|
|
@@ -3522,12 +4557,12 @@ function writable(dir) {
|
|
|
3522
4557
|
}
|
|
3523
4558
|
}
|
|
3524
4559
|
function worktreeRoot(env) {
|
|
3525
|
-
return env.DAHRK_WORKTREES_DIR ??
|
|
4560
|
+
return env.DAHRK_WORKTREES_DIR ?? join11(env.DAHRK_STATE_DIR ?? join11(homedir2(), ".dahrk"), "worktrees");
|
|
3526
4561
|
}
|
|
3527
4562
|
function nearestExisting(dir) {
|
|
3528
4563
|
let cur = dir;
|
|
3529
|
-
while (!
|
|
3530
|
-
const parent =
|
|
4564
|
+
while (!existsSync9(cur)) {
|
|
4565
|
+
const parent = join11(cur, "..");
|
|
3531
4566
|
if (parent === cur) break;
|
|
3532
4567
|
cur = parent;
|
|
3533
4568
|
}
|
|
@@ -3542,7 +4577,7 @@ function freeDiskBytes(dir) {
|
|
|
3542
4577
|
}
|
|
3543
4578
|
}
|
|
3544
4579
|
function probeRepo(repoPath) {
|
|
3545
|
-
const git = (args) =>
|
|
4580
|
+
const git = (args) => execFileSync5("git", ["-C", repoPath, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
3546
4581
|
try {
|
|
3547
4582
|
if (git(["rev-parse", "--is-inside-work-tree"]) !== "true") {
|
|
3548
4583
|
return { path: repoPath, isGitRepo: false, headResolves: false, detail: "not inside a work tree" };
|
|
@@ -3577,10 +4612,98 @@ function gatherHostFacts(repoPath) {
|
|
|
3577
4612
|
}
|
|
3578
4613
|
|
|
3579
4614
|
// src/service.ts
|
|
3580
|
-
import { execFileSync as
|
|
3581
|
-
import { existsSync as
|
|
3582
|
-
import { homedir as
|
|
3583
|
-
import { join as
|
|
4615
|
+
import { execFileSync as execFileSync6 } from "child_process";
|
|
4616
|
+
import { chmodSync as chmodSync2, existsSync as existsSync11, mkdirSync as mkdirSync11, readFileSync as readFileSync9, realpathSync as realpathSync2, rmSync as rmSync7, writeFileSync as writeFileSync10 } from "fs";
|
|
4617
|
+
import { homedir as homedir4, platform as osPlatform3, userInfo } from "os";
|
|
4618
|
+
import { join as join13 } from "path";
|
|
4619
|
+
|
|
4620
|
+
// src/state.ts
|
|
4621
|
+
import { chmodSync, existsSync as existsSync10, mkdirSync as mkdirSync10, readFileSync as readFileSync8, writeFileSync as writeFileSync9 } from "fs";
|
|
4622
|
+
import { homedir as homedir3 } from "os";
|
|
4623
|
+
import { join as join12 } from "path";
|
|
4624
|
+
var STRING_FIELDS = ["nodeId", "enrolToken", "name", "tenantId", "updateCheckedAt", "updateLatest"];
|
|
4625
|
+
var isDesired = (v) => v === "running" || v === "stopped";
|
|
4626
|
+
var FILE_MODE = 384;
|
|
4627
|
+
var DIR_MODE = 448;
|
|
4628
|
+
function stateDir(env) {
|
|
4629
|
+
return env.DAHRK_STATE_DIR ?? join12(homedir3(), ".dahrk");
|
|
4630
|
+
}
|
|
4631
|
+
function legacyStateDir(env) {
|
|
4632
|
+
return env.DAHRK_STATE_DIR ? void 0 : join12(homedir3(), ".skakel");
|
|
4633
|
+
}
|
|
4634
|
+
function stateFile(env) {
|
|
4635
|
+
return join12(stateDir(env), "node.json");
|
|
4636
|
+
}
|
|
4637
|
+
function logDir(env) {
|
|
4638
|
+
return join12(stateDir(env), "logs");
|
|
4639
|
+
}
|
|
4640
|
+
function logFiles(env) {
|
|
4641
|
+
const dir = logDir(env);
|
|
4642
|
+
return { out: join12(dir, "node.out.log"), err: join12(dir, "node.err.log") };
|
|
4643
|
+
}
|
|
4644
|
+
function jsonlLogFile(env) {
|
|
4645
|
+
return join12(logDir(env), "node.jsonl");
|
|
4646
|
+
}
|
|
4647
|
+
function crashDir(env) {
|
|
4648
|
+
return join12(logDir(env), "crashes");
|
|
4649
|
+
}
|
|
4650
|
+
function lockFile(env) {
|
|
4651
|
+
return join12(stateDir(env), "node.pid");
|
|
4652
|
+
}
|
|
4653
|
+
function setDesired(env, desired) {
|
|
4654
|
+
writeState(env, { desired });
|
|
4655
|
+
}
|
|
4656
|
+
function readState(file) {
|
|
4657
|
+
if (!existsSync10(file)) return {};
|
|
4658
|
+
try {
|
|
4659
|
+
const parsed = JSON.parse(readFileSync8(file, "utf8"));
|
|
4660
|
+
const state = {};
|
|
4661
|
+
for (const key of STRING_FIELDS) {
|
|
4662
|
+
const value = parsed[key];
|
|
4663
|
+
if (typeof value === "string" && value) state[key] = value;
|
|
4664
|
+
}
|
|
4665
|
+
if (isDesired(parsed["desired"])) state.desired = parsed["desired"];
|
|
4666
|
+
return state;
|
|
4667
|
+
} catch {
|
|
4668
|
+
return {};
|
|
4669
|
+
}
|
|
4670
|
+
}
|
|
4671
|
+
function writeState(env, patch) {
|
|
4672
|
+
const dir = stateDir(env);
|
|
4673
|
+
const file = stateFile(env);
|
|
4674
|
+
try {
|
|
4675
|
+
mkdirSync10(dir, { recursive: true, mode: DIR_MODE });
|
|
4676
|
+
const next = { ...readState(file), ...patch };
|
|
4677
|
+
writeFileSync9(file, `${JSON.stringify(next, null, 2)}
|
|
4678
|
+
`, { mode: FILE_MODE });
|
|
4679
|
+
chmodSync(file, FILE_MODE);
|
|
4680
|
+
} catch (e) {
|
|
4681
|
+
console.warn(`could not persist node state to ${file}: ${e.message}`);
|
|
4682
|
+
}
|
|
4683
|
+
}
|
|
4684
|
+
function readPersistedToken(env) {
|
|
4685
|
+
return readState(stateFile(env)).enrolToken;
|
|
4686
|
+
}
|
|
4687
|
+
function persistEnrolment(env, enrolment) {
|
|
4688
|
+
const { token, name, tenantId } = enrolment;
|
|
4689
|
+
if (!token) return;
|
|
4690
|
+
const current = readState(stateFile(env));
|
|
4691
|
+
const unchanged = current.enrolToken === token && (name === void 0 || current.name === name) && (tenantId === void 0 || current.tenantId === tenantId);
|
|
4692
|
+
if (unchanged) return;
|
|
4693
|
+
writeState(env, {
|
|
4694
|
+
enrolToken: token,
|
|
4695
|
+
...name ? { name } : {},
|
|
4696
|
+
...tenantId ? { tenantId } : {}
|
|
4697
|
+
});
|
|
4698
|
+
}
|
|
4699
|
+
function resolveEnrolToken(env, opts = {}) {
|
|
4700
|
+
if (env.DAHRK_ENROL_TOKEN) return env.DAHRK_ENROL_TOKEN;
|
|
4701
|
+
if (opts.ephemeral) return void 0;
|
|
4702
|
+
return readPersistedToken(env);
|
|
4703
|
+
}
|
|
4704
|
+
|
|
4705
|
+
// src/service.ts
|
|
4706
|
+
var UNIT_FILE_MODE = 384;
|
|
3584
4707
|
var LAUNCHD_LABEL = "ai.dahrk.node";
|
|
3585
4708
|
var SYSTEMD_UNIT = "dahrk-node.service";
|
|
3586
4709
|
function detectManager(plat) {
|
|
@@ -3588,9 +4711,13 @@ function detectManager(plat) {
|
|
|
3588
4711
|
if (plat === "linux") return "systemd";
|
|
3589
4712
|
return "unsupported";
|
|
3590
4713
|
}
|
|
4714
|
+
function serviceArgv(inputs) {
|
|
4715
|
+
return [inputs.nodeBin, inputs.scriptPath, "start", "--foreground"];
|
|
4716
|
+
}
|
|
3591
4717
|
function serviceEnv(inputs) {
|
|
3592
4718
|
return {
|
|
3593
4719
|
DAHRK_ENROL_TOKEN: inputs.token,
|
|
4720
|
+
DAHRK_SUPERVISED: "1",
|
|
3594
4721
|
...inputs.hubUrl ? { DAHRK_HUB_URL: inputs.hubUrl } : {},
|
|
3595
4722
|
...inputs.name ? { DAHRK_NODE_NAME: inputs.name } : {},
|
|
3596
4723
|
...inputs.pathEnv ? { PATH: inputs.pathEnv } : {}
|
|
@@ -3600,7 +4727,7 @@ function xmlEscape(s) {
|
|
|
3600
4727
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
3601
4728
|
}
|
|
3602
4729
|
function renderLaunchdPlist(inputs) {
|
|
3603
|
-
const argv =
|
|
4730
|
+
const argv = serviceArgv(inputs);
|
|
3604
4731
|
const env = serviceEnv(inputs);
|
|
3605
4732
|
const progArgs = argv.map((a) => ` <string>${xmlEscape(a)}</string>`).join("\n");
|
|
3606
4733
|
const envEntries = Object.entries(env).map(([k, v]) => ` <key>${xmlEscape(k)}</key>
|
|
@@ -3628,9 +4755,9 @@ ${envEntries}
|
|
|
3628
4755
|
<key>ThrottleInterval</key>
|
|
3629
4756
|
<integer>10</integer>
|
|
3630
4757
|
<key>StandardOutPath</key>
|
|
3631
|
-
<string>${xmlEscape(
|
|
4758
|
+
<string>${xmlEscape(join13(inputs.logDir, "node.out.log"))}</string>
|
|
3632
4759
|
<key>StandardErrorPath</key>
|
|
3633
|
-
<string>${xmlEscape(
|
|
4760
|
+
<string>${xmlEscape(join13(inputs.logDir, "node.err.log"))}</string>
|
|
3634
4761
|
</dict>
|
|
3635
4762
|
</plist>
|
|
3636
4763
|
`;
|
|
@@ -3639,7 +4766,7 @@ function systemdEnvValue(v) {
|
|
|
3639
4766
|
return /\s/.test(v) ? `"${v.replace(/"/g, '\\"')}"` : v;
|
|
3640
4767
|
}
|
|
3641
4768
|
function renderSystemdUnit(inputs) {
|
|
3642
|
-
const exec =
|
|
4769
|
+
const exec = serviceArgv(inputs).map((a) => /\s/.test(a) ? `"${a}"` : a).join(" ");
|
|
3643
4770
|
const env = serviceEnv(inputs);
|
|
3644
4771
|
const envLines = Object.entries(env).map(([k, v]) => `Environment=${k}=${systemdEnvValue(v)}`).join("\n");
|
|
3645
4772
|
return `[Unit]
|
|
@@ -3653,6 +4780,8 @@ Type=simple
|
|
|
3653
4780
|
ExecStart=${exec}
|
|
3654
4781
|
${envLines}
|
|
3655
4782
|
WorkingDirectory=${inputs.homeDir}
|
|
4783
|
+
StandardOutput=append:${join13(inputs.logDir, "node.out.log")}
|
|
4784
|
+
StandardError=append:${join13(inputs.logDir, "node.err.log")}
|
|
3656
4785
|
Restart=on-failure
|
|
3657
4786
|
RestartSec=3
|
|
3658
4787
|
RestartPreventExitStatus=78
|
|
@@ -3663,22 +4792,24 @@ WantedBy=default.target
|
|
|
3663
4792
|
}
|
|
3664
4793
|
function buildPlan(inputs) {
|
|
3665
4794
|
if (inputs.manager === "launchd") {
|
|
3666
|
-
const filePath2 =
|
|
4795
|
+
const filePath2 = join13(inputs.homeDir, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
3667
4796
|
return {
|
|
3668
4797
|
manager: "launchd",
|
|
3669
4798
|
label: LAUNCHD_LABEL,
|
|
3670
4799
|
filePath: filePath2,
|
|
3671
4800
|
content: renderLaunchdPlist(inputs),
|
|
3672
|
-
// Unload first so a re-install picks up the rewritten plist; a not-loaded unload is a no-op.
|
|
4801
|
+
// Unload first so a re-install picks up the rewritten plist; a not-loaded unload is a no-op. The
|
|
4802
|
+
// `-w` on load also clears the "disabled" flag a previous `dahrk stop` set, so start-after-stop works.
|
|
3673
4803
|
installCommands: [
|
|
3674
4804
|
{ argv: ["launchctl", "unload", filePath2], ignoreFailure: true },
|
|
3675
4805
|
{ argv: ["launchctl", "load", "-w", filePath2] }
|
|
3676
4806
|
],
|
|
4807
|
+
stopCommands: [{ argv: ["launchctl", "unload", "-w", filePath2], ignoreFailure: true }],
|
|
3677
4808
|
uninstallCommands: [{ argv: ["launchctl", "unload", "-w", filePath2], ignoreFailure: true }],
|
|
3678
|
-
logHint:
|
|
4809
|
+
logHint: "dahrk logs -f"
|
|
3679
4810
|
};
|
|
3680
4811
|
}
|
|
3681
|
-
const filePath =
|
|
4812
|
+
const filePath = join13(inputs.homeDir, ".config", "systemd", "user", SYSTEMD_UNIT);
|
|
3682
4813
|
const user = userInfo().username;
|
|
3683
4814
|
return {
|
|
3684
4815
|
manager: "systemd",
|
|
@@ -3692,13 +4823,39 @@ function buildPlan(inputs) {
|
|
|
3692
4823
|
{ argv: ["systemctl", "--user", "enable", "--now", SYSTEMD_UNIT] },
|
|
3693
4824
|
{ argv: ["loginctl", "enable-linger", user], ignoreFailure: true }
|
|
3694
4825
|
],
|
|
4826
|
+
// `disable`, not `stop`: a stopped-but-enabled unit comes straight back at the next boot, which is not
|
|
4827
|
+
// what anyone means by `dahrk stop`. `dahrk start` re-enables it (`enable --now` above).
|
|
4828
|
+
stopCommands: [
|
|
4829
|
+
{ argv: ["systemctl", "--user", "disable", "--now", SYSTEMD_UNIT], ignoreFailure: true }
|
|
4830
|
+
],
|
|
3695
4831
|
uninstallCommands: [
|
|
3696
4832
|
{ argv: ["systemctl", "--user", "disable", "--now", SYSTEMD_UNIT], ignoreFailure: true },
|
|
3697
4833
|
{ argv: ["systemctl", "--user", "daemon-reload"], ignoreFailure: true }
|
|
3698
4834
|
],
|
|
3699
|
-
logHint:
|
|
4835
|
+
logHint: "dahrk logs -f"
|
|
3700
4836
|
};
|
|
3701
4837
|
}
|
|
4838
|
+
function unitIsCurrent(plan, onDisk) {
|
|
4839
|
+
return onDisk === plan.content;
|
|
4840
|
+
}
|
|
4841
|
+
function unitPath(manager, homeDir) {
|
|
4842
|
+
return manager === "launchd" ? join13(homeDir, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`) : join13(homeDir, ".config", "systemd", "user", SYSTEMD_UNIT);
|
|
4843
|
+
}
|
|
4844
|
+
function statusCommand(manager) {
|
|
4845
|
+
return manager === "launchd" ? ["launchctl", "list", LAUNCHD_LABEL] : ["systemctl", "--user", "show", SYSTEMD_UNIT, "-p", "ActiveState", "-p", "MainPID"];
|
|
4846
|
+
}
|
|
4847
|
+
function parseServiceStatus(manager, unitExists, probe2) {
|
|
4848
|
+
if (!unitExists) return { installed: false, running: false };
|
|
4849
|
+
if (probe2.code !== 0) return { installed: true, running: false };
|
|
4850
|
+
if (manager === "launchd") {
|
|
4851
|
+
const pid2 = Number(/"PID"\s*=\s*(\d+);/.exec(probe2.stdout)?.[1]);
|
|
4852
|
+
return pid2 > 0 ? { installed: true, running: true, pid: pid2 } : { installed: true, running: false };
|
|
4853
|
+
}
|
|
4854
|
+
const active = /^ActiveState=(.*)$/m.exec(probe2.stdout)?.[1]?.trim();
|
|
4855
|
+
const pid = Number(/^MainPID=(\d+)$/m.exec(probe2.stdout)?.[1]);
|
|
4856
|
+
const running = active === "active";
|
|
4857
|
+
return running && pid > 0 ? { installed: true, running: true, pid } : { installed: true, running };
|
|
4858
|
+
}
|
|
3702
4859
|
function printUnsupported(out) {
|
|
3703
4860
|
out("dahrk service is supported on macOS (launchd) and Linux (systemd) only.");
|
|
3704
4861
|
out("On other platforms, run the node under a process manager such as pm2 (see the README).");
|
|
@@ -3737,7 +4894,7 @@ async function runServiceInstall(inputs, deps = {}) {
|
|
|
3737
4894
|
logDir: d.logDir
|
|
3738
4895
|
});
|
|
3739
4896
|
try {
|
|
3740
|
-
|
|
4897
|
+
d.mkdirp(d.logDir);
|
|
3741
4898
|
d.mkdirp(dirOf(plan.filePath));
|
|
3742
4899
|
d.writeFile(plan.filePath, plan.content);
|
|
3743
4900
|
} catch (e) {
|
|
@@ -3757,6 +4914,88 @@ async function runServiceInstall(inputs, deps = {}) {
|
|
|
3757
4914
|
d.out(" uninstall: dahrk service uninstall");
|
|
3758
4915
|
return 0;
|
|
3759
4916
|
}
|
|
4917
|
+
function availabilityCommand(manager) {
|
|
4918
|
+
return manager === "systemd" ? ["systemctl", "--user", "show", "-p", "Version"] : void 0;
|
|
4919
|
+
}
|
|
4920
|
+
async function runNodeStart(inputs, deps = {}) {
|
|
4921
|
+
const d = { ...defaultDeps3(), ...deps };
|
|
4922
|
+
const manager = detectManager(d.platform);
|
|
4923
|
+
if (manager === "unsupported") {
|
|
4924
|
+
return { kind: "foreground", reason: "no supported supervisor on this platform (launchd / systemd)" };
|
|
4925
|
+
}
|
|
4926
|
+
const probeCmd = availabilityCommand(manager);
|
|
4927
|
+
if (probeCmd && d.capture(probeCmd).code !== 0) {
|
|
4928
|
+
return { kind: "foreground", reason: "no systemd user session on this host (a container, typically)" };
|
|
4929
|
+
}
|
|
4930
|
+
const filePath = unitPath(manager, d.homeDir);
|
|
4931
|
+
const unitExists = d.fileExists(filePath);
|
|
4932
|
+
if (!inputs.token && !unitExists) {
|
|
4933
|
+
d.out("No enrolment token: pass --token <token> or set DAHRK_ENROL_TOKEN.");
|
|
4934
|
+
d.out("Get one at https://app.dahrk.ai.");
|
|
4935
|
+
return { kind: "error", code: 2 };
|
|
4936
|
+
}
|
|
4937
|
+
const plan = buildPlan({
|
|
4938
|
+
manager,
|
|
4939
|
+
nodeBin: d.nodeBin,
|
|
4940
|
+
scriptPath: d.scriptPath,
|
|
4941
|
+
token: inputs.token ?? "-",
|
|
4942
|
+
...inputs.name ? { name: inputs.name } : {},
|
|
4943
|
+
...inputs.hubUrl ? { hubUrl: inputs.hubUrl } : {},
|
|
4944
|
+
...d.pathEnv ? { pathEnv: d.pathEnv } : {},
|
|
4945
|
+
homeDir: d.homeDir,
|
|
4946
|
+
logDir: d.logDir
|
|
4947
|
+
});
|
|
4948
|
+
const canRender = inputs.token !== void 0;
|
|
4949
|
+
const current = canRender && unitExists && unitIsCurrent(plan, d.readFile(filePath));
|
|
4950
|
+
const probe2 = unitExists ? d.capture(statusCommand(manager)) : { code: 1, stdout: "" };
|
|
4951
|
+
const status = parseServiceStatus(manager, unitExists, probe2);
|
|
4952
|
+
if (current && status.running) {
|
|
4953
|
+
d.out(`Node is already running${status.pid ? ` (pid ${status.pid})` : ""}.`);
|
|
4954
|
+
d.out(` logs: ${plan.logHint}`);
|
|
4955
|
+
return { kind: "running", code: 0 };
|
|
4956
|
+
}
|
|
4957
|
+
if (canRender && !current) {
|
|
4958
|
+
try {
|
|
4959
|
+
d.mkdirp(d.logDir);
|
|
4960
|
+
d.mkdirp(dirOf(plan.filePath));
|
|
4961
|
+
d.writeFile(plan.filePath, plan.content);
|
|
4962
|
+
} catch (e) {
|
|
4963
|
+
d.out(`Could not write the service file at ${plan.filePath}: ${e.message}`);
|
|
4964
|
+
return { kind: "error", code: 1 };
|
|
4965
|
+
}
|
|
4966
|
+
d.out(unitExists ? `Updated ${plan.manager} service: ${plan.filePath}` : `Installed ${plan.manager} service: ${plan.filePath}`);
|
|
4967
|
+
}
|
|
4968
|
+
const code = runCommands(plan.installCommands, d);
|
|
4969
|
+
if (code !== 0) {
|
|
4970
|
+
d.out(`The service file is in place but starting it failed (exit ${code}).`);
|
|
4971
|
+
return { kind: "error", code };
|
|
4972
|
+
}
|
|
4973
|
+
d.out("Node is running. It will start on boot and restart on failure.");
|
|
4974
|
+
d.out(` logs: ${plan.logHint}`);
|
|
4975
|
+
d.out(" stop: dahrk stop");
|
|
4976
|
+
return { kind: "running", code: 0 };
|
|
4977
|
+
}
|
|
4978
|
+
async function runNodeStop(deps = {}) {
|
|
4979
|
+
const d = { ...defaultDeps3(), ...deps };
|
|
4980
|
+
const manager = detectManager(d.platform);
|
|
4981
|
+
if (manager === "unsupported") return printUnsupported(d.out);
|
|
4982
|
+
const plan = buildPlan({
|
|
4983
|
+
manager,
|
|
4984
|
+
nodeBin: d.nodeBin,
|
|
4985
|
+
scriptPath: d.scriptPath,
|
|
4986
|
+
token: "-",
|
|
4987
|
+
homeDir: d.homeDir,
|
|
4988
|
+
logDir: d.logDir
|
|
4989
|
+
});
|
|
4990
|
+
if (!d.fileExists(plan.filePath)) {
|
|
4991
|
+
d.out("No service installed, so there is nothing to stop.");
|
|
4992
|
+
d.out("If you are running a node in a terminal (`dahrk start --foreground`), stop it with Ctrl-C.");
|
|
4993
|
+
return 0;
|
|
4994
|
+
}
|
|
4995
|
+
runCommands(plan.stopCommands, d);
|
|
4996
|
+
d.out("Node stopped. It will stay stopped across reboots until you run `dahrk start`.");
|
|
4997
|
+
return 0;
|
|
4998
|
+
}
|
|
3760
4999
|
async function runServiceUninstall(deps = {}) {
|
|
3761
5000
|
const d = { ...defaultDeps3(), ...deps };
|
|
3762
5001
|
d.out("dahrk service uninstall");
|
|
@@ -3792,41 +5031,83 @@ function dirOf(path) {
|
|
|
3792
5031
|
function resolveScriptPath() {
|
|
3793
5032
|
const argv1 = process.argv[1] ?? "";
|
|
3794
5033
|
try {
|
|
3795
|
-
return
|
|
5034
|
+
return realpathSync2(argv1);
|
|
3796
5035
|
} catch {
|
|
3797
5036
|
return argv1;
|
|
3798
5037
|
}
|
|
3799
5038
|
}
|
|
5039
|
+
function stableNodeBin(execPath, realpath) {
|
|
5040
|
+
const m = /^(?<prefix>.*)\/Cellar\/(?<formula>node(?:@[\d.]+)?)\/[^/]+\/bin\/node$/.exec(execPath);
|
|
5041
|
+
const { prefix, formula } = m?.groups ?? {};
|
|
5042
|
+
if (!prefix || !formula) return execPath;
|
|
5043
|
+
const candidates = [`${prefix}/opt/${formula}/bin/node`, `${prefix}/bin/node`];
|
|
5044
|
+
return candidates.find((c) => realpath(c) === execPath) ?? execPath;
|
|
5045
|
+
}
|
|
5046
|
+
var realpathOrUndefined = (p) => {
|
|
5047
|
+
try {
|
|
5048
|
+
return realpathSync2(p);
|
|
5049
|
+
} catch {
|
|
5050
|
+
return void 0;
|
|
5051
|
+
}
|
|
5052
|
+
};
|
|
3800
5053
|
var defaultDeps3 = () => ({
|
|
3801
5054
|
platform: osPlatform3(),
|
|
3802
|
-
homeDir:
|
|
3803
|
-
|
|
5055
|
+
homeDir: homedir4(),
|
|
5056
|
+
// Not `process.execPath` raw: that is the versioned Homebrew Cellar path, which the next
|
|
5057
|
+
// `brew upgrade node` deletes out from under the unit. See `stableNodeBin`.
|
|
5058
|
+
nodeBin: stableNodeBin(process.execPath, realpathOrUndefined),
|
|
3804
5059
|
scriptPath: resolveScriptPath(),
|
|
3805
|
-
logDir:
|
|
5060
|
+
logDir: logDir(process.env),
|
|
3806
5061
|
// Snapshot the operator's PATH at install time so the daemon finds git + the runtime CLIs (Homebrew /
|
|
3807
5062
|
// npm-global bins) that a supervisor's minimal PATH would otherwise hide.
|
|
3808
5063
|
pathEnv: process.env.PATH,
|
|
3809
|
-
mkdirp: (dir) => void
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
5064
|
+
mkdirp: (dir) => void mkdirSync11(dir, { recursive: true }),
|
|
5065
|
+
// The unit's environment block carries the enrolment token, so the file is a secret: write it
|
|
5066
|
+
// owner-only. `writeFileSync`'s `mode` applies only when it CREATES the file, so chmod explicitly
|
|
5067
|
+
// too - re-installing over a unit an older client left at 0644 must tighten it, not keep it.
|
|
5068
|
+
writeFile: (path, content) => {
|
|
5069
|
+
writeFileSync10(path, content, { mode: UNIT_FILE_MODE });
|
|
5070
|
+
chmodSync2(path, UNIT_FILE_MODE);
|
|
5071
|
+
},
|
|
5072
|
+
readFile: (path) => {
|
|
5073
|
+
try {
|
|
5074
|
+
return readFileSync9(path, "utf8");
|
|
5075
|
+
} catch {
|
|
5076
|
+
return void 0;
|
|
5077
|
+
}
|
|
5078
|
+
},
|
|
5079
|
+
removeFile: (path) => rmSync7(path, { force: true }),
|
|
5080
|
+
fileExists: (path) => existsSync11(path),
|
|
3813
5081
|
run: (argv) => {
|
|
3814
5082
|
const [cmd, ...args] = argv;
|
|
3815
5083
|
try {
|
|
3816
|
-
|
|
5084
|
+
execFileSync6(cmd, args, { stdio: "inherit" });
|
|
3817
5085
|
return 0;
|
|
3818
5086
|
} catch (e) {
|
|
3819
5087
|
const status = e.status;
|
|
3820
5088
|
return typeof status === "number" ? status : 1;
|
|
3821
5089
|
}
|
|
3822
5090
|
},
|
|
5091
|
+
capture: (argv) => {
|
|
5092
|
+
const [cmd, ...args] = argv;
|
|
5093
|
+
try {
|
|
5094
|
+
const stdout = execFileSync6(cmd, args, {
|
|
5095
|
+
encoding: "utf8",
|
|
5096
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
5097
|
+
});
|
|
5098
|
+
return { code: 0, stdout };
|
|
5099
|
+
} catch (e) {
|
|
5100
|
+
const status = e.status;
|
|
5101
|
+
return { code: typeof status === "number" ? status : 1, stdout: "" };
|
|
5102
|
+
}
|
|
5103
|
+
},
|
|
3823
5104
|
out: (line) => void process.stdout.write(`${line}
|
|
3824
5105
|
`)
|
|
3825
5106
|
});
|
|
3826
5107
|
|
|
3827
5108
|
// src/update.ts
|
|
3828
|
-
import { execFileSync as
|
|
3829
|
-
import { realpathSync as
|
|
5109
|
+
import { execFileSync as execFileSync7 } from "child_process";
|
|
5110
|
+
import { realpathSync as realpathSync3 } from "fs";
|
|
3830
5111
|
var LATEST_URL = "https://registry.npmjs.org/dahrk-node/latest";
|
|
3831
5112
|
var CHANNEL_COMMANDS = {
|
|
3832
5113
|
npm: "npm install -g dahrk-node@latest",
|
|
@@ -3847,7 +5128,7 @@ function detectChannel(binPath) {
|
|
|
3847
5128
|
if (!binPath) return "unknown";
|
|
3848
5129
|
let resolved = binPath;
|
|
3849
5130
|
try {
|
|
3850
|
-
resolved =
|
|
5131
|
+
resolved = realpathSync3(binPath);
|
|
3851
5132
|
} catch {
|
|
3852
5133
|
}
|
|
3853
5134
|
if (/[\\/]node_modules[\\/]dahrk-node[\\/]/.test(resolved)) return "npm";
|
|
@@ -3922,8 +5203,11 @@ async function runUpdate(inputs, deps = {}) {
|
|
|
3922
5203
|
}
|
|
3923
5204
|
return code;
|
|
3924
5205
|
}
|
|
3925
|
-
async function fetchLatestVersion() {
|
|
3926
|
-
const res = await fetch(LATEST_URL, {
|
|
5206
|
+
async function fetchLatestVersion(signal) {
|
|
5207
|
+
const res = await fetch(LATEST_URL, {
|
|
5208
|
+
headers: { accept: "application/json" },
|
|
5209
|
+
...signal ? { signal } : {}
|
|
5210
|
+
});
|
|
3927
5211
|
if (!res.ok) throw new Error(`registry responded ${res.status}`);
|
|
3928
5212
|
const body = await res.json();
|
|
3929
5213
|
if (typeof body.version !== "string" || !body.version) throw new Error("registry returned no version");
|
|
@@ -3932,7 +5216,7 @@ async function fetchLatestVersion() {
|
|
|
3932
5216
|
function spawnUpgrade(argv) {
|
|
3933
5217
|
const [cmd, ...args] = argv;
|
|
3934
5218
|
try {
|
|
3935
|
-
|
|
5219
|
+
execFileSync7(cmd, args, { stdio: "inherit" });
|
|
3936
5220
|
return 0;
|
|
3937
5221
|
} catch (e) {
|
|
3938
5222
|
const status = e.status;
|
|
@@ -3947,47 +5231,150 @@ var defaultDeps4 = () => ({
|
|
|
3947
5231
|
`)
|
|
3948
5232
|
});
|
|
3949
5233
|
|
|
5234
|
+
// src/update-check.ts
|
|
5235
|
+
var DEFAULT_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
5236
|
+
var FETCH_TIMEOUT_MS = 1500;
|
|
5237
|
+
function checkSuppressed(env) {
|
|
5238
|
+
return Boolean(env.DAHRK_NO_UPDATE_CHECK || env.NO_UPDATE_NOTIFIER || env.CI);
|
|
5239
|
+
}
|
|
5240
|
+
function checkIntervalMs(env) {
|
|
5241
|
+
const raw = env.DAHRK_UPDATE_CHECK_INTERVAL_MS;
|
|
5242
|
+
if (raw === void 0) return DEFAULT_INTERVAL_MS;
|
|
5243
|
+
const n = Number(raw);
|
|
5244
|
+
return Number.isFinite(n) && n >= 0 ? n : DEFAULT_INTERVAL_MS;
|
|
5245
|
+
}
|
|
5246
|
+
function shouldCheck(now, checkedAt, intervalMs, env) {
|
|
5247
|
+
if (checkSuppressed(env)) return false;
|
|
5248
|
+
if (!checkedAt) return true;
|
|
5249
|
+
const last = Date.parse(checkedAt);
|
|
5250
|
+
if (!Number.isFinite(last)) return true;
|
|
5251
|
+
return now - last >= intervalMs || last > now;
|
|
5252
|
+
}
|
|
5253
|
+
function jitterMs(random, spreadMs = 60 * 60 * 1e3) {
|
|
5254
|
+
return Math.floor(random * spreadMs);
|
|
5255
|
+
}
|
|
5256
|
+
function renderUpdateNotice(u) {
|
|
5257
|
+
const cmd = upgradeCommand(u.channel);
|
|
5258
|
+
const how = cmd ? ` (${u.channel}: ${cmd.display})` : " (run `dahrk update`)";
|
|
5259
|
+
return `update available: ${u.current} -> ${u.latest}${how}`;
|
|
5260
|
+
}
|
|
5261
|
+
function renderUpdateLogLine(u) {
|
|
5262
|
+
return `UPDATE_AVAILABLE:${u.latest} current=${u.current}`;
|
|
5263
|
+
}
|
|
5264
|
+
async function checkForUpdate(currentVersion, deps) {
|
|
5265
|
+
if (checkSuppressed(deps.env)) return void 0;
|
|
5266
|
+
const state = deps.readState();
|
|
5267
|
+
if (!shouldCheck(deps.now(), state.updateCheckedAt, checkIntervalMs(deps.env), deps.env)) {
|
|
5268
|
+
return cachedUpdate(state, currentVersion, deps.binPath);
|
|
5269
|
+
}
|
|
5270
|
+
let latest;
|
|
5271
|
+
try {
|
|
5272
|
+
latest = await deps.fetchLatest(AbortSignal.timeout(FETCH_TIMEOUT_MS));
|
|
5273
|
+
} catch {
|
|
5274
|
+
return void 0;
|
|
5275
|
+
}
|
|
5276
|
+
deps.saveResult({ updateCheckedAt: new Date(deps.now()).toISOString(), updateLatest: latest });
|
|
5277
|
+
if (!isNewer(latest, currentVersion)) return void 0;
|
|
5278
|
+
return { current: currentVersion, latest, channel: detectChannel(deps.binPath) };
|
|
5279
|
+
}
|
|
5280
|
+
function cachedUpdate(state, currentVersion, binPath) {
|
|
5281
|
+
const latest = state.updateLatest;
|
|
5282
|
+
if (!latest || !isNewer(latest, currentVersion)) return void 0;
|
|
5283
|
+
return { current: currentVersion, latest, channel: detectChannel(binPath) };
|
|
5284
|
+
}
|
|
5285
|
+
|
|
5286
|
+
// src/status.ts
|
|
5287
|
+
var bullet = (label, value) => ` ${label.padEnd(12)}${value}`;
|
|
5288
|
+
function renderStatus(f) {
|
|
5289
|
+
const lines = ["dahrk status", ""];
|
|
5290
|
+
const { nodeId, name, tenantId, enrolToken } = f.state;
|
|
5291
|
+
if (enrolToken) {
|
|
5292
|
+
lines.push(bullet("Enrolled", name ? `yes, as ${name}` : "yes"));
|
|
5293
|
+
if (tenantId) lines.push(bullet("Tenant", tenantId));
|
|
5294
|
+
} else if (f.envToken) {
|
|
5295
|
+
lines.push(bullet("Enrolled", "yes, via DAHRK_ENROL_TOKEN (caches on the next successful start)"));
|
|
5296
|
+
} else {
|
|
5297
|
+
lines.push(bullet("Enrolled", "no - run `dahrk start --token <token>` once to enrol"));
|
|
5298
|
+
}
|
|
5299
|
+
lines.push(bullet("Node id", nodeId ?? "not yet minted (first `dahrk start` mints one)"));
|
|
5300
|
+
lines.push(
|
|
5301
|
+
bullet(
|
|
5302
|
+
"Client",
|
|
5303
|
+
f.update ? `${f.clientVersion} (update available: ${f.update.latest} - run \`dahrk update\`)` : f.clientVersion
|
|
5304
|
+
)
|
|
5305
|
+
);
|
|
5306
|
+
lines.push(bullet("Hub", f.hubUrl));
|
|
5307
|
+
lines.push(
|
|
5308
|
+
bullet(
|
|
5309
|
+
"Runtimes",
|
|
5310
|
+
f.runtimes.length > 0 ? f.runtimes.join(", ") : "none detected - this node will serve no Jobs (install claude / codex / pi)"
|
|
5311
|
+
)
|
|
5312
|
+
);
|
|
5313
|
+
if (!f.service) {
|
|
5314
|
+
lines.push(bullet("Node", "no supervisor on this host - run `dahrk start --foreground` (or under pm2)"));
|
|
5315
|
+
} else if (!f.service.installed) {
|
|
5316
|
+
lines.push(bullet("Node", "not installed - run `dahrk start` to run it always-on"));
|
|
5317
|
+
} else if (f.service.running) {
|
|
5318
|
+
const pid = f.service.pid ? ` (pid ${f.service.pid})` : "";
|
|
5319
|
+
lines.push(bullet("Node", `running${pid}`));
|
|
5320
|
+
} else if (f.state.desired === "stopped") {
|
|
5321
|
+
lines.push(bullet("Node", "stopped - run `dahrk start` to bring it back"));
|
|
5322
|
+
} else {
|
|
5323
|
+
lines.push(bullet("Node", "INSTALLED BUT NOT RUNNING - it is failing to start or crash-looping"));
|
|
5324
|
+
if (f.logHint) lines.push(bullet("", `check the logs: ${f.logHint}`));
|
|
5325
|
+
}
|
|
5326
|
+
lines.push("", `State file: ${f.stateFile}`);
|
|
5327
|
+
return lines;
|
|
5328
|
+
}
|
|
5329
|
+
function isUnhealthy(f) {
|
|
5330
|
+
return Boolean(f.service?.installed && !f.service.running && f.state.desired !== "stopped");
|
|
5331
|
+
}
|
|
5332
|
+
async function runStatus(inputs, deps) {
|
|
5333
|
+
const manager = detectManager(deps.platform);
|
|
5334
|
+
let service;
|
|
5335
|
+
let logHint;
|
|
5336
|
+
if (manager !== "unsupported") {
|
|
5337
|
+
const unit = unitPath(manager, deps.homeDir);
|
|
5338
|
+
const exists = deps.fileExists(unit);
|
|
5339
|
+
const probe2 = exists ? deps.capture(statusCommand(manager)) : { code: 1, stdout: "" };
|
|
5340
|
+
service = parseServiceStatus(manager, exists, probe2);
|
|
5341
|
+
logHint = "dahrk logs -f";
|
|
5342
|
+
}
|
|
5343
|
+
const state = readState(stateFile(deps.env));
|
|
5344
|
+
const update = checkSuppressed(deps.env) ? void 0 : cachedUpdate(state, inputs.clientVersion, deps.binPath);
|
|
5345
|
+
const facts = {
|
|
5346
|
+
clientVersion: inputs.clientVersion,
|
|
5347
|
+
hubUrl: inputs.hubUrl,
|
|
5348
|
+
stateFile: stateFile(deps.env),
|
|
5349
|
+
state,
|
|
5350
|
+
envToken: Boolean(deps.env.DAHRK_ENROL_TOKEN),
|
|
5351
|
+
runtimes: await deps.detectRuntimes(),
|
|
5352
|
+
...service ? { service } : {},
|
|
5353
|
+
...logHint ? { logHint } : {},
|
|
5354
|
+
...update ? { update } : {}
|
|
5355
|
+
};
|
|
5356
|
+
for (const line of renderStatus(facts)) deps.out(line);
|
|
5357
|
+
return isUnhealthy(facts) ? 1 : 0;
|
|
5358
|
+
}
|
|
5359
|
+
|
|
3950
5360
|
// src/main.ts
|
|
3951
|
-
var CLIENT_VERSION = "0.1.
|
|
5361
|
+
var CLIENT_VERSION = "0.1.9";
|
|
3952
5362
|
var DEFAULT_HUB_URL = "wss://api.dahrk.ai";
|
|
3953
5363
|
var list = (v) => (v ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
3954
5364
|
var RUNTIMES = ["claude-code", "codex", "pi"];
|
|
3955
5365
|
var isRuntime = (r) => RUNTIMES.includes(r);
|
|
3956
|
-
function stateDir(env) {
|
|
3957
|
-
return env.DAHRK_STATE_DIR ?? join9(homedir4(), ".dahrk");
|
|
3958
|
-
}
|
|
3959
|
-
function legacyStateDir(env) {
|
|
3960
|
-
return env.DAHRK_STATE_DIR ? void 0 : join9(homedir4(), ".skakel");
|
|
3961
|
-
}
|
|
3962
|
-
function readNodeId(file) {
|
|
3963
|
-
if (!existsSync6(file)) return void 0;
|
|
3964
|
-
try {
|
|
3965
|
-
const parsed = JSON.parse(readFileSync5(file, "utf8"));
|
|
3966
|
-
if (typeof parsed.nodeId === "string" && parsed.nodeId) return parsed.nodeId;
|
|
3967
|
-
} catch {
|
|
3968
|
-
}
|
|
3969
|
-
return void 0;
|
|
3970
|
-
}
|
|
3971
5366
|
function resolveNodeId(env, opts = {}) {
|
|
3972
5367
|
if (env.DAHRK_NODE_ID) return env.DAHRK_NODE_ID;
|
|
3973
5368
|
if (opts.ephemeral) return randomUUID3();
|
|
3974
|
-
const
|
|
3975
|
-
const file = join9(dir, "node.json");
|
|
3976
|
-
const existing = readNodeId(file);
|
|
5369
|
+
const existing = readState(stateFile(env)).nodeId;
|
|
3977
5370
|
if (existing) return existing;
|
|
3978
5371
|
const legacy = legacyStateDir(env);
|
|
3979
5372
|
if (legacy) {
|
|
3980
|
-
const legacyId =
|
|
5373
|
+
const legacyId = readState(join14(legacy, "node.json")).nodeId;
|
|
3981
5374
|
if (legacyId) return legacyId;
|
|
3982
5375
|
}
|
|
3983
5376
|
const nodeId = randomUUID3();
|
|
3984
|
-
|
|
3985
|
-
mkdirSync7(dir, { recursive: true });
|
|
3986
|
-
writeFileSync7(file, `${JSON.stringify({ nodeId }, null, 2)}
|
|
3987
|
-
`);
|
|
3988
|
-
} catch (e) {
|
|
3989
|
-
console.warn(`could not persist node id to ${file}: ${e.message}`);
|
|
3990
|
-
}
|
|
5377
|
+
writeState(env, { nodeId });
|
|
3991
5378
|
return nodeId;
|
|
3992
5379
|
}
|
|
3993
5380
|
async function resolveRuntimes(env) {
|
|
@@ -4050,17 +5437,136 @@ function envWithFlags(env, flags) {
|
|
|
4050
5437
|
if (flags.hubUrl) merged.DAHRK_HUB_URL = flags.hubUrl;
|
|
4051
5438
|
return merged;
|
|
4052
5439
|
}
|
|
5440
|
+
function wantsForeground(env, flags) {
|
|
5441
|
+
return flags.foreground || env.DAHRK_FOREGROUND === "1" || env.DAHRK_SUPERVISED === "1";
|
|
5442
|
+
}
|
|
4053
5443
|
async function start(flags) {
|
|
4054
5444
|
const env = envWithFlags(process.env, flags);
|
|
5445
|
+
if (wantsForeground(env, flags)) return startForeground(env, flags);
|
|
5446
|
+
await offerUpdate(env);
|
|
5447
|
+
const outcome = await runNodeStart({
|
|
5448
|
+
...resolveEnrolToken(env) ? { token: resolveEnrolToken(env) } : {},
|
|
5449
|
+
...env.DAHRK_NODE_NAME ? { name: env.DAHRK_NODE_NAME } : {},
|
|
5450
|
+
...env.DAHRK_HUB_URL ? { hubUrl: env.DAHRK_HUB_URL } : {}
|
|
5451
|
+
});
|
|
5452
|
+
if (outcome.kind === "error") return outcome.code;
|
|
5453
|
+
if (outcome.kind === "running") {
|
|
5454
|
+
setDesired(env, "running");
|
|
5455
|
+
return 0;
|
|
5456
|
+
}
|
|
5457
|
+
console.warn(`${outcome.reason}; running the node in this terminal instead.`);
|
|
5458
|
+
console.warn("Use `dahrk start --foreground` (or DAHRK_FOREGROUND=1) to ask for this explicitly.");
|
|
5459
|
+
return startForeground(env, flags);
|
|
5460
|
+
}
|
|
5461
|
+
async function startForeground(env, flags) {
|
|
5462
|
+
if (!flags.ephemeral) {
|
|
5463
|
+
const lock = acquireLock(defaultLockDeps(lockFile(env)));
|
|
5464
|
+
if (!lock.ok) {
|
|
5465
|
+
console.error(`A node is already running on this host (pid ${lock.heldBy}).`);
|
|
5466
|
+
console.error("Follow it with `dahrk logs -f`, or stop it with `dahrk stop` first.");
|
|
5467
|
+
return 1;
|
|
5468
|
+
}
|
|
5469
|
+
process.on("exit", lock.release);
|
|
5470
|
+
}
|
|
5471
|
+
const files = logFiles(env);
|
|
5472
|
+
rotateIfLarge(files.out);
|
|
5473
|
+
rotateIfLarge(files.err);
|
|
4055
5474
|
const nodeId = resolveNodeId(env, { ephemeral: flags.ephemeral });
|
|
5475
|
+
const logger = createNodeLoggerFromEnv(env, logDir(env), { nodeId, clientVersion: CLIENT_VERSION });
|
|
5476
|
+
installProcessSafetyNet({
|
|
5477
|
+
logger,
|
|
5478
|
+
crashDir: crashDir(env),
|
|
5479
|
+
clientVersion: CLIENT_VERSION,
|
|
5480
|
+
...env.DAHRK_CRASH_EXIT === "1" ? { exitOnCrash: true } : {}
|
|
5481
|
+
});
|
|
5482
|
+
const token = resolveEnrolToken(env, { ephemeral: flags.ephemeral });
|
|
5483
|
+
if (token) env.DAHRK_ENROL_TOKEN = token;
|
|
4056
5484
|
const runtimes = await resolveRuntimes(env);
|
|
4057
5485
|
if (runtimes.length === 0) {
|
|
4058
5486
|
console.warn(
|
|
4059
5487
|
"no agent runtimes detected on this host (claude/codex/pi not on PATH); the node will advertise none and serve no Jobs. Install a runtime or set DAHRK_RUNTIMES to override. Run `dahrk doctor` to check."
|
|
4060
5488
|
);
|
|
4061
5489
|
}
|
|
5490
|
+
if (!flags.ephemeral) setDesired(env, "running");
|
|
5491
|
+
scheduleUpdateChecks(env);
|
|
4062
5492
|
const resolved = { nodeId, runtimes, clientVersion: CLIENT_VERSION };
|
|
4063
|
-
|
|
5493
|
+
const persist = token !== void 0 && !flags.ephemeral;
|
|
5494
|
+
await startEdgeNode({
|
|
5495
|
+
...buildEdgeOptions(env, resolved),
|
|
5496
|
+
logger,
|
|
5497
|
+
...persist ? {
|
|
5498
|
+
onEnrolled: (welcome) => persistEnrolment(env, { token, name: welcome.name, tenantId: welcome.tenantId })
|
|
5499
|
+
} : {}
|
|
5500
|
+
});
|
|
5501
|
+
return 0;
|
|
5502
|
+
}
|
|
5503
|
+
async function stop(env) {
|
|
5504
|
+
const code = await runNodeStop();
|
|
5505
|
+
if (code === 0) setDesired(env, "stopped");
|
|
5506
|
+
return code;
|
|
5507
|
+
}
|
|
5508
|
+
function updateCheckDeps(env) {
|
|
5509
|
+
return {
|
|
5510
|
+
env,
|
|
5511
|
+
now: () => Date.now(),
|
|
5512
|
+
binPath: process.argv[1],
|
|
5513
|
+
readState: () => readState(stateFile(env)),
|
|
5514
|
+
saveResult: (patch) => writeState(env, patch),
|
|
5515
|
+
fetchLatest: fetchLatestVersion
|
|
5516
|
+
};
|
|
5517
|
+
}
|
|
5518
|
+
var isInteractive = () => Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
5519
|
+
async function confirm(question) {
|
|
5520
|
+
const { createInterface } = await import("readline/promises");
|
|
5521
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
5522
|
+
try {
|
|
5523
|
+
const answer = (await rl.question(`${question} [Y/n] `)).trim().toLowerCase();
|
|
5524
|
+
return answer === "" || answer === "y" || answer === "yes";
|
|
5525
|
+
} finally {
|
|
5526
|
+
rl.close();
|
|
5527
|
+
}
|
|
5528
|
+
}
|
|
5529
|
+
async function offerUpdate(env) {
|
|
5530
|
+
const available = await checkForUpdate(CLIENT_VERSION, updateCheckDeps(env));
|
|
5531
|
+
if (!available) return;
|
|
5532
|
+
console.log(renderUpdateNotice(available));
|
|
5533
|
+
if (!isInteractive()) return;
|
|
5534
|
+
if (!await confirm("update now?")) return;
|
|
5535
|
+
const code = await runUpdate({ currentVersion: CLIENT_VERSION, check: false });
|
|
5536
|
+
if (code !== 0) console.warn("Update failed; starting the node on the current version anyway.");
|
|
5537
|
+
}
|
|
5538
|
+
function scheduleUpdateChecks(env) {
|
|
5539
|
+
if (checkSuppressed(env)) return;
|
|
5540
|
+
const deps = updateCheckDeps(env);
|
|
5541
|
+
const tick = async () => {
|
|
5542
|
+
const available = await checkForUpdate(CLIENT_VERSION, deps);
|
|
5543
|
+
if (available) process.stdout.write(`${renderUpdateLogLine(available)}
|
|
5544
|
+
`);
|
|
5545
|
+
};
|
|
5546
|
+
setTimeout(() => void tick(), jitterMs(Math.random())).unref();
|
|
5547
|
+
setInterval(() => void tick(), checkIntervalMs(env)).unref();
|
|
5548
|
+
}
|
|
5549
|
+
function statusDeps(env) {
|
|
5550
|
+
return {
|
|
5551
|
+
platform: osPlatform4(),
|
|
5552
|
+
homeDir: homedir5(),
|
|
5553
|
+
env,
|
|
5554
|
+
binPath: process.argv[1],
|
|
5555
|
+
detectRuntimes: () => resolveRuntimes(env),
|
|
5556
|
+
fileExists: (path) => existsSync12(path),
|
|
5557
|
+
capture: (argv) => {
|
|
5558
|
+
const [cmd, ...args] = argv;
|
|
5559
|
+
try {
|
|
5560
|
+
const stdout = execFileSync8(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
5561
|
+
return { code: 0, stdout };
|
|
5562
|
+
} catch (e) {
|
|
5563
|
+
const status = e.status;
|
|
5564
|
+
return { code: typeof status === "number" ? status : 1, stdout: "" };
|
|
5565
|
+
}
|
|
5566
|
+
},
|
|
5567
|
+
out: (line) => void process.stdout.write(`${line}
|
|
5568
|
+
`)
|
|
5569
|
+
};
|
|
4064
5570
|
}
|
|
4065
5571
|
var KNOWN_WORKFLOWS = ["preflight"];
|
|
4066
5572
|
async function runWorkflow(flags) {
|
|
@@ -4072,7 +5578,7 @@ async function runWorkflow(flags) {
|
|
|
4072
5578
|
}
|
|
4073
5579
|
const env = applyEnvAliases(process.env);
|
|
4074
5580
|
const hubUrl = flags.hubUrl ?? env.DAHRK_HUB_URL;
|
|
4075
|
-
const token = flags.token ?? env
|
|
5581
|
+
const token = flags.token ?? resolveEnrolToken(env);
|
|
4076
5582
|
return runPreflight({
|
|
4077
5583
|
...flags.repo ? { repoPath: flags.repo } : {},
|
|
4078
5584
|
...hubUrl ? { hubUrl } : {},
|
|
@@ -4081,7 +5587,7 @@ async function runWorkflow(flags) {
|
|
|
4081
5587
|
});
|
|
4082
5588
|
}
|
|
4083
5589
|
async function main() {
|
|
4084
|
-
const invoked =
|
|
5590
|
+
const invoked = basename2(process.argv[1] ?? "");
|
|
4085
5591
|
const bin = !invoked || invoked.startsWith("main.") ? "dahrk" : invoked;
|
|
4086
5592
|
const parsed = parseCli(process.argv.slice(2));
|
|
4087
5593
|
switch (parsed.kind) {
|
|
@@ -4101,11 +5607,21 @@ async function main() {
|
|
|
4101
5607
|
const env = envWithFlags(process.env, parsed.flags);
|
|
4102
5608
|
process.exitCode = await runDoctor({
|
|
4103
5609
|
hubUrl: env.DAHRK_HUB_URL,
|
|
4104
|
-
token:
|
|
5610
|
+
// Same resolution as `start`, so doctor checks the token the node would actually present:
|
|
5611
|
+
// the flag/env if given, else the one cached by the last successful enrolment.
|
|
5612
|
+
token: resolveEnrolToken(env),
|
|
4105
5613
|
clientVersion: CLIENT_VERSION
|
|
4106
5614
|
});
|
|
4107
5615
|
break;
|
|
4108
5616
|
}
|
|
5617
|
+
case "status": {
|
|
5618
|
+
const env = envWithFlags(process.env, parsed.flags);
|
|
5619
|
+
process.exitCode = await runStatus(
|
|
5620
|
+
{ clientVersion: CLIENT_VERSION, hubUrl: env.DAHRK_HUB_URL ?? DEFAULT_HUB_URL },
|
|
5621
|
+
statusDeps(env)
|
|
5622
|
+
);
|
|
5623
|
+
break;
|
|
5624
|
+
}
|
|
4109
5625
|
case "run":
|
|
4110
5626
|
process.exitCode = await runWorkflow(parsed.flags);
|
|
4111
5627
|
break;
|
|
@@ -4115,7 +5631,7 @@ async function main() {
|
|
|
4115
5631
|
break;
|
|
4116
5632
|
}
|
|
4117
5633
|
const env = applyEnvAliases(process.env);
|
|
4118
|
-
const token = parsed.flags.token ?? env
|
|
5634
|
+
const token = parsed.flags.token ?? resolveEnrolToken(env);
|
|
4119
5635
|
const name = parsed.flags.name ?? env.DAHRK_NODE_NAME;
|
|
4120
5636
|
const hubUrl = parsed.flags.hubUrl ?? env.DAHRK_HUB_URL;
|
|
4121
5637
|
process.exitCode = await runServiceInstall({
|
|
@@ -4129,22 +5645,84 @@ async function main() {
|
|
|
4129
5645
|
process.exitCode = await runUpdate({ currentVersion: CLIENT_VERSION, check: parsed.flags.check });
|
|
4130
5646
|
break;
|
|
4131
5647
|
case "start":
|
|
4132
|
-
await start(parsed.flags);
|
|
5648
|
+
process.exitCode = await start(parsed.flags);
|
|
5649
|
+
break;
|
|
5650
|
+
case "stop":
|
|
5651
|
+
process.exitCode = await stop(applyEnvAliases(process.env));
|
|
5652
|
+
break;
|
|
5653
|
+
case "restart": {
|
|
5654
|
+
const env = envWithFlags(process.env, parsed.flags);
|
|
5655
|
+
const code = await stop(env);
|
|
5656
|
+
if (code !== 0) {
|
|
5657
|
+
process.exitCode = code;
|
|
5658
|
+
break;
|
|
5659
|
+
}
|
|
5660
|
+
process.exitCode = await start(parsed.flags);
|
|
5661
|
+
break;
|
|
5662
|
+
}
|
|
5663
|
+
case "logs": {
|
|
5664
|
+
const env = applyEnvAliases(process.env);
|
|
5665
|
+
process.exitCode = await runLogs(parsed.flags, defaultLogsDeps(logFiles(env), jsonlLogFile(env)));
|
|
5666
|
+
break;
|
|
5667
|
+
}
|
|
5668
|
+
case "diagnose": {
|
|
5669
|
+
const env = applyEnvAliases(process.env);
|
|
5670
|
+
const doctorLines = [];
|
|
5671
|
+
const doctor = async () => {
|
|
5672
|
+
await runDoctor(
|
|
5673
|
+
{
|
|
5674
|
+
...resolveEnrolToken(env) ? { token: resolveEnrolToken(env) } : {},
|
|
5675
|
+
hubUrl: env.DAHRK_HUB_URL ?? DEFAULT_HUB_URL
|
|
5676
|
+
},
|
|
5677
|
+
{ out: (line) => void doctorLines.push(line) }
|
|
5678
|
+
);
|
|
5679
|
+
return doctorLines;
|
|
5680
|
+
};
|
|
5681
|
+
process.exitCode = await runDiagnose(
|
|
5682
|
+
defaultDiagnoseDeps(
|
|
5683
|
+
{
|
|
5684
|
+
stateFile: stateFile(env),
|
|
5685
|
+
jsonlFile: jsonlLogFile(env),
|
|
5686
|
+
crashDir: crashDir(env),
|
|
5687
|
+
outFile: parsed.flags.out ?? defaultBundlePath(process.cwd(), /* @__PURE__ */ new Date())
|
|
5688
|
+
},
|
|
5689
|
+
CLIENT_VERSION,
|
|
5690
|
+
doctor
|
|
5691
|
+
)
|
|
5692
|
+
);
|
|
4133
5693
|
break;
|
|
5694
|
+
}
|
|
4134
5695
|
}
|
|
4135
5696
|
}
|
|
4136
5697
|
var invokedAsEntrypoint = (() => {
|
|
4137
5698
|
const argv1 = process.argv[1];
|
|
4138
5699
|
if (!argv1) return false;
|
|
4139
5700
|
try {
|
|
4140
|
-
return pathToFileURL(
|
|
5701
|
+
return pathToFileURL(realpathSync4(argv1)).href === import.meta.url;
|
|
4141
5702
|
} catch {
|
|
4142
5703
|
return false;
|
|
4143
5704
|
}
|
|
4144
5705
|
})();
|
|
4145
5706
|
if (invokedAsEntrypoint) {
|
|
4146
5707
|
main().catch((err) => {
|
|
5708
|
+
const enrolmentRejected = process.exitCode === ENROLMENT_REJECTED_EXIT_CODE;
|
|
4147
5709
|
console.error(err instanceof Error ? err.message : String(err));
|
|
5710
|
+
if (!enrolmentRejected) {
|
|
5711
|
+
if (err instanceof Error && err.stack) console.error(err.stack);
|
|
5712
|
+
const env = applyEnvAliases(process.env);
|
|
5713
|
+
writeCrashRecord(crashDir(env), {
|
|
5714
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5715
|
+
kind: "uncaughtException",
|
|
5716
|
+
name: err instanceof Error ? err.name : typeof err,
|
|
5717
|
+
message: err instanceof Error ? err.message : String(err),
|
|
5718
|
+
...err instanceof Error && err.stack ? { stack: err.stack } : {},
|
|
5719
|
+
clientVersion: CLIENT_VERSION,
|
|
5720
|
+
nodeVersion: process.version,
|
|
5721
|
+
platform: process.platform,
|
|
5722
|
+
arch: process.arch,
|
|
5723
|
+
uptimeSec: Math.round(process.uptime())
|
|
5724
|
+
});
|
|
5725
|
+
}
|
|
4148
5726
|
process.exit(process.exitCode || 1);
|
|
4149
5727
|
});
|
|
4150
5728
|
}
|