dahrk-node 0.1.2 → 0.1.4
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 +619 -60
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/main.ts
|
|
4
|
-
import { existsSync as
|
|
5
|
-
import { randomUUID as
|
|
6
|
-
import { homedir as
|
|
7
|
-
import { basename, join as
|
|
4
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync6, readFileSync as readFileSync5, realpathSync as realpathSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
5
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
6
|
+
import { homedir as homedir3 } from "os";
|
|
7
|
+
import { basename, join as join8 } from "path";
|
|
8
8
|
import { pathToFileURL } from "url";
|
|
9
9
|
|
|
10
10
|
// ../../packages/edge/src/ws-client.ts
|
|
@@ -335,7 +335,7 @@ var ManagedMailbox = class {
|
|
|
335
335
|
const v = this.q.shift();
|
|
336
336
|
if (v !== void 0) return Promise.resolve({ value: v, done: false });
|
|
337
337
|
if (this.done) return Promise.resolve({ value: void 0, done: true });
|
|
338
|
-
return new Promise((
|
|
338
|
+
return new Promise((resolve2) => this.waiters.push(resolve2));
|
|
339
339
|
}
|
|
340
340
|
};
|
|
341
341
|
}
|
|
@@ -346,7 +346,7 @@ function interactiveIdleWindows(ctx) {
|
|
|
346
346
|
return { firstReplyMs: Math.max(firstReplyMs, idleMs), idleMs };
|
|
347
347
|
}
|
|
348
348
|
function raceNextTurn(pending, idleMs, signal) {
|
|
349
|
-
return new Promise((
|
|
349
|
+
return new Promise((resolve2) => {
|
|
350
350
|
let settled = false;
|
|
351
351
|
let timer;
|
|
352
352
|
function finish(r) {
|
|
@@ -354,7 +354,7 @@ function raceNextTurn(pending, idleMs, signal) {
|
|
|
354
354
|
settled = true;
|
|
355
355
|
if (timer) clearTimeout(timer);
|
|
356
356
|
signal.removeEventListener("abort", onAbort);
|
|
357
|
-
|
|
357
|
+
resolve2(r);
|
|
358
358
|
}
|
|
359
359
|
function onAbort() {
|
|
360
360
|
finish({ kind: "cancelled" });
|
|
@@ -412,6 +412,13 @@ var userMsg = (text) => ({
|
|
|
412
412
|
message: { role: "user", content: text }
|
|
413
413
|
});
|
|
414
414
|
var sessionIdOf = (msg) => "session_id" in msg && typeof msg.session_id === "string" ? msg.session_id : void 0;
|
|
415
|
+
var policyCanUseTool = async (ctx, toolName, input) => {
|
|
416
|
+
const verdict = ctx.authorizeToolUse?.(toolName, input);
|
|
417
|
+
if (verdict?.verdict === "deny") {
|
|
418
|
+
return { behavior: "deny", message: verdict.reason ?? `tool "${toolName}" denied by policy ${verdict.policy}` };
|
|
419
|
+
}
|
|
420
|
+
return { behavior: "allow", updatedInput: input };
|
|
421
|
+
};
|
|
415
422
|
function buildBrokeredMcpServers(ctx) {
|
|
416
423
|
const servers = ctx.config.mcpServers;
|
|
417
424
|
if (!servers || servers.length === 0 || !ctx.mcpProxyBaseUrl) return void 0;
|
|
@@ -477,7 +484,7 @@ function createClaudeRunner() {
|
|
|
477
484
|
// Tools stay allowed by canUseTool below; we do NOT set a restrictive allowedTools here.
|
|
478
485
|
...mcpServers ? { mcpServers } : {},
|
|
479
486
|
// Headless: allow tools to run without an interactive permission prompt (M6 wires policy).
|
|
480
|
-
canUseTool: async (
|
|
487
|
+
canUseTool: async (toolName, input) => policyCanUseTool(ctx, toolName, input)
|
|
481
488
|
};
|
|
482
489
|
const state = newBufferState();
|
|
483
490
|
let status = "ok";
|
|
@@ -512,7 +519,7 @@ function createClaudeRunner() {
|
|
|
512
519
|
// Auto-approve the exit tool; `allowedTools` is an auto-approve list, not a whitelist, so it
|
|
513
520
|
// does not restrict the other tools canUseTool allows.
|
|
514
521
|
allowedTools: [stageTool.allowedToolName],
|
|
515
|
-
canUseTool: async (toolName, input) => summarising && toolName !== stageTool.allowedToolName ? { behavior: "deny", message: "Summarise from the work you just did; reply with the sentence only, no tools." } :
|
|
522
|
+
canUseTool: async (toolName, input) => summarising && toolName !== stageTool.allowedToolName ? { behavior: "deny", message: "Summarise from the work you just did; reply with the sentence only, no tools." } : policyCanUseTool(ctx, toolName, input),
|
|
516
523
|
maxTurns: MAX_TURNS,
|
|
517
524
|
includePartialMessages: false
|
|
518
525
|
};
|
|
@@ -1214,6 +1221,7 @@ import { dirname, isAbsolute, join as join2 } from "path";
|
|
|
1214
1221
|
var noopLogger = { info: () => {
|
|
1215
1222
|
}, warn: () => {
|
|
1216
1223
|
} };
|
|
1224
|
+
var SCRATCH_DIR = ".skakel/scratch";
|
|
1217
1225
|
function parseOwnerRepo(gitUrl) {
|
|
1218
1226
|
const m = /[:/]([^/:]+)\/([^/]+?)(?:\.git)?$/.exec(gitUrl.trim());
|
|
1219
1227
|
return m ? `${m[1]}/${m[2]}` : void 0;
|
|
@@ -1255,24 +1263,23 @@ function createGitService(opts = {}) {
|
|
|
1255
1263
|
return (stderr?.trim() || err?.message || String(e)).split("\n")[0] ?? String(e);
|
|
1256
1264
|
};
|
|
1257
1265
|
const excludeScratchLocally = (worktreePath) => {
|
|
1258
|
-
const entry =
|
|
1266
|
+
const entry = `${SCRATCH_DIR}/`;
|
|
1259
1267
|
try {
|
|
1260
1268
|
const rel = git(worktreePath, ["rev-parse", "--git-path", "info/exclude"]).trim();
|
|
1261
1269
|
const excludePath = isAbsolute(rel) ? rel : join2(worktreePath, rel);
|
|
1262
1270
|
const existing = existsSync(excludePath) ? readFileSync2(excludePath, "utf-8") : "";
|
|
1263
1271
|
if (existing.split("\n").some((l) => l.trim() === entry)) return;
|
|
1264
1272
|
mkdirSync(dirname(excludePath), { recursive: true });
|
|
1265
|
-
const
|
|
1266
|
-
writeFileSync(excludePath, `${existing}${
|
|
1273
|
+
const sep3 = existing && !existing.endsWith("\n") ? "\n" : "";
|
|
1274
|
+
writeFileSync(excludePath, `${existing}${sep3}${entry}
|
|
1267
1275
|
`);
|
|
1268
1276
|
} catch (e) {
|
|
1269
1277
|
log2.warn(`could not set worktree scratch exclude at ${worktreePath}: ${e.message}`);
|
|
1270
1278
|
}
|
|
1271
1279
|
};
|
|
1272
1280
|
const commitPending = (worktreePath, message) => {
|
|
1273
|
-
const SCRATCH = ".skakel/scratch";
|
|
1274
1281
|
excludeScratchLocally(worktreePath);
|
|
1275
|
-
git(worktreePath, ["rm", "-r", "--cached", "--ignore-unmatch", "--quiet",
|
|
1282
|
+
git(worktreePath, ["rm", "-r", "--cached", "--ignore-unmatch", "--quiet", SCRATCH_DIR]);
|
|
1276
1283
|
git(worktreePath, ["add", "-A", "--", "."]);
|
|
1277
1284
|
const dirty = !gitOk(worktreePath, ["diff", "--cached", "--quiet"]);
|
|
1278
1285
|
if (dirty) {
|
|
@@ -1394,6 +1401,11 @@ function createGitService(opts = {}) {
|
|
|
1394
1401
|
if (!gitOk(worktreePath, ["merge-base", "HEAD", "FETCH_HEAD"])) {
|
|
1395
1402
|
return { headSha, pushed: false, nothingToCommit: !dirty, commitsAhead, integration: "diverged" };
|
|
1396
1403
|
}
|
|
1404
|
+
const delta = git(worktreePath, ["diff", "--name-only", "FETCH_HEAD...HEAD"]).split("\n").map((l) => l.trim()).filter(Boolean);
|
|
1405
|
+
const isScratchPath = (p) => p === SCRATCH_DIR || p.startsWith(`${SCRATCH_DIR}/`) || gitOk(worktreePath, ["check-ignore", "-q", "--no-index", "--", p]);
|
|
1406
|
+
if (!delta.some((p) => !isScratchPath(p))) {
|
|
1407
|
+
return { headSha, pushed: false, nothingToCommit: true, commitsAhead, integration: "noop" };
|
|
1408
|
+
}
|
|
1397
1409
|
try {
|
|
1398
1410
|
git(worktreePath, [
|
|
1399
1411
|
"-c",
|
|
@@ -1667,7 +1679,7 @@ import { execFileSync as execFileSync3 } from "child_process";
|
|
|
1667
1679
|
import { createHash as createHash3 } from "crypto";
|
|
1668
1680
|
import { mkdirSync as mkdirSync5, readdirSync as readdirSync2, readFileSync as readFileSync4, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
1669
1681
|
import { tmpdir as tmpdir2 } from "os";
|
|
1670
|
-
import { join as join6 } from "path";
|
|
1682
|
+
import { isAbsolute as isAbsolute2, join as join6, relative as relative2, resolve, sep as sep2 } from "path";
|
|
1671
1683
|
import { attachedDocBasename as attachedDocBasename2 } from "@dahrk/contracts";
|
|
1672
1684
|
|
|
1673
1685
|
// ../../packages/edge/src/builtins.ts
|
|
@@ -1841,11 +1853,11 @@ async function startMcpGateway(opts) {
|
|
|
1841
1853
|
if (upstream.body) await pipeline(Readable.fromWeb(upstream.body), res);
|
|
1842
1854
|
else res.end();
|
|
1843
1855
|
};
|
|
1844
|
-
await new Promise((
|
|
1856
|
+
await new Promise((resolve2) => server.listen(0, "127.0.0.1", resolve2));
|
|
1845
1857
|
const { port } = server.address();
|
|
1846
1858
|
return {
|
|
1847
1859
|
baseUrl: `http://127.0.0.1:${port}`,
|
|
1848
|
-
stop: () => new Promise((
|
|
1860
|
+
stop: () => new Promise((resolve2) => server.close(() => resolve2()))
|
|
1849
1861
|
};
|
|
1850
1862
|
}
|
|
1851
1863
|
|
|
@@ -1933,9 +1945,24 @@ var SCRATCH_OUTPUT_DIR = ".skakel/scratch/output";
|
|
|
1933
1945
|
function capContent(raw) {
|
|
1934
1946
|
return raw.length > ARTIFACT_CAP_BYTES ? raw.slice(0, ARTIFACT_CAP_BYTES) : raw;
|
|
1935
1947
|
}
|
|
1948
|
+
function resolveWorktreeRelativePath(ref, relPath) {
|
|
1949
|
+
if (isAbsolute2(relPath)) return void 0;
|
|
1950
|
+
const root = resolve(ref.worktreePath);
|
|
1951
|
+
const target = resolve(root, relPath);
|
|
1952
|
+
const fromRoot = relative2(root, target);
|
|
1953
|
+
if (fromRoot === "" || fromRoot === ".." || fromRoot.startsWith(`..${sep2}`) || isAbsolute2(fromRoot)) {
|
|
1954
|
+
return void 0;
|
|
1955
|
+
}
|
|
1956
|
+
return target;
|
|
1957
|
+
}
|
|
1958
|
+
function isSafeWorktreeRelativePath(ref, relPath) {
|
|
1959
|
+
return resolveWorktreeRelativePath(ref, relPath) !== void 0;
|
|
1960
|
+
}
|
|
1936
1961
|
function readEmittedArtifact(ref, relPath) {
|
|
1962
|
+
const path = resolveWorktreeRelativePath(ref, relPath);
|
|
1963
|
+
if (!path) return void 0;
|
|
1937
1964
|
try {
|
|
1938
|
-
const raw = readFileSync4(
|
|
1965
|
+
const raw = readFileSync4(path, "utf8");
|
|
1939
1966
|
return { path: relPath, content: capContent(raw) };
|
|
1940
1967
|
} catch {
|
|
1941
1968
|
return void 0;
|
|
@@ -1981,7 +2008,7 @@ function resolveStageArtifact(ref, emitArtifact, handedBack) {
|
|
|
1981
2008
|
const declared = readEmittedArtifact(ref, emitArtifact);
|
|
1982
2009
|
if (declared && declared.content.trim().length > 0) return { artifact: declared, source: "declared-file" };
|
|
1983
2010
|
}
|
|
1984
|
-
if (handedBack && handedBack.content.trim().length > 0) {
|
|
2011
|
+
if (handedBack && handedBack.content.trim().length > 0 && isSafeWorktreeRelativePath(ref, handedBack.path)) {
|
|
1985
2012
|
return { artifact: { path: handedBack.path, content: capContent(handedBack.content) }, source: "tool-handoff" };
|
|
1986
2013
|
}
|
|
1987
2014
|
const scratch = scanScratchOutput(ref, emitArtifact);
|
|
@@ -2081,8 +2108,9 @@ function createStageRunner(deps) {
|
|
|
2081
2108
|
writeGuidance(ref, job.guidance);
|
|
2082
2109
|
writeAttachedDocuments(ref, job.attachedDocuments);
|
|
2083
2110
|
if (job.agentConfig.emitArtifact) {
|
|
2111
|
+
const artifactDir = resolveWorktreeRelativePath(ref, job.agentConfig.emitArtifact);
|
|
2084
2112
|
const slash = job.agentConfig.emitArtifact.lastIndexOf("/");
|
|
2085
|
-
if (slash > 0) {
|
|
2113
|
+
if (artifactDir && slash > 0) {
|
|
2086
2114
|
try {
|
|
2087
2115
|
mkdirSync5(join6(ref.worktreePath, job.agentConfig.emitArtifact.slice(0, slash)), { recursive: true });
|
|
2088
2116
|
} catch {
|
|
@@ -2219,20 +2247,50 @@ function createStageRunner(deps) {
|
|
|
2219
2247
|
return finish("fail", `${stageId}: denied at stage entry (${entry.policy})`, job.sessionId);
|
|
2220
2248
|
}
|
|
2221
2249
|
let denied = false;
|
|
2250
|
+
const authorisedActions = [];
|
|
2222
2251
|
const runtime = agentConfig.runtime;
|
|
2252
|
+
const actionKey = (tool2, input) => {
|
|
2253
|
+
try {
|
|
2254
|
+
return `${tool2}\0${JSON.stringify(input)}`;
|
|
2255
|
+
} catch {
|
|
2256
|
+
return `${tool2}\0`;
|
|
2257
|
+
}
|
|
2258
|
+
};
|
|
2259
|
+
const policyReason = (verdict) => verdict.reason ?? `tool action denied by ${verdict.policy}`;
|
|
2260
|
+
const recordDeny = (verdict, toolUseId) => {
|
|
2261
|
+
denied = true;
|
|
2262
|
+
const reason = policyReason(verdict);
|
|
2263
|
+
if (toolUseId) {
|
|
2264
|
+
streamEvent(writer.append({ seq: 0, ts: nowIso2(), type: "observation", runtime, toolUseId, isError: true, output: { error: reason } }));
|
|
2265
|
+
}
|
|
2266
|
+
streamEvent(writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime, event: "policy-deny", detail: reason }));
|
|
2267
|
+
deps.sendProgress({ jobId, kind: "error", ts: nowIso2(), text: reason });
|
|
2268
|
+
};
|
|
2269
|
+
const authorizeToolUse = (tool2, input) => {
|
|
2270
|
+
const verdict = evaluatePolicies({ kind: "action", stageId, tool: tool2, input }, rules);
|
|
2271
|
+
if (verdict.verdict === "deny") {
|
|
2272
|
+
recordDeny(verdict);
|
|
2273
|
+
} else {
|
|
2274
|
+
authorisedActions.push(actionKey(tool2, input));
|
|
2275
|
+
}
|
|
2276
|
+
return verdict;
|
|
2277
|
+
};
|
|
2223
2278
|
const onTrace = (event) => {
|
|
2224
2279
|
if (event.type === "action") {
|
|
2225
|
-
const
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2280
|
+
const key = actionKey(event.tool, event.input);
|
|
2281
|
+
const authorised = authorisedActions.indexOf(key);
|
|
2282
|
+
if (authorised >= 0) {
|
|
2283
|
+
authorisedActions.splice(authorised, 1);
|
|
2284
|
+
} else {
|
|
2285
|
+
const verdict = evaluatePolicies(
|
|
2286
|
+
{ kind: "action", stageId, tool: event.tool, input: event.input },
|
|
2287
|
+
rules
|
|
2288
|
+
);
|
|
2289
|
+
if (verdict.verdict === "deny") {
|
|
2290
|
+
streamEvent(writer.append(event));
|
|
2291
|
+
recordDeny(verdict, event.toolUseId);
|
|
2292
|
+
return;
|
|
2293
|
+
}
|
|
2236
2294
|
}
|
|
2237
2295
|
}
|
|
2238
2296
|
streamEvent(writer.append(event));
|
|
@@ -2260,7 +2318,8 @@ function createStageRunner(deps) {
|
|
|
2260
2318
|
...job.runtimeEnv ? { runtimeEnv: job.runtimeEnv } : {},
|
|
2261
2319
|
// The adapter persists each runtime-native record under the attempt's raw/ sidecar
|
|
2262
2320
|
// and stamps the rawRef onto the emitted event.
|
|
2263
|
-
writeRaw: writer.writeRaw
|
|
2321
|
+
writeRaw: writer.writeRaw,
|
|
2322
|
+
authorizeToolUse
|
|
2264
2323
|
};
|
|
2265
2324
|
const interactive = agentConfig.interaction === "interactive";
|
|
2266
2325
|
let result;
|
|
@@ -2364,6 +2423,18 @@ function createStageRunner(deps) {
|
|
|
2364
2423
|
base: job.base,
|
|
2365
2424
|
...job.workspaceRef.credentialToken ? { credentialToken: job.workspaceRef.credentialToken } : {}
|
|
2366
2425
|
});
|
|
2426
|
+
if (r.integration === "noop") {
|
|
2427
|
+
return {
|
|
2428
|
+
jobId,
|
|
2429
|
+
status: "ok",
|
|
2430
|
+
branch: job.branch,
|
|
2431
|
+
headSha: r.headSha,
|
|
2432
|
+
pushed: false,
|
|
2433
|
+
nothingToCommit: true,
|
|
2434
|
+
commitsAhead: r.commitsAhead,
|
|
2435
|
+
summary: `no changes to deliver on ${job.branch} - work already present on ${job.base}`
|
|
2436
|
+
};
|
|
2437
|
+
}
|
|
2367
2438
|
if (r.integration === "conflict") {
|
|
2368
2439
|
return {
|
|
2369
2440
|
jobId,
|
|
@@ -2442,11 +2513,27 @@ async function startEdgeNode(opts) {
|
|
|
2442
2513
|
});
|
|
2443
2514
|
let ws;
|
|
2444
2515
|
let heartbeat;
|
|
2516
|
+
let reconnectTimer;
|
|
2517
|
+
let lastPongAt = 0;
|
|
2445
2518
|
let shuttingDown = false;
|
|
2446
2519
|
let onFatal;
|
|
2447
2520
|
const send = (msg) => {
|
|
2448
2521
|
if (ws && ws.readyState === WebSocket.OPEN) ws.send(encode(msg));
|
|
2449
2522
|
};
|
|
2523
|
+
const MISSED_PONGS_BEFORE_DEAD = 3;
|
|
2524
|
+
const startHeartbeat = (sock, intervalMs) => {
|
|
2525
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
2526
|
+
lastPongAt = Date.now();
|
|
2527
|
+
heartbeat = setInterval(() => {
|
|
2528
|
+
if (Date.now() - lastPongAt > MISSED_PONGS_BEFORE_DEAD * intervalMs) {
|
|
2529
|
+
log(`EDGE_STALE:${Date.now() - lastPongAt}ms without a pong, terminating socket`);
|
|
2530
|
+
sock.terminate();
|
|
2531
|
+
return;
|
|
2532
|
+
}
|
|
2533
|
+
send({ type: "heartbeat" });
|
|
2534
|
+
if (sock.readyState === WebSocket.OPEN) sock.ping();
|
|
2535
|
+
}, intervalMs);
|
|
2536
|
+
};
|
|
2450
2537
|
const MAX_RESEND = 100;
|
|
2451
2538
|
const lastResults = /* @__PURE__ */ new Map();
|
|
2452
2539
|
const rememberResult = (jobId, frame) => {
|
|
@@ -2462,12 +2549,12 @@ async function startEdgeNode(opts) {
|
|
|
2462
2549
|
const trace = {
|
|
2463
2550
|
event: (frame) => send({ type: "trace-event", ...frame }),
|
|
2464
2551
|
finalised: (frame) => send({ type: "trace-finalised", ...frame }),
|
|
2465
|
-
requestBlobUrl: (req) => new Promise((
|
|
2552
|
+
requestBlobUrl: (req) => new Promise((resolve2) => {
|
|
2466
2553
|
const reqId = `${req.runId}:${req.stageId}:${req.attempt}:${blobReqCounter++}`;
|
|
2467
|
-
pendingBlob.set(reqId,
|
|
2554
|
+
pendingBlob.set(reqId, resolve2);
|
|
2468
2555
|
send({ type: "blob-put-request", reqId, ...req });
|
|
2469
2556
|
setTimeout(() => {
|
|
2470
|
-
if (pendingBlob.delete(reqId))
|
|
2557
|
+
if (pendingBlob.delete(reqId)) resolve2({ key: "" });
|
|
2471
2558
|
}, 3e4).unref?.();
|
|
2472
2559
|
})
|
|
2473
2560
|
};
|
|
@@ -2489,18 +2576,17 @@ async function startEdgeNode(opts) {
|
|
|
2489
2576
|
if (msg.type === "welcome") {
|
|
2490
2577
|
stageDeps.tenantId = msg.tenantId;
|
|
2491
2578
|
if (opts.retention === void 0 && msg.retention) stageDeps.retention = msg.retention;
|
|
2492
|
-
if (opts.heartbeatMs === void 0 && msg.heartbeatMs > 0) {
|
|
2493
|
-
|
|
2494
|
-
heartbeat = setInterval(() => send({ type: "heartbeat" }), msg.heartbeatMs);
|
|
2579
|
+
if (opts.heartbeatMs === void 0 && msg.heartbeatMs > 0 && ws) {
|
|
2580
|
+
startHeartbeat(ws, msg.heartbeatMs);
|
|
2495
2581
|
}
|
|
2496
2582
|
log(`EDGE_WELCOMED:${msg.name} tenant=${msg.tenantId} credentialMode=${msg.credentialMode}`);
|
|
2497
2583
|
return;
|
|
2498
2584
|
}
|
|
2499
2585
|
if (msg.type === "blob-put-url") {
|
|
2500
|
-
const
|
|
2501
|
-
if (
|
|
2586
|
+
const resolve2 = pendingBlob.get(msg.reqId);
|
|
2587
|
+
if (resolve2) {
|
|
2502
2588
|
pendingBlob.delete(msg.reqId);
|
|
2503
|
-
|
|
2589
|
+
resolve2({ key: msg.key, ...msg.url ? { url: msg.url } : {} });
|
|
2504
2590
|
}
|
|
2505
2591
|
return;
|
|
2506
2592
|
}
|
|
@@ -2522,6 +2608,12 @@ async function startEdgeNode(opts) {
|
|
|
2522
2608
|
log(`PUSH_DUPLICATE:${job2.runId} ${job2.jobId}`);
|
|
2523
2609
|
return;
|
|
2524
2610
|
}
|
|
2611
|
+
const cachedPush = lastResults.get(job2.jobId);
|
|
2612
|
+
if (cachedPush) {
|
|
2613
|
+
send(cachedPush);
|
|
2614
|
+
log(`PUSH_REPLAY:${job2.runId} ${job2.jobId}`);
|
|
2615
|
+
return;
|
|
2616
|
+
}
|
|
2525
2617
|
running.add(job2.jobId);
|
|
2526
2618
|
log(`PUSH_STARTED:${job2.runId} ${job2.branch}`);
|
|
2527
2619
|
try {
|
|
@@ -2550,6 +2642,12 @@ async function startEdgeNode(opts) {
|
|
|
2550
2642
|
log(`JOB_DUPLICATE:${job.stageId} ${job.jobId}`);
|
|
2551
2643
|
return;
|
|
2552
2644
|
}
|
|
2645
|
+
const cachedJob = lastResults.get(job.jobId);
|
|
2646
|
+
if (cachedJob) {
|
|
2647
|
+
send(cachedJob);
|
|
2648
|
+
log(`JOB_REPLAY:${job.stageId} ${job.jobId}`);
|
|
2649
|
+
return;
|
|
2650
|
+
}
|
|
2553
2651
|
running.add(job.jobId);
|
|
2554
2652
|
log(`JOB_STARTED:${job.stageId} ${job.jobId}`);
|
|
2555
2653
|
try {
|
|
@@ -2589,12 +2687,16 @@ async function startEdgeNode(opts) {
|
|
|
2589
2687
|
clientVersion: opts.clientVersion ?? "0.0.0"
|
|
2590
2688
|
});
|
|
2591
2689
|
for (const frame of lastResults.values()) send(frame);
|
|
2592
|
-
|
|
2690
|
+
startHeartbeat(sock, opts.heartbeatMs ?? 5e3);
|
|
2593
2691
|
});
|
|
2594
2692
|
sock.on("message", (raw) => void onMessage(raw.toString()));
|
|
2693
|
+
sock.on("pong", () => {
|
|
2694
|
+
lastPongAt = Date.now();
|
|
2695
|
+
});
|
|
2595
2696
|
sock.on("error", (e) => log(`EDGE_ERROR ${e.message}`));
|
|
2596
2697
|
sock.on("close", (code, reason) => {
|
|
2597
2698
|
if (heartbeat) clearInterval(heartbeat);
|
|
2699
|
+
heartbeat = void 0;
|
|
2598
2700
|
if (isEnrolmentRejection(code)) {
|
|
2599
2701
|
shuttingDown = true;
|
|
2600
2702
|
const detail = reason.toString() || "enrolment rejected";
|
|
@@ -2603,15 +2705,27 @@ async function startEdgeNode(opts) {
|
|
|
2603
2705
|
onFatal?.(new Error(`hub rejected edge enrolment (${code}): ${detail}`));
|
|
2604
2706
|
return;
|
|
2605
2707
|
}
|
|
2606
|
-
if (!shuttingDown) setTimeout(connect, 500);
|
|
2708
|
+
if (!shuttingDown) reconnectTimer = setTimeout(connect, 500);
|
|
2607
2709
|
});
|
|
2608
2710
|
};
|
|
2711
|
+
opts.signal?.addEventListener(
|
|
2712
|
+
"abort",
|
|
2713
|
+
() => {
|
|
2714
|
+
shuttingDown = true;
|
|
2715
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
2716
|
+
heartbeat = void 0;
|
|
2717
|
+
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
2718
|
+
reconnectTimer = void 0;
|
|
2719
|
+
ws?.close(1e3, "shutting down");
|
|
2720
|
+
},
|
|
2721
|
+
{ once: true }
|
|
2722
|
+
);
|
|
2609
2723
|
connect();
|
|
2610
|
-
await new Promise((
|
|
2724
|
+
await new Promise((resolve2, reject) => {
|
|
2611
2725
|
const t = setInterval(() => {
|
|
2612
2726
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
2613
2727
|
clearInterval(t);
|
|
2614
|
-
|
|
2728
|
+
resolve2();
|
|
2615
2729
|
}
|
|
2616
2730
|
}, 50);
|
|
2617
2731
|
onFatal = (err) => {
|
|
@@ -2629,11 +2743,11 @@ var PROBES = [
|
|
|
2629
2743
|
{ runtime: "pi", cmd: "pi" }
|
|
2630
2744
|
];
|
|
2631
2745
|
function probe(cmd, timeoutMs) {
|
|
2632
|
-
return new Promise((
|
|
2746
|
+
return new Promise((resolve2) => {
|
|
2633
2747
|
execFile(cmd, ["--version"], { timeout: timeoutMs }, (err, stdout) => {
|
|
2634
|
-
if (err) return
|
|
2748
|
+
if (err) return resolve2(void 0);
|
|
2635
2749
|
const line = stdout.split("\n").map((s) => s.trim()).find(Boolean);
|
|
2636
|
-
|
|
2750
|
+
resolve2(line ?? "");
|
|
2637
2751
|
});
|
|
2638
2752
|
});
|
|
2639
2753
|
}
|
|
@@ -2676,7 +2790,7 @@ function probeHub(opts) {
|
|
|
2676
2790
|
clientVersion = "0.0.0",
|
|
2677
2791
|
timeoutMs = 8e3
|
|
2678
2792
|
} = opts;
|
|
2679
|
-
return new Promise((
|
|
2793
|
+
return new Promise((resolve2) => {
|
|
2680
2794
|
let settled = false;
|
|
2681
2795
|
let opened = false;
|
|
2682
2796
|
let ws;
|
|
@@ -2688,7 +2802,7 @@ function probeHub(opts) {
|
|
|
2688
2802
|
ws.terminate();
|
|
2689
2803
|
} catch {
|
|
2690
2804
|
}
|
|
2691
|
-
|
|
2805
|
+
resolve2(result);
|
|
2692
2806
|
};
|
|
2693
2807
|
const timer = setTimeout(
|
|
2694
2808
|
() => done({ ok: false, reason: "timeout", detail: `no welcome within ${timeoutMs}ms` }),
|
|
@@ -2751,7 +2865,7 @@ function probeHub(opts) {
|
|
|
2751
2865
|
|
|
2752
2866
|
// src/cli.ts
|
|
2753
2867
|
import { parseArgs } from "util";
|
|
2754
|
-
var COMMANDS = /* @__PURE__ */ new Set(["start", "doctor"]);
|
|
2868
|
+
var COMMANDS = /* @__PURE__ */ new Set(["start", "run", "doctor", "update"]);
|
|
2755
2869
|
var isCommand = (s) => COMMANDS.has(s);
|
|
2756
2870
|
function parseCli(argv) {
|
|
2757
2871
|
const [first, ...rest] = argv;
|
|
@@ -2769,6 +2883,8 @@ function parseCli(argv) {
|
|
|
2769
2883
|
command = first;
|
|
2770
2884
|
flagArgs = rest;
|
|
2771
2885
|
}
|
|
2886
|
+
if (command === "run") return parseRun(flagArgs);
|
|
2887
|
+
if (command === "update") return parseUpdate(flagArgs);
|
|
2772
2888
|
let values;
|
|
2773
2889
|
try {
|
|
2774
2890
|
({ values } = parseArgs({
|
|
@@ -2794,6 +2910,55 @@ function parseCli(argv) {
|
|
|
2794
2910
|
};
|
|
2795
2911
|
return { kind: command, flags };
|
|
2796
2912
|
}
|
|
2913
|
+
function parseRun(flagArgs) {
|
|
2914
|
+
let values;
|
|
2915
|
+
let positionals;
|
|
2916
|
+
try {
|
|
2917
|
+
({ values, positionals } = parseArgs({
|
|
2918
|
+
args: flagArgs,
|
|
2919
|
+
options: {
|
|
2920
|
+
repo: { type: "string" },
|
|
2921
|
+
token: { type: "string" },
|
|
2922
|
+
"hub-url": { type: "string" },
|
|
2923
|
+
help: { type: "boolean", default: false }
|
|
2924
|
+
},
|
|
2925
|
+
allowPositionals: true
|
|
2926
|
+
}));
|
|
2927
|
+
} catch (e) {
|
|
2928
|
+
return { kind: "error", message: e.message };
|
|
2929
|
+
}
|
|
2930
|
+
if (values.help) return { kind: "help", command: "run" };
|
|
2931
|
+
if (positionals.length === 0) {
|
|
2932
|
+
return { kind: "error", message: "run: missing workflow (e.g. `dahrk run preflight`)" };
|
|
2933
|
+
}
|
|
2934
|
+
if (positionals.length > 1) {
|
|
2935
|
+
return { kind: "error", message: `run: unexpected argument "${positionals[1]}" (one workflow at a time)` };
|
|
2936
|
+
}
|
|
2937
|
+
const flags = {
|
|
2938
|
+
workflow: positionals[0],
|
|
2939
|
+
...values.repo ? { repo: values.repo } : {},
|
|
2940
|
+
...values.token ? { token: values.token } : {},
|
|
2941
|
+
...values["hub-url"] ? { hubUrl: values["hub-url"] } : {}
|
|
2942
|
+
};
|
|
2943
|
+
return { kind: "run", flags };
|
|
2944
|
+
}
|
|
2945
|
+
function parseUpdate(flagArgs) {
|
|
2946
|
+
let values;
|
|
2947
|
+
try {
|
|
2948
|
+
({ values } = parseArgs({
|
|
2949
|
+
args: flagArgs,
|
|
2950
|
+
options: {
|
|
2951
|
+
check: { type: "boolean", default: false },
|
|
2952
|
+
help: { type: "boolean", default: false }
|
|
2953
|
+
},
|
|
2954
|
+
allowPositionals: false
|
|
2955
|
+
}));
|
|
2956
|
+
} catch (e) {
|
|
2957
|
+
return { kind: "error", message: e.message };
|
|
2958
|
+
}
|
|
2959
|
+
if (values.help) return { kind: "help", command: "update" };
|
|
2960
|
+
return { kind: "update", flags: { check: values.check ?? false } };
|
|
2961
|
+
}
|
|
2797
2962
|
function usage(bin, command) {
|
|
2798
2963
|
if (command === "start") {
|
|
2799
2964
|
return [
|
|
@@ -2808,6 +2973,22 @@ function usage(bin, command) {
|
|
|
2808
2973
|
" --ephemeral Do not persist a node id; mint a throwaway one (CI / one-shot)."
|
|
2809
2974
|
].join("\n");
|
|
2810
2975
|
}
|
|
2976
|
+
if (command === "run") {
|
|
2977
|
+
return [
|
|
2978
|
+
`Usage: ${bin} run <workflow> [options]`,
|
|
2979
|
+
"",
|
|
2980
|
+
"Run a workflow through the engine locally against this node's worktree, streaming stage progress.",
|
|
2981
|
+
"The engine-backed twin of `doctor`: the same run and stages, from the terminal - no Linear, no issue.",
|
|
2982
|
+
"",
|
|
2983
|
+
"Workflows:",
|
|
2984
|
+
" preflight Is this floor sound enough to run? Checks node, repo, and tools, then reports.",
|
|
2985
|
+
"",
|
|
2986
|
+
"Options:",
|
|
2987
|
+
" --repo <path> Repo to inspect (default: the current directory).",
|
|
2988
|
+
" --hub-url <url> Hub WebSocket URL to probe for reachability (or set DAHRK_HUB_URL).",
|
|
2989
|
+
" --token <token> Enrolment token to verify against the hub (or set DAHRK_ENROL_TOKEN)."
|
|
2990
|
+
].join("\n");
|
|
2991
|
+
}
|
|
2811
2992
|
if (command === "doctor") {
|
|
2812
2993
|
return [
|
|
2813
2994
|
`Usage: ${bin} doctor [options]`,
|
|
@@ -2819,12 +3000,26 @@ function usage(bin, command) {
|
|
|
2819
3000
|
" --hub-url <url> Hub WebSocket URL to reach (or set DAHRK_HUB_URL)."
|
|
2820
3001
|
].join("\n");
|
|
2821
3002
|
}
|
|
3003
|
+
if (command === "update") {
|
|
3004
|
+
return [
|
|
3005
|
+
`Usage: ${bin} update [--check]`,
|
|
3006
|
+
"",
|
|
3007
|
+
"Update this client in place to the latest published release. Detects how it was installed",
|
|
3008
|
+
"(npm / Homebrew / curl) and runs the right upgrade, or prints the exact command when it cannot.",
|
|
3009
|
+
"Reports current -> latest, and a no-op when already current.",
|
|
3010
|
+
"",
|
|
3011
|
+
"Options:",
|
|
3012
|
+
" --check Report whether an update is available without applying it (dry run)."
|
|
3013
|
+
].join("\n");
|
|
3014
|
+
}
|
|
2822
3015
|
return [
|
|
2823
3016
|
`Usage: ${bin} <command> [options]`,
|
|
2824
3017
|
"",
|
|
2825
3018
|
"Commands:",
|
|
2826
3019
|
" start Run the edge node (default). Needs a --token and a hub URL.",
|
|
3020
|
+
" run Run a workflow locally (engine-backed), e.g. `run preflight`.",
|
|
2827
3021
|
" doctor Preflight checks: Node, runtimes, hub reachability, token validity.",
|
|
3022
|
+
" update Update the client to the latest release (or print how for your channel).",
|
|
2828
3023
|
" version Print the client version.",
|
|
2829
3024
|
" help Show this help, or `help <command>` for a command's options.",
|
|
2830
3025
|
"",
|
|
@@ -2963,20 +3158,359 @@ async function runDoctor(inputs, deps = {}) {
|
|
|
2963
3158
|
return checks.some((c) => c.status === "fail") ? 1 : 0;
|
|
2964
3159
|
}
|
|
2965
3160
|
|
|
3161
|
+
// src/preflight.ts
|
|
3162
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
3163
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
3164
|
+
import { accessSync, constants as fsConstants, existsSync as existsSync4, readdirSync as readdirSync3, statfsSync } from "fs";
|
|
3165
|
+
import { homedir as homedir2 } from "os";
|
|
3166
|
+
import { join as join7 } from "path";
|
|
3167
|
+
var REPORT_BASE_URL = "https://app.dahrk.ai/r";
|
|
3168
|
+
var LOW_DISK_BYTES = 512 * 1024 * 1024;
|
|
3169
|
+
var PREFLIGHT_STAGES = [
|
|
3170
|
+
{ id: "check-node", label: "check node" },
|
|
3171
|
+
{ id: "check-repo", label: "check repo" },
|
|
3172
|
+
{ id: "check-tools", label: "check tools" },
|
|
3173
|
+
{ id: "analyse", label: "analyse" },
|
|
3174
|
+
{ id: "report", label: "report" }
|
|
3175
|
+
];
|
|
3176
|
+
function checkWorktreeRoot(writable2) {
|
|
3177
|
+
return writable2 ? { status: "pass", label: "Worktree root", detail: "writable" } : { status: "fail", label: "Worktree root", detail: "not writable; the node cannot create worktrees here" };
|
|
3178
|
+
}
|
|
3179
|
+
function checkDiskSpace(freeBytes) {
|
|
3180
|
+
if (freeBytes === void 0) {
|
|
3181
|
+
return { status: "warn", label: "Free space", detail: "could not be determined" };
|
|
3182
|
+
}
|
|
3183
|
+
const gib = (freeBytes / (1024 * 1024 * 1024)).toFixed(1);
|
|
3184
|
+
return freeBytes >= LOW_DISK_BYTES ? { status: "pass", label: "Free space", detail: `${gib} GiB` } : { status: "warn", label: "Free space", detail: `only ${gib} GiB free; a clone + worktree may not fit` };
|
|
3185
|
+
}
|
|
3186
|
+
function checkRepo(repo) {
|
|
3187
|
+
if (!repo.isGitRepo) {
|
|
3188
|
+
return {
|
|
3189
|
+
status: "fail",
|
|
3190
|
+
label: "Repository",
|
|
3191
|
+
detail: `${repo.path} is not a git repository${repo.detail ? ` (${repo.detail})` : ""}`
|
|
3192
|
+
};
|
|
3193
|
+
}
|
|
3194
|
+
if (!repo.headResolves) {
|
|
3195
|
+
return { status: "warn", label: "Repository", detail: `${repo.path}: no commits yet (base branch does not resolve)` };
|
|
3196
|
+
}
|
|
3197
|
+
return { status: "pass", label: "Repository", detail: `${repo.path} on ${repo.baseBranch ?? "(detached)"}` };
|
|
3198
|
+
}
|
|
3199
|
+
function checkTools(tools) {
|
|
3200
|
+
const git = tools.git ? { status: "pass", label: "git", detail: "installed" } : { status: "fail", label: "git", detail: "not found; git is required to run a workflow" };
|
|
3201
|
+
const finding = (present, label, missing) => present ? { status: "pass", label, detail: "available" } : { status: "warn", label, detail: missing };
|
|
3202
|
+
return [
|
|
3203
|
+
git,
|
|
3204
|
+
finding(tools.sshKey, "SSH key", "no key or agent identity found; pushing over SSH will fail"),
|
|
3205
|
+
finding(tools.claude, "Claude runtime", "not on PATH; the node will serve no agent stages"),
|
|
3206
|
+
finding(tools.gh, "gh CLI", "not installed; ambient PR opening is unavailable"),
|
|
3207
|
+
finding(tools.docker, "docker", "not present; container stages are unavailable")
|
|
3208
|
+
];
|
|
3209
|
+
}
|
|
3210
|
+
function asFinding(check) {
|
|
3211
|
+
return check.status === "fail" ? { ...check, status: "warn" } : check;
|
|
3212
|
+
}
|
|
3213
|
+
function synthesise(checks) {
|
|
3214
|
+
const fails = checks.filter((c) => c.status === "fail");
|
|
3215
|
+
const warns = checks.filter((c) => c.status === "warn");
|
|
3216
|
+
const list2 = (cs) => cs.map((c) => `${c.label.toLowerCase()} (${c.detail ?? "finding"})`).join("; ");
|
|
3217
|
+
if (fails.length > 0) {
|
|
3218
|
+
return `The floor is unsound: ${list2(fails)}. Fix ${fails.length === 1 ? "this" : "these"} before running a workflow here.`;
|
|
3219
|
+
}
|
|
3220
|
+
if (warns.length > 0) {
|
|
3221
|
+
return `The floor is sound, with ${warns.length} early warning${warns.length === 1 ? "" : "s"}: ${list2(warns)}. ${warns.length === 1 ? "It does" : "These do"} not block a run, but ${warns.length === 1 ? "is" : "are"} worth addressing.`;
|
|
3222
|
+
}
|
|
3223
|
+
return "The floor is sound. Node, repo, and tools all check out - this host is ready to run a workflow.";
|
|
3224
|
+
}
|
|
3225
|
+
function worst(checks) {
|
|
3226
|
+
if (checks.some((c) => c.status === "fail")) return "fail";
|
|
3227
|
+
if (checks.some((c) => c.status === "warn")) return "warn";
|
|
3228
|
+
return "pass";
|
|
3229
|
+
}
|
|
3230
|
+
function renderStage(index, total, verdict, out) {
|
|
3231
|
+
const head = `[${index}/${total}] ${verdict.label}`;
|
|
3232
|
+
const findings = verdict.checks.filter((c) => c.status !== "pass");
|
|
3233
|
+
if (findings.length === 0) {
|
|
3234
|
+
out(`${head} \u2713`);
|
|
3235
|
+
return;
|
|
3236
|
+
}
|
|
3237
|
+
out(head);
|
|
3238
|
+
for (const f of findings) out(` \u2022 ${f.label}: ${f.detail ?? f.status}`);
|
|
3239
|
+
}
|
|
3240
|
+
async function runPreflight(inputs, deps = {}) {
|
|
3241
|
+
const d = { ...defaultDeps2(), ...deps };
|
|
3242
|
+
const repoPath = inputs.repoPath ?? process.cwd();
|
|
3243
|
+
const total = PREFLIGHT_STAGES.length;
|
|
3244
|
+
const runId = d.newRunId();
|
|
3245
|
+
d.out("dahrk run preflight");
|
|
3246
|
+
d.out("");
|
|
3247
|
+
const host = await d.gatherHost(repoPath);
|
|
3248
|
+
const probe2 = inputs.hubUrl ? await d.probeHub({
|
|
3249
|
+
hubUrl: inputs.hubUrl,
|
|
3250
|
+
...inputs.token ? { enrolToken: inputs.token } : {},
|
|
3251
|
+
runtimes: host.tools.claude ? ["claude-code"] : [],
|
|
3252
|
+
...inputs.clientVersion ? { clientVersion: inputs.clientVersion } : {}
|
|
3253
|
+
}) : void 0;
|
|
3254
|
+
const nodeChecks = [
|
|
3255
|
+
checkNode(d.nodeVersion),
|
|
3256
|
+
checkWorktreeRoot(host.worktreeRootWritable),
|
|
3257
|
+
checkDiskSpace(host.freeDiskBytes),
|
|
3258
|
+
asFinding(checkHub(inputs.hubUrl, probe2)),
|
|
3259
|
+
...inputs.token ? [asFinding(checkToken(true, inputs.hubUrl, probe2))] : []
|
|
3260
|
+
];
|
|
3261
|
+
const repoChecks = [checkRepo(host.repo)];
|
|
3262
|
+
const toolChecks = checkTools(host.tools);
|
|
3263
|
+
const stageVerdicts = [
|
|
3264
|
+
{ label: PREFLIGHT_STAGES[0].label, status: worst(nodeChecks), checks: nodeChecks },
|
|
3265
|
+
{ label: PREFLIGHT_STAGES[1].label, status: worst(repoChecks), checks: repoChecks },
|
|
3266
|
+
{ label: PREFLIGHT_STAGES[2].label, status: worst(toolChecks), checks: toolChecks }
|
|
3267
|
+
];
|
|
3268
|
+
stageVerdicts.forEach((v, i) => renderStage(i + 1, total, v, d.out));
|
|
3269
|
+
const allChecks = [...nodeChecks, ...repoChecks, ...toolChecks];
|
|
3270
|
+
const read = synthesise(allChecks);
|
|
3271
|
+
renderStage(4, total, { label: PREFLIGHT_STAGES[3].label, status: "pass", checks: [] }, d.out);
|
|
3272
|
+
renderStage(5, total, { label: PREFLIGHT_STAGES[4].label, status: "pass", checks: [] }, d.out);
|
|
3273
|
+
const failed = allChecks.filter((c) => c.status === "fail").length;
|
|
3274
|
+
const warned = allChecks.filter((c) => c.status === "warn").length;
|
|
3275
|
+
const summary = failed > 0 ? `UNSOUND - ${failed} floor check${failed === 1 ? "" : "s"} failed${warned ? `, ${warned} finding${warned === 1 ? "" : "s"}` : ""}.` : warned > 0 ? `SOUND with ${warned} finding${warned === 1 ? "" : "s"}.` : "SOUND - all checks green.";
|
|
3276
|
+
d.out("");
|
|
3277
|
+
d.out(read);
|
|
3278
|
+
d.out("");
|
|
3279
|
+
d.out(summary);
|
|
3280
|
+
d.out(`Full report: ${REPORT_BASE_URL}/${runId}`);
|
|
3281
|
+
d.out("");
|
|
3282
|
+
d.out("no Linear, no OAuth, no issue. just the machine and the engine.");
|
|
3283
|
+
return failed > 0 ? 1 : 0;
|
|
3284
|
+
}
|
|
3285
|
+
var defaultDeps2 = () => ({
|
|
3286
|
+
nodeVersion: process.versions.node,
|
|
3287
|
+
probeHub,
|
|
3288
|
+
gatherHost: gatherHostFacts,
|
|
3289
|
+
newRunId: () => randomUUID2(),
|
|
3290
|
+
out: (line) => void process.stdout.write(`${line}
|
|
3291
|
+
`)
|
|
3292
|
+
});
|
|
3293
|
+
function commandPresent(cmd) {
|
|
3294
|
+
try {
|
|
3295
|
+
execFileSync4("sh", ["-c", `command -v ${cmd}`], { stdio: "ignore" });
|
|
3296
|
+
return true;
|
|
3297
|
+
} catch {
|
|
3298
|
+
return false;
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
function sshKeyPresent() {
|
|
3302
|
+
try {
|
|
3303
|
+
const dir = join7(homedir2(), ".ssh");
|
|
3304
|
+
if (existsSync4(dir) && readdirSync3(dir).some((f) => f.endsWith(".pub"))) return true;
|
|
3305
|
+
} catch {
|
|
3306
|
+
}
|
|
3307
|
+
try {
|
|
3308
|
+
execFileSync4("ssh-add", ["-l"], { stdio: "ignore" });
|
|
3309
|
+
return true;
|
|
3310
|
+
} catch {
|
|
3311
|
+
return false;
|
|
3312
|
+
}
|
|
3313
|
+
}
|
|
3314
|
+
function writable(dir) {
|
|
3315
|
+
try {
|
|
3316
|
+
accessSync(dir, fsConstants.W_OK);
|
|
3317
|
+
return true;
|
|
3318
|
+
} catch {
|
|
3319
|
+
return false;
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
function worktreeRoot(env) {
|
|
3323
|
+
return env.DAHRK_WORKTREES_DIR ?? join7(env.DAHRK_STATE_DIR ?? join7(homedir2(), ".dahrk"), "worktrees");
|
|
3324
|
+
}
|
|
3325
|
+
function nearestExisting(dir) {
|
|
3326
|
+
let cur = dir;
|
|
3327
|
+
while (!existsSync4(cur)) {
|
|
3328
|
+
const parent = join7(cur, "..");
|
|
3329
|
+
if (parent === cur) break;
|
|
3330
|
+
cur = parent;
|
|
3331
|
+
}
|
|
3332
|
+
return cur;
|
|
3333
|
+
}
|
|
3334
|
+
function freeDiskBytes(dir) {
|
|
3335
|
+
try {
|
|
3336
|
+
const s = statfsSync(nearestExisting(dir));
|
|
3337
|
+
return s.bavail * s.bsize;
|
|
3338
|
+
} catch {
|
|
3339
|
+
return void 0;
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
function probeRepo(repoPath) {
|
|
3343
|
+
const git = (args) => execFileSync4("git", ["-C", repoPath, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
3344
|
+
try {
|
|
3345
|
+
if (git(["rev-parse", "--is-inside-work-tree"]) !== "true") {
|
|
3346
|
+
return { path: repoPath, isGitRepo: false, headResolves: false, detail: "not inside a work tree" };
|
|
3347
|
+
}
|
|
3348
|
+
} catch {
|
|
3349
|
+
return { path: repoPath, isGitRepo: false, headResolves: false };
|
|
3350
|
+
}
|
|
3351
|
+
let headResolves = false;
|
|
3352
|
+
let baseBranch;
|
|
3353
|
+
try {
|
|
3354
|
+
git(["rev-parse", "--verify", "HEAD"]);
|
|
3355
|
+
headResolves = true;
|
|
3356
|
+
baseBranch = git(["rev-parse", "--abbrev-ref", "HEAD"]) || void 0;
|
|
3357
|
+
} catch {
|
|
3358
|
+
}
|
|
3359
|
+
return { path: repoPath, isGitRepo: true, headResolves, ...baseBranch ? { baseBranch } : {} };
|
|
3360
|
+
}
|
|
3361
|
+
function gatherHostFacts(repoPath) {
|
|
3362
|
+
const root = worktreeRoot(process.env);
|
|
3363
|
+
return {
|
|
3364
|
+
worktreeRootWritable: writable(nearestExisting(root)),
|
|
3365
|
+
freeDiskBytes: freeDiskBytes(root),
|
|
3366
|
+
tools: {
|
|
3367
|
+
git: commandPresent("git"),
|
|
3368
|
+
sshKey: sshKeyPresent(),
|
|
3369
|
+
claude: commandPresent("claude"),
|
|
3370
|
+
gh: commandPresent("gh"),
|
|
3371
|
+
docker: commandPresent("docker")
|
|
3372
|
+
},
|
|
3373
|
+
repo: probeRepo(repoPath)
|
|
3374
|
+
};
|
|
3375
|
+
}
|
|
3376
|
+
|
|
3377
|
+
// src/update.ts
|
|
3378
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
3379
|
+
import { realpathSync } from "fs";
|
|
3380
|
+
var LATEST_URL = "https://registry.npmjs.org/dahrk-node/latest";
|
|
3381
|
+
var CHANNEL_COMMANDS = {
|
|
3382
|
+
npm: "npm install -g dahrk-node@latest",
|
|
3383
|
+
homebrew: "brew upgrade dahrkai/tap/dahrk",
|
|
3384
|
+
curl: "curl -fsSL https://dahrk.ai/install.sh | sh"
|
|
3385
|
+
};
|
|
3386
|
+
function upgradeCommand(channel) {
|
|
3387
|
+
switch (channel) {
|
|
3388
|
+
case "npm":
|
|
3389
|
+
return { argv: ["npm", "install", "-g", "dahrk-node@latest"], display: CHANNEL_COMMANDS.npm };
|
|
3390
|
+
case "homebrew":
|
|
3391
|
+
return { argv: ["brew", "upgrade", "dahrkai/tap/dahrk"], display: CHANNEL_COMMANDS.homebrew };
|
|
3392
|
+
case "unknown":
|
|
3393
|
+
return null;
|
|
3394
|
+
}
|
|
3395
|
+
}
|
|
3396
|
+
function detectChannel(binPath) {
|
|
3397
|
+
if (!binPath) return "unknown";
|
|
3398
|
+
let resolved = binPath;
|
|
3399
|
+
try {
|
|
3400
|
+
resolved = realpathSync(binPath);
|
|
3401
|
+
} catch {
|
|
3402
|
+
}
|
|
3403
|
+
if (/[\\/]node_modules[\\/]dahrk-node[\\/]/.test(resolved)) return "npm";
|
|
3404
|
+
if (/[\\/](Cellar|homebrew)[\\/]/.test(resolved)) return "homebrew";
|
|
3405
|
+
return "unknown";
|
|
3406
|
+
}
|
|
3407
|
+
function core(v) {
|
|
3408
|
+
const m = /^(\d+)\.(\d+)\.(\d+)/.exec(v.trim().replace(/^v/, ""));
|
|
3409
|
+
if (!m) return null;
|
|
3410
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
3411
|
+
}
|
|
3412
|
+
function isNewer(latest, current) {
|
|
3413
|
+
const a = core(latest);
|
|
3414
|
+
const b = core(current);
|
|
3415
|
+
if (!a || !b) return false;
|
|
3416
|
+
for (let i = 0; i < 3; i++) {
|
|
3417
|
+
if (a[i] > b[i]) return true;
|
|
3418
|
+
if (a[i] < b[i]) return false;
|
|
3419
|
+
}
|
|
3420
|
+
return false;
|
|
3421
|
+
}
|
|
3422
|
+
function printChannelCommands(out) {
|
|
3423
|
+
out(` npm: ${CHANNEL_COMMANDS.npm}`);
|
|
3424
|
+
out(` Homebrew: ${CHANNEL_COMMANDS.homebrew}`);
|
|
3425
|
+
out(` curl: ${CHANNEL_COMMANDS.curl}`);
|
|
3426
|
+
}
|
|
3427
|
+
async function runUpdate(inputs, deps = {}) {
|
|
3428
|
+
const d = { ...defaultDeps3(), ...deps };
|
|
3429
|
+
const current = inputs.currentVersion;
|
|
3430
|
+
d.out("dahrk update");
|
|
3431
|
+
d.out("");
|
|
3432
|
+
let latest;
|
|
3433
|
+
try {
|
|
3434
|
+
latest = await d.fetchLatest();
|
|
3435
|
+
} catch (e) {
|
|
3436
|
+
d.out(`Could not determine the latest version: ${e.message}`);
|
|
3437
|
+
d.out(`You are on ${current}. See https://www.npmjs.com/package/dahrk-node for releases.`);
|
|
3438
|
+
return 1;
|
|
3439
|
+
}
|
|
3440
|
+
if (!isNewer(latest, current)) {
|
|
3441
|
+
d.out(`Already on the latest version (${current}).`);
|
|
3442
|
+
return 0;
|
|
3443
|
+
}
|
|
3444
|
+
d.out(`Update available: ${current} -> ${latest}`);
|
|
3445
|
+
const channel = detectChannel(d.binPath);
|
|
3446
|
+
const cmd = upgradeCommand(channel);
|
|
3447
|
+
if (inputs.check) {
|
|
3448
|
+
d.out("");
|
|
3449
|
+
if (cmd) {
|
|
3450
|
+
d.out(`Run \`dahrk update\` to upgrade (${channel}: ${cmd.display}).`);
|
|
3451
|
+
} else {
|
|
3452
|
+
d.out("Run `dahrk update`, or upgrade with the command for your install channel:");
|
|
3453
|
+
printChannelCommands(d.out);
|
|
3454
|
+
}
|
|
3455
|
+
return 0;
|
|
3456
|
+
}
|
|
3457
|
+
if (!cmd) {
|
|
3458
|
+
d.out("");
|
|
3459
|
+
d.out("Could not tell how this client was installed. Upgrade with the command for your channel:");
|
|
3460
|
+
printChannelCommands(d.out);
|
|
3461
|
+
return 0;
|
|
3462
|
+
}
|
|
3463
|
+
d.out("");
|
|
3464
|
+
d.out(`Upgrading via ${channel}: ${cmd.display}`);
|
|
3465
|
+
d.out("");
|
|
3466
|
+
const code = d.runUpgrade(cmd.argv);
|
|
3467
|
+
d.out("");
|
|
3468
|
+
if (code === 0) {
|
|
3469
|
+
d.out(`Upgraded to ${latest}. Restart a running node (\`dahrk start\`) to pick it up.`);
|
|
3470
|
+
} else {
|
|
3471
|
+
d.out(`Upgrade exited with code ${code}. Run it yourself to see the error: ${cmd.display}`);
|
|
3472
|
+
}
|
|
3473
|
+
return code;
|
|
3474
|
+
}
|
|
3475
|
+
async function fetchLatestVersion() {
|
|
3476
|
+
const res = await fetch(LATEST_URL, { headers: { accept: "application/json" } });
|
|
3477
|
+
if (!res.ok) throw new Error(`registry responded ${res.status}`);
|
|
3478
|
+
const body = await res.json();
|
|
3479
|
+
if (typeof body.version !== "string" || !body.version) throw new Error("registry returned no version");
|
|
3480
|
+
return body.version;
|
|
3481
|
+
}
|
|
3482
|
+
function spawnUpgrade(argv) {
|
|
3483
|
+
const [cmd, ...args] = argv;
|
|
3484
|
+
try {
|
|
3485
|
+
execFileSync5(cmd, args, { stdio: "inherit" });
|
|
3486
|
+
return 0;
|
|
3487
|
+
} catch (e) {
|
|
3488
|
+
const status = e.status;
|
|
3489
|
+
return typeof status === "number" ? status : 1;
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
3492
|
+
var defaultDeps3 = () => ({
|
|
3493
|
+
binPath: process.argv[1],
|
|
3494
|
+
fetchLatest: fetchLatestVersion,
|
|
3495
|
+
runUpgrade: spawnUpgrade,
|
|
3496
|
+
out: (line) => void process.stdout.write(`${line}
|
|
3497
|
+
`)
|
|
3498
|
+
});
|
|
3499
|
+
|
|
2966
3500
|
// src/main.ts
|
|
2967
|
-
var CLIENT_VERSION = "0.1.
|
|
3501
|
+
var CLIENT_VERSION = "0.1.4";
|
|
2968
3502
|
var DEFAULT_HUB_URL = "wss://api.dahrk.ai";
|
|
2969
3503
|
var list = (v) => (v ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
2970
3504
|
var RUNTIMES = ["claude-code", "codex", "pi"];
|
|
2971
3505
|
var isRuntime = (r) => RUNTIMES.includes(r);
|
|
2972
3506
|
function stateDir(env) {
|
|
2973
|
-
return env.DAHRK_STATE_DIR ??
|
|
3507
|
+
return env.DAHRK_STATE_DIR ?? join8(homedir3(), ".dahrk");
|
|
2974
3508
|
}
|
|
2975
3509
|
function legacyStateDir(env) {
|
|
2976
|
-
return env.DAHRK_STATE_DIR ? void 0 :
|
|
3510
|
+
return env.DAHRK_STATE_DIR ? void 0 : join8(homedir3(), ".skakel");
|
|
2977
3511
|
}
|
|
2978
3512
|
function readNodeId(file) {
|
|
2979
|
-
if (!
|
|
3513
|
+
if (!existsSync5(file)) return void 0;
|
|
2980
3514
|
try {
|
|
2981
3515
|
const parsed = JSON.parse(readFileSync5(file, "utf8"));
|
|
2982
3516
|
if (typeof parsed.nodeId === "string" && parsed.nodeId) return parsed.nodeId;
|
|
@@ -2986,17 +3520,17 @@ function readNodeId(file) {
|
|
|
2986
3520
|
}
|
|
2987
3521
|
function resolveNodeId(env, opts = {}) {
|
|
2988
3522
|
if (env.DAHRK_NODE_ID) return env.DAHRK_NODE_ID;
|
|
2989
|
-
if (opts.ephemeral) return
|
|
3523
|
+
if (opts.ephemeral) return randomUUID3();
|
|
2990
3524
|
const dir = stateDir(env);
|
|
2991
|
-
const file =
|
|
3525
|
+
const file = join8(dir, "node.json");
|
|
2992
3526
|
const existing = readNodeId(file);
|
|
2993
3527
|
if (existing) return existing;
|
|
2994
3528
|
const legacy = legacyStateDir(env);
|
|
2995
3529
|
if (legacy) {
|
|
2996
|
-
const legacyId = readNodeId(
|
|
3530
|
+
const legacyId = readNodeId(join8(legacy, "node.json"));
|
|
2997
3531
|
if (legacyId) return legacyId;
|
|
2998
3532
|
}
|
|
2999
|
-
const nodeId =
|
|
3533
|
+
const nodeId = randomUUID3();
|
|
3000
3534
|
try {
|
|
3001
3535
|
mkdirSync6(dir, { recursive: true });
|
|
3002
3536
|
writeFileSync6(file, `${JSON.stringify({ nodeId }, null, 2)}
|
|
@@ -3078,6 +3612,24 @@ async function start(flags) {
|
|
|
3078
3612
|
const resolved = { nodeId, runtimes, clientVersion: CLIENT_VERSION };
|
|
3079
3613
|
await startEdgeNode(buildEdgeOptions(env, resolved));
|
|
3080
3614
|
}
|
|
3615
|
+
var KNOWN_WORKFLOWS = ["preflight"];
|
|
3616
|
+
async function runWorkflow(flags) {
|
|
3617
|
+
if (flags.workflow !== "preflight") {
|
|
3618
|
+
console.error(`unknown workflow: ${flags.workflow}
|
|
3619
|
+
`);
|
|
3620
|
+
console.error(`Available workflows: ${KNOWN_WORKFLOWS.join(", ")}`);
|
|
3621
|
+
return 2;
|
|
3622
|
+
}
|
|
3623
|
+
const env = applyEnvAliases(process.env);
|
|
3624
|
+
const hubUrl = flags.hubUrl ?? env.DAHRK_HUB_URL;
|
|
3625
|
+
const token = flags.token ?? env.DAHRK_ENROL_TOKEN;
|
|
3626
|
+
return runPreflight({
|
|
3627
|
+
...flags.repo ? { repoPath: flags.repo } : {},
|
|
3628
|
+
...hubUrl ? { hubUrl } : {},
|
|
3629
|
+
...token ? { token } : {},
|
|
3630
|
+
clientVersion: CLIENT_VERSION
|
|
3631
|
+
});
|
|
3632
|
+
}
|
|
3081
3633
|
async function main() {
|
|
3082
3634
|
const invoked = basename(process.argv[1] ?? "");
|
|
3083
3635
|
const bin = !invoked || invoked.startsWith("main.") ? "dahrk" : invoked;
|
|
@@ -3104,6 +3656,12 @@ async function main() {
|
|
|
3104
3656
|
});
|
|
3105
3657
|
break;
|
|
3106
3658
|
}
|
|
3659
|
+
case "run":
|
|
3660
|
+
process.exitCode = await runWorkflow(parsed.flags);
|
|
3661
|
+
break;
|
|
3662
|
+
case "update":
|
|
3663
|
+
process.exitCode = await runUpdate({ currentVersion: CLIENT_VERSION, check: parsed.flags.check });
|
|
3664
|
+
break;
|
|
3107
3665
|
case "start":
|
|
3108
3666
|
await start(parsed.flags);
|
|
3109
3667
|
break;
|
|
@@ -3113,7 +3671,7 @@ var invokedAsEntrypoint = (() => {
|
|
|
3113
3671
|
const argv1 = process.argv[1];
|
|
3114
3672
|
if (!argv1) return false;
|
|
3115
3673
|
try {
|
|
3116
|
-
return pathToFileURL(
|
|
3674
|
+
return pathToFileURL(realpathSync2(argv1)).href === import.meta.url;
|
|
3117
3675
|
} catch {
|
|
3118
3676
|
return false;
|
|
3119
3677
|
}
|
|
@@ -3126,6 +3684,7 @@ if (invokedAsEntrypoint) {
|
|
|
3126
3684
|
}
|
|
3127
3685
|
export {
|
|
3128
3686
|
DEFAULT_HUB_URL,
|
|
3687
|
+
KNOWN_WORKFLOWS,
|
|
3129
3688
|
buildEdgeOptions,
|
|
3130
3689
|
resolveNodeId,
|
|
3131
3690
|
resolveRuntimes
|