dahrk-node 0.1.18 → 0.1.20

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 +324 -19
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -1510,6 +1510,14 @@ function createGitService(opts = {}) {
1510
1510
  };
1511
1511
  const netEnv = (authEnv) => authEnv ?? { ...process.env, GIT_TERMINAL_PROMPT: "0" };
1512
1512
  const withTokenUser = (gitUrl) => /^https:\/\/[^@/]+@/.test(gitUrl) ? gitUrl : gitUrl.replace(/^https:\/\//, "https://x-access-token@");
1513
+ const resolveRemoteAuth = (gitUrl, credentialToken) => {
1514
+ const auth = credentialToken ? setupAuth(credentialToken) : void 0;
1515
+ return {
1516
+ remote: credentialToken ? withTokenUser(gitUrl) : gitUrl,
1517
+ authEnv: auth?.env,
1518
+ cleanup: () => auth?.cleanup()
1519
+ };
1520
+ };
1513
1521
  const mirrorPathFor = (repoId) => join3(mirrorsDir, sanitizeBranchName(repoId));
1514
1522
  const listWorktrees = (mirror) => {
1515
1523
  let out2;
@@ -1689,15 +1697,14 @@ function createGitService(opts = {}) {
1689
1697
  } catch {
1690
1698
  }
1691
1699
  }
1692
- const auth = opts2.credentialToken ? setupAuth(opts2.credentialToken) : void 0;
1693
- const remote = opts2.credentialToken ? withTokenUser(ref.gitUrl) : ref.gitUrl;
1700
+ const { remote, authEnv, cleanup } = resolveRemoteAuth(ref.gitUrl, opts2.credentialToken);
1694
1701
  let pushed = false;
1695
1702
  let integration;
1696
1703
  try {
1697
1704
  let fetched = false;
1698
1705
  if (opts2.base) {
1699
1706
  try {
1700
- git(worktreePath, ["fetch", remote, opts2.base], netEnv(auth?.env));
1707
+ git(worktreePath, ["fetch", remote, opts2.base], netEnv(authEnv));
1701
1708
  fetched = true;
1702
1709
  } catch (e) {
1703
1710
  log.warn(`base fetch failed for ${opts2.base}; skipping push-time integration: ${e.message}`);
@@ -1721,7 +1728,7 @@ function createGitService(opts = {}) {
1721
1728
  "merge",
1722
1729
  "--no-edit",
1723
1730
  "FETCH_HEAD"
1724
- ], auth?.env);
1731
+ ], authEnv);
1725
1732
  integration = "clean";
1726
1733
  headSha = git(worktreePath, ["rev-parse", "HEAD"]).trim();
1727
1734
  } catch (mergeErr) {
@@ -1738,10 +1745,10 @@ function createGitService(opts = {}) {
1738
1745
  throw mergeErr;
1739
1746
  }
1740
1747
  }
1741
- git(worktreePath, ["push", remote, `HEAD:refs/heads/${branch}`], netEnv(auth?.env));
1748
+ git(worktreePath, ["push", remote, `HEAD:refs/heads/${branch}`], netEnv(authEnv));
1742
1749
  pushed = true;
1743
1750
  } finally {
1744
- auth?.cleanup();
1751
+ cleanup();
1745
1752
  }
1746
1753
  return { headSha, pushed, nothingToCommit: !dirty, commitsAhead, ...integration ? { integration } : {} };
1747
1754
  },
@@ -1752,12 +1759,11 @@ function createGitService(opts = {}) {
1752
1759
  }
1753
1760
  const wipRef = sanitizeBranchName(opts2.branch);
1754
1761
  const { headSha, dirty } = commitPending(worktreePath, opts2.message);
1755
- const auth = opts2.credentialToken ? setupAuth(opts2.credentialToken) : void 0;
1756
- const remote = opts2.credentialToken ? withTokenUser(ref.gitUrl) : ref.gitUrl;
1762
+ const { remote, authEnv, cleanup } = resolveRemoteAuth(ref.gitUrl, opts2.credentialToken);
1757
1763
  try {
1758
- git(worktreePath, ["push", "--force", remote, `HEAD:refs/heads/${wipRef}`], netEnv(auth?.env));
1764
+ git(worktreePath, ["push", "--force", remote, `HEAD:refs/heads/${wipRef}`], netEnv(authEnv));
1759
1765
  } finally {
1760
- auth?.cleanup();
1766
+ cleanup();
1761
1767
  }
1762
1768
  return { headSha, pushed: true, nothingToCommit: !dirty, wipRef };
1763
1769
  },
@@ -1772,15 +1778,14 @@ function createGitService(opts = {}) {
1772
1778
  if (!dirty) return { dirty: false, headSha, pushed: false };
1773
1779
  git(worktreePath, ["branch", "--force", wipRef, tailSha]);
1774
1780
  let pushed = false;
1775
- const auth = opts2.credentialToken ? setupAuth(opts2.credentialToken) : void 0;
1776
- const remote = opts2.credentialToken ? withTokenUser(ref.gitUrl) : ref.gitUrl;
1781
+ const { remote, authEnv, cleanup } = resolveRemoteAuth(ref.gitUrl, opts2.credentialToken);
1777
1782
  try {
1778
- git(worktreePath, ["push", "--force", remote, `${tailSha}:refs/heads/${wipRef}`], netEnv(auth?.env));
1783
+ git(worktreePath, ["push", "--force", remote, `${tailSha}:refs/heads/${wipRef}`], netEnv(authEnv));
1779
1784
  pushed = true;
1780
1785
  } catch (e) {
1781
1786
  log.warn(`could not push the preserved tail to ${wipRef}: ${e.message}`);
1782
1787
  } finally {
1783
- auth?.cleanup();
1788
+ cleanup();
1784
1789
  }
1785
1790
  git(worktreePath, ["reset", "--hard", headSha]);
1786
1791
  git(worktreePath, ["clean", "-fd", "--exclude", SCRATCH_DIR]);
@@ -2740,12 +2745,30 @@ function tokenise(cmd) {
2740
2745
  let quoted = false;
2741
2746
  let started = false;
2742
2747
  let quote = null;
2748
+ const pendingHeredocs = [];
2743
2749
  const push = () => {
2744
2750
  if (started) tokens.push({ value: cur, quoted });
2745
2751
  cur = "";
2746
2752
  quoted = false;
2747
2753
  started = false;
2748
2754
  };
2755
+ const consumeHeredocs = (s) => {
2756
+ let p = s;
2757
+ for (; ; ) {
2758
+ let lineEnd = p;
2759
+ while (lineEnd < cmd.length && cmd[lineEnd] !== "\n") lineEnd++;
2760
+ const line = cmd.slice(p, lineEnd);
2761
+ const { delim, dash } = pendingHeredocs[0];
2762
+ const terminator = dash ? line.replace(/^\t+/, "") : line;
2763
+ const atEnd = lineEnd >= cmd.length;
2764
+ if (terminator === delim) {
2765
+ pendingHeredocs.shift();
2766
+ if (pendingHeredocs.length === 0) return atEnd ? lineEnd : lineEnd + 1;
2767
+ }
2768
+ if (atEnd) return -1;
2769
+ p = lineEnd + 1;
2770
+ }
2771
+ };
2749
2772
  for (let i = 0; i < cmd.length; i++) {
2750
2773
  const c = cmd[i];
2751
2774
  if (quote) {
@@ -2793,6 +2816,13 @@ function tokenise(cmd) {
2793
2816
  i = j;
2794
2817
  continue;
2795
2818
  }
2819
+ if (c === "\n" && pendingHeredocs.length > 0) {
2820
+ push();
2821
+ const resume = consumeHeredocs(i + 1);
2822
+ if (resume < 0) return null;
2823
+ i = resume - 1;
2824
+ continue;
2825
+ }
2796
2826
  if (/\s/.test(c)) {
2797
2827
  push();
2798
2828
  continue;
@@ -2805,6 +2835,38 @@ function tokenise(cmd) {
2805
2835
  i += 2;
2806
2836
  continue;
2807
2837
  }
2838
+ if (two === "<<" && cmd[i + 2] !== "<") {
2839
+ push();
2840
+ let k = i + 2;
2841
+ const dash = cmd[k] === "-";
2842
+ if (dash) k++;
2843
+ while (k < cmd.length && (cmd[k] === " " || cmd[k] === " ")) k++;
2844
+ let delim = "";
2845
+ let dq = null;
2846
+ for (; k < cmd.length; k++) {
2847
+ const d = cmd[k];
2848
+ if (dq) {
2849
+ if (d === dq) dq = null;
2850
+ else delim += d;
2851
+ continue;
2852
+ }
2853
+ if (d === '"' || d === "'") {
2854
+ dq = d;
2855
+ continue;
2856
+ }
2857
+ if (d === "\\" && k + 1 < cmd.length) {
2858
+ delim += cmd[++k];
2859
+ continue;
2860
+ }
2861
+ if (/[\s;&|<>]/.test(d)) break;
2862
+ delim += d;
2863
+ }
2864
+ if (dq) return null;
2865
+ if (!delim) return null;
2866
+ pendingHeredocs.push({ delim, dash });
2867
+ i = k - 1;
2868
+ continue;
2869
+ }
2808
2870
  if (["&&", "||", ">>", "2>", "&>"].includes(two)) {
2809
2871
  push();
2810
2872
  tokens.push({ value: two, quoted: false, operator: two });
@@ -2820,6 +2882,7 @@ function tokenise(cmd) {
2820
2882
  started = true;
2821
2883
  }
2822
2884
  if (quote) return null;
2885
+ if (pendingHeredocs.length > 0) return null;
2823
2886
  push();
2824
2887
  return { tokens, subshells };
2825
2888
  }
@@ -2949,7 +3012,7 @@ function scanCommand(cmd, roots, cwd = roots.cwd) {
2949
3012
  }
2950
3013
  if (out2.kind !== "ok") return out2;
2951
3014
  }
2952
- return { kind: "ok" };
3015
+ return { kind: "ok", cwd: cwdNow };
2953
3016
  }
2954
3017
 
2955
3018
  // ../../packages/edge/src/builtins.ts
@@ -3028,6 +3091,7 @@ function pathsIn(input) {
3028
3091
  return out2;
3029
3092
  }
3030
3093
  function fsConfineRule(roots) {
3094
+ let sessionCwd = roots.cwd;
3031
3095
  return {
3032
3096
  name: "fs_confine",
3033
3097
  evaluate(event) {
@@ -3038,8 +3102,12 @@ function fsConfineRule(roots) {
3038
3102
  reason: `${need2 === "write" ? "write to" : "read of"} "${path}" is outside the run's worktree`
3039
3103
  });
3040
3104
  if (SHELL_TOOLS.has(event.tool)) {
3041
- const result = scanCommand(commandOf(event.input), roots);
3105
+ const result = scanCommand(commandOf(event.input), roots, sessionCwd);
3042
3106
  if (result.kind === "escape") return deny(result.path, result.need);
3107
+ if (result.kind === "ok") {
3108
+ if (result.cwd) sessionCwd = result.cwd;
3109
+ return null;
3110
+ }
3043
3111
  if (result.kind === "unparseable") {
3044
3112
  return {
3045
3113
  verdict: "deny",
@@ -4533,6 +4601,7 @@ var COMMANDS = /* @__PURE__ */ new Set([
4533
4601
  "logs",
4534
4602
  "diagnose",
4535
4603
  "run",
4604
+ "repo",
4536
4605
  "service",
4537
4606
  "doctor",
4538
4607
  "status",
@@ -4556,6 +4625,7 @@ function parseCli(argv) {
4556
4625
  flagArgs = rest;
4557
4626
  }
4558
4627
  if (command === "run") return parseRun(flagArgs);
4628
+ if (command === "repo") return parseRepo(flagArgs);
4559
4629
  if (command === "service") return parseService(flagArgs);
4560
4630
  if (command === "update") return parseUpdate(flagArgs);
4561
4631
  if (command === "logs") return parseLogs(flagArgs);
@@ -4570,6 +4640,8 @@ function parseCli(argv) {
4570
4640
  "hub-url": { type: "string" },
4571
4641
  ephemeral: { type: "boolean", default: false },
4572
4642
  foreground: { type: "boolean", default: false },
4643
+ // `start`: enrol but do not install the service (the operator supervises the node themselves).
4644
+ "no-service": { type: "boolean", default: false },
4573
4645
  // `stop` / `restart`: take the node down even though it has work in flight.
4574
4646
  force: { type: "boolean", default: false },
4575
4647
  // `status`: machine-readable output.
@@ -4591,7 +4663,8 @@ function parseCli(argv) {
4591
4663
  ephemeral,
4592
4664
  // An ephemeral node mints a throwaway id and persists nothing, so there is nothing coherent to hand to
4593
4665
  // a supervisor that restarts it on boot. It is a foreground node by definition.
4594
- foreground: (values.foreground ?? false) || ephemeral
4666
+ foreground: (values.foreground ?? false) || ephemeral,
4667
+ ...values["no-service"] ? { noService: true } : {}
4595
4668
  };
4596
4669
  if (command === "stop") return { kind: "stop", force };
4597
4670
  if (command === "restart") return { kind: "restart", flags, force };
@@ -4723,6 +4796,43 @@ function parseService(flagArgs) {
4723
4796
  };
4724
4797
  return { kind: "service", flags };
4725
4798
  }
4799
+ var REPO_ACTIONS = /* @__PURE__ */ new Set(["add"]);
4800
+ var isRepoAction = (s) => REPO_ACTIONS.has(s);
4801
+ function parseRepo(flagArgs) {
4802
+ let values;
4803
+ let positionals;
4804
+ try {
4805
+ ({ values, positionals } = parseArgs({
4806
+ args: flagArgs,
4807
+ options: {
4808
+ name: { type: "string" },
4809
+ "hub-url": { type: "string" },
4810
+ token: { type: "string" },
4811
+ help: { type: "boolean", default: false }
4812
+ },
4813
+ allowPositionals: true
4814
+ }));
4815
+ } catch (e) {
4816
+ return { kind: "error", message: e.message };
4817
+ }
4818
+ if (values.help) return { kind: "help", command: "repo" };
4819
+ if (positionals.length === 0) {
4820
+ return { kind: "error", message: "repo: missing action (`dahrk repo add`)" };
4821
+ }
4822
+ if (positionals.length > 1) {
4823
+ return { kind: "error", message: `repo: unexpected argument "${positionals[1]}" (one action at a time)` };
4824
+ }
4825
+ const action = positionals[0];
4826
+ if (!isRepoAction(action)) {
4827
+ return { kind: "error", message: `repo: unknown action "${action}" (expected add)` };
4828
+ }
4829
+ const flags = {
4830
+ ...values.name ? { name: values.name } : {},
4831
+ ...values["hub-url"] ? { hubUrl: values["hub-url"] } : {},
4832
+ ...values.token ? { token: values.token } : {}
4833
+ };
4834
+ return { kind: "repo", flags };
4835
+ }
4726
4836
  function parseUpdate(flagArgs) {
4727
4837
  let values;
4728
4838
  try {
@@ -4767,6 +4877,8 @@ function usage(bin, command) {
4767
4877
  " setting DAHRK_FOREGROUND=1, which is easier to set in a Dockerfile or pm2 config.",
4768
4878
  " --ephemeral Do not persist (or read) node id and token; mint a throwaway id (CI / one-shot).",
4769
4879
  " Implies --foreground: a node with no persistent identity has nothing to daemonise.",
4880
+ " --no-service Enrol (cache the token) but do not install the always-on service, for a node you",
4881
+ " supervise yourself. Run it later with `dahrk start --foreground` (or your own supervisor).",
4770
4882
  "",
4771
4883
  "Only one node may run at a time on a host - they would share this machine's node id and race each",
4772
4884
  "other for Jobs - so a second `start` refuses rather than dialling the hub twice."
@@ -4867,6 +4979,30 @@ function usage(bin, command) {
4867
4979
  " --token <token> Enrolment token to verify against the hub (or set DAHRK_ENROL_TOKEN)."
4868
4980
  ].join("\n");
4869
4981
  }
4982
+ if (command === "repo") {
4983
+ return [
4984
+ `Usage: ${bin} repo add [options]`,
4985
+ "",
4986
+ "Register the current git repository with the hub, so it can run workflows. Run it from inside the",
4987
+ "repo - `cd your-repo && dahrk repo add` - and the node reads the `origin` remote and the current",
4988
+ "branch itself: no form, no pasted git URL, because the node already sits next to the code.",
4989
+ "",
4990
+ "The git URL is registered in the form the host can authenticate. An HTTPS origin is kept as-is; an",
4991
+ "SSH origin is kept when this host has an SSH key, and otherwise normalised to HTTPS with a warning.",
4992
+ "Re-running on an already-registered repo is a no-op, not an error or a duplicate.",
4993
+ "",
4994
+ "This uses the node's existing enrolment - run `dahrk start --token <token>` first if it is not yet",
4995
+ "enrolled. It dials the hub itself, so the daemon need not be running.",
4996
+ "",
4997
+ "Actions:",
4998
+ " add Register the repository in the current directory.",
4999
+ "",
5000
+ "Options:",
5001
+ " --name <name> Display name for the repo (default: the slug from the origin URL).",
5002
+ " --hub-url <url> Hub URL to register with (or set DAHRK_HUB_URL).",
5003
+ " --token <token> Enrolment token to authenticate with (default: the cached one)."
5004
+ ].join("\n");
5005
+ }
4870
5006
  if (command === "service") {
4871
5007
  return [
4872
5008
  `Usage: ${bin} service install|uninstall [options]`,
@@ -4943,6 +5079,7 @@ function usage(bin, command) {
4943
5079
  " diagnose Write a support bundle you can read, and send on if you choose. Uploads nothing.",
4944
5080
  " status Is it up, is it enrolled, what is it working on? (local, dials nothing)",
4945
5081
  " run Run a workflow locally (engine-backed), e.g. `run preflight`.",
5082
+ " repo Register the current repository with the hub (`repo add`).",
4946
5083
  " doctor Preflight checks: Node, runtimes, hub reachability, token validity.",
4947
5084
  " service Install/uninstall the always-on service by hand (`start` does this for you).",
4948
5085
  " update Update the client to the latest release (or print how for your channel).",
@@ -5272,6 +5409,91 @@ async function runDoctor(inputs, deps = {}) {
5272
5409
  return checks.some((c) => c.status === "fail") ? 1 : 0;
5273
5410
  }
5274
5411
 
5412
+ // src/repo-add.ts
5413
+ import { createHash as createHash4 } from "crypto";
5414
+ var stripRepoSuffix = (s) => s.replace(/\.git$/i, "").replace(/\/+$/, "");
5415
+ function splitOwnerRepo(path) {
5416
+ const parts = stripRepoSuffix(path).split("/").filter(Boolean);
5417
+ if (parts.length < 2) return void 0;
5418
+ const repo = parts[parts.length - 1];
5419
+ const owner = parts.slice(0, -1).join("/");
5420
+ return { owner, repo };
5421
+ }
5422
+ function parseGitRemote(url) {
5423
+ const trimmed = url.trim();
5424
+ if (!trimmed) return void 0;
5425
+ const urlMatch = /^(?:ssh|https?):\/\/(?:[^@/]+@)?([^/:]+)(?::\d+)?\/(.+)$/i.exec(trimmed);
5426
+ if (urlMatch) {
5427
+ const parts = splitOwnerRepo(urlMatch[2]);
5428
+ return parts ? { host: urlMatch[1], ...parts } : void 0;
5429
+ }
5430
+ const scpMatch = /^(?:[^@\s]+@)?([^/:\s]+):(.+)$/.exec(trimmed);
5431
+ if (scpMatch) {
5432
+ const parts = splitOwnerRepo(scpMatch[2]);
5433
+ return parts ? { host: scpMatch[1], ...parts } : void 0;
5434
+ }
5435
+ return void 0;
5436
+ }
5437
+ var httpsFormOf = (r) => `https://${r.host}/${r.owner}/${r.repo}.git`;
5438
+ var isHttpsRemote = (url) => /^https?:\/\//i.test(url.trim());
5439
+ function chooseGitUrl(input) {
5440
+ const originUrl = input.originUrl.trim();
5441
+ if (isHttpsRemote(originUrl)) return { gitUrl: originUrl, converted: false };
5442
+ if (input.sshKeyPresent) return { gitUrl: originUrl, converted: false };
5443
+ const parsed = parseGitRemote(originUrl);
5444
+ if (!parsed) return { gitUrl: originUrl, converted: false };
5445
+ return { gitUrl: httpsFormOf(parsed), converted: true };
5446
+ }
5447
+ var deriveRepoName = (remote) => remote.repo;
5448
+ function deriveRepoId(gitUrl) {
5449
+ const parsed = parseGitRemote(gitUrl);
5450
+ const canonical2 = parsed ? `${parsed.host}/${parsed.owner}/${parsed.repo}`.toLowerCase() : gitUrl.trim().toLowerCase();
5451
+ const hash = createHash4("sha256").update(canonical2).digest("hex").slice(0, 12);
5452
+ const slug = (parsed ? parsed.repo : "repo").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
5453
+ return `${slug}-${hash}`;
5454
+ }
5455
+ function hubHttpBase(url) {
5456
+ const trimmed = url.trim().replace(/\/+$/, "");
5457
+ if (/^wss:\/\//i.test(trimmed)) return trimmed.replace(/^wss:\/\//i, "https://");
5458
+ if (/^ws:\/\//i.test(trimmed)) return trimmed.replace(/^ws:\/\//i, "http://");
5459
+ return trimmed;
5460
+ }
5461
+ async function readJson(res) {
5462
+ try {
5463
+ const body = await res.json();
5464
+ return body && typeof body === "object" ? body : void 0;
5465
+ } catch {
5466
+ return void 0;
5467
+ }
5468
+ }
5469
+ function driftOf(stored, repo) {
5470
+ if (!stored) return void 0;
5471
+ const drift = {};
5472
+ if (typeof stored.defaultBranch === "string" && stored.defaultBranch !== repo.defaultBranch) drift.branch = stored.defaultBranch;
5473
+ if (typeof stored.name === "string" && stored.name !== repo.name) drift.name = stored.name;
5474
+ return drift.branch || drift.name ? drift : void 0;
5475
+ }
5476
+ async function registerRepo(deps, args) {
5477
+ const url = `${args.base}/config/api/repositories`;
5478
+ let res;
5479
+ try {
5480
+ res = await deps.fetch(url, {
5481
+ method: "POST",
5482
+ headers: { "content-type": "application/json", authorization: `Bearer ${args.token}` },
5483
+ body: JSON.stringify(args.repo)
5484
+ });
5485
+ } catch (e) {
5486
+ return { kind: "error", message: `could not reach the hub at ${args.base}: ${e.message}` };
5487
+ }
5488
+ const stored = await readJson(res);
5489
+ if (res.status === 200 || res.status === 409) {
5490
+ const drift = driftOf(stored, args.repo);
5491
+ return { kind: "already", ...drift ? { drift } : {} };
5492
+ }
5493
+ if (res.ok) return { kind: "registered" };
5494
+ return { kind: "error", message: `the hub rejected the repo (HTTP ${res.status})` };
5495
+ }
5496
+
5275
5497
  // src/lock.ts
5276
5498
  import { existsSync as existsSync9, mkdirSync as mkdirSync9, readFileSync as readFileSync7, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "fs";
5277
5499
  import { dirname as dirname6 } from "path";
@@ -5681,7 +5903,18 @@ function probeRepo(repoPath) {
5681
5903
  baseBranch = git(["rev-parse", "--abbrev-ref", "HEAD"]) || void 0;
5682
5904
  } catch {
5683
5905
  }
5684
- return { path: repoPath, isGitRepo: true, headResolves, ...baseBranch ? { baseBranch } : {} };
5906
+ let remoteUrl;
5907
+ try {
5908
+ remoteUrl = git(["remote", "get-url", "origin"]) || void 0;
5909
+ } catch {
5910
+ }
5911
+ return {
5912
+ path: repoPath,
5913
+ isGitRepo: true,
5914
+ headResolves,
5915
+ ...baseBranch ? { baseBranch } : {},
5916
+ ...remoteUrl ? { remoteUrl } : {}
5917
+ };
5685
5918
  }
5686
5919
  function gatherHostFacts(repoPath) {
5687
5920
  const root = worktreeRoot(process.env);
@@ -6686,7 +6919,7 @@ async function runStatus(inputs, deps) {
6686
6919
  }
6687
6920
 
6688
6921
  // src/main.ts
6689
- var CLIENT_VERSION = "0.1.18";
6922
+ var CLIENT_VERSION = "0.1.20";
6690
6923
  var DEFAULT_HUB_URL = "wss://api.dahrk.ai";
6691
6924
  var list = (v) => (v ?? "").split(",").map((s) => s.trim()).filter(Boolean);
6692
6925
  var RUNTIMES2 = ["claude-code", "codex", "pi"];
@@ -6773,6 +7006,12 @@ async function start(flags) {
6773
7006
  if (wantsForeground(env, flags)) return startForeground(env, flags);
6774
7007
  const enrolled = await enrolToDisk(env);
6775
7008
  if (enrolled !== 0) return enrolled;
7009
+ if (flags.noService) {
7010
+ out("");
7011
+ out(verdict("ok", "Enrolled. Not installing a service (--no-service)."));
7012
+ out(hint("Run the node with `dahrk start --foreground` (or your own supervisor), or `dahrk start` to install the service."));
7013
+ return 0;
7014
+ }
6776
7015
  await offerUpdate(env);
6777
7016
  const outcome = await runNodeStart(serviceInputs(env));
6778
7017
  if (outcome.kind === "error") return outcome.code;
@@ -7018,6 +7257,69 @@ async function runWorkflow(flags) {
7018
7257
  clientVersion: CLIENT_VERSION
7019
7258
  });
7020
7259
  }
7260
+ async function runRepoAdd(flags) {
7261
+ const env = applyEnvAliases(process.env);
7262
+ if (flags.hubUrl) env.DAHRK_HUB_URL = flags.hubUrl;
7263
+ if (flags.token) env.DAHRK_ENROL_TOKEN = flags.token;
7264
+ const cwd = process.cwd();
7265
+ const probe2 = probeRepo(cwd);
7266
+ if (!probe2.isGitRepo) {
7267
+ out("");
7268
+ out(verdict("fail", `${cwd} is not a git repository.`));
7269
+ out(hint("Run `dahrk repo add` from inside the repository you want to register."));
7270
+ return 1;
7271
+ }
7272
+ if (!probe2.remoteUrl) {
7273
+ out("");
7274
+ out(verdict("fail", "This repository has no `origin` remote, so there is nothing to register."));
7275
+ out(hint("Add one with `git remote add origin <url>`, then run `dahrk repo add` again."));
7276
+ return 1;
7277
+ }
7278
+ const token = resolveEnrolToken(env);
7279
+ if (!token) {
7280
+ out("");
7281
+ out(verdict("fail", "This node is not enrolled, so it cannot register a repository."));
7282
+ out(hint("Enrol it first with `dahrk start --token <token>`."));
7283
+ return 1;
7284
+ }
7285
+ const { gitUrl, converted } = chooseGitUrl({ originUrl: probe2.remoteUrl, sshKeyPresent: sshKeyPresent() });
7286
+ if (converted) {
7287
+ out("");
7288
+ out(verdict("warn", `No SSH key found on this host, so the SSH remote was registered as HTTPS: ${gitUrl}`));
7289
+ out(hint("Add an SSH key (or set up brokered credentials) if this repo must be cloned over SSH - see DHK-252."));
7290
+ }
7291
+ const parsed = parseGitRemote(gitUrl);
7292
+ const name = flags.name ?? (parsed ? deriveRepoName(parsed) : "repo");
7293
+ const repo = {
7294
+ id: deriveRepoId(gitUrl),
7295
+ name,
7296
+ gitUrl,
7297
+ // HEAD's branch is the run's base. An empty repo (no commits) has none yet; default to `main`.
7298
+ defaultBranch: probe2.baseBranch ?? "main"
7299
+ };
7300
+ const base = hubHttpBase(env.DAHRK_HUB_URL ?? DEFAULT_HUB_URL);
7301
+ const result = await registerRepo({ fetch }, { base, token, repo });
7302
+ out("");
7303
+ switch (result.kind) {
7304
+ case "registered":
7305
+ out(verdict("ok", `Registered ${name} with the hub.`));
7306
+ out(kv("URL", gitUrl));
7307
+ out(kv("Branch", repo.defaultBranch));
7308
+ return 0;
7309
+ case "already":
7310
+ out(verdict("ok", `${name} is already registered - nothing to do.`));
7311
+ if (result.drift) {
7312
+ const bits = [];
7313
+ if (result.drift.branch) bits.push(`branch ${result.drift.branch} (this repo is on ${repo.defaultBranch})`);
7314
+ if (result.drift.name) bits.push(`name ${result.drift.name}`);
7315
+ out(hint(`The hub's stored record differs - ${bits.join(", ")} - and was left unchanged.`));
7316
+ }
7317
+ return 0;
7318
+ case "error":
7319
+ out(verdict("fail", `Could not register the repository: ${result.message}`));
7320
+ return 1;
7321
+ }
7322
+ }
7021
7323
  async function main() {
7022
7324
  const invoked = basename2(process.argv[1] ?? "");
7023
7325
  const bin = !invoked || invoked.startsWith("main.") ? "dahrk" : invoked;
@@ -7064,6 +7366,9 @@ async function main() {
7064
7366
  case "run":
7065
7367
  process.exitCode = await runWorkflow(parsed.flags);
7066
7368
  break;
7369
+ case "repo":
7370
+ process.exitCode = await runRepoAdd(parsed.flags);
7371
+ break;
7067
7372
  case "service": {
7068
7373
  if (parsed.flags.action === "uninstall") {
7069
7374
  process.exitCode = await runServiceUninstall();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dahrk-node",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "private": false,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",