dahrk-node 0.1.3 → 0.1.5
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 +1103 -78
- package/package.json +2 -2
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 existsSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync5, realpathSync as realpathSync3, writeFileSync as writeFileSync7 } from "fs";
|
|
5
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
6
|
+
import { homedir as homedir4 } from "os";
|
|
7
|
+
import { basename, join as join9 } from "path";
|
|
8
8
|
import { pathToFileURL } from "url";
|
|
9
9
|
|
|
10
10
|
// ../../packages/edge/src/ws-client.ts
|
|
@@ -22,11 +22,11 @@ function createMockRunner(runtime) {
|
|
|
22
22
|
return {
|
|
23
23
|
runtime,
|
|
24
24
|
async runBatch(ctx, onTrace) {
|
|
25
|
-
const
|
|
25
|
+
const tool3 = ctx.config.tools?.[0] ?? "shell";
|
|
26
26
|
const toolUseId = "mock-tool-1";
|
|
27
27
|
const events = [
|
|
28
28
|
{ seq: 0, runtime, type: "thought", ts: nowIso(), text: "mock: planning the stage" },
|
|
29
|
-
{ seq: 1, runtime, type: "action", ts: nowIso(), tool:
|
|
29
|
+
{ seq: 1, runtime, type: "action", ts: nowIso(), tool: tool3, toolUseId, input: { note: "mock action" } },
|
|
30
30
|
{ seq: 2, runtime, type: "observation", ts: nowIso(), toolUseId, output: { ok: true } },
|
|
31
31
|
{ seq: 3, runtime, type: "response", ts: nowIso(), text: "mock: stage complete" }
|
|
32
32
|
];
|
|
@@ -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" });
|
|
@@ -371,6 +371,47 @@ function raceNextTurn(pending, idleMs, signal) {
|
|
|
371
371
|
);
|
|
372
372
|
});
|
|
373
373
|
}
|
|
374
|
+
function createElicitTurnRouter(turns, opts) {
|
|
375
|
+
const conversation = new ManagedMailbox();
|
|
376
|
+
const ref = { settle: null };
|
|
377
|
+
void (async () => {
|
|
378
|
+
try {
|
|
379
|
+
for await (const t of turns) {
|
|
380
|
+
const settle = ref.settle;
|
|
381
|
+
if (settle) settle({ kind: "reply", text: t.text });
|
|
382
|
+
else conversation.push(t);
|
|
383
|
+
}
|
|
384
|
+
} finally {
|
|
385
|
+
conversation.end();
|
|
386
|
+
const settle = ref.settle;
|
|
387
|
+
if (settle) settle({ kind: "cancel" });
|
|
388
|
+
}
|
|
389
|
+
})();
|
|
390
|
+
const ask = (firstReply, onRaise) => {
|
|
391
|
+
if (ref.settle) return Promise.resolve({ kind: "busy" });
|
|
392
|
+
onRaise();
|
|
393
|
+
return new Promise((resolve2) => {
|
|
394
|
+
let settled = false;
|
|
395
|
+
const finish = (o) => {
|
|
396
|
+
if (settled) return;
|
|
397
|
+
settled = true;
|
|
398
|
+
clearTimeout(timer);
|
|
399
|
+
opts.signal.removeEventListener("abort", onAbort);
|
|
400
|
+
ref.settle = null;
|
|
401
|
+
resolve2(o);
|
|
402
|
+
};
|
|
403
|
+
const onAbort = () => finish({ kind: "cancel" });
|
|
404
|
+
const timer = setTimeout(() => finish({ kind: "noreply" }), firstReply ? opts.firstReplyMs : opts.idleMs);
|
|
405
|
+
if (opts.signal.aborted) {
|
|
406
|
+
finish({ kind: "cancel" });
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
410
|
+
ref.settle = finish;
|
|
411
|
+
});
|
|
412
|
+
};
|
|
413
|
+
return { conversation, ask };
|
|
414
|
+
}
|
|
374
415
|
|
|
375
416
|
// ../../packages/executor-worktree/src/stage-complete-tool.ts
|
|
376
417
|
import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
|
|
@@ -401,6 +442,52 @@ function createStageCompleteTool() {
|
|
|
401
442
|
};
|
|
402
443
|
}
|
|
403
444
|
|
|
445
|
+
// ../../packages/executor-worktree/src/ask-user-question-tool.ts
|
|
446
|
+
import { createSdkMcpServer as createSdkMcpServer2, tool as tool2 } from "@anthropic-ai/claude-agent-sdk";
|
|
447
|
+
import { z as z2 } from "zod";
|
|
448
|
+
var ASK_USER_QUESTION_TOOL_NAME = "mcp__ask__ask_user_question";
|
|
449
|
+
var ASK_USER_QUESTION_ALIAS = "AskUserQuestion";
|
|
450
|
+
var optionSchema = z2.object({
|
|
451
|
+
label: z2.string(),
|
|
452
|
+
description: z2.string().optional().default(""),
|
|
453
|
+
preview: z2.string().optional()
|
|
454
|
+
});
|
|
455
|
+
var questionSchema = z2.object({
|
|
456
|
+
question: z2.string(),
|
|
457
|
+
header: z2.string().optional().default(""),
|
|
458
|
+
options: z2.array(optionSchema).min(1),
|
|
459
|
+
multiSelect: z2.boolean().optional()
|
|
460
|
+
});
|
|
461
|
+
function toElicitQuestion(q) {
|
|
462
|
+
const options = q.options.map((o) => ({ label: o.label, value: o.label }));
|
|
463
|
+
const described = q.options.filter((o) => (o.description ?? "").trim());
|
|
464
|
+
const lines = described.map((o) => `- ${o.label}: ${(o.description ?? "").trim()}`);
|
|
465
|
+
const prompt = lines.length ? `${q.question}
|
|
466
|
+
|
|
467
|
+
${lines.join("\n")}` : q.question;
|
|
468
|
+
return { prompt, options, ...q.multiSelect ? { multiSelect: true } : {} };
|
|
469
|
+
}
|
|
470
|
+
function createAskUserQuestionTool(deps) {
|
|
471
|
+
const askTool = tool2(
|
|
472
|
+
"ask_user_question",
|
|
473
|
+
"Ask the human a structured multiple-choice question and wait for their selection. Use this when you need the human to choose between options before you can continue.",
|
|
474
|
+
{ questions: z2.array(questionSchema).min(1) },
|
|
475
|
+
async (args) => {
|
|
476
|
+
const first = args.questions[0];
|
|
477
|
+
const question = toElicitQuestion(first);
|
|
478
|
+
const prompt = args.questions.length > 1 ? `${question.prompt}
|
|
479
|
+
|
|
480
|
+
(Note: ${args.questions.length} questions were asked at once; answer this one first, then ask the rest.)` : question.prompt;
|
|
481
|
+
const text = await deps.ask({ ...question, prompt });
|
|
482
|
+
return { content: [{ type: "text", text }] };
|
|
483
|
+
}
|
|
484
|
+
);
|
|
485
|
+
return {
|
|
486
|
+
server: createSdkMcpServer2({ name: "ask", version: "0.0.0", tools: [askTool] }),
|
|
487
|
+
allowedToolName: ASK_USER_QUESTION_TOOL_NAME
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
404
491
|
// ../../packages/executor-worktree/src/claude-adapter.ts
|
|
405
492
|
var COALESCE_MS = Number(process.env.DAHRK_COALESCE_MS ?? process.env.SKAKEL_COALESCE_MS ?? 40);
|
|
406
493
|
var MAX_TURNS = Number(process.env.DAHRK_MAX_TURNS ?? process.env.SKAKEL_MAX_TURNS ?? 64);
|
|
@@ -412,6 +499,17 @@ var userMsg = (text) => ({
|
|
|
412
499
|
message: { role: "user", content: text }
|
|
413
500
|
});
|
|
414
501
|
var sessionIdOf = (msg) => "session_id" in msg && typeof msg.session_id === "string" ? msg.session_id : void 0;
|
|
502
|
+
var policyCanUseTool = async (ctx, toolName, input) => {
|
|
503
|
+
const verdict = ctx.authorizeToolUse?.(toolName, input);
|
|
504
|
+
if (verdict?.verdict === "deny") {
|
|
505
|
+
return { behavior: "deny", message: verdict.reason ?? `tool "${toolName}" denied by policy ${verdict.policy}` };
|
|
506
|
+
}
|
|
507
|
+
return { behavior: "allow", updatedInput: input };
|
|
508
|
+
};
|
|
509
|
+
var interactiveCanUseTool = (summarising, stageAllowedToolName, ctx, toolName, input) => summarising && toolName !== stageAllowedToolName ? Promise.resolve({
|
|
510
|
+
behavior: "deny",
|
|
511
|
+
message: "Summarise from the work you just did; reply with the sentence only, no tools."
|
|
512
|
+
}) : policyCanUseTool(ctx, toolName, input);
|
|
415
513
|
function buildBrokeredMcpServers(ctx) {
|
|
416
514
|
const servers = ctx.config.mcpServers;
|
|
417
515
|
if (!servers || servers.length === 0 || !ctx.mcpProxyBaseUrl) return void 0;
|
|
@@ -477,7 +575,7 @@ function createClaudeRunner() {
|
|
|
477
575
|
// Tools stay allowed by canUseTool below; we do NOT set a restrictive allowedTools here.
|
|
478
576
|
...mcpServers ? { mcpServers } : {},
|
|
479
577
|
// Headless: allow tools to run without an interactive permission prompt (M6 wires policy).
|
|
480
|
-
canUseTool: async (
|
|
578
|
+
canUseTool: async (toolName, input) => policyCanUseTool(ctx, toolName, input)
|
|
481
579
|
};
|
|
482
580
|
const state = newBufferState();
|
|
483
581
|
let status = "ok";
|
|
@@ -500,6 +598,27 @@ function createClaudeRunner() {
|
|
|
500
598
|
async runInteractive(ctx, turns, onTrace) {
|
|
501
599
|
const emit = makeEmit("claude-code", onTrace);
|
|
502
600
|
const stageTool = createStageCompleteTool();
|
|
601
|
+
const { firstReplyMs, idleMs } = interactiveIdleWindows(ctx);
|
|
602
|
+
let awaitingFirstReply = true;
|
|
603
|
+
const router = createElicitTurnRouter(turns, { signal: abortController.signal, firstReplyMs, idleMs });
|
|
604
|
+
const askTool = createAskUserQuestionTool({
|
|
605
|
+
ask: async (question) => {
|
|
606
|
+
const outcome = await router.ask(awaitingFirstReply, () => {
|
|
607
|
+
emit({ type: "elicitation", prompt: question.prompt, signal: "select", options: question.options });
|
|
608
|
+
ctx.emitElicit?.(question);
|
|
609
|
+
});
|
|
610
|
+
switch (outcome.kind) {
|
|
611
|
+
case "reply":
|
|
612
|
+
return `The user selected: ${outcome.text}`;
|
|
613
|
+
case "busy":
|
|
614
|
+
return "Only one question can be asked at a time; wait for the current one to be answered, then ask again.";
|
|
615
|
+
case "noreply":
|
|
616
|
+
return "No response from the user; proceed with your best judgement.";
|
|
617
|
+
case "cancel":
|
|
618
|
+
return "The question was cancelled.";
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
});
|
|
503
622
|
const brokered = buildBrokeredMcpServers(ctx);
|
|
504
623
|
let summarising = false;
|
|
505
624
|
const options = {
|
|
@@ -507,12 +626,17 @@ function createClaudeRunner() {
|
|
|
507
626
|
// Keep the cwd-anchoring preset; fold the stage instruction in via `append` so the
|
|
508
627
|
// interactive persona still gets it without losing the working-directory context.
|
|
509
628
|
systemPrompt: hasSystemPrompt(ctx) ? { type: "preset", preset: "claude_code", append: resolveStagePrompt(ctx) } : CLAUDE_CODE_SYSTEM_PROMPT,
|
|
510
|
-
// Inject the stage-complete exit tool
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
//
|
|
514
|
-
|
|
515
|
-
|
|
629
|
+
// Inject the stage-complete exit tool and the AskUserQuestion shadow alongside any brokered
|
|
630
|
+
// MCP servers (parity with batch).
|
|
631
|
+
mcpServers: { dahrk: stageTool.server, ask: askTool.server, ...brokered ?? {} },
|
|
632
|
+
// Redirect the built-in AskUserQuestion to the shadow tool so a structured question surfaces
|
|
633
|
+
// as a Linear elicitation. The redirect is name-only and single-hop; the tool still runs, so
|
|
634
|
+
// this is mapping, not gating (DHK-344 / DHK-223).
|
|
635
|
+
toolAliases: { [ASK_USER_QUESTION_ALIAS]: askTool.allowedToolName },
|
|
636
|
+
// Auto-approve the injected tools; `allowedTools` is an auto-approve list, not a whitelist, so
|
|
637
|
+
// it does not restrict the other tools canUseTool allows.
|
|
638
|
+
allowedTools: [stageTool.allowedToolName, askTool.allowedToolName],
|
|
639
|
+
canUseTool: async (toolName, input) => interactiveCanUseTool(summarising, stageTool.allowedToolName, ctx, toolName, input),
|
|
516
640
|
maxTurns: MAX_TURNS,
|
|
517
641
|
includePartialMessages: false
|
|
518
642
|
};
|
|
@@ -522,7 +646,7 @@ function createClaudeRunner() {
|
|
|
522
646
|
const q = query({ prompt: mailbox, options });
|
|
523
647
|
active = q;
|
|
524
648
|
const it = q[Symbol.asyncIterator]();
|
|
525
|
-
const humanIter =
|
|
649
|
+
const humanIter = router.conversation[Symbol.asyncIterator]();
|
|
526
650
|
const state = newBufferState();
|
|
527
651
|
const consumeTurn = async () => {
|
|
528
652
|
for (; ; ) {
|
|
@@ -532,8 +656,6 @@ function createClaudeRunner() {
|
|
|
532
656
|
if (res.isResult) return res.responseText;
|
|
533
657
|
}
|
|
534
658
|
};
|
|
535
|
-
const { firstReplyMs, idleMs } = interactiveIdleWindows(ctx);
|
|
536
|
-
let awaitingFirstReply = true;
|
|
537
659
|
let exited = null;
|
|
538
660
|
let pending = humanIter.next();
|
|
539
661
|
try {
|
|
@@ -1214,6 +1336,7 @@ import { dirname, isAbsolute, join as join2 } from "path";
|
|
|
1214
1336
|
var noopLogger = { info: () => {
|
|
1215
1337
|
}, warn: () => {
|
|
1216
1338
|
} };
|
|
1339
|
+
var SCRATCH_DIR = ".skakel/scratch";
|
|
1217
1340
|
function parseOwnerRepo(gitUrl) {
|
|
1218
1341
|
const m = /[:/]([^/:]+)\/([^/]+?)(?:\.git)?$/.exec(gitUrl.trim());
|
|
1219
1342
|
return m ? `${m[1]}/${m[2]}` : void 0;
|
|
@@ -1222,8 +1345,11 @@ function sanitizeBranchName(name) {
|
|
|
1222
1345
|
if (!name) return name;
|
|
1223
1346
|
return name.replace(/[`~^:?*[\]\\@{}\s]/g, "-").replace(/\.{2,}/g, ".").replace(/\/{2,}/g, "/").replace(/\.lock(\/|$)/g, "$1").replace(/^[.\-/]+/, "").replace(/[.\-/]+$/, "").replace(/-{2,}/g, "-");
|
|
1224
1347
|
}
|
|
1348
|
+
function resolveWorktreesDir(override) {
|
|
1349
|
+
return override ?? process.env.DAHRK_WORKTREES_DIR ?? process.env.SKAKEL_WORKTREES_DIR ?? join2(homedir(), ".dahrk", "worktrees");
|
|
1350
|
+
}
|
|
1225
1351
|
function createGitService(opts = {}) {
|
|
1226
|
-
const worktreesDir = opts.worktreesDir
|
|
1352
|
+
const worktreesDir = resolveWorktreesDir(opts.worktreesDir);
|
|
1227
1353
|
const mirrorsDir = opts.mirrorsDir ?? process.env.DAHRK_MIRRORS_DIR ?? process.env.SKAKEL_MIRRORS_DIR ?? join2(homedir(), ".dahrk", "mirrors");
|
|
1228
1354
|
const authorName = opts.authorName ?? process.env.DAHRK_GIT_AUTHOR_NAME ?? "Dahrk";
|
|
1229
1355
|
const authorEmail = opts.authorEmail ?? process.env.DAHRK_GIT_AUTHOR_EMAIL ?? "noreply@dahrk.ai";
|
|
@@ -1255,24 +1381,23 @@ function createGitService(opts = {}) {
|
|
|
1255
1381
|
return (stderr?.trim() || err?.message || String(e)).split("\n")[0] ?? String(e);
|
|
1256
1382
|
};
|
|
1257
1383
|
const excludeScratchLocally = (worktreePath) => {
|
|
1258
|
-
const entry =
|
|
1384
|
+
const entry = `${SCRATCH_DIR}/`;
|
|
1259
1385
|
try {
|
|
1260
1386
|
const rel = git(worktreePath, ["rev-parse", "--git-path", "info/exclude"]).trim();
|
|
1261
1387
|
const excludePath = isAbsolute(rel) ? rel : join2(worktreePath, rel);
|
|
1262
1388
|
const existing = existsSync(excludePath) ? readFileSync2(excludePath, "utf-8") : "";
|
|
1263
1389
|
if (existing.split("\n").some((l) => l.trim() === entry)) return;
|
|
1264
1390
|
mkdirSync(dirname(excludePath), { recursive: true });
|
|
1265
|
-
const
|
|
1266
|
-
writeFileSync(excludePath, `${existing}${
|
|
1391
|
+
const sep3 = existing && !existing.endsWith("\n") ? "\n" : "";
|
|
1392
|
+
writeFileSync(excludePath, `${existing}${sep3}${entry}
|
|
1267
1393
|
`);
|
|
1268
1394
|
} catch (e) {
|
|
1269
1395
|
log2.warn(`could not set worktree scratch exclude at ${worktreePath}: ${e.message}`);
|
|
1270
1396
|
}
|
|
1271
1397
|
};
|
|
1272
1398
|
const commitPending = (worktreePath, message) => {
|
|
1273
|
-
const SCRATCH = ".skakel/scratch";
|
|
1274
1399
|
excludeScratchLocally(worktreePath);
|
|
1275
|
-
git(worktreePath, ["rm", "-r", "--cached", "--ignore-unmatch", "--quiet",
|
|
1400
|
+
git(worktreePath, ["rm", "-r", "--cached", "--ignore-unmatch", "--quiet", SCRATCH_DIR]);
|
|
1276
1401
|
git(worktreePath, ["add", "-A", "--", "."]);
|
|
1277
1402
|
const dirty = !gitOk(worktreePath, ["diff", "--cached", "--quiet"]);
|
|
1278
1403
|
if (dirty) {
|
|
@@ -1327,6 +1452,7 @@ function createGitService(opts = {}) {
|
|
|
1327
1452
|
scratchPath: join2(worktreePath, ".skakel", "scratch")
|
|
1328
1453
|
});
|
|
1329
1454
|
return {
|
|
1455
|
+
worktreesDir,
|
|
1330
1456
|
async createWorktree(spec) {
|
|
1331
1457
|
const { repoId, gitUrl, baseBranch, runId } = spec;
|
|
1332
1458
|
const branchName = sanitizeBranchName(spec.branch ?? `dahrk/${runId}`);
|
|
@@ -1394,6 +1520,11 @@ function createGitService(opts = {}) {
|
|
|
1394
1520
|
if (!gitOk(worktreePath, ["merge-base", "HEAD", "FETCH_HEAD"])) {
|
|
1395
1521
|
return { headSha, pushed: false, nothingToCommit: !dirty, commitsAhead, integration: "diverged" };
|
|
1396
1522
|
}
|
|
1523
|
+
const delta = git(worktreePath, ["diff", "--name-only", "FETCH_HEAD...HEAD"]).split("\n").map((l) => l.trim()).filter(Boolean);
|
|
1524
|
+
const isScratchPath = (p) => p === SCRATCH_DIR || p.startsWith(`${SCRATCH_DIR}/`) || gitOk(worktreePath, ["check-ignore", "-q", "--no-index", "--", p]);
|
|
1525
|
+
if (!delta.some((p) => !isScratchPath(p))) {
|
|
1526
|
+
return { headSha, pushed: false, nothingToCommit: true, commitsAhead, integration: "noop" };
|
|
1527
|
+
}
|
|
1397
1528
|
try {
|
|
1398
1529
|
git(worktreePath, [
|
|
1399
1530
|
"-c",
|
|
@@ -1650,12 +1781,12 @@ function evaluatePolicies(event, rules) {
|
|
|
1650
1781
|
}
|
|
1651
1782
|
return ALLOW;
|
|
1652
1783
|
}
|
|
1653
|
-
function denyToolRule(
|
|
1784
|
+
function denyToolRule(tool3) {
|
|
1654
1785
|
return {
|
|
1655
1786
|
name: "deny_tool",
|
|
1656
1787
|
evaluate(event) {
|
|
1657
|
-
if (event.kind === "action" && event.tool ===
|
|
1658
|
-
return { verdict: "deny", policy: "deny_tool", reason: `tool "${
|
|
1788
|
+
if (event.kind === "action" && event.tool === tool3) {
|
|
1789
|
+
return { verdict: "deny", policy: "deny_tool", reason: `tool "${tool3}" is denied` };
|
|
1659
1790
|
}
|
|
1660
1791
|
return null;
|
|
1661
1792
|
}
|
|
@@ -1667,7 +1798,7 @@ import { execFileSync as execFileSync3 } from "child_process";
|
|
|
1667
1798
|
import { createHash as createHash3 } from "crypto";
|
|
1668
1799
|
import { mkdirSync as mkdirSync5, readdirSync as readdirSync2, readFileSync as readFileSync4, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
1669
1800
|
import { tmpdir as tmpdir2 } from "os";
|
|
1670
|
-
import { join as join6 } from "path";
|
|
1801
|
+
import { isAbsolute as isAbsolute2, join as join6, relative as relative2, resolve, sep as sep2 } from "path";
|
|
1671
1802
|
import { attachedDocBasename as attachedDocBasename2 } from "@dahrk/contracts";
|
|
1672
1803
|
|
|
1673
1804
|
// ../../packages/edge/src/builtins.ts
|
|
@@ -1736,7 +1867,15 @@ function buildRules(policies, ctx) {
|
|
|
1736
1867
|
const rules = [];
|
|
1737
1868
|
let stageToolCalls = 0;
|
|
1738
1869
|
for (const p of policies) {
|
|
1739
|
-
if ("
|
|
1870
|
+
if ("read_only" in p && p.read_only) {
|
|
1871
|
+
rules.push({
|
|
1872
|
+
name: "read_only",
|
|
1873
|
+
evaluate(event) {
|
|
1874
|
+
if (event.kind !== "action" || !WRITE_TOOLS.has(event.tool)) return null;
|
|
1875
|
+
return { verdict: "deny", policy: "read_only", reason: `read-only stage: "${event.tool}" is not permitted` };
|
|
1876
|
+
}
|
|
1877
|
+
});
|
|
1878
|
+
} else if ("write_scope" in p) {
|
|
1740
1879
|
const { branches, repos } = p.write_scope;
|
|
1741
1880
|
rules.push({
|
|
1742
1881
|
name: "write_scope",
|
|
@@ -1841,11 +1980,11 @@ async function startMcpGateway(opts) {
|
|
|
1841
1980
|
if (upstream.body) await pipeline(Readable.fromWeb(upstream.body), res);
|
|
1842
1981
|
else res.end();
|
|
1843
1982
|
};
|
|
1844
|
-
await new Promise((
|
|
1983
|
+
await new Promise((resolve2) => server.listen(0, "127.0.0.1", resolve2));
|
|
1845
1984
|
const { port } = server.address();
|
|
1846
1985
|
return {
|
|
1847
1986
|
baseUrl: `http://127.0.0.1:${port}`,
|
|
1848
|
-
stop: () => new Promise((
|
|
1987
|
+
stop: () => new Promise((resolve2) => server.close(() => resolve2()))
|
|
1849
1988
|
};
|
|
1850
1989
|
}
|
|
1851
1990
|
|
|
@@ -1933,9 +2072,24 @@ var SCRATCH_OUTPUT_DIR = ".skakel/scratch/output";
|
|
|
1933
2072
|
function capContent(raw) {
|
|
1934
2073
|
return raw.length > ARTIFACT_CAP_BYTES ? raw.slice(0, ARTIFACT_CAP_BYTES) : raw;
|
|
1935
2074
|
}
|
|
2075
|
+
function resolveWorktreeRelativePath(ref, relPath) {
|
|
2076
|
+
if (isAbsolute2(relPath)) return void 0;
|
|
2077
|
+
const root = resolve(ref.worktreePath);
|
|
2078
|
+
const target = resolve(root, relPath);
|
|
2079
|
+
const fromRoot = relative2(root, target);
|
|
2080
|
+
if (fromRoot === "" || fromRoot === ".." || fromRoot.startsWith(`..${sep2}`) || isAbsolute2(fromRoot)) {
|
|
2081
|
+
return void 0;
|
|
2082
|
+
}
|
|
2083
|
+
return target;
|
|
2084
|
+
}
|
|
2085
|
+
function isSafeWorktreeRelativePath(ref, relPath) {
|
|
2086
|
+
return resolveWorktreeRelativePath(ref, relPath) !== void 0;
|
|
2087
|
+
}
|
|
1936
2088
|
function readEmittedArtifact(ref, relPath) {
|
|
2089
|
+
const path = resolveWorktreeRelativePath(ref, relPath);
|
|
2090
|
+
if (!path) return void 0;
|
|
1937
2091
|
try {
|
|
1938
|
-
const raw = readFileSync4(
|
|
2092
|
+
const raw = readFileSync4(path, "utf8");
|
|
1939
2093
|
return { path: relPath, content: capContent(raw) };
|
|
1940
2094
|
} catch {
|
|
1941
2095
|
return void 0;
|
|
@@ -1981,7 +2135,7 @@ function resolveStageArtifact(ref, emitArtifact, handedBack) {
|
|
|
1981
2135
|
const declared = readEmittedArtifact(ref, emitArtifact);
|
|
1982
2136
|
if (declared && declared.content.trim().length > 0) return { artifact: declared, source: "declared-file" };
|
|
1983
2137
|
}
|
|
1984
|
-
if (handedBack && handedBack.content.trim().length > 0) {
|
|
2138
|
+
if (handedBack && handedBack.content.trim().length > 0 && isSafeWorktreeRelativePath(ref, handedBack.path)) {
|
|
1985
2139
|
return { artifact: { path: handedBack.path, content: capContent(handedBack.content) }, source: "tool-handoff" };
|
|
1986
2140
|
}
|
|
1987
2141
|
const scratch = scanScratchOutput(ref, emitArtifact);
|
|
@@ -2081,8 +2235,9 @@ function createStageRunner(deps) {
|
|
|
2081
2235
|
writeGuidance(ref, job.guidance);
|
|
2082
2236
|
writeAttachedDocuments(ref, job.attachedDocuments);
|
|
2083
2237
|
if (job.agentConfig.emitArtifact) {
|
|
2238
|
+
const artifactDir = resolveWorktreeRelativePath(ref, job.agentConfig.emitArtifact);
|
|
2084
2239
|
const slash = job.agentConfig.emitArtifact.lastIndexOf("/");
|
|
2085
|
-
if (slash > 0) {
|
|
2240
|
+
if (artifactDir && slash > 0) {
|
|
2086
2241
|
try {
|
|
2087
2242
|
mkdirSync5(join6(ref.worktreePath, job.agentConfig.emitArtifact.slice(0, slash)), { recursive: true });
|
|
2088
2243
|
} catch {
|
|
@@ -2219,20 +2374,50 @@ function createStageRunner(deps) {
|
|
|
2219
2374
|
return finish("fail", `${stageId}: denied at stage entry (${entry.policy})`, job.sessionId);
|
|
2220
2375
|
}
|
|
2221
2376
|
let denied = false;
|
|
2377
|
+
const authorisedActions = [];
|
|
2222
2378
|
const runtime = agentConfig.runtime;
|
|
2379
|
+
const actionKey = (tool3, input) => {
|
|
2380
|
+
try {
|
|
2381
|
+
return `${tool3}\0${JSON.stringify(input)}`;
|
|
2382
|
+
} catch {
|
|
2383
|
+
return `${tool3}\0`;
|
|
2384
|
+
}
|
|
2385
|
+
};
|
|
2386
|
+
const policyReason = (verdict) => verdict.reason ?? `tool action denied by ${verdict.policy}`;
|
|
2387
|
+
const recordDeny = (verdict, toolUseId) => {
|
|
2388
|
+
denied = true;
|
|
2389
|
+
const reason = policyReason(verdict);
|
|
2390
|
+
if (toolUseId) {
|
|
2391
|
+
streamEvent(writer.append({ seq: 0, ts: nowIso2(), type: "observation", runtime, toolUseId, isError: true, output: { error: reason } }));
|
|
2392
|
+
}
|
|
2393
|
+
streamEvent(writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime, event: "policy-deny", detail: reason }));
|
|
2394
|
+
deps.sendProgress({ jobId, kind: "error", ts: nowIso2(), text: reason });
|
|
2395
|
+
};
|
|
2396
|
+
const authorizeToolUse = (tool3, input) => {
|
|
2397
|
+
const verdict = evaluatePolicies({ kind: "action", stageId, tool: tool3, input }, rules);
|
|
2398
|
+
if (verdict.verdict === "deny") {
|
|
2399
|
+
recordDeny(verdict);
|
|
2400
|
+
} else {
|
|
2401
|
+
authorisedActions.push(actionKey(tool3, input));
|
|
2402
|
+
}
|
|
2403
|
+
return verdict;
|
|
2404
|
+
};
|
|
2223
2405
|
const onTrace = (event) => {
|
|
2224
2406
|
if (event.type === "action") {
|
|
2225
|
-
const
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2407
|
+
const key = actionKey(event.tool, event.input);
|
|
2408
|
+
const authorised = authorisedActions.indexOf(key);
|
|
2409
|
+
if (authorised >= 0) {
|
|
2410
|
+
authorisedActions.splice(authorised, 1);
|
|
2411
|
+
} else {
|
|
2412
|
+
const verdict = evaluatePolicies(
|
|
2413
|
+
{ kind: "action", stageId, tool: event.tool, input: event.input },
|
|
2414
|
+
rules
|
|
2415
|
+
);
|
|
2416
|
+
if (verdict.verdict === "deny") {
|
|
2417
|
+
streamEvent(writer.append(event));
|
|
2418
|
+
recordDeny(verdict, event.toolUseId);
|
|
2419
|
+
return;
|
|
2420
|
+
}
|
|
2236
2421
|
}
|
|
2237
2422
|
}
|
|
2238
2423
|
streamEvent(writer.append(event));
|
|
@@ -2260,7 +2445,18 @@ function createStageRunner(deps) {
|
|
|
2260
2445
|
...job.runtimeEnv ? { runtimeEnv: job.runtimeEnv } : {},
|
|
2261
2446
|
// The adapter persists each runtime-native record under the attempt's raw/ sidecar
|
|
2262
2447
|
// and stamps the rawRef onto the emitted event.
|
|
2263
|
-
writeRaw: writer.writeRaw
|
|
2448
|
+
writeRaw: writer.writeRaw,
|
|
2449
|
+
authorizeToolUse,
|
|
2450
|
+
// Wire the interactive AskUserQuestion elicitation seam (DHK-344): the adapter calls this when
|
|
2451
|
+
// the agent asks a structured question, and the edge relays it to the hub as an `elicit` frame.
|
|
2452
|
+
...deps.sendElicit ? {
|
|
2453
|
+
emitElicit: (question) => deps.sendElicit({
|
|
2454
|
+
jobId,
|
|
2455
|
+
prompt: question.prompt,
|
|
2456
|
+
options: question.options,
|
|
2457
|
+
...question.multiSelect ? { multiSelect: true } : {}
|
|
2458
|
+
})
|
|
2459
|
+
} : {}
|
|
2264
2460
|
};
|
|
2265
2461
|
const interactive = agentConfig.interaction === "interactive";
|
|
2266
2462
|
let result;
|
|
@@ -2364,6 +2560,18 @@ function createStageRunner(deps) {
|
|
|
2364
2560
|
base: job.base,
|
|
2365
2561
|
...job.workspaceRef.credentialToken ? { credentialToken: job.workspaceRef.credentialToken } : {}
|
|
2366
2562
|
});
|
|
2563
|
+
if (r.integration === "noop") {
|
|
2564
|
+
return {
|
|
2565
|
+
jobId,
|
|
2566
|
+
status: "ok",
|
|
2567
|
+
branch: job.branch,
|
|
2568
|
+
headSha: r.headSha,
|
|
2569
|
+
pushed: false,
|
|
2570
|
+
nothingToCommit: true,
|
|
2571
|
+
commitsAhead: r.commitsAhead,
|
|
2572
|
+
summary: `no changes to deliver on ${job.branch} - work already present on ${job.base}`
|
|
2573
|
+
};
|
|
2574
|
+
}
|
|
2367
2575
|
if (r.integration === "conflict") {
|
|
2368
2576
|
return {
|
|
2369
2577
|
jobId,
|
|
@@ -2442,11 +2650,27 @@ async function startEdgeNode(opts) {
|
|
|
2442
2650
|
});
|
|
2443
2651
|
let ws;
|
|
2444
2652
|
let heartbeat;
|
|
2653
|
+
let reconnectTimer;
|
|
2654
|
+
let lastPongAt = 0;
|
|
2445
2655
|
let shuttingDown = false;
|
|
2446
2656
|
let onFatal;
|
|
2447
2657
|
const send = (msg) => {
|
|
2448
2658
|
if (ws && ws.readyState === WebSocket.OPEN) ws.send(encode(msg));
|
|
2449
2659
|
};
|
|
2660
|
+
const MISSED_PONGS_BEFORE_DEAD = 3;
|
|
2661
|
+
const startHeartbeat = (sock, intervalMs) => {
|
|
2662
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
2663
|
+
lastPongAt = Date.now();
|
|
2664
|
+
heartbeat = setInterval(() => {
|
|
2665
|
+
if (Date.now() - lastPongAt > MISSED_PONGS_BEFORE_DEAD * intervalMs) {
|
|
2666
|
+
log(`EDGE_STALE:${Date.now() - lastPongAt}ms without a pong, terminating socket`);
|
|
2667
|
+
sock.terminate();
|
|
2668
|
+
return;
|
|
2669
|
+
}
|
|
2670
|
+
send({ type: "heartbeat" });
|
|
2671
|
+
if (sock.readyState === WebSocket.OPEN) sock.ping();
|
|
2672
|
+
}, intervalMs);
|
|
2673
|
+
};
|
|
2450
2674
|
const MAX_RESEND = 100;
|
|
2451
2675
|
const lastResults = /* @__PURE__ */ new Map();
|
|
2452
2676
|
const rememberResult = (jobId, frame) => {
|
|
@@ -2462,12 +2686,12 @@ async function startEdgeNode(opts) {
|
|
|
2462
2686
|
const trace = {
|
|
2463
2687
|
event: (frame) => send({ type: "trace-event", ...frame }),
|
|
2464
2688
|
finalised: (frame) => send({ type: "trace-finalised", ...frame }),
|
|
2465
|
-
requestBlobUrl: (req) => new Promise((
|
|
2689
|
+
requestBlobUrl: (req) => new Promise((resolve2) => {
|
|
2466
2690
|
const reqId = `${req.runId}:${req.stageId}:${req.attempt}:${blobReqCounter++}`;
|
|
2467
|
-
pendingBlob.set(reqId,
|
|
2691
|
+
pendingBlob.set(reqId, resolve2);
|
|
2468
2692
|
send({ type: "blob-put-request", reqId, ...req });
|
|
2469
2693
|
setTimeout(() => {
|
|
2470
|
-
if (pendingBlob.delete(reqId))
|
|
2694
|
+
if (pendingBlob.delete(reqId)) resolve2({ key: "" });
|
|
2471
2695
|
}, 3e4).unref?.();
|
|
2472
2696
|
})
|
|
2473
2697
|
};
|
|
@@ -2478,6 +2702,9 @@ async function startEdgeNode(opts) {
|
|
|
2478
2702
|
...opts.tenantId ? { tenantId: opts.tenantId } : {},
|
|
2479
2703
|
rules,
|
|
2480
2704
|
sendProgress: (progress) => send({ type: "progress", progress }),
|
|
2705
|
+
// DHK-344: relay a mid-interactive-stage AskUserQuestion to the hub as an `elicit` frame; the hub
|
|
2706
|
+
// raises the Linear elicitation and the human's pick returns on the existing `turn` frame.
|
|
2707
|
+
sendElicit: (frame) => send({ type: "elicit", ...frame }),
|
|
2481
2708
|
trace,
|
|
2482
2709
|
...opts.retention ? { retention: opts.retention } : {}
|
|
2483
2710
|
};
|
|
@@ -2489,18 +2716,17 @@ async function startEdgeNode(opts) {
|
|
|
2489
2716
|
if (msg.type === "welcome") {
|
|
2490
2717
|
stageDeps.tenantId = msg.tenantId;
|
|
2491
2718
|
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);
|
|
2719
|
+
if (opts.heartbeatMs === void 0 && msg.heartbeatMs > 0 && ws) {
|
|
2720
|
+
startHeartbeat(ws, msg.heartbeatMs);
|
|
2495
2721
|
}
|
|
2496
2722
|
log(`EDGE_WELCOMED:${msg.name} tenant=${msg.tenantId} credentialMode=${msg.credentialMode}`);
|
|
2497
2723
|
return;
|
|
2498
2724
|
}
|
|
2499
2725
|
if (msg.type === "blob-put-url") {
|
|
2500
|
-
const
|
|
2501
|
-
if (
|
|
2726
|
+
const resolve2 = pendingBlob.get(msg.reqId);
|
|
2727
|
+
if (resolve2) {
|
|
2502
2728
|
pendingBlob.delete(msg.reqId);
|
|
2503
|
-
|
|
2729
|
+
resolve2({ key: msg.key, ...msg.url ? { url: msg.url } : {} });
|
|
2504
2730
|
}
|
|
2505
2731
|
return;
|
|
2506
2732
|
}
|
|
@@ -2522,6 +2748,12 @@ async function startEdgeNode(opts) {
|
|
|
2522
2748
|
log(`PUSH_DUPLICATE:${job2.runId} ${job2.jobId}`);
|
|
2523
2749
|
return;
|
|
2524
2750
|
}
|
|
2751
|
+
const cachedPush = lastResults.get(job2.jobId);
|
|
2752
|
+
if (cachedPush) {
|
|
2753
|
+
send(cachedPush);
|
|
2754
|
+
log(`PUSH_REPLAY:${job2.runId} ${job2.jobId}`);
|
|
2755
|
+
return;
|
|
2756
|
+
}
|
|
2525
2757
|
running.add(job2.jobId);
|
|
2526
2758
|
log(`PUSH_STARTED:${job2.runId} ${job2.branch}`);
|
|
2527
2759
|
try {
|
|
@@ -2550,6 +2782,12 @@ async function startEdgeNode(opts) {
|
|
|
2550
2782
|
log(`JOB_DUPLICATE:${job.stageId} ${job.jobId}`);
|
|
2551
2783
|
return;
|
|
2552
2784
|
}
|
|
2785
|
+
const cachedJob = lastResults.get(job.jobId);
|
|
2786
|
+
if (cachedJob) {
|
|
2787
|
+
send(cachedJob);
|
|
2788
|
+
log(`JOB_REPLAY:${job.stageId} ${job.jobId}`);
|
|
2789
|
+
return;
|
|
2790
|
+
}
|
|
2553
2791
|
running.add(job.jobId);
|
|
2554
2792
|
log(`JOB_STARTED:${job.stageId} ${job.jobId}`);
|
|
2555
2793
|
try {
|
|
@@ -2586,15 +2824,23 @@ async function startEdgeNode(opts) {
|
|
|
2586
2824
|
...opts.name ? { name: opts.name } : {},
|
|
2587
2825
|
os: osPlatform(),
|
|
2588
2826
|
arch: osArch(),
|
|
2589
|
-
clientVersion: opts.clientVersion ?? "0.0.0"
|
|
2827
|
+
clientVersion: opts.clientVersion ?? "0.0.0",
|
|
2828
|
+
// Advertise the resolved worktree base so the hub records each run's real worktree location in
|
|
2829
|
+
// the projection instead of an advisory placeholder. Single-sourced from the git service so it
|
|
2830
|
+
// always matches where worktrees actually land.
|
|
2831
|
+
worktreesDir: gitService.worktreesDir
|
|
2590
2832
|
});
|
|
2591
2833
|
for (const frame of lastResults.values()) send(frame);
|
|
2592
|
-
|
|
2834
|
+
startHeartbeat(sock, opts.heartbeatMs ?? 5e3);
|
|
2593
2835
|
});
|
|
2594
2836
|
sock.on("message", (raw) => void onMessage(raw.toString()));
|
|
2837
|
+
sock.on("pong", () => {
|
|
2838
|
+
lastPongAt = Date.now();
|
|
2839
|
+
});
|
|
2595
2840
|
sock.on("error", (e) => log(`EDGE_ERROR ${e.message}`));
|
|
2596
2841
|
sock.on("close", (code, reason) => {
|
|
2597
2842
|
if (heartbeat) clearInterval(heartbeat);
|
|
2843
|
+
heartbeat = void 0;
|
|
2598
2844
|
if (isEnrolmentRejection(code)) {
|
|
2599
2845
|
shuttingDown = true;
|
|
2600
2846
|
const detail = reason.toString() || "enrolment rejected";
|
|
@@ -2603,15 +2849,27 @@ async function startEdgeNode(opts) {
|
|
|
2603
2849
|
onFatal?.(new Error(`hub rejected edge enrolment (${code}): ${detail}`));
|
|
2604
2850
|
return;
|
|
2605
2851
|
}
|
|
2606
|
-
if (!shuttingDown) setTimeout(connect, 500);
|
|
2852
|
+
if (!shuttingDown) reconnectTimer = setTimeout(connect, 500);
|
|
2607
2853
|
});
|
|
2608
2854
|
};
|
|
2855
|
+
opts.signal?.addEventListener(
|
|
2856
|
+
"abort",
|
|
2857
|
+
() => {
|
|
2858
|
+
shuttingDown = true;
|
|
2859
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
2860
|
+
heartbeat = void 0;
|
|
2861
|
+
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
2862
|
+
reconnectTimer = void 0;
|
|
2863
|
+
ws?.close(1e3, "shutting down");
|
|
2864
|
+
},
|
|
2865
|
+
{ once: true }
|
|
2866
|
+
);
|
|
2609
2867
|
connect();
|
|
2610
|
-
await new Promise((
|
|
2868
|
+
await new Promise((resolve2, reject) => {
|
|
2611
2869
|
const t = setInterval(() => {
|
|
2612
2870
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
2613
2871
|
clearInterval(t);
|
|
2614
|
-
|
|
2872
|
+
resolve2();
|
|
2615
2873
|
}
|
|
2616
2874
|
}, 50);
|
|
2617
2875
|
onFatal = (err) => {
|
|
@@ -2629,11 +2887,11 @@ var PROBES = [
|
|
|
2629
2887
|
{ runtime: "pi", cmd: "pi" }
|
|
2630
2888
|
];
|
|
2631
2889
|
function probe(cmd, timeoutMs) {
|
|
2632
|
-
return new Promise((
|
|
2890
|
+
return new Promise((resolve2) => {
|
|
2633
2891
|
execFile(cmd, ["--version"], { timeout: timeoutMs }, (err, stdout) => {
|
|
2634
|
-
if (err) return
|
|
2892
|
+
if (err) return resolve2(void 0);
|
|
2635
2893
|
const line = stdout.split("\n").map((s) => s.trim()).find(Boolean);
|
|
2636
|
-
|
|
2894
|
+
resolve2(line ?? "");
|
|
2637
2895
|
});
|
|
2638
2896
|
});
|
|
2639
2897
|
}
|
|
@@ -2676,7 +2934,7 @@ function probeHub(opts) {
|
|
|
2676
2934
|
clientVersion = "0.0.0",
|
|
2677
2935
|
timeoutMs = 8e3
|
|
2678
2936
|
} = opts;
|
|
2679
|
-
return new Promise((
|
|
2937
|
+
return new Promise((resolve2) => {
|
|
2680
2938
|
let settled = false;
|
|
2681
2939
|
let opened = false;
|
|
2682
2940
|
let ws;
|
|
@@ -2688,7 +2946,7 @@ function probeHub(opts) {
|
|
|
2688
2946
|
ws.terminate();
|
|
2689
2947
|
} catch {
|
|
2690
2948
|
}
|
|
2691
|
-
|
|
2949
|
+
resolve2(result);
|
|
2692
2950
|
};
|
|
2693
2951
|
const timer = setTimeout(
|
|
2694
2952
|
() => done({ ok: false, reason: "timeout", detail: `no welcome within ${timeoutMs}ms` }),
|
|
@@ -2751,7 +3009,7 @@ function probeHub(opts) {
|
|
|
2751
3009
|
|
|
2752
3010
|
// src/cli.ts
|
|
2753
3011
|
import { parseArgs } from "util";
|
|
2754
|
-
var COMMANDS = /* @__PURE__ */ new Set(["start", "doctor"]);
|
|
3012
|
+
var COMMANDS = /* @__PURE__ */ new Set(["start", "run", "service", "doctor", "update"]);
|
|
2755
3013
|
var isCommand = (s) => COMMANDS.has(s);
|
|
2756
3014
|
function parseCli(argv) {
|
|
2757
3015
|
const [first, ...rest] = argv;
|
|
@@ -2769,6 +3027,9 @@ function parseCli(argv) {
|
|
|
2769
3027
|
command = first;
|
|
2770
3028
|
flagArgs = rest;
|
|
2771
3029
|
}
|
|
3030
|
+
if (command === "run") return parseRun(flagArgs);
|
|
3031
|
+
if (command === "service") return parseService(flagArgs);
|
|
3032
|
+
if (command === "update") return parseUpdate(flagArgs);
|
|
2772
3033
|
let values;
|
|
2773
3034
|
try {
|
|
2774
3035
|
({ values } = parseArgs({
|
|
@@ -2794,6 +3055,93 @@ function parseCli(argv) {
|
|
|
2794
3055
|
};
|
|
2795
3056
|
return { kind: command, flags };
|
|
2796
3057
|
}
|
|
3058
|
+
function parseRun(flagArgs) {
|
|
3059
|
+
let values;
|
|
3060
|
+
let positionals;
|
|
3061
|
+
try {
|
|
3062
|
+
({ values, positionals } = parseArgs({
|
|
3063
|
+
args: flagArgs,
|
|
3064
|
+
options: {
|
|
3065
|
+
repo: { type: "string" },
|
|
3066
|
+
token: { type: "string" },
|
|
3067
|
+
"hub-url": { type: "string" },
|
|
3068
|
+
help: { type: "boolean", default: false }
|
|
3069
|
+
},
|
|
3070
|
+
allowPositionals: true
|
|
3071
|
+
}));
|
|
3072
|
+
} catch (e) {
|
|
3073
|
+
return { kind: "error", message: e.message };
|
|
3074
|
+
}
|
|
3075
|
+
if (values.help) return { kind: "help", command: "run" };
|
|
3076
|
+
if (positionals.length === 0) {
|
|
3077
|
+
return { kind: "error", message: "run: missing workflow (e.g. `dahrk run preflight`)" };
|
|
3078
|
+
}
|
|
3079
|
+
if (positionals.length > 1) {
|
|
3080
|
+
return { kind: "error", message: `run: unexpected argument "${positionals[1]}" (one workflow at a time)` };
|
|
3081
|
+
}
|
|
3082
|
+
const flags = {
|
|
3083
|
+
workflow: positionals[0],
|
|
3084
|
+
...values.repo ? { repo: values.repo } : {},
|
|
3085
|
+
...values.token ? { token: values.token } : {},
|
|
3086
|
+
...values["hub-url"] ? { hubUrl: values["hub-url"] } : {}
|
|
3087
|
+
};
|
|
3088
|
+
return { kind: "run", flags };
|
|
3089
|
+
}
|
|
3090
|
+
var SERVICE_ACTIONS = /* @__PURE__ */ new Set(["install", "uninstall"]);
|
|
3091
|
+
var isServiceAction = (s) => SERVICE_ACTIONS.has(s);
|
|
3092
|
+
function parseService(flagArgs) {
|
|
3093
|
+
let values;
|
|
3094
|
+
let positionals;
|
|
3095
|
+
try {
|
|
3096
|
+
({ values, positionals } = parseArgs({
|
|
3097
|
+
args: flagArgs,
|
|
3098
|
+
options: {
|
|
3099
|
+
token: { type: "string" },
|
|
3100
|
+
name: { type: "string" },
|
|
3101
|
+
"hub-url": { type: "string" },
|
|
3102
|
+
help: { type: "boolean", default: false }
|
|
3103
|
+
},
|
|
3104
|
+
allowPositionals: true
|
|
3105
|
+
}));
|
|
3106
|
+
} catch (e) {
|
|
3107
|
+
return { kind: "error", message: e.message };
|
|
3108
|
+
}
|
|
3109
|
+
if (values.help) return { kind: "help", command: "service" };
|
|
3110
|
+
if (positionals.length === 0) {
|
|
3111
|
+
return { kind: "error", message: "service: missing action (`dahrk service install` or `uninstall`)" };
|
|
3112
|
+
}
|
|
3113
|
+
if (positionals.length > 1) {
|
|
3114
|
+
return { kind: "error", message: `service: unexpected argument "${positionals[1]}" (one action at a time)` };
|
|
3115
|
+
}
|
|
3116
|
+
const action = positionals[0];
|
|
3117
|
+
if (!isServiceAction(action)) {
|
|
3118
|
+
return { kind: "error", message: `service: unknown action "${action}" (expected install or uninstall)` };
|
|
3119
|
+
}
|
|
3120
|
+
const flags = {
|
|
3121
|
+
action,
|
|
3122
|
+
...values.token ? { token: values.token } : {},
|
|
3123
|
+
...values.name ? { name: values.name } : {},
|
|
3124
|
+
...values["hub-url"] ? { hubUrl: values["hub-url"] } : {}
|
|
3125
|
+
};
|
|
3126
|
+
return { kind: "service", flags };
|
|
3127
|
+
}
|
|
3128
|
+
function parseUpdate(flagArgs) {
|
|
3129
|
+
let values;
|
|
3130
|
+
try {
|
|
3131
|
+
({ values } = parseArgs({
|
|
3132
|
+
args: flagArgs,
|
|
3133
|
+
options: {
|
|
3134
|
+
check: { type: "boolean", default: false },
|
|
3135
|
+
help: { type: "boolean", default: false }
|
|
3136
|
+
},
|
|
3137
|
+
allowPositionals: false
|
|
3138
|
+
}));
|
|
3139
|
+
} catch (e) {
|
|
3140
|
+
return { kind: "error", message: e.message };
|
|
3141
|
+
}
|
|
3142
|
+
if (values.help) return { kind: "help", command: "update" };
|
|
3143
|
+
return { kind: "update", flags: { check: values.check ?? false } };
|
|
3144
|
+
}
|
|
2797
3145
|
function usage(bin, command) {
|
|
2798
3146
|
if (command === "start") {
|
|
2799
3147
|
return [
|
|
@@ -2808,6 +3156,40 @@ function usage(bin, command) {
|
|
|
2808
3156
|
" --ephemeral Do not persist a node id; mint a throwaway one (CI / one-shot)."
|
|
2809
3157
|
].join("\n");
|
|
2810
3158
|
}
|
|
3159
|
+
if (command === "run") {
|
|
3160
|
+
return [
|
|
3161
|
+
`Usage: ${bin} run <workflow> [options]`,
|
|
3162
|
+
"",
|
|
3163
|
+
"Run a workflow through the engine locally against this node's worktree, streaming stage progress.",
|
|
3164
|
+
"The engine-backed twin of `doctor`: the same run and stages, from the terminal - no Linear, no issue.",
|
|
3165
|
+
"",
|
|
3166
|
+
"Workflows:",
|
|
3167
|
+
" preflight Is this floor sound enough to run? Checks node, repo, and tools, then reports.",
|
|
3168
|
+
"",
|
|
3169
|
+
"Options:",
|
|
3170
|
+
" --repo <path> Repo to inspect (default: the current directory).",
|
|
3171
|
+
" --hub-url <url> Hub WebSocket URL to probe for reachability (or set DAHRK_HUB_URL).",
|
|
3172
|
+
" --token <token> Enrolment token to verify against the hub (or set DAHRK_ENROL_TOKEN)."
|
|
3173
|
+
].join("\n");
|
|
3174
|
+
}
|
|
3175
|
+
if (command === "service") {
|
|
3176
|
+
return [
|
|
3177
|
+
`Usage: ${bin} service install|uninstall [options]`,
|
|
3178
|
+
"",
|
|
3179
|
+
"Install (or remove) the node as an always-on service that starts on boot and restarts on",
|
|
3180
|
+
"failure - a launchd LaunchAgent on macOS, a systemd user service on Linux. No pm2, no root.",
|
|
3181
|
+
"The node id persisted at ~/.dahrk/node.json means it re-attaches as the same node across reboots.",
|
|
3182
|
+
"",
|
|
3183
|
+
"Actions:",
|
|
3184
|
+
" install Generate and register the service, then start it.",
|
|
3185
|
+
" uninstall Stop, deregister, and remove the service.",
|
|
3186
|
+
"",
|
|
3187
|
+
"Options (install; baked into the service, uninstall ignores them):",
|
|
3188
|
+
" --token <token> Enrolment token (required; or set DAHRK_ENROL_TOKEN).",
|
|
3189
|
+
" --hub-url <url> Hub WebSocket URL (or set DAHRK_HUB_URL).",
|
|
3190
|
+
" --name <name> Display-name override (else the hub assigns one)."
|
|
3191
|
+
].join("\n");
|
|
3192
|
+
}
|
|
2811
3193
|
if (command === "doctor") {
|
|
2812
3194
|
return [
|
|
2813
3195
|
`Usage: ${bin} doctor [options]`,
|
|
@@ -2819,12 +3201,27 @@ function usage(bin, command) {
|
|
|
2819
3201
|
" --hub-url <url> Hub WebSocket URL to reach (or set DAHRK_HUB_URL)."
|
|
2820
3202
|
].join("\n");
|
|
2821
3203
|
}
|
|
3204
|
+
if (command === "update") {
|
|
3205
|
+
return [
|
|
3206
|
+
`Usage: ${bin} update [--check]`,
|
|
3207
|
+
"",
|
|
3208
|
+
"Update this client in place to the latest published release. Detects how it was installed",
|
|
3209
|
+
"(npm / Homebrew / curl) and runs the right upgrade, or prints the exact command when it cannot.",
|
|
3210
|
+
"Reports current -> latest, and a no-op when already current.",
|
|
3211
|
+
"",
|
|
3212
|
+
"Options:",
|
|
3213
|
+
" --check Report whether an update is available without applying it (dry run)."
|
|
3214
|
+
].join("\n");
|
|
3215
|
+
}
|
|
2822
3216
|
return [
|
|
2823
3217
|
`Usage: ${bin} <command> [options]`,
|
|
2824
3218
|
"",
|
|
2825
3219
|
"Commands:",
|
|
2826
3220
|
" start Run the edge node (default). Needs a --token and a hub URL.",
|
|
3221
|
+
" run Run a workflow locally (engine-backed), e.g. `run preflight`.",
|
|
3222
|
+
" service Install/uninstall the node as an always-on service (launchd/systemd).",
|
|
2827
3223
|
" doctor Preflight checks: Node, runtimes, hub reachability, token validity.",
|
|
3224
|
+
" update Update the client to the latest release (or print how for your channel).",
|
|
2828
3225
|
" version Print the client version.",
|
|
2829
3226
|
" help Show this help, or `help <command>` for a command's options.",
|
|
2830
3227
|
"",
|
|
@@ -2963,20 +3360,607 @@ async function runDoctor(inputs, deps = {}) {
|
|
|
2963
3360
|
return checks.some((c) => c.status === "fail") ? 1 : 0;
|
|
2964
3361
|
}
|
|
2965
3362
|
|
|
3363
|
+
// src/preflight.ts
|
|
3364
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
3365
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
3366
|
+
import { accessSync, constants as fsConstants, existsSync as existsSync4, readdirSync as readdirSync3, statfsSync } from "fs";
|
|
3367
|
+
import { homedir as homedir2 } from "os";
|
|
3368
|
+
import { join as join7 } from "path";
|
|
3369
|
+
var REPORT_BASE_URL = "https://app.dahrk.ai/r";
|
|
3370
|
+
var LOW_DISK_BYTES = 512 * 1024 * 1024;
|
|
3371
|
+
var PREFLIGHT_STAGES = [
|
|
3372
|
+
{ id: "check-node", label: "check node" },
|
|
3373
|
+
{ id: "check-repo", label: "check repo" },
|
|
3374
|
+
{ id: "check-tools", label: "check tools" },
|
|
3375
|
+
{ id: "analyse", label: "analyse" },
|
|
3376
|
+
{ id: "report", label: "report" }
|
|
3377
|
+
];
|
|
3378
|
+
function checkWorktreeRoot(writable2) {
|
|
3379
|
+
return writable2 ? { status: "pass", label: "Worktree root", detail: "writable" } : { status: "fail", label: "Worktree root", detail: "not writable; the node cannot create worktrees here" };
|
|
3380
|
+
}
|
|
3381
|
+
function checkDiskSpace(freeBytes) {
|
|
3382
|
+
if (freeBytes === void 0) {
|
|
3383
|
+
return { status: "warn", label: "Free space", detail: "could not be determined" };
|
|
3384
|
+
}
|
|
3385
|
+
const gib = (freeBytes / (1024 * 1024 * 1024)).toFixed(1);
|
|
3386
|
+
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` };
|
|
3387
|
+
}
|
|
3388
|
+
function checkRepo(repo) {
|
|
3389
|
+
if (!repo.isGitRepo) {
|
|
3390
|
+
return {
|
|
3391
|
+
status: "fail",
|
|
3392
|
+
label: "Repository",
|
|
3393
|
+
detail: `${repo.path} is not a git repository${repo.detail ? ` (${repo.detail})` : ""}`
|
|
3394
|
+
};
|
|
3395
|
+
}
|
|
3396
|
+
if (!repo.headResolves) {
|
|
3397
|
+
return { status: "warn", label: "Repository", detail: `${repo.path}: no commits yet (base branch does not resolve)` };
|
|
3398
|
+
}
|
|
3399
|
+
return { status: "pass", label: "Repository", detail: `${repo.path} on ${repo.baseBranch ?? "(detached)"}` };
|
|
3400
|
+
}
|
|
3401
|
+
function checkTools(tools) {
|
|
3402
|
+
const git = tools.git ? { status: "pass", label: "git", detail: "installed" } : { status: "fail", label: "git", detail: "not found; git is required to run a workflow" };
|
|
3403
|
+
const finding = (present, label, missing) => present ? { status: "pass", label, detail: "available" } : { status: "warn", label, detail: missing };
|
|
3404
|
+
return [
|
|
3405
|
+
git,
|
|
3406
|
+
finding(tools.sshKey, "SSH key", "no key or agent identity found; pushing over SSH will fail"),
|
|
3407
|
+
finding(tools.claude, "Claude runtime", "not on PATH; the node will serve no agent stages"),
|
|
3408
|
+
finding(tools.gh, "gh CLI", "not installed; ambient PR opening is unavailable"),
|
|
3409
|
+
finding(tools.docker, "docker", "not present; container stages are unavailable")
|
|
3410
|
+
];
|
|
3411
|
+
}
|
|
3412
|
+
function asFinding(check) {
|
|
3413
|
+
return check.status === "fail" ? { ...check, status: "warn" } : check;
|
|
3414
|
+
}
|
|
3415
|
+
function synthesise(checks) {
|
|
3416
|
+
const fails = checks.filter((c) => c.status === "fail");
|
|
3417
|
+
const warns = checks.filter((c) => c.status === "warn");
|
|
3418
|
+
const list2 = (cs) => cs.map((c) => `${c.label.toLowerCase()} (${c.detail ?? "finding"})`).join("; ");
|
|
3419
|
+
if (fails.length > 0) {
|
|
3420
|
+
return `The floor is unsound: ${list2(fails)}. Fix ${fails.length === 1 ? "this" : "these"} before running a workflow here.`;
|
|
3421
|
+
}
|
|
3422
|
+
if (warns.length > 0) {
|
|
3423
|
+
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.`;
|
|
3424
|
+
}
|
|
3425
|
+
return "The floor is sound. Node, repo, and tools all check out - this host is ready to run a workflow.";
|
|
3426
|
+
}
|
|
3427
|
+
function worst(checks) {
|
|
3428
|
+
if (checks.some((c) => c.status === "fail")) return "fail";
|
|
3429
|
+
if (checks.some((c) => c.status === "warn")) return "warn";
|
|
3430
|
+
return "pass";
|
|
3431
|
+
}
|
|
3432
|
+
function renderStage(index, total, verdict, out) {
|
|
3433
|
+
const head = `[${index}/${total}] ${verdict.label}`;
|
|
3434
|
+
const findings = verdict.checks.filter((c) => c.status !== "pass");
|
|
3435
|
+
if (findings.length === 0) {
|
|
3436
|
+
out(`${head} \u2713`);
|
|
3437
|
+
return;
|
|
3438
|
+
}
|
|
3439
|
+
out(head);
|
|
3440
|
+
for (const f of findings) out(` \u2022 ${f.label}: ${f.detail ?? f.status}`);
|
|
3441
|
+
}
|
|
3442
|
+
async function runPreflight(inputs, deps = {}) {
|
|
3443
|
+
const d = { ...defaultDeps2(), ...deps };
|
|
3444
|
+
const repoPath = inputs.repoPath ?? process.cwd();
|
|
3445
|
+
const total = PREFLIGHT_STAGES.length;
|
|
3446
|
+
const runId = d.newRunId();
|
|
3447
|
+
d.out("dahrk run preflight");
|
|
3448
|
+
d.out("");
|
|
3449
|
+
const host = await d.gatherHost(repoPath);
|
|
3450
|
+
const probe2 = inputs.hubUrl ? await d.probeHub({
|
|
3451
|
+
hubUrl: inputs.hubUrl,
|
|
3452
|
+
...inputs.token ? { enrolToken: inputs.token } : {},
|
|
3453
|
+
runtimes: host.tools.claude ? ["claude-code"] : [],
|
|
3454
|
+
...inputs.clientVersion ? { clientVersion: inputs.clientVersion } : {}
|
|
3455
|
+
}) : void 0;
|
|
3456
|
+
const nodeChecks = [
|
|
3457
|
+
checkNode(d.nodeVersion),
|
|
3458
|
+
checkWorktreeRoot(host.worktreeRootWritable),
|
|
3459
|
+
checkDiskSpace(host.freeDiskBytes),
|
|
3460
|
+
asFinding(checkHub(inputs.hubUrl, probe2)),
|
|
3461
|
+
...inputs.token ? [asFinding(checkToken(true, inputs.hubUrl, probe2))] : []
|
|
3462
|
+
];
|
|
3463
|
+
const repoChecks = [checkRepo(host.repo)];
|
|
3464
|
+
const toolChecks = checkTools(host.tools);
|
|
3465
|
+
const stageVerdicts = [
|
|
3466
|
+
{ label: PREFLIGHT_STAGES[0].label, status: worst(nodeChecks), checks: nodeChecks },
|
|
3467
|
+
{ label: PREFLIGHT_STAGES[1].label, status: worst(repoChecks), checks: repoChecks },
|
|
3468
|
+
{ label: PREFLIGHT_STAGES[2].label, status: worst(toolChecks), checks: toolChecks }
|
|
3469
|
+
];
|
|
3470
|
+
stageVerdicts.forEach((v, i) => renderStage(i + 1, total, v, d.out));
|
|
3471
|
+
const allChecks = [...nodeChecks, ...repoChecks, ...toolChecks];
|
|
3472
|
+
const read = synthesise(allChecks);
|
|
3473
|
+
renderStage(4, total, { label: PREFLIGHT_STAGES[3].label, status: "pass", checks: [] }, d.out);
|
|
3474
|
+
renderStage(5, total, { label: PREFLIGHT_STAGES[4].label, status: "pass", checks: [] }, d.out);
|
|
3475
|
+
const failed = allChecks.filter((c) => c.status === "fail").length;
|
|
3476
|
+
const warned = allChecks.filter((c) => c.status === "warn").length;
|
|
3477
|
+
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.";
|
|
3478
|
+
d.out("");
|
|
3479
|
+
d.out(read);
|
|
3480
|
+
d.out("");
|
|
3481
|
+
d.out(summary);
|
|
3482
|
+
d.out(`Full report: ${REPORT_BASE_URL}/${runId}`);
|
|
3483
|
+
d.out("");
|
|
3484
|
+
d.out("no Linear, no OAuth, no issue. just the machine and the engine.");
|
|
3485
|
+
return failed > 0 ? 1 : 0;
|
|
3486
|
+
}
|
|
3487
|
+
var defaultDeps2 = () => ({
|
|
3488
|
+
nodeVersion: process.versions.node,
|
|
3489
|
+
probeHub,
|
|
3490
|
+
gatherHost: gatherHostFacts,
|
|
3491
|
+
newRunId: () => randomUUID2(),
|
|
3492
|
+
out: (line) => void process.stdout.write(`${line}
|
|
3493
|
+
`)
|
|
3494
|
+
});
|
|
3495
|
+
function commandPresent(cmd) {
|
|
3496
|
+
try {
|
|
3497
|
+
execFileSync4("sh", ["-c", `command -v ${cmd}`], { stdio: "ignore" });
|
|
3498
|
+
return true;
|
|
3499
|
+
} catch {
|
|
3500
|
+
return false;
|
|
3501
|
+
}
|
|
3502
|
+
}
|
|
3503
|
+
function sshKeyPresent() {
|
|
3504
|
+
try {
|
|
3505
|
+
const dir = join7(homedir2(), ".ssh");
|
|
3506
|
+
if (existsSync4(dir) && readdirSync3(dir).some((f) => f.endsWith(".pub"))) return true;
|
|
3507
|
+
} catch {
|
|
3508
|
+
}
|
|
3509
|
+
try {
|
|
3510
|
+
execFileSync4("ssh-add", ["-l"], { stdio: "ignore" });
|
|
3511
|
+
return true;
|
|
3512
|
+
} catch {
|
|
3513
|
+
return false;
|
|
3514
|
+
}
|
|
3515
|
+
}
|
|
3516
|
+
function writable(dir) {
|
|
3517
|
+
try {
|
|
3518
|
+
accessSync(dir, fsConstants.W_OK);
|
|
3519
|
+
return true;
|
|
3520
|
+
} catch {
|
|
3521
|
+
return false;
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
function worktreeRoot(env) {
|
|
3525
|
+
return env.DAHRK_WORKTREES_DIR ?? join7(env.DAHRK_STATE_DIR ?? join7(homedir2(), ".dahrk"), "worktrees");
|
|
3526
|
+
}
|
|
3527
|
+
function nearestExisting(dir) {
|
|
3528
|
+
let cur = dir;
|
|
3529
|
+
while (!existsSync4(cur)) {
|
|
3530
|
+
const parent = join7(cur, "..");
|
|
3531
|
+
if (parent === cur) break;
|
|
3532
|
+
cur = parent;
|
|
3533
|
+
}
|
|
3534
|
+
return cur;
|
|
3535
|
+
}
|
|
3536
|
+
function freeDiskBytes(dir) {
|
|
3537
|
+
try {
|
|
3538
|
+
const s = statfsSync(nearestExisting(dir));
|
|
3539
|
+
return s.bavail * s.bsize;
|
|
3540
|
+
} catch {
|
|
3541
|
+
return void 0;
|
|
3542
|
+
}
|
|
3543
|
+
}
|
|
3544
|
+
function probeRepo(repoPath) {
|
|
3545
|
+
const git = (args) => execFileSync4("git", ["-C", repoPath, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
3546
|
+
try {
|
|
3547
|
+
if (git(["rev-parse", "--is-inside-work-tree"]) !== "true") {
|
|
3548
|
+
return { path: repoPath, isGitRepo: false, headResolves: false, detail: "not inside a work tree" };
|
|
3549
|
+
}
|
|
3550
|
+
} catch {
|
|
3551
|
+
return { path: repoPath, isGitRepo: false, headResolves: false };
|
|
3552
|
+
}
|
|
3553
|
+
let headResolves = false;
|
|
3554
|
+
let baseBranch;
|
|
3555
|
+
try {
|
|
3556
|
+
git(["rev-parse", "--verify", "HEAD"]);
|
|
3557
|
+
headResolves = true;
|
|
3558
|
+
baseBranch = git(["rev-parse", "--abbrev-ref", "HEAD"]) || void 0;
|
|
3559
|
+
} catch {
|
|
3560
|
+
}
|
|
3561
|
+
return { path: repoPath, isGitRepo: true, headResolves, ...baseBranch ? { baseBranch } : {} };
|
|
3562
|
+
}
|
|
3563
|
+
function gatherHostFacts(repoPath) {
|
|
3564
|
+
const root = worktreeRoot(process.env);
|
|
3565
|
+
return {
|
|
3566
|
+
worktreeRootWritable: writable(nearestExisting(root)),
|
|
3567
|
+
freeDiskBytes: freeDiskBytes(root),
|
|
3568
|
+
tools: {
|
|
3569
|
+
git: commandPresent("git"),
|
|
3570
|
+
sshKey: sshKeyPresent(),
|
|
3571
|
+
claude: commandPresent("claude"),
|
|
3572
|
+
gh: commandPresent("gh"),
|
|
3573
|
+
docker: commandPresent("docker")
|
|
3574
|
+
},
|
|
3575
|
+
repo: probeRepo(repoPath)
|
|
3576
|
+
};
|
|
3577
|
+
}
|
|
3578
|
+
|
|
3579
|
+
// src/service.ts
|
|
3580
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
3581
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync6, realpathSync, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
|
|
3582
|
+
import { homedir as homedir3, platform as osPlatform3, userInfo } from "os";
|
|
3583
|
+
import { join as join8 } from "path";
|
|
3584
|
+
var LAUNCHD_LABEL = "ai.dahrk.node";
|
|
3585
|
+
var SYSTEMD_UNIT = "dahrk-node.service";
|
|
3586
|
+
function detectManager(plat) {
|
|
3587
|
+
if (plat === "darwin") return "launchd";
|
|
3588
|
+
if (plat === "linux") return "systemd";
|
|
3589
|
+
return "unsupported";
|
|
3590
|
+
}
|
|
3591
|
+
function serviceEnv(inputs) {
|
|
3592
|
+
return {
|
|
3593
|
+
DAHRK_ENROL_TOKEN: inputs.token,
|
|
3594
|
+
...inputs.hubUrl ? { DAHRK_HUB_URL: inputs.hubUrl } : {},
|
|
3595
|
+
...inputs.name ? { DAHRK_NODE_NAME: inputs.name } : {},
|
|
3596
|
+
...inputs.pathEnv ? { PATH: inputs.pathEnv } : {}
|
|
3597
|
+
};
|
|
3598
|
+
}
|
|
3599
|
+
function xmlEscape(s) {
|
|
3600
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
3601
|
+
}
|
|
3602
|
+
function renderLaunchdPlist(inputs) {
|
|
3603
|
+
const argv = [inputs.nodeBin, inputs.scriptPath, "start"];
|
|
3604
|
+
const env = serviceEnv(inputs);
|
|
3605
|
+
const progArgs = argv.map((a) => ` <string>${xmlEscape(a)}</string>`).join("\n");
|
|
3606
|
+
const envEntries = Object.entries(env).map(([k, v]) => ` <key>${xmlEscape(k)}</key>
|
|
3607
|
+
<string>${xmlEscape(v)}</string>`).join("\n");
|
|
3608
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
3609
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3610
|
+
<plist version="1.0">
|
|
3611
|
+
<dict>
|
|
3612
|
+
<key>Label</key>
|
|
3613
|
+
<string>${LAUNCHD_LABEL}</string>
|
|
3614
|
+
<key>ProgramArguments</key>
|
|
3615
|
+
<array>
|
|
3616
|
+
${progArgs}
|
|
3617
|
+
</array>
|
|
3618
|
+
<key>EnvironmentVariables</key>
|
|
3619
|
+
<dict>
|
|
3620
|
+
${envEntries}
|
|
3621
|
+
</dict>
|
|
3622
|
+
<key>WorkingDirectory</key>
|
|
3623
|
+
<string>${xmlEscape(inputs.homeDir)}</string>
|
|
3624
|
+
<key>RunAtLoad</key>
|
|
3625
|
+
<true/>
|
|
3626
|
+
<key>KeepAlive</key>
|
|
3627
|
+
<true/>
|
|
3628
|
+
<key>ThrottleInterval</key>
|
|
3629
|
+
<integer>10</integer>
|
|
3630
|
+
<key>StandardOutPath</key>
|
|
3631
|
+
<string>${xmlEscape(join8(inputs.logDir, "node.out.log"))}</string>
|
|
3632
|
+
<key>StandardErrorPath</key>
|
|
3633
|
+
<string>${xmlEscape(join8(inputs.logDir, "node.err.log"))}</string>
|
|
3634
|
+
</dict>
|
|
3635
|
+
</plist>
|
|
3636
|
+
`;
|
|
3637
|
+
}
|
|
3638
|
+
function systemdEnvValue(v) {
|
|
3639
|
+
return /\s/.test(v) ? `"${v.replace(/"/g, '\\"')}"` : v;
|
|
3640
|
+
}
|
|
3641
|
+
function renderSystemdUnit(inputs) {
|
|
3642
|
+
const exec = [inputs.nodeBin, inputs.scriptPath, "start"].map((a) => /\s/.test(a) ? `"${a}"` : a).join(" ");
|
|
3643
|
+
const env = serviceEnv(inputs);
|
|
3644
|
+
const envLines = Object.entries(env).map(([k, v]) => `Environment=${k}=${systemdEnvValue(v)}`).join("\n");
|
|
3645
|
+
return `[Unit]
|
|
3646
|
+
Description=Dahrk node (self-managed edge node)
|
|
3647
|
+
Documentation=https://dahrk.ai/docs
|
|
3648
|
+
After=network-online.target
|
|
3649
|
+
Wants=network-online.target
|
|
3650
|
+
|
|
3651
|
+
[Service]
|
|
3652
|
+
Type=simple
|
|
3653
|
+
ExecStart=${exec}
|
|
3654
|
+
${envLines}
|
|
3655
|
+
WorkingDirectory=${inputs.homeDir}
|
|
3656
|
+
Restart=on-failure
|
|
3657
|
+
RestartSec=3
|
|
3658
|
+
RestartPreventExitStatus=78
|
|
3659
|
+
|
|
3660
|
+
[Install]
|
|
3661
|
+
WantedBy=default.target
|
|
3662
|
+
`;
|
|
3663
|
+
}
|
|
3664
|
+
function buildPlan(inputs) {
|
|
3665
|
+
if (inputs.manager === "launchd") {
|
|
3666
|
+
const filePath2 = join8(inputs.homeDir, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
3667
|
+
return {
|
|
3668
|
+
manager: "launchd",
|
|
3669
|
+
label: LAUNCHD_LABEL,
|
|
3670
|
+
filePath: filePath2,
|
|
3671
|
+
content: renderLaunchdPlist(inputs),
|
|
3672
|
+
// Unload first so a re-install picks up the rewritten plist; a not-loaded unload is a no-op.
|
|
3673
|
+
installCommands: [
|
|
3674
|
+
{ argv: ["launchctl", "unload", filePath2], ignoreFailure: true },
|
|
3675
|
+
{ argv: ["launchctl", "load", "-w", filePath2] }
|
|
3676
|
+
],
|
|
3677
|
+
uninstallCommands: [{ argv: ["launchctl", "unload", "-w", filePath2], ignoreFailure: true }],
|
|
3678
|
+
logHint: `tail -f ${join8(inputs.logDir, "node.err.log")}`
|
|
3679
|
+
};
|
|
3680
|
+
}
|
|
3681
|
+
const filePath = join8(inputs.homeDir, ".config", "systemd", "user", SYSTEMD_UNIT);
|
|
3682
|
+
const user = userInfo().username;
|
|
3683
|
+
return {
|
|
3684
|
+
manager: "systemd",
|
|
3685
|
+
label: SYSTEMD_UNIT,
|
|
3686
|
+
filePath,
|
|
3687
|
+
content: renderSystemdUnit(inputs),
|
|
3688
|
+
// Reload to see the new unit, enable+start it, then enable linger so it survives logout / boots
|
|
3689
|
+
// headless. Linger can be denied without privilege on some hosts, so it is best-effort.
|
|
3690
|
+
installCommands: [
|
|
3691
|
+
{ argv: ["systemctl", "--user", "daemon-reload"] },
|
|
3692
|
+
{ argv: ["systemctl", "--user", "enable", "--now", SYSTEMD_UNIT] },
|
|
3693
|
+
{ argv: ["loginctl", "enable-linger", user], ignoreFailure: true }
|
|
3694
|
+
],
|
|
3695
|
+
uninstallCommands: [
|
|
3696
|
+
{ argv: ["systemctl", "--user", "disable", "--now", SYSTEMD_UNIT], ignoreFailure: true },
|
|
3697
|
+
{ argv: ["systemctl", "--user", "daemon-reload"], ignoreFailure: true }
|
|
3698
|
+
],
|
|
3699
|
+
logHint: `journalctl --user -u ${SYSTEMD_UNIT} -f`
|
|
3700
|
+
};
|
|
3701
|
+
}
|
|
3702
|
+
function printUnsupported(out) {
|
|
3703
|
+
out("dahrk service is supported on macOS (launchd) and Linux (systemd) only.");
|
|
3704
|
+
out("On other platforms, run the node under a process manager such as pm2 (see the README).");
|
|
3705
|
+
return 1;
|
|
3706
|
+
}
|
|
3707
|
+
function runCommands(commands, d) {
|
|
3708
|
+
for (const cmd of commands) {
|
|
3709
|
+
const code = d.run(cmd.argv);
|
|
3710
|
+
if (code !== 0 && !cmd.ignoreFailure) {
|
|
3711
|
+
d.out(` command failed (${code}): ${cmd.argv.join(" ")}`);
|
|
3712
|
+
return code;
|
|
3713
|
+
}
|
|
3714
|
+
}
|
|
3715
|
+
return 0;
|
|
3716
|
+
}
|
|
3717
|
+
async function runServiceInstall(inputs, deps = {}) {
|
|
3718
|
+
const d = { ...defaultDeps3(), ...deps };
|
|
3719
|
+
d.out("dahrk service install");
|
|
3720
|
+
d.out("");
|
|
3721
|
+
const manager = detectManager(d.platform);
|
|
3722
|
+
if (manager === "unsupported") return printUnsupported(d.out);
|
|
3723
|
+
if (!inputs.token) {
|
|
3724
|
+
d.out("No enrolment token: pass --token <token> or set DAHRK_ENROL_TOKEN.");
|
|
3725
|
+
d.out("Get one at https://app.dahrk.ai.");
|
|
3726
|
+
return 2;
|
|
3727
|
+
}
|
|
3728
|
+
const plan = buildPlan({
|
|
3729
|
+
manager,
|
|
3730
|
+
nodeBin: d.nodeBin,
|
|
3731
|
+
scriptPath: d.scriptPath,
|
|
3732
|
+
token: inputs.token,
|
|
3733
|
+
...inputs.name ? { name: inputs.name } : {},
|
|
3734
|
+
...inputs.hubUrl ? { hubUrl: inputs.hubUrl } : {},
|
|
3735
|
+
...d.pathEnv ? { pathEnv: d.pathEnv } : {},
|
|
3736
|
+
homeDir: d.homeDir,
|
|
3737
|
+
logDir: d.logDir
|
|
3738
|
+
});
|
|
3739
|
+
try {
|
|
3740
|
+
if (manager === "launchd") d.mkdirp(d.logDir);
|
|
3741
|
+
d.mkdirp(dirOf(plan.filePath));
|
|
3742
|
+
d.writeFile(plan.filePath, plan.content);
|
|
3743
|
+
} catch (e) {
|
|
3744
|
+
d.out(`Could not write the service file at ${plan.filePath}: ${e.message}`);
|
|
3745
|
+
return 1;
|
|
3746
|
+
}
|
|
3747
|
+
d.out(`Wrote ${plan.manager} service: ${plan.filePath}`);
|
|
3748
|
+
const code = runCommands(plan.installCommands, d);
|
|
3749
|
+
d.out("");
|
|
3750
|
+
if (code !== 0) {
|
|
3751
|
+
d.out(`The service file is in place but registering it failed (exit ${code}).`);
|
|
3752
|
+
d.out("Fix the reported error and re-run `dahrk service install`, or load it yourself.");
|
|
3753
|
+
return code;
|
|
3754
|
+
}
|
|
3755
|
+
d.out("Installed. The node will start on boot and restart on failure.");
|
|
3756
|
+
d.out(` logs: ${plan.logHint}`);
|
|
3757
|
+
d.out(" uninstall: dahrk service uninstall");
|
|
3758
|
+
return 0;
|
|
3759
|
+
}
|
|
3760
|
+
async function runServiceUninstall(deps = {}) {
|
|
3761
|
+
const d = { ...defaultDeps3(), ...deps };
|
|
3762
|
+
d.out("dahrk service uninstall");
|
|
3763
|
+
d.out("");
|
|
3764
|
+
const manager = detectManager(d.platform);
|
|
3765
|
+
if (manager === "unsupported") return printUnsupported(d.out);
|
|
3766
|
+
const plan = buildPlan({
|
|
3767
|
+
manager,
|
|
3768
|
+
nodeBin: d.nodeBin,
|
|
3769
|
+
scriptPath: d.scriptPath,
|
|
3770
|
+
token: "-",
|
|
3771
|
+
homeDir: d.homeDir,
|
|
3772
|
+
logDir: d.logDir
|
|
3773
|
+
});
|
|
3774
|
+
if (!d.fileExists(plan.filePath)) {
|
|
3775
|
+
d.out(`No service installed (${plan.filePath} not found). Nothing to do.`);
|
|
3776
|
+
return 0;
|
|
3777
|
+
}
|
|
3778
|
+
runCommands(plan.uninstallCommands, d);
|
|
3779
|
+
try {
|
|
3780
|
+
d.removeFile(plan.filePath);
|
|
3781
|
+
} catch (e) {
|
|
3782
|
+
d.out(`Deregistered, but could not delete ${plan.filePath}: ${e.message}`);
|
|
3783
|
+
return 1;
|
|
3784
|
+
}
|
|
3785
|
+
d.out(`Removed ${plan.filePath}. The node will no longer start on boot.`);
|
|
3786
|
+
return 0;
|
|
3787
|
+
}
|
|
3788
|
+
function dirOf(path) {
|
|
3789
|
+
const i = path.lastIndexOf("/");
|
|
3790
|
+
return i <= 0 ? path : path.slice(0, i);
|
|
3791
|
+
}
|
|
3792
|
+
function resolveScriptPath() {
|
|
3793
|
+
const argv1 = process.argv[1] ?? "";
|
|
3794
|
+
try {
|
|
3795
|
+
return realpathSync(argv1);
|
|
3796
|
+
} catch {
|
|
3797
|
+
return argv1;
|
|
3798
|
+
}
|
|
3799
|
+
}
|
|
3800
|
+
var defaultDeps3 = () => ({
|
|
3801
|
+
platform: osPlatform3(),
|
|
3802
|
+
homeDir: homedir3(),
|
|
3803
|
+
nodeBin: process.execPath,
|
|
3804
|
+
scriptPath: resolveScriptPath(),
|
|
3805
|
+
logDir: join8(process.env.DAHRK_STATE_DIR ?? join8(homedir3(), ".dahrk"), "logs"),
|
|
3806
|
+
// Snapshot the operator's PATH at install time so the daemon finds git + the runtime CLIs (Homebrew /
|
|
3807
|
+
// npm-global bins) that a supervisor's minimal PATH would otherwise hide.
|
|
3808
|
+
pathEnv: process.env.PATH,
|
|
3809
|
+
mkdirp: (dir) => void mkdirSync6(dir, { recursive: true }),
|
|
3810
|
+
writeFile: (path, content) => writeFileSync6(path, content),
|
|
3811
|
+
removeFile: (path) => rmSync4(path, { force: true }),
|
|
3812
|
+
fileExists: (path) => existsSync5(path),
|
|
3813
|
+
run: (argv) => {
|
|
3814
|
+
const [cmd, ...args] = argv;
|
|
3815
|
+
try {
|
|
3816
|
+
execFileSync5(cmd, args, { stdio: "inherit" });
|
|
3817
|
+
return 0;
|
|
3818
|
+
} catch (e) {
|
|
3819
|
+
const status = e.status;
|
|
3820
|
+
return typeof status === "number" ? status : 1;
|
|
3821
|
+
}
|
|
3822
|
+
},
|
|
3823
|
+
out: (line) => void process.stdout.write(`${line}
|
|
3824
|
+
`)
|
|
3825
|
+
});
|
|
3826
|
+
|
|
3827
|
+
// src/update.ts
|
|
3828
|
+
import { execFileSync as execFileSync6 } from "child_process";
|
|
3829
|
+
import { realpathSync as realpathSync2 } from "fs";
|
|
3830
|
+
var LATEST_URL = "https://registry.npmjs.org/dahrk-node/latest";
|
|
3831
|
+
var CHANNEL_COMMANDS = {
|
|
3832
|
+
npm: "npm install -g dahrk-node@latest",
|
|
3833
|
+
homebrew: "brew upgrade dahrkai/tap/dahrk",
|
|
3834
|
+
curl: "curl -fsSL https://dahrk.ai/install.sh | sh"
|
|
3835
|
+
};
|
|
3836
|
+
function upgradeCommand(channel) {
|
|
3837
|
+
switch (channel) {
|
|
3838
|
+
case "npm":
|
|
3839
|
+
return { argv: ["npm", "install", "-g", "dahrk-node@latest"], display: CHANNEL_COMMANDS.npm };
|
|
3840
|
+
case "homebrew":
|
|
3841
|
+
return { argv: ["brew", "upgrade", "dahrkai/tap/dahrk"], display: CHANNEL_COMMANDS.homebrew };
|
|
3842
|
+
case "unknown":
|
|
3843
|
+
return null;
|
|
3844
|
+
}
|
|
3845
|
+
}
|
|
3846
|
+
function detectChannel(binPath) {
|
|
3847
|
+
if (!binPath) return "unknown";
|
|
3848
|
+
let resolved = binPath;
|
|
3849
|
+
try {
|
|
3850
|
+
resolved = realpathSync2(binPath);
|
|
3851
|
+
} catch {
|
|
3852
|
+
}
|
|
3853
|
+
if (/[\\/]node_modules[\\/]dahrk-node[\\/]/.test(resolved)) return "npm";
|
|
3854
|
+
if (/[\\/](Cellar|homebrew)[\\/]/.test(resolved)) return "homebrew";
|
|
3855
|
+
return "unknown";
|
|
3856
|
+
}
|
|
3857
|
+
function core(v) {
|
|
3858
|
+
const m = /^(\d+)\.(\d+)\.(\d+)/.exec(v.trim().replace(/^v/, ""));
|
|
3859
|
+
if (!m) return null;
|
|
3860
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
3861
|
+
}
|
|
3862
|
+
function isNewer(latest, current) {
|
|
3863
|
+
const a = core(latest);
|
|
3864
|
+
const b = core(current);
|
|
3865
|
+
if (!a || !b) return false;
|
|
3866
|
+
for (let i = 0; i < 3; i++) {
|
|
3867
|
+
if (a[i] > b[i]) return true;
|
|
3868
|
+
if (a[i] < b[i]) return false;
|
|
3869
|
+
}
|
|
3870
|
+
return false;
|
|
3871
|
+
}
|
|
3872
|
+
function printChannelCommands(out) {
|
|
3873
|
+
out(` npm: ${CHANNEL_COMMANDS.npm}`);
|
|
3874
|
+
out(` Homebrew: ${CHANNEL_COMMANDS.homebrew}`);
|
|
3875
|
+
out(` curl: ${CHANNEL_COMMANDS.curl}`);
|
|
3876
|
+
}
|
|
3877
|
+
async function runUpdate(inputs, deps = {}) {
|
|
3878
|
+
const d = { ...defaultDeps4(), ...deps };
|
|
3879
|
+
const current = inputs.currentVersion;
|
|
3880
|
+
d.out("dahrk update");
|
|
3881
|
+
d.out("");
|
|
3882
|
+
let latest;
|
|
3883
|
+
try {
|
|
3884
|
+
latest = await d.fetchLatest();
|
|
3885
|
+
} catch (e) {
|
|
3886
|
+
d.out(`Could not determine the latest version: ${e.message}`);
|
|
3887
|
+
d.out(`You are on ${current}. See https://www.npmjs.com/package/dahrk-node for releases.`);
|
|
3888
|
+
return 1;
|
|
3889
|
+
}
|
|
3890
|
+
if (!isNewer(latest, current)) {
|
|
3891
|
+
d.out(`Already on the latest version (${current}).`);
|
|
3892
|
+
return 0;
|
|
3893
|
+
}
|
|
3894
|
+
d.out(`Update available: ${current} -> ${latest}`);
|
|
3895
|
+
const channel = detectChannel(d.binPath);
|
|
3896
|
+
const cmd = upgradeCommand(channel);
|
|
3897
|
+
if (inputs.check) {
|
|
3898
|
+
d.out("");
|
|
3899
|
+
if (cmd) {
|
|
3900
|
+
d.out(`Run \`dahrk update\` to upgrade (${channel}: ${cmd.display}).`);
|
|
3901
|
+
} else {
|
|
3902
|
+
d.out("Run `dahrk update`, or upgrade with the command for your install channel:");
|
|
3903
|
+
printChannelCommands(d.out);
|
|
3904
|
+
}
|
|
3905
|
+
return 0;
|
|
3906
|
+
}
|
|
3907
|
+
if (!cmd) {
|
|
3908
|
+
d.out("");
|
|
3909
|
+
d.out("Could not tell how this client was installed. Upgrade with the command for your channel:");
|
|
3910
|
+
printChannelCommands(d.out);
|
|
3911
|
+
return 0;
|
|
3912
|
+
}
|
|
3913
|
+
d.out("");
|
|
3914
|
+
d.out(`Upgrading via ${channel}: ${cmd.display}`);
|
|
3915
|
+
d.out("");
|
|
3916
|
+
const code = d.runUpgrade(cmd.argv);
|
|
3917
|
+
d.out("");
|
|
3918
|
+
if (code === 0) {
|
|
3919
|
+
d.out(`Upgraded to ${latest}. Restart a running node (\`dahrk start\`) to pick it up.`);
|
|
3920
|
+
} else {
|
|
3921
|
+
d.out(`Upgrade exited with code ${code}. Run it yourself to see the error: ${cmd.display}`);
|
|
3922
|
+
}
|
|
3923
|
+
return code;
|
|
3924
|
+
}
|
|
3925
|
+
async function fetchLatestVersion() {
|
|
3926
|
+
const res = await fetch(LATEST_URL, { headers: { accept: "application/json" } });
|
|
3927
|
+
if (!res.ok) throw new Error(`registry responded ${res.status}`);
|
|
3928
|
+
const body = await res.json();
|
|
3929
|
+
if (typeof body.version !== "string" || !body.version) throw new Error("registry returned no version");
|
|
3930
|
+
return body.version;
|
|
3931
|
+
}
|
|
3932
|
+
function spawnUpgrade(argv) {
|
|
3933
|
+
const [cmd, ...args] = argv;
|
|
3934
|
+
try {
|
|
3935
|
+
execFileSync6(cmd, args, { stdio: "inherit" });
|
|
3936
|
+
return 0;
|
|
3937
|
+
} catch (e) {
|
|
3938
|
+
const status = e.status;
|
|
3939
|
+
return typeof status === "number" ? status : 1;
|
|
3940
|
+
}
|
|
3941
|
+
}
|
|
3942
|
+
var defaultDeps4 = () => ({
|
|
3943
|
+
binPath: process.argv[1],
|
|
3944
|
+
fetchLatest: fetchLatestVersion,
|
|
3945
|
+
runUpgrade: spawnUpgrade,
|
|
3946
|
+
out: (line) => void process.stdout.write(`${line}
|
|
3947
|
+
`)
|
|
3948
|
+
});
|
|
3949
|
+
|
|
2966
3950
|
// src/main.ts
|
|
2967
|
-
var CLIENT_VERSION = "0.1.
|
|
3951
|
+
var CLIENT_VERSION = "0.1.5";
|
|
2968
3952
|
var DEFAULT_HUB_URL = "wss://api.dahrk.ai";
|
|
2969
3953
|
var list = (v) => (v ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
2970
3954
|
var RUNTIMES = ["claude-code", "codex", "pi"];
|
|
2971
3955
|
var isRuntime = (r) => RUNTIMES.includes(r);
|
|
2972
3956
|
function stateDir(env) {
|
|
2973
|
-
return env.DAHRK_STATE_DIR ??
|
|
3957
|
+
return env.DAHRK_STATE_DIR ?? join9(homedir4(), ".dahrk");
|
|
2974
3958
|
}
|
|
2975
3959
|
function legacyStateDir(env) {
|
|
2976
|
-
return env.DAHRK_STATE_DIR ? void 0 :
|
|
3960
|
+
return env.DAHRK_STATE_DIR ? void 0 : join9(homedir4(), ".skakel");
|
|
2977
3961
|
}
|
|
2978
3962
|
function readNodeId(file) {
|
|
2979
|
-
if (!
|
|
3963
|
+
if (!existsSync6(file)) return void 0;
|
|
2980
3964
|
try {
|
|
2981
3965
|
const parsed = JSON.parse(readFileSync5(file, "utf8"));
|
|
2982
3966
|
if (typeof parsed.nodeId === "string" && parsed.nodeId) return parsed.nodeId;
|
|
@@ -2986,20 +3970,20 @@ function readNodeId(file) {
|
|
|
2986
3970
|
}
|
|
2987
3971
|
function resolveNodeId(env, opts = {}) {
|
|
2988
3972
|
if (env.DAHRK_NODE_ID) return env.DAHRK_NODE_ID;
|
|
2989
|
-
if (opts.ephemeral) return
|
|
3973
|
+
if (opts.ephemeral) return randomUUID3();
|
|
2990
3974
|
const dir = stateDir(env);
|
|
2991
|
-
const file =
|
|
3975
|
+
const file = join9(dir, "node.json");
|
|
2992
3976
|
const existing = readNodeId(file);
|
|
2993
3977
|
if (existing) return existing;
|
|
2994
3978
|
const legacy = legacyStateDir(env);
|
|
2995
3979
|
if (legacy) {
|
|
2996
|
-
const legacyId = readNodeId(
|
|
3980
|
+
const legacyId = readNodeId(join9(legacy, "node.json"));
|
|
2997
3981
|
if (legacyId) return legacyId;
|
|
2998
3982
|
}
|
|
2999
|
-
const nodeId =
|
|
3983
|
+
const nodeId = randomUUID3();
|
|
3000
3984
|
try {
|
|
3001
|
-
|
|
3002
|
-
|
|
3985
|
+
mkdirSync7(dir, { recursive: true });
|
|
3986
|
+
writeFileSync7(file, `${JSON.stringify({ nodeId }, null, 2)}
|
|
3003
3987
|
`);
|
|
3004
3988
|
} catch (e) {
|
|
3005
3989
|
console.warn(`could not persist node id to ${file}: ${e.message}`);
|
|
@@ -3078,6 +4062,24 @@ async function start(flags) {
|
|
|
3078
4062
|
const resolved = { nodeId, runtimes, clientVersion: CLIENT_VERSION };
|
|
3079
4063
|
await startEdgeNode(buildEdgeOptions(env, resolved));
|
|
3080
4064
|
}
|
|
4065
|
+
var KNOWN_WORKFLOWS = ["preflight"];
|
|
4066
|
+
async function runWorkflow(flags) {
|
|
4067
|
+
if (flags.workflow !== "preflight") {
|
|
4068
|
+
console.error(`unknown workflow: ${flags.workflow}
|
|
4069
|
+
`);
|
|
4070
|
+
console.error(`Available workflows: ${KNOWN_WORKFLOWS.join(", ")}`);
|
|
4071
|
+
return 2;
|
|
4072
|
+
}
|
|
4073
|
+
const env = applyEnvAliases(process.env);
|
|
4074
|
+
const hubUrl = flags.hubUrl ?? env.DAHRK_HUB_URL;
|
|
4075
|
+
const token = flags.token ?? env.DAHRK_ENROL_TOKEN;
|
|
4076
|
+
return runPreflight({
|
|
4077
|
+
...flags.repo ? { repoPath: flags.repo } : {},
|
|
4078
|
+
...hubUrl ? { hubUrl } : {},
|
|
4079
|
+
...token ? { token } : {},
|
|
4080
|
+
clientVersion: CLIENT_VERSION
|
|
4081
|
+
});
|
|
4082
|
+
}
|
|
3081
4083
|
async function main() {
|
|
3082
4084
|
const invoked = basename(process.argv[1] ?? "");
|
|
3083
4085
|
const bin = !invoked || invoked.startsWith("main.") ? "dahrk" : invoked;
|
|
@@ -3104,6 +4106,28 @@ async function main() {
|
|
|
3104
4106
|
});
|
|
3105
4107
|
break;
|
|
3106
4108
|
}
|
|
4109
|
+
case "run":
|
|
4110
|
+
process.exitCode = await runWorkflow(parsed.flags);
|
|
4111
|
+
break;
|
|
4112
|
+
case "service": {
|
|
4113
|
+
if (parsed.flags.action === "uninstall") {
|
|
4114
|
+
process.exitCode = await runServiceUninstall();
|
|
4115
|
+
break;
|
|
4116
|
+
}
|
|
4117
|
+
const env = applyEnvAliases(process.env);
|
|
4118
|
+
const token = parsed.flags.token ?? env.DAHRK_ENROL_TOKEN;
|
|
4119
|
+
const name = parsed.flags.name ?? env.DAHRK_NODE_NAME;
|
|
4120
|
+
const hubUrl = parsed.flags.hubUrl ?? env.DAHRK_HUB_URL;
|
|
4121
|
+
process.exitCode = await runServiceInstall({
|
|
4122
|
+
...token ? { token } : {},
|
|
4123
|
+
...name ? { name } : {},
|
|
4124
|
+
...hubUrl ? { hubUrl } : {}
|
|
4125
|
+
});
|
|
4126
|
+
break;
|
|
4127
|
+
}
|
|
4128
|
+
case "update":
|
|
4129
|
+
process.exitCode = await runUpdate({ currentVersion: CLIENT_VERSION, check: parsed.flags.check });
|
|
4130
|
+
break;
|
|
3107
4131
|
case "start":
|
|
3108
4132
|
await start(parsed.flags);
|
|
3109
4133
|
break;
|
|
@@ -3113,7 +4137,7 @@ var invokedAsEntrypoint = (() => {
|
|
|
3113
4137
|
const argv1 = process.argv[1];
|
|
3114
4138
|
if (!argv1) return false;
|
|
3115
4139
|
try {
|
|
3116
|
-
return pathToFileURL(
|
|
4140
|
+
return pathToFileURL(realpathSync3(argv1)).href === import.meta.url;
|
|
3117
4141
|
} catch {
|
|
3118
4142
|
return false;
|
|
3119
4143
|
}
|
|
@@ -3126,6 +4150,7 @@ if (invokedAsEntrypoint) {
|
|
|
3126
4150
|
}
|
|
3127
4151
|
export {
|
|
3128
4152
|
DEFAULT_HUB_URL,
|
|
4153
|
+
KNOWN_WORKFLOWS,
|
|
3129
4154
|
buildEdgeOptions,
|
|
3130
4155
|
resolveNodeId,
|
|
3131
4156
|
resolveRuntimes
|