dahrk-node 0.1.13 → 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 +879 -361
- package/package.json +2 -2
package/dist/main.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
// src/main.ts
|
|
4
4
|
import { execFileSync as execFileSync9 } from "child_process";
|
|
5
|
-
import { existsSync as
|
|
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
|
-
import { basename as basename2, join as
|
|
8
|
+
import { basename as basename2, join as join17 } from "path";
|
|
9
9
|
import { pathToFileURL } from "url";
|
|
10
10
|
|
|
11
11
|
// ../../packages/edge/src/ws-client.ts
|
|
@@ -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\//, "");
|
|
@@ -1590,6 +1590,7 @@ function createGitService(opts = {}) {
|
|
|
1590
1590
|
gitUrl: spec.gitUrl,
|
|
1591
1591
|
repo: spec.repo ?? spec.repoId,
|
|
1592
1592
|
baseBranch: spec.baseBranch,
|
|
1593
|
+
branch: sanitizeBranchName(spec.branch ?? `dahrk/${spec.runId}`),
|
|
1593
1594
|
worktreePath,
|
|
1594
1595
|
scratchPath: join3(worktreePath, ".skakel", "scratch")
|
|
1595
1596
|
});
|
|
@@ -1730,6 +1731,31 @@ function createGitService(opts = {}) {
|
|
|
1730
1731
|
}
|
|
1731
1732
|
return { headSha, pushed: true, nothingToCommit: !dirty, wipRef };
|
|
1732
1733
|
},
|
|
1734
|
+
async reconcileInterrupted(ref, opts2) {
|
|
1735
|
+
const { worktreePath } = ref;
|
|
1736
|
+
if (!existsSync(worktreePath) || !gitOk2(worktreePath, ["rev-parse", "--git-dir"])) {
|
|
1737
|
+
throw new Error(`worktree missing for reconcile: ${worktreePath}`);
|
|
1738
|
+
}
|
|
1739
|
+
const headSha = git(worktreePath, ["rev-parse", "HEAD"]).trim();
|
|
1740
|
+
const wipRef = sanitizeBranchName(opts2.branch);
|
|
1741
|
+
const { headSha: tailSha, dirty } = commitPending(worktreePath, opts2.message);
|
|
1742
|
+
if (!dirty) return { dirty: false, headSha, pushed: false };
|
|
1743
|
+
git(worktreePath, ["branch", "--force", wipRef, tailSha]);
|
|
1744
|
+
let pushed = false;
|
|
1745
|
+
const auth = opts2.credentialToken ? setupAuth(opts2.credentialToken) : void 0;
|
|
1746
|
+
const remote = opts2.credentialToken ? withTokenUser(ref.gitUrl) : ref.gitUrl;
|
|
1747
|
+
try {
|
|
1748
|
+
git(worktreePath, ["push", "--force", remote, `${tailSha}:refs/heads/${wipRef}`], netEnv(auth?.env));
|
|
1749
|
+
pushed = true;
|
|
1750
|
+
} catch (e) {
|
|
1751
|
+
log.warn(`could not push the preserved tail to ${wipRef}: ${e.message}`);
|
|
1752
|
+
} finally {
|
|
1753
|
+
auth?.cleanup();
|
|
1754
|
+
}
|
|
1755
|
+
git(worktreePath, ["reset", "--hard", headSha]);
|
|
1756
|
+
git(worktreePath, ["clean", "-fd", "--exclude", SCRATCH_DIR]);
|
|
1757
|
+
return { dirty: true, headSha, tailSha, wipRef, pushed };
|
|
1758
|
+
},
|
|
1733
1759
|
async openPrAmbient(ref, opts2) {
|
|
1734
1760
|
const ownerRepo = parseOwnerRepo(ref.gitUrl);
|
|
1735
1761
|
if (!ownerRepo) return { prError: `cannot derive owner/repo from ${ref.gitUrl}` };
|
|
@@ -1822,13 +1848,13 @@ function lastUsedMs(worktreePath) {
|
|
|
1822
1848
|
return newest;
|
|
1823
1849
|
}
|
|
1824
1850
|
function registeredWorktrees(mirror) {
|
|
1825
|
-
let
|
|
1851
|
+
let out2;
|
|
1826
1852
|
try {
|
|
1827
|
-
|
|
1853
|
+
out2 = gitOut(mirror, ["worktree", "list", "--porcelain"]);
|
|
1828
1854
|
} catch {
|
|
1829
1855
|
return [];
|
|
1830
1856
|
}
|
|
1831
|
-
return
|
|
1857
|
+
return out2.split("\n").filter((l) => l.startsWith("worktree ")).map((l) => canonical(l.slice(9).trim())).filter((p) => p && p !== canonical(mirror));
|
|
1832
1858
|
}
|
|
1833
1859
|
function createWorktreeReaper(opts) {
|
|
1834
1860
|
const log = opts.logger ?? noop;
|
|
@@ -1989,16 +2015,16 @@ import { createHash as createHash2 } from "crypto";
|
|
|
1989
2015
|
import { cpSync, existsSync as existsSync3, mkdirSync as mkdirSync3, mkdtempSync as mkdtempSync2, readdirSync as readdirSync2, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
1990
2016
|
import { dirname as dirname2, join as join6, relative, sep } from "path";
|
|
1991
2017
|
function readManifestFiles(dir) {
|
|
1992
|
-
const
|
|
2018
|
+
const out2 = [];
|
|
1993
2019
|
const walk = (cur) => {
|
|
1994
2020
|
for (const entry of readdirSync2(cur, { withFileTypes: true })) {
|
|
1995
2021
|
const abs = join6(cur, entry.name);
|
|
1996
2022
|
if (entry.isDirectory()) walk(abs);
|
|
1997
|
-
else
|
|
2023
|
+
else out2.push(relative(dir, abs).split(sep).join("/"));
|
|
1998
2024
|
}
|
|
1999
2025
|
};
|
|
2000
2026
|
walk(dir);
|
|
2001
|
-
return
|
|
2027
|
+
return out2.sort();
|
|
2002
2028
|
}
|
|
2003
2029
|
|
|
2004
2030
|
// ../../packages/executor-worktree/src/overlay.ts
|
|
@@ -2110,6 +2136,75 @@ function collectHealth(inputs) {
|
|
|
2110
2136
|
};
|
|
2111
2137
|
}
|
|
2112
2138
|
|
|
2139
|
+
// ../../packages/edge/src/job-ledger.ts
|
|
2140
|
+
import { chmodSync, existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync4, renameSync, unlinkSync, writeFileSync as writeFileSync5 } from "fs";
|
|
2141
|
+
import { dirname as dirname4, join as join8 } from "path";
|
|
2142
|
+
var FILE_MODE = 384;
|
|
2143
|
+
var DIR_MODE = 448;
|
|
2144
|
+
function nullJobLedger() {
|
|
2145
|
+
return { all: () => [], stale: () => [], upsert: () => {
|
|
2146
|
+
}, remove: () => {
|
|
2147
|
+
}, clear: () => {
|
|
2148
|
+
} };
|
|
2149
|
+
}
|
|
2150
|
+
var isEntry = (v) => {
|
|
2151
|
+
if (typeof v !== "object" || v === null) return false;
|
|
2152
|
+
const e = v;
|
|
2153
|
+
return typeof e["jobId"] === "string" && !!e["jobId"] && typeof e["runId"] === "string" && (e["kind"] === "stage" || e["kind"] === "push") && typeof e["startedAt"] === "number" && typeof e["nodePid"] === "number";
|
|
2154
|
+
};
|
|
2155
|
+
function fileJobLedger(file, warn = console.warn) {
|
|
2156
|
+
const read = () => {
|
|
2157
|
+
if (!existsSync5(file)) return [];
|
|
2158
|
+
try {
|
|
2159
|
+
const parsed = JSON.parse(readFileSync4(file, "utf8"));
|
|
2160
|
+
if (!Array.isArray(parsed)) return [];
|
|
2161
|
+
return parsed.filter(isEntry);
|
|
2162
|
+
} catch {
|
|
2163
|
+
return [];
|
|
2164
|
+
}
|
|
2165
|
+
};
|
|
2166
|
+
const write = (entries) => {
|
|
2167
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
2168
|
+
try {
|
|
2169
|
+
mkdirSync5(dirname4(file), { recursive: true, mode: DIR_MODE });
|
|
2170
|
+
writeFileSync5(tmp, `${JSON.stringify(entries, null, 2)}
|
|
2171
|
+
`, { mode: FILE_MODE });
|
|
2172
|
+
chmodSync(tmp, FILE_MODE);
|
|
2173
|
+
renameSync(tmp, file);
|
|
2174
|
+
} catch (e) {
|
|
2175
|
+
warn(`could not persist the job ledger to ${file}: ${e.message}`);
|
|
2176
|
+
try {
|
|
2177
|
+
if (existsSync5(tmp)) unlinkSync(tmp);
|
|
2178
|
+
} catch {
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
};
|
|
2182
|
+
return {
|
|
2183
|
+
all: read,
|
|
2184
|
+
stale: (currentPid) => read().filter((e) => e.nodePid !== currentPid),
|
|
2185
|
+
upsert(entry) {
|
|
2186
|
+
write([...read().filter((e) => e.jobId !== entry.jobId), entry]);
|
|
2187
|
+
},
|
|
2188
|
+
remove(jobId) {
|
|
2189
|
+
const next = read().filter((e) => e.jobId !== jobId);
|
|
2190
|
+
write(next);
|
|
2191
|
+
},
|
|
2192
|
+
clear() {
|
|
2193
|
+
write([]);
|
|
2194
|
+
}
|
|
2195
|
+
};
|
|
2196
|
+
}
|
|
2197
|
+
function announceableJobs(entries) {
|
|
2198
|
+
const out2 = [];
|
|
2199
|
+
for (const e of entries) {
|
|
2200
|
+
if (e.payloadVersion) out2.push({ jobId: e.jobId, payloadVersion: e.payloadVersion });
|
|
2201
|
+
}
|
|
2202
|
+
return out2;
|
|
2203
|
+
}
|
|
2204
|
+
function jobLedgerFile(stateDir2) {
|
|
2205
|
+
return join8(stateDir2, "jobs.json");
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2113
2208
|
// ../../packages/edge/src/log-shipper.ts
|
|
2114
2209
|
import { shouldShip } from "@dahrk/contracts";
|
|
2115
2210
|
var SHIP_BUFFER_MAX = 500;
|
|
@@ -2210,8 +2305,8 @@ function ceilingFromEnv(env) {
|
|
|
2210
2305
|
}
|
|
2211
2306
|
|
|
2212
2307
|
// ../../packages/edge/src/logger.ts
|
|
2213
|
-
import { closeSync, existsSync as
|
|
2214
|
-
import { join as
|
|
2308
|
+
import { closeSync, existsSync as existsSync6, mkdirSync as mkdirSync6, openSync, renameSync as renameSync2, rmSync as rmSync4, statSync as statSync2, writeSync } from "fs";
|
|
2309
|
+
import { join as join9 } from "path";
|
|
2215
2310
|
import pino from "pino";
|
|
2216
2311
|
|
|
2217
2312
|
// ../../packages/edge/src/redact.ts
|
|
@@ -2262,18 +2357,18 @@ function scrubValue(value, depth = 0) {
|
|
|
2262
2357
|
}
|
|
2263
2358
|
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return value;
|
|
2264
2359
|
if (value instanceof Error) {
|
|
2265
|
-
const
|
|
2266
|
-
|
|
2267
|
-
if (value.stack)
|
|
2268
|
-
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;
|
|
2269
2364
|
}
|
|
2270
2365
|
if (Array.isArray(value)) return value.map((v) => scrubValue(v, depth + 1));
|
|
2271
2366
|
if (typeof value === "object") {
|
|
2272
|
-
const
|
|
2367
|
+
const out2 = {};
|
|
2273
2368
|
for (const [k, v] of Object.entries(value)) {
|
|
2274
|
-
|
|
2369
|
+
out2[k] = isSensitiveKey(k) ? REDACTED : scrubValue(v, depth + 1);
|
|
2275
2370
|
}
|
|
2276
|
-
return
|
|
2371
|
+
return out2;
|
|
2277
2372
|
}
|
|
2278
2373
|
return value;
|
|
2279
2374
|
}
|
|
@@ -2307,7 +2402,7 @@ var RotatingFile = class {
|
|
|
2307
2402
|
open() {
|
|
2308
2403
|
try {
|
|
2309
2404
|
this.fd = openSync(this.path, "a");
|
|
2310
|
-
this.size =
|
|
2405
|
+
this.size = existsSync6(this.path) ? statSync2(this.path).size : 0;
|
|
2311
2406
|
} catch (e) {
|
|
2312
2407
|
this.disable(e);
|
|
2313
2408
|
}
|
|
@@ -2335,12 +2430,12 @@ var RotatingFile = class {
|
|
|
2335
2430
|
closeSync(this.fd);
|
|
2336
2431
|
this.fd = void 0;
|
|
2337
2432
|
const oldest = `${this.path}.${this.maxFiles}`;
|
|
2338
|
-
if (
|
|
2433
|
+
if (existsSync6(oldest)) rmSync4(oldest, { force: true });
|
|
2339
2434
|
for (let i = this.maxFiles - 1; i >= 1; i--) {
|
|
2340
2435
|
const from = `${this.path}.${i}`;
|
|
2341
|
-
if (
|
|
2436
|
+
if (existsSync6(from)) renameSync2(from, `${this.path}.${i + 1}`);
|
|
2342
2437
|
}
|
|
2343
|
-
if (
|
|
2438
|
+
if (existsSync6(this.path)) renameSync2(this.path, `${this.path}.1`);
|
|
2344
2439
|
this.open();
|
|
2345
2440
|
} catch (e) {
|
|
2346
2441
|
this.disable(e);
|
|
@@ -2400,8 +2495,8 @@ function createNodeLogger(opts = {}) {
|
|
|
2400
2495
|
if (level !== "silent") streams.push({ level, stream: humanSink });
|
|
2401
2496
|
if (opts.dir && fileLevel !== "silent") {
|
|
2402
2497
|
try {
|
|
2403
|
-
|
|
2404
|
-
const file = new RotatingFile(
|
|
2498
|
+
mkdirSync6(opts.dir, { recursive: true, mode: 448 });
|
|
2499
|
+
const file = new RotatingFile(join9(opts.dir, "node.jsonl"), opts.maxBytes ?? 10 * 1024 * 1024, opts.maxFiles ?? 5);
|
|
2405
2500
|
streams.push({ level: fileLevel, stream: file });
|
|
2406
2501
|
} catch (e) {
|
|
2407
2502
|
process.stderr.write(`dahrk: file logging disabled (${e.message})
|
|
@@ -2459,9 +2554,9 @@ function denyToolRule(tool3) {
|
|
|
2459
2554
|
// ../../packages/edge/src/stage-runner.ts
|
|
2460
2555
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
2461
2556
|
import { createHash as createHash3 } from "crypto";
|
|
2462
|
-
import { mkdirSync as
|
|
2557
|
+
import { mkdirSync as mkdirSync7, readdirSync as readdirSync3, readFileSync as readFileSync5, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
2463
2558
|
import { tmpdir as tmpdir4 } from "os";
|
|
2464
|
-
import { isAbsolute as isAbsolute3, join as
|
|
2559
|
+
import { isAbsolute as isAbsolute3, join as join11, relative as relative3, resolve as resolve2, sep as sep3 } from "path";
|
|
2465
2560
|
import { attachedDocBasename as attachedDocBasename2 } from "@dahrk/contracts";
|
|
2466
2561
|
|
|
2467
2562
|
// ../../packages/edge/src/builtins.ts
|
|
@@ -2469,9 +2564,9 @@ import { execFileSync as execFileSync4 } from "child_process";
|
|
|
2469
2564
|
|
|
2470
2565
|
// ../../packages/edge/src/fs-roots.ts
|
|
2471
2566
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
2472
|
-
import { existsSync as
|
|
2567
|
+
import { existsSync as existsSync7, realpathSync as realpathSync2 } from "fs";
|
|
2473
2568
|
import { homedir as homedir3, tmpdir as tmpdir3 } from "os";
|
|
2474
|
-
import { dirname as
|
|
2569
|
+
import { dirname as dirname5, isAbsolute as isAbsolute2, join as join10, relative as relative2, resolve, sep as sep2 } from "path";
|
|
2475
2570
|
function isUnder(root, target) {
|
|
2476
2571
|
const rel = relative2(resolve(root), resolve(target));
|
|
2477
2572
|
return rel === "" || !rel.startsWith(`..${sep2}`) && rel !== ".." && !isAbsolute2(rel);
|
|
@@ -2484,16 +2579,16 @@ function expandPath(raw, cwd) {
|
|
|
2484
2579
|
function realish(p) {
|
|
2485
2580
|
let head = p;
|
|
2486
2581
|
const tail = [];
|
|
2487
|
-
while (head !==
|
|
2488
|
-
if (
|
|
2582
|
+
while (head !== dirname5(head)) {
|
|
2583
|
+
if (existsSync7(head)) {
|
|
2489
2584
|
try {
|
|
2490
|
-
return
|
|
2585
|
+
return join10(realpathSync2.native(head), ...tail.reverse());
|
|
2491
2586
|
} catch {
|
|
2492
2587
|
return p;
|
|
2493
2588
|
}
|
|
2494
2589
|
}
|
|
2495
|
-
tail.push(head.slice(
|
|
2496
|
-
head =
|
|
2590
|
+
tail.push(head.slice(dirname5(head).length + 1));
|
|
2591
|
+
head = dirname5(head);
|
|
2497
2592
|
}
|
|
2498
2593
|
return p;
|
|
2499
2594
|
}
|
|
@@ -2507,11 +2602,11 @@ function withinRoots(raw, roots, need) {
|
|
|
2507
2602
|
}
|
|
2508
2603
|
function gitCommonDir(worktreePath) {
|
|
2509
2604
|
try {
|
|
2510
|
-
const
|
|
2605
|
+
const out2 = execFileSync3("git", ["rev-parse", "--path-format=absolute", "--git-common-dir"], {
|
|
2511
2606
|
cwd: worktreePath,
|
|
2512
2607
|
stdio: ["ignore", "pipe", "ignore"]
|
|
2513
2608
|
}).toString().trim();
|
|
2514
|
-
return
|
|
2609
|
+
return out2 || void 0;
|
|
2515
2610
|
} catch {
|
|
2516
2611
|
return void 0;
|
|
2517
2612
|
}
|
|
@@ -2520,8 +2615,8 @@ var pnpmStoreCache;
|
|
|
2520
2615
|
function pnpmStore() {
|
|
2521
2616
|
if (pnpmStoreCache) return pnpmStoreCache.path;
|
|
2522
2617
|
try {
|
|
2523
|
-
const
|
|
2524
|
-
pnpmStoreCache = { path:
|
|
2618
|
+
const out2 = execFileSync3("pnpm", ["store", "path"], { stdio: ["ignore", "pipe", "ignore"], timeout: 5e3 }).toString().trim();
|
|
2619
|
+
pnpmStoreCache = { path: out2 || void 0 };
|
|
2525
2620
|
} catch {
|
|
2526
2621
|
pnpmStoreCache = { path: void 0 };
|
|
2527
2622
|
}
|
|
@@ -2556,26 +2651,26 @@ function computeFsRoots(opts) {
|
|
|
2556
2651
|
"/System",
|
|
2557
2652
|
"/proc",
|
|
2558
2653
|
"/sys",
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2654
|
+
join10(home, ".gitconfig"),
|
|
2655
|
+
join10(home, ".config"),
|
|
2656
|
+
join10(home, ".npmrc"),
|
|
2657
|
+
join10(home, ".cache"),
|
|
2658
|
+
join10(home, "Library", "Caches"),
|
|
2659
|
+
join10(home, "Library", "pnpm"),
|
|
2660
|
+
join10(home, ".local", "share"),
|
|
2661
|
+
join10(home, ".nvm"),
|
|
2662
|
+
join10(home, ".volta"),
|
|
2663
|
+
join10(home, ".asdf"),
|
|
2664
|
+
join10(home, ".cargo"),
|
|
2665
|
+
join10(home, ".rustup"),
|
|
2571
2666
|
...splitRoots(process.env.DAHRK_FS_EXTRA_READ_ROOTS).map((p) => realish(resolve(p)))
|
|
2572
2667
|
].map(realish);
|
|
2573
2668
|
const deny = [
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2669
|
+
join10(home, ".ssh"),
|
|
2670
|
+
join10(home, ".aws"),
|
|
2671
|
+
join10(home, ".gnupg"),
|
|
2672
|
+
join10(home, ".config", "gcloud"),
|
|
2673
|
+
join10(home, "Library", "Keychains"),
|
|
2579
2674
|
"/Volumes",
|
|
2580
2675
|
"/etc/shadow",
|
|
2581
2676
|
"/etc/sudoers"
|
|
@@ -2817,12 +2912,12 @@ function scanCommand(cmd, roots, cwd = roots.cwd) {
|
|
|
2817
2912
|
}
|
|
2818
2913
|
plain.push(t);
|
|
2819
2914
|
}
|
|
2820
|
-
const
|
|
2821
|
-
if (
|
|
2822
|
-
cwdNow = expandPath(
|
|
2915
|
+
const out2 = scanSimple(plain, roots, cwdNow);
|
|
2916
|
+
if (out2.kind === "cd") {
|
|
2917
|
+
cwdNow = expandPath(out2.to, cwdNow);
|
|
2823
2918
|
continue;
|
|
2824
2919
|
}
|
|
2825
|
-
if (
|
|
2920
|
+
if (out2.kind !== "ok") return out2;
|
|
2826
2921
|
}
|
|
2827
2922
|
return { kind: "ok" };
|
|
2828
2923
|
}
|
|
@@ -2893,14 +2988,14 @@ var PATH_WRITE_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit", "N
|
|
|
2893
2988
|
function pathsIn(input) {
|
|
2894
2989
|
if (!input || typeof input !== "object") return [];
|
|
2895
2990
|
const o = input;
|
|
2896
|
-
const
|
|
2991
|
+
const out2 = [];
|
|
2897
2992
|
for (const f of PATH_FIELDS) {
|
|
2898
|
-
if (typeof o[f] === "string" && o[f])
|
|
2993
|
+
if (typeof o[f] === "string" && o[f]) out2.push(o[f]);
|
|
2899
2994
|
}
|
|
2900
2995
|
if (o.changes && typeof o.changes === "object") {
|
|
2901
|
-
|
|
2996
|
+
out2.push(...Object.keys(o.changes));
|
|
2902
2997
|
}
|
|
2903
|
-
return
|
|
2998
|
+
return out2;
|
|
2904
2999
|
}
|
|
2905
3000
|
function fsConfineRule(roots) {
|
|
2906
3001
|
return {
|
|
@@ -3097,32 +3192,32 @@ var attemptOf = (jobId) => {
|
|
|
3097
3192
|
};
|
|
3098
3193
|
var digest = (value) => `sha256:${createHash3("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 16)}`;
|
|
3099
3194
|
function writeScratchState(ref, job, attempt, status) {
|
|
3100
|
-
const statePath =
|
|
3195
|
+
const statePath = join11(ref.scratchPath, "state.json");
|
|
3101
3196
|
let state;
|
|
3102
3197
|
try {
|
|
3103
|
-
state = JSON.parse(
|
|
3198
|
+
state = JSON.parse(readFileSync5(statePath, "utf8"));
|
|
3104
3199
|
} catch {
|
|
3105
3200
|
state = { runId: job.runId, tenantId: job.tenantId, stages: {} };
|
|
3106
3201
|
}
|
|
3107
3202
|
state.stages[job.stageId] = { currentAttempt: attempt, status };
|
|
3108
|
-
|
|
3109
|
-
|
|
3203
|
+
mkdirSync7(ref.scratchPath, { recursive: true });
|
|
3204
|
+
writeFileSync6(statePath, JSON.stringify(state, null, 2));
|
|
3110
3205
|
}
|
|
3111
3206
|
function writeIssueContext(ref, issueContext) {
|
|
3112
3207
|
if (issueContext === void 0) return;
|
|
3113
3208
|
try {
|
|
3114
|
-
|
|
3115
|
-
|
|
3209
|
+
mkdirSync7(ref.scratchPath, { recursive: true });
|
|
3210
|
+
writeFileSync6(join11(ref.scratchPath, "issue.md"), issueContext);
|
|
3116
3211
|
} catch {
|
|
3117
3212
|
}
|
|
3118
3213
|
}
|
|
3119
3214
|
function writeAttachedDocuments(ref, docs) {
|
|
3120
3215
|
if (!docs || docs.length === 0) return;
|
|
3121
3216
|
try {
|
|
3122
|
-
const dir =
|
|
3123
|
-
|
|
3217
|
+
const dir = join11(ref.scratchPath, "docs");
|
|
3218
|
+
mkdirSync7(dir, { recursive: true });
|
|
3124
3219
|
for (const doc of docs) {
|
|
3125
|
-
|
|
3220
|
+
writeFileSync6(join11(dir, `${attachedDocBasename2(doc)}.md`), doc.content);
|
|
3126
3221
|
}
|
|
3127
3222
|
} catch {
|
|
3128
3223
|
}
|
|
@@ -3140,8 +3235,8 @@ ${lines.join("\n")}
|
|
|
3140
3235
|
function writeGuidance(ref, guidance) {
|
|
3141
3236
|
if (!guidance || guidance.length === 0) return;
|
|
3142
3237
|
try {
|
|
3143
|
-
|
|
3144
|
-
|
|
3238
|
+
mkdirSync7(ref.scratchPath, { recursive: true });
|
|
3239
|
+
writeFileSync6(join11(ref.scratchPath, "guidance.md"), renderGuidanceMarkdown(guidance));
|
|
3145
3240
|
} catch {
|
|
3146
3241
|
}
|
|
3147
3242
|
}
|
|
@@ -3167,7 +3262,7 @@ function readEmittedArtifact(ref, relPath) {
|
|
|
3167
3262
|
const path = resolveWorktreeRelativePath(ref, relPath);
|
|
3168
3263
|
if (!path) return void 0;
|
|
3169
3264
|
try {
|
|
3170
|
-
const raw =
|
|
3265
|
+
const raw = readFileSync5(path, "utf8");
|
|
3171
3266
|
return { path: relPath, content: capContent(raw) };
|
|
3172
3267
|
} catch {
|
|
3173
3268
|
return void 0;
|
|
@@ -3175,13 +3270,13 @@ function readEmittedArtifact(ref, relPath) {
|
|
|
3175
3270
|
}
|
|
3176
3271
|
function scanScratchOutput(ref, preferRel) {
|
|
3177
3272
|
try {
|
|
3178
|
-
const names = readdirSync3(
|
|
3273
|
+
const names = readdirSync3(join11(ref.worktreePath, SCRATCH_OUTPUT_DIR)).filter(
|
|
3179
3274
|
(n) => n.toLowerCase().endsWith(".md")
|
|
3180
3275
|
);
|
|
3181
3276
|
if (names.length === 0) return void 0;
|
|
3182
3277
|
const preferBase = preferRel?.split("/").pop();
|
|
3183
3278
|
const pick = preferBase && names.includes(preferBase) ? preferBase : names[0];
|
|
3184
|
-
const raw =
|
|
3279
|
+
const raw = readFileSync5(join11(ref.worktreePath, SCRATCH_OUTPUT_DIR, pick), "utf8");
|
|
3185
3280
|
if (raw.trim().length === 0) return void 0;
|
|
3186
3281
|
return { path: `${SCRATCH_OUTPUT_DIR}/${pick}`, content: capContent(raw) };
|
|
3187
3282
|
} catch {
|
|
@@ -3201,7 +3296,7 @@ function scanChangedMarkdown(ref) {
|
|
|
3201
3296
|
);
|
|
3202
3297
|
for (const rel of rels) {
|
|
3203
3298
|
try {
|
|
3204
|
-
const raw =
|
|
3299
|
+
const raw = readFileSync5(join11(ref.worktreePath, rel), "utf8");
|
|
3205
3300
|
if (raw.trim().length > 0) return { path: rel, content: capContent(raw) };
|
|
3206
3301
|
} catch {
|
|
3207
3302
|
}
|
|
@@ -3324,10 +3419,10 @@ function createStageRunner(deps) {
|
|
|
3324
3419
|
...job.workspaceRef.credentialToken ? { credentialToken: job.workspaceRef.credentialToken } : {}
|
|
3325
3420
|
});
|
|
3326
3421
|
} else {
|
|
3327
|
-
const base = deps.scratchRoot ??
|
|
3328
|
-
const worktreePath =
|
|
3329
|
-
const scratchPath =
|
|
3330
|
-
|
|
3422
|
+
const base = deps.scratchRoot ?? join11(tmpdir4(), "dahrk", "scratch");
|
|
3423
|
+
const worktreePath = join11(base, runId);
|
|
3424
|
+
const scratchPath = join11(worktreePath, ".skakel", "scratch");
|
|
3425
|
+
mkdirSync7(scratchPath, { recursive: true });
|
|
3331
3426
|
ref = { repoId: "", gitUrl: "", repo: "", baseBranch: "", worktreePath, scratchPath };
|
|
3332
3427
|
scratchOnly.add(runId);
|
|
3333
3428
|
}
|
|
@@ -3342,7 +3437,7 @@ function createStageRunner(deps) {
|
|
|
3342
3437
|
const slash = job.agentConfig.emitArtifact.lastIndexOf("/");
|
|
3343
3438
|
if (artifactDir && slash > 0) {
|
|
3344
3439
|
try {
|
|
3345
|
-
|
|
3440
|
+
mkdirSync7(join11(ref.worktreePath, job.agentConfig.emitArtifact.slice(0, slash)), { recursive: true });
|
|
3346
3441
|
} catch {
|
|
3347
3442
|
}
|
|
3348
3443
|
}
|
|
@@ -3370,8 +3465,8 @@ function createStageRunner(deps) {
|
|
|
3370
3465
|
if (!sink) return;
|
|
3371
3466
|
const base = { tenantId: job.tenantId, runId, stageId, attempt };
|
|
3372
3467
|
try {
|
|
3373
|
-
for (const name of readdirSync3(
|
|
3374
|
-
const bytes =
|
|
3468
|
+
for (const name of readdirSync3(join11(writer.dir, "blobs"))) {
|
|
3469
|
+
const bytes = readFileSync5(join11(writer.dir, "blobs", name));
|
|
3375
3470
|
const { url } = await sink.requestBlobUrl({
|
|
3376
3471
|
...base,
|
|
3377
3472
|
sha256: name,
|
|
@@ -3385,7 +3480,7 @@ function createStageRunner(deps) {
|
|
|
3385
3480
|
}
|
|
3386
3481
|
let archiveKey;
|
|
3387
3482
|
try {
|
|
3388
|
-
const bytes =
|
|
3483
|
+
const bytes = readFileSync5(join11(writer.dir, "trace.jsonl"));
|
|
3389
3484
|
const sha = createHash3("sha256").update(bytes).digest("hex");
|
|
3390
3485
|
const { key, url } = await sink.requestBlobUrl({
|
|
3391
3486
|
...base,
|
|
@@ -3492,10 +3587,10 @@ function createStageRunner(deps) {
|
|
|
3492
3587
|
return `${tool3}\0`;
|
|
3493
3588
|
}
|
|
3494
3589
|
};
|
|
3495
|
-
const policyReason = (
|
|
3496
|
-
const recordDeny = (
|
|
3590
|
+
const policyReason = (verdict2) => verdict2.reason ?? `tool action denied by ${verdict2.policy}`;
|
|
3591
|
+
const recordDeny = (verdict2, toolUseId) => {
|
|
3497
3592
|
denied = true;
|
|
3498
|
-
const reason = policyReason(
|
|
3593
|
+
const reason = policyReason(verdict2);
|
|
3499
3594
|
if (toolUseId) {
|
|
3500
3595
|
streamEvent(writer.append({ seq: 0, ts: nowIso2(), type: "observation", runtime, toolUseId, isError: true, output: { error: reason } }));
|
|
3501
3596
|
}
|
|
@@ -3503,13 +3598,13 @@ function createStageRunner(deps) {
|
|
|
3503
3598
|
deps.sendProgress({ jobId, kind: "error", ts: nowIso2(), text: reason });
|
|
3504
3599
|
};
|
|
3505
3600
|
const authorizeToolUse = (tool3, input) => {
|
|
3506
|
-
const
|
|
3507
|
-
if (
|
|
3508
|
-
recordDeny(
|
|
3601
|
+
const verdict2 = evaluatePolicies({ kind: "action", stageId, tool: tool3, input }, rules);
|
|
3602
|
+
if (verdict2.verdict === "deny") {
|
|
3603
|
+
recordDeny(verdict2);
|
|
3509
3604
|
} else {
|
|
3510
3605
|
authorisedActions.push(actionKey(tool3, input));
|
|
3511
3606
|
}
|
|
3512
|
-
return
|
|
3607
|
+
return verdict2;
|
|
3513
3608
|
};
|
|
3514
3609
|
const onTrace = (event) => {
|
|
3515
3610
|
if (event.type === "action") {
|
|
@@ -3518,14 +3613,14 @@ function createStageRunner(deps) {
|
|
|
3518
3613
|
if (authorised >= 0) {
|
|
3519
3614
|
authorisedActions.splice(authorised, 1);
|
|
3520
3615
|
} else {
|
|
3521
|
-
const
|
|
3616
|
+
const verdict2 = evaluatePolicies(
|
|
3522
3617
|
{ kind: "action", stageId, tool: event.tool, input: event.input },
|
|
3523
3618
|
rules
|
|
3524
3619
|
);
|
|
3525
|
-
if (
|
|
3620
|
+
if (verdict2.verdict === "deny") {
|
|
3526
3621
|
streamEvent(writer.append(event));
|
|
3527
|
-
recordDeny(
|
|
3528
|
-
if (
|
|
3622
|
+
recordDeny(verdict2, event.toolUseId);
|
|
3623
|
+
if (verdict2.policy === "fs_confine" && runtime !== "claude-code") escapedUnblocked = true;
|
|
3529
3624
|
return;
|
|
3530
3625
|
}
|
|
3531
3626
|
}
|
|
@@ -3771,6 +3866,7 @@ async function startEdgeNode(opts) {
|
|
|
3771
3866
|
const log = opts.logger ?? createNodeLogger({ level: levelFromEnv(process.env) });
|
|
3772
3867
|
const rules = opts.denyTool ? [denyToolRule(opts.denyTool)] : [];
|
|
3773
3868
|
const counters = new HealthCounters();
|
|
3869
|
+
const ledger = opts.jobLedger ?? nullJobLedger();
|
|
3774
3870
|
const shipper = opts.shipper;
|
|
3775
3871
|
const telemetryCeiling = ceilingFromEnv(process.env);
|
|
3776
3872
|
let currentRuntimes = opts.runtimes;
|
|
@@ -3869,6 +3965,39 @@ async function startEdgeNode(opts) {
|
|
|
3869
3965
|
};
|
|
3870
3966
|
const stageRunner = createStageRunner(stageDeps);
|
|
3871
3967
|
stageRunnerRef = stageRunner;
|
|
3968
|
+
const reconcileInterruptedJobs = async () => {
|
|
3969
|
+
const stale = ledger.stale(process.pid);
|
|
3970
|
+
if (!stale.length) return;
|
|
3971
|
+
log.warn({ count: stale.length }, `EDGE_INTERRUPTED:${stale.length} jobs died with the previous process`);
|
|
3972
|
+
for (const entry of stale) {
|
|
3973
|
+
const entryLog = log.child({ runId: entry.runId, jobId: entry.jobId, ...entry.stageId ? { stageId: entry.stageId } : {} });
|
|
3974
|
+
if (!entry.worktreePath || !entry.gitUrl) {
|
|
3975
|
+
entryLog.warn({}, `EDGE_INTERRUPTED_ABANDONED:${entry.jobId} no worktree recorded`);
|
|
3976
|
+
continue;
|
|
3977
|
+
}
|
|
3978
|
+
try {
|
|
3979
|
+
const r = await gitService.reconcileInterrupted(
|
|
3980
|
+
{ worktreePath: entry.worktreePath, gitUrl: entry.gitUrl },
|
|
3981
|
+
{
|
|
3982
|
+
message: `wip: work in progress when the node was interrupted (run ${entry.runId})`,
|
|
3983
|
+
branch: `dahrk/wip/${entry.runId}`
|
|
3984
|
+
}
|
|
3985
|
+
);
|
|
3986
|
+
if (r.dirty) {
|
|
3987
|
+
entryLog.warn(
|
|
3988
|
+
{ wipRef: r.wipRef, tailSha: r.tailSha, headSha: r.headSha, pushed: r.pushed },
|
|
3989
|
+
`EDGE_INTERRUPTED_RESET:${entry.jobId} preserved the uncommitted tail on ${r.wipRef}${r.pushed ? "" : " (locally only)"} and reset to ${r.headSha.slice(0, 8)}`
|
|
3990
|
+
);
|
|
3991
|
+
} else {
|
|
3992
|
+
entryLog.info({ headSha: r.headSha }, `EDGE_INTERRUPTED_CLEAN:${entry.jobId} worktree was already clean`);
|
|
3993
|
+
}
|
|
3994
|
+
} catch (e) {
|
|
3995
|
+
entryLog.warn({ err: e }, `EDGE_INTERRUPTED_ERROR:${entry.jobId} ${e.message}`);
|
|
3996
|
+
}
|
|
3997
|
+
}
|
|
3998
|
+
ledger.clear();
|
|
3999
|
+
};
|
|
4000
|
+
await reconcileInterruptedJobs();
|
|
3872
4001
|
void stageRunner.reapWorktrees().then((r) => {
|
|
3873
4002
|
counters.worktreeCount = Math.max(r.scanned - r.reaped.length, 0);
|
|
3874
4003
|
if (r.reaped.length) {
|
|
@@ -3895,7 +4024,22 @@ async function startEdgeNode(opts) {
|
|
|
3895
4024
|
// Advertise the resolved worktree base so the hub records each run's real worktree location in
|
|
3896
4025
|
// the projection instead of an advisory placeholder. Single-sourced from the git service so it
|
|
3897
4026
|
// always matches where worktrees actually land.
|
|
3898
|
-
worktreesDir: gitService.worktreesDir
|
|
4027
|
+
worktreesDir: gitService.worktreesDir,
|
|
4028
|
+
// What we are running RIGHT NOW (DHK-416), which is what lets a new hub build ADOPT an in-flight
|
|
4029
|
+
// stage rather than duplicate it (DHK-415). Without this the hub cannot tell "this node is midway
|
|
4030
|
+
// through the stage you are about to re-dispatch" from "this node is idle", so a hub roll or a
|
|
4031
|
+
// reconnect re-ran the stage from scratch.
|
|
4032
|
+
//
|
|
4033
|
+
// Always sent, even empty. Per the wire contract an ABSENT list means "unknown" (a node too old to
|
|
4034
|
+
// answer, for which the hub must keep its old re-dispatch behaviour), while an EMPTY list means "I
|
|
4035
|
+
// have nothing in flight" - a positive statement the hub can act on. We always know, so we always
|
|
4036
|
+
// say, and the two must not be conflated.
|
|
4037
|
+
//
|
|
4038
|
+
// Only genuinely-running jobs are in `running`: boot reconciliation has already dropped the entries
|
|
4039
|
+
// whose runner died with the previous process, so we never claim to be running something we are not.
|
|
4040
|
+
// `announceableJobs` drops what we cannot version-stamp - announcing such a job would make the hub
|
|
4041
|
+
// KILL it, not adopt it. See there.
|
|
4042
|
+
inFlightJobs: announceableJobs(running.values())
|
|
3899
4043
|
});
|
|
3900
4044
|
};
|
|
3901
4045
|
let reprobeTimer;
|
|
@@ -3915,7 +4059,15 @@ async function startEdgeNode(opts) {
|
|
|
3915
4059
|
}, opts.runtimeRecheckMs ?? 6e4);
|
|
3916
4060
|
reprobeTimer.unref?.();
|
|
3917
4061
|
}
|
|
3918
|
-
const running = /* @__PURE__ */ new
|
|
4062
|
+
const running = /* @__PURE__ */ new Map();
|
|
4063
|
+
const trackJob = (entry) => {
|
|
4064
|
+
running.set(entry.jobId, entry);
|
|
4065
|
+
ledger.upsert(entry);
|
|
4066
|
+
};
|
|
4067
|
+
const untrackJob = (jobId) => {
|
|
4068
|
+
running.delete(jobId);
|
|
4069
|
+
ledger.remove(jobId);
|
|
4070
|
+
};
|
|
3919
4071
|
const onMessage = async (raw) => {
|
|
3920
4072
|
const msg = decode(raw);
|
|
3921
4073
|
if (msg.type === "welcome") {
|
|
@@ -3984,8 +4136,20 @@ async function startEdgeNode(opts) {
|
|
|
3984
4136
|
pushLog.info({}, `PUSH_REPLAY:${job2.runId} ${job2.jobId}`);
|
|
3985
4137
|
return;
|
|
3986
4138
|
}
|
|
3987
|
-
running.add(job2.jobId);
|
|
3988
4139
|
const pushStartedAt = Date.now();
|
|
4140
|
+
trackJob({
|
|
4141
|
+
jobId: job2.jobId,
|
|
4142
|
+
runId: job2.runId,
|
|
4143
|
+
kind: "push",
|
|
4144
|
+
// No `payloadVersion`: `PushJob` does not carry one (DHK-415 added the field to `JobRequest`
|
|
4145
|
+
// only). It is ledgered anyway - boot reconciliation still has to clean its worktree - but it is
|
|
4146
|
+
// deliberately never announced. See `sendHello`.
|
|
4147
|
+
...job2.workspaceRef?.worktreePath ? { worktreePath: job2.workspaceRef.worktreePath } : {},
|
|
4148
|
+
...job2.branch ? { branch: job2.branch } : {},
|
|
4149
|
+
...job2.workspaceRef?.gitUrl ? { gitUrl: job2.workspaceRef.gitUrl } : {},
|
|
4150
|
+
startedAt: pushStartedAt,
|
|
4151
|
+
nodePid: process.pid
|
|
4152
|
+
});
|
|
3989
4153
|
pushLog.info({}, `PUSH_STARTED:${job2.runId} ${job2.branch}`);
|
|
3990
4154
|
try {
|
|
3991
4155
|
const result = await stageRunner.runPush(job2);
|
|
@@ -4006,7 +4170,7 @@ async function startEdgeNode(opts) {
|
|
|
4006
4170
|
send(frame);
|
|
4007
4171
|
pushLog.error({ err: e, durationMs: Date.now() - pushStartedAt }, `PUSH_ERROR:${job2.runId} ${e.message}`);
|
|
4008
4172
|
} finally {
|
|
4009
|
-
|
|
4173
|
+
untrackJob(job2.jobId);
|
|
4010
4174
|
}
|
|
4011
4175
|
return;
|
|
4012
4176
|
}
|
|
@@ -4030,9 +4194,23 @@ async function startEdgeNode(opts) {
|
|
|
4030
4194
|
jobLog.info({}, `JOB_REPLAY:${job.stageId} ${job.jobId}`);
|
|
4031
4195
|
return;
|
|
4032
4196
|
}
|
|
4033
|
-
running.add(job.jobId);
|
|
4034
|
-
counters.activeJobs = running.size;
|
|
4035
4197
|
const startedAt = Date.now();
|
|
4198
|
+
trackJob({
|
|
4199
|
+
jobId: job.jobId,
|
|
4200
|
+
runId: job.runId,
|
|
4201
|
+
kind: "stage",
|
|
4202
|
+
stageId: job.stageId,
|
|
4203
|
+
// The hub stamped this on dispatch. Persisting it is what lets a reconnecting node prove, after a
|
|
4204
|
+
// restart, that the job it is announcing was dispatched under a contract the adopting build can
|
|
4205
|
+
// still read. Announced without it, the hub's gate version-rejects the job rather than adopting it.
|
|
4206
|
+
...job.payloadVersion ? { payloadVersion: job.payloadVersion } : {},
|
|
4207
|
+
...job.workspaceRef?.worktreePath ? { worktreePath: job.workspaceRef.worktreePath } : {},
|
|
4208
|
+
...job.workspaceRef?.branch ? { branch: job.workspaceRef.branch } : {},
|
|
4209
|
+
...job.workspaceRef?.gitUrl ? { gitUrl: job.workspaceRef.gitUrl } : {},
|
|
4210
|
+
startedAt,
|
|
4211
|
+
nodePid: process.pid
|
|
4212
|
+
});
|
|
4213
|
+
counters.activeJobs = running.size;
|
|
4036
4214
|
jobLog.info({}, `JOB_STARTED:${job.stageId} ${job.jobId}`);
|
|
4037
4215
|
try {
|
|
4038
4216
|
const result = await stageRunner.runJob(job);
|
|
@@ -4054,7 +4232,7 @@ async function startEdgeNode(opts) {
|
|
|
4054
4232
|
counters.recordError(classifyError(e));
|
|
4055
4233
|
jobLog.error({ err: e, durationMs: Date.now() - startedAt }, `JOB_ERROR:${job.stageId} ${e.message}`);
|
|
4056
4234
|
} finally {
|
|
4057
|
-
|
|
4235
|
+
untrackJob(job.jobId);
|
|
4058
4236
|
counters.activeJobs = running.size;
|
|
4059
4237
|
}
|
|
4060
4238
|
};
|
|
@@ -4308,6 +4486,10 @@ function parseCli(argv) {
|
|
|
4308
4486
|
"hub-url": { type: "string" },
|
|
4309
4487
|
ephemeral: { type: "boolean", default: false },
|
|
4310
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 },
|
|
4311
4493
|
help: { type: "boolean", default: false }
|
|
4312
4494
|
},
|
|
4313
4495
|
allowPositionals: false
|
|
@@ -4317,6 +4499,7 @@ function parseCli(argv) {
|
|
|
4317
4499
|
}
|
|
4318
4500
|
if (values.help) return { kind: "help", command };
|
|
4319
4501
|
const ephemeral = values.ephemeral ?? false;
|
|
4502
|
+
const force = values.force ?? false;
|
|
4320
4503
|
const flags = {
|
|
4321
4504
|
...values.token ? { token: values.token } : {},
|
|
4322
4505
|
...values.name ? { name: values.name } : {},
|
|
@@ -4326,7 +4509,9 @@ function parseCli(argv) {
|
|
|
4326
4509
|
// a supervisor that restarts it on boot. It is a foreground node by definition.
|
|
4327
4510
|
foreground: (values.foreground ?? false) || ephemeral
|
|
4328
4511
|
};
|
|
4329
|
-
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 } };
|
|
4330
4515
|
return { kind: command, flags };
|
|
4331
4516
|
}
|
|
4332
4517
|
var LOG_LEVELS = ["trace", "debug", "info", "warn", "error", "fatal"];
|
|
@@ -4461,6 +4646,7 @@ function parseUpdate(flagArgs) {
|
|
|
4461
4646
|
args: flagArgs,
|
|
4462
4647
|
options: {
|
|
4463
4648
|
check: { type: "boolean", default: false },
|
|
4649
|
+
verbose: { type: "boolean", default: false },
|
|
4464
4650
|
help: { type: "boolean", default: false }
|
|
4465
4651
|
},
|
|
4466
4652
|
allowPositionals: false
|
|
@@ -4469,7 +4655,7 @@ function parseUpdate(flagArgs) {
|
|
|
4469
4655
|
return { kind: "error", message: e.message };
|
|
4470
4656
|
}
|
|
4471
4657
|
if (values.help) return { kind: "help", command: "update" };
|
|
4472
|
-
return { kind: "update", flags: { check: values.check ?? false } };
|
|
4658
|
+
return { kind: "update", flags: { check: values.check ?? false, verbose: values.verbose ?? false } };
|
|
4473
4659
|
}
|
|
4474
4660
|
function usage(bin, command) {
|
|
4475
4661
|
if (command === "start") {
|
|
@@ -4504,26 +4690,39 @@ function usage(bin, command) {
|
|
|
4504
4690
|
}
|
|
4505
4691
|
if (command === "stop") {
|
|
4506
4692
|
return [
|
|
4507
|
-
`Usage: ${bin} stop`,
|
|
4693
|
+
`Usage: ${bin} stop [--force]`,
|
|
4508
4694
|
"",
|
|
4509
4695
|
"Stop the node. It stays stopped across reboots until you run `dahrk start` again - a stop that",
|
|
4510
4696
|
"quietly undid itself at the next boot would not be a stop.",
|
|
4511
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
|
+
"",
|
|
4512
4701
|
"The service stays installed, so starting it again is instant. To remove it entirely, use",
|
|
4513
|
-
"`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)."
|
|
4514
4706
|
].join("\n");
|
|
4515
4707
|
}
|
|
4516
4708
|
if (command === "restart") {
|
|
4517
4709
|
return [
|
|
4518
4710
|
`Usage: ${bin} restart [options]`,
|
|
4519
4711
|
"",
|
|
4520
|
-
"Stop the node and start it again. Picks up a new client version, a rotated
|
|
4521
|
-
"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.",
|
|
4522
4720
|
"",
|
|
4523
4721
|
"Options:",
|
|
4524
4722
|
" --token <token> Re-enrol with a new token (or set DAHRK_ENROL_TOKEN).",
|
|
4525
4723
|
" --hub-url <url> Hub WebSocket URL (or set DAHRK_HUB_URL).",
|
|
4526
|
-
" --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)."
|
|
4527
4726
|
].join("\n");
|
|
4528
4727
|
}
|
|
4529
4728
|
if (command === "logs") {
|
|
@@ -4615,26 +4814,38 @@ function usage(bin, command) {
|
|
|
4615
4814
|
}
|
|
4616
4815
|
if (command === "status") {
|
|
4617
4816
|
return [
|
|
4618
|
-
`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.",
|
|
4619
4821
|
"",
|
|
4620
|
-
"
|
|
4621
|
-
"
|
|
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.",
|
|
4622
4826
|
"",
|
|
4623
|
-
"
|
|
4624
|
-
"
|
|
4625
|
-
"
|
|
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."
|
|
4626
4833
|
].join("\n");
|
|
4627
4834
|
}
|
|
4628
4835
|
if (command === "update") {
|
|
4629
4836
|
return [
|
|
4630
|
-
`Usage: ${bin} update [--check]`,
|
|
4837
|
+
`Usage: ${bin} update [--check] [--verbose]`,
|
|
4631
4838
|
"",
|
|
4632
4839
|
"Update this client in place to the latest published release. Detects how it was installed",
|
|
4633
4840
|
"(npm / Homebrew / curl) and runs the right upgrade, or prints the exact command when it cannot.",
|
|
4634
4841
|
"Reports current -> latest, and a no-op when already current.",
|
|
4635
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
|
+
"",
|
|
4636
4846
|
"Options:",
|
|
4637
|
-
" --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."
|
|
4638
4849
|
].join("\n");
|
|
4639
4850
|
}
|
|
4640
4851
|
return [
|
|
@@ -4643,10 +4854,10 @@ function usage(bin, command) {
|
|
|
4643
4854
|
"Commands:",
|
|
4644
4855
|
" start Run the node, and keep it running (installs the service). --token to enrol, once.",
|
|
4645
4856
|
" stop Stop the node. It stays stopped until the next `start`.",
|
|
4646
|
-
" restart Stop it and start it again.",
|
|
4857
|
+
" restart Stop it and start it again. This is what picks up a `dahrk update`.",
|
|
4647
4858
|
" logs Show what the node is doing (-f to follow, --run <id> to narrow to one run).",
|
|
4648
4859
|
" diagnose Write a support bundle you can read, and send on if you choose. Uploads nothing.",
|
|
4649
|
-
" status Is
|
|
4860
|
+
" status Is it up, is it enrolled, what is it working on? (local, dials nothing)",
|
|
4650
4861
|
" run Run a workflow locally (engine-backed), e.g. `run preflight`.",
|
|
4651
4862
|
" doctor Preflight checks: Node, runtimes, hub reachability, token validity.",
|
|
4652
4863
|
" service Install/uninstall the always-on service by hand (`start` does this for you).",
|
|
@@ -4660,8 +4871,82 @@ function usage(bin, command) {
|
|
|
4660
4871
|
}
|
|
4661
4872
|
|
|
4662
4873
|
// src/diagnose.ts
|
|
4663
|
-
import { existsSync as
|
|
4664
|
-
import { join as
|
|
4874
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync8, readdirSync as readdirSync4, readFileSync as readFileSync6, statSync as statSync3, writeFileSync as writeFileSync7 } from "fs";
|
|
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
|
|
4665
4950
|
var BUNDLE_LOG_LINES = 2e3;
|
|
4666
4951
|
function tailJsonl(raw, n) {
|
|
4667
4952
|
const records = [];
|
|
@@ -4702,7 +4987,7 @@ async function buildBundle(deps) {
|
|
|
4702
4987
|
if (deps.exists(deps.crashDir)) {
|
|
4703
4988
|
for (const name of deps.listDir(deps.crashDir).filter((f) => f.endsWith(".json")).sort()) {
|
|
4704
4989
|
try {
|
|
4705
|
-
crashes.push(JSON.parse(deps.readFile(
|
|
4990
|
+
crashes.push(JSON.parse(deps.readFile(join12(deps.crashDir, name))));
|
|
4706
4991
|
} catch (e) {
|
|
4707
4992
|
warnings.push(`could not read crash record ${name} (${e.message})`);
|
|
4708
4993
|
}
|
|
@@ -4759,24 +5044,23 @@ var defaultDiagnoseDeps = (paths, clientVersion, doctor) => ({
|
|
|
4759
5044
|
...paths,
|
|
4760
5045
|
clientVersion,
|
|
4761
5046
|
...doctor ? { doctor } : {},
|
|
4762
|
-
readFile: (p) =>
|
|
5047
|
+
readFile: (p) => readFileSync6(p, "utf8"),
|
|
4763
5048
|
listDir: (p) => readdirSync4(p),
|
|
4764
|
-
exists: (p) =>
|
|
5049
|
+
exists: (p) => existsSync8(p),
|
|
4765
5050
|
writeFile: (p, content) => {
|
|
4766
|
-
const dir =
|
|
4767
|
-
if (!
|
|
4768
|
-
|
|
5051
|
+
const dir = join12(p, "..");
|
|
5052
|
+
if (!existsSync8(dir)) mkdirSync8(dir, { recursive: true });
|
|
5053
|
+
writeFileSync7(p, content, { mode: 384 });
|
|
4769
5054
|
},
|
|
4770
|
-
out
|
|
4771
|
-
`)
|
|
5055
|
+
out
|
|
4772
5056
|
});
|
|
4773
5057
|
function defaultBundlePath(cwd, now) {
|
|
4774
|
-
return
|
|
5058
|
+
return join12(cwd, `dahrk-diagnose-${now.toISOString().replace(/[:.]/g, "-")}.json`);
|
|
4775
5059
|
}
|
|
4776
5060
|
|
|
4777
5061
|
// src/doctor.ts
|
|
4778
5062
|
var MIN_NODE_MAJOR = 22;
|
|
4779
|
-
var
|
|
5063
|
+
var LEVEL = { pass: "ok", warn: "warn", fail: "fail" };
|
|
4780
5064
|
function checkNode(nodeVersion) {
|
|
4781
5065
|
const major = Number.parseInt(nodeVersion.replace(/^v/, "").split(".")[0] ?? "", 10);
|
|
4782
5066
|
if (Number.isNaN(major)) {
|
|
@@ -4874,16 +5158,15 @@ function checkToken(tokenPresent, hubUrl, probe2) {
|
|
|
4874
5158
|
function formatReport(checks) {
|
|
4875
5159
|
const failed = checks.filter((c) => c.status === "fail").length;
|
|
4876
5160
|
const warned = checks.filter((c) => c.status === "warn").length;
|
|
4877
|
-
const lines = checks.map((c) =>
|
|
4878
|
-
const summary = failed > 0 ?
|
|
4879
|
-
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");
|
|
4880
5164
|
}
|
|
4881
5165
|
var defaultDeps = () => ({
|
|
4882
5166
|
nodeVersion: process.versions.node,
|
|
4883
5167
|
probeRuntimes: probeRuntimeStatuses,
|
|
4884
5168
|
probeHub,
|
|
4885
|
-
out
|
|
4886
|
-
`)
|
|
5169
|
+
out
|
|
4887
5170
|
});
|
|
4888
5171
|
async function runDoctor(inputs, deps = {}) {
|
|
4889
5172
|
const d = { ...defaultDeps(), ...deps };
|
|
@@ -4906,8 +5189,8 @@ async function runDoctor(inputs, deps = {}) {
|
|
|
4906
5189
|
}
|
|
4907
5190
|
|
|
4908
5191
|
// src/lock.ts
|
|
4909
|
-
import { existsSync as
|
|
4910
|
-
import { dirname as
|
|
5192
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync9, readFileSync as readFileSync7, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "fs";
|
|
5193
|
+
import { dirname as dirname6 } from "path";
|
|
4911
5194
|
function parseLock(content) {
|
|
4912
5195
|
if (!content) return void 0;
|
|
4913
5196
|
const pid = Number(content.trim());
|
|
@@ -4939,14 +5222,14 @@ var defaultLockDeps = (file) => ({
|
|
|
4939
5222
|
pid: process.pid,
|
|
4940
5223
|
readFile: (path) => {
|
|
4941
5224
|
try {
|
|
4942
|
-
return
|
|
5225
|
+
return readFileSync7(path, "utf8");
|
|
4943
5226
|
} catch {
|
|
4944
5227
|
return void 0;
|
|
4945
5228
|
}
|
|
4946
5229
|
},
|
|
4947
5230
|
writeFile: (path, content) => {
|
|
4948
|
-
if (!
|
|
4949
|
-
|
|
5231
|
+
if (!existsSync9(dirname6(path))) mkdirSync9(dirname6(path), { recursive: true, mode: 448 });
|
|
5232
|
+
writeFileSync8(path, content);
|
|
4950
5233
|
},
|
|
4951
5234
|
removeFile: (path) => rmSync6(path, { force: true }),
|
|
4952
5235
|
isAlive
|
|
@@ -4954,7 +5237,7 @@ var defaultLockDeps = (file) => ({
|
|
|
4954
5237
|
|
|
4955
5238
|
// src/logs.ts
|
|
4956
5239
|
import { spawn } from "child_process";
|
|
4957
|
-
import { copyFileSync, existsSync as
|
|
5240
|
+
import { copyFileSync, existsSync as existsSync10, readFileSync as readFileSync8, statSync as statSync4, truncateSync } from "fs";
|
|
4958
5241
|
var MAX_LOG_BYTES = 10 * 1024 * 1024;
|
|
4959
5242
|
function logsCommand(inputs) {
|
|
4960
5243
|
return [
|
|
@@ -4968,15 +5251,15 @@ function logsCommand(inputs) {
|
|
|
4968
5251
|
var LEVEL_RANK = { trace: 10, debug: 20, info: 30, warn: 40, error: 50, fatal: 60 };
|
|
4969
5252
|
var levelName = (n) => Object.entries(LEVEL_RANK).find(([, rank]) => rank === n)?.[0] ?? String(n);
|
|
4970
5253
|
function parseRecords(raw) {
|
|
4971
|
-
const
|
|
5254
|
+
const out2 = [];
|
|
4972
5255
|
for (const line of raw.split("\n")) {
|
|
4973
5256
|
if (!line.trim()) continue;
|
|
4974
5257
|
try {
|
|
4975
|
-
|
|
5258
|
+
out2.push(JSON.parse(line));
|
|
4976
5259
|
} catch {
|
|
4977
5260
|
}
|
|
4978
5261
|
}
|
|
4979
|
-
return
|
|
5262
|
+
return out2;
|
|
4980
5263
|
}
|
|
4981
5264
|
function filterRecords(records, q) {
|
|
4982
5265
|
const min = q.level ? LEVEL_RANK[q.level] ?? 0 : 0;
|
|
@@ -5035,7 +5318,7 @@ async function runStructuredLogs(inputs, deps) {
|
|
|
5035
5318
|
}
|
|
5036
5319
|
function rotateIfLarge(file, maxBytes = MAX_LOG_BYTES) {
|
|
5037
5320
|
try {
|
|
5038
|
-
if (!
|
|
5321
|
+
if (!existsSync10(file) || statSync4(file).size <= maxBytes) return;
|
|
5039
5322
|
copyFileSync(file, `${file}.1`);
|
|
5040
5323
|
truncateSync(file, 0);
|
|
5041
5324
|
} catch {
|
|
@@ -5044,26 +5327,25 @@ function rotateIfLarge(file, maxBytes = MAX_LOG_BYTES) {
|
|
|
5044
5327
|
var defaultLogsDeps = (files, jsonlFile) => ({
|
|
5045
5328
|
files,
|
|
5046
5329
|
jsonlFile,
|
|
5047
|
-
fileExists: (path) =>
|
|
5048
|
-
readFile: (path) =>
|
|
5330
|
+
fileExists: (path) => existsSync10(path),
|
|
5331
|
+
readFile: (path) => readFileSync8(path, "utf8"),
|
|
5049
5332
|
run: (argv) => new Promise((resolve3) => {
|
|
5050
5333
|
const [cmd, ...args] = argv;
|
|
5051
5334
|
const child = spawn(cmd, args, { stdio: "inherit" });
|
|
5052
5335
|
child.on("error", () => resolve3(1));
|
|
5053
5336
|
child.on("close", (code) => resolve3(code ?? 0));
|
|
5054
5337
|
}),
|
|
5055
|
-
out
|
|
5056
|
-
`)
|
|
5338
|
+
out
|
|
5057
5339
|
});
|
|
5058
5340
|
|
|
5059
5341
|
// src/process-safety.ts
|
|
5060
|
-
import { mkdirSync as
|
|
5061
|
-
import { join as
|
|
5342
|
+
import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync9 } from "fs";
|
|
5343
|
+
import { join as join13 } from "path";
|
|
5062
5344
|
function writeCrashRecord(dir, record) {
|
|
5063
5345
|
try {
|
|
5064
|
-
|
|
5065
|
-
const file =
|
|
5066
|
-
|
|
5346
|
+
mkdirSync10(dir, { recursive: true, mode: 448 });
|
|
5347
|
+
const file = join13(dir, `${record.at.replace(/[:.]/g, "-")}.json`);
|
|
5348
|
+
writeFileSync9(file, `${JSON.stringify(record, null, 2)}
|
|
5067
5349
|
`, { mode: 384 });
|
|
5068
5350
|
return file;
|
|
5069
5351
|
} catch {
|
|
@@ -5118,9 +5400,9 @@ function installProcessSafetyNet(opts) {
|
|
|
5118
5400
|
// src/preflight.ts
|
|
5119
5401
|
import { execFileSync as execFileSync6 } from "child_process";
|
|
5120
5402
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
5121
|
-
import { accessSync, constants as fsConstants, existsSync as
|
|
5403
|
+
import { accessSync, constants as fsConstants, existsSync as existsSync11, readdirSync as readdirSync5, statfsSync as statfsSync2 } from "fs";
|
|
5122
5404
|
import { homedir as homedir4 } from "os";
|
|
5123
|
-
import { join as
|
|
5405
|
+
import { join as join14 } from "path";
|
|
5124
5406
|
var REPORT_BASE_URL = "https://app.dahrk.ai/r";
|
|
5125
5407
|
var LOW_DISK_BYTES = 512 * 1024 * 1024;
|
|
5126
5408
|
var PREFLIGHT_STAGES = [
|
|
@@ -5184,15 +5466,18 @@ function worst(checks) {
|
|
|
5184
5466
|
if (checks.some((c) => c.status === "warn")) return "warn";
|
|
5185
5467
|
return "pass";
|
|
5186
5468
|
}
|
|
5187
|
-
function renderStage(index, total,
|
|
5188
|
-
const head = `[${index}/${total}] ${
|
|
5189
|
-
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");
|
|
5190
5472
|
if (findings.length === 0) {
|
|
5191
|
-
|
|
5473
|
+
out2(`${head} ${symbol("ok")}`);
|
|
5192
5474
|
return;
|
|
5193
5475
|
}
|
|
5194
|
-
|
|
5195
|
-
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
|
+
}
|
|
5196
5481
|
}
|
|
5197
5482
|
async function runPreflight(inputs, deps = {}) {
|
|
5198
5483
|
const d = { ...defaultDeps2(), ...deps };
|
|
@@ -5244,8 +5529,7 @@ var defaultDeps2 = () => ({
|
|
|
5244
5529
|
probeHub,
|
|
5245
5530
|
gatherHost: gatherHostFacts,
|
|
5246
5531
|
newRunId: () => randomUUID2(),
|
|
5247
|
-
out
|
|
5248
|
-
`)
|
|
5532
|
+
out
|
|
5249
5533
|
});
|
|
5250
5534
|
function commandPresent(cmd) {
|
|
5251
5535
|
try {
|
|
@@ -5257,8 +5541,8 @@ function commandPresent(cmd) {
|
|
|
5257
5541
|
}
|
|
5258
5542
|
function sshKeyPresent() {
|
|
5259
5543
|
try {
|
|
5260
|
-
const dir =
|
|
5261
|
-
if (
|
|
5544
|
+
const dir = join14(homedir4(), ".ssh");
|
|
5545
|
+
if (existsSync11(dir) && readdirSync5(dir).some((f) => f.endsWith(".pub"))) return true;
|
|
5262
5546
|
} catch {
|
|
5263
5547
|
}
|
|
5264
5548
|
try {
|
|
@@ -5277,12 +5561,12 @@ function writable(dir) {
|
|
|
5277
5561
|
}
|
|
5278
5562
|
}
|
|
5279
5563
|
function worktreeRoot(env) {
|
|
5280
|
-
return env.DAHRK_WORKTREES_DIR ??
|
|
5564
|
+
return env.DAHRK_WORKTREES_DIR ?? join14(env.DAHRK_STATE_DIR ?? join14(homedir4(), ".dahrk"), "worktrees");
|
|
5281
5565
|
}
|
|
5282
5566
|
function nearestExisting(dir) {
|
|
5283
5567
|
let cur = dir;
|
|
5284
|
-
while (!
|
|
5285
|
-
const parent =
|
|
5568
|
+
while (!existsSync11(cur)) {
|
|
5569
|
+
const parent = join14(cur, "..");
|
|
5286
5570
|
if (parent === cur) break;
|
|
5287
5571
|
cur = parent;
|
|
5288
5572
|
}
|
|
@@ -5333,52 +5617,52 @@ function gatherHostFacts(repoPath) {
|
|
|
5333
5617
|
|
|
5334
5618
|
// src/service.ts
|
|
5335
5619
|
import { execFileSync as execFileSync7 } from "child_process";
|
|
5336
|
-
import { chmodSync as
|
|
5620
|
+
import { chmodSync as chmodSync3, existsSync as existsSync13, mkdirSync as mkdirSync12, readFileSync as readFileSync10, realpathSync as realpathSync3, rmSync as rmSync7, writeFileSync as writeFileSync11 } from "fs";
|
|
5337
5621
|
import { homedir as homedir6, platform as osPlatform3, userInfo } from "os";
|
|
5338
|
-
import { join as
|
|
5622
|
+
import { join as join16 } from "path";
|
|
5339
5623
|
|
|
5340
5624
|
// src/state.ts
|
|
5341
|
-
import { chmodSync, existsSync as
|
|
5625
|
+
import { chmodSync as chmodSync2, existsSync as existsSync12, mkdirSync as mkdirSync11, readFileSync as readFileSync9, writeFileSync as writeFileSync10 } from "fs";
|
|
5342
5626
|
import { homedir as homedir5 } from "os";
|
|
5343
|
-
import { join as
|
|
5627
|
+
import { join as join15 } from "path";
|
|
5344
5628
|
var RUNTIMES = ["claude-code", "codex", "pi"];
|
|
5345
5629
|
var isRuntime = (v) => RUNTIMES.includes(v);
|
|
5346
5630
|
var STRING_FIELDS = ["nodeId", "enrolToken", "name", "tenantId", "updateCheckedAt", "updateLatest"];
|
|
5347
5631
|
var isDesired = (v) => v === "running" || v === "stopped";
|
|
5348
|
-
var
|
|
5349
|
-
var
|
|
5632
|
+
var FILE_MODE2 = 384;
|
|
5633
|
+
var DIR_MODE2 = 448;
|
|
5350
5634
|
function stateDir(env) {
|
|
5351
|
-
return env.DAHRK_STATE_DIR ??
|
|
5635
|
+
return env.DAHRK_STATE_DIR ?? join15(homedir5(), ".dahrk");
|
|
5352
5636
|
}
|
|
5353
5637
|
function legacyStateDir(env) {
|
|
5354
|
-
return env.DAHRK_STATE_DIR ? void 0 :
|
|
5638
|
+
return env.DAHRK_STATE_DIR ? void 0 : join15(homedir5(), ".skakel");
|
|
5355
5639
|
}
|
|
5356
5640
|
function stateFile(env) {
|
|
5357
|
-
return
|
|
5641
|
+
return join15(stateDir(env), "node.json");
|
|
5358
5642
|
}
|
|
5359
5643
|
function logDir(env) {
|
|
5360
|
-
return
|
|
5644
|
+
return join15(stateDir(env), "logs");
|
|
5361
5645
|
}
|
|
5362
5646
|
function logFiles(env) {
|
|
5363
5647
|
const dir = logDir(env);
|
|
5364
|
-
return { out:
|
|
5648
|
+
return { out: join15(dir, "node.out.log"), err: join15(dir, "node.err.log") };
|
|
5365
5649
|
}
|
|
5366
5650
|
function jsonlLogFile(env) {
|
|
5367
|
-
return
|
|
5651
|
+
return join15(logDir(env), "node.jsonl");
|
|
5368
5652
|
}
|
|
5369
5653
|
function crashDir(env) {
|
|
5370
|
-
return
|
|
5654
|
+
return join15(logDir(env), "crashes");
|
|
5371
5655
|
}
|
|
5372
5656
|
function lockFile(env) {
|
|
5373
|
-
return
|
|
5657
|
+
return join15(stateDir(env), "node.pid");
|
|
5374
5658
|
}
|
|
5375
5659
|
function setDesired(env, desired) {
|
|
5376
5660
|
writeState(env, { desired });
|
|
5377
5661
|
}
|
|
5378
5662
|
function readState(file) {
|
|
5379
|
-
if (!
|
|
5663
|
+
if (!existsSync12(file)) return {};
|
|
5380
5664
|
try {
|
|
5381
|
-
const parsed = JSON.parse(
|
|
5665
|
+
const parsed = JSON.parse(readFileSync9(file, "utf8"));
|
|
5382
5666
|
const state = {};
|
|
5383
5667
|
for (const key of STRING_FIELDS) {
|
|
5384
5668
|
const value = parsed[key];
|
|
@@ -5398,11 +5682,11 @@ function writeState(env, patch) {
|
|
|
5398
5682
|
const dir = stateDir(env);
|
|
5399
5683
|
const file = stateFile(env);
|
|
5400
5684
|
try {
|
|
5401
|
-
|
|
5685
|
+
mkdirSync11(dir, { recursive: true, mode: DIR_MODE2 });
|
|
5402
5686
|
const next = { ...readState(file), ...patch };
|
|
5403
|
-
|
|
5404
|
-
`, { mode:
|
|
5405
|
-
|
|
5687
|
+
writeFileSync10(file, `${JSON.stringify(next, null, 2)}
|
|
5688
|
+
`, { mode: FILE_MODE2 });
|
|
5689
|
+
chmodSync2(file, FILE_MODE2);
|
|
5406
5690
|
} catch (e) {
|
|
5407
5691
|
console.warn(`could not persist node state to ${file}: ${e.message}`);
|
|
5408
5692
|
}
|
|
@@ -5481,9 +5765,9 @@ ${envEntries}
|
|
|
5481
5765
|
<key>ThrottleInterval</key>
|
|
5482
5766
|
<integer>10</integer>
|
|
5483
5767
|
<key>StandardOutPath</key>
|
|
5484
|
-
<string>${xmlEscape(
|
|
5768
|
+
<string>${xmlEscape(join16(inputs.logDir, "node.out.log"))}</string>
|
|
5485
5769
|
<key>StandardErrorPath</key>
|
|
5486
|
-
<string>${xmlEscape(
|
|
5770
|
+
<string>${xmlEscape(join16(inputs.logDir, "node.err.log"))}</string>
|
|
5487
5771
|
</dict>
|
|
5488
5772
|
</plist>
|
|
5489
5773
|
`;
|
|
@@ -5506,8 +5790,8 @@ Type=simple
|
|
|
5506
5790
|
ExecStart=${exec}
|
|
5507
5791
|
${envLines}
|
|
5508
5792
|
WorkingDirectory=${inputs.homeDir}
|
|
5509
|
-
StandardOutput=append:${
|
|
5510
|
-
StandardError=append:${
|
|
5793
|
+
StandardOutput=append:${join16(inputs.logDir, "node.out.log")}
|
|
5794
|
+
StandardError=append:${join16(inputs.logDir, "node.err.log")}
|
|
5511
5795
|
Restart=on-failure
|
|
5512
5796
|
RestartSec=3
|
|
5513
5797
|
RestartPreventExitStatus=78
|
|
@@ -5518,7 +5802,7 @@ WantedBy=default.target
|
|
|
5518
5802
|
}
|
|
5519
5803
|
function buildPlan(inputs) {
|
|
5520
5804
|
if (inputs.manager === "launchd") {
|
|
5521
|
-
const filePath2 =
|
|
5805
|
+
const filePath2 = join16(inputs.homeDir, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
5522
5806
|
return {
|
|
5523
5807
|
manager: "launchd",
|
|
5524
5808
|
label: LAUNCHD_LABEL,
|
|
@@ -5535,7 +5819,7 @@ function buildPlan(inputs) {
|
|
|
5535
5819
|
logHint: "dahrk logs -f"
|
|
5536
5820
|
};
|
|
5537
5821
|
}
|
|
5538
|
-
const filePath =
|
|
5822
|
+
const filePath = join16(inputs.homeDir, ".config", "systemd", "user", SYSTEMD_UNIT);
|
|
5539
5823
|
const user = userInfo().username;
|
|
5540
5824
|
return {
|
|
5541
5825
|
manager: "systemd",
|
|
@@ -5565,7 +5849,7 @@ function unitIsCurrent(plan, onDisk) {
|
|
|
5565
5849
|
return onDisk === plan.content;
|
|
5566
5850
|
}
|
|
5567
5851
|
function unitPath(manager, homeDir) {
|
|
5568
|
-
return manager === "launchd" ?
|
|
5852
|
+
return manager === "launchd" ? join16(homeDir, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`) : join16(homeDir, ".config", "systemd", "user", SYSTEMD_UNIT);
|
|
5569
5853
|
}
|
|
5570
5854
|
function statusCommand(manager) {
|
|
5571
5855
|
return manager === "launchd" ? ["launchctl", "list", LAUNCHD_LABEL] : ["systemctl", "--user", "show", SYSTEMD_UNIT, "-p", "ActiveState", "-p", "MainPID"];
|
|
@@ -5582,16 +5866,18 @@ function parseServiceStatus(manager, unitExists, probe2) {
|
|
|
5582
5866
|
const running = active === "active";
|
|
5583
5867
|
return running && pid > 0 ? { installed: true, running: true, pid } : { installed: true, running };
|
|
5584
5868
|
}
|
|
5585
|
-
function printUnsupported(
|
|
5586
|
-
|
|
5587
|
-
|
|
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)."));
|
|
5588
5873
|
return 1;
|
|
5589
5874
|
}
|
|
5590
5875
|
function runCommands(commands, d) {
|
|
5591
5876
|
for (const cmd of commands) {
|
|
5592
|
-
const code = d.run(cmd.argv);
|
|
5877
|
+
const { code, output } = d.run(cmd.argv);
|
|
5593
5878
|
if (code !== 0 && !cmd.ignoreFailure) {
|
|
5594
|
-
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)}`);
|
|
5595
5881
|
return code;
|
|
5596
5882
|
}
|
|
5597
5883
|
}
|
|
@@ -5599,13 +5885,13 @@ function runCommands(commands, d) {
|
|
|
5599
5885
|
}
|
|
5600
5886
|
async function runServiceInstall(inputs, deps = {}) {
|
|
5601
5887
|
const d = { ...defaultDeps3(), ...deps };
|
|
5602
|
-
d.out("dahrk service install");
|
|
5603
5888
|
d.out("");
|
|
5604
5889
|
const manager = detectManager(d.platform);
|
|
5605
5890
|
if (manager === "unsupported") return printUnsupported(d.out);
|
|
5606
5891
|
if (!inputs.token) {
|
|
5607
|
-
d.out("No enrolment token
|
|
5608
|
-
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."));
|
|
5609
5895
|
return 2;
|
|
5610
5896
|
}
|
|
5611
5897
|
const plan = buildPlan({
|
|
@@ -5624,26 +5910,41 @@ async function runServiceInstall(inputs, deps = {}) {
|
|
|
5624
5910
|
d.mkdirp(dirOf(plan.filePath));
|
|
5625
5911
|
d.writeFile(plan.filePath, plan.content);
|
|
5626
5912
|
} catch (e) {
|
|
5627
|
-
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));
|
|
5628
5916
|
return 1;
|
|
5629
5917
|
}
|
|
5630
|
-
d.out(`Wrote ${plan.manager} service: ${plan.filePath}`);
|
|
5631
5918
|
const code = runCommands(plan.installCommands, d);
|
|
5632
|
-
d.out("");
|
|
5633
5919
|
if (code !== 0) {
|
|
5634
|
-
d.out(
|
|
5635
|
-
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.`));
|
|
5636
5922
|
return code;
|
|
5637
5923
|
}
|
|
5638
|
-
d.out("Installed. The node
|
|
5639
|
-
d.out(
|
|
5640
|
-
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"]]));
|
|
5641
5929
|
return 0;
|
|
5642
5930
|
}
|
|
5643
5931
|
function availabilityCommand(manager) {
|
|
5644
5932
|
return manager === "systemd" ? ["systemctl", "--user", "show", "-p", "Version"] : void 0;
|
|
5645
5933
|
}
|
|
5646
|
-
|
|
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 = {}) {
|
|
5647
5948
|
const d = { ...defaultDeps3(), ...deps };
|
|
5648
5949
|
const manager = detectManager(d.platform);
|
|
5649
5950
|
if (manager === "unsupported") {
|
|
@@ -5656,8 +5957,9 @@ async function runNodeStart(inputs, deps = {}) {
|
|
|
5656
5957
|
const filePath = unitPath(manager, d.homeDir);
|
|
5657
5958
|
const unitExists = d.fileExists(filePath);
|
|
5658
5959
|
if (!inputs.token && !unitExists) {
|
|
5659
|
-
d.out("No enrolment token
|
|
5660
|
-
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."));
|
|
5661
5963
|
return { kind: "error", code: 2 };
|
|
5662
5964
|
}
|
|
5663
5965
|
const plan = buildPlan({
|
|
@@ -5673,12 +5975,10 @@ async function runNodeStart(inputs, deps = {}) {
|
|
|
5673
5975
|
});
|
|
5674
5976
|
const canRender = inputs.token !== void 0;
|
|
5675
5977
|
const current = canRender && unitExists && unitIsCurrent(plan, d.readFile(filePath));
|
|
5676
|
-
|
|
5677
|
-
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
d.out(` logs: ${plan.logHint}`);
|
|
5681
|
-
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 };
|
|
5682
5982
|
}
|
|
5683
5983
|
if (canRender && !current) {
|
|
5684
5984
|
try {
|
|
@@ -5689,28 +5989,65 @@ async function runNodeStart(inputs, deps = {}) {
|
|
|
5689
5989
|
d.out(`Could not write the service file at ${plan.filePath}: ${e.message}`);
|
|
5690
5990
|
return { kind: "error", code: 1 };
|
|
5691
5991
|
}
|
|
5692
|
-
d.out(unitExists ? `Updated ${plan.manager} service
|
|
5992
|
+
d.out(hint(unitExists ? `Updated the ${plan.manager} service.` : `Installed the ${plan.manager} service.`));
|
|
5693
5993
|
}
|
|
5694
5994
|
const code = runCommands(plan.installCommands, d);
|
|
5695
5995
|
if (code !== 0) {
|
|
5696
|
-
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.`));
|
|
5697
5997
|
return { kind: "error", code };
|
|
5698
5998
|
}
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
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;
|
|
5703
6037
|
}
|
|
6038
|
+
var BUSY_NODE = 4;
|
|
5704
6039
|
var STOP_FOREIGN_NODE = 3;
|
|
5705
6040
|
function foreignNodePid(lock, servicePid, isAlive2) {
|
|
5706
6041
|
const held = parseLock(lock);
|
|
5707
6042
|
if (held === void 0 || held === servicePid) return void 0;
|
|
5708
6043
|
return isAlive2(held) ? held : void 0;
|
|
5709
6044
|
}
|
|
5710
|
-
async function runNodeStop(deps = {}) {
|
|
6045
|
+
async function runNodeStop(inputs = {}, deps = {}) {
|
|
5711
6046
|
const d = { ...defaultDeps3(), ...deps };
|
|
5712
6047
|
const manager = detectManager(d.platform);
|
|
5713
6048
|
if (manager === "unsupported") return printUnsupported(d.out);
|
|
6049
|
+
const busy = guardBusy(d, inputs.force ?? false, "stop");
|
|
6050
|
+
if (busy) return busy;
|
|
5714
6051
|
const plan = buildPlan({
|
|
5715
6052
|
manager,
|
|
5716
6053
|
nodeBin: d.nodeBin,
|
|
@@ -5720,28 +6057,31 @@ async function runNodeStop(deps = {}) {
|
|
|
5720
6057
|
logDir: d.logDir
|
|
5721
6058
|
});
|
|
5722
6059
|
if (!d.fileExists(plan.filePath)) {
|
|
5723
|
-
d.out("No service installed, so there is nothing to stop.");
|
|
5724
|
-
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."));
|
|
5725
6063
|
return reportForeignNode(d, void 0) ?? 0;
|
|
5726
6064
|
}
|
|
5727
6065
|
const servicePid = parseServiceStatus(manager, true, d.capture(statusCommand(manager))).pid;
|
|
5728
6066
|
runCommands(plan.stopCommands, d);
|
|
5729
|
-
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`."));
|
|
5730
6070
|
return reportForeignNode(d, servicePid) ?? 0;
|
|
5731
6071
|
}
|
|
5732
6072
|
function reportForeignNode(d, servicePid) {
|
|
5733
6073
|
const pid = foreignNodePid(d.readFile(d.lockFile), servicePid, d.isAlive);
|
|
5734
6074
|
if (pid === void 0) return void 0;
|
|
5735
6075
|
d.out("");
|
|
5736
|
-
d.out(`
|
|
5737
|
-
d.out("
|
|
5738
|
-
d.out("
|
|
5739
|
-
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."));
|
|
5740
6081
|
return STOP_FOREIGN_NODE;
|
|
5741
6082
|
}
|
|
5742
6083
|
async function runServiceUninstall(deps = {}) {
|
|
5743
6084
|
const d = { ...defaultDeps3(), ...deps };
|
|
5744
|
-
d.out("dahrk service uninstall");
|
|
5745
6085
|
d.out("");
|
|
5746
6086
|
const manager = detectManager(d.platform);
|
|
5747
6087
|
if (manager === "unsupported") return printUnsupported(d.out);
|
|
@@ -5754,17 +6094,19 @@ async function runServiceUninstall(deps = {}) {
|
|
|
5754
6094
|
logDir: d.logDir
|
|
5755
6095
|
});
|
|
5756
6096
|
if (!d.fileExists(plan.filePath)) {
|
|
5757
|
-
d.out(
|
|
6097
|
+
d.out(verdict("info", "No service installed. Nothing to do."));
|
|
5758
6098
|
return 0;
|
|
5759
6099
|
}
|
|
5760
6100
|
runCommands(plan.uninstallCommands, d);
|
|
5761
6101
|
try {
|
|
5762
6102
|
d.removeFile(plan.filePath);
|
|
5763
6103
|
} catch (e) {
|
|
5764
|
-
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));
|
|
5765
6107
|
return 1;
|
|
5766
6108
|
}
|
|
5767
|
-
d.out(
|
|
6109
|
+
d.out(verdict("ok", "Removed. The node will no longer start on boot."));
|
|
5768
6110
|
return 0;
|
|
5769
6111
|
}
|
|
5770
6112
|
function dirOf(path) {
|
|
@@ -5803,34 +6145,45 @@ var defaultDeps3 = () => ({
|
|
|
5803
6145
|
logDir: logDir(process.env),
|
|
5804
6146
|
lockFile: lockFile(process.env),
|
|
5805
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(),
|
|
5806
6152
|
// Snapshot the operator's PATH at install time so the daemon finds git + the runtime CLIs (Homebrew /
|
|
5807
6153
|
// npm-global bins) that a supervisor's minimal PATH would otherwise hide.
|
|
5808
6154
|
pathEnv: process.env.PATH,
|
|
5809
|
-
mkdirp: (dir) => void
|
|
6155
|
+
mkdirp: (dir) => void mkdirSync12(dir, { recursive: true }),
|
|
5810
6156
|
// The unit's environment block carries the enrolment token, so the file is a secret: write it
|
|
5811
6157
|
// owner-only. `writeFileSync`'s `mode` applies only when it CREATES the file, so chmod explicitly
|
|
5812
6158
|
// too - re-installing over a unit an older client left at 0644 must tighten it, not keep it.
|
|
5813
6159
|
writeFile: (path, content) => {
|
|
5814
|
-
|
|
5815
|
-
|
|
6160
|
+
writeFileSync11(path, content, { mode: UNIT_FILE_MODE });
|
|
6161
|
+
chmodSync3(path, UNIT_FILE_MODE);
|
|
5816
6162
|
},
|
|
5817
6163
|
readFile: (path) => {
|
|
5818
6164
|
try {
|
|
5819
|
-
return
|
|
6165
|
+
return readFileSync10(path, "utf8");
|
|
5820
6166
|
} catch {
|
|
5821
6167
|
return void 0;
|
|
5822
6168
|
}
|
|
5823
6169
|
},
|
|
5824
6170
|
removeFile: (path) => rmSync7(path, { force: true }),
|
|
5825
|
-
fileExists: (path) =>
|
|
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.
|
|
5826
6174
|
run: (argv) => {
|
|
5827
6175
|
const [cmd, ...args] = argv;
|
|
5828
6176
|
try {
|
|
5829
|
-
execFileSync7(cmd, args, {
|
|
5830
|
-
|
|
6177
|
+
const stdout = execFileSync7(cmd, args, {
|
|
6178
|
+
encoding: "utf8",
|
|
6179
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
6180
|
+
});
|
|
6181
|
+
return { code: 0, output: stdout };
|
|
5831
6182
|
} catch (e) {
|
|
5832
6183
|
const status = e.status;
|
|
5833
|
-
|
|
6184
|
+
const { stdout, stderr } = e;
|
|
6185
|
+
const output = `${stdout?.toString() ?? ""}${stderr?.toString() ?? ""}`;
|
|
6186
|
+
return { code: typeof status === "number" ? status : 1, output };
|
|
5834
6187
|
}
|
|
5835
6188
|
},
|
|
5836
6189
|
capture: (argv) => {
|
|
@@ -5846,8 +6199,7 @@ var defaultDeps3 = () => ({
|
|
|
5846
6199
|
return { code: typeof status === "number" ? status : 1, stdout: "" };
|
|
5847
6200
|
}
|
|
5848
6201
|
},
|
|
5849
|
-
out
|
|
5850
|
-
`)
|
|
6202
|
+
out
|
|
5851
6203
|
});
|
|
5852
6204
|
|
|
5853
6205
|
// src/update.ts
|
|
@@ -5895,58 +6247,79 @@ function isNewer(latest, current) {
|
|
|
5895
6247
|
}
|
|
5896
6248
|
return false;
|
|
5897
6249
|
}
|
|
5898
|
-
function printChannelCommands(
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
|
|
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));
|
|
5902
6254
|
}
|
|
5903
6255
|
async function runUpdate(inputs, deps = {}) {
|
|
5904
6256
|
const d = { ...defaultDeps4(), ...deps };
|
|
5905
6257
|
const current = inputs.currentVersion;
|
|
5906
|
-
d.out("dahrk update");
|
|
5907
6258
|
d.out("");
|
|
5908
6259
|
let latest;
|
|
5909
6260
|
try {
|
|
5910
6261
|
latest = await d.fetchLatest();
|
|
5911
6262
|
} catch (e) {
|
|
5912
|
-
d.out(`Could not determine the latest version: ${e.message}`);
|
|
5913
|
-
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.`));
|
|
5914
6266
|
return 1;
|
|
5915
6267
|
}
|
|
5916
6268
|
if (!isNewer(latest, current)) {
|
|
5917
|
-
d.out(`Already on the latest version (${current}).`);
|
|
6269
|
+
d.out(verdict("ok", `Already on the latest version (${current}).`));
|
|
5918
6270
|
return 0;
|
|
5919
6271
|
}
|
|
5920
|
-
d.out(`Update available: ${current}
|
|
6272
|
+
d.out(verdict("warn", `Update available: ${current} ${arrow()} ${latest}`));
|
|
5921
6273
|
const channel = detectChannel(d.binPath);
|
|
5922
6274
|
const cmd = upgradeCommand(channel);
|
|
5923
6275
|
if (inputs.check) {
|
|
5924
6276
|
d.out("");
|
|
5925
6277
|
if (cmd) {
|
|
5926
|
-
d.out(`Run \`dahrk update\` to upgrade (${channel}: ${cmd.display}).`);
|
|
6278
|
+
d.out(hint(`Run \`dahrk update\` to upgrade (${channel}: ${cmd.display}).`));
|
|
5927
6279
|
} else {
|
|
5928
|
-
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:"));
|
|
5929
6281
|
printChannelCommands(d.out);
|
|
5930
6282
|
}
|
|
5931
6283
|
return 0;
|
|
5932
6284
|
}
|
|
5933
6285
|
if (!cmd) {
|
|
5934
6286
|
d.out("");
|
|
5935
|
-
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:"));
|
|
5936
6288
|
printChannelCommands(d.out);
|
|
5937
6289
|
return 0;
|
|
5938
6290
|
}
|
|
5939
6291
|
d.out("");
|
|
5940
|
-
d.out(`Upgrading via ${channel}: ${cmd.display}`);
|
|
6292
|
+
d.out(hint(`Upgrading via ${channel}: ${cmd.display}`));
|
|
6293
|
+
const { code, output } = d.runUpgrade(cmd.argv);
|
|
5941
6294
|
d.out("");
|
|
5942
|
-
|
|
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
|
+
}
|
|
5943
6315
|
d.out("");
|
|
5944
|
-
if (
|
|
5945
|
-
d.out(
|
|
5946
|
-
|
|
5947
|
-
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;
|
|
5948
6319
|
}
|
|
5949
|
-
|
|
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."));
|
|
5950
6323
|
}
|
|
5951
6324
|
async function fetchLatestVersion(signal) {
|
|
5952
6325
|
const res = await fetch(LATEST_URL, {
|
|
@@ -5961,19 +6334,36 @@ async function fetchLatestVersion(signal) {
|
|
|
5961
6334
|
function spawnUpgrade(argv) {
|
|
5962
6335
|
const [cmd, ...args] = argv;
|
|
5963
6336
|
try {
|
|
5964
|
-
execFileSync8(cmd, args, {
|
|
5965
|
-
|
|
6337
|
+
const stdout = execFileSync8(cmd, args, {
|
|
6338
|
+
encoding: "utf8",
|
|
6339
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
6340
|
+
});
|
|
6341
|
+
return { code: 0, output: stdout };
|
|
5966
6342
|
} catch (e) {
|
|
5967
6343
|
const status = e.status;
|
|
5968
|
-
|
|
6344
|
+
const { stdout, stderr } = e;
|
|
6345
|
+
return {
|
|
6346
|
+
code: typeof status === "number" ? status : 1,
|
|
6347
|
+
output: `${stdout?.toString() ?? ""}${stderr?.toString() ?? ""}`
|
|
6348
|
+
};
|
|
5969
6349
|
}
|
|
5970
6350
|
}
|
|
5971
6351
|
var defaultDeps4 = () => ({
|
|
5972
6352
|
binPath: process.argv[1],
|
|
5973
6353
|
fetchLatest: fetchLatestVersion,
|
|
5974
6354
|
runUpgrade: spawnUpgrade,
|
|
5975
|
-
|
|
5976
|
-
|
|
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
|
|
5977
6367
|
});
|
|
5978
6368
|
|
|
5979
6369
|
// src/update-check.ts
|
|
@@ -6029,81 +6419,155 @@ function cachedUpdate(state, currentVersion, binPath) {
|
|
|
6029
6419
|
}
|
|
6030
6420
|
|
|
6031
6421
|
// src/status.ts
|
|
6032
|
-
|
|
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
|
+
}
|
|
6033
6455
|
function renderStatus(f) {
|
|
6034
|
-
const
|
|
6456
|
+
const v = presenceVerdict(f);
|
|
6457
|
+
const lines = ["", verdict(v.level, v.text), ""];
|
|
6035
6458
|
const { nodeId, name, tenantId, enrolToken } = f.state;
|
|
6036
6459
|
if (enrolToken) {
|
|
6037
|
-
|
|
6038
|
-
|
|
6460
|
+
const who = name ? name : "yes";
|
|
6461
|
+
lines.push(kv("Enrolled", tenantId ? `${who} ${dim("\xB7")} ${tenantId}` : who));
|
|
6039
6462
|
} else if (f.envToken) {
|
|
6040
|
-
lines.push(
|
|
6463
|
+
lines.push(kv("Enrolled", "via DAHRK_ENROL_TOKEN (caches on the next successful start)"));
|
|
6041
6464
|
} else {
|
|
6042
|
-
lines.push(
|
|
6465
|
+
lines.push(kv("Enrolled", `no ${dim("run `dahrk start --token <token>` once to enrol")}`));
|
|
6043
6466
|
}
|
|
6044
|
-
lines.push(
|
|
6467
|
+
lines.push(kv("Node id", nodeId ?? dim("not yet minted (first `dahrk start` mints one)")));
|
|
6045
6468
|
lines.push(
|
|
6046
|
-
|
|
6469
|
+
kv(
|
|
6047
6470
|
"Client",
|
|
6048
|
-
f.update ? `${f.clientVersion}
|
|
6471
|
+
f.update ? `${f.clientVersion} ${dim(`(update available: ${f.update.latest} - run \`dahrk update\`)`)}` : f.clientVersion
|
|
6049
6472
|
)
|
|
6050
6473
|
);
|
|
6051
|
-
|
|
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);
|
|
6052
6477
|
lines.push(
|
|
6053
|
-
|
|
6478
|
+
kv(
|
|
6054
6479
|
"Runtimes",
|
|
6055
|
-
|
|
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)")}`
|
|
6056
6481
|
)
|
|
6057
6482
|
);
|
|
6058
|
-
if (
|
|
6059
|
-
lines.push(
|
|
6060
|
-
} else if (!f.service.installed) {
|
|
6061
|
-
lines.push(bullet("Node", "not installed - run `dahrk start` to run it always-on"));
|
|
6062
|
-
} else if (f.service.running) {
|
|
6063
|
-
const pid = f.service.pid ? ` (pid ${f.service.pid})` : "";
|
|
6064
|
-
lines.push(bullet("Node", `running${pid}`));
|
|
6065
|
-
} else if (f.state.desired === "stopped") {
|
|
6066
|
-
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")));
|
|
6067
6485
|
} else {
|
|
6068
|
-
|
|
6069
|
-
|
|
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)));
|
|
6070
6489
|
}
|
|
6071
|
-
lines.push("",
|
|
6490
|
+
lines.push(...presenceHints(f).flatMap((h) => ["", h]));
|
|
6491
|
+
lines.push("", dim(` State file: ${f.stateFile}`));
|
|
6072
6492
|
return lines;
|
|
6073
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
|
+
}
|
|
6074
6498
|
function isUnhealthy(f) {
|
|
6075
|
-
return
|
|
6499
|
+
return f.presence.kind === "crashed";
|
|
6076
6500
|
}
|
|
6077
|
-
|
|
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) {
|
|
6078
6509
|
const manager = detectManager(deps.platform);
|
|
6079
6510
|
let service;
|
|
6080
|
-
let logHint;
|
|
6081
6511
|
if (manager !== "unsupported") {
|
|
6082
6512
|
const unit = unitPath(manager, deps.homeDir);
|
|
6083
6513
|
const exists = deps.fileExists(unit);
|
|
6084
6514
|
const probe2 = exists ? deps.capture(statusCommand(manager)) : { code: 1, stdout: "" };
|
|
6085
6515
|
service = parseServiceStatus(manager, exists, probe2);
|
|
6086
|
-
logHint = "dahrk logs -f";
|
|
6087
6516
|
}
|
|
6088
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);
|
|
6089
6522
|
const update = checkSuppressed(deps.env) ? void 0 : cachedUpdate(state, inputs.clientVersion, deps.binPath);
|
|
6090
|
-
const
|
|
6523
|
+
const connection = deps.connection();
|
|
6524
|
+
return {
|
|
6091
6525
|
clientVersion: inputs.clientVersion,
|
|
6092
6526
|
hubUrl: inputs.hubUrl,
|
|
6093
6527
|
stateFile: stateFile(deps.env),
|
|
6094
6528
|
state,
|
|
6095
6529
|
envToken: Boolean(deps.env.DAHRK_ENROL_TOKEN),
|
|
6096
|
-
runtimes: await deps.
|
|
6530
|
+
runtimes: await deps.probeRuntimes(),
|
|
6531
|
+
presence,
|
|
6532
|
+
jobs,
|
|
6533
|
+
now: deps.now(),
|
|
6097
6534
|
...service ? { service } : {},
|
|
6098
|
-
...
|
|
6535
|
+
...connection ? { connection } : {},
|
|
6099
6536
|
...update ? { update } : {}
|
|
6100
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
|
+
}
|
|
6101
6565
|
for (const line of renderStatus(facts)) deps.out(line);
|
|
6102
6566
|
return isUnhealthy(facts) ? 1 : 0;
|
|
6103
6567
|
}
|
|
6104
6568
|
|
|
6105
6569
|
// src/main.ts
|
|
6106
|
-
var CLIENT_VERSION = "0.1.
|
|
6570
|
+
var CLIENT_VERSION = "0.1.15";
|
|
6107
6571
|
var DEFAULT_HUB_URL = "wss://api.dahrk.ai";
|
|
6108
6572
|
var list = (v) => (v ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
6109
6573
|
var RUNTIMES2 = ["claude-code", "codex", "pi"];
|
|
@@ -6115,7 +6579,7 @@ function resolveNodeId(env, opts = {}) {
|
|
|
6115
6579
|
if (existing) return existing;
|
|
6116
6580
|
const legacy = legacyStateDir(env);
|
|
6117
6581
|
if (legacy) {
|
|
6118
|
-
const legacyId = readState(
|
|
6582
|
+
const legacyId = readState(join17(legacy, "node.json")).nodeId;
|
|
6119
6583
|
if (legacyId) return legacyId;
|
|
6120
6584
|
}
|
|
6121
6585
|
const nodeId = randomUUID3();
|
|
@@ -6189,20 +6653,40 @@ async function start(flags) {
|
|
|
6189
6653
|
const env = envWithFlags(process.env, flags);
|
|
6190
6654
|
if (wantsForeground(env, flags)) return startForeground(env, flags);
|
|
6191
6655
|
await offerUpdate(env);
|
|
6192
|
-
const outcome = await runNodeStart(
|
|
6193
|
-
...resolveEnrolToken(env) ? { token: resolveEnrolToken(env) } : {},
|
|
6194
|
-
...env.DAHRK_NODE_NAME ? { name: env.DAHRK_NODE_NAME } : {},
|
|
6195
|
-
...env.DAHRK_HUB_URL ? { hubUrl: env.DAHRK_HUB_URL } : {}
|
|
6196
|
-
});
|
|
6656
|
+
const outcome = await runNodeStart(serviceInputs(env));
|
|
6197
6657
|
if (outcome.kind === "error") return outcome.code;
|
|
6198
6658
|
if (outcome.kind === "running") {
|
|
6199
6659
|
setDesired(env, "running");
|
|
6660
|
+
await printStatus(env);
|
|
6200
6661
|
return 0;
|
|
6201
6662
|
}
|
|
6202
|
-
|
|
6203
|
-
|
|
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."));
|
|
6204
6667
|
return startForeground(env, flags);
|
|
6205
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
|
+
}
|
|
6206
6690
|
async function startForeground(env, flags) {
|
|
6207
6691
|
if (!flags.ephemeral) {
|
|
6208
6692
|
const lock = acquireLock(defaultLockDeps(lockFile(env)));
|
|
@@ -6258,14 +6742,18 @@ async function startForeground(env, flags) {
|
|
|
6258
6742
|
// (a pinned override never re-probes to something else) and the mock runner path stays stable.
|
|
6259
6743
|
reprobeRuntimes: () => resolveRuntimes(env),
|
|
6260
6744
|
...env.DAHRK_RUNTIME_RECHECK_MS ? { runtimeRecheckMs: Number(env.DAHRK_RUNTIME_RECHECK_MS) } : {},
|
|
6745
|
+
// What this node is running, on disk, so a crash mid-stage does not silently re-run the stage from
|
|
6746
|
+
// scratch (DHK-416). Skipped for an ephemeral node for the same reason it never caches a token: a
|
|
6747
|
+
// one-shot CI node touches no state dir, and has no next boot to recover into.
|
|
6748
|
+
...flags.ephemeral ? {} : { jobLedger: fileJobLedger(jobLedgerFile(stateDir(env))) },
|
|
6261
6749
|
...persist ? {
|
|
6262
6750
|
onEnrolled: (welcome) => persistEnrolment(env, { token, name: welcome.name, tenantId: welcome.tenantId })
|
|
6263
6751
|
} : {}
|
|
6264
6752
|
});
|
|
6265
6753
|
return 0;
|
|
6266
6754
|
}
|
|
6267
|
-
async function stop(env) {
|
|
6268
|
-
const code = await runNodeStop();
|
|
6755
|
+
async function stop(env, force) {
|
|
6756
|
+
const code = await runNodeStop({ force });
|
|
6269
6757
|
if (code === 0 || code === STOP_FOREIGN_NODE) setDesired(env, "stopped");
|
|
6270
6758
|
return code;
|
|
6271
6759
|
}
|
|
@@ -6279,25 +6767,15 @@ function updateCheckDeps(env) {
|
|
|
6279
6767
|
fetchLatest: fetchLatestVersion
|
|
6280
6768
|
};
|
|
6281
6769
|
}
|
|
6282
|
-
var isInteractive = () => Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
6283
|
-
async function confirm(question) {
|
|
6284
|
-
const { createInterface } = await import("readline/promises");
|
|
6285
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
6286
|
-
try {
|
|
6287
|
-
const answer = (await rl.question(`${question} [Y/n] `)).trim().toLowerCase();
|
|
6288
|
-
return answer === "" || answer === "y" || answer === "yes";
|
|
6289
|
-
} finally {
|
|
6290
|
-
rl.close();
|
|
6291
|
-
}
|
|
6292
|
-
}
|
|
6293
6770
|
async function offerUpdate(env) {
|
|
6294
6771
|
const available = await checkForUpdate(CLIENT_VERSION, updateCheckDeps(env));
|
|
6295
6772
|
if (!available) return;
|
|
6296
|
-
|
|
6773
|
+
out("");
|
|
6774
|
+
out(renderUpdateNotice(available));
|
|
6297
6775
|
if (!isInteractive()) return;
|
|
6298
|
-
if (!await confirm("
|
|
6776
|
+
if (!await confirm("Update now?")) return;
|
|
6299
6777
|
const code = await runUpdate({ currentVersion: CLIENT_VERSION, check: false });
|
|
6300
|
-
if (code !== 0)
|
|
6778
|
+
if (code !== 0) out(hint("Update failed; starting the node on the current version anyway."));
|
|
6301
6779
|
}
|
|
6302
6780
|
function scheduleUpdateChecks(env) {
|
|
6303
6781
|
if (checkSuppressed(env)) return;
|
|
@@ -6316,8 +6794,16 @@ function statusDeps(env) {
|
|
|
6316
6794
|
homeDir: homedir7(),
|
|
6317
6795
|
env,
|
|
6318
6796
|
binPath: process.argv[1],
|
|
6319
|
-
|
|
6320
|
-
|
|
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
|
+
},
|
|
6806
|
+
fileExists: (path) => existsSync14(path),
|
|
6321
6807
|
capture: (argv) => {
|
|
6322
6808
|
const [cmd, ...args] = argv;
|
|
6323
6809
|
try {
|
|
@@ -6328,10 +6814,35 @@ function statusDeps(env) {
|
|
|
6328
6814
|
return { code: typeof status === "number" ? status : 1, stdout: "" };
|
|
6329
6815
|
}
|
|
6330
6816
|
},
|
|
6331
|
-
|
|
6332
|
-
|
|
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
|
|
6333
6831
|
};
|
|
6334
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
|
+
}
|
|
6335
6846
|
var KNOWN_WORKFLOWS = ["preflight"];
|
|
6336
6847
|
async function runWorkflow(flags) {
|
|
6337
6848
|
if (flags.workflow !== "preflight") {
|
|
@@ -6341,7 +6852,7 @@ async function runWorkflow(flags) {
|
|
|
6341
6852
|
return 2;
|
|
6342
6853
|
}
|
|
6343
6854
|
const env = applyEnvAliases(process.env);
|
|
6344
|
-
const hubUrl = flags.hubUrl ?? env.DAHRK_HUB_URL;
|
|
6855
|
+
const hubUrl = flags.hubUrl ?? env.DAHRK_HUB_URL ?? DEFAULT_HUB_URL;
|
|
6345
6856
|
const token = flags.token ?? resolveEnrolToken(env);
|
|
6346
6857
|
return runPreflight({
|
|
6347
6858
|
...flags.repo ? { repoPath: flags.repo } : {},
|
|
@@ -6370,7 +6881,10 @@ async function main() {
|
|
|
6370
6881
|
case "doctor": {
|
|
6371
6882
|
const env = envWithFlags(process.env, parsed.flags);
|
|
6372
6883
|
process.exitCode = await runDoctor({
|
|
6373
|
-
|
|
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,
|
|
6374
6888
|
// Same resolution as `start`, so doctor checks the token the node would actually present:
|
|
6375
6889
|
// the flag/env if given, else the one cached by the last successful enrolment.
|
|
6376
6890
|
token: resolveEnrolToken(env),
|
|
@@ -6381,7 +6895,11 @@ async function main() {
|
|
|
6381
6895
|
case "status": {
|
|
6382
6896
|
const env = envWithFlags(process.env, parsed.flags);
|
|
6383
6897
|
process.exitCode = await runStatus(
|
|
6384
|
-
{
|
|
6898
|
+
{
|
|
6899
|
+
clientVersion: CLIENT_VERSION,
|
|
6900
|
+
hubUrl: env.DAHRK_HUB_URL ?? DEFAULT_HUB_URL,
|
|
6901
|
+
json: parsed.flags.json
|
|
6902
|
+
},
|
|
6385
6903
|
statusDeps(env)
|
|
6386
6904
|
);
|
|
6387
6905
|
break;
|
|
@@ -6406,24 +6924,21 @@ async function main() {
|
|
|
6406
6924
|
break;
|
|
6407
6925
|
}
|
|
6408
6926
|
case "update":
|
|
6409
|
-
process.exitCode = await runUpdate({
|
|
6927
|
+
process.exitCode = await runUpdate({
|
|
6928
|
+
currentVersion: CLIENT_VERSION,
|
|
6929
|
+
check: parsed.flags.check,
|
|
6930
|
+
verbose: parsed.flags.verbose
|
|
6931
|
+
});
|
|
6410
6932
|
break;
|
|
6411
6933
|
case "start":
|
|
6412
6934
|
process.exitCode = await start(parsed.flags);
|
|
6413
6935
|
break;
|
|
6414
6936
|
case "stop":
|
|
6415
|
-
process.exitCode = await stop(applyEnvAliases(process.env));
|
|
6937
|
+
process.exitCode = await stop(applyEnvAliases(process.env), parsed.force);
|
|
6416
6938
|
break;
|
|
6417
|
-
case "restart":
|
|
6418
|
-
|
|
6419
|
-
const code = await stop(env);
|
|
6420
|
-
if (code !== 0) {
|
|
6421
|
-
process.exitCode = code;
|
|
6422
|
-
break;
|
|
6423
|
-
}
|
|
6424
|
-
process.exitCode = await start(parsed.flags);
|
|
6939
|
+
case "restart":
|
|
6940
|
+
process.exitCode = await restart(parsed.flags, parsed.force);
|
|
6425
6941
|
break;
|
|
6426
|
-
}
|
|
6427
6942
|
case "logs": {
|
|
6428
6943
|
const env = applyEnvAliases(process.env);
|
|
6429
6944
|
process.exitCode = await runLogs(parsed.flags, defaultLogsDeps(logFiles(env), jsonlLogFile(env)));
|
|
@@ -6438,7 +6953,10 @@ async function main() {
|
|
|
6438
6953
|
...resolveEnrolToken(env) ? { token: resolveEnrolToken(env) } : {},
|
|
6439
6954
|
hubUrl: env.DAHRK_HUB_URL ?? DEFAULT_HUB_URL
|
|
6440
6955
|
},
|
|
6441
|
-
|
|
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)) }
|
|
6442
6960
|
);
|
|
6443
6961
|
return doctorLines;
|
|
6444
6962
|
};
|