dahrk-node 0.1.14 → 0.1.15
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 +571 -235
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/main.ts
|
|
4
4
|
import { execFileSync as execFileSync9 } from "child_process";
|
|
5
|
-
import { existsSync as existsSync14, realpathSync as realpathSync5 } from "fs";
|
|
5
|
+
import { existsSync as existsSync14, readFileSync as readFileSync11, realpathSync as realpathSync5 } from "fs";
|
|
6
6
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
7
7
|
import { homedir as homedir7, platform as osPlatform4 } from "os";
|
|
8
8
|
import { basename as basename2, join as join17 } from "path";
|
|
@@ -506,9 +506,9 @@ var userMsg = (text) => ({
|
|
|
506
506
|
});
|
|
507
507
|
var sessionIdOf = (msg) => "session_id" in msg && typeof msg.session_id === "string" ? msg.session_id : void 0;
|
|
508
508
|
var policyCanUseTool = async (ctx, toolName, input) => {
|
|
509
|
-
const
|
|
510
|
-
if (
|
|
511
|
-
return { behavior: "deny", message:
|
|
509
|
+
const verdict2 = ctx.authorizeToolUse?.(toolName, input);
|
|
510
|
+
if (verdict2?.verdict === "deny") {
|
|
511
|
+
return { behavior: "deny", message: verdict2.reason ?? `tool "${toolName}" denied by policy ${verdict2.policy}` };
|
|
512
512
|
}
|
|
513
513
|
return { behavior: "allow", updatedInput: input };
|
|
514
514
|
};
|
|
@@ -801,15 +801,15 @@ function createClaudeRunner() {
|
|
|
801
801
|
})
|
|
802
802
|
};
|
|
803
803
|
try {
|
|
804
|
-
let
|
|
804
|
+
let out2 = "";
|
|
805
805
|
const q = query({ prompt: SUMMARISE_PROMPT, options });
|
|
806
806
|
active = q;
|
|
807
807
|
for await (const msg of q) {
|
|
808
808
|
const found = sessionIdOf(msg);
|
|
809
809
|
if (found) sessionId = found;
|
|
810
|
-
if (msg.type === "result" && msg.subtype === "success")
|
|
810
|
+
if (msg.type === "result" && msg.subtype === "success") out2 += msg.result;
|
|
811
811
|
}
|
|
812
|
-
return
|
|
812
|
+
return out2.trim() || "(no summary produced)";
|
|
813
813
|
} catch (e) {
|
|
814
814
|
return `(summary unavailable: ${e.message})`;
|
|
815
815
|
} finally {
|
|
@@ -1311,15 +1311,15 @@ function createPiRunner(deps = {}) {
|
|
|
1311
1311
|
const s = session;
|
|
1312
1312
|
if (s.agent) s.agent.state.tools = [];
|
|
1313
1313
|
const state = newPiBufferState();
|
|
1314
|
-
let
|
|
1314
|
+
let out2;
|
|
1315
1315
|
const unsub = s.subscribe((ev) => {
|
|
1316
1316
|
const r = consumePiEvent(ev, state, true);
|
|
1317
|
-
if (r.responseText)
|
|
1317
|
+
if (r.responseText) out2 = r.responseText;
|
|
1318
1318
|
});
|
|
1319
1319
|
try {
|
|
1320
1320
|
await s.prompt(SUMMARISE_PROMPT);
|
|
1321
1321
|
captureSessionId(s);
|
|
1322
|
-
return (
|
|
1322
|
+
return (out2 ?? "").trim() || "(no summary produced)";
|
|
1323
1323
|
} catch (e) {
|
|
1324
1324
|
return `(summary unavailable: ${e.message})`;
|
|
1325
1325
|
} finally {
|
|
@@ -1482,15 +1482,15 @@ function createGitService(opts = {}) {
|
|
|
1482
1482
|
const withTokenUser = (gitUrl) => /^https:\/\/[^@/]+@/.test(gitUrl) ? gitUrl : gitUrl.replace(/^https:\/\//, "https://x-access-token@");
|
|
1483
1483
|
const mirrorPathFor = (repoId) => join3(mirrorsDir, sanitizeBranchName(repoId));
|
|
1484
1484
|
const listWorktrees = (mirror) => {
|
|
1485
|
-
let
|
|
1485
|
+
let out2;
|
|
1486
1486
|
try {
|
|
1487
|
-
|
|
1487
|
+
out2 = git(mirror, ["worktree", "list", "--porcelain"]);
|
|
1488
1488
|
} catch {
|
|
1489
1489
|
return [];
|
|
1490
1490
|
}
|
|
1491
1491
|
const entries = [];
|
|
1492
1492
|
let path = "";
|
|
1493
|
-
for (const line of
|
|
1493
|
+
for (const line of out2.split("\n")) {
|
|
1494
1494
|
if (line.startsWith("worktree ")) path = line.slice(9).trim();
|
|
1495
1495
|
else if (line.startsWith("branch ")) {
|
|
1496
1496
|
const branch = line.slice(7).trim().replace(/^refs\/heads\//, "");
|
|
@@ -1848,13 +1848,13 @@ function lastUsedMs(worktreePath) {
|
|
|
1848
1848
|
return newest;
|
|
1849
1849
|
}
|
|
1850
1850
|
function registeredWorktrees(mirror) {
|
|
1851
|
-
let
|
|
1851
|
+
let out2;
|
|
1852
1852
|
try {
|
|
1853
|
-
|
|
1853
|
+
out2 = gitOut(mirror, ["worktree", "list", "--porcelain"]);
|
|
1854
1854
|
} catch {
|
|
1855
1855
|
return [];
|
|
1856
1856
|
}
|
|
1857
|
-
return
|
|
1857
|
+
return out2.split("\n").filter((l) => l.startsWith("worktree ")).map((l) => canonical(l.slice(9).trim())).filter((p) => p && p !== canonical(mirror));
|
|
1858
1858
|
}
|
|
1859
1859
|
function createWorktreeReaper(opts) {
|
|
1860
1860
|
const log = opts.logger ?? noop;
|
|
@@ -2015,16 +2015,16 @@ import { createHash as createHash2 } from "crypto";
|
|
|
2015
2015
|
import { cpSync, existsSync as existsSync3, mkdirSync as mkdirSync3, mkdtempSync as mkdtempSync2, readdirSync as readdirSync2, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
2016
2016
|
import { dirname as dirname2, join as join6, relative, sep } from "path";
|
|
2017
2017
|
function readManifestFiles(dir) {
|
|
2018
|
-
const
|
|
2018
|
+
const out2 = [];
|
|
2019
2019
|
const walk = (cur) => {
|
|
2020
2020
|
for (const entry of readdirSync2(cur, { withFileTypes: true })) {
|
|
2021
2021
|
const abs = join6(cur, entry.name);
|
|
2022
2022
|
if (entry.isDirectory()) walk(abs);
|
|
2023
|
-
else
|
|
2023
|
+
else out2.push(relative(dir, abs).split(sep).join("/"));
|
|
2024
2024
|
}
|
|
2025
2025
|
};
|
|
2026
2026
|
walk(dir);
|
|
2027
|
-
return
|
|
2027
|
+
return out2.sort();
|
|
2028
2028
|
}
|
|
2029
2029
|
|
|
2030
2030
|
// ../../packages/executor-worktree/src/overlay.ts
|
|
@@ -2195,11 +2195,11 @@ function fileJobLedger(file, warn = console.warn) {
|
|
|
2195
2195
|
};
|
|
2196
2196
|
}
|
|
2197
2197
|
function announceableJobs(entries) {
|
|
2198
|
-
const
|
|
2198
|
+
const out2 = [];
|
|
2199
2199
|
for (const e of entries) {
|
|
2200
|
-
if (e.payloadVersion)
|
|
2200
|
+
if (e.payloadVersion) out2.push({ jobId: e.jobId, payloadVersion: e.payloadVersion });
|
|
2201
2201
|
}
|
|
2202
|
-
return
|
|
2202
|
+
return out2;
|
|
2203
2203
|
}
|
|
2204
2204
|
function jobLedgerFile(stateDir2) {
|
|
2205
2205
|
return join8(stateDir2, "jobs.json");
|
|
@@ -2357,18 +2357,18 @@ function scrubValue(value, depth = 0) {
|
|
|
2357
2357
|
}
|
|
2358
2358
|
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return value;
|
|
2359
2359
|
if (value instanceof Error) {
|
|
2360
|
-
const
|
|
2361
|
-
|
|
2362
|
-
if (value.stack)
|
|
2363
|
-
return
|
|
2360
|
+
const out2 = new Error(scrubString(value.message));
|
|
2361
|
+
out2.name = value.name;
|
|
2362
|
+
if (value.stack) out2.stack = scrubString(value.stack);
|
|
2363
|
+
return out2;
|
|
2364
2364
|
}
|
|
2365
2365
|
if (Array.isArray(value)) return value.map((v) => scrubValue(v, depth + 1));
|
|
2366
2366
|
if (typeof value === "object") {
|
|
2367
|
-
const
|
|
2367
|
+
const out2 = {};
|
|
2368
2368
|
for (const [k, v] of Object.entries(value)) {
|
|
2369
|
-
|
|
2369
|
+
out2[k] = isSensitiveKey(k) ? REDACTED : scrubValue(v, depth + 1);
|
|
2370
2370
|
}
|
|
2371
|
-
return
|
|
2371
|
+
return out2;
|
|
2372
2372
|
}
|
|
2373
2373
|
return value;
|
|
2374
2374
|
}
|
|
@@ -2602,11 +2602,11 @@ function withinRoots(raw, roots, need) {
|
|
|
2602
2602
|
}
|
|
2603
2603
|
function gitCommonDir(worktreePath) {
|
|
2604
2604
|
try {
|
|
2605
|
-
const
|
|
2605
|
+
const out2 = execFileSync3("git", ["rev-parse", "--path-format=absolute", "--git-common-dir"], {
|
|
2606
2606
|
cwd: worktreePath,
|
|
2607
2607
|
stdio: ["ignore", "pipe", "ignore"]
|
|
2608
2608
|
}).toString().trim();
|
|
2609
|
-
return
|
|
2609
|
+
return out2 || void 0;
|
|
2610
2610
|
} catch {
|
|
2611
2611
|
return void 0;
|
|
2612
2612
|
}
|
|
@@ -2615,8 +2615,8 @@ var pnpmStoreCache;
|
|
|
2615
2615
|
function pnpmStore() {
|
|
2616
2616
|
if (pnpmStoreCache) return pnpmStoreCache.path;
|
|
2617
2617
|
try {
|
|
2618
|
-
const
|
|
2619
|
-
pnpmStoreCache = { path:
|
|
2618
|
+
const out2 = execFileSync3("pnpm", ["store", "path"], { stdio: ["ignore", "pipe", "ignore"], timeout: 5e3 }).toString().trim();
|
|
2619
|
+
pnpmStoreCache = { path: out2 || void 0 };
|
|
2620
2620
|
} catch {
|
|
2621
2621
|
pnpmStoreCache = { path: void 0 };
|
|
2622
2622
|
}
|
|
@@ -2912,12 +2912,12 @@ function scanCommand(cmd, roots, cwd = roots.cwd) {
|
|
|
2912
2912
|
}
|
|
2913
2913
|
plain.push(t);
|
|
2914
2914
|
}
|
|
2915
|
-
const
|
|
2916
|
-
if (
|
|
2917
|
-
cwdNow = expandPath(
|
|
2915
|
+
const out2 = scanSimple(plain, roots, cwdNow);
|
|
2916
|
+
if (out2.kind === "cd") {
|
|
2917
|
+
cwdNow = expandPath(out2.to, cwdNow);
|
|
2918
2918
|
continue;
|
|
2919
2919
|
}
|
|
2920
|
-
if (
|
|
2920
|
+
if (out2.kind !== "ok") return out2;
|
|
2921
2921
|
}
|
|
2922
2922
|
return { kind: "ok" };
|
|
2923
2923
|
}
|
|
@@ -2988,14 +2988,14 @@ var PATH_WRITE_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit", "N
|
|
|
2988
2988
|
function pathsIn(input) {
|
|
2989
2989
|
if (!input || typeof input !== "object") return [];
|
|
2990
2990
|
const o = input;
|
|
2991
|
-
const
|
|
2991
|
+
const out2 = [];
|
|
2992
2992
|
for (const f of PATH_FIELDS) {
|
|
2993
|
-
if (typeof o[f] === "string" && o[f])
|
|
2993
|
+
if (typeof o[f] === "string" && o[f]) out2.push(o[f]);
|
|
2994
2994
|
}
|
|
2995
2995
|
if (o.changes && typeof o.changes === "object") {
|
|
2996
|
-
|
|
2996
|
+
out2.push(...Object.keys(o.changes));
|
|
2997
2997
|
}
|
|
2998
|
-
return
|
|
2998
|
+
return out2;
|
|
2999
2999
|
}
|
|
3000
3000
|
function fsConfineRule(roots) {
|
|
3001
3001
|
return {
|
|
@@ -3587,10 +3587,10 @@ function createStageRunner(deps) {
|
|
|
3587
3587
|
return `${tool3}\0`;
|
|
3588
3588
|
}
|
|
3589
3589
|
};
|
|
3590
|
-
const policyReason = (
|
|
3591
|
-
const recordDeny = (
|
|
3590
|
+
const policyReason = (verdict2) => verdict2.reason ?? `tool action denied by ${verdict2.policy}`;
|
|
3591
|
+
const recordDeny = (verdict2, toolUseId) => {
|
|
3592
3592
|
denied = true;
|
|
3593
|
-
const reason = policyReason(
|
|
3593
|
+
const reason = policyReason(verdict2);
|
|
3594
3594
|
if (toolUseId) {
|
|
3595
3595
|
streamEvent(writer.append({ seq: 0, ts: nowIso2(), type: "observation", runtime, toolUseId, isError: true, output: { error: reason } }));
|
|
3596
3596
|
}
|
|
@@ -3598,13 +3598,13 @@ function createStageRunner(deps) {
|
|
|
3598
3598
|
deps.sendProgress({ jobId, kind: "error", ts: nowIso2(), text: reason });
|
|
3599
3599
|
};
|
|
3600
3600
|
const authorizeToolUse = (tool3, input) => {
|
|
3601
|
-
const
|
|
3602
|
-
if (
|
|
3603
|
-
recordDeny(
|
|
3601
|
+
const verdict2 = evaluatePolicies({ kind: "action", stageId, tool: tool3, input }, rules);
|
|
3602
|
+
if (verdict2.verdict === "deny") {
|
|
3603
|
+
recordDeny(verdict2);
|
|
3604
3604
|
} else {
|
|
3605
3605
|
authorisedActions.push(actionKey(tool3, input));
|
|
3606
3606
|
}
|
|
3607
|
-
return
|
|
3607
|
+
return verdict2;
|
|
3608
3608
|
};
|
|
3609
3609
|
const onTrace = (event) => {
|
|
3610
3610
|
if (event.type === "action") {
|
|
@@ -3613,14 +3613,14 @@ function createStageRunner(deps) {
|
|
|
3613
3613
|
if (authorised >= 0) {
|
|
3614
3614
|
authorisedActions.splice(authorised, 1);
|
|
3615
3615
|
} else {
|
|
3616
|
-
const
|
|
3616
|
+
const verdict2 = evaluatePolicies(
|
|
3617
3617
|
{ kind: "action", stageId, tool: event.tool, input: event.input },
|
|
3618
3618
|
rules
|
|
3619
3619
|
);
|
|
3620
|
-
if (
|
|
3620
|
+
if (verdict2.verdict === "deny") {
|
|
3621
3621
|
streamEvent(writer.append(event));
|
|
3622
|
-
recordDeny(
|
|
3623
|
-
if (
|
|
3622
|
+
recordDeny(verdict2, event.toolUseId);
|
|
3623
|
+
if (verdict2.policy === "fs_confine" && runtime !== "claude-code") escapedUnblocked = true;
|
|
3624
3624
|
return;
|
|
3625
3625
|
}
|
|
3626
3626
|
}
|
|
@@ -4486,6 +4486,10 @@ function parseCli(argv) {
|
|
|
4486
4486
|
"hub-url": { type: "string" },
|
|
4487
4487
|
ephemeral: { type: "boolean", default: false },
|
|
4488
4488
|
foreground: { type: "boolean", default: false },
|
|
4489
|
+
// `stop` / `restart`: take the node down even though it has work in flight.
|
|
4490
|
+
force: { type: "boolean", default: false },
|
|
4491
|
+
// `status`: machine-readable output.
|
|
4492
|
+
json: { type: "boolean", default: false },
|
|
4489
4493
|
help: { type: "boolean", default: false }
|
|
4490
4494
|
},
|
|
4491
4495
|
allowPositionals: false
|
|
@@ -4495,6 +4499,7 @@ function parseCli(argv) {
|
|
|
4495
4499
|
}
|
|
4496
4500
|
if (values.help) return { kind: "help", command };
|
|
4497
4501
|
const ephemeral = values.ephemeral ?? false;
|
|
4502
|
+
const force = values.force ?? false;
|
|
4498
4503
|
const flags = {
|
|
4499
4504
|
...values.token ? { token: values.token } : {},
|
|
4500
4505
|
...values.name ? { name: values.name } : {},
|
|
@@ -4504,7 +4509,9 @@ function parseCli(argv) {
|
|
|
4504
4509
|
// a supervisor that restarts it on boot. It is a foreground node by definition.
|
|
4505
4510
|
foreground: (values.foreground ?? false) || ephemeral
|
|
4506
4511
|
};
|
|
4507
|
-
if (command === "stop") return { kind: "stop" };
|
|
4512
|
+
if (command === "stop") return { kind: "stop", force };
|
|
4513
|
+
if (command === "restart") return { kind: "restart", flags, force };
|
|
4514
|
+
if (command === "status") return { kind: "status", flags: { ...flags, json: values.json ?? false } };
|
|
4508
4515
|
return { kind: command, flags };
|
|
4509
4516
|
}
|
|
4510
4517
|
var LOG_LEVELS = ["trace", "debug", "info", "warn", "error", "fatal"];
|
|
@@ -4639,6 +4646,7 @@ function parseUpdate(flagArgs) {
|
|
|
4639
4646
|
args: flagArgs,
|
|
4640
4647
|
options: {
|
|
4641
4648
|
check: { type: "boolean", default: false },
|
|
4649
|
+
verbose: { type: "boolean", default: false },
|
|
4642
4650
|
help: { type: "boolean", default: false }
|
|
4643
4651
|
},
|
|
4644
4652
|
allowPositionals: false
|
|
@@ -4647,7 +4655,7 @@ function parseUpdate(flagArgs) {
|
|
|
4647
4655
|
return { kind: "error", message: e.message };
|
|
4648
4656
|
}
|
|
4649
4657
|
if (values.help) return { kind: "help", command: "update" };
|
|
4650
|
-
return { kind: "update", flags: { check: values.check ?? false } };
|
|
4658
|
+
return { kind: "update", flags: { check: values.check ?? false, verbose: values.verbose ?? false } };
|
|
4651
4659
|
}
|
|
4652
4660
|
function usage(bin, command) {
|
|
4653
4661
|
if (command === "start") {
|
|
@@ -4682,26 +4690,39 @@ function usage(bin, command) {
|
|
|
4682
4690
|
}
|
|
4683
4691
|
if (command === "stop") {
|
|
4684
4692
|
return [
|
|
4685
|
-
`Usage: ${bin} stop`,
|
|
4693
|
+
`Usage: ${bin} stop [--force]`,
|
|
4686
4694
|
"",
|
|
4687
4695
|
"Stop the node. It stays stopped across reboots until you run `dahrk start` again - a stop that",
|
|
4688
4696
|
"quietly undid itself at the next boot would not be a stop.",
|
|
4689
4697
|
"",
|
|
4698
|
+
"A node that is running a stage refuses to stop, because stopping it would kill work that costs real",
|
|
4699
|
+
"agent time. It lists what is in flight and leaves the node up; --force stops it anyway.",
|
|
4700
|
+
"",
|
|
4690
4701
|
"The service stays installed, so starting it again is instant. To remove it entirely, use",
|
|
4691
|
-
"`dahrk service uninstall`. To stop a node running in a terminal (--foreground), press Ctrl-C."
|
|
4702
|
+
"`dahrk service uninstall`. To stop a node running in a terminal (--foreground), press Ctrl-C.",
|
|
4703
|
+
"",
|
|
4704
|
+
"Options:",
|
|
4705
|
+
" --force Stop even if the node has stages in flight (this kills them)."
|
|
4692
4706
|
].join("\n");
|
|
4693
4707
|
}
|
|
4694
4708
|
if (command === "restart") {
|
|
4695
4709
|
return [
|
|
4696
4710
|
`Usage: ${bin} restart [options]`,
|
|
4697
4711
|
"",
|
|
4698
|
-
"Stop the node and start it again. Picks up a new client version, a rotated
|
|
4699
|
-
"have just installed (the node detects runtimes at boot, so a fresh `claude`
|
|
4712
|
+
"Stop the node and start it again, then report its status. Picks up a new client version, a rotated",
|
|
4713
|
+
"token, or a runtime you have just installed (the node detects runtimes at boot, so a fresh `claude`",
|
|
4714
|
+
"on PATH needs a restart).",
|
|
4715
|
+
"",
|
|
4716
|
+
"This is what picks up a `dahrk update`: a running node keeps executing the build it started with, so",
|
|
4717
|
+
"`dahrk start` on it does nothing at all.",
|
|
4718
|
+
"",
|
|
4719
|
+
"Like `stop`, it refuses on a node with stages in flight rather than killing them; --force overrides.",
|
|
4700
4720
|
"",
|
|
4701
4721
|
"Options:",
|
|
4702
4722
|
" --token <token> Re-enrol with a new token (or set DAHRK_ENROL_TOKEN).",
|
|
4703
4723
|
" --hub-url <url> Hub WebSocket URL (or set DAHRK_HUB_URL).",
|
|
4704
|
-
" --name <name> Display-name override (else the hub assigns one)."
|
|
4724
|
+
" --name <name> Display-name override (else the hub assigns one).",
|
|
4725
|
+
" --force Restart even if the node has stages in flight (this kills them)."
|
|
4705
4726
|
].join("\n");
|
|
4706
4727
|
}
|
|
4707
4728
|
if (command === "logs") {
|
|
@@ -4793,26 +4814,38 @@ function usage(bin, command) {
|
|
|
4793
4814
|
}
|
|
4794
4815
|
if (command === "status") {
|
|
4795
4816
|
return [
|
|
4796
|
-
`Usage: ${bin} status`,
|
|
4817
|
+
`Usage: ${bin} status [--json]`,
|
|
4818
|
+
"",
|
|
4819
|
+
"Report this node's local state: whether it is running (and how), whether it is enrolled and as whom,",
|
|
4820
|
+
"its node id, the runtimes it can serve with their versions, and what it is working on right now.",
|
|
4797
4821
|
"",
|
|
4798
|
-
"
|
|
4799
|
-
"
|
|
4822
|
+
"Local only - it dials nothing, so it is instant and works offline. That is also why the hub line says",
|
|
4823
|
+
"when the node was LAST known to be connected (from its log) rather than claiming it is connected now,",
|
|
4824
|
+
"which this command cannot know without dialling. Use `doctor` for that: it checks the hub is",
|
|
4825
|
+
"reachable and the token still valid.",
|
|
4800
4826
|
"",
|
|
4801
|
-
"
|
|
4802
|
-
"
|
|
4803
|
-
"
|
|
4827
|
+
"Exits non-zero only when the node is installed but not running and nobody asked for that (i.e. it is",
|
|
4828
|
+
"crash-looping), so it works as a health check in a script. A node you deliberately stopped is not a",
|
|
4829
|
+
"failure.",
|
|
4830
|
+
"",
|
|
4831
|
+
"Options:",
|
|
4832
|
+
" --json Print the facts as JSON (for a script) instead of the report."
|
|
4804
4833
|
].join("\n");
|
|
4805
4834
|
}
|
|
4806
4835
|
if (command === "update") {
|
|
4807
4836
|
return [
|
|
4808
|
-
`Usage: ${bin} update [--check]`,
|
|
4837
|
+
`Usage: ${bin} update [--check] [--verbose]`,
|
|
4809
4838
|
"",
|
|
4810
4839
|
"Update this client in place to the latest published release. Detects how it was installed",
|
|
4811
4840
|
"(npm / Homebrew / curl) and runs the right upgrade, or prints the exact command when it cannot.",
|
|
4812
4841
|
"Reports current -> latest, and a no-op when already current.",
|
|
4813
4842
|
"",
|
|
4843
|
+
"A node that is already running keeps executing the OLD build until it is restarted, so if one is up",
|
|
4844
|
+
"this offers to restart it for you (and tells you to run `dahrk restart` when there is nobody to ask).",
|
|
4845
|
+
"",
|
|
4814
4846
|
"Options:",
|
|
4815
|
-
" --check Report whether an update is available without applying it (dry run)."
|
|
4847
|
+
" --check Report whether an update is available without applying it (dry run).",
|
|
4848
|
+
" --verbose Show the package manager's own output, which is hidden unless the upgrade fails."
|
|
4816
4849
|
].join("\n");
|
|
4817
4850
|
}
|
|
4818
4851
|
return [
|
|
@@ -4821,10 +4854,10 @@ function usage(bin, command) {
|
|
|
4821
4854
|
"Commands:",
|
|
4822
4855
|
" start Run the node, and keep it running (installs the service). --token to enrol, once.",
|
|
4823
4856
|
" stop Stop the node. It stays stopped until the next `start`.",
|
|
4824
|
-
" restart Stop it and start it again.",
|
|
4857
|
+
" restart Stop it and start it again. This is what picks up a `dahrk update`.",
|
|
4825
4858
|
" logs Show what the node is doing (-f to follow, --run <id> to narrow to one run).",
|
|
4826
4859
|
" diagnose Write a support bundle you can read, and send on if you choose. Uploads nothing.",
|
|
4827
|
-
" status Is
|
|
4860
|
+
" status Is it up, is it enrolled, what is it working on? (local, dials nothing)",
|
|
4828
4861
|
" run Run a workflow locally (engine-backed), e.g. `run preflight`.",
|
|
4829
4862
|
" doctor Preflight checks: Node, runtimes, hub reachability, token validity.",
|
|
4830
4863
|
" service Install/uninstall the always-on service by hand (`start` does this for you).",
|
|
@@ -4840,6 +4873,80 @@ function usage(bin, command) {
|
|
|
4840
4873
|
// src/diagnose.ts
|
|
4841
4874
|
import { existsSync as existsSync8, mkdirSync as mkdirSync8, readdirSync as readdirSync4, readFileSync as readFileSync6, statSync as statSync3, writeFileSync as writeFileSync7 } from "fs";
|
|
4842
4875
|
import { join as join12 } from "path";
|
|
4876
|
+
|
|
4877
|
+
// src/ui.ts
|
|
4878
|
+
import { styleText } from "util";
|
|
4879
|
+
function detectCapabilities(env = process.env, isTty = Boolean(process.stdout.isTTY)) {
|
|
4880
|
+
const forced = env.FORCE_COLOR !== void 0 && env.FORCE_COLOR !== "0";
|
|
4881
|
+
const colour = forced || isTty && env.NO_COLOR === void 0 && env.TERM !== "dumb";
|
|
4882
|
+
const locale = `${env.LC_ALL ?? ""}${env.LC_CTYPE ?? ""}${env.LANG ?? ""}`;
|
|
4883
|
+
const unicode = process.platform !== "win32" && (locale === "" || /UTF-?8/i.test(locale));
|
|
4884
|
+
return { colour, unicode };
|
|
4885
|
+
}
|
|
4886
|
+
var caps = detectCapabilities();
|
|
4887
|
+
var paint = (style, text) => (
|
|
4888
|
+
// `validateStream: false` because our own gate above has already decided; the option is ignored by the
|
|
4889
|
+
// Node 22.0-22.7 signature, which had no stream check at all, so this is correct on every supported Node.
|
|
4890
|
+
caps.colour ? styleText(style, text, { validateStream: false }) : text
|
|
4891
|
+
);
|
|
4892
|
+
var dim = (s) => paint("dim", s);
|
|
4893
|
+
var bold = (s) => paint("bold", s);
|
|
4894
|
+
var green = (s) => paint("green", s);
|
|
4895
|
+
var red = (s) => paint("red", s);
|
|
4896
|
+
function amber(s) {
|
|
4897
|
+
if (!caps.colour) return s;
|
|
4898
|
+
const depth = process.stdout.getColorDepth?.() ?? 4;
|
|
4899
|
+
return depth >= 24 ? `\x1B[38;2;245;165;36m${s}\x1B[39m` : paint("yellow", s);
|
|
4900
|
+
}
|
|
4901
|
+
var GLYPH = {
|
|
4902
|
+
ok: { unicode: "\u2714", ascii: "OK" },
|
|
4903
|
+
warn: { unicode: "\u25B2", ascii: "!" },
|
|
4904
|
+
fail: { unicode: "\u2716", ascii: "x" },
|
|
4905
|
+
info: { unicode: "\u2022", ascii: "-" }
|
|
4906
|
+
};
|
|
4907
|
+
var COLOUR = {
|
|
4908
|
+
ok: green,
|
|
4909
|
+
warn: amber,
|
|
4910
|
+
fail: red,
|
|
4911
|
+
info: dim
|
|
4912
|
+
};
|
|
4913
|
+
var symbol = (level) => COLOUR[level](caps.unicode ? GLYPH[level].unicode : GLYPH[level].ascii);
|
|
4914
|
+
var arrow = () => caps.unicode ? "\u2192" : "->";
|
|
4915
|
+
var verdict = (level, text) => ` ${symbol(level)} ${bold(text)}`;
|
|
4916
|
+
var LABEL_WIDTH = 11;
|
|
4917
|
+
function kv(label, value) {
|
|
4918
|
+
const gutter = label ? dim(label.padEnd(LABEL_WIDTH)) : " ".repeat(LABEL_WIDTH);
|
|
4919
|
+
return ` ${gutter}${value}`;
|
|
4920
|
+
}
|
|
4921
|
+
var hint = (text) => ` ${dim(text)}`;
|
|
4922
|
+
var hints = (pairs) => hint(pairs.map(([label, cmd]) => `${label}: ${cmd}`).join(" "));
|
|
4923
|
+
var out = (line) => void process.stdout.write(`${line}
|
|
4924
|
+
`);
|
|
4925
|
+
var stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
4926
|
+
function humanDuration(ms) {
|
|
4927
|
+
const s = Math.max(0, Math.round(ms / 1e3));
|
|
4928
|
+
if (s < 60) return `${s}s`;
|
|
4929
|
+
const m = Math.floor(s / 60);
|
|
4930
|
+
if (m < 60) return `${m}m`;
|
|
4931
|
+
const h = Math.floor(m / 60);
|
|
4932
|
+
if (h < 24) return m % 60 === 0 ? `${h}h` : `${h}h ${m % 60}m`;
|
|
4933
|
+
const d = Math.floor(h / 24);
|
|
4934
|
+
return h % 24 === 0 ? `${d}d` : `${d}d ${h % 24}h`;
|
|
4935
|
+
}
|
|
4936
|
+
var ago = (ms) => ms < 5e3 ? "just now" : `${humanDuration(ms)} ago`;
|
|
4937
|
+
var isInteractive = () => Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
4938
|
+
async function confirm(question) {
|
|
4939
|
+
const { createInterface } = await import("readline/promises");
|
|
4940
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
4941
|
+
try {
|
|
4942
|
+
const answer = (await rl.question(` ${question} ${dim("[Y/n]")} `)).trim().toLowerCase();
|
|
4943
|
+
return answer === "" || answer === "y" || answer === "yes";
|
|
4944
|
+
} finally {
|
|
4945
|
+
rl.close();
|
|
4946
|
+
}
|
|
4947
|
+
}
|
|
4948
|
+
|
|
4949
|
+
// src/diagnose.ts
|
|
4843
4950
|
var BUNDLE_LOG_LINES = 2e3;
|
|
4844
4951
|
function tailJsonl(raw, n) {
|
|
4845
4952
|
const records = [];
|
|
@@ -4945,8 +5052,7 @@ var defaultDiagnoseDeps = (paths, clientVersion, doctor) => ({
|
|
|
4945
5052
|
if (!existsSync8(dir)) mkdirSync8(dir, { recursive: true });
|
|
4946
5053
|
writeFileSync7(p, content, { mode: 384 });
|
|
4947
5054
|
},
|
|
4948
|
-
out
|
|
4949
|
-
`)
|
|
5055
|
+
out
|
|
4950
5056
|
});
|
|
4951
5057
|
function defaultBundlePath(cwd, now) {
|
|
4952
5058
|
return join12(cwd, `dahrk-diagnose-${now.toISOString().replace(/[:.]/g, "-")}.json`);
|
|
@@ -4954,7 +5060,7 @@ function defaultBundlePath(cwd, now) {
|
|
|
4954
5060
|
|
|
4955
5061
|
// src/doctor.ts
|
|
4956
5062
|
var MIN_NODE_MAJOR = 22;
|
|
4957
|
-
var
|
|
5063
|
+
var LEVEL = { pass: "ok", warn: "warn", fail: "fail" };
|
|
4958
5064
|
function checkNode(nodeVersion) {
|
|
4959
5065
|
const major = Number.parseInt(nodeVersion.replace(/^v/, "").split(".")[0] ?? "", 10);
|
|
4960
5066
|
if (Number.isNaN(major)) {
|
|
@@ -5052,16 +5158,15 @@ function checkToken(tokenPresent, hubUrl, probe2) {
|
|
|
5052
5158
|
function formatReport(checks) {
|
|
5053
5159
|
const failed = checks.filter((c) => c.status === "fail").length;
|
|
5054
5160
|
const warned = checks.filter((c) => c.status === "warn").length;
|
|
5055
|
-
const lines = checks.map((c) =>
|
|
5056
|
-
const summary = failed > 0 ?
|
|
5057
|
-
return ["
|
|
5161
|
+
const lines = checks.map((c) => ` ${symbol(LEVEL[c.status])} ${c.label}${c.detail ? `: ${dim(c.detail)}` : ""}`);
|
|
5162
|
+
const summary = failed > 0 ? verdict("fail", `${failed} check${failed === 1 ? "" : "s"} failed${warned ? `, ${warned} warning${warned === 1 ? "" : "s"}` : ""}.`) : warned > 0 ? verdict("warn", `Passed with ${warned} warning${warned === 1 ? "" : "s"}.`) : verdict("ok", "All checks green.");
|
|
5163
|
+
return ["", ...lines, "", summary].join("\n");
|
|
5058
5164
|
}
|
|
5059
5165
|
var defaultDeps = () => ({
|
|
5060
5166
|
nodeVersion: process.versions.node,
|
|
5061
5167
|
probeRuntimes: probeRuntimeStatuses,
|
|
5062
5168
|
probeHub,
|
|
5063
|
-
out
|
|
5064
|
-
`)
|
|
5169
|
+
out
|
|
5065
5170
|
});
|
|
5066
5171
|
async function runDoctor(inputs, deps = {}) {
|
|
5067
5172
|
const d = { ...defaultDeps(), ...deps };
|
|
@@ -5146,15 +5251,15 @@ function logsCommand(inputs) {
|
|
|
5146
5251
|
var LEVEL_RANK = { trace: 10, debug: 20, info: 30, warn: 40, error: 50, fatal: 60 };
|
|
5147
5252
|
var levelName = (n) => Object.entries(LEVEL_RANK).find(([, rank]) => rank === n)?.[0] ?? String(n);
|
|
5148
5253
|
function parseRecords(raw) {
|
|
5149
|
-
const
|
|
5254
|
+
const out2 = [];
|
|
5150
5255
|
for (const line of raw.split("\n")) {
|
|
5151
5256
|
if (!line.trim()) continue;
|
|
5152
5257
|
try {
|
|
5153
|
-
|
|
5258
|
+
out2.push(JSON.parse(line));
|
|
5154
5259
|
} catch {
|
|
5155
5260
|
}
|
|
5156
5261
|
}
|
|
5157
|
-
return
|
|
5262
|
+
return out2;
|
|
5158
5263
|
}
|
|
5159
5264
|
function filterRecords(records, q) {
|
|
5160
5265
|
const min = q.level ? LEVEL_RANK[q.level] ?? 0 : 0;
|
|
@@ -5230,8 +5335,7 @@ var defaultLogsDeps = (files, jsonlFile) => ({
|
|
|
5230
5335
|
child.on("error", () => resolve3(1));
|
|
5231
5336
|
child.on("close", (code) => resolve3(code ?? 0));
|
|
5232
5337
|
}),
|
|
5233
|
-
out
|
|
5234
|
-
`)
|
|
5338
|
+
out
|
|
5235
5339
|
});
|
|
5236
5340
|
|
|
5237
5341
|
// src/process-safety.ts
|
|
@@ -5362,15 +5466,18 @@ function worst(checks) {
|
|
|
5362
5466
|
if (checks.some((c) => c.status === "warn")) return "warn";
|
|
5363
5467
|
return "pass";
|
|
5364
5468
|
}
|
|
5365
|
-
function renderStage(index, total,
|
|
5366
|
-
const head = `[${index}/${total}] ${
|
|
5367
|
-
const findings =
|
|
5469
|
+
function renderStage(index, total, stage, out2) {
|
|
5470
|
+
const head = ` ${dim(`[${index}/${total}]`)} ${stage.label}`;
|
|
5471
|
+
const findings = stage.checks.filter((c) => c.status !== "pass");
|
|
5368
5472
|
if (findings.length === 0) {
|
|
5369
|
-
|
|
5473
|
+
out2(`${head} ${symbol("ok")}`);
|
|
5370
5474
|
return;
|
|
5371
5475
|
}
|
|
5372
|
-
|
|
5373
|
-
for (const f of findings)
|
|
5476
|
+
out2(head);
|
|
5477
|
+
for (const f of findings) {
|
|
5478
|
+
const level = f.status === "fail" ? "fail" : "warn";
|
|
5479
|
+
out2(` ${symbol(level)} ${f.label}: ${dim(f.detail ?? f.status)}`);
|
|
5480
|
+
}
|
|
5374
5481
|
}
|
|
5375
5482
|
async function runPreflight(inputs, deps = {}) {
|
|
5376
5483
|
const d = { ...defaultDeps2(), ...deps };
|
|
@@ -5422,8 +5529,7 @@ var defaultDeps2 = () => ({
|
|
|
5422
5529
|
probeHub,
|
|
5423
5530
|
gatherHost: gatherHostFacts,
|
|
5424
5531
|
newRunId: () => randomUUID2(),
|
|
5425
|
-
out
|
|
5426
|
-
`)
|
|
5532
|
+
out
|
|
5427
5533
|
});
|
|
5428
5534
|
function commandPresent(cmd) {
|
|
5429
5535
|
try {
|
|
@@ -5760,16 +5866,18 @@ function parseServiceStatus(manager, unitExists, probe2) {
|
|
|
5760
5866
|
const running = active === "active";
|
|
5761
5867
|
return running && pid > 0 ? { installed: true, running: true, pid } : { installed: true, running };
|
|
5762
5868
|
}
|
|
5763
|
-
function printUnsupported(
|
|
5764
|
-
|
|
5765
|
-
|
|
5869
|
+
function printUnsupported(out2) {
|
|
5870
|
+
out2(verdict("fail", "dahrk service is supported on macOS (launchd) and Linux (systemd) only."));
|
|
5871
|
+
out2("");
|
|
5872
|
+
out2(hint("On other platforms, run the node under a process manager such as pm2 (see the README)."));
|
|
5766
5873
|
return 1;
|
|
5767
5874
|
}
|
|
5768
5875
|
function runCommands(commands, d) {
|
|
5769
5876
|
for (const cmd of commands) {
|
|
5770
|
-
const code = d.run(cmd.argv);
|
|
5877
|
+
const { code, output } = d.run(cmd.argv);
|
|
5771
5878
|
if (code !== 0 && !cmd.ignoreFailure) {
|
|
5772
|
-
d.out(`
|
|
5879
|
+
d.out(verdict("fail", `command failed (${code}): ${cmd.argv.join(" ")}`));
|
|
5880
|
+
for (const line of output.split("\n")) if (line.trim()) d.out(` ${dim(line)}`);
|
|
5773
5881
|
return code;
|
|
5774
5882
|
}
|
|
5775
5883
|
}
|
|
@@ -5777,13 +5885,13 @@ function runCommands(commands, d) {
|
|
|
5777
5885
|
}
|
|
5778
5886
|
async function runServiceInstall(inputs, deps = {}) {
|
|
5779
5887
|
const d = { ...defaultDeps3(), ...deps };
|
|
5780
|
-
d.out("dahrk service install");
|
|
5781
5888
|
d.out("");
|
|
5782
5889
|
const manager = detectManager(d.platform);
|
|
5783
5890
|
if (manager === "unsupported") return printUnsupported(d.out);
|
|
5784
5891
|
if (!inputs.token) {
|
|
5785
|
-
d.out("No enrolment token
|
|
5786
|
-
d.out("
|
|
5892
|
+
d.out(verdict("fail", "No enrolment token."));
|
|
5893
|
+
d.out("");
|
|
5894
|
+
d.out(hint("Pass --token <token>, or set DAHRK_ENROL_TOKEN. Get one at https://app.dahrk.ai."));
|
|
5787
5895
|
return 2;
|
|
5788
5896
|
}
|
|
5789
5897
|
const plan = buildPlan({
|
|
@@ -5802,26 +5910,41 @@ async function runServiceInstall(inputs, deps = {}) {
|
|
|
5802
5910
|
d.mkdirp(dirOf(plan.filePath));
|
|
5803
5911
|
d.writeFile(plan.filePath, plan.content);
|
|
5804
5912
|
} catch (e) {
|
|
5805
|
-
d.out(`Could not write the service file at ${plan.filePath}
|
|
5913
|
+
d.out(verdict("fail", `Could not write the service file at ${plan.filePath}`));
|
|
5914
|
+
d.out("");
|
|
5915
|
+
d.out(hint(e.message));
|
|
5806
5916
|
return 1;
|
|
5807
5917
|
}
|
|
5808
|
-
d.out(`Wrote ${plan.manager} service: ${plan.filePath}`);
|
|
5809
5918
|
const code = runCommands(plan.installCommands, d);
|
|
5810
|
-
d.out("");
|
|
5811
5919
|
if (code !== 0) {
|
|
5812
|
-
d.out(
|
|
5813
|
-
d.out(
|
|
5920
|
+
d.out("");
|
|
5921
|
+
d.out(hint(`The service file is in place at ${plan.filePath}. Fix the error above and re-run.`));
|
|
5814
5922
|
return code;
|
|
5815
5923
|
}
|
|
5816
|
-
d.out("Installed. The node
|
|
5817
|
-
d.out(
|
|
5818
|
-
d.out("
|
|
5924
|
+
d.out(verdict("ok", "Installed. The node starts on boot and restarts on failure."));
|
|
5925
|
+
d.out("");
|
|
5926
|
+
d.out(kv("unit", plan.filePath));
|
|
5927
|
+
d.out("");
|
|
5928
|
+
d.out(hints([["logs", plan.logHint], ["uninstall", "dahrk service uninstall"]]));
|
|
5819
5929
|
return 0;
|
|
5820
5930
|
}
|
|
5821
5931
|
function availabilityCommand(manager) {
|
|
5822
5932
|
return manager === "systemd" ? ["systemctl", "--user", "show", "-p", "Version"] : void 0;
|
|
5823
5933
|
}
|
|
5824
|
-
|
|
5934
|
+
function liveNodePid(d) {
|
|
5935
|
+
const manager = detectManager(d.platform);
|
|
5936
|
+
if (manager !== "unsupported") {
|
|
5937
|
+
const unit = unitPath(manager, d.homeDir);
|
|
5938
|
+
if (d.fileExists(unit)) {
|
|
5939
|
+
const status = parseServiceStatus(manager, true, d.capture(statusCommand(manager)));
|
|
5940
|
+
if (status.running && status.pid) return status.pid;
|
|
5941
|
+
}
|
|
5942
|
+
}
|
|
5943
|
+
const held = parseLock(d.readFile(d.lockFile));
|
|
5944
|
+
return held !== void 0 && d.isAlive(held) ? held : void 0;
|
|
5945
|
+
}
|
|
5946
|
+
var nodeIsRunning = (deps = {}) => liveNodePid({ ...defaultDeps3(), ...deps }) !== void 0;
|
|
5947
|
+
async function runNodeStart(inputs, deps = {}, opts = {}) {
|
|
5825
5948
|
const d = { ...defaultDeps3(), ...deps };
|
|
5826
5949
|
const manager = detectManager(d.platform);
|
|
5827
5950
|
if (manager === "unsupported") {
|
|
@@ -5834,8 +5957,9 @@ async function runNodeStart(inputs, deps = {}) {
|
|
|
5834
5957
|
const filePath = unitPath(manager, d.homeDir);
|
|
5835
5958
|
const unitExists = d.fileExists(filePath);
|
|
5836
5959
|
if (!inputs.token && !unitExists) {
|
|
5837
|
-
d.out("No enrolment token
|
|
5838
|
-
d.out("
|
|
5960
|
+
d.out(verdict("fail", "No enrolment token."));
|
|
5961
|
+
d.out("");
|
|
5962
|
+
d.out(hint("Pass --token <token>, or set DAHRK_ENROL_TOKEN. Get one at https://app.dahrk.ai."));
|
|
5839
5963
|
return { kind: "error", code: 2 };
|
|
5840
5964
|
}
|
|
5841
5965
|
const plan = buildPlan({
|
|
@@ -5851,12 +5975,10 @@ async function runNodeStart(inputs, deps = {}) {
|
|
|
5851
5975
|
});
|
|
5852
5976
|
const canRender = inputs.token !== void 0;
|
|
5853
5977
|
const current = canRender && unitExists && unitIsCurrent(plan, d.readFile(filePath));
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
d.out(` logs: ${plan.logHint}`);
|
|
5859
|
-
return { kind: "running", code: 0 };
|
|
5978
|
+
if (!opts.alwaysLoad) {
|
|
5979
|
+
const probe2 = unitExists ? d.capture(statusCommand(manager)) : { code: 1, stdout: "" };
|
|
5980
|
+
const status = parseServiceStatus(manager, unitExists, probe2);
|
|
5981
|
+
if (current && status.running) return { kind: "running", code: 0, already: true };
|
|
5860
5982
|
}
|
|
5861
5983
|
if (canRender && !current) {
|
|
5862
5984
|
try {
|
|
@@ -5867,28 +5989,65 @@ async function runNodeStart(inputs, deps = {}) {
|
|
|
5867
5989
|
d.out(`Could not write the service file at ${plan.filePath}: ${e.message}`);
|
|
5868
5990
|
return { kind: "error", code: 1 };
|
|
5869
5991
|
}
|
|
5870
|
-
d.out(unitExists ? `Updated ${plan.manager} service
|
|
5992
|
+
d.out(hint(unitExists ? `Updated the ${plan.manager} service.` : `Installed the ${plan.manager} service.`));
|
|
5871
5993
|
}
|
|
5872
5994
|
const code = runCommands(plan.installCommands, d);
|
|
5873
5995
|
if (code !== 0) {
|
|
5874
|
-
d.out(`The service file is in place but starting it failed
|
|
5996
|
+
d.out(hint(`The service file is in place at ${plan.filePath}, but starting it failed.`));
|
|
5875
5997
|
return { kind: "error", code };
|
|
5876
5998
|
}
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5999
|
+
return { kind: "running", code: 0, already: false };
|
|
6000
|
+
}
|
|
6001
|
+
async function runNodeRestart(inputs, deps = {}) {
|
|
6002
|
+
const d = { ...defaultDeps3(), ...deps };
|
|
6003
|
+
const manager = detectManager(d.platform);
|
|
6004
|
+
if (manager === "unsupported") {
|
|
6005
|
+
return { kind: "foreground", reason: "no supported supervisor on this platform (launchd / systemd)" };
|
|
6006
|
+
}
|
|
6007
|
+
const busy = guardBusy(d, inputs.force ?? false, "restart");
|
|
6008
|
+
if (busy) return { kind: "error", code: busy };
|
|
6009
|
+
const plan = buildPlan({
|
|
6010
|
+
manager,
|
|
6011
|
+
nodeBin: d.nodeBin,
|
|
6012
|
+
scriptPath: d.scriptPath,
|
|
6013
|
+
token: "-",
|
|
6014
|
+
homeDir: d.homeDir,
|
|
6015
|
+
logDir: d.logDir
|
|
6016
|
+
});
|
|
6017
|
+
if (d.fileExists(plan.filePath)) runCommands(plan.stopCommands, d);
|
|
6018
|
+
return runNodeStart(inputs, deps, { alwaysLoad: true });
|
|
6019
|
+
}
|
|
6020
|
+
function guardBusy(d, force, verb) {
|
|
6021
|
+
const live = liveNodePid(d);
|
|
6022
|
+
const jobs = live === void 0 ? [] : d.inFlightJobs().filter((j) => j.nodePid === live);
|
|
6023
|
+
if (jobs.length === 0) return void 0;
|
|
6024
|
+
if (force) {
|
|
6025
|
+
d.out(verdict("warn", `Forcing ${verb} with ${jobs.length} job(s) in flight.`));
|
|
6026
|
+
return void 0;
|
|
6027
|
+
}
|
|
6028
|
+
d.out(verdict("warn", `This node is running ${jobs.length} job(s); \`${verb}\` would kill them.`));
|
|
6029
|
+
d.out("");
|
|
6030
|
+
for (const j of jobs) {
|
|
6031
|
+
const where = j.stageId ? `${j.runId} / ${j.stageId}` : j.runId;
|
|
6032
|
+
d.out(kv("", `${where} ${dim(humanDuration(Date.now() - j.startedAt))}`));
|
|
6033
|
+
}
|
|
6034
|
+
d.out("");
|
|
6035
|
+
d.out(hint(`Wait for them to finish, or \`dahrk ${verb} --force\` to interrupt them anyway.`));
|
|
6036
|
+
return BUSY_NODE;
|
|
5881
6037
|
}
|
|
6038
|
+
var BUSY_NODE = 4;
|
|
5882
6039
|
var STOP_FOREIGN_NODE = 3;
|
|
5883
6040
|
function foreignNodePid(lock, servicePid, isAlive2) {
|
|
5884
6041
|
const held = parseLock(lock);
|
|
5885
6042
|
if (held === void 0 || held === servicePid) return void 0;
|
|
5886
6043
|
return isAlive2(held) ? held : void 0;
|
|
5887
6044
|
}
|
|
5888
|
-
async function runNodeStop(deps = {}) {
|
|
6045
|
+
async function runNodeStop(inputs = {}, deps = {}) {
|
|
5889
6046
|
const d = { ...defaultDeps3(), ...deps };
|
|
5890
6047
|
const manager = detectManager(d.platform);
|
|
5891
6048
|
if (manager === "unsupported") return printUnsupported(d.out);
|
|
6049
|
+
const busy = guardBusy(d, inputs.force ?? false, "stop");
|
|
6050
|
+
if (busy) return busy;
|
|
5892
6051
|
const plan = buildPlan({
|
|
5893
6052
|
manager,
|
|
5894
6053
|
nodeBin: d.nodeBin,
|
|
@@ -5898,28 +6057,31 @@ async function runNodeStop(deps = {}) {
|
|
|
5898
6057
|
logDir: d.logDir
|
|
5899
6058
|
});
|
|
5900
6059
|
if (!d.fileExists(plan.filePath)) {
|
|
5901
|
-
d.out("No service installed, so there is nothing to stop.");
|
|
5902
|
-
d.out("
|
|
6060
|
+
d.out(verdict("info", "No service installed, so there is nothing to stop."));
|
|
6061
|
+
d.out("");
|
|
6062
|
+
d.out(hint("A node running in a terminal (`dahrk start --foreground`) stops with Ctrl-C."));
|
|
5903
6063
|
return reportForeignNode(d, void 0) ?? 0;
|
|
5904
6064
|
}
|
|
5905
6065
|
const servicePid = parseServiceStatus(manager, true, d.capture(statusCommand(manager))).pid;
|
|
5906
6066
|
runCommands(plan.stopCommands, d);
|
|
5907
|
-
d.out("Node stopped.
|
|
6067
|
+
d.out(verdict("ok", "Node stopped."));
|
|
6068
|
+
d.out("");
|
|
6069
|
+
d.out(hint("It stays stopped across reboots until you run `dahrk start`."));
|
|
5908
6070
|
return reportForeignNode(d, servicePid) ?? 0;
|
|
5909
6071
|
}
|
|
5910
6072
|
function reportForeignNode(d, servicePid) {
|
|
5911
6073
|
const pid = foreignNodePid(d.readFile(d.lockFile), servicePid, d.isAlive);
|
|
5912
6074
|
if (pid === void 0) return void 0;
|
|
5913
6075
|
d.out("");
|
|
5914
|
-
d.out(`
|
|
5915
|
-
d.out("
|
|
5916
|
-
d.out("
|
|
5917
|
-
d.out("
|
|
6076
|
+
d.out(verdict("warn", `A node is STILL RUNNING on this host (pid ${pid}). \`dahrk stop\` cannot stop it.`));
|
|
6077
|
+
d.out("");
|
|
6078
|
+
d.out(hint("Something else supervises it: pm2, a container, or `dahrk start --foreground` in a terminal."));
|
|
6079
|
+
d.out(hint("It is not a stray copy - it holds this node's identity, so it is still connected to the hub"));
|
|
6080
|
+
d.out(hint("and still taking Jobs. Stop it where it was started, or kill the pid."));
|
|
5918
6081
|
return STOP_FOREIGN_NODE;
|
|
5919
6082
|
}
|
|
5920
6083
|
async function runServiceUninstall(deps = {}) {
|
|
5921
6084
|
const d = { ...defaultDeps3(), ...deps };
|
|
5922
|
-
d.out("dahrk service uninstall");
|
|
5923
6085
|
d.out("");
|
|
5924
6086
|
const manager = detectManager(d.platform);
|
|
5925
6087
|
if (manager === "unsupported") return printUnsupported(d.out);
|
|
@@ -5932,17 +6094,19 @@ async function runServiceUninstall(deps = {}) {
|
|
|
5932
6094
|
logDir: d.logDir
|
|
5933
6095
|
});
|
|
5934
6096
|
if (!d.fileExists(plan.filePath)) {
|
|
5935
|
-
d.out(
|
|
6097
|
+
d.out(verdict("info", "No service installed. Nothing to do."));
|
|
5936
6098
|
return 0;
|
|
5937
6099
|
}
|
|
5938
6100
|
runCommands(plan.uninstallCommands, d);
|
|
5939
6101
|
try {
|
|
5940
6102
|
d.removeFile(plan.filePath);
|
|
5941
6103
|
} catch (e) {
|
|
5942
|
-
d.out(`Deregistered, but could not delete ${plan.filePath}
|
|
6104
|
+
d.out(verdict("warn", `Deregistered, but could not delete ${plan.filePath}`));
|
|
6105
|
+
d.out("");
|
|
6106
|
+
d.out(hint(e.message));
|
|
5943
6107
|
return 1;
|
|
5944
6108
|
}
|
|
5945
|
-
d.out(
|
|
6109
|
+
d.out(verdict("ok", "Removed. The node will no longer start on boot."));
|
|
5946
6110
|
return 0;
|
|
5947
6111
|
}
|
|
5948
6112
|
function dirOf(path) {
|
|
@@ -5981,6 +6145,10 @@ var defaultDeps3 = () => ({
|
|
|
5981
6145
|
logDir: logDir(process.env),
|
|
5982
6146
|
lockFile: lockFile(process.env),
|
|
5983
6147
|
isAlive,
|
|
6148
|
+
// Read-only here, and best-effort by construction: a missing or corrupt ledger reads as "no jobs", which
|
|
6149
|
+
// degrades the guard to today's behaviour rather than blocking a stop the operator needs.
|
|
6150
|
+
inFlightJobs: () => fileJobLedger(jobLedgerFile(stateDir(process.env)), () => {
|
|
6151
|
+
}).all(),
|
|
5984
6152
|
// Snapshot the operator's PATH at install time so the daemon finds git + the runtime CLIs (Homebrew /
|
|
5985
6153
|
// npm-global bins) that a supervisor's minimal PATH would otherwise hide.
|
|
5986
6154
|
pathEnv: process.env.PATH,
|
|
@@ -6001,14 +6169,21 @@ var defaultDeps3 = () => ({
|
|
|
6001
6169
|
},
|
|
6002
6170
|
removeFile: (path) => rmSync7(path, { force: true }),
|
|
6003
6171
|
fileExists: (path) => existsSync13(path),
|
|
6172
|
+
// Captured, never inherited. A supervisor's stderr is ours to relay when it matters, not to leak
|
|
6173
|
+
// unconditionally: `runCommands` decides who needs to read it.
|
|
6004
6174
|
run: (argv) => {
|
|
6005
6175
|
const [cmd, ...args] = argv;
|
|
6006
6176
|
try {
|
|
6007
|
-
execFileSync7(cmd, args, {
|
|
6008
|
-
|
|
6177
|
+
const stdout = execFileSync7(cmd, args, {
|
|
6178
|
+
encoding: "utf8",
|
|
6179
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
6180
|
+
});
|
|
6181
|
+
return { code: 0, output: stdout };
|
|
6009
6182
|
} catch (e) {
|
|
6010
6183
|
const status = e.status;
|
|
6011
|
-
|
|
6184
|
+
const { stdout, stderr } = e;
|
|
6185
|
+
const output = `${stdout?.toString() ?? ""}${stderr?.toString() ?? ""}`;
|
|
6186
|
+
return { code: typeof status === "number" ? status : 1, output };
|
|
6012
6187
|
}
|
|
6013
6188
|
},
|
|
6014
6189
|
capture: (argv) => {
|
|
@@ -6024,8 +6199,7 @@ var defaultDeps3 = () => ({
|
|
|
6024
6199
|
return { code: typeof status === "number" ? status : 1, stdout: "" };
|
|
6025
6200
|
}
|
|
6026
6201
|
},
|
|
6027
|
-
out
|
|
6028
|
-
`)
|
|
6202
|
+
out
|
|
6029
6203
|
});
|
|
6030
6204
|
|
|
6031
6205
|
// src/update.ts
|
|
@@ -6073,58 +6247,79 @@ function isNewer(latest, current) {
|
|
|
6073
6247
|
}
|
|
6074
6248
|
return false;
|
|
6075
6249
|
}
|
|
6076
|
-
function printChannelCommands(
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6250
|
+
function printChannelCommands(out2) {
|
|
6251
|
+
out2(kv("npm", CHANNEL_COMMANDS.npm));
|
|
6252
|
+
out2(kv("Homebrew", CHANNEL_COMMANDS.homebrew));
|
|
6253
|
+
out2(kv("curl", CHANNEL_COMMANDS.curl));
|
|
6080
6254
|
}
|
|
6081
6255
|
async function runUpdate(inputs, deps = {}) {
|
|
6082
6256
|
const d = { ...defaultDeps4(), ...deps };
|
|
6083
6257
|
const current = inputs.currentVersion;
|
|
6084
|
-
d.out("dahrk update");
|
|
6085
6258
|
d.out("");
|
|
6086
6259
|
let latest;
|
|
6087
6260
|
try {
|
|
6088
6261
|
latest = await d.fetchLatest();
|
|
6089
6262
|
} catch (e) {
|
|
6090
|
-
d.out(`Could not determine the latest version: ${e.message}`);
|
|
6091
|
-
d.out(
|
|
6263
|
+
d.out(verdict("fail", `Could not determine the latest version: ${e.message}`));
|
|
6264
|
+
d.out("");
|
|
6265
|
+
d.out(hint(`You are on ${current}. See https://www.npmjs.com/package/dahrk-node for releases.`));
|
|
6092
6266
|
return 1;
|
|
6093
6267
|
}
|
|
6094
6268
|
if (!isNewer(latest, current)) {
|
|
6095
|
-
d.out(`Already on the latest version (${current}).`);
|
|
6269
|
+
d.out(verdict("ok", `Already on the latest version (${current}).`));
|
|
6096
6270
|
return 0;
|
|
6097
6271
|
}
|
|
6098
|
-
d.out(`Update available: ${current}
|
|
6272
|
+
d.out(verdict("warn", `Update available: ${current} ${arrow()} ${latest}`));
|
|
6099
6273
|
const channel = detectChannel(d.binPath);
|
|
6100
6274
|
const cmd = upgradeCommand(channel);
|
|
6101
6275
|
if (inputs.check) {
|
|
6102
6276
|
d.out("");
|
|
6103
6277
|
if (cmd) {
|
|
6104
|
-
d.out(`Run \`dahrk update\` to upgrade (${channel}: ${cmd.display}).`);
|
|
6278
|
+
d.out(hint(`Run \`dahrk update\` to upgrade (${channel}: ${cmd.display}).`));
|
|
6105
6279
|
} else {
|
|
6106
|
-
d.out("Run `dahrk update`, or upgrade with the command for your install channel:");
|
|
6280
|
+
d.out(hint("Run `dahrk update`, or upgrade with the command for your install channel:"));
|
|
6107
6281
|
printChannelCommands(d.out);
|
|
6108
6282
|
}
|
|
6109
6283
|
return 0;
|
|
6110
6284
|
}
|
|
6111
6285
|
if (!cmd) {
|
|
6112
6286
|
d.out("");
|
|
6113
|
-
d.out("Could not tell how this client was installed. Upgrade with the command for your channel:");
|
|
6287
|
+
d.out(hint("Could not tell how this client was installed. Upgrade with the command for your channel:"));
|
|
6114
6288
|
printChannelCommands(d.out);
|
|
6115
6289
|
return 0;
|
|
6116
6290
|
}
|
|
6117
6291
|
d.out("");
|
|
6118
|
-
d.out(`Upgrading via ${channel}: ${cmd.display}`);
|
|
6292
|
+
d.out(hint(`Upgrading via ${channel}: ${cmd.display}`));
|
|
6293
|
+
const { code, output } = d.runUpgrade(cmd.argv);
|
|
6119
6294
|
d.out("");
|
|
6120
|
-
|
|
6295
|
+
if (code !== 0) {
|
|
6296
|
+
d.out(verdict("fail", `Upgrade failed (exit ${code}).`));
|
|
6297
|
+
for (const line of output.split("\n")) if (line.trim()) d.out(` ${dim(line)}`);
|
|
6298
|
+
d.out("");
|
|
6299
|
+
d.out(hint(`Run it yourself to retry: ${cmd.display}`));
|
|
6300
|
+
return code;
|
|
6301
|
+
}
|
|
6302
|
+
if (inputs.verbose) {
|
|
6303
|
+
for (const line of output.split("\n")) if (line.trim()) d.out(` ${dim(line)}`);
|
|
6304
|
+
}
|
|
6305
|
+
d.out(verdict("ok", `Upgraded to ${latest}.`));
|
|
6306
|
+
await offerRestart(d);
|
|
6307
|
+
return 0;
|
|
6308
|
+
}
|
|
6309
|
+
async function offerRestart(d) {
|
|
6310
|
+
if (!d.nodeRunning()) return;
|
|
6311
|
+
if (!d.interactive()) {
|
|
6312
|
+
d.out(hint("A node is running on the old build. Run `dahrk restart` to pick this up."));
|
|
6313
|
+
return;
|
|
6314
|
+
}
|
|
6121
6315
|
d.out("");
|
|
6122
|
-
if (
|
|
6123
|
-
d.out(
|
|
6124
|
-
|
|
6125
|
-
d.out(`Upgrade exited with code ${code}. Run it yourself to see the error: ${cmd.display}`);
|
|
6316
|
+
if (!await d.confirm("A node is running on the old build. Restart it now?")) {
|
|
6317
|
+
d.out(hint("Left running on the old build. Run `dahrk restart` when you are ready."));
|
|
6318
|
+
return;
|
|
6126
6319
|
}
|
|
6127
|
-
|
|
6320
|
+
const code = await d.restart();
|
|
6321
|
+
if (code !== 0) d.out(hint("The node is still on the old build. Run `dahrk restart` once it is free."));
|
|
6322
|
+
else d.out(verdict("ok", "Node restarted on the new build."));
|
|
6128
6323
|
}
|
|
6129
6324
|
async function fetchLatestVersion(signal) {
|
|
6130
6325
|
const res = await fetch(LATEST_URL, {
|
|
@@ -6139,19 +6334,36 @@ async function fetchLatestVersion(signal) {
|
|
|
6139
6334
|
function spawnUpgrade(argv) {
|
|
6140
6335
|
const [cmd, ...args] = argv;
|
|
6141
6336
|
try {
|
|
6142
|
-
execFileSync8(cmd, args, {
|
|
6143
|
-
|
|
6337
|
+
const stdout = execFileSync8(cmd, args, {
|
|
6338
|
+
encoding: "utf8",
|
|
6339
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
6340
|
+
});
|
|
6341
|
+
return { code: 0, output: stdout };
|
|
6144
6342
|
} catch (e) {
|
|
6145
6343
|
const status = e.status;
|
|
6146
|
-
|
|
6344
|
+
const { stdout, stderr } = e;
|
|
6345
|
+
return {
|
|
6346
|
+
code: typeof status === "number" ? status : 1,
|
|
6347
|
+
output: `${stdout?.toString() ?? ""}${stderr?.toString() ?? ""}`
|
|
6348
|
+
};
|
|
6147
6349
|
}
|
|
6148
6350
|
}
|
|
6149
6351
|
var defaultDeps4 = () => ({
|
|
6150
6352
|
binPath: process.argv[1],
|
|
6151
6353
|
fetchLatest: fetchLatestVersion,
|
|
6152
6354
|
runUpgrade: spawnUpgrade,
|
|
6153
|
-
|
|
6154
|
-
|
|
6355
|
+
nodeRunning: nodeIsRunning,
|
|
6356
|
+
interactive: isInteractive,
|
|
6357
|
+
confirm,
|
|
6358
|
+
// No inputs: the installed unit already carries this node's token and overrides in its env block, so a
|
|
6359
|
+
// restart re-registers exactly what was there before, with the new client on disk behind it. `desired` is
|
|
6360
|
+
// untouched on purpose - the node was running, and it still is.
|
|
6361
|
+
restart: async () => {
|
|
6362
|
+
const outcome = await runNodeRestart({});
|
|
6363
|
+
if (outcome.kind === "running") return 0;
|
|
6364
|
+
return outcome.kind === "error" ? outcome.code : 1;
|
|
6365
|
+
},
|
|
6366
|
+
out
|
|
6155
6367
|
});
|
|
6156
6368
|
|
|
6157
6369
|
// src/update-check.ts
|
|
@@ -6207,81 +6419,155 @@ function cachedUpdate(state, currentVersion, binPath) {
|
|
|
6207
6419
|
}
|
|
6208
6420
|
|
|
6209
6421
|
// src/status.ts
|
|
6210
|
-
|
|
6422
|
+
function presenceVerdict(f) {
|
|
6423
|
+
switch (f.presence.kind) {
|
|
6424
|
+
case "running": {
|
|
6425
|
+
const pid = f.presence.pid ? ` (pid ${f.presence.pid})` : "";
|
|
6426
|
+
return { level: "ok", text: `Node running${pid}` };
|
|
6427
|
+
}
|
|
6428
|
+
case "foreign":
|
|
6429
|
+
return { level: "ok", text: `Node running under another supervisor (pid ${f.presence.pid})` };
|
|
6430
|
+
case "stopped":
|
|
6431
|
+
return { level: "warn", text: "Node stopped" };
|
|
6432
|
+
case "not-installed":
|
|
6433
|
+
return { level: "warn", text: "Node not installed" };
|
|
6434
|
+
case "crashed":
|
|
6435
|
+
return { level: "fail", text: "Node is installed but NOT running - it is failing to start or crash-looping" };
|
|
6436
|
+
case "no-supervisor":
|
|
6437
|
+
return { level: "warn", text: "No supervisor on this host" };
|
|
6438
|
+
}
|
|
6439
|
+
}
|
|
6440
|
+
function presenceHints(f) {
|
|
6441
|
+
switch (f.presence.kind) {
|
|
6442
|
+
case "running":
|
|
6443
|
+
case "foreign":
|
|
6444
|
+
return [hints([["logs", "dahrk logs -f"], ["stop", "dahrk stop"]])];
|
|
6445
|
+
case "stopped":
|
|
6446
|
+
return [hints([["start", "dahrk start"]])];
|
|
6447
|
+
case "not-installed":
|
|
6448
|
+
return [hints([["start", "dahrk start"]])];
|
|
6449
|
+
case "crashed":
|
|
6450
|
+
return [hints([["logs", "dahrk logs -f"], ["check", "dahrk doctor"], ["report", "dahrk diagnose"]])];
|
|
6451
|
+
case "no-supervisor":
|
|
6452
|
+
return [hints([["run it here", "dahrk start --foreground"]])];
|
|
6453
|
+
}
|
|
6454
|
+
}
|
|
6211
6455
|
function renderStatus(f) {
|
|
6212
|
-
const
|
|
6456
|
+
const v = presenceVerdict(f);
|
|
6457
|
+
const lines = ["", verdict(v.level, v.text), ""];
|
|
6213
6458
|
const { nodeId, name, tenantId, enrolToken } = f.state;
|
|
6214
6459
|
if (enrolToken) {
|
|
6215
|
-
|
|
6216
|
-
|
|
6460
|
+
const who = name ? name : "yes";
|
|
6461
|
+
lines.push(kv("Enrolled", tenantId ? `${who} ${dim("\xB7")} ${tenantId}` : who));
|
|
6217
6462
|
} else if (f.envToken) {
|
|
6218
|
-
lines.push(
|
|
6463
|
+
lines.push(kv("Enrolled", "via DAHRK_ENROL_TOKEN (caches on the next successful start)"));
|
|
6219
6464
|
} else {
|
|
6220
|
-
lines.push(
|
|
6465
|
+
lines.push(kv("Enrolled", `no ${dim("run `dahrk start --token <token>` once to enrol")}`));
|
|
6221
6466
|
}
|
|
6222
|
-
lines.push(
|
|
6467
|
+
lines.push(kv("Node id", nodeId ?? dim("not yet minted (first `dahrk start` mints one)")));
|
|
6223
6468
|
lines.push(
|
|
6224
|
-
|
|
6469
|
+
kv(
|
|
6225
6470
|
"Client",
|
|
6226
|
-
f.update ? `${f.clientVersion}
|
|
6471
|
+
f.update ? `${f.clientVersion} ${dim(`(update available: ${f.update.latest} - run \`dahrk update\`)`)}` : f.clientVersion
|
|
6227
6472
|
)
|
|
6228
6473
|
);
|
|
6229
|
-
|
|
6474
|
+
const conn = f.connection ? ` ${dim(`(${f.connection.event} ${ago(f.now - f.connection.at)}${f.connection.detail ? `, ${f.connection.detail}` : ""})`)}` : "";
|
|
6475
|
+
lines.push(kv("Hub", `${f.hubUrl}${conn}`));
|
|
6476
|
+
const installed = f.runtimes.filter((r) => r.installed);
|
|
6230
6477
|
lines.push(
|
|
6231
|
-
|
|
6478
|
+
kv(
|
|
6232
6479
|
"Runtimes",
|
|
6233
|
-
|
|
6480
|
+
installed.length > 0 ? installed.map((r) => r.version ? `${r.runtime} ${dim(r.version)}` : r.runtime).join(", ") : `${dim("none detected - this node will serve no Jobs (install claude / codex / pi)")}`
|
|
6234
6481
|
)
|
|
6235
6482
|
);
|
|
6236
|
-
if (
|
|
6237
|
-
lines.push(
|
|
6238
|
-
} else if (!f.service.installed) {
|
|
6239
|
-
lines.push(bullet("Node", "not installed - run `dahrk start` to run it always-on"));
|
|
6240
|
-
} else if (f.service.running) {
|
|
6241
|
-
const pid = f.service.pid ? ` (pid ${f.service.pid})` : "";
|
|
6242
|
-
lines.push(bullet("Node", `running${pid}`));
|
|
6243
|
-
} else if (f.state.desired === "stopped") {
|
|
6244
|
-
lines.push(bullet("Node", "stopped - run `dahrk start` to bring it back"));
|
|
6483
|
+
if (f.jobs.length === 0) {
|
|
6484
|
+
lines.push(kv("Work", dim("idle")));
|
|
6245
6485
|
} else {
|
|
6246
|
-
|
|
6247
|
-
|
|
6486
|
+
const [first, ...rest] = f.jobs;
|
|
6487
|
+
lines.push(kv("Work", jobLine(first, f.now)));
|
|
6488
|
+
for (const j of rest) lines.push(kv("", jobLine(j, f.now)));
|
|
6248
6489
|
}
|
|
6249
|
-
lines.push("",
|
|
6490
|
+
lines.push(...presenceHints(f).flatMap((h) => ["", h]));
|
|
6491
|
+
lines.push("", dim(` State file: ${f.stateFile}`));
|
|
6250
6492
|
return lines;
|
|
6251
6493
|
}
|
|
6494
|
+
function jobLine(j, now) {
|
|
6495
|
+
const where = j.stageId ? `${j.runId} ${dim("/")} ${j.stageId}` : `${j.runId} ${dim(`(${j.kind})`)}`;
|
|
6496
|
+
return `${where} ${dim(humanDuration(now - j.startedAt))}`;
|
|
6497
|
+
}
|
|
6252
6498
|
function isUnhealthy(f) {
|
|
6253
|
-
return
|
|
6499
|
+
return f.presence.kind === "crashed";
|
|
6254
6500
|
}
|
|
6255
|
-
|
|
6501
|
+
function resolvePresence(service, lockedPid, desired) {
|
|
6502
|
+
if (service?.running) return { kind: "running", ...service.pid ? { pid: service.pid } : {} };
|
|
6503
|
+
if (lockedPid !== void 0) return { kind: "foreign", pid: lockedPid };
|
|
6504
|
+
if (!service) return { kind: "no-supervisor" };
|
|
6505
|
+
if (!service.installed) return { kind: "not-installed" };
|
|
6506
|
+
return desired === "stopped" ? { kind: "stopped" } : { kind: "crashed" };
|
|
6507
|
+
}
|
|
6508
|
+
async function gatherFacts(inputs, deps) {
|
|
6256
6509
|
const manager = detectManager(deps.platform);
|
|
6257
6510
|
let service;
|
|
6258
|
-
let logHint;
|
|
6259
6511
|
if (manager !== "unsupported") {
|
|
6260
6512
|
const unit = unitPath(manager, deps.homeDir);
|
|
6261
6513
|
const exists = deps.fileExists(unit);
|
|
6262
6514
|
const probe2 = exists ? deps.capture(statusCommand(manager)) : { code: 1, stdout: "" };
|
|
6263
6515
|
service = parseServiceStatus(manager, exists, probe2);
|
|
6264
|
-
logHint = "dahrk logs -f";
|
|
6265
6516
|
}
|
|
6266
6517
|
const state = readState(stateFile(deps.env));
|
|
6518
|
+
const lockedPid = deps.lockedPid();
|
|
6519
|
+
const presence = resolvePresence(service, lockedPid, state.desired);
|
|
6520
|
+
const livePid = presence.kind === "running" || presence.kind === "foreign" ? presence.pid : void 0;
|
|
6521
|
+
const jobs = livePid === void 0 ? [] : deps.jobs().filter((j) => j.nodePid === livePid);
|
|
6267
6522
|
const update = checkSuppressed(deps.env) ? void 0 : cachedUpdate(state, inputs.clientVersion, deps.binPath);
|
|
6268
|
-
const
|
|
6523
|
+
const connection = deps.connection();
|
|
6524
|
+
return {
|
|
6269
6525
|
clientVersion: inputs.clientVersion,
|
|
6270
6526
|
hubUrl: inputs.hubUrl,
|
|
6271
6527
|
stateFile: stateFile(deps.env),
|
|
6272
6528
|
state,
|
|
6273
6529
|
envToken: Boolean(deps.env.DAHRK_ENROL_TOKEN),
|
|
6274
|
-
runtimes: await deps.
|
|
6530
|
+
runtimes: await deps.probeRuntimes(),
|
|
6531
|
+
presence,
|
|
6532
|
+
jobs,
|
|
6533
|
+
now: deps.now(),
|
|
6275
6534
|
...service ? { service } : {},
|
|
6276
|
-
...
|
|
6535
|
+
...connection ? { connection } : {},
|
|
6277
6536
|
...update ? { update } : {}
|
|
6278
6537
|
};
|
|
6538
|
+
}
|
|
6539
|
+
var CONNECTION_MARKERS = [
|
|
6540
|
+
{ prefix: "EDGE_WELCOMED", event: "welcomed", detail: false },
|
|
6541
|
+
{ prefix: "EDGE_CONNECTED", event: "connected", detail: false },
|
|
6542
|
+
{ prefix: "EDGE_DISCONNECTED", event: "disconnected", detail: true },
|
|
6543
|
+
{ prefix: "EDGE_STALE", event: "went stale", detail: false }
|
|
6544
|
+
];
|
|
6545
|
+
function lastConnection(records) {
|
|
6546
|
+
for (let i = records.length - 1; i >= 0; i--) {
|
|
6547
|
+
const r = records[i];
|
|
6548
|
+
if (typeof r?.msg !== "string") continue;
|
|
6549
|
+
const marker = CONNECTION_MARKERS.find((m) => r.msg === m.prefix || r.msg.startsWith(`${m.prefix}:`));
|
|
6550
|
+
if (!marker) continue;
|
|
6551
|
+
const at = typeof r.time === "string" ? Date.parse(r.time) : typeof r.time === "number" ? r.time : NaN;
|
|
6552
|
+
if (Number.isNaN(at)) continue;
|
|
6553
|
+
const detail = marker.detail ? r.msg.slice(marker.prefix.length + 1).split(/\s+/)[0] ?? "" : "";
|
|
6554
|
+
return { event: marker.event, at, ...detail ? { detail } : {} };
|
|
6555
|
+
}
|
|
6556
|
+
return void 0;
|
|
6557
|
+
}
|
|
6558
|
+
async function runStatus(inputs, deps) {
|
|
6559
|
+
const facts = await gatherFacts(inputs, deps);
|
|
6560
|
+
if (inputs.json) {
|
|
6561
|
+
const { enrolToken: _omitted, ...state } = facts.state;
|
|
6562
|
+
deps.out(JSON.stringify({ ...facts, state, healthy: !isUnhealthy(facts) }, null, 2));
|
|
6563
|
+
return isUnhealthy(facts) ? 1 : 0;
|
|
6564
|
+
}
|
|
6279
6565
|
for (const line of renderStatus(facts)) deps.out(line);
|
|
6280
6566
|
return isUnhealthy(facts) ? 1 : 0;
|
|
6281
6567
|
}
|
|
6282
6568
|
|
|
6283
6569
|
// src/main.ts
|
|
6284
|
-
var CLIENT_VERSION = "0.1.
|
|
6570
|
+
var CLIENT_VERSION = "0.1.15";
|
|
6285
6571
|
var DEFAULT_HUB_URL = "wss://api.dahrk.ai";
|
|
6286
6572
|
var list = (v) => (v ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
6287
6573
|
var RUNTIMES2 = ["claude-code", "codex", "pi"];
|
|
@@ -6367,20 +6653,40 @@ async function start(flags) {
|
|
|
6367
6653
|
const env = envWithFlags(process.env, flags);
|
|
6368
6654
|
if (wantsForeground(env, flags)) return startForeground(env, flags);
|
|
6369
6655
|
await offerUpdate(env);
|
|
6370
|
-
const outcome = await runNodeStart(
|
|
6371
|
-
...resolveEnrolToken(env) ? { token: resolveEnrolToken(env) } : {},
|
|
6372
|
-
...env.DAHRK_NODE_NAME ? { name: env.DAHRK_NODE_NAME } : {},
|
|
6373
|
-
...env.DAHRK_HUB_URL ? { hubUrl: env.DAHRK_HUB_URL } : {}
|
|
6374
|
-
});
|
|
6656
|
+
const outcome = await runNodeStart(serviceInputs(env));
|
|
6375
6657
|
if (outcome.kind === "error") return outcome.code;
|
|
6376
6658
|
if (outcome.kind === "running") {
|
|
6377
6659
|
setDesired(env, "running");
|
|
6660
|
+
await printStatus(env);
|
|
6378
6661
|
return 0;
|
|
6379
6662
|
}
|
|
6380
|
-
|
|
6381
|
-
|
|
6663
|
+
out("");
|
|
6664
|
+
out(verdict("warn", `${outcome.reason}; running the node in this terminal instead.`));
|
|
6665
|
+
out("");
|
|
6666
|
+
out(hint("Use `dahrk start --foreground` (or DAHRK_FOREGROUND=1) to ask for this explicitly."));
|
|
6382
6667
|
return startForeground(env, flags);
|
|
6383
6668
|
}
|
|
6669
|
+
var serviceInputs = (env) => ({
|
|
6670
|
+
...resolveEnrolToken(env) ? { token: resolveEnrolToken(env) } : {},
|
|
6671
|
+
...env.DAHRK_NODE_NAME ? { name: env.DAHRK_NODE_NAME } : {},
|
|
6672
|
+
...env.DAHRK_HUB_URL ? { hubUrl: env.DAHRK_HUB_URL } : {}
|
|
6673
|
+
});
|
|
6674
|
+
async function restart(flags, force) {
|
|
6675
|
+
const env = envWithFlags(process.env, flags);
|
|
6676
|
+
out("");
|
|
6677
|
+
out(hint("Restarting the node..."));
|
|
6678
|
+
const outcome = await runNodeRestart({ ...serviceInputs(env), force });
|
|
6679
|
+
if (outcome.kind === "error") return outcome.code;
|
|
6680
|
+
if (outcome.kind === "foreground") {
|
|
6681
|
+
out(verdict("warn", `${outcome.reason}; there is no service to restart.`));
|
|
6682
|
+
out("");
|
|
6683
|
+
out(hint("A node running in a terminal is restarted by stopping it (Ctrl-C) and starting it again."));
|
|
6684
|
+
return 1;
|
|
6685
|
+
}
|
|
6686
|
+
setDesired(env, "running");
|
|
6687
|
+
await printStatus(env);
|
|
6688
|
+
return 0;
|
|
6689
|
+
}
|
|
6384
6690
|
async function startForeground(env, flags) {
|
|
6385
6691
|
if (!flags.ephemeral) {
|
|
6386
6692
|
const lock = acquireLock(defaultLockDeps(lockFile(env)));
|
|
@@ -6446,8 +6752,8 @@ async function startForeground(env, flags) {
|
|
|
6446
6752
|
});
|
|
6447
6753
|
return 0;
|
|
6448
6754
|
}
|
|
6449
|
-
async function stop(env) {
|
|
6450
|
-
const code = await runNodeStop();
|
|
6755
|
+
async function stop(env, force) {
|
|
6756
|
+
const code = await runNodeStop({ force });
|
|
6451
6757
|
if (code === 0 || code === STOP_FOREIGN_NODE) setDesired(env, "stopped");
|
|
6452
6758
|
return code;
|
|
6453
6759
|
}
|
|
@@ -6461,25 +6767,15 @@ function updateCheckDeps(env) {
|
|
|
6461
6767
|
fetchLatest: fetchLatestVersion
|
|
6462
6768
|
};
|
|
6463
6769
|
}
|
|
6464
|
-
var isInteractive = () => Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
6465
|
-
async function confirm(question) {
|
|
6466
|
-
const { createInterface } = await import("readline/promises");
|
|
6467
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
6468
|
-
try {
|
|
6469
|
-
const answer = (await rl.question(`${question} [Y/n] `)).trim().toLowerCase();
|
|
6470
|
-
return answer === "" || answer === "y" || answer === "yes";
|
|
6471
|
-
} finally {
|
|
6472
|
-
rl.close();
|
|
6473
|
-
}
|
|
6474
|
-
}
|
|
6475
6770
|
async function offerUpdate(env) {
|
|
6476
6771
|
const available = await checkForUpdate(CLIENT_VERSION, updateCheckDeps(env));
|
|
6477
6772
|
if (!available) return;
|
|
6478
|
-
|
|
6773
|
+
out("");
|
|
6774
|
+
out(renderUpdateNotice(available));
|
|
6479
6775
|
if (!isInteractive()) return;
|
|
6480
|
-
if (!await confirm("
|
|
6776
|
+
if (!await confirm("Update now?")) return;
|
|
6481
6777
|
const code = await runUpdate({ currentVersion: CLIENT_VERSION, check: false });
|
|
6482
|
-
if (code !== 0)
|
|
6778
|
+
if (code !== 0) out(hint("Update failed; starting the node on the current version anyway."));
|
|
6483
6779
|
}
|
|
6484
6780
|
function scheduleUpdateChecks(env) {
|
|
6485
6781
|
if (checkSuppressed(env)) return;
|
|
@@ -6498,7 +6794,15 @@ function statusDeps(env) {
|
|
|
6498
6794
|
homeDir: homedir7(),
|
|
6499
6795
|
env,
|
|
6500
6796
|
binPath: process.argv[1],
|
|
6501
|
-
|
|
6797
|
+
// The rich probe, with versions - the same one `doctor` runs. An explicit DAHRK_RUNTIMES override still
|
|
6798
|
+
// wins, and is reported as installed-without-a-version, because a pinned runtime was never probed.
|
|
6799
|
+
probeRuntimes: async () => {
|
|
6800
|
+
const override = list(env.DAHRK_RUNTIMES).filter(isRuntime2);
|
|
6801
|
+
if (override.length > 0) {
|
|
6802
|
+
return RUNTIMES2.map((r) => ({ runtime: r, cmd: r, installed: override.includes(r) }));
|
|
6803
|
+
}
|
|
6804
|
+
return probeRuntimeStatuses();
|
|
6805
|
+
},
|
|
6502
6806
|
fileExists: (path) => existsSync14(path),
|
|
6503
6807
|
capture: (argv) => {
|
|
6504
6808
|
const [cmd, ...args] = argv;
|
|
@@ -6510,10 +6814,35 @@ function statusDeps(env) {
|
|
|
6510
6814
|
return { code: typeof status === "number" ? status : 1, stdout: "" };
|
|
6511
6815
|
}
|
|
6512
6816
|
},
|
|
6513
|
-
|
|
6514
|
-
|
|
6817
|
+
lockedPid: () => {
|
|
6818
|
+
const held = parseLock(readFileOrUndefined(lockFile(env)));
|
|
6819
|
+
return held !== void 0 && isAlive(held) ? held : void 0;
|
|
6820
|
+
},
|
|
6821
|
+
// Best-effort by construction: a missing or unreadable ledger reads as "no jobs in flight", which shows
|
|
6822
|
+
// an idle node rather than failing the whole report over a file that only exists while work is running.
|
|
6823
|
+
jobs: () => fileJobLedger(jobLedgerFile(stateDir(env)), () => {
|
|
6824
|
+
}).all(),
|
|
6825
|
+
connection: () => {
|
|
6826
|
+
const raw = readFileOrUndefined(jsonlLogFile(env));
|
|
6827
|
+
return raw ? lastConnection(parseRecords(raw)) : void 0;
|
|
6828
|
+
},
|
|
6829
|
+
now: () => Date.now(),
|
|
6830
|
+
out
|
|
6515
6831
|
};
|
|
6516
6832
|
}
|
|
6833
|
+
function readFileOrUndefined(path) {
|
|
6834
|
+
try {
|
|
6835
|
+
return readFileSync11(path, "utf8");
|
|
6836
|
+
} catch {
|
|
6837
|
+
return void 0;
|
|
6838
|
+
}
|
|
6839
|
+
}
|
|
6840
|
+
async function printStatus(env) {
|
|
6841
|
+
return runStatus(
|
|
6842
|
+
{ clientVersion: CLIENT_VERSION, hubUrl: env.DAHRK_HUB_URL ?? DEFAULT_HUB_URL },
|
|
6843
|
+
statusDeps(env)
|
|
6844
|
+
);
|
|
6845
|
+
}
|
|
6517
6846
|
var KNOWN_WORKFLOWS = ["preflight"];
|
|
6518
6847
|
async function runWorkflow(flags) {
|
|
6519
6848
|
if (flags.workflow !== "preflight") {
|
|
@@ -6523,7 +6852,7 @@ async function runWorkflow(flags) {
|
|
|
6523
6852
|
return 2;
|
|
6524
6853
|
}
|
|
6525
6854
|
const env = applyEnvAliases(process.env);
|
|
6526
|
-
const hubUrl = flags.hubUrl ?? env.DAHRK_HUB_URL;
|
|
6855
|
+
const hubUrl = flags.hubUrl ?? env.DAHRK_HUB_URL ?? DEFAULT_HUB_URL;
|
|
6527
6856
|
const token = flags.token ?? resolveEnrolToken(env);
|
|
6528
6857
|
return runPreflight({
|
|
6529
6858
|
...flags.repo ? { repoPath: flags.repo } : {},
|
|
@@ -6552,7 +6881,10 @@ async function main() {
|
|
|
6552
6881
|
case "doctor": {
|
|
6553
6882
|
const env = envWithFlags(process.env, parsed.flags);
|
|
6554
6883
|
process.exitCode = await runDoctor({
|
|
6555
|
-
|
|
6884
|
+
// The same default the node itself dials. Without this, `dahrk doctor` on a stock install FAILED
|
|
6885
|
+
// with "no hub URL configured" while the node was connected to that very hub - it was reporting
|
|
6886
|
+
// that an env var was unset, not that anything was wrong.
|
|
6887
|
+
hubUrl: env.DAHRK_HUB_URL ?? DEFAULT_HUB_URL,
|
|
6556
6888
|
// Same resolution as `start`, so doctor checks the token the node would actually present:
|
|
6557
6889
|
// the flag/env if given, else the one cached by the last successful enrolment.
|
|
6558
6890
|
token: resolveEnrolToken(env),
|
|
@@ -6563,7 +6895,11 @@ async function main() {
|
|
|
6563
6895
|
case "status": {
|
|
6564
6896
|
const env = envWithFlags(process.env, parsed.flags);
|
|
6565
6897
|
process.exitCode = await runStatus(
|
|
6566
|
-
{
|
|
6898
|
+
{
|
|
6899
|
+
clientVersion: CLIENT_VERSION,
|
|
6900
|
+
hubUrl: env.DAHRK_HUB_URL ?? DEFAULT_HUB_URL,
|
|
6901
|
+
json: parsed.flags.json
|
|
6902
|
+
},
|
|
6567
6903
|
statusDeps(env)
|
|
6568
6904
|
);
|
|
6569
6905
|
break;
|
|
@@ -6588,24 +6924,21 @@ async function main() {
|
|
|
6588
6924
|
break;
|
|
6589
6925
|
}
|
|
6590
6926
|
case "update":
|
|
6591
|
-
process.exitCode = await runUpdate({
|
|
6927
|
+
process.exitCode = await runUpdate({
|
|
6928
|
+
currentVersion: CLIENT_VERSION,
|
|
6929
|
+
check: parsed.flags.check,
|
|
6930
|
+
verbose: parsed.flags.verbose
|
|
6931
|
+
});
|
|
6592
6932
|
break;
|
|
6593
6933
|
case "start":
|
|
6594
6934
|
process.exitCode = await start(parsed.flags);
|
|
6595
6935
|
break;
|
|
6596
6936
|
case "stop":
|
|
6597
|
-
process.exitCode = await stop(applyEnvAliases(process.env));
|
|
6937
|
+
process.exitCode = await stop(applyEnvAliases(process.env), parsed.force);
|
|
6598
6938
|
break;
|
|
6599
|
-
case "restart":
|
|
6600
|
-
|
|
6601
|
-
const code = await stop(env);
|
|
6602
|
-
if (code !== 0) {
|
|
6603
|
-
process.exitCode = code;
|
|
6604
|
-
break;
|
|
6605
|
-
}
|
|
6606
|
-
process.exitCode = await start(parsed.flags);
|
|
6939
|
+
case "restart":
|
|
6940
|
+
process.exitCode = await restart(parsed.flags, parsed.force);
|
|
6607
6941
|
break;
|
|
6608
|
-
}
|
|
6609
6942
|
case "logs": {
|
|
6610
6943
|
const env = applyEnvAliases(process.env);
|
|
6611
6944
|
process.exitCode = await runLogs(parsed.flags, defaultLogsDeps(logFiles(env), jsonlLogFile(env)));
|
|
@@ -6620,7 +6953,10 @@ async function main() {
|
|
|
6620
6953
|
...resolveEnrolToken(env) ? { token: resolveEnrolToken(env) } : {},
|
|
6621
6954
|
hubUrl: env.DAHRK_HUB_URL ?? DEFAULT_HUB_URL
|
|
6622
6955
|
},
|
|
6623
|
-
|
|
6956
|
+
// Stripped: stdout is a TTY here (the operator is watching), so the doctor correctly colours its
|
|
6957
|
+
// report - but this copy is going into a JSON file that someone will open in an editor and paste
|
|
6958
|
+
// into an issue, and escape codes have no business being in it.
|
|
6959
|
+
{ out: (line) => void doctorLines.push(stripAnsi(line)) }
|
|
6624
6960
|
);
|
|
6625
6961
|
return doctorLines;
|
|
6626
6962
|
};
|