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.
Files changed (2) hide show
  1. package/dist/main.js +619 -60
  2. 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 existsSync4, mkdirSync as mkdirSync6, readFileSync as readFileSync5, realpathSync, writeFileSync as writeFileSync6 } from "fs";
5
- import { randomUUID as randomUUID2 } from "crypto";
6
- import { homedir as homedir2 } from "os";
7
- import { basename, join as join7 } from "path";
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((resolve) => this.waiters.push(resolve));
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((resolve) => {
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
- resolve(r);
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 (_toolName, input) => ({ behavior: "allow", updatedInput: input })
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." } : { behavior: "allow", updatedInput: input },
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 = ".skakel/scratch/";
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 sep2 = existing && !existing.endsWith("\n") ? "\n" : "";
1266
- writeFileSync(excludePath, `${existing}${sep2}${entry}
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", SCRATCH]);
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((resolve) => server.listen(0, "127.0.0.1", resolve));
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((resolve) => server.close(() => resolve()))
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(join6(ref.worktreePath, relPath), "utf8");
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 verdict = evaluatePolicies(
2226
- { kind: "action", stageId, tool: event.tool, input: event.input },
2227
- rules
2228
- );
2229
- if (verdict.verdict === "deny") {
2230
- denied = true;
2231
- streamEvent(writer.append(event));
2232
- streamEvent(writer.append({ seq: 0, ts: nowIso2(), type: "observation", runtime, toolUseId: event.toolUseId, isError: true, output: { error: verdict.reason } }));
2233
- streamEvent(writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime, event: "policy-deny", detail: verdict.reason }));
2234
- deps.sendProgress({ jobId, kind: "error", ts: nowIso2(), text: verdict.reason });
2235
- return;
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((resolve) => {
2552
+ requestBlobUrl: (req) => new Promise((resolve2) => {
2466
2553
  const reqId = `${req.runId}:${req.stageId}:${req.attempt}:${blobReqCounter++}`;
2467
- pendingBlob.set(reqId, resolve);
2554
+ pendingBlob.set(reqId, resolve2);
2468
2555
  send({ type: "blob-put-request", reqId, ...req });
2469
2556
  setTimeout(() => {
2470
- if (pendingBlob.delete(reqId)) resolve({ key: "" });
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
- if (heartbeat) clearInterval(heartbeat);
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 resolve = pendingBlob.get(msg.reqId);
2501
- if (resolve) {
2586
+ const resolve2 = pendingBlob.get(msg.reqId);
2587
+ if (resolve2) {
2502
2588
  pendingBlob.delete(msg.reqId);
2503
- resolve({ key: msg.key, ...msg.url ? { url: msg.url } : {} });
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
- heartbeat = setInterval(() => send({ type: "heartbeat" }), opts.heartbeatMs ?? 5e3);
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((resolve, reject) => {
2724
+ await new Promise((resolve2, reject) => {
2611
2725
  const t = setInterval(() => {
2612
2726
  if (ws && ws.readyState === WebSocket.OPEN) {
2613
2727
  clearInterval(t);
2614
- resolve();
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((resolve) => {
2746
+ return new Promise((resolve2) => {
2633
2747
  execFile(cmd, ["--version"], { timeout: timeoutMs }, (err, stdout) => {
2634
- if (err) return resolve(void 0);
2748
+ if (err) return resolve2(void 0);
2635
2749
  const line = stdout.split("\n").map((s) => s.trim()).find(Boolean);
2636
- resolve(line ?? "");
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((resolve) => {
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
- resolve(result);
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.2";
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 ?? join7(homedir2(), ".dahrk");
3507
+ return env.DAHRK_STATE_DIR ?? join8(homedir3(), ".dahrk");
2974
3508
  }
2975
3509
  function legacyStateDir(env) {
2976
- return env.DAHRK_STATE_DIR ? void 0 : join7(homedir2(), ".skakel");
3510
+ return env.DAHRK_STATE_DIR ? void 0 : join8(homedir3(), ".skakel");
2977
3511
  }
2978
3512
  function readNodeId(file) {
2979
- if (!existsSync4(file)) return void 0;
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 randomUUID2();
3523
+ if (opts.ephemeral) return randomUUID3();
2990
3524
  const dir = stateDir(env);
2991
- const file = join7(dir, "node.json");
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(join7(legacy, "node.json"));
3530
+ const legacyId = readNodeId(join8(legacy, "node.json"));
2997
3531
  if (legacyId) return legacyId;
2998
3532
  }
2999
- const nodeId = randomUUID2();
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(realpathSync(argv1)).href === import.meta.url;
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dahrk-node",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "private": false,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",