@hasna/machines 0.0.45 → 0.0.47

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 (110) hide show
  1. package/README.md +53 -4
  2. package/dist/agent/index.d.ts +0 -1
  3. package/dist/agent/index.js +250 -15
  4. package/dist/agent/runtime.d.ts +0 -1
  5. package/dist/cli/index.d.ts +0 -1
  6. package/dist/cli/index.js +1659 -233
  7. package/dist/cli-utils.d.ts +0 -1
  8. package/dist/commands/apps.d.ts +7 -5
  9. package/dist/commands/backup.d.ts +0 -1
  10. package/dist/commands/cert.d.ts +0 -1
  11. package/dist/commands/clipboard-daemon.d.ts +0 -1
  12. package/dist/commands/clipboard-server.d.ts +0 -1
  13. package/dist/commands/clipboard.d.ts +0 -1
  14. package/dist/commands/daemon.d.ts +0 -1
  15. package/dist/commands/diff.d.ts +0 -1
  16. package/dist/commands/dns.d.ts +0 -1
  17. package/dist/commands/doctor.d.ts +0 -1
  18. package/dist/commands/heal-daemon.d.ts +0 -1
  19. package/dist/commands/heal.d.ts +0 -1
  20. package/dist/commands/hosts.d.ts +81 -0
  21. package/dist/commands/install-claude.d.ts +5 -3
  22. package/dist/commands/install-tailscale.d.ts +5 -3
  23. package/dist/commands/manifest.d.ts +0 -1
  24. package/dist/commands/mutation-approval.d.ts +54 -0
  25. package/dist/commands/notifications.d.ts +14 -2
  26. package/dist/commands/ports.d.ts +0 -1
  27. package/dist/commands/runtime.d.ts +15 -1
  28. package/dist/commands/screen.d.ts +4 -1
  29. package/dist/commands/self-test.d.ts +0 -1
  30. package/dist/commands/serve.d.ts +0 -1
  31. package/dist/commands/setup.d.ts +5 -3
  32. package/dist/commands/ssh.d.ts +8 -1
  33. package/dist/commands/status.d.ts +0 -1
  34. package/dist/commands/sync.d.ts +5 -3
  35. package/dist/commands/workspace.d.ts +0 -1
  36. package/dist/compatibility.d.ts +0 -1
  37. package/dist/consumer-schema.d.ts +0 -1
  38. package/dist/consumer.d.ts +0 -1
  39. package/dist/consumer.js +253 -12
  40. package/dist/cross-project-types.d.ts +0 -1
  41. package/dist/db.d.ts +0 -1
  42. package/dist/index.d.ts +2 -2
  43. package/dist/index.js +1092 -185
  44. package/dist/manifests.d.ts +0 -1
  45. package/dist/mcp/http.d.ts +26 -2
  46. package/dist/mcp/index.d.ts +0 -1
  47. package/dist/mcp/index.js +1004 -162
  48. package/dist/mcp/server.d.ts +5 -3
  49. package/dist/paths.d.ts +0 -1
  50. package/dist/pg-migrations.d.ts +0 -1
  51. package/dist/redaction.d.ts +0 -1
  52. package/dist/remote-storage.d.ts +0 -1
  53. package/dist/remote.d.ts +14 -5
  54. package/dist/storage-sync.d.ts +0 -1
  55. package/dist/storage.d.ts +0 -1
  56. package/dist/storage.js +18 -0
  57. package/dist/topology.d.ts +0 -1
  58. package/dist/types.d.ts +3 -1
  59. package/dist/version.d.ts +0 -1
  60. package/package.json +5 -3
  61. package/dist/agent/index.d.ts.map +0 -1
  62. package/dist/agent/runtime.d.ts.map +0 -1
  63. package/dist/cli/index.d.ts.map +0 -1
  64. package/dist/cli-utils.d.ts.map +0 -1
  65. package/dist/commands/apps.d.ts.map +0 -1
  66. package/dist/commands/backup.d.ts.map +0 -1
  67. package/dist/commands/cert.d.ts.map +0 -1
  68. package/dist/commands/clipboard-daemon.d.ts.map +0 -1
  69. package/dist/commands/clipboard-server.d.ts.map +0 -1
  70. package/dist/commands/clipboard.d.ts.map +0 -1
  71. package/dist/commands/daemon.d.ts.map +0 -1
  72. package/dist/commands/diff.d.ts.map +0 -1
  73. package/dist/commands/dns.d.ts.map +0 -1
  74. package/dist/commands/doctor.d.ts.map +0 -1
  75. package/dist/commands/heal-daemon.d.ts.map +0 -1
  76. package/dist/commands/heal.d.ts.map +0 -1
  77. package/dist/commands/install-claude.d.ts.map +0 -1
  78. package/dist/commands/install-tailscale.d.ts.map +0 -1
  79. package/dist/commands/manifest.d.ts.map +0 -1
  80. package/dist/commands/notifications.d.ts.map +0 -1
  81. package/dist/commands/ports.d.ts.map +0 -1
  82. package/dist/commands/runtime.d.ts.map +0 -1
  83. package/dist/commands/screen.d.ts.map +0 -1
  84. package/dist/commands/self-test.d.ts.map +0 -1
  85. package/dist/commands/serve.d.ts.map +0 -1
  86. package/dist/commands/setup.d.ts.map +0 -1
  87. package/dist/commands/ssh.d.ts.map +0 -1
  88. package/dist/commands/status.d.ts.map +0 -1
  89. package/dist/commands/sync.d.ts.map +0 -1
  90. package/dist/commands/workspace.d.ts.map +0 -1
  91. package/dist/compatibility.d.ts.map +0 -1
  92. package/dist/consumer-schema.d.ts.map +0 -1
  93. package/dist/consumer.d.ts.map +0 -1
  94. package/dist/cross-project-types.d.ts.map +0 -1
  95. package/dist/db.d.ts.map +0 -1
  96. package/dist/index.d.ts.map +0 -1
  97. package/dist/manifests.d.ts.map +0 -1
  98. package/dist/mcp/http.d.ts.map +0 -1
  99. package/dist/mcp/index.d.ts.map +0 -1
  100. package/dist/mcp/server.d.ts.map +0 -1
  101. package/dist/paths.d.ts.map +0 -1
  102. package/dist/pg-migrations.d.ts.map +0 -1
  103. package/dist/redaction.d.ts.map +0 -1
  104. package/dist/remote-storage.d.ts.map +0 -1
  105. package/dist/remote.d.ts.map +0 -1
  106. package/dist/storage-sync.d.ts.map +0 -1
  107. package/dist/storage.d.ts.map +0 -1
  108. package/dist/topology.d.ts.map +0 -1
  109. package/dist/types.d.ts.map +0 -1
  110. package/dist/version.d.ts.map +0 -1
package/README.md CHANGED
@@ -23,6 +23,12 @@ Endpoints on `127.0.0.1` only:
23
23
  - `GET /health` → `{"status":"ok","name":"machines"}`
24
24
  - `POST /mcp` → MCP Streamable HTTP
25
25
 
26
+ HTTP mode rejects browser requests with untrusted `Origin` headers, caps JSON
27
+ bodies at `MACHINES_HTTP_MAX_BODY_BYTES` (default 1 MiB), and requires either
28
+ `MACHINES_API_KEY` or loopback-only `MACHINES_ALLOW_UNAUTHENTICATED=1`. Use
29
+ `MACHINES_HTTP_ALLOWED_ORIGINS=https://ops.example` for an explicit browser
30
+ origin allowlist.
31
+
26
32
  ## Manifest
27
33
 
28
34
  `machines.json` is the desired fleet declaration.
@@ -373,6 +379,7 @@ machines install-tailscale --machine mac-lab-01 --json
373
379
 
374
380
  ```bash
375
381
  machines notifications add --id ops --type webhook --target https://example.com/hook --event sync_failed
382
+ machines notifications add --id cmd --type command --target /bin/sh --arg -c --arg 'printf "%s\n" "$HASNA_MACHINES_NOTIFICATION_EVENT"'
376
383
  machines notifications list
377
384
  machines notifications test --channel ops
378
385
  machines notifications test --channel ops --apply --yes
@@ -381,7 +388,9 @@ machines notifications dispatch --event manual.test --message "hello fleet"
381
388
 
382
389
  - `email` channels deliver through local `sendmail` or `mail` when available
383
390
  - `webhook` channels deliver JSON via HTTP POST
384
- - `command` channels execute the configured command with `HASNA_MACHINES_NOTIFICATION_*` env vars
391
+ - `command` channels execute an explicit command executable plus optional `--arg`
392
+ values with `HASNA_MACHINES_NOTIFICATION_*` env vars; use `/bin/sh -c ...`
393
+ explicitly if a shell is required
385
394
 
386
395
  ## Runtime Events
387
396
 
@@ -390,19 +399,57 @@ events without sending keys, killing panes, or changing tmux state.
390
399
 
391
400
  ```bash
392
401
  machines runtime tmux-watch %11 --once --json
393
- machines runtime tmux-watch session:0.1 --interval-ms 5000
402
+ machines runtime tmux-watch session:0.1 --interval-ms 5000 --approval-token "$TOKEN"
403
+ machines runtime tmux-hook-plan --trusted-local-mutation --json
404
+ machines runtime tmux-hook-plan --approval-token "$TOKEN"
394
405
  machines webhooks add https://example.com/hook --id tmux-alerts --type machines.tmux.pane_died
395
406
  ```
396
407
 
397
408
  When a pane was present and later disappears, the command records
398
409
  `machines.tmux.pane_died`. With `--once`, a missing pane records
399
410
  `machines.tmux.pane_missing`; add `--no-deliver` to record without webhook
400
- delivery.
411
+ delivery. Runtime event delivery requires a scoped mutation approval token; local
412
+ no-deliver recording remains available for diagnostics.
413
+
414
+ `machines runtime tmux-hook-plan` prints a native tmux `pane-died` hook command
415
+ for operators that prefer tmux hooks over polling. It is read-only and does not
416
+ install hooks. Pass `--approval-token` when you want the generated hook command
417
+ to be scoped to a short-lived approval token, or pass
418
+ `--trusted-local-mutation` to generate a process-local
419
+ `HASNA_MACHINES_ALLOW_MUTATIONS=1` prefix for local event recording.
420
+
421
+ ## Fleet hostnames (`machines hosts`)
422
+
423
+ Make every fleet machine reachable by its bare name from any other machine —
424
+ `curl http://machine001:3000` works the same on every box — without depending on
425
+ Tailscale MagicDNS being configured. `machines hosts` writes a managed block into
426
+ `/etc/hosts` for each machine in the manifest, choosing the best address:
427
+
428
+ 1. `metadata.lanAddress` from the manifest, when it is on the local machine's `/24`
429
+ 2. the peer's live direct Tailscale LAN endpoint (`CurAddr`) on the local `/24`
430
+ 3. the peer's tailnet IP (`100.64.0.0/10`) — always routable, auto-routed over the
431
+ LAN when co-located
432
+
433
+ ```bash
434
+ machines hosts # dry-run plan (default)
435
+ machines hosts plan -j # JSON plan
436
+ machines hosts apply # write /etc/hosts (uses sudo when the file is root-owned)
437
+ machines hosts plan --no-warm # skip discovering LAN endpoints (faster, tailnet IPs)
438
+ ```
439
+
440
+ By default the command first runs `tailscale ping` against online peers so their
441
+ LAN endpoints become visible and same-LAN machines resolve to their `192.168.x.x`
442
+ address (true LAN-direct) instead of the tailnet IP. Off-LAN or offline peers fall
443
+ back to the tailnet IP. The local machine is skipped. The managed block is delimited
444
+ by markers, so re-running `apply` only rewrites that block and leaves the rest of
445
+ `/etc/hosts` untouched.
401
446
 
402
447
  ## Dashboard
403
448
 
404
449
  ```bash
405
450
  machines serve --json
451
+ machines serve --port 7676
452
+ # Explicitly expose beyond loopback only on a trusted network:
406
453
  machines serve --host 0.0.0.0 --port 7676
407
454
  ```
408
455
 
@@ -416,13 +463,15 @@ The dashboard exposes:
416
463
  - `/api/daemon/status` daemon heartbeat rows
417
464
  - `/api/manifest` current manifest JSON
418
465
  - `/api/notifications` notification channel JSON
466
+ - `/api/webhooks` shared event webhook channel JSON
467
+ - `/api/events` shared event JSON
419
468
  - `/api/doctor` doctor report JSON
420
469
  - `/api/self-test` smoke-check JSON
421
470
  - `/api/apps/status` app inventory JSON
422
471
  - `/api/apps/diff` app drift JSON
423
472
  - `/api/install-claude/status` CLI inventory JSON
424
473
  - `/api/install-claude/diff` CLI drift JSON
425
- - `/api/notifications/test` POST endpoint for test delivery
474
+ - `/api/events`, `/api/notifications/test`, `/api/webhooks/test` POST mutation routes require scoped dashboard mutation approval tokens
426
475
 
427
476
  ## Local development
428
477
 
@@ -1,3 +1,2 @@
1
1
  #!/usr/bin/env bun
2
2
  export {};
3
- //# sourceMappingURL=index.d.ts.map
@@ -992,7 +992,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
992
992
  this._exitCallback = (err) => {
993
993
  if (err.code !== "commander.executeSubCommandAsync") {
994
994
  throw err;
995
- }
995
+ } else {}
996
996
  };
997
997
  }
998
998
  return this;
@@ -6999,6 +6999,7 @@ class SqliteAdapter {
6999
6999
  raw;
7000
7000
  constructor(path) {
7001
7001
  this.raw = new Database(path);
7002
+ this.raw.exec("PRAGMA busy_timeout = 5000");
7002
7003
  }
7003
7004
  close() {
7004
7005
  this.raw.close();
@@ -7064,6 +7065,23 @@ function createTables(db) {
7064
7065
  updated_at TEXT NOT NULL
7065
7066
  )
7066
7067
  `);
7068
+ db.exec(`
7069
+ CREATE TABLE IF NOT EXISTS mutation_approval_nonces (
7070
+ nonce_sha256 TEXT PRIMARY KEY,
7071
+ token_sha256 TEXT NOT NULL,
7072
+ surface TEXT NOT NULL,
7073
+ operation TEXT NOT NULL,
7074
+ caller_id TEXT NOT NULL,
7075
+ run_id TEXT NOT NULL,
7076
+ transport TEXT NOT NULL,
7077
+ expires_at INTEGER NOT NULL,
7078
+ used_at INTEGER NOT NULL
7079
+ )
7080
+ `);
7081
+ db.exec(`
7082
+ CREATE INDEX IF NOT EXISTS mutation_approval_nonces_expires_at_idx
7083
+ ON mutation_approval_nonces (expires_at)
7084
+ `);
7067
7085
  }
7068
7086
  function migrateAgentHeartbeats(db) {
7069
7087
  const columns = db.query("PRAGMA table_info(agent_heartbeats)").all();
@@ -11387,7 +11405,9 @@ function readManifestWithSource(options = {}) {
11387
11405
 
11388
11406
  // src/remote.ts
11389
11407
  import { spawnSync as spawnSync2 } from "child_process";
11390
- import { hostname as hostname3 } from "os";
11408
+ import { existsSync as existsSync5, mkdtempSync, readFileSync as readFileSync3, rmSync } from "fs";
11409
+ import { hostname as hostname3, tmpdir } from "os";
11410
+ import { join as join3 } from "path";
11391
11411
 
11392
11412
  // src/topology.ts
11393
11413
  import { existsSync as existsSync4 } from "fs";
@@ -11845,6 +11865,16 @@ function resolveMachineRoute(machineId, options = {}) {
11845
11865
  function shellQuote(value) {
11846
11866
  return `'${value.replace(/'/g, "'\\''")}'`;
11847
11867
  }
11868
+ function validateSshTarget(target) {
11869
+ const trimmed = target.trim();
11870
+ if (!trimmed || trimmed.startsWith("-") || /[\s"'`$\\;&|<>()[\]{}]/.test(trimmed)) {
11871
+ throw new Error(`Unsafe SSH target: ${target}`);
11872
+ }
11873
+ if (!/^(?:[A-Za-z0-9._%+-]+@)?[A-Za-z0-9._:-]+$/.test(trimmed)) {
11874
+ throw new Error(`Unsafe SSH target: ${target}`);
11875
+ }
11876
+ return trimmed;
11877
+ }
11848
11878
  function resolveSshTarget(machineId, options = {}) {
11849
11879
  const resolved = resolveMachineRoute(machineId, options);
11850
11880
  if (!resolved.ok || !resolved.target) {
@@ -11855,15 +11885,22 @@ function resolveSshTarget(machineId, options = {}) {
11855
11885
  }
11856
11886
  return {
11857
11887
  machineId: resolved.machine_id ?? machineId,
11858
- target: resolved.command_target ?? resolved.target,
11888
+ target: validateSshTarget(resolved.command_target ?? resolved.target),
11859
11889
  route: resolved.route,
11860
11890
  confidence: resolved.confidence,
11861
11891
  warnings: resolved.warnings
11862
11892
  };
11863
11893
  }
11864
- function buildSshCommand(machineId, remoteCommand, options = {}) {
11894
+ function buildSshCommandPlan(machineId, remoteCommand, options = {}) {
11865
11895
  const resolved = resolveSshTarget(machineId, options);
11866
- return remoteCommand ? `ssh ${resolved.target} ${shellQuote(remoteCommand)}` : `ssh ${resolved.target}`;
11896
+ const args = remoteCommand ? [resolved.target, remoteCommand] : [resolved.target];
11897
+ const shellCommand = `ssh ${args.map(shellQuote).join(" ")}`;
11898
+ return {
11899
+ ...resolved,
11900
+ command: "ssh",
11901
+ args,
11902
+ shellCommand
11903
+ };
11867
11904
  }
11868
11905
 
11869
11906
  // src/remote.ts
@@ -11875,35 +11912,233 @@ function machineIsLocal(machineId, localMachineId) {
11875
11912
  }
11876
11913
  function resolveMachineCommand(machineId, command, localMachineId = getLocalMachineId()) {
11877
11914
  if (machineIsLocal(machineId, localMachineId)) {
11878
- return { source: "local", shellCommand: command };
11915
+ return { source: "local", command: "bash", args: ["-c", command], shellCommand: command, usesShell: true };
11879
11916
  }
11880
11917
  try {
11918
+ const plan = buildSshCommandPlan(machineId, command);
11881
11919
  return {
11882
- source: resolveSshTarget(machineId).route,
11883
- shellCommand: buildSshCommand(machineId, command)
11920
+ source: plan.route,
11921
+ command: plan.command,
11922
+ args: plan.args,
11923
+ shellCommand: plan.shellCommand,
11924
+ usesShell: false
11884
11925
  };
11885
11926
  } catch (error) {
11886
11927
  const message = String(error.message ?? error);
11887
11928
  if (message.includes("Machine route not found") || message.includes("Machine not found in manifest")) {
11888
- return { source: "ssh", shellCommand: `ssh ${shellQuote2(machineId)} ${shellQuote2(command)}` };
11929
+ const target = validateSshTarget(machineId);
11930
+ return {
11931
+ source: "ssh",
11932
+ command: "ssh",
11933
+ args: [target, command],
11934
+ shellCommand: `ssh ${shellQuote2(target)} ${shellQuote2(command)}`,
11935
+ usesShell: false
11936
+ };
11889
11937
  }
11890
11938
  throw error;
11891
11939
  }
11892
11940
  }
11893
- function runMachineCommand(machineId, command) {
11941
+ function runMachineCommand(machineId, command, options = {}) {
11894
11942
  const resolved = resolveMachineCommand(machineId, command);
11895
- const result = spawnSync2("bash", ["-c", resolved.shellCommand], {
11943
+ if (options.timeoutMs && options.timeoutMs > 0 && process.platform !== "win32") {
11944
+ return runMachineCommandWithProcessGroupTimeout(machineId, resolved, options);
11945
+ }
11946
+ const result = spawnSync2(resolved.command, resolved.args, {
11896
11947
  encoding: "utf8",
11897
- env: process.env
11948
+ env: process.env,
11949
+ timeout: options.timeoutMs,
11950
+ killSignal: "SIGTERM"
11898
11951
  });
11952
+ const timedOut = Boolean(result.error && "code" in result.error && result.error.code === "ETIMEDOUT");
11953
+ const timeoutMessage = timedOut ? `Command timed out after ${options.timeoutMs}ms.` : "";
11954
+ const stderr = [result.stderr || "", timeoutMessage].filter(Boolean).join(result.stderr ? `
11955
+ ` : "");
11899
11956
  return {
11900
11957
  machineId,
11901
11958
  source: resolved.source,
11902
11959
  stdout: result.stdout || "",
11903
- stderr: result.stderr || "",
11904
- exitCode: result.status ?? 1
11905
- };
11960
+ stderr,
11961
+ exitCode: timedOut ? 124 : result.status ?? 1,
11962
+ timedOut,
11963
+ signal: result.signal
11964
+ };
11965
+ }
11966
+ function runMachineCommandWithProcessGroupTimeout(machineId, resolved, options) {
11967
+ const timeoutMs = Math.max(1, options.timeoutMs ?? 1);
11968
+ const killGraceMs = Math.max(1, options.killGraceMs ?? 1000);
11969
+ const helperDir = mkdtempSync(join3(tmpdir(), "machines-timeout-helper-"));
11970
+ const pgidFile = join3(helperDir, "pgid");
11971
+ const helper = spawnSync2(process.execPath, ["--eval", PROCESS_GROUP_TIMEOUT_HELPER], {
11972
+ input: JSON.stringify({ command: resolved.command, args: resolved.args }),
11973
+ encoding: "utf8",
11974
+ env: {
11975
+ ...process.env,
11976
+ HASNA_MACHINES_COMMAND_TIMEOUT_MS: String(timeoutMs),
11977
+ HASNA_MACHINES_COMMAND_KILL_GRACE_MS: String(killGraceMs),
11978
+ HASNA_MACHINES_COMMAND_PGID_FILE: pgidFile
11979
+ },
11980
+ timeout: timeoutMs + killGraceMs + 2000,
11981
+ killSignal: "SIGKILL",
11982
+ maxBuffer: 64 * 1024 * 1024
11983
+ });
11984
+ try {
11985
+ const parsed = parseHelperResult(helper.stdout);
11986
+ if (parsed) {
11987
+ return {
11988
+ machineId,
11989
+ source: resolved.source,
11990
+ stdout: parsed.stdout,
11991
+ stderr: parsed.stderr,
11992
+ exitCode: parsed.exitCode,
11993
+ timedOut: parsed.timedOut,
11994
+ signal: parsed.signal
11995
+ };
11996
+ }
11997
+ const helperTimedOut = Boolean(helper.error && "code" in helper.error && helper.error.code === "ETIMEDOUT");
11998
+ if (helperTimedOut)
11999
+ killPublishedProcessGroup(pgidFile);
12000
+ const timeoutMessage = helperTimedOut ? `Command timed out after ${timeoutMs}ms; timeout helper exceeded cleanup grace ${killGraceMs}ms.` : "";
12001
+ const stderr = [helper.stderr || "", timeoutMessage].filter(Boolean).join(helper.stderr ? `
12002
+ ` : "");
12003
+ return {
12004
+ machineId,
12005
+ source: resolved.source,
12006
+ stdout: "",
12007
+ stderr,
12008
+ exitCode: helperTimedOut ? 124 : helper.status ?? 1,
12009
+ timedOut: helperTimedOut,
12010
+ signal: helper.signal
12011
+ };
12012
+ } finally {
12013
+ rmSync(helperDir, { recursive: true, force: true });
12014
+ }
12015
+ }
12016
+ function killPublishedProcessGroup(pgidFile) {
12017
+ if (!existsSync5(pgidFile))
12018
+ return;
12019
+ try {
12020
+ const pid = Number.parseInt(readFileSync3(pgidFile, "utf8").trim(), 10);
12021
+ if (!Number.isInteger(pid) || pid <= 1)
12022
+ return;
12023
+ process.kill(-pid, "SIGKILL");
12024
+ } catch {}
11906
12025
  }
12026
+ function parseHelperResult(stdout) {
12027
+ if (!stdout)
12028
+ return null;
12029
+ try {
12030
+ const parsed = JSON.parse(stdout);
12031
+ if (typeof parsed.stdout !== "string" || typeof parsed.stderr !== "string" || typeof parsed.exitCode !== "number")
12032
+ return null;
12033
+ return {
12034
+ machineId: "",
12035
+ source: "local",
12036
+ stdout: parsed.stdout,
12037
+ stderr: parsed.stderr,
12038
+ exitCode: parsed.exitCode,
12039
+ timedOut: parsed.timedOut === true,
12040
+ signal: typeof parsed.signal === "string" ? parsed.signal : null
12041
+ };
12042
+ } catch {
12043
+ return null;
12044
+ }
12045
+ }
12046
+ var PROCESS_GROUP_TIMEOUT_HELPER = `
12047
+ const { spawn } = require("node:child_process");
12048
+ const { readFileSync, writeFileSync } = require("node:fs");
12049
+
12050
+ const plan = JSON.parse(readFileSync(0, "utf8"));
12051
+ const command = String(plan.command || "");
12052
+ const args = Array.isArray(plan.args) ? plan.args.map(String) : [];
12053
+ const timeoutMs = Math.max(1, Number.parseInt(process.env.HASNA_MACHINES_COMMAND_TIMEOUT_MS || "1", 10));
12054
+ const killGraceMs = Math.max(1, Number.parseInt(process.env.HASNA_MACHINES_COMMAND_KILL_GRACE_MS || "1000", 10));
12055
+ const pgidFile = process.env.HASNA_MACHINES_COMMAND_PGID_FILE || "";
12056
+ let stdout = "";
12057
+ let stderr = "";
12058
+ let timedOut = false;
12059
+ let finished = false;
12060
+ let timeoutTimer;
12061
+ let killTimer;
12062
+ let sigkillSent = false;
12063
+ let pendingExit = null;
12064
+
12065
+ const child = spawn(command, args, {
12066
+ detached: true,
12067
+ stdio: ["ignore", "pipe", "pipe"],
12068
+ env: process.env,
12069
+ });
12070
+
12071
+ if (pgidFile && child.pid) {
12072
+ try {
12073
+ writeFileSync(pgidFile, String(child.pid), { mode: 0o600 });
12074
+ } catch {}
12075
+ }
12076
+
12077
+ function appendText(target, chunk) {
12078
+ return target + String(chunk);
12079
+ }
12080
+
12081
+ function killTarget(signal) {
12082
+ if (!child.pid) return;
12083
+ if (process.platform === "win32") {
12084
+ try {
12085
+ process.kill(child.pid, signal);
12086
+ } catch {}
12087
+ return;
12088
+ }
12089
+ try {
12090
+ process.kill(-child.pid, signal);
12091
+ } catch {}
12092
+ }
12093
+
12094
+ function finish(code, signal) {
12095
+ if (finished) return;
12096
+ if (timedOut && !sigkillSent) {
12097
+ pendingExit = { code, signal };
12098
+ return;
12099
+ }
12100
+ finished = true;
12101
+ if (timeoutTimer) clearTimeout(timeoutTimer);
12102
+ if (killTimer) clearTimeout(killTimer);
12103
+ if (timedOut) {
12104
+ stderr = [stderr, "Command timed out after " + timeoutMs + "ms."].filter(Boolean).join(stderr ? "\\n" : "");
12105
+ }
12106
+ const exitCode = timedOut ? 124 : code ?? 1;
12107
+ process.stdout.write(JSON.stringify({
12108
+ stdout,
12109
+ stderr,
12110
+ exitCode,
12111
+ timedOut,
12112
+ signal: signal ?? null,
12113
+ }), () => process.exit(exitCode));
12114
+ }
12115
+
12116
+ child.stdout.setEncoding("utf8");
12117
+ child.stderr.setEncoding("utf8");
12118
+ child.stdout.on("data", (chunk) => { stdout = appendText(stdout, chunk); });
12119
+ child.stderr.on("data", (chunk) => { stderr = appendText(stderr, chunk); });
12120
+ let childExit = { code: null, signal: null };
12121
+ child.on("error", (error) => {
12122
+ stderr = [stderr, error instanceof Error ? error.message : String(error)].filter(Boolean).join(stderr ? "\\n" : "");
12123
+ finish(1, null);
12124
+ });
12125
+ child.on("exit", (code, signal) => {
12126
+ childExit = { code, signal };
12127
+ });
12128
+ child.on("close", (code, signal) => {
12129
+ finish(code ?? childExit.code, signal ?? childExit.signal);
12130
+ });
12131
+
12132
+ timeoutTimer = setTimeout(() => {
12133
+ timedOut = true;
12134
+ killTarget("SIGTERM");
12135
+ killTimer = setTimeout(() => {
12136
+ sigkillSent = true;
12137
+ killTarget("SIGKILL");
12138
+ if (pendingExit) finish(pendingExit.code, pendingExit.signal);
12139
+ }, killGraceMs);
12140
+ }, timeoutMs);
12141
+ `;
11907
12142
 
11908
12143
  // src/commands/doctor.ts
11909
12144
  var DOCTOR_OPTIONAL_ADAPTER_DOMAINS = ["secrets", "configs", "monitor", "repos", "mcps", "shield"];
@@ -37,4 +37,3 @@ export declare function writeHeartbeat(status?: "online" | "offline", options?:
37
37
  export declare function writeHeartbeatTick(status?: "online" | "offline", options?: AgentTickOptions): Promise<AgentRuntimeStatus>;
38
38
  export declare function markOffline(options?: AgentRuntimeOptions): AgentRuntimeStatus;
39
39
  export declare function getAgentStatus(machineId?: string, options?: AgentStatusOptions): AgentRuntimeStatus[];
40
- //# sourceMappingURL=runtime.d.ts.map
@@ -1,3 +1,2 @@
1
1
  #!/usr/bin/env bun
2
2
  export {};
3
- //# sourceMappingURL=index.d.ts.map