@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/dist/cli/index.js CHANGED
@@ -993,7 +993,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
993
993
  this._exitCallback = (err) => {
994
994
  if (err.code !== "commander.executeSubCommandAsync") {
995
995
  throw err;
996
- }
996
+ } else {}
997
997
  };
998
998
  }
999
999
  return this;
@@ -2123,6 +2123,7 @@ class SqliteAdapter {
2123
2123
  raw;
2124
2124
  constructor(path) {
2125
2125
  this.raw = new Database(path);
2126
+ this.raw.exec("PRAGMA busy_timeout = 5000");
2126
2127
  }
2127
2128
  close() {
2128
2129
  this.raw.close();
@@ -2172,6 +2173,23 @@ function createTables(db) {
2172
2173
  updated_at TEXT NOT NULL
2173
2174
  )
2174
2175
  `);
2176
+ db.exec(`
2177
+ CREATE TABLE IF NOT EXISTS mutation_approval_nonces (
2178
+ nonce_sha256 TEXT PRIMARY KEY,
2179
+ token_sha256 TEXT NOT NULL,
2180
+ surface TEXT NOT NULL,
2181
+ operation TEXT NOT NULL,
2182
+ caller_id TEXT NOT NULL,
2183
+ run_id TEXT NOT NULL,
2184
+ transport TEXT NOT NULL,
2185
+ expires_at INTEGER NOT NULL,
2186
+ used_at INTEGER NOT NULL
2187
+ )
2188
+ `);
2189
+ db.exec(`
2190
+ CREATE INDEX IF NOT EXISTS mutation_approval_nonces_expires_at_idx
2191
+ ON mutation_approval_nonces (expires_at)
2192
+ `);
2175
2193
  }
2176
2194
  function migrateAgentHeartbeats(db) {
2177
2195
  const columns = db.query("PRAGMA table_info(agent_heartbeats)").all();
@@ -2689,8 +2707,15 @@ var {
2689
2707
  } = import__.default;
2690
2708
 
2691
2709
  // src/cli/index.ts
2692
- import { registerEventCommands, registerWebhookCommands } from "@hasna/events/commander";
2710
+ import {
2711
+ EventsClient as EventsClient3,
2712
+ getEventsDataDir as getEventsDataDir2,
2713
+ sanitizeChannelForOutput,
2714
+ sanitizeChannelsForOutput as sanitizeChannelsForOutput2
2715
+ } from "@hasna/events";
2693
2716
  import { execFileSync as execFileSync2 } from "child_process";
2717
+ import { existsSync as existsSync14, readFileSync as readFileSync14, rmSync as rmSync3 } from "fs";
2718
+ import { join as join12, resolve as resolve4 } from "path";
2694
2719
 
2695
2720
  // node_modules/chalk/source/vendor/ansi-styles/index.js
2696
2721
  var ANSI_BACKGROUND_OFFSET = 10;
@@ -7521,10 +7546,271 @@ function manifestValidate() {
7521
7546
  import { homedir as homedir2 } from "os";
7522
7547
  init_db();
7523
7548
 
7549
+ // src/commands/mutation-approval.ts
7550
+ init_db();
7551
+ import { createHash, createHmac, randomUUID, timingSafeEqual } from "crypto";
7552
+ import { resolve as resolve2 } from "path";
7553
+ var MUTATION_APPROVAL_FLAG_ENV = "HASNA_MACHINES_ALLOW_MUTATIONS";
7554
+ var LEGACY_MUTATION_APPROVAL_FLAG_ENV = "HASNA_MACHINES_MUTATION_APPROVAL";
7555
+ var MUTATION_APPROVAL_TOKEN_ENV = "HASNA_MACHINES_MUTATION_TOKEN";
7556
+ var MUTATION_APPROVAL_CALLER_ENV = "HASNA_MACHINES_MUTATION_CALLER_ID";
7557
+ var MUTATION_APPROVAL_RUN_ENV = "HASNA_MACHINES_MUTATION_RUN_ID";
7558
+ var MUTATION_APPROVAL_REPLAY_PATH_ENV = "HASNA_MACHINES_MUTATION_REPLAY_PATH";
7559
+ var TOKEN_PREFIX = "machines-mut-v1";
7560
+ var DEFAULT_TOKEN_TTL_MS = 5 * 60 * 1000;
7561
+ var MAX_TOKEN_TTL_MS = 5 * 60 * 1000;
7562
+ var MAX_CLOCK_SKEW_MS = 30000;
7563
+ function isTruthy(value) {
7564
+ return value === "1" || value?.toLowerCase() === "true" || value?.toLowerCase() === "yes";
7565
+ }
7566
+ function nowMs(now) {
7567
+ if (typeof now === "number")
7568
+ return now;
7569
+ if (now instanceof Date)
7570
+ return now.getTime();
7571
+ return Date.now();
7572
+ }
7573
+ function signingSecret(env2, explicitSecret) {
7574
+ return explicitSecret?.trim() || env2[MUTATION_APPROVAL_TOKEN_ENV]?.trim();
7575
+ }
7576
+ function hmac(payload, secret) {
7577
+ return createHmac("sha256", secret).update(payload).digest("base64url");
7578
+ }
7579
+ function sha256Hex(payload) {
7580
+ return createHash("sha256").update(payload).digest("hex");
7581
+ }
7582
+ function replayDbPath(env2) {
7583
+ const configured = env2[MUTATION_APPROVAL_REPLAY_PATH_ENV]?.trim();
7584
+ return configured ? resolve2(configured) : undefined;
7585
+ }
7586
+ function replayNonceKey(claims) {
7587
+ return sha256Hex(JSON.stringify({ nonce: claims.nonce }));
7588
+ }
7589
+ function recordReplayNonce(env2, claims, tokenPayload, now) {
7590
+ const dbPath = replayDbPath(env2);
7591
+ if (!dbPath)
7592
+ return;
7593
+ if (!claims.nonce) {
7594
+ return { approved: false, reason: "approval_token nonce claim is required for replay protection." };
7595
+ }
7596
+ try {
7597
+ const db = getDb(dbPath);
7598
+ db.query("DELETE FROM mutation_approval_nonces WHERE expires_at <= ?").run(now);
7599
+ const result = db.query(`
7600
+ INSERT OR IGNORE INTO mutation_approval_nonces (
7601
+ nonce_sha256,
7602
+ token_sha256,
7603
+ surface,
7604
+ operation,
7605
+ caller_id,
7606
+ run_id,
7607
+ transport,
7608
+ expires_at,
7609
+ used_at
7610
+ )
7611
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
7612
+ `).run(replayNonceKey(claims), sha256Hex(tokenPayload), claims.surface, claims.operation, claims.callerId ?? "", claims.runId ?? "", claims.transport ?? "", claims.expiresAt, now);
7613
+ if (result.changes !== 1) {
7614
+ return { approved: false, reason: "approval_token nonce has already been used." };
7615
+ }
7616
+ return;
7617
+ } catch (error) {
7618
+ const message = error instanceof Error ? error.message : String(error);
7619
+ return { approved: false, reason: `approval_token replay store is unavailable: ${message}` };
7620
+ }
7621
+ }
7622
+ function safeEqual(left, right) {
7623
+ const leftBuffer = Buffer.from(left);
7624
+ const rightBuffer = Buffer.from(right);
7625
+ return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer);
7626
+ }
7627
+ function canonicalizeMutationArg(value, inArray = false) {
7628
+ if (value === undefined)
7629
+ return inArray ? null : undefined;
7630
+ if (value === null || typeof value === "boolean" || typeof value === "string")
7631
+ return value;
7632
+ if (typeof value === "number")
7633
+ return Number.isFinite(value) ? value : null;
7634
+ if (Array.isArray(value)) {
7635
+ return value.map((entry) => canonicalizeMutationArg(entry, true) ?? null);
7636
+ }
7637
+ if (value instanceof Date)
7638
+ return value.toISOString();
7639
+ if (typeof value === "object") {
7640
+ const result = {};
7641
+ for (const key of Object.keys(value).sort()) {
7642
+ if (key === "approval_token" || key === "approvalToken")
7643
+ continue;
7644
+ const canonicalValue = canonicalizeMutationArg(value[key]);
7645
+ if (canonicalValue !== undefined)
7646
+ result[key] = canonicalValue;
7647
+ }
7648
+ return result;
7649
+ }
7650
+ return inArray ? null : undefined;
7651
+ }
7652
+ function canonicalMutationArgs(value) {
7653
+ return JSON.stringify(canonicalizeMutationArg(value) ?? {});
7654
+ }
7655
+ function mutationArgsSha256(value) {
7656
+ return sha256Hex(canonicalMutationArgs(value));
7657
+ }
7658
+ function stripPlanRuntimeFields(value) {
7659
+ if (Array.isArray(value))
7660
+ return value.map(stripPlanRuntimeFields);
7661
+ if (value instanceof Date)
7662
+ return value;
7663
+ if (value && typeof value === "object") {
7664
+ const result = {};
7665
+ for (const [key, entry] of Object.entries(value)) {
7666
+ if (key === "planDigest" || key === "plan_digest" || key === "mode" || key === "executed")
7667
+ continue;
7668
+ result[key] = stripPlanRuntimeFields(entry);
7669
+ }
7670
+ return result;
7671
+ }
7672
+ return value;
7673
+ }
7674
+ function mutationPlanDigest(plan) {
7675
+ return mutationArgsSha256(stripPlanRuntimeFields(plan));
7676
+ }
7677
+ function attachMutationPlanDigest(plan) {
7678
+ return {
7679
+ ...plan,
7680
+ planDigest: mutationPlanDigest(plan)
7681
+ };
7682
+ }
7683
+ function assertMutationPlanDigest(plan, expectedPlanDigest) {
7684
+ if (expectedPlanDigest && mutationPlanDigest(plan) !== expectedPlanDigest) {
7685
+ throw new Error("Approved plan digest does not match the current execution plan.");
7686
+ }
7687
+ }
7688
+ function parseToken(token) {
7689
+ if (!token)
7690
+ return null;
7691
+ const parts = token.split(".");
7692
+ if (parts.length !== 3 || parts[0] !== TOKEN_PREFIX)
7693
+ return null;
7694
+ try {
7695
+ const claims = JSON.parse(Buffer.from(parts[1] ?? "", "base64url").toString("utf8"));
7696
+ return { payload: parts[1] ?? "", signature: parts[2] ?? "", claims };
7697
+ } catch {
7698
+ return null;
7699
+ }
7700
+ }
7701
+ function claimMatches(expected, actual) {
7702
+ if (expected === undefined)
7703
+ return actual === undefined;
7704
+ return actual === expected;
7705
+ }
7706
+ function verifyMutationApprovalToken(options) {
7707
+ const env2 = options.env ?? process.env;
7708
+ const secret = signingSecret(env2);
7709
+ if (!secret)
7710
+ return { approved: false, reason: `${MUTATION_APPROVAL_TOKEN_ENV} is not configured.` };
7711
+ const parsed = parseToken(options.approvalToken);
7712
+ if (!parsed)
7713
+ return { approved: false, reason: "approval_token is not a scoped mutation token." };
7714
+ if (!safeEqual(hmac(parsed.payload, secret), parsed.signature)) {
7715
+ return { approved: false, reason: "approval_token signature is invalid." };
7716
+ }
7717
+ const claims = parsed.claims;
7718
+ if (claims.version !== 1)
7719
+ return { approved: false, reason: "approval_token version is unsupported." };
7720
+ if (!claims.callerId || !claims.runId) {
7721
+ return { approved: false, reason: "approval_token must include caller and run claims." };
7722
+ }
7723
+ if (!claims.transport) {
7724
+ return { approved: false, reason: "approval_token must include a transport claim." };
7725
+ }
7726
+ if (!Number.isFinite(claims.expiresAt) || claims.expiresAt <= nowMs(options.now)) {
7727
+ return { approved: false, reason: "approval_token is expired." };
7728
+ }
7729
+ const now = nowMs(options.now);
7730
+ if (!Number.isFinite(claims.issuedAt) || claims.issuedAt > now + MAX_CLOCK_SKEW_MS) {
7731
+ return { approved: false, reason: "approval_token issue time is invalid." };
7732
+ }
7733
+ if (claims.expiresAt - claims.issuedAt > MAX_TOKEN_TTL_MS) {
7734
+ return { approved: false, reason: "approval_token TTL is too long." };
7735
+ }
7736
+ for (const key of ["surface", "operation", "machineId", "resourceId", "transport"]) {
7737
+ if (!claimMatches(options[key], claims[key])) {
7738
+ return { approved: false, reason: `approval_token ${key} claim does not match this mutation.` };
7739
+ }
7740
+ }
7741
+ for (const key of ["callerId", "runId"]) {
7742
+ if (options[key] !== undefined && options[key] !== claims[key]) {
7743
+ return { approved: false, reason: `approval_token ${key} claim does not match this mutation.` };
7744
+ }
7745
+ }
7746
+ const expectedArgsSha256 = options.argsSha256 || (options.args === undefined ? undefined : mutationArgsSha256(options.args));
7747
+ if (expectedArgsSha256 !== undefined && claims.args_sha256 !== expectedArgsSha256) {
7748
+ return { approved: false, reason: "approval_token args_sha256 claim does not match this mutation." };
7749
+ }
7750
+ const replayDecision = recordReplayNonce(env2, claims, parsed.payload, now);
7751
+ if (replayDecision)
7752
+ return replayDecision;
7753
+ return { approved: true, claims };
7754
+ }
7755
+ function isMutationApproved(options = {}) {
7756
+ const env2 = options.env ?? process.env;
7757
+ const surface = options.surface ?? "cli";
7758
+ if (surface === "mcp") {
7759
+ if (!options.operation)
7760
+ return false;
7761
+ return verifyMutationApprovalToken({
7762
+ surface,
7763
+ operation: options.operation,
7764
+ machineId: options.machineId,
7765
+ resourceId: options.resourceId,
7766
+ callerId: options.callerId,
7767
+ runId: options.runId,
7768
+ transport: options.transport ?? "mcp",
7769
+ args: options.args,
7770
+ argsSha256: options.argsSha256,
7771
+ approvalToken: options.approvalToken,
7772
+ env: env2,
7773
+ now: options.now
7774
+ }).approved;
7775
+ }
7776
+ if (options.approvalToken) {
7777
+ const decision = options.operation ? verifyMutationApprovalToken({
7778
+ surface,
7779
+ operation: options.operation,
7780
+ machineId: options.machineId,
7781
+ resourceId: options.resourceId,
7782
+ callerId: options.callerId,
7783
+ runId: options.runId,
7784
+ transport: options.transport ?? surface,
7785
+ args: options.args,
7786
+ argsSha256: options.argsSha256,
7787
+ approvalToken: options.approvalToken,
7788
+ env: env2,
7789
+ now: options.now
7790
+ }) : { approved: false };
7791
+ if (decision.approved)
7792
+ return true;
7793
+ if (env2[MUTATION_APPROVAL_TOKEN_ENV]?.trim())
7794
+ return false;
7795
+ }
7796
+ return isTruthy(env2[MUTATION_APPROVAL_FLAG_ENV]) || isTruthy(env2[LEGACY_MUTATION_APPROVAL_FLAG_ENV]);
7797
+ }
7798
+ function assertMutationApproved(options) {
7799
+ if (isMutationApproved(options)) {
7800
+ return;
7801
+ }
7802
+ const env2 = options.env ?? process.env;
7803
+ const tokenConfigured = Boolean(env2[MUTATION_APPROVAL_TOKEN_ENV]?.trim());
7804
+ const approvalHint = options.surface === "mcp" ? `pass a scoped approval_token signed with ${MUTATION_APPROVAL_TOKEN_ENV}` : tokenConfigured ? `pass a scoped approval_token signed with ${MUTATION_APPROVAL_TOKEN_ENV} or set ${MUTATION_APPROVAL_FLAG_ENV}=1 for a trusted local session` : `set ${MUTATION_APPROVAL_FLAG_ENV}=1 for a trusted local session or configure ${MUTATION_APPROVAL_TOKEN_ENV}`;
7805
+ throw new Error(`Fleet mutation blocked: ${options.surface}.${options.operation} requires operator approval; ${approvalHint}.`);
7806
+ }
7807
+
7524
7808
  // src/remote.ts
7525
7809
  init_db();
7526
7810
  import { spawnSync as spawnSync2 } from "child_process";
7527
- import { hostname as hostname4 } from "os";
7811
+ import { existsSync as existsSync5, mkdtempSync, readFileSync as readFileSync3, rmSync } from "fs";
7812
+ import { hostname as hostname4, tmpdir } from "os";
7813
+ import { join as join3 } from "path";
7528
7814
 
7529
7815
  // src/topology.ts
7530
7816
  init_db();
@@ -8498,6 +8784,16 @@ function resolveMachineWorkspace(options) {
8498
8784
  function shellQuote2(value) {
8499
8785
  return `'${value.replace(/'/g, "'\\''")}'`;
8500
8786
  }
8787
+ function validateSshTarget(target) {
8788
+ const trimmed = target.trim();
8789
+ if (!trimmed || trimmed.startsWith("-") || /[\s"'`$\\;&|<>()[\]{}]/.test(trimmed)) {
8790
+ throw new Error(`Unsafe SSH target: ${target}`);
8791
+ }
8792
+ if (!/^(?:[A-Za-z0-9._%+-]+@)?[A-Za-z0-9._:-]+$/.test(trimmed)) {
8793
+ throw new Error(`Unsafe SSH target: ${target}`);
8794
+ }
8795
+ return trimmed;
8796
+ }
8501
8797
  function resolveSshTarget(machineId, options = {}) {
8502
8798
  const resolved = resolveMachineRoute(machineId, options);
8503
8799
  if (!resolved.ok || !resolved.target) {
@@ -8508,15 +8804,25 @@ function resolveSshTarget(machineId, options = {}) {
8508
8804
  }
8509
8805
  return {
8510
8806
  machineId: resolved.machine_id ?? machineId,
8511
- target: resolved.command_target ?? resolved.target,
8807
+ target: validateSshTarget(resolved.command_target ?? resolved.target),
8512
8808
  route: resolved.route,
8513
8809
  confidence: resolved.confidence,
8514
8810
  warnings: resolved.warnings
8515
8811
  };
8516
8812
  }
8517
8813
  function buildSshCommand(machineId, remoteCommand, options = {}) {
8814
+ return buildSshCommandPlan(machineId, remoteCommand, options).shellCommand;
8815
+ }
8816
+ function buildSshCommandPlan(machineId, remoteCommand, options = {}) {
8518
8817
  const resolved = resolveSshTarget(machineId, options);
8519
- return remoteCommand ? `ssh ${resolved.target} ${shellQuote2(remoteCommand)}` : `ssh ${resolved.target}`;
8818
+ const args = remoteCommand ? [resolved.target, remoteCommand] : [resolved.target];
8819
+ const shellCommand2 = `ssh ${args.map(shellQuote2).join(" ")}`;
8820
+ return {
8821
+ ...resolved,
8822
+ command: "ssh",
8823
+ args,
8824
+ shellCommand: shellCommand2
8825
+ };
8520
8826
  }
8521
8827
 
8522
8828
  // src/remote.ts
@@ -8528,35 +8834,233 @@ function machineIsLocal(machineId, localMachineId) {
8528
8834
  }
8529
8835
  function resolveMachineCommand(machineId, command, localMachineId = getLocalMachineId()) {
8530
8836
  if (machineIsLocal(machineId, localMachineId)) {
8531
- return { source: "local", shellCommand: command };
8837
+ return { source: "local", command: "bash", args: ["-c", command], shellCommand: command, usesShell: true };
8532
8838
  }
8533
8839
  try {
8840
+ const plan = buildSshCommandPlan(machineId, command);
8534
8841
  return {
8535
- source: resolveSshTarget(machineId).route,
8536
- shellCommand: buildSshCommand(machineId, command)
8842
+ source: plan.route,
8843
+ command: plan.command,
8844
+ args: plan.args,
8845
+ shellCommand: plan.shellCommand,
8846
+ usesShell: false
8537
8847
  };
8538
8848
  } catch (error) {
8539
8849
  const message = String(error.message ?? error);
8540
8850
  if (message.includes("Machine route not found") || message.includes("Machine not found in manifest")) {
8541
- return { source: "ssh", shellCommand: `ssh ${shellQuote3(machineId)} ${shellQuote3(command)}` };
8851
+ const target = validateSshTarget(machineId);
8852
+ return {
8853
+ source: "ssh",
8854
+ command: "ssh",
8855
+ args: [target, command],
8856
+ shellCommand: `ssh ${shellQuote3(target)} ${shellQuote3(command)}`,
8857
+ usesShell: false
8858
+ };
8542
8859
  }
8543
8860
  throw error;
8544
8861
  }
8545
8862
  }
8546
- function runMachineCommand(machineId, command) {
8863
+ function runMachineCommand(machineId, command, options = {}) {
8547
8864
  const resolved = resolveMachineCommand(machineId, command);
8548
- const result = spawnSync2("bash", ["-c", resolved.shellCommand], {
8865
+ if (options.timeoutMs && options.timeoutMs > 0 && process.platform !== "win32") {
8866
+ return runMachineCommandWithProcessGroupTimeout(machineId, resolved, options);
8867
+ }
8868
+ const result = spawnSync2(resolved.command, resolved.args, {
8549
8869
  encoding: "utf8",
8550
- env: process.env
8870
+ env: process.env,
8871
+ timeout: options.timeoutMs,
8872
+ killSignal: "SIGTERM"
8551
8873
  });
8874
+ const timedOut = Boolean(result.error && "code" in result.error && result.error.code === "ETIMEDOUT");
8875
+ const timeoutMessage = timedOut ? `Command timed out after ${options.timeoutMs}ms.` : "";
8876
+ const stderr = [result.stderr || "", timeoutMessage].filter(Boolean).join(result.stderr ? `
8877
+ ` : "");
8552
8878
  return {
8553
8879
  machineId,
8554
8880
  source: resolved.source,
8555
8881
  stdout: result.stdout || "",
8556
- stderr: result.stderr || "",
8557
- exitCode: result.status ?? 1
8882
+ stderr,
8883
+ exitCode: timedOut ? 124 : result.status ?? 1,
8884
+ timedOut,
8885
+ signal: result.signal
8558
8886
  };
8559
8887
  }
8888
+ function runMachineCommandWithProcessGroupTimeout(machineId, resolved, options) {
8889
+ const timeoutMs = Math.max(1, options.timeoutMs ?? 1);
8890
+ const killGraceMs = Math.max(1, options.killGraceMs ?? 1000);
8891
+ const helperDir = mkdtempSync(join3(tmpdir(), "machines-timeout-helper-"));
8892
+ const pgidFile = join3(helperDir, "pgid");
8893
+ const helper = spawnSync2(process.execPath, ["--eval", PROCESS_GROUP_TIMEOUT_HELPER], {
8894
+ input: JSON.stringify({ command: resolved.command, args: resolved.args }),
8895
+ encoding: "utf8",
8896
+ env: {
8897
+ ...process.env,
8898
+ HASNA_MACHINES_COMMAND_TIMEOUT_MS: String(timeoutMs),
8899
+ HASNA_MACHINES_COMMAND_KILL_GRACE_MS: String(killGraceMs),
8900
+ HASNA_MACHINES_COMMAND_PGID_FILE: pgidFile
8901
+ },
8902
+ timeout: timeoutMs + killGraceMs + 2000,
8903
+ killSignal: "SIGKILL",
8904
+ maxBuffer: 64 * 1024 * 1024
8905
+ });
8906
+ try {
8907
+ const parsed = parseHelperResult(helper.stdout);
8908
+ if (parsed) {
8909
+ return {
8910
+ machineId,
8911
+ source: resolved.source,
8912
+ stdout: parsed.stdout,
8913
+ stderr: parsed.stderr,
8914
+ exitCode: parsed.exitCode,
8915
+ timedOut: parsed.timedOut,
8916
+ signal: parsed.signal
8917
+ };
8918
+ }
8919
+ const helperTimedOut = Boolean(helper.error && "code" in helper.error && helper.error.code === "ETIMEDOUT");
8920
+ if (helperTimedOut)
8921
+ killPublishedProcessGroup(pgidFile);
8922
+ const timeoutMessage = helperTimedOut ? `Command timed out after ${timeoutMs}ms; timeout helper exceeded cleanup grace ${killGraceMs}ms.` : "";
8923
+ const stderr = [helper.stderr || "", timeoutMessage].filter(Boolean).join(helper.stderr ? `
8924
+ ` : "");
8925
+ return {
8926
+ machineId,
8927
+ source: resolved.source,
8928
+ stdout: "",
8929
+ stderr,
8930
+ exitCode: helperTimedOut ? 124 : helper.status ?? 1,
8931
+ timedOut: helperTimedOut,
8932
+ signal: helper.signal
8933
+ };
8934
+ } finally {
8935
+ rmSync(helperDir, { recursive: true, force: true });
8936
+ }
8937
+ }
8938
+ function killPublishedProcessGroup(pgidFile) {
8939
+ if (!existsSync5(pgidFile))
8940
+ return;
8941
+ try {
8942
+ const pid = Number.parseInt(readFileSync3(pgidFile, "utf8").trim(), 10);
8943
+ if (!Number.isInteger(pid) || pid <= 1)
8944
+ return;
8945
+ process.kill(-pid, "SIGKILL");
8946
+ } catch {}
8947
+ }
8948
+ function parseHelperResult(stdout) {
8949
+ if (!stdout)
8950
+ return null;
8951
+ try {
8952
+ const parsed = JSON.parse(stdout);
8953
+ if (typeof parsed.stdout !== "string" || typeof parsed.stderr !== "string" || typeof parsed.exitCode !== "number")
8954
+ return null;
8955
+ return {
8956
+ machineId: "",
8957
+ source: "local",
8958
+ stdout: parsed.stdout,
8959
+ stderr: parsed.stderr,
8960
+ exitCode: parsed.exitCode,
8961
+ timedOut: parsed.timedOut === true,
8962
+ signal: typeof parsed.signal === "string" ? parsed.signal : null
8963
+ };
8964
+ } catch {
8965
+ return null;
8966
+ }
8967
+ }
8968
+ var PROCESS_GROUP_TIMEOUT_HELPER = `
8969
+ const { spawn } = require("node:child_process");
8970
+ const { readFileSync, writeFileSync } = require("node:fs");
8971
+
8972
+ const plan = JSON.parse(readFileSync(0, "utf8"));
8973
+ const command = String(plan.command || "");
8974
+ const args = Array.isArray(plan.args) ? plan.args.map(String) : [];
8975
+ const timeoutMs = Math.max(1, Number.parseInt(process.env.HASNA_MACHINES_COMMAND_TIMEOUT_MS || "1", 10));
8976
+ const killGraceMs = Math.max(1, Number.parseInt(process.env.HASNA_MACHINES_COMMAND_KILL_GRACE_MS || "1000", 10));
8977
+ const pgidFile = process.env.HASNA_MACHINES_COMMAND_PGID_FILE || "";
8978
+ let stdout = "";
8979
+ let stderr = "";
8980
+ let timedOut = false;
8981
+ let finished = false;
8982
+ let timeoutTimer;
8983
+ let killTimer;
8984
+ let sigkillSent = false;
8985
+ let pendingExit = null;
8986
+
8987
+ const child = spawn(command, args, {
8988
+ detached: true,
8989
+ stdio: ["ignore", "pipe", "pipe"],
8990
+ env: process.env,
8991
+ });
8992
+
8993
+ if (pgidFile && child.pid) {
8994
+ try {
8995
+ writeFileSync(pgidFile, String(child.pid), { mode: 0o600 });
8996
+ } catch {}
8997
+ }
8998
+
8999
+ function appendText(target, chunk) {
9000
+ return target + String(chunk);
9001
+ }
9002
+
9003
+ function killTarget(signal) {
9004
+ if (!child.pid) return;
9005
+ if (process.platform === "win32") {
9006
+ try {
9007
+ process.kill(child.pid, signal);
9008
+ } catch {}
9009
+ return;
9010
+ }
9011
+ try {
9012
+ process.kill(-child.pid, signal);
9013
+ } catch {}
9014
+ }
9015
+
9016
+ function finish(code, signal) {
9017
+ if (finished) return;
9018
+ if (timedOut && !sigkillSent) {
9019
+ pendingExit = { code, signal };
9020
+ return;
9021
+ }
9022
+ finished = true;
9023
+ if (timeoutTimer) clearTimeout(timeoutTimer);
9024
+ if (killTimer) clearTimeout(killTimer);
9025
+ if (timedOut) {
9026
+ stderr = [stderr, "Command timed out after " + timeoutMs + "ms."].filter(Boolean).join(stderr ? "\\n" : "");
9027
+ }
9028
+ const exitCode = timedOut ? 124 : code ?? 1;
9029
+ process.stdout.write(JSON.stringify({
9030
+ stdout,
9031
+ stderr,
9032
+ exitCode,
9033
+ timedOut,
9034
+ signal: signal ?? null,
9035
+ }), () => process.exit(exitCode));
9036
+ }
9037
+
9038
+ child.stdout.setEncoding("utf8");
9039
+ child.stderr.setEncoding("utf8");
9040
+ child.stdout.on("data", (chunk) => { stdout = appendText(stdout, chunk); });
9041
+ child.stderr.on("data", (chunk) => { stderr = appendText(stderr, chunk); });
9042
+ let childExit = { code: null, signal: null };
9043
+ child.on("error", (error) => {
9044
+ stderr = [stderr, error instanceof Error ? error.message : String(error)].filter(Boolean).join(stderr ? "\\n" : "");
9045
+ finish(1, null);
9046
+ });
9047
+ child.on("exit", (code, signal) => {
9048
+ childExit = { code, signal };
9049
+ });
9050
+ child.on("close", (code, signal) => {
9051
+ finish(code ?? childExit.code, signal ?? childExit.signal);
9052
+ });
9053
+
9054
+ timeoutTimer = setTimeout(() => {
9055
+ timedOut = true;
9056
+ killTarget("SIGTERM");
9057
+ killTimer = setTimeout(() => {
9058
+ sigkillSent = true;
9059
+ killTarget("SIGKILL");
9060
+ if (pendingExit) finish(pendingExit.code, pendingExit.signal);
9061
+ }, killGraceMs);
9062
+ }, timeoutMs);
9063
+ `;
8560
9064
  function describeMachineCommandFailure(operation, result) {
8561
9065
  const detail = (result.stderr || result.stdout || "").trim();
8562
9066
  const suffix = detail ? `: ${detail}` : "";
@@ -8671,17 +9175,17 @@ function buildSetupPlan(machineId) {
8671
9175
  workspacePath: `${homedir2()}/workspace`
8672
9176
  };
8673
9177
  const steps = [...buildBaseSteps(target), ...buildPackageSteps(target)];
8674
- return {
9178
+ return attachMutationPlanDigest({
8675
9179
  machineId: target.id,
8676
9180
  mode: "plan",
8677
9181
  steps,
8678
9182
  executed: 0
8679
- };
9183
+ });
8680
9184
  }
8681
- function runSetup(machineId, options = {}, runner = runMachineCommand) {
8682
- const plan = buildSetupPlan(machineId);
9185
+ function runSetupPlan(plan, options = {}, runner = runMachineCommand) {
9186
+ assertMutationPlanDigest(plan, options.expectedPlanDigest);
8683
9187
  if (!options.apply) {
8684
- return plan;
9188
+ return attachMutationPlanDigest({ ...plan, mode: "plan", executed: 0 });
8685
9189
  }
8686
9190
  if (!options.yes) {
8687
9191
  throw new Error("Setup execution requires --yes.");
@@ -8702,19 +9206,19 @@ function runSetup(machineId, options = {}, runner = runMachineCommand) {
8702
9206
  }
8703
9207
  executed += 1;
8704
9208
  }
8705
- const summary = {
9209
+ const summary = attachMutationPlanDigest({
8706
9210
  machineId: plan.machineId,
8707
9211
  mode: "apply",
8708
9212
  steps: plan.steps,
8709
9213
  executed
8710
- };
9214
+ });
8711
9215
  recordSetupRun(plan.machineId, "completed", summary);
8712
9216
  return summary;
8713
9217
  }
8714
9218
 
8715
9219
  // src/commands/backup.ts
8716
9220
  import { homedir as homedir3, hostname as hostname5 } from "os";
8717
- import { join as join3 } from "path";
9221
+ import { join as join4 } from "path";
8718
9222
  var MACHINES_BACKUP_BUCKET_ENV = "HASNA_MACHINES_S3_BUCKET";
8719
9223
  var MACHINES_BACKUP_BUCKET_FALLBACK_ENV = "MACHINES_S3_BUCKET";
8720
9224
  var MACHINES_BACKUP_PREFIX_ENV = "HASNA_MACHINES_S3_PREFIX";
@@ -8764,14 +9268,14 @@ function resolveBackupTarget(options = {}) {
8764
9268
  function defaultBackupSources() {
8765
9269
  const home = homedir3();
8766
9270
  return [
8767
- join3(home, ".hasna"),
8768
- join3(home, ".ssh"),
8769
- join3(home, ".secrets")
9271
+ join4(home, ".hasna"),
9272
+ join4(home, ".ssh"),
9273
+ join4(home, ".secrets")
8770
9274
  ];
8771
9275
  }
8772
9276
  function buildBackupPlan(bucket, prefix) {
8773
9277
  const target = resolveBackupTarget({ bucket, prefix });
8774
- const archivePath = join3(homedir3(), ".hasna", "machines", "backup.tgz");
9278
+ const archivePath = join4(homedir3(), ".hasna", "machines", "backup.tgz");
8775
9279
  const sources = defaultBackupSources();
8776
9280
  const steps = [
8777
9281
  {
@@ -8823,20 +9327,20 @@ function runBackup(bucket, prefix, options = {}) {
8823
9327
 
8824
9328
  // src/commands/cert.ts
8825
9329
  import { homedir as homedir4, platform as platform3 } from "os";
8826
- import { join as join4 } from "path";
9330
+ import { join as join5 } from "path";
8827
9331
  function quote3(value) {
8828
9332
  return `'${value.replace(/'/g, `'\\''`)}'`;
8829
9333
  }
8830
9334
  function certDir() {
8831
- return join4(homedir4(), ".hasna", "machines", "certs");
9335
+ return join5(homedir4(), ".hasna", "machines", "certs");
8832
9336
  }
8833
9337
  function buildCertPlan(domains) {
8834
9338
  if (domains.length === 0) {
8835
9339
  throw new Error("At least one domain is required.");
8836
9340
  }
8837
9341
  const primary = domains[0];
8838
- const certPath = join4(certDir(), `${primary}.pem`);
8839
- const keyPath = join4(certDir(), `${primary}-key.pem`);
9342
+ const certPath = join5(certDir(), `${primary}.pem`);
9343
+ const keyPath = join5(certDir(), `${primary}-key.pem`);
8840
9344
  const steps = [];
8841
9345
  if (platform3() === "darwin") {
8842
9346
  steps.push({
@@ -8901,16 +9405,16 @@ function runCertPlan(domains, options = {}) {
8901
9405
 
8902
9406
  // src/commands/dns.ts
8903
9407
  init_paths();
8904
- import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
8905
- import { join as join5 } from "path";
9408
+ import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
9409
+ import { join as join6 } from "path";
8906
9410
  function getDnsPath() {
8907
- return join5(getDataDir(), "dns.json");
9411
+ return join6(getDataDir(), "dns.json");
8908
9412
  }
8909
9413
  function readMappings() {
8910
9414
  const path = getDnsPath();
8911
- if (!existsSync5(path))
9415
+ if (!existsSync6(path))
8912
9416
  return [];
8913
- return JSON.parse(readFileSync3(path, "utf8"));
9417
+ return JSON.parse(readFileSync4(path, "utf8"));
8914
9418
  }
8915
9419
  function writeMappings(mappings) {
8916
9420
  const path = getDnsPath();
@@ -8937,11 +9441,315 @@ function renderDomainMapping(domain) {
8937
9441
  hostsEntry: `${entry.targetHost} ${entry.domain}`,
8938
9442
  caddySnippet: `${entry.domain} {
8939
9443
  reverse_proxy 127.0.0.1:${entry.port}
8940
- tls ${join5(getDataDir(), "certs", `${entry.domain}.pem`)} ${join5(getDataDir(), "certs", `${entry.domain}-key.pem`)}
9444
+ tls ${join6(getDataDir(), "certs", `${entry.domain}.pem`)} ${join6(getDataDir(), "certs", `${entry.domain}-key.pem`)}
8941
9445
  }`,
8942
- certPath: join5(getDataDir(), "certs", `${entry.domain}.pem`),
8943
- keyPath: join5(getDataDir(), "certs", `${entry.domain}-key.pem`)
9446
+ certPath: join6(getDataDir(), "certs", `${entry.domain}.pem`),
9447
+ keyPath: join6(getDataDir(), "certs", `${entry.domain}-key.pem`)
9448
+ };
9449
+ }
9450
+
9451
+ // src/commands/hosts.ts
9452
+ import { spawnSync as spawnSync3 } from "child_process";
9453
+ import { existsSync as existsSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
9454
+ import { networkInterfaces, platform as osPlatform } from "os";
9455
+ var HOSTS_BLOCK_BEGIN = "# >>> hasna machines fleet >>>";
9456
+ var HOSTS_BLOCK_END = "# <<< hasna machines fleet <<<";
9457
+ function defaultRunner2(command) {
9458
+ const result = spawnSync3("bash", ["-c", command], { encoding: "utf8", env: process.env });
9459
+ return { stdout: result.stdout || "", exitCode: result.status ?? 1 };
9460
+ }
9461
+ function getHostsPath() {
9462
+ const override = process.env["HASNA_MACHINES_HOSTS_PATH"];
9463
+ if (override)
9464
+ return override;
9465
+ if (osPlatform() === "win32") {
9466
+ return "C:\\Windows\\System32\\drivers\\etc\\hosts";
9467
+ }
9468
+ return "/etc/hosts";
9469
+ }
9470
+ function isIpv4(value) {
9471
+ const parts = value.split(".");
9472
+ if (parts.length !== 4)
9473
+ return false;
9474
+ return parts.every((part) => {
9475
+ if (!/^\d{1,3}$/.test(part))
9476
+ return false;
9477
+ const num = Number(part);
9478
+ return num >= 0 && num <= 255;
9479
+ });
9480
+ }
9481
+ function isPrivateIpv4(value) {
9482
+ if (!isIpv4(value))
9483
+ return false;
9484
+ const [a, b] = value.split(".").map(Number);
9485
+ if (a === 10)
9486
+ return true;
9487
+ if (a === 192 && b === 168)
9488
+ return true;
9489
+ if (a === 172 && b >= 16 && b <= 31)
9490
+ return true;
9491
+ return false;
9492
+ }
9493
+ function isTailnetIpv4(value) {
9494
+ if (!isIpv4(value))
9495
+ return false;
9496
+ const [a, b] = value.split(".").map(Number);
9497
+ return a === 100 && b >= 64 && b <= 127;
9498
+ }
9499
+ function subnet24(value) {
9500
+ if (!isIpv4(value))
9501
+ return null;
9502
+ return value.split(".").slice(0, 3).join(".");
9503
+ }
9504
+ function ipFromEndpoint(endpoint) {
9505
+ if (!endpoint)
9506
+ return null;
9507
+ const host = endpoint.replace(/:\d+$/, "").trim();
9508
+ return isIpv4(host) ? host : null;
9509
+ }
9510
+ function localPrivateSubnets() {
9511
+ const subnets = new Set;
9512
+ const interfaces = networkInterfaces();
9513
+ for (const addrs of Object.values(interfaces)) {
9514
+ for (const addr of addrs ?? []) {
9515
+ if (addr.family !== "IPv4" || addr.internal)
9516
+ continue;
9517
+ if (!isPrivateIpv4(addr.address))
9518
+ continue;
9519
+ const subnet = subnet24(addr.address);
9520
+ if (subnet)
9521
+ subnets.add(subnet);
9522
+ }
9523
+ }
9524
+ return [...subnets];
9525
+ }
9526
+ function shortName(value) {
9527
+ if (!value)
9528
+ return null;
9529
+ const trimmed = value.replace(/\.$/, "").trim();
9530
+ if (!trimmed)
9531
+ return null;
9532
+ return trimmed.split(".")[0]?.toLowerCase() || null;
9533
+ }
9534
+ function machineNames(machine) {
9535
+ const names = [];
9536
+ for (const candidate of [machine.id, machine.hostname, shortName(machine.tailscaleName)]) {
9537
+ const name = shortName(candidate);
9538
+ if (name && !names.includes(name))
9539
+ names.push(name);
9540
+ }
9541
+ return names;
9542
+ }
9543
+ function peerKeysForMachine(machine) {
9544
+ return [
9545
+ machine.id,
9546
+ machine.hostname,
9547
+ shortName(machine.tailscaleName),
9548
+ machine.tailscaleName,
9549
+ machine.sshAddress?.split("@").pop()
9550
+ ].filter((value) => Boolean(value));
9551
+ }
9552
+ function indexPeers(tailscale) {
9553
+ const peers = new Map;
9554
+ if (!tailscale)
9555
+ return peers;
9556
+ const add = (peer) => {
9557
+ if (!peer)
9558
+ return;
9559
+ const id = shortName(peer.HostName) || shortName(peer.DNSName);
9560
+ if (id)
9561
+ peers.set(id, peer);
8944
9562
  };
9563
+ add(tailscale.Self);
9564
+ for (const peer of Object.values(tailscale.Peer ?? {}))
9565
+ add(peer);
9566
+ return peers;
9567
+ }
9568
+ function findPeer(machine, peers) {
9569
+ for (const key of peerKeysForMachine(machine)) {
9570
+ const peer = peers.get(shortName(key) || key);
9571
+ if (peer)
9572
+ return peer;
9573
+ }
9574
+ return null;
9575
+ }
9576
+ function tailnetIp(peer) {
9577
+ if (!peer)
9578
+ return null;
9579
+ for (const ip of peer.TailscaleIPs ?? []) {
9580
+ if (isTailnetIpv4(ip))
9581
+ return ip;
9582
+ }
9583
+ return null;
9584
+ }
9585
+ function metadataString2(metadata, key) {
9586
+ if (!metadata)
9587
+ return null;
9588
+ const value = metadata[key];
9589
+ return typeof value === "string" && value.trim() ? value.trim() : null;
9590
+ }
9591
+ function buildFleetHostEntries(input) {
9592
+ const { manifest, tailscale, localSubnets, localMachineId } = input;
9593
+ const peers = indexPeers(tailscale);
9594
+ const subnetSet = new Set(localSubnets);
9595
+ const entries = [];
9596
+ const unresolved = [];
9597
+ const warnings = [];
9598
+ for (const machine of manifest.machines) {
9599
+ if (localMachineId && machine.id === localMachineId)
9600
+ continue;
9601
+ const names = machineNames(machine);
9602
+ if (names.length === 0)
9603
+ continue;
9604
+ const peer = findPeer(machine, peers);
9605
+ const lanAddress = metadataString2(machine.metadata, "lanAddress");
9606
+ const explicitIp = metadataString2(machine.metadata, "ipAddress");
9607
+ const curLanIp = ipFromEndpoint(peer?.CurAddr);
9608
+ const tnet = tailnetIp(peer);
9609
+ let ip = null;
9610
+ let source = null;
9611
+ if (lanAddress && isPrivateIpv4(lanAddress) && subnetSet.has(subnet24(lanAddress) ?? "")) {
9612
+ ip = lanAddress;
9613
+ source = "manifest_lan";
9614
+ } else if (curLanIp && isPrivateIpv4(curLanIp) && subnetSet.has(subnet24(curLanIp) ?? "")) {
9615
+ ip = curLanIp;
9616
+ source = "tailscale_lan";
9617
+ } else if (tnet) {
9618
+ ip = tnet;
9619
+ source = "tailscale";
9620
+ } else if (explicitIp && isIpv4(explicitIp)) {
9621
+ ip = explicitIp;
9622
+ source = "manifest_ip";
9623
+ }
9624
+ if (!ip || !source) {
9625
+ unresolved.push(machine.id);
9626
+ continue;
9627
+ }
9628
+ entries.push({ id: machine.id, ip, names, source });
9629
+ }
9630
+ entries.sort((left, right) => left.id.localeCompare(right.id));
9631
+ return { entries, unresolved, warnings };
9632
+ }
9633
+ function renderHostsBlock(entries) {
9634
+ const lines = [
9635
+ HOSTS_BLOCK_BEGIN,
9636
+ "# Managed by `machines hosts apply`. Do not edit between these markers.",
9637
+ ...entries.map((entry) => `${entry.ip} ${entry.names.join(" ")}`),
9638
+ HOSTS_BLOCK_END
9639
+ ];
9640
+ return lines.join(`
9641
+ `);
9642
+ }
9643
+ function mergeHostsContent(existing, block) {
9644
+ const beginIndex = existing.indexOf(HOSTS_BLOCK_BEGIN);
9645
+ const endIndex = existing.indexOf(HOSTS_BLOCK_END);
9646
+ if (beginIndex !== -1 && endIndex !== -1 && endIndex > beginIndex) {
9647
+ const before = existing.slice(0, beginIndex).replace(/\n+$/, "");
9648
+ const after = existing.slice(endIndex + HOSTS_BLOCK_END.length).replace(/^\n+/, "");
9649
+ const head = before ? `${before}
9650
+ ` : "";
9651
+ const tail = after ? `
9652
+ ${after}` : `
9653
+ `;
9654
+ return `${head}${block}${tail}`;
9655
+ }
9656
+ const base = existing.replace(/\n+$/, "");
9657
+ const prefix = base ? `${base}
9658
+
9659
+ ` : "";
9660
+ return `${prefix}${block}
9661
+ `;
9662
+ }
9663
+ function loadTailscale(runner, warnings) {
9664
+ if (runner("command -v tailscale >/dev/null 2>&1").exitCode !== 0) {
9665
+ warnings.push("tailscale_not_available");
9666
+ return null;
9667
+ }
9668
+ const result = runner("tailscale status --json");
9669
+ if (result.exitCode !== 0) {
9670
+ warnings.push("tailscale_status_failed");
9671
+ return null;
9672
+ }
9673
+ try {
9674
+ return JSON.parse(result.stdout);
9675
+ } catch {
9676
+ warnings.push("tailscale_status_invalid_json");
9677
+ return null;
9678
+ }
9679
+ }
9680
+ function collectPingTargets(tailscale, localSubnets) {
9681
+ if (!tailscale)
9682
+ return [];
9683
+ const subnetSet = new Set(localSubnets);
9684
+ const targets = [];
9685
+ for (const peer of Object.values(tailscale.Peer ?? {})) {
9686
+ if (!peer.Online)
9687
+ continue;
9688
+ const cur = ipFromEndpoint(peer.CurAddr);
9689
+ if (cur && isPrivateIpv4(cur) && subnetSet.has(subnet24(cur) ?? ""))
9690
+ continue;
9691
+ const tnet = tailnetIp(peer);
9692
+ if (tnet && !targets.includes(tnet))
9693
+ targets.push(tnet);
9694
+ }
9695
+ return targets;
9696
+ }
9697
+ function warmDirectPaths(runner, targets, timeoutSeconds = 2) {
9698
+ for (const target of targets) {
9699
+ runner(`tailscale ping --c 1 --timeout ${timeoutSeconds}s ${target} >/dev/null 2>&1 || true`);
9700
+ }
9701
+ }
9702
+ function resolveLocalMachineId(tailscale, explicit) {
9703
+ if (explicit)
9704
+ return explicit;
9705
+ return shortName(tailscale?.Self?.HostName) || shortName(process.env["HOSTNAME"]) || null;
9706
+ }
9707
+ function planFleetHosts(options = {}) {
9708
+ const runner = options.runner ?? defaultRunner2;
9709
+ const warnings = [];
9710
+ let tailscale = loadTailscale(runner, warnings);
9711
+ const manifest = readManifest();
9712
+ const localSubnets = options.localSubnets ?? localPrivateSubnets();
9713
+ if (options.warm !== false && tailscale && localSubnets.length > 0) {
9714
+ const targets = collectPingTargets(tailscale, localSubnets);
9715
+ if (targets.length > 0) {
9716
+ warmDirectPaths(runner, targets, options.warmTimeoutSeconds);
9717
+ tailscale = loadTailscale(runner, warnings) ?? tailscale;
9718
+ }
9719
+ }
9720
+ const localMachineId = resolveLocalMachineId(tailscale, options.localMachineId);
9721
+ const built = buildFleetHostEntries({ manifest, tailscale, localSubnets, localMachineId });
9722
+ const block = renderHostsBlock(built.entries);
9723
+ return {
9724
+ hostsPath: getHostsPath(),
9725
+ entries: built.entries,
9726
+ unresolved: built.unresolved,
9727
+ warnings: [...warnings, ...built.warnings],
9728
+ block,
9729
+ localSubnets
9730
+ };
9731
+ }
9732
+ function applyFleetHosts(options = {}) {
9733
+ const plan = planFleetHosts(options);
9734
+ const hostsPath = plan.hostsPath;
9735
+ const existing = existsSync7(hostsPath) ? readFileSync5(hostsPath, "utf8") : "";
9736
+ const merged = mergeHostsContent(existing, plan.block);
9737
+ let viaSudo = false;
9738
+ try {
9739
+ writeFileSync3(hostsPath, merged, "utf8");
9740
+ } catch (error) {
9741
+ const code = error?.code;
9742
+ if (code !== "EACCES" && code !== "EPERM")
9743
+ throw error;
9744
+ const runner = options.runner ?? defaultRunner2;
9745
+ const encoded = Buffer.from(merged, "utf8").toString("base64");
9746
+ const result = runner(`printf %s '${encoded}' | base64 -d | sudo tee ${hostsPath} >/dev/null`);
9747
+ if (result.exitCode !== 0) {
9748
+ throw new Error(`Failed to write ${hostsPath} (need sudo). Re-run with elevated privileges.`);
9749
+ }
9750
+ viaSudo = true;
9751
+ }
9752
+ return { ...plan, written: true, viaSudo };
8945
9753
  }
8946
9754
 
8947
9755
  // src/commands/diff.ts
@@ -9077,12 +9885,12 @@ function listApps(machineId) {
9077
9885
  }
9078
9886
  function buildAppsPlan(machineId) {
9079
9887
  const machine = resolveMachine(machineId);
9080
- return {
9888
+ return attachMutationPlanDigest({
9081
9889
  machineId: machine.id,
9082
9890
  mode: "plan",
9083
9891
  steps: buildAppSteps(machine),
9084
9892
  executed: 0
9085
- };
9893
+ });
9086
9894
  }
9087
9895
  function getAppsStatus(machineId, runner = runMachineCommand) {
9088
9896
  const machine = resolveMachine(machineId);
@@ -9105,10 +9913,10 @@ function diffApps(machineId, runner = runMachineCommand) {
9105
9913
  installed: status.apps.filter((app) => app.installed).map((app) => app.name)
9106
9914
  };
9107
9915
  }
9108
- function runAppsInstall(machineId, options = {}, runner = runMachineCommand) {
9109
- const plan = buildAppsPlan(machineId);
9916
+ function runAppsPlan(plan, options = {}, runner = runMachineCommand) {
9917
+ assertMutationPlanDigest(plan, options.expectedPlanDigest);
9110
9918
  if (!options.apply)
9111
- return plan;
9919
+ return attachMutationPlanDigest({ ...plan, mode: "plan", executed: 0 });
9112
9920
  if (!options.yes) {
9113
9921
  throw new Error("App installation requires --yes.");
9114
9922
  }
@@ -9117,12 +9925,12 @@ function runAppsInstall(machineId, options = {}, runner = runMachineCommand) {
9117
9925
  requireMachineCommandSuccess(`App install ${step.id}`, runner(plan.machineId, step.command));
9118
9926
  executed += 1;
9119
9927
  }
9120
- return {
9928
+ return attachMutationPlanDigest({
9121
9929
  machineId: plan.machineId,
9122
9930
  mode: "apply",
9123
9931
  steps: plan.steps,
9124
9932
  executed
9125
- };
9933
+ });
9126
9934
  }
9127
9935
 
9128
9936
  // src/commands/install-claude.ts
@@ -9184,12 +9992,12 @@ function parseProbe(tool, stdout) {
9184
9992
  }
9185
9993
  function buildClaudeInstallPlan(machineId, tools) {
9186
9994
  const machine = resolveMachine2(machineId);
9187
- return {
9995
+ return attachMutationPlanDigest({
9188
9996
  machineId: machine.id,
9189
9997
  mode: "plan",
9190
9998
  steps: buildInstallSteps(machine, tools),
9191
9999
  executed: 0
9192
- };
10000
+ });
9193
10001
  }
9194
10002
  function getClaudeCliStatus(machineId, tools, runner = runMachineCommand) {
9195
10003
  const machine = resolveMachine2(machineId);
@@ -9212,10 +10020,10 @@ function diffClaudeCli(machineId, tools, runner = runMachineCommand) {
9212
10020
  installed: status.tools.filter((tool) => tool.installed).map((tool) => tool.tool)
9213
10021
  };
9214
10022
  }
9215
- function runClaudeInstall(machineId, tools, options = {}, runner = runMachineCommand) {
9216
- const plan = buildClaudeInstallPlan(machineId, tools);
10023
+ function runClaudeInstallPlan(plan, options = {}, runner = runMachineCommand) {
10024
+ assertMutationPlanDigest(plan, options.expectedPlanDigest);
9217
10025
  if (!options.apply)
9218
- return plan;
10026
+ return attachMutationPlanDigest({ ...plan, mode: "plan", executed: 0 });
9219
10027
  if (!options.yes) {
9220
10028
  throw new Error("Claude CLI installation requires --yes.");
9221
10029
  }
@@ -9224,12 +10032,12 @@ function runClaudeInstall(machineId, tools, options = {}, runner = runMachineCom
9224
10032
  requireMachineCommandSuccess(`AI CLI install ${step.id}`, runner(plan.machineId, step.command));
9225
10033
  executed += 1;
9226
10034
  }
9227
- return {
10035
+ return attachMutationPlanDigest({
9228
10036
  machineId: plan.machineId,
9229
10037
  mode: "apply",
9230
10038
  steps: plan.steps,
9231
10039
  executed
9232
- };
10040
+ });
9233
10041
  }
9234
10042
 
9235
10043
  // src/commands/install-tailscale.ts
@@ -9269,17 +10077,17 @@ function buildTailscaleInstallPlan(machineId) {
9269
10077
  if (!machine) {
9270
10078
  throw new Error(`Machine not found in manifest: ${machineId}`);
9271
10079
  }
9272
- return {
10080
+ return attachMutationPlanDigest({
9273
10081
  machineId: machine.id,
9274
10082
  mode: "plan",
9275
10083
  steps: buildInstallSteps2(machine),
9276
10084
  executed: 0
9277
- };
10085
+ });
9278
10086
  }
9279
- function runTailscaleInstall(machineId, options = {}, runner = runMachineCommand) {
9280
- const plan = buildTailscaleInstallPlan(machineId);
10087
+ function runTailscaleInstallPlan(plan, options = {}, runner = runMachineCommand) {
10088
+ assertMutationPlanDigest(plan, options.expectedPlanDigest);
9281
10089
  if (!options.apply)
9282
- return plan;
10090
+ return attachMutationPlanDigest({ ...plan, mode: "plan", executed: 0 });
9283
10091
  if (!options.yes) {
9284
10092
  throw new Error("Tailscale install requires --yes.");
9285
10093
  }
@@ -9288,21 +10096,23 @@ function runTailscaleInstall(machineId, options = {}, runner = runMachineCommand
9288
10096
  requireMachineCommandSuccess(`Tailscale install ${step.id}`, runner(plan.machineId, step.command));
9289
10097
  executed += 1;
9290
10098
  }
9291
- return {
10099
+ return attachMutationPlanDigest({
9292
10100
  machineId: plan.machineId,
9293
10101
  mode: "apply",
9294
10102
  steps: plan.steps,
9295
10103
  executed
9296
- };
10104
+ });
9297
10105
  }
9298
10106
 
9299
10107
  // src/commands/notifications.ts
9300
- import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
10108
+ import { accessSync, constants, existsSync as existsSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
10109
+ import { delimiter, isAbsolute, join as join7 } from "path";
9301
10110
  init_paths();
9302
10111
  var notificationChannelSchema = exports_external.object({
9303
10112
  id: exports_external.string(),
9304
10113
  type: exports_external.enum(["email", "webhook", "command"]),
9305
10114
  target: exports_external.string(),
10115
+ commandArgs: exports_external.array(exports_external.string()).optional(),
9306
10116
  events: exports_external.array(exports_external.string()),
9307
10117
  enabled: exports_external.boolean()
9308
10118
  });
@@ -9311,19 +10121,31 @@ var notificationConfigSchema = exports_external.object({
9311
10121
  updatedAt: exports_external.string().optional(),
9312
10122
  channels: exports_external.array(notificationChannelSchema)
9313
10123
  });
10124
+ var trustedNotificationApproval = Symbol("trustedNotificationApproval");
10125
+ function createTrustedNotificationApproval() {
10126
+ return { [trustedNotificationApproval]: true };
10127
+ }
10128
+ function isTrustedNotificationApproval(approval) {
10129
+ return approval?.[trustedNotificationApproval] === true;
10130
+ }
9314
10131
  function sortChannels(channels) {
9315
10132
  return [...channels].sort((left, right) => left.id.localeCompare(right.id));
9316
10133
  }
9317
- function shellQuote5(value) {
9318
- return `'${value.replace(/'/g, `'\\''`)}'`;
9319
- }
9320
10134
  function hasCommand2(binary) {
9321
- const result = Bun.spawnSync(["bash", "-lc", `command -v ${binary} >/dev/null 2>&1`], {
9322
- stdout: "ignore",
9323
- stderr: "ignore",
9324
- env: process.env
9325
- });
9326
- return result.exitCode === 0;
10135
+ return Boolean(resolveExecutable(binary));
10136
+ }
10137
+ function resolveExecutable(binary) {
10138
+ const trimmed = binary.trim();
10139
+ if (!trimmed)
10140
+ return null;
10141
+ const candidates = isAbsolute(trimmed) ? [trimmed] : (process.env.PATH ?? "").split(delimiter).filter(Boolean).map((dir) => join7(dir, trimmed));
10142
+ for (const candidate of candidates) {
10143
+ try {
10144
+ accessSync(candidate, constants.X_OK);
10145
+ return candidate;
10146
+ } catch {}
10147
+ }
10148
+ return null;
9327
10149
  }
9328
10150
  function buildNotificationPreview(channel, event, message) {
9329
10151
  if (channel.type === "email") {
@@ -9332,7 +10154,8 @@ function buildNotificationPreview(channel, event, message) {
9332
10154
  if (channel.type === "webhook") {
9333
10155
  return `POST ${channel.target} with payload {"event":"${event}","message":"${message}"}`;
9334
10156
  }
9335
- return `${channel.target} --event ${event} --message ${JSON.stringify(message)}`;
10157
+ const args = channel.commandArgs?.length ? ` ${channel.commandArgs.join(" ")}` : "";
10158
+ return `${channel.target}${args} with HASNA_MACHINES_NOTIFICATION_* environment`;
9336
10159
  }
9337
10160
  async function dispatchEmail(channel, event, message) {
9338
10161
  const subject = `[${event}] machines notification`;
@@ -9343,7 +10166,7 @@ Content-Type: text/plain; charset=utf-8
9343
10166
  ${message}
9344
10167
  `;
9345
10168
  if (hasCommand2("sendmail")) {
9346
- const result = Bun.spawnSync(["bash", "-lc", "sendmail -t"], {
10169
+ const result = Bun.spawnSync(["sendmail", "-t"], {
9347
10170
  stdin: new TextEncoder().encode(body),
9348
10171
  stdout: "pipe",
9349
10172
  stderr: "pipe",
@@ -9361,8 +10184,9 @@ ${message}
9361
10184
  };
9362
10185
  }
9363
10186
  if (hasCommand2("mail")) {
9364
- const command = `printf %s ${shellQuote5(message)} | mail -s ${shellQuote5(subject)} ${shellQuote5(channel.target)}`;
9365
- const result = Bun.spawnSync(["bash", "-lc", command], {
10187
+ const result = Bun.spawnSync(["mail", "-s", subject, channel.target], {
10188
+ stdin: new TextEncoder().encode(`${message}
10189
+ `),
9366
10190
  stdout: "pipe",
9367
10191
  stderr: "pipe",
9368
10192
  env: process.env
@@ -9405,8 +10229,20 @@ async function dispatchWebhook(channel, event, message) {
9405
10229
  detail: `Webhook accepted with HTTP ${response.status}`
9406
10230
  };
9407
10231
  }
9408
- async function dispatchCommand(channel, event, message) {
9409
- const result = Bun.spawnSync(["bash", "-lc", channel.target], {
10232
+ async function dispatchCommand(channel, event, message, options = {}) {
10233
+ if (!isTrustedNotificationApproval(options.trustedApproval)) {
10234
+ assertMutationApproved({
10235
+ surface: "notifications",
10236
+ operation: "dispatch_command",
10237
+ resourceId: channel.id,
10238
+ approvalToken: options.approvalToken
10239
+ });
10240
+ }
10241
+ const executable = resolveExecutable(channel.target);
10242
+ if (!executable) {
10243
+ throw new Error(`Command executable not found or not executable: ${channel.target}`);
10244
+ }
10245
+ const result = Bun.spawnSync([executable, ...channel.commandArgs ?? []], {
9410
10246
  stdout: "pipe",
9411
10247
  stderr: "pipe",
9412
10248
  env: {
@@ -9428,7 +10264,7 @@ async function dispatchCommand(channel, event, message) {
9428
10264
  detail: stdout || "Command completed successfully"
9429
10265
  };
9430
10266
  }
9431
- async function dispatchChannel(channel, event, message) {
10267
+ async function dispatchChannel(channel, event, message, options = {}) {
9432
10268
  if (!channel.enabled) {
9433
10269
  return {
9434
10270
  channelId: channel.id,
@@ -9444,7 +10280,7 @@ async function dispatchChannel(channel, event, message) {
9444
10280
  if (channel.type === "webhook") {
9445
10281
  return dispatchWebhook(channel, event, message);
9446
10282
  }
9447
- return dispatchCommand(channel, event, message);
10283
+ return dispatchCommand(channel, event, message, options);
9448
10284
  }
9449
10285
  function getDefaultNotificationConfig() {
9450
10286
  return {
@@ -9454,10 +10290,10 @@ function getDefaultNotificationConfig() {
9454
10290
  };
9455
10291
  }
9456
10292
  function readNotificationConfig(path = getNotificationsPath()) {
9457
- if (!existsSync6(path)) {
10293
+ if (!existsSync8(path)) {
9458
10294
  return getDefaultNotificationConfig();
9459
10295
  }
9460
- return notificationConfigSchema.parse(JSON.parse(readFileSync4(path, "utf8")));
10296
+ return notificationConfigSchema.parse(JSON.parse(readFileSync6(path, "utf8")));
9461
10297
  }
9462
10298
  function writeNotificationConfig(config, path = getNotificationsPath()) {
9463
10299
  ensureParentDir(path);
@@ -9466,18 +10302,27 @@ function writeNotificationConfig(config, path = getNotificationsPath()) {
9466
10302
  updatedAt: new Date().toISOString(),
9467
10303
  channels: sortChannels(config.channels)
9468
10304
  };
9469
- writeFileSync3(path, `${JSON.stringify(nextConfig, null, 2)}
10305
+ writeFileSync4(path, `${JSON.stringify(nextConfig, null, 2)}
9470
10306
  `, "utf8");
9471
10307
  return nextConfig;
9472
10308
  }
9473
10309
  function listNotificationChannels() {
9474
10310
  return readNotificationConfig();
9475
10311
  }
9476
- function addNotificationChannel(channel) {
10312
+ function addNotificationChannel(channel, options = {}) {
10313
+ if (channel.type === "command" && !isTrustedNotificationApproval(options.trustedApproval)) {
10314
+ assertMutationApproved({
10315
+ surface: "notifications",
10316
+ operation: "add_command_channel",
10317
+ resourceId: channel.id,
10318
+ approvalToken: options.approvalToken
10319
+ });
10320
+ }
9477
10321
  const config = readNotificationConfig();
9478
10322
  const channels = config.channels.filter((entry) => entry.id !== channel.id);
9479
10323
  channels.push({
9480
10324
  ...channel,
10325
+ commandArgs: channel.commandArgs?.map(String),
9481
10326
  events: [...new Set(channel.events)]
9482
10327
  });
9483
10328
  return writeNotificationConfig({ ...config, channels });
@@ -9499,7 +10344,7 @@ async function dispatchNotificationEvent(event, message, options = {}) {
9499
10344
  const deliveries = [];
9500
10345
  for (const channel of channels) {
9501
10346
  try {
9502
- deliveries.push(await dispatchChannel(channel, event, message));
10347
+ deliveries.push(await dispatchChannel(channel, event, message, { approvalToken: options.approvalToken, trustedApproval: options.trustedApproval }));
9503
10348
  } catch (error) {
9504
10349
  deliveries.push({
9505
10350
  channelId: channel.id,
@@ -9534,7 +10379,7 @@ async function testNotificationChannel(channelId, event = "manual.test", message
9534
10379
  if (!options.yes) {
9535
10380
  throw new Error("Notification test execution requires --yes.");
9536
10381
  }
9537
- const delivery = await dispatchChannel(channel, event, message);
10382
+ const delivery = await dispatchChannel(channel, event, message, { approvalToken: options.approvalToken, trustedApproval: options.trustedApproval });
9538
10383
  return {
9539
10384
  channelId,
9540
10385
  mode: "apply",
@@ -9546,7 +10391,7 @@ async function testNotificationChannel(channelId, event = "manual.test", message
9546
10391
 
9547
10392
  // src/commands/ports.ts
9548
10393
  init_db();
9549
- import { spawnSync as spawnSync3 } from "child_process";
10394
+ import { spawnSync as spawnSync4 } from "child_process";
9550
10395
  function parseSsOutput(output) {
9551
10396
  return output.trim().split(`
9552
10397
  `).map((line) => line.trim()).filter(Boolean).map((line) => {
@@ -9588,7 +10433,7 @@ function listPorts(machineId) {
9588
10433
  const isLocal = targetMachineId === getLocalMachineId();
9589
10434
  const localCommand = "if command -v ss >/dev/null 2>&1; then ss -ltnpH; else lsof -nP -iTCP -sTCP:LISTEN; fi";
9590
10435
  const command = isLocal ? localCommand : buildSshCommand(targetMachineId, localCommand);
9591
- const result = spawnSync3("bash", ["-lc", command], { encoding: "utf8" });
10436
+ const result = spawnSync4("bash", ["-lc", command], { encoding: "utf8" });
9592
10437
  if (result.status !== 0) {
9593
10438
  throw new Error(result.stderr || `Failed to list ports for ${targetMachineId}`);
9594
10439
  }
@@ -9600,12 +10445,52 @@ function listPorts(machineId) {
9600
10445
  }
9601
10446
 
9602
10447
  // src/commands/runtime.ts
9603
- import { spawnSync as spawnSync4 } from "child_process";
10448
+ import { spawnSync as spawnSync5 } from "child_process";
9604
10449
  import { setTimeout as sleep } from "timers/promises";
9605
10450
  import { EventsClient } from "@hasna/events";
10451
+ function shellQuote5(value) {
10452
+ return `'${value.replace(/'/g, "'\\''")}'`;
10453
+ }
10454
+ function buildTmuxPaneDiedHookPlan(options = {}) {
10455
+ const tmuxCommand = options.tmuxCommand ?? process.env["HASNA_MACHINES_TMUX_BIN"] ?? "tmux";
10456
+ const machinesCommand = options.machinesCommand ?? "machines";
10457
+ const deliver = options.deliver === true;
10458
+ const approvalToken = options.approvalToken?.trim();
10459
+ const trustedLocalMutation = approvalToken ? false : options.trustedLocalMutation === true;
10460
+ const emitArgs = [
10461
+ "events",
10462
+ "emit",
10463
+ "machines.tmux.pane_died",
10464
+ "--source",
10465
+ "machines",
10466
+ "--subject",
10467
+ "tmux:#{hook_pane}",
10468
+ "--severity",
10469
+ "warning",
10470
+ "--message",
10471
+ "tmux pane died: #{hook_pane}",
10472
+ "--data",
10473
+ '{"target":"#{hook_pane}","session":"#{session_name}","window":"#{window_index}"}'
10474
+ ];
10475
+ if (!deliver)
10476
+ emitArgs.push("--no-deliver");
10477
+ if (approvalToken)
10478
+ emitArgs.push("--approval-token", approvalToken);
10479
+ const command = [machinesCommand, ...emitArgs].map(shellQuote5).join(" ");
10480
+ const runShell = trustedLocalMutation ? `HASNA_MACHINES_ALLOW_MUTATIONS=1 ${command}` : command;
10481
+ const args = ["set-hook", "-g", "pane-died", `run-shell ${shellQuote5(runShell)}`];
10482
+ return {
10483
+ tmuxCommand,
10484
+ args,
10485
+ shellCommand: [tmuxCommand, ...args].map(shellQuote5).join(" "),
10486
+ eventType: "machines.tmux.pane_died",
10487
+ deliver,
10488
+ trustedLocalMutation
10489
+ };
10490
+ }
9606
10491
  function probeTmuxPane(target, tmuxCommand = process.env["HASNA_MACHINES_TMUX_BIN"] || "tmux") {
9607
10492
  const checkedAt = new Date().toISOString();
9608
- const result = spawnSync4(tmuxCommand, ["display-message", "-p", "-t", target, "#{pane_id}"], {
10493
+ const result = spawnSync5(tmuxCommand, ["display-message", "-p", "-t", target, "#{pane_id}"], {
9609
10494
  encoding: "utf8",
9610
10495
  timeout: 5000
9611
10496
  });
@@ -9697,7 +10582,7 @@ function shellQuote6(value) {
9697
10582
  function shellCommand2(command) {
9698
10583
  return command.map(shellQuote6).join(" ");
9699
10584
  }
9700
- function metadataString2(metadata, keys) {
10585
+ function metadataString3(metadata, keys) {
9701
10586
  if (!metadata)
9702
10587
  return null;
9703
10588
  for (const key of keys) {
@@ -9747,8 +10632,8 @@ function resolveScreenCredentials(machineId, options = {}) {
9747
10632
  const screen = resolveScreenTarget(machineId, { ...options, topology });
9748
10633
  const entry = topology.machines.find((machine) => machine.machine_id === screen.machineId);
9749
10634
  const metadata = entry?.metadata;
9750
- const metadataUser = metadataString2(metadata, ["screenUser", "screen_user", "user", "username"]);
9751
- const metadataPasswordSecret = metadataString2(metadata, [
10635
+ const metadataUser = metadataString3(metadata, ["screenUser", "screen_user", "user", "username"]);
10636
+ const metadataPasswordSecret = metadataString3(metadata, [
9752
10637
  "screenPasswordSecret",
9753
10638
  "screen_password_secret",
9754
10639
  "screenVncPasswordSecret",
@@ -9789,17 +10674,23 @@ function buildScreenEnableCommand(machineId, options = {}) {
9789
10674
  }
9790
10675
  const secretsCommand = options.secretsCommand || "secrets";
9791
10676
  const remoteCommand = buildScreenEnableRemoteCommandFromStdin(credentials.user);
10677
+ const secretsCommandArgs = [secretsCommand, "get", credentials.passwordSecretKey];
10678
+ const sshPlan = buildSshCommandPlan(machineId, remoteCommand, options);
9792
10679
  return {
9793
10680
  machineId: credentials.machineId,
9794
10681
  user: credentials.user,
9795
10682
  passwordSecretKey: credentials.passwordSecretKey,
9796
10683
  remoteCommand,
9797
- command: `${shellCommand2([secretsCommand, "get", credentials.passwordSecretKey])} | ${buildSshCommand(machineId, remoteCommand, options)}`
10684
+ secretsCommand,
10685
+ secretsCommandArgs,
10686
+ sshCommand: sshPlan.command,
10687
+ sshCommandArgs: sshPlan.args,
10688
+ command: `${shellCommand2(secretsCommandArgs)} | ${sshPlan.shellCommand}`
9798
10689
  };
9799
10690
  }
9800
10691
 
9801
10692
  // src/commands/sync.ts
9802
- import { existsSync as existsSync7, lstatSync, readFileSync as readFileSync5, symlinkSync, copyFileSync } from "fs";
10693
+ import { existsSync as existsSync9, lstatSync, readFileSync as readFileSync7, symlinkSync, copyFileSync } from "fs";
9803
10694
  import { homedir as homedir5 } from "os";
9804
10695
  init_paths();
9805
10696
  init_db();
@@ -9854,15 +10745,15 @@ function detectFileActions(machine) {
9854
10745
  throw new Error(`Remote file sync planning is not supported for ${machine.id}; refusing to inspect or apply local paths as remote state.`);
9855
10746
  }
9856
10747
  return (machine.files || []).map((file, index) => {
9857
- const sourceExists = existsSync7(file.source);
9858
- const targetExists = existsSync7(file.target);
10748
+ const sourceExists = existsSync9(file.source);
10749
+ const targetExists = existsSync9(file.target);
9859
10750
  let status = "missing";
9860
10751
  if (sourceExists && targetExists) {
9861
10752
  if (file.mode === "symlink") {
9862
10753
  status = lstatSync(file.target).isSymbolicLink() ? "ok" : "drifted";
9863
10754
  } else {
9864
- const source = readFileSync5(file.source, "utf8");
9865
- const target = readFileSync5(file.target, "utf8");
10755
+ const source = readFileSync7(file.source, "utf8");
10756
+ const target = readFileSync7(file.target, "utf8");
9866
10757
  status = source === target ? "ok" : "drifted";
9867
10758
  }
9868
10759
  }
@@ -9892,12 +10783,12 @@ function buildSyncPlan(machineId, runner = runMachineCommand) {
9892
10783
  ...detectPackageActions(target, runner),
9893
10784
  ...detectFileActions(target)
9894
10785
  ];
9895
- return {
10786
+ return attachMutationPlanDigest({
9896
10787
  machineId: target.id,
9897
10788
  mode: "plan",
9898
10789
  actions,
9899
10790
  executed: 0
9900
- };
10791
+ });
9901
10792
  }
9902
10793
  function applyFileAction(command) {
9903
10794
  const [verb, source, target] = command.split(" ");
@@ -9919,10 +10810,10 @@ function applyFileAction(command) {
9919
10810
  symlinkSync(sourcePath, targetPath);
9920
10811
  }
9921
10812
  }
9922
- function runSync(machineId, options = {}, runner = runMachineCommand) {
9923
- const plan = buildSyncPlan(machineId, runner);
10813
+ function runSyncPlan(plan, options = {}, runner = runMachineCommand) {
10814
+ assertMutationPlanDigest(plan, options.expectedPlanDigest);
9924
10815
  if (!options.apply) {
9925
- return plan;
10816
+ return attachMutationPlanDigest({ ...plan, mode: "plan", executed: 0 });
9926
10817
  }
9927
10818
  if (!options.yes) {
9928
10819
  throw new Error("Sync execution requires --yes.");
@@ -9949,12 +10840,12 @@ function runSync(machineId, options = {}, runner = runMachineCommand) {
9949
10840
  }
9950
10841
  executed += 1;
9951
10842
  }
9952
- const summary = {
10843
+ const summary = attachMutationPlanDigest({
9953
10844
  machineId: plan.machineId,
9954
10845
  mode: "apply",
9955
10846
  actions: plan.actions,
9956
10847
  executed
9957
- };
10848
+ });
9958
10849
  recordSyncRun(plan.machineId, "completed", summary);
9959
10850
  return summary;
9960
10851
  }
@@ -10215,7 +11106,7 @@ function parseKeyValue(stdout) {
10215
11106
  }
10216
11107
  return result;
10217
11108
  }
10218
- function defaultRunner2(machineId, command) {
11109
+ function defaultRunner3(machineId, command) {
10219
11110
  return runMachineCommand(machineId, command);
10220
11111
  }
10221
11112
  function inspectCommand(machineId, spec, runner) {
@@ -10369,7 +11260,7 @@ function workspaceCheck(machineId, spec, runner) {
10369
11260
  }
10370
11261
  function checkMachineCompatibility(options = {}) {
10371
11262
  const machineId = options.machineId ?? getLocalMachineId();
10372
- const runner = options.runner ?? defaultRunner2;
11263
+ const runner = options.runner ?? defaultRunner3;
10373
11264
  const commands = options.commands ?? DEFAULT_COMMANDS;
10374
11265
  const packages = options.packages ?? defaultPackages();
10375
11266
  const workspaces = options.workspaces ?? [];
@@ -10589,9 +11480,9 @@ function runDoctor(machineId, options = {}) {
10589
11480
 
10590
11481
  // src/commands/daemon.ts
10591
11482
  import { execFileSync } from "child_process";
10592
- import { chmodSync, existsSync as existsSync8, readFileSync as readFileSync6, statSync, mkdirSync as mkdirSync2, writeFileSync as writeFileSync4 } from "fs";
10593
- import { delimiter, dirname as dirname4 } from "path";
10594
- import { platform as osPlatform } from "os";
11483
+ import { chmodSync, existsSync as existsSync10, readFileSync as readFileSync8, statSync, mkdirSync as mkdirSync2, writeFileSync as writeFileSync5 } from "fs";
11484
+ import { delimiter as delimiter2, dirname as dirname4 } from "path";
11485
+ import { platform as osPlatform2 } from "os";
10595
11486
  var DEFAULT_SERVICE_NAME = "machines-agent";
10596
11487
  var DEFAULT_EXECUTABLE = "/usr/local/bin/machines-agent";
10597
11488
  var DEFAULT_INTERVAL_MS = 30000;
@@ -10648,7 +11539,7 @@ function runDaemonServicePlan(plan, options = {}) {
10648
11539
  };
10649
11540
  }
10650
11541
  mkdirSync2(dirname4(path), { recursive: true });
10651
- writeFileSync4(path, content, "utf8");
11542
+ writeFileSync5(path, content, "utf8");
10652
11543
  chmodSync(path, Number.parseInt(file.mode, 8));
10653
11544
  filesWritten.push(path);
10654
11545
  }
@@ -10743,7 +11634,7 @@ function normalizeServiceName(value, warnings) {
10743
11634
  return DEFAULT_SERVICE_NAME;
10744
11635
  }
10745
11636
  function normalizePlatform3(value, warnings) {
10746
- const raw = value ?? osPlatform();
11637
+ const raw = value ?? osPlatform2();
10747
11638
  if (raw === "darwin" || raw === "macos")
10748
11639
  return "macos";
10749
11640
  if (raw === "linux")
@@ -10998,13 +11889,13 @@ function bunRuntimeCandidates(executable) {
10998
11889
  `${dirname4(executable)}/bun`,
10999
11890
  process.env["BUN_INSTALL"] ? `${process.env["BUN_INSTALL"]}/bin/bun` : null,
11000
11891
  process.env["HOME"] ? `${process.env["HOME"]}/.bun/bin/bun` : null,
11001
- ...(process.env["PATH"] ?? "").split(delimiter).filter(Boolean).map((entry) => `${entry}/bun`)
11892
+ ...(process.env["PATH"] ?? "").split(delimiter2).filter(Boolean).map((entry) => `${entry}/bun`)
11002
11893
  ].filter((value) => Boolean(value));
11003
11894
  return [...new Set(candidates)];
11004
11895
  }
11005
11896
  function isBunShebangScript(executable) {
11006
11897
  try {
11007
- const content = readFileSync6(executable, "utf8").slice(0, 256);
11898
+ const content = readFileSync8(executable, "utf8").slice(0, 256);
11008
11899
  const firstLine2 = content.split(/\r?\n/, 1)[0] ?? "";
11009
11900
  return /^#!.*\bbun\b/.test(firstLine2);
11010
11901
  } catch {
@@ -11012,7 +11903,7 @@ function isBunShebangScript(executable) {
11012
11903
  }
11013
11904
  }
11014
11905
  function isExecutableFile(path) {
11015
- if (!existsSync8(path))
11906
+ if (!existsSync10(path))
11016
11907
  return false;
11017
11908
  try {
11018
11909
  const stats = statSync(path);
@@ -11062,7 +11953,8 @@ function basename(path) {
11062
11953
  init_db();
11063
11954
 
11064
11955
  // src/commands/serve.ts
11065
- import { EventsClient as EventsClient2, sanitizeChannelsForOutput } from "@hasna/events";
11956
+ import { EventsClient as EventsClient2, getEventsDataDir, sanitizeChannelsForOutput } from "@hasna/events";
11957
+ import { resolve as resolve3 } from "path";
11066
11958
 
11067
11959
  // src/agent/runtime.ts
11068
11960
  import { arch as arch3, hostname as hostname6, platform as platform4, release, uptime, version as osVersion } from "os";
@@ -11149,7 +12041,7 @@ function escapeHtml(value) {
11149
12041
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
11150
12042
  }
11151
12043
  function getServeInfo(options = {}) {
11152
- const host = options.host || "0.0.0.0";
12044
+ const host = options.host || "127.0.0.1";
11153
12045
  const port = options.port || 7676;
11154
12046
  return {
11155
12047
  host,
@@ -11352,6 +12244,59 @@ async function parseJsonBody(request) {
11352
12244
  function jsonError(message, status = 400) {
11353
12245
  return Response.json({ error: message }, { status });
11354
12246
  }
12247
+ function dashboardResourceId(kind, ...parts) {
12248
+ const values = parts.map((part) => String(part ?? "*").trim()).filter(Boolean).join(":");
12249
+ return values ? `${kind}:${values}` : kind;
12250
+ }
12251
+ function eventStoreDir() {
12252
+ return resolve3(getEventsDataDir());
12253
+ }
12254
+ function eventStoreScope() {
12255
+ return { event_store_dir: eventStoreDir() };
12256
+ }
12257
+ function eventStoreResourceId(kind, ...parts) {
12258
+ return dashboardResourceId(kind, mutationArgsSha256(eventStoreScope()), ...parts);
12259
+ }
12260
+ function withEventStoreScope(args) {
12261
+ return { event_store_dir: eventStoreDir(), ...args };
12262
+ }
12263
+ function dashboardMutationCallerId() {
12264
+ return process.env[MUTATION_APPROVAL_CALLER_ENV]?.trim() || "dashboard";
12265
+ }
12266
+ function dashboardMutationRunId() {
12267
+ return process.env[MUTATION_APPROVAL_RUN_ENV]?.trim() || "dashboard";
12268
+ }
12269
+ function approvalTokenFromRequest(request, body) {
12270
+ const bodyToken = typeof body["approval_token"] === "string" ? body["approval_token"] : typeof body["approvalToken"] === "string" ? body["approvalToken"] : undefined;
12271
+ if (bodyToken?.trim())
12272
+ return bodyToken;
12273
+ const headerToken = request.headers.get("x-hasna-approval-token")?.trim();
12274
+ if (headerToken)
12275
+ return headerToken;
12276
+ const authorization = request.headers.get("authorization")?.trim();
12277
+ if (authorization?.toLowerCase().startsWith("bearer ")) {
12278
+ return authorization.slice("bearer ".length).trim();
12279
+ }
12280
+ return;
12281
+ }
12282
+ function requireDashboardMutation(operation, request, body, scope = {}) {
12283
+ const decision = verifyMutationApprovalToken({
12284
+ surface: "dashboard",
12285
+ operation,
12286
+ transport: "dashboard:http",
12287
+ callerId: dashboardMutationCallerId(),
12288
+ runId: dashboardMutationRunId(),
12289
+ resourceId: scope.resourceId,
12290
+ args: scope.args,
12291
+ approvalToken: approvalTokenFromRequest(request, body)
12292
+ });
12293
+ if (decision.approved)
12294
+ return;
12295
+ return jsonError(`Mutation approval denied: ${decision.reason ?? "approval_token is invalid."}`, 403);
12296
+ }
12297
+ function objectBodyValue(value) {
12298
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
12299
+ }
11355
12300
  function privateOutputWarnings(requested, allowed) {
11356
12301
  return requested && !allowed ? [PRIVATE_OUTPUT_DENIED_WARNING] : [];
11357
12302
  }
@@ -11363,6 +12308,7 @@ function appendWarnings(payload, warnings) {
11363
12308
  function startDashboardServer(options = {}) {
11364
12309
  const info = getServeInfo(options);
11365
12310
  const events = new EventsClient2;
12311
+ const trustedNotificationApproval2 = createTrustedNotificationApproval();
11366
12312
  return Bun.serve({
11367
12313
  hostname: info.host,
11368
12314
  port: info.port,
@@ -11427,8 +12373,25 @@ function startDashboardServer(options = {}) {
11427
12373
  const severity = typeof body["severity"] === "string" ? body["severity"] : undefined;
11428
12374
  const message = typeof body["message"] === "string" ? body["message"] : undefined;
11429
12375
  const dedupeKey = typeof body["dedupeKey"] === "string" ? body["dedupeKey"] : undefined;
11430
- const data = body["data"] && typeof body["data"] === "object" && !Array.isArray(body["data"]) ? body["data"] : {};
11431
- const metadata = body["metadata"] && typeof body["metadata"] === "object" && !Array.isArray(body["metadata"]) ? body["metadata"] : {};
12376
+ const data = objectBodyValue(body["data"]);
12377
+ const metadata = objectBodyValue(body["metadata"]);
12378
+ const denied = requireDashboardMutation("machines_events_emit", request, body, {
12379
+ resourceId: eventStoreResourceId("event", type, subject, dedupeKey),
12380
+ args: withEventStoreScope({
12381
+ event_type: type,
12382
+ source,
12383
+ subject,
12384
+ severity,
12385
+ message,
12386
+ data,
12387
+ metadata,
12388
+ dedupe_key: dedupeKey,
12389
+ deliver: true,
12390
+ dedupe: true
12391
+ })
12392
+ });
12393
+ if (denied)
12394
+ return denied;
11432
12395
  return Response.json(await events.emit({
11433
12396
  source,
11434
12397
  type,
@@ -11471,8 +12434,20 @@ function startDashboardServer(options = {}) {
11471
12434
  const message = typeof body["message"] === "string" ? body["message"] : undefined;
11472
12435
  const apply = body["apply"] === true;
11473
12436
  const yes = body["yes"] === true;
12437
+ const resolvedEvent = event ?? "manual.test";
12438
+ const resolvedMessage = message ?? "machines notification test";
12439
+ const denied = requireDashboardMutation("machines_notifications_test", request, body, {
12440
+ resourceId: dashboardResourceId("notification-test", channelId, resolvedEvent),
12441
+ args: { channel_id: channelId, event: resolvedEvent, message: resolvedMessage, apply, yes }
12442
+ });
12443
+ if (denied)
12444
+ return denied;
11474
12445
  try {
11475
- return Response.json(await testNotificationChannel(channelId, event, message, { apply, yes }));
12446
+ return Response.json(await testNotificationChannel(channelId, resolvedEvent, resolvedMessage, {
12447
+ apply,
12448
+ yes,
12449
+ trustedApproval: apply ? trustedNotificationApproval2 : undefined
12450
+ }));
11476
12451
  } catch (error) {
11477
12452
  return jsonError(error instanceof Error ? error.message : String(error));
11478
12453
  }
@@ -11487,13 +12462,22 @@ function startDashboardServer(options = {}) {
11487
12462
  return jsonError("channelId is required.");
11488
12463
  }
11489
12464
  const type = typeof body["type"] === "string" ? body["type"] : "events.test";
11490
- const message = typeof body["message"] === "string" ? body["message"] : undefined;
12465
+ const subject = channelId;
12466
+ const message = typeof body["message"] === "string" ? body["message"] : "Hasna events test delivery";
12467
+ const data = objectBodyValue(body["data"]);
12468
+ const denied = requireDashboardMutation("machines_webhooks_test", request, body, {
12469
+ resourceId: eventStoreResourceId("webhook-test", channelId, type),
12470
+ args: withEventStoreScope({ channel_id: channelId, event_type: type, subject, message, data })
12471
+ });
12472
+ if (denied)
12473
+ return denied;
11491
12474
  try {
11492
12475
  return Response.json(await events.testChannel(channelId, {
11493
12476
  source: "machines",
11494
12477
  type,
11495
- subject: channelId,
11496
- message
12478
+ subject,
12479
+ message,
12480
+ data
11497
12481
  }));
11498
12482
  } catch (error) {
11499
12483
  return jsonError(error instanceof Error ? error.message : String(error));
@@ -11541,9 +12525,9 @@ function runSelfTest() {
11541
12525
 
11542
12526
  // src/commands/clipboard.ts
11543
12527
  init_paths();
11544
- import { createHash } from "crypto";
11545
- import { existsSync as existsSync9, readFileSync as readFileSync7, rmSync, writeFileSync as writeFileSync5 } from "fs";
11546
- import { join as join6 } from "path";
12528
+ import { createHash as createHash2 } from "crypto";
12529
+ import { existsSync as existsSync11, readFileSync as readFileSync9, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
12530
+ import { join as join8 } from "path";
11547
12531
  var DEFAULT_CONFIG = {
11548
12532
  version: 1,
11549
12533
  enabled: true,
@@ -11561,7 +12545,7 @@ var DEFAULT_CONFIG = {
11561
12545
  function resolveConfigPath(configPath) {
11562
12546
  if (configPath)
11563
12547
  return configPath;
11564
- return join6(getDataDir(), "clipboard-config.json");
12548
+ return join8(getDataDir(), "clipboard-config.json");
11565
12549
  }
11566
12550
  function resolveHistoryPath(historyPath) {
11567
12551
  if (historyPath)
@@ -11573,25 +12557,25 @@ function getDefaultConfig() {
11573
12557
  }
11574
12558
  function readConfig(configPath) {
11575
12559
  const path = resolveConfigPath(configPath);
11576
- if (!existsSync9(path)) {
12560
+ if (!existsSync11(path)) {
11577
12561
  return getDefaultConfig();
11578
12562
  }
11579
- const parsed = JSON.parse(readFileSync7(path, "utf8"));
12563
+ const parsed = JSON.parse(readFileSync9(path, "utf8"));
11580
12564
  return { ...getDefaultConfig(), ...parsed };
11581
12565
  }
11582
12566
  function writeConfig(config, configPath) {
11583
12567
  const path = resolveConfigPath(configPath);
11584
12568
  ensureParentDir(path);
11585
- writeFileSync5(path, `${JSON.stringify(config, null, 2)}
12569
+ writeFileSync6(path, `${JSON.stringify(config, null, 2)}
11586
12570
  `, "utf8");
11587
12571
  }
11588
12572
  function readHistory(historyPath) {
11589
12573
  const path = resolveHistoryPath(historyPath);
11590
- if (!existsSync9(path)) {
12574
+ if (!existsSync11(path)) {
11591
12575
  return [];
11592
12576
  }
11593
12577
  try {
11594
- return JSON.parse(readFileSync7(path, "utf8"));
12578
+ return JSON.parse(readFileSync9(path, "utf8"));
11595
12579
  } catch {
11596
12580
  return [];
11597
12581
  }
@@ -11599,11 +12583,11 @@ function readHistory(historyPath) {
11599
12583
  function writeHistory(entries, historyPath) {
11600
12584
  const path = resolveHistoryPath(historyPath);
11601
12585
  ensureParentDir(path);
11602
- writeFileSync5(path, `${JSON.stringify(entries, null, 2)}
12586
+ writeFileSync6(path, `${JSON.stringify(entries, null, 2)}
11603
12587
  `, "utf8");
11604
12588
  }
11605
12589
  function computeHash(content) {
11606
- return createHash("sha256").update(content).digest("hex").slice(0, 16);
12590
+ return createHash2("sha256").update(content).digest("hex").slice(0, 16);
11607
12591
  }
11608
12592
  function shouldSkipContent(content, skipPatterns) {
11609
12593
  const lower = content.toLowerCase();
@@ -11620,12 +12604,12 @@ function sanitizeClipboardForRead(content, maxSizeBytes, skipPatterns) {
11620
12604
  }
11621
12605
  function getOrCreateClipboardKey() {
11622
12606
  const keyPath = getClipboardKeyPath();
11623
- if (existsSync9(keyPath)) {
11624
- return readFileSync7(keyPath, "utf8").trim();
12607
+ if (existsSync11(keyPath)) {
12608
+ return readFileSync9(keyPath, "utf8").trim();
11625
12609
  }
11626
- const key = createHash("sha256").update(crypto.randomUUID()).digest("hex").slice(0, 32);
12610
+ const key = createHash2("sha256").update(crypto.randomUUID()).digest("hex").slice(0, 32);
11627
12611
  ensureParentDir(keyPath);
11628
- writeFileSync5(keyPath, `${key}
12612
+ writeFileSync6(keyPath, `${key}
11629
12613
  `, "utf8");
11630
12614
  return key;
11631
12615
  }
@@ -11660,8 +12644,8 @@ function addClipboardEntry(entry, historyPath) {
11660
12644
  }
11661
12645
  function clearClipboardHistory(historyPath) {
11662
12646
  const path = resolveHistoryPath(historyPath);
11663
- if (existsSync9(path)) {
11664
- rmSync(path);
12647
+ if (existsSync11(path)) {
12648
+ rmSync2(path);
11665
12649
  }
11666
12650
  }
11667
12651
  function getClipboardStatus(historyPath) {
@@ -11676,15 +12660,15 @@ function getClipboardStatus(historyPath) {
11676
12660
 
11677
12661
  // src/commands/clipboard-daemon.ts
11678
12662
  init_paths();
11679
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "fs";
11680
- import { join as join7 } from "path";
11681
- import { createHash as createHash3 } from "crypto";
12663
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync7 } from "fs";
12664
+ import { join as join9 } from "path";
12665
+ import { createHash as createHash4 } from "crypto";
11682
12666
 
11683
12667
  // src/commands/clipboard-server.ts
11684
12668
  init_paths();
11685
12669
  import { createServer } from "http";
11686
- import { createHash as createHash2 } from "crypto";
11687
- import { readFileSync as readFileSync8 } from "fs";
12670
+ import { createHash as createHash3 } from "crypto";
12671
+ import { readFileSync as readFileSync10 } from "fs";
11688
12672
  function readLocalClipboardSync() {
11689
12673
  const platform5 = process.platform;
11690
12674
  if (platform5 === "darwin") {
@@ -11730,7 +12714,7 @@ function hasCommand3(binary) {
11730
12714
  function loadSharedSecret() {
11731
12715
  const keyPath = getClipboardKeyPath();
11732
12716
  try {
11733
- return readFileSync8(keyPath, "utf8").trim();
12717
+ return readFileSync10(keyPath, "utf8").trim();
11734
12718
  } catch {
11735
12719
  return "";
11736
12720
  }
@@ -11744,7 +12728,7 @@ function authenticate(request) {
11744
12728
  const secret = loadSharedSecret();
11745
12729
  if (!secret)
11746
12730
  return false;
11747
- return createHash2("sha256").update(token).digest("hex") === createHash2("sha256").update(secret).digest("hex");
12731
+ return createHash3("sha256").update(token).digest("hex") === createHash3("sha256").update(secret).digest("hex");
11748
12732
  }
11749
12733
  function jsonResponse(response, status, data) {
11750
12734
  response.writeHead(status, { "content-type": "application/json" });
@@ -11784,7 +12768,7 @@ function startClipboardServer(options = {}) {
11784
12768
  server,
11785
12769
  port,
11786
12770
  close: async () => {
11787
- await new Promise((resolve2) => server.close(() => resolve2()));
12771
+ await new Promise((resolve4) => server.close(() => resolve4()));
11788
12772
  }
11789
12773
  };
11790
12774
  }
@@ -11835,7 +12819,7 @@ function handleGetClipboard(response, config) {
11835
12819
  }
11836
12820
 
11837
12821
  // src/commands/clipboard-daemon.ts
11838
- var DAEMON_PID_PATH = join7(getDataDir(), "clipboard-daemon.pid");
12822
+ var DAEMON_PID_PATH = join9(getDataDir(), "clipboard-daemon.pid");
11839
12823
  function readLocalClipboardSync2() {
11840
12824
  const platform5 = process.platform;
11841
12825
  if (platform5 === "darwin") {
@@ -11893,22 +12877,22 @@ function hasDisplayServer() {
11893
12877
  return false;
11894
12878
  }
11895
12879
  function computeHash2(content) {
11896
- return createHash3("sha256").update(content).digest("hex").slice(0, 16);
12880
+ return createHash4("sha256").update(content).digest("hex").slice(0, 16);
11897
12881
  }
11898
12882
  function loadSharedSecret2() {
11899
12883
  try {
11900
- return readFileSync9(getClipboardKeyPath(), "utf8").trim();
12884
+ return readFileSync11(getClipboardKeyPath(), "utf8").trim();
11901
12885
  } catch {
11902
12886
  return "";
11903
12887
  }
11904
12888
  }
11905
12889
  function writePid(pid) {
11906
- writeFileSync6(DAEMON_PID_PATH, `${pid}
12890
+ writeFileSync7(DAEMON_PID_PATH, `${pid}
11907
12891
  `);
11908
12892
  }
11909
12893
  function readPid() {
11910
12894
  try {
11911
- const pid = Number.parseInt(readFileSync9(DAEMON_PID_PATH, "utf8").trim());
12895
+ const pid = Number.parseInt(readFileSync11(DAEMON_PID_PATH, "utf8").trim());
11912
12896
  return Number.isFinite(pid) ? pid : null;
11913
12897
  } catch {
11914
12898
  return null;
@@ -12009,8 +12993,8 @@ async function discoverPeers() {
12009
12993
 
12010
12994
  // src/commands/heal.ts
12011
12995
  init_paths();
12012
- import { existsSync as existsSync10, readFileSync as readFileSync10, writeFileSync as writeFileSync7 } from "fs";
12013
- import { join as join8 } from "path";
12996
+ import { existsSync as existsSync12, readFileSync as readFileSync12, writeFileSync as writeFileSync8 } from "fs";
12997
+ import { join as join10 } from "path";
12014
12998
  var DEFAULT_THRESHOLDS = {
12015
12999
  reconnect: 3,
12016
13000
  nmRestart: 7,
@@ -12054,16 +13038,16 @@ function defaultHealState() {
12054
13038
  };
12055
13039
  }
12056
13040
  function getHealConfigPath() {
12057
- return process.env["HASNA_MACHINES_HEAL_CONFIG_PATH"] || join8(getDataDir(), "heal-config.json");
13041
+ return process.env["HASNA_MACHINES_HEAL_CONFIG_PATH"] || join10(getDataDir(), "heal-config.json");
12058
13042
  }
12059
13043
  function getHealStatePath() {
12060
- return process.env["HASNA_MACHINES_HEAL_STATE_PATH"] || join8(getDataDir(), "heal-state.json");
13044
+ return process.env["HASNA_MACHINES_HEAL_STATE_PATH"] || join10(getDataDir(), "heal-state.json");
12061
13045
  }
12062
13046
  function readHealConfig(path) {
12063
13047
  const p = path || getHealConfigPath();
12064
- if (!existsSync10(p))
13048
+ if (!existsSync12(p))
12065
13049
  return { ...DEFAULT_HEAL_CONFIG, thresholds: { ...DEFAULT_THRESHOLDS } };
12066
- const parsed = JSON.parse(readFileSync10(p, "utf8"));
13050
+ const parsed = JSON.parse(readFileSync12(p, "utf8"));
12067
13051
  return {
12068
13052
  ...DEFAULT_HEAL_CONFIG,
12069
13053
  ...parsed,
@@ -12074,15 +13058,15 @@ function readHealConfig(path) {
12074
13058
  function writeHealConfig(config, path) {
12075
13059
  const p = path || getHealConfigPath();
12076
13060
  ensureParentDir(p);
12077
- writeFileSync7(p, `${JSON.stringify(config, null, 2)}
13061
+ writeFileSync8(p, `${JSON.stringify(config, null, 2)}
12078
13062
  `, "utf8");
12079
13063
  }
12080
13064
  function readHealState(path) {
12081
13065
  const p = path || getHealStatePath();
12082
- if (!existsSync10(p))
13066
+ if (!existsSync12(p))
12083
13067
  return defaultHealState();
12084
13068
  try {
12085
- return { ...defaultHealState(), ...JSON.parse(readFileSync10(p, "utf8")) };
13069
+ return { ...defaultHealState(), ...JSON.parse(readFileSync12(p, "utf8")) };
12086
13070
  } catch {
12087
13071
  return defaultHealState();
12088
13072
  }
@@ -12090,7 +13074,7 @@ function readHealState(path) {
12090
13074
  function writeHealState(state, path) {
12091
13075
  const p = path || getHealStatePath();
12092
13076
  ensureParentDir(p);
12093
- writeFileSync7(p, `${JSON.stringify(state, null, 2)}
13077
+ writeFileSync8(p, `${JSON.stringify(state, null, 2)}
12094
13078
  `, "utf8");
12095
13079
  }
12096
13080
  function evaluateHealth(probe, config, state) {
@@ -12209,7 +13193,7 @@ function sh(cmd, timeoutMs = 8000) {
12209
13193
  }
12210
13194
  function getCurrentBootId() {
12211
13195
  try {
12212
- return readFileSync10("/proc/sys/kernel/random/boot_id", "utf8").trim();
13196
+ return readFileSync12("/proc/sys/kernel/random/boot_id", "utf8").trim();
12213
13197
  } catch {
12214
13198
  return "";
12215
13199
  }
@@ -12295,9 +13279,9 @@ function executeAction(action, config) {
12295
13279
 
12296
13280
  // src/commands/heal-daemon.ts
12297
13281
  init_paths();
12298
- import { existsSync as existsSync11, readFileSync as readFileSync11, writeFileSync as writeFileSync8 } from "fs";
12299
- import { join as join9 } from "path";
12300
- var DAEMON_PID_PATH2 = join9(getDataDir(), "heal-daemon.pid");
13282
+ import { existsSync as existsSync13, readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "fs";
13283
+ import { join as join11 } from "path";
13284
+ var DAEMON_PID_PATH2 = join11(getDataDir(), "heal-daemon.pid");
12301
13285
  var SERVICE_PATH = "/etc/systemd/system/machines-heal.service";
12302
13286
  var SYSTEM_CONF = "/etc/systemd/system.conf";
12303
13287
  function log(msg) {
@@ -12338,12 +13322,12 @@ function runHealOnce(config, opts = {}) {
12338
13322
  return result;
12339
13323
  }
12340
13324
  function writePid2(pid) {
12341
- writeFileSync8(DAEMON_PID_PATH2, `${pid}
13325
+ writeFileSync9(DAEMON_PID_PATH2, `${pid}
12342
13326
  `);
12343
13327
  }
12344
13328
  function readPid2() {
12345
13329
  try {
12346
- const pid = Number.parseInt(readFileSync11(DAEMON_PID_PATH2, "utf8").trim());
13330
+ const pid = Number.parseInt(readFileSync13(DAEMON_PID_PATH2, "utf8").trim());
12347
13331
  return Number.isFinite(pid) ? pid : null;
12348
13332
  } catch {
12349
13333
  return null;
@@ -12415,9 +13399,9 @@ function applyDeterminism(config) {
12415
13399
  }
12416
13400
  function enableHardwareWatchdog() {
12417
13401
  const log2 = [];
12418
- if (!existsSync11(SYSTEM_CONF))
13402
+ if (!existsSync13(SYSTEM_CONF))
12419
13403
  return ["/etc/systemd/system.conf not found; skipping hardware watchdog"];
12420
- let conf = readFileSync11(SYSTEM_CONF, "utf8");
13404
+ let conf = readFileSync13(SYSTEM_CONF, "utf8");
12421
13405
  const set = (key, value) => {
12422
13406
  const re = new RegExp(`^#?\\s*${key}=.*$`, "m");
12423
13407
  if (re.test(conf))
@@ -12429,7 +13413,7 @@ ${key}=${value}
12429
13413
  };
12430
13414
  set("RuntimeWatchdogSec", "20s");
12431
13415
  set("RebootWatchdogSec", "2min");
12432
- writeFileSync8(SYSTEM_CONF, conf);
13416
+ writeFileSync9(SYSTEM_CONF, conf);
12433
13417
  sh2("systemctl daemon-reexec");
12434
13418
  log2.push("hardware watchdog: RuntimeWatchdogSec=20s RebootWatchdogSec=2min");
12435
13419
  return log2;
@@ -12447,7 +13431,7 @@ function binPath() {
12447
13431
  candidates.push(`${home}/.bun/bin/machines`);
12448
13432
  candidates.push("/root/.bun/bin/machines", "/usr/local/bin/machines");
12449
13433
  for (const c of candidates) {
12450
- if (c && existsSync11(c))
13434
+ if (c && existsSync13(c))
12451
13435
  return c;
12452
13436
  }
12453
13437
  return "machines";
@@ -12474,7 +13458,7 @@ Environment=PATH=${binDir}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sb
12474
13458
  [Install]
12475
13459
  WantedBy=multi-user.target
12476
13460
  `;
12477
- writeFileSync8(SERVICE_PATH, unit);
13461
+ writeFileSync9(SERVICE_PATH, unit);
12478
13462
  sh2("systemctl daemon-reload");
12479
13463
  sh2("systemctl enable --now machines-heal.service");
12480
13464
  log2.push(`installed + enabled ${SERVICE_PATH} (ExecStart=${exec} heal daemon)`);
@@ -12483,7 +13467,7 @@ WantedBy=multi-user.target
12483
13467
  function uninstallHealService() {
12484
13468
  const log2 = [];
12485
13469
  sh2("systemctl disable --now machines-heal.service 2>/dev/null || true");
12486
- if (existsSync11(SERVICE_PATH)) {
13470
+ if (existsSync13(SERVICE_PATH)) {
12487
13471
  sh2(`rm -f ${SERVICE_PATH}`);
12488
13472
  sh2("systemctl daemon-reload");
12489
13473
  log2.push(`removed ${SERVICE_PATH}`);
@@ -12494,7 +13478,7 @@ function uninstallHealService() {
12494
13478
  }
12495
13479
  function healServiceStatus() {
12496
13480
  return {
12497
- installed: existsSync11(SERVICE_PATH),
13481
+ installed: existsSync13(SERVICE_PATH),
12498
13482
  active: sh2("systemctl is-active machines-heal.service").out === "active",
12499
13483
  enabled: sh2("systemctl is-enabled machines-heal.service 2>/dev/null").out === "enabled"
12500
13484
  };
@@ -12533,8 +13517,6 @@ ${items.map((item) => `- ${item}`).join(`
12533
13517
  }
12534
13518
 
12535
13519
  // src/cli/index.ts
12536
- import { rmSync as rmSync2 } from "fs";
12537
- import { readFileSync as readFileSync12 } from "fs";
12538
13520
  var program2 = new Command;
12539
13521
  function printJsonOrText(data, text, json = false) {
12540
13522
  if (json || program2.opts().quiet) {
@@ -12802,23 +13784,307 @@ program2.name("machines").description("Machine fleet management CLI + MCP for de
12802
13784
  var manifestCommand = program2.command("manifest").description("Manage the fleet manifest");
12803
13785
  var appsCommand = program2.command("apps").description("Manage installed applications per machine");
12804
13786
  var notificationsCommand = program2.command("notifications").description("Manage fleet alert delivery channels");
12805
- var eventWebhooksCommand = registerWebhookCommands(program2, { source: "machines" });
12806
- eventWebhooksCommand.description("Manage shared event webhook subscriptions");
12807
- var webhookTestCommand = eventWebhooksCommand.commands.find((command2) => command2.name() === "test");
12808
- var webhookOptions = webhookTestCommand?.options ?? [];
12809
- var webhookMessageOption = webhookOptions.find((option) => option.long === "--message");
12810
- if (webhookMessageOption) {
12811
- webhookMessageOption.defaultValue = "Shared events test delivery";
12812
- }
12813
- var eventsCommand = registerEventCommands(program2, { source: "machines" });
12814
- eventsCommand.description("Emit, list, and replay shared events");
13787
+ var eventWebhooksCommand = program2.command("webhooks").description("Manage shared event webhook subscriptions");
13788
+ var eventsCommand = program2.command("events").description("Emit, list, and replay shared events");
12815
13789
  var runtimeCommand = program2.command("runtime").description("Watch runtime conditions and emit shared events");
12816
13790
  var clipboardCommand = program2.command("clipboard").description("Real-time clipboard sync across fleet machines");
12817
13791
  var installClaudeCommand = program2.command("install-claude").description("Install or inspect Claude, Codex, and Gemini CLIs");
12818
13792
  var daemonCommand = program2.command("daemon").description("Install and inspect the machines-agent fleet daemon service");
13793
+ var trustedNotificationApproval2 = createTrustedNotificationApproval();
13794
+ function cliMachineId(machineId) {
13795
+ return machineId?.trim() || "local";
13796
+ }
13797
+ function cliResourceId(kind, ...parts) {
13798
+ const values = parts.map((part) => String(part ?? "*").trim()).filter(Boolean).join(":");
13799
+ return values ? `${kind}:${values}` : kind;
13800
+ }
13801
+ function cliMutationCallerId() {
13802
+ return process.env["HASNA_MACHINES_MUTATION_CALLER_ID"]?.trim() || "cli";
13803
+ }
13804
+ function cliMutationRunId() {
13805
+ return process.env["HASNA_MACHINES_MUTATION_RUN_ID"]?.trim() || "cli";
13806
+ }
13807
+ function requireCliMutation(operation, approvalToken, scope = {}) {
13808
+ assertMutationApproved({
13809
+ surface: "cli",
13810
+ operation,
13811
+ transport: "cli",
13812
+ callerId: cliMutationCallerId(),
13813
+ runId: cliMutationRunId(),
13814
+ machineId: scope.machineId === undefined ? undefined : cliMachineId(scope.machineId),
13815
+ resourceId: scope.resourceId === undefined || scope.resourceId === null ? undefined : scope.resourceId,
13816
+ args: scope.args,
13817
+ approvalToken
13818
+ });
13819
+ }
13820
+ function cliPlanApprovalArgs(args, plan) {
13821
+ return {
13822
+ ...args,
13823
+ plan_digest: mutationPlanDigest(plan)
13824
+ };
13825
+ }
13826
+ function cliPlanResourceId(operation, machineId, plan) {
13827
+ return cliResourceId("plan", operation, machineId, mutationPlanDigest(plan));
13828
+ }
13829
+ function createEventsClient() {
13830
+ return new EventsClient3;
13831
+ }
13832
+ function eventStoreDir2() {
13833
+ return resolve4(getEventsDataDir2());
13834
+ }
13835
+ function eventStoreScope2() {
13836
+ return { event_store_dir: eventStoreDir2() };
13837
+ }
13838
+ function eventStoreResourceId2(kind, ...parts) {
13839
+ return cliResourceId(kind, mutationArgsSha256(eventStoreScope2()), ...parts);
13840
+ }
13841
+ function withEventStoreScope2(args) {
13842
+ return { event_store_dir: eventStoreDir2(), ...args };
13843
+ }
13844
+ function readJsonArrayFile(path) {
13845
+ if (!existsSync14(path))
13846
+ return [];
13847
+ const raw = readFileSync14(path, "utf8").trim();
13848
+ if (!raw)
13849
+ return [];
13850
+ const parsed = JSON.parse(raw);
13851
+ if (!Array.isArray(parsed))
13852
+ throw new Error(`Expected ${path} to contain a JSON array.`);
13853
+ return parsed;
13854
+ }
13855
+ function readEventChannelsWithoutInit() {
13856
+ return readJsonArrayFile(join12(eventStoreDir2(), "channels.json"));
13857
+ }
13858
+ function readEventsWithoutInit() {
13859
+ return readJsonArrayFile(join12(eventStoreDir2(), "events.json"));
13860
+ }
13861
+ function filterEventsForReplay(events, options) {
13862
+ return events.filter((event) => {
13863
+ if (options.id && event.id !== options.id)
13864
+ return false;
13865
+ if (options.source && event.source !== options.source)
13866
+ return false;
13867
+ if (options.type && event.type !== options.type)
13868
+ return false;
13869
+ return true;
13870
+ });
13871
+ }
13872
+ function collectOptionValues(value, previous = []) {
13873
+ previous.push(value);
13874
+ return previous;
13875
+ }
13876
+ function parseNumberOption(value) {
13877
+ const parsed = Number(value);
13878
+ if (!Number.isFinite(parsed))
13879
+ throw new Error(`Expected a finite number, got ${value}`);
13880
+ return parsed;
13881
+ }
13882
+ function parseJsonObjectOption(value, fallback) {
13883
+ if (value === undefined)
13884
+ return fallback;
13885
+ const parsed = JSON.parse(value);
13886
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
13887
+ throw new Error("Expected a JSON object.");
13888
+ }
13889
+ return parsed;
13890
+ }
13891
+ function parseHeaderOptions(values) {
13892
+ if (!values?.length)
13893
+ return;
13894
+ const headers = {};
13895
+ for (const value of values) {
13896
+ const separator = value.indexOf("=");
13897
+ if (separator === -1)
13898
+ throw new Error(`Invalid header, expected name=value: ${value}`);
13899
+ headers[value.slice(0, separator)] = value.slice(separator + 1);
13900
+ }
13901
+ return headers;
13902
+ }
13903
+ function buildEventFilter(options) {
13904
+ const filter = {};
13905
+ if (options.source)
13906
+ filter.source = options.source;
13907
+ if (options.type)
13908
+ filter.type = options.type;
13909
+ if (options.subject)
13910
+ filter.subject = options.subject;
13911
+ if (options.severity)
13912
+ filter.severity = options.severity;
13913
+ return Object.keys(filter).length > 0 ? [filter] : undefined;
13914
+ }
13915
+ function wantsCommandJson(options, command2) {
13916
+ return Boolean(options.json || command2.optsWithGlobals?.().json || command2.parent?.optsWithGlobals?.().json || program2.opts().quiet);
13917
+ }
13918
+ function printCommandResult(data, text, json) {
13919
+ if (json || program2.opts().quiet) {
13920
+ console.log(JSON.stringify(data, null, 2));
13921
+ return;
13922
+ }
13923
+ console.log(text);
13924
+ }
13925
+ function runtimeTmuxCommand() {
13926
+ return process.env["HASNA_MACHINES_TMUX_BIN"]?.trim() || "tmux";
13927
+ }
13928
+ function runtimeTmuxEventTypes(once) {
13929
+ return once ? ["machines.tmux.pane_missing"] : ["machines.tmux.pane_died"];
13930
+ }
13931
+ eventWebhooksCommand.command("add").description("Add or replace a webhook or command subscription").argument("<target>", "Webhook URL or command binary").requiredOption("--id <id>", "Subscription/channel identifier").option("--transport <kind>", "Transport kind: webhook or command", "webhook").option("--name <name>", "Display name").option("--type <pattern>", "Event type filter, e.g. todos.task.*").option("--source <pattern>", "Event source filter").option("--subject <pattern>", "Event subject filter").option("--severity <pattern>", "Event severity filter").option("--secret <secret>", "Webhook HMAC secret").option("--header <name=value...>", "Webhook header", collectOptionValues, []).option("--arg <arg...>", "Command argument", collectOptionValues, []).option("--timeout-ms <ms>", "Transport timeout in milliseconds", parseNumberOption).option("--retry-attempts <n>", "Maximum delivery attempts", parseNumberOption).option("--retry-backoff-ms <ms>", "Initial retry backoff in milliseconds", parseNumberOption).option("--redact <path...>", "Event field path to redact before delivery", collectOptionValues, []).option("--disabled", "Create channel disabled", false).option("--approval-token <token>", "Scoped mutation approval token").option("-j, --json", "Print JSON output", false).action(async (target, options, command2) => {
13932
+ const headers = parseHeaderOptions(options.header);
13933
+ const commandArgs = options.arg ?? [];
13934
+ const redactPaths = options.redact ?? [];
13935
+ const enabled = !options.disabled;
13936
+ const filter = buildEventFilter(options);
13937
+ const channel = {
13938
+ id: options.id,
13939
+ name: options.name,
13940
+ enabled,
13941
+ transport: options.transport,
13942
+ filters: filter,
13943
+ retry: options.retryAttempts || options.retryBackoffMs ? { maxAttempts: options.retryAttempts, backoffMs: options.retryBackoffMs } : undefined,
13944
+ redact: redactPaths.length > 0 ? { paths: redactPaths } : undefined
13945
+ };
13946
+ if (options.transport === "webhook") {
13947
+ channel.webhook = { url: target, secret: options.secret, headers, timeoutMs: options.timeoutMs };
13948
+ } else if (options.transport === "command") {
13949
+ channel.command = { command: target, args: commandArgs, timeoutMs: options.timeoutMs };
13950
+ } else {
13951
+ throw new Error(`Transport ${options.transport} is reserved for future use and cannot be added yet`);
13952
+ }
13953
+ requireCliMutation("machines_webhooks_add", options.approvalToken, {
13954
+ resourceId: eventStoreResourceId2("webhook", options.id),
13955
+ args: withEventStoreScope2({
13956
+ channel_id: options.id,
13957
+ target,
13958
+ transport: options.transport,
13959
+ name: options.name,
13960
+ event_type: options.type,
13961
+ source: options.source,
13962
+ subject: options.subject,
13963
+ severity: options.severity,
13964
+ secret: options.secret,
13965
+ headers,
13966
+ args: commandArgs,
13967
+ timeout_ms: options.timeoutMs,
13968
+ retry_attempts: options.retryAttempts,
13969
+ retry_backoff_ms: options.retryBackoffMs,
13970
+ redact: redactPaths,
13971
+ enabled
13972
+ })
13973
+ });
13974
+ const saved = await createEventsClient().addChannel(channel);
13975
+ printCommandResult(sanitizeChannelForOutput(saved), `Added ${saved.transport} channel ${saved.id}`, wantsCommandJson(options, command2));
13976
+ });
13977
+ eventWebhooksCommand.command("list").description("List configured subscriptions").option("-j, --json", "Print JSON output", false).action(async (options, command2) => {
13978
+ const channels = readEventChannelsWithoutInit();
13979
+ if (wantsCommandJson(options, command2)) {
13980
+ console.log(JSON.stringify(sanitizeChannelsForOutput2(channels), null, 2));
13981
+ return;
13982
+ }
13983
+ if (!channels.length) {
13984
+ console.log("No channels configured.");
13985
+ return;
13986
+ }
13987
+ for (const channel of channels) {
13988
+ console.log(`${channel.id} ${channel.enabled ? "enabled" : "disabled"} ${channel.transport} ${channel.webhook?.url ?? channel.command?.command ?? channel.transport}`);
13989
+ }
13990
+ });
13991
+ eventWebhooksCommand.command("remove").description("Remove a subscription").argument("<id>", "Subscription/channel identifier").option("--approval-token <token>", "Scoped mutation approval token").option("-j, --json", "Print JSON output", false).action(async (id, options, command2) => {
13992
+ requireCliMutation("machines_webhooks_remove", options.approvalToken, {
13993
+ resourceId: eventStoreResourceId2("webhook", id),
13994
+ args: withEventStoreScope2({ channel_id: id })
13995
+ });
13996
+ const removed = await createEventsClient().removeChannel(id);
13997
+ printCommandResult({ removed }, removed ? `Removed ${id}` : `Channel not found: ${id}`, wantsCommandJson(options, command2));
13998
+ });
13999
+ eventWebhooksCommand.command("test").description("Send a test event to one subscription").argument("<id>", "Subscription/channel identifier").option("--type <type>", "Event type", "events.test").option("--subject <subject>", "Event subject").option("--message <message>", "Event message", "Shared events test delivery").option("--data <json>", "Event data JSON object").option("--approval-token <token>", "Scoped mutation approval token").option("-j, --json", "Print JSON output", false).action(async (id, options, command2) => {
14000
+ const data = parseJsonObjectOption(options.data, { test: true });
14001
+ const subject = options.subject ?? id;
14002
+ requireCliMutation("machines_webhooks_test", options.approvalToken, {
14003
+ resourceId: eventStoreResourceId2("webhook-test", id, options.type),
14004
+ args: withEventStoreScope2({ channel_id: id, event_type: options.type, subject, message: options.message, data })
14005
+ });
14006
+ const result = await createEventsClient().testChannel(id, {
14007
+ source: "machines",
14008
+ type: options.type,
14009
+ subject,
14010
+ message: options.message,
14011
+ data
14012
+ });
14013
+ printCommandResult(result, `${result.status}: ${result.channelId}`, wantsCommandJson(options, command2));
14014
+ });
14015
+ eventsCommand.command("emit").description("Emit an event from this app").argument("<type>", "Event type").option("--source <source>", "Event source override").option("--subject <subject>", "Event subject").option("--severity <severity>", "Event severity", "info").option("--message <message>", "Event message").option("--dedupe-key <key>", "Dedupe key").option("--data <json>", "Event data JSON object").option("--metadata <json>", "Event metadata JSON object").option("--no-deliver", "Record without delivering").option("--no-dedupe", "Allow duplicate id/dedupeKey events").option("--approval-token <token>", "Scoped mutation approval token").option("-j, --json", "Print JSON output", false).action(async (type, options, command2) => {
14016
+ const source = options.source ?? "machines";
14017
+ const data = parseJsonObjectOption(options.data, {});
14018
+ const metadata = parseJsonObjectOption(options.metadata, {});
14019
+ requireCliMutation("machines_events_emit", options.approvalToken, {
14020
+ resourceId: eventStoreResourceId2("event", type, options.subject, options.dedupeKey),
14021
+ args: withEventStoreScope2({
14022
+ event_type: type,
14023
+ source,
14024
+ subject: options.subject,
14025
+ severity: options.severity,
14026
+ message: options.message,
14027
+ data,
14028
+ metadata,
14029
+ dedupe_key: options.dedupeKey,
14030
+ deliver: options.deliver,
14031
+ dedupe: options.dedupe
14032
+ })
14033
+ });
14034
+ const result = await createEventsClient().emit({
14035
+ source,
14036
+ type,
14037
+ subject: options.subject,
14038
+ severity: options.severity,
14039
+ message: options.message,
14040
+ dedupeKey: options.dedupeKey,
14041
+ data,
14042
+ metadata
14043
+ }, { deliver: options.deliver, dedupe: options.dedupe });
14044
+ printCommandResult(result, `${result.deduped ? "Deduped" : "Emitted"} ${result.event.id} to ${result.deliveries.length} channel(s)`, wantsCommandJson(options, command2));
14045
+ });
14046
+ eventsCommand.command("list").description("List recorded events").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--limit <n>", "Limit results", parseNumberOption).option("-j, --json", "Print JSON output", false).action(async (options, command2) => {
14047
+ let rows = readEventsWithoutInit();
14048
+ if (options.source)
14049
+ rows = rows.filter((event) => event.source === options.source);
14050
+ if (options.type)
14051
+ rows = rows.filter((event) => event.type === options.type);
14052
+ if (options.limit)
14053
+ rows = rows.slice(-options.limit);
14054
+ if (wantsCommandJson(options, command2)) {
14055
+ console.log(JSON.stringify(rows, null, 2));
14056
+ return;
14057
+ }
14058
+ if (!rows.length) {
14059
+ console.log("No events recorded.");
14060
+ return;
14061
+ }
14062
+ for (const event of rows) {
14063
+ console.log(`${event.time} ${event.id} ${event.source} ${event.type} ${event.severity}`);
14064
+ }
14065
+ });
14066
+ eventsCommand.command("replay").description("Replay recorded events").option("--id <id>", "Replay one event id").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--dry-run", "Preview without delivery", false).option("--approval-token <token>", "Scoped mutation approval token").option("-j, --json", "Print JSON output", false).action(async (options, command2) => {
14067
+ if (options.dryRun !== true) {
14068
+ requireCliMutation("machines_events_replay", options.approvalToken, {
14069
+ resourceId: eventStoreResourceId2("event-replay", options.id, options.source, options.type),
14070
+ args: withEventStoreScope2({ event_id: options.id, source: options.source, event_type: options.type, dry_run: false })
14071
+ });
14072
+ }
14073
+ const result = options.dryRun === true ? { events: filterEventsForReplay(readEventsWithoutInit(), options), deliveries: [] } : await createEventsClient().replay({
14074
+ eventId: options.id,
14075
+ source: options.source,
14076
+ type: options.type,
14077
+ dryRun: options.dryRun
14078
+ });
14079
+ printCommandResult(result, `Replayed ${result.events.length} event(s), ${result.deliveries.length} delivery result(s)`, wantsCommandJson(options, command2));
14080
+ });
12819
14081
  function addDaemonLifecycleCommand(action, description) {
12820
- daemonCommand.command(action).description(description).option("--platform <platform>", "Service platform to plan for (macos, linux)").option("--mode <mode>", "Service mode (user, system)", "user").option("--service-name <name>", "Service name/label", "machines-agent").option("--executable <path>", "Absolute machines-agent executable path").option("--interval-ms <ms>", "Heartbeat interval in milliseconds").option("--storage-push", "Configure daemon to push heartbeat rows to storage", false).option("--doctor-summary", "Configure daemon to include lightweight doctor summaries", false).option("--private-metadata", "Opt in to private host/network metadata in heartbeat rows", false).option("--env <name...>", "Environment variable names to include as placeholders").option("--apply", "Write service files and run planned commands", false).option("--yes", "Confirm execution when using --apply", false).option("-j, --json", "Print JSON output", false).action((options) => {
12821
- const plan = buildDaemonServicePlan(parseDaemonOptions(action, options));
14082
+ daemonCommand.command(action).description(description).option("--platform <platform>", "Service platform to plan for (macos, linux)").option("--mode <mode>", "Service mode (user, system)", "user").option("--service-name <name>", "Service name/label", "machines-agent").option("--executable <path>", "Absolute machines-agent executable path").option("--interval-ms <ms>", "Heartbeat interval in milliseconds").option("--storage-push", "Configure daemon to push heartbeat rows to storage", false).option("--doctor-summary", "Configure daemon to include lightweight doctor summaries", false).option("--private-metadata", "Opt in to private host/network metadata in heartbeat rows", false).option("--env <name...>", "Environment variable names to include as placeholders").option("--apply", "Write service files and run planned commands", false).option("--yes", "Confirm execution when using --apply", false).option("--approval-token <token>", "Scoped mutation approval token").option("-j, --json", "Print JSON output", false).action((options) => {
14083
+ const planOptions = parseDaemonOptions(action, options);
14084
+ const plan = buildDaemonServicePlan(planOptions);
14085
+ if (options.apply) {
14086
+ requireCliMutation(`daemon_${action}`, options.approvalToken, { resourceId: cliResourceId("daemon", action, options.serviceName), args: planOptions });
14087
+ }
12822
14088
  const result = runDaemonServicePlan(plan, { apply: options.apply, yes: options.yes });
12823
14089
  if (options.json || options.apply) {
12824
14090
  console.log(JSON.stringify(result, null, 2));
@@ -12832,7 +14098,8 @@ addDaemonLifecycleCommand("uninstall", "Plan or uninstall the machines-agent dae
12832
14098
  addDaemonLifecycleCommand("restart", "Plan or restart the machines-agent daemon service");
12833
14099
  addDaemonLifecycleCommand("status", "Plan a daemon service status command");
12834
14100
  addDaemonLifecycleCommand("logs", "Plan a daemon service log command");
12835
- manifestCommand.command("init").description("Create an empty fleet manifest").action(() => {
14101
+ manifestCommand.command("init").description("Create an empty fleet manifest").option("--approval-token <token>", "Scoped mutation approval token").action((options) => {
14102
+ requireCliMutation("manifest_init", options.approvalToken, { resourceId: "manifest:init", args: {} });
12836
14103
  console.log(manifestInit());
12837
14104
  });
12838
14105
  manifestCommand.command("path").description("Print the manifest path").action(() => {
@@ -12844,7 +14111,8 @@ manifestCommand.command("list").description("Print the fleet manifest").action((
12844
14111
  manifestCommand.command("validate").description("Validate the fleet manifest").action(() => {
12845
14112
  console.log(JSON.stringify(manifestValidate(), null, 2));
12846
14113
  });
12847
- manifestCommand.command("bootstrap").description("Detect and upsert the current machine into the manifest").action(() => {
14114
+ manifestCommand.command("bootstrap").description("Detect and upsert the current machine into the manifest").option("--approval-token <token>", "Scoped mutation approval token").action((options) => {
14115
+ requireCliMutation("manifest_bootstrap", options.approvalToken, { resourceId: "manifest:bootstrap", args: {} });
12848
14116
  console.log(JSON.stringify(manifestBootstrapCurrentMachine(), null, 2));
12849
14117
  });
12850
14118
  manifestCommand.command("get").description("Print a single machine from the manifest").argument("<id>", "Machine identifier").action((id) => {
@@ -12856,18 +14124,20 @@ manifestCommand.command("get").description("Print a single machine from the mani
12856
14124
  }
12857
14125
  console.log(JSON.stringify(machine, null, 2));
12858
14126
  });
12859
- manifestCommand.command("remove").description("Remove a machine from the manifest").argument("<id>", "Machine identifier").action((id) => {
14127
+ manifestCommand.command("remove").description("Remove a machine from the manifest").argument("<id>", "Machine identifier").option("--approval-token <token>", "Scoped mutation approval token").action((id, options) => {
14128
+ requireCliMutation("manifest_remove", options.approvalToken, { machineId: id, args: { machine_id: id } });
12860
14129
  console.log(JSON.stringify(manifestRemove(id), null, 2));
12861
14130
  });
12862
- manifestCommand.command("add").description("Add or replace a machine in the fleet manifest").option("--id <id>", "Machine identifier").option("--platform <platform>", "linux | macos | windows").option("--workspace-path <path>", "Primary workspace path").option("--hostname <hostname>", "Machine hostname").option("--ssh-address <sshAddress>", "Machine SSH address").option("--tailscale-name <tailscaleName>", "Machine Tailscale DNS name").option("--connection <connection>", "local | ssh | tailscale").option("--bun-path <path>", "Bun executable directory").option("--tag <tag...>", "Machine tags").option("--package <name...>", "Desired packages").option("--app <spec...>", "Desired apps as name[:manager[:packageName]]").option("--file <spec...>", "File sync spec source:target[:copy|symlink]").option("--metadata <json>", "Machine metadata as JSON").option("--from-stdin", "Read the full MachineManifest JSON from stdin").action((options) => {
14131
+ manifestCommand.command("add").description("Add or replace a machine in the fleet manifest").option("--id <id>", "Machine identifier").option("--platform <platform>", "linux | macos | windows").option("--workspace-path <path>", "Primary workspace path").option("--hostname <hostname>", "Machine hostname").option("--ssh-address <sshAddress>", "Machine SSH address").option("--tailscale-name <tailscaleName>", "Machine Tailscale DNS name").option("--connection <connection>", "local | ssh | tailscale").option("--bun-path <path>", "Bun executable directory").option("--tag <tag...>", "Machine tags").option("--package <name...>", "Desired packages").option("--app <spec...>", "Desired apps as name[:manager[:packageName]]").option("--file <spec...>", "File sync spec source:target[:copy|symlink]").option("--metadata <json>", "Machine metadata as JSON").option("--from-stdin", "Read the full MachineManifest JSON from stdin").option("--approval-token <token>", "Scoped mutation approval token").action((options) => {
12863
14132
  const fromStdin = Boolean(options["fromStdin"] || options["from-stdin"]);
12864
14133
  if (fromStdin) {
12865
14134
  if (process.stdin.isTTY) {
12866
14135
  console.error("error: --from-stdin requires piped input");
12867
14136
  process.exit(1);
12868
14137
  }
12869
- const input = readFileSync12(0, "utf8");
14138
+ const input = readFileSync14(0, "utf8");
12870
14139
  const machine2 = JSON.parse(input);
14140
+ requireCliMutation("manifest_add", typeof options["approvalToken"] === "string" ? options["approvalToken"] : undefined, { machineId: machine2.id, args: machine2 });
12871
14141
  console.log(JSON.stringify(manifestAdd(machine2), null, 2));
12872
14142
  return;
12873
14143
  }
@@ -12906,6 +14176,7 @@ manifestCommand.command("add").description("Add or replace a machine in the flee
12906
14176
  apps,
12907
14177
  files
12908
14178
  };
14179
+ requireCliMutation("manifest_add", typeof options["approvalToken"] === "string" ? options["approvalToken"] : undefined, { machineId: machine.id, args: machine });
12909
14180
  console.log(JSON.stringify(manifestAdd(machine), null, 2));
12910
14181
  });
12911
14182
  appsCommand.command("list").description("List manifest-managed apps for a machine").option("--machine <id>", "Machine identifier").option("-j, --json", "Print JSON output", false).action((options) => {
@@ -12924,16 +14195,47 @@ appsCommand.command("plan").description("Preview app install steps for a machine
12924
14195
  const result = buildAppsPlan(options.machine);
12925
14196
  console.log(JSON.stringify(result, null, 2));
12926
14197
  });
12927
- appsCommand.command("apply").description("Install manifest-managed apps for a machine").option("--machine <id>", "Machine identifier").option("--yes", "Confirm execution", false).action((options) => {
12928
- const result = runAppsInstall(options.machine, { apply: true, yes: options.yes });
14198
+ appsCommand.command("apply").description("Install manifest-managed apps for a machine").option("--machine <id>", "Machine identifier").option("--yes", "Confirm execution", false).option("--approval-token <token>", "Scoped mutation approval token").action((options) => {
14199
+ const resolvedMachineId = cliMachineId(options.machine);
14200
+ const plan = buildAppsPlan(options.machine);
14201
+ requireCliMutation("apps_apply", options.approvalToken, {
14202
+ machineId: resolvedMachineId,
14203
+ resourceId: cliPlanResourceId("apps_apply", resolvedMachineId, plan),
14204
+ args: cliPlanApprovalArgs({ machine_id: resolvedMachineId, yes: options.yes }, plan)
14205
+ });
14206
+ const result = runAppsPlan(plan, { apply: true, yes: options.yes });
12929
14207
  console.log(JSON.stringify(result, null, 2));
12930
14208
  });
12931
- program2.command("setup").description("Prepare a machine from the fleet manifest").option("--machine <id>", "Machine identifier").option("--apply", "Execute provisioning commands instead of previewing the plan", false).option("--yes", "Confirm execution when using --apply", false).option("-j, --json", "Print JSON output", false).action((options) => {
12932
- const result = options.apply ? runSetup(options.machine, { apply: true, yes: options.yes }) : buildSetupPlan(options.machine);
14209
+ program2.command("setup").description("Prepare a machine from the fleet manifest").option("--machine <id>", "Machine identifier").option("--apply", "Execute provisioning commands instead of previewing the plan", false).option("--yes", "Confirm execution when using --apply", false).option("--approval-token <token>", "Scoped mutation approval token").option("-j, --json", "Print JSON output", false).action((options) => {
14210
+ if (options.apply) {
14211
+ const resolvedMachineId = cliMachineId(options.machine);
14212
+ const plan = buildSetupPlan(options.machine);
14213
+ requireCliMutation("setup_apply", options.approvalToken, {
14214
+ machineId: resolvedMachineId,
14215
+ resourceId: cliPlanResourceId("setup_apply", resolvedMachineId, plan),
14216
+ args: cliPlanApprovalArgs({ machine_id: resolvedMachineId, yes: options.yes }, plan)
14217
+ });
14218
+ const result2 = runSetupPlan(plan, { apply: true, yes: options.yes });
14219
+ console.log(JSON.stringify(result2, null, 2));
14220
+ return;
14221
+ }
14222
+ const result = buildSetupPlan(options.machine);
12933
14223
  console.log(JSON.stringify(result, null, 2));
12934
14224
  });
12935
- program2.command("sync").description("Reconcile a machine against the fleet manifest").option("--machine <id>", "Machine identifier").option("--apply", "Execute reconciliation commands instead of previewing the plan", false).option("--yes", "Confirm execution when using --apply", false).option("-j, --json", "Print JSON output", false).action((options) => {
12936
- const result = options.apply ? runSync(options.machine, { apply: true, yes: options.yes }) : buildSyncPlan(options.machine);
14225
+ program2.command("sync").description("Reconcile a machine against the fleet manifest").option("--machine <id>", "Machine identifier").option("--apply", "Execute reconciliation commands instead of previewing the plan", false).option("--yes", "Confirm execution when using --apply", false).option("--approval-token <token>", "Scoped mutation approval token").option("-j, --json", "Print JSON output", false).action((options) => {
14226
+ if (options.apply) {
14227
+ const resolvedMachineId = cliMachineId(options.machine);
14228
+ const plan = buildSyncPlan(options.machine);
14229
+ requireCliMutation("sync_apply", options.approvalToken, {
14230
+ machineId: resolvedMachineId,
14231
+ resourceId: cliPlanResourceId("sync_apply", resolvedMachineId, plan),
14232
+ args: cliPlanApprovalArgs({ machine_id: resolvedMachineId, yes: options.yes }, plan)
14233
+ });
14234
+ const result2 = runSyncPlan(plan, { apply: true, yes: options.yes });
14235
+ console.log(JSON.stringify(result2, null, 2));
14236
+ return;
14237
+ }
14238
+ const result = buildSyncPlan(options.machine);
12937
14239
  console.log(JSON.stringify(result, null, 2));
12938
14240
  });
12939
14241
  program2.command("topology").description("Discover local, manifest, heartbeat, SSH, and Tailscale machine topology").option("--no-tailscale", "Skip tailscale status probing").option("--private-metadata", "Print private host/network route fields", false).option("-j, --json", "Print JSON output", false).action((options) => {
@@ -12999,7 +14301,19 @@ workspaceCommand.command("doctor").description("Diagnose repo and open-files wor
12999
14301
  if (result.diagnostics.some((entry) => entry.severity === "fail") && !options.json)
13000
14302
  process.exitCode = 1;
13001
14303
  });
13002
- workspaceCommand.command("repair").description("Preview or write explicit manifest path mappings for inferred workspace roots").requiredOption("--machine <id>", "Machine identifier").requiredOption("--project <id>", "Canonical project id").option("--repo <name>", "Repository name; defaults to project id").option("--open-files-repo <name>", "Open-files repository name", "open-files").option("--workspace-root <path>", "Override the machine workspace root for resolution").option("--project-root <path>", "Explicit project root to write").option("--open-files-root <path>", "Explicit open-files root to write").option("--apply", "Write the mappings into the manifest", false).option("--allow-untrusted", "Allow writing mappings for machines not marked trusted", false).option("--no-tailscale", "Skip tailscale status probing").option("-j, --json", "Print JSON output", false).action((options) => {
14304
+ workspaceCommand.command("repair").description("Preview or write explicit manifest path mappings for inferred workspace roots").requiredOption("--machine <id>", "Machine identifier").requiredOption("--project <id>", "Canonical project id").option("--repo <name>", "Repository name; defaults to project id").option("--open-files-repo <name>", "Open-files repository name", "open-files").option("--workspace-root <path>", "Override the machine workspace root for resolution").option("--project-root <path>", "Explicit project root to write").option("--open-files-root <path>", "Explicit open-files root to write").option("--apply", "Write the mappings into the manifest", false).option("--allow-untrusted", "Allow writing mappings for machines not marked trusted", false).option("--no-tailscale", "Skip tailscale status probing").option("--approval-token <token>", "Scoped mutation approval token").option("-j, --json", "Print JSON output", false).action((options) => {
14305
+ if (options.apply)
14306
+ requireCliMutation("workspace_repair", options.approvalToken, { machineId: options.machine, args: {
14307
+ machine: options.machine,
14308
+ project: options.project,
14309
+ repo: options.repo,
14310
+ openFilesRepo: options.openFilesRepo,
14311
+ workspaceRoot: options.workspaceRoot,
14312
+ projectRoot: options.projectRoot,
14313
+ openFilesRoot: options.openFilesRoot,
14314
+ allowUntrusted: options.allowUntrusted,
14315
+ tailscale: options.tailscale
14316
+ } });
13003
14317
  const result = repairWorkspaceManifestMappings({
13004
14318
  machineId: options.machine,
13005
14319
  projectId: options.project,
@@ -13020,18 +14334,26 @@ program2.command("diff").description("Show manifest differences between two mach
13020
14334
  const result = diffMachines(options.left, options.right);
13021
14335
  console.log(JSON.stringify(result, null, 2));
13022
14336
  });
13023
- program2.command("backup").description("Create and optionally upload a machine backup archive").option("--bucket <name>", "S3 bucket name; defaults to HASNA_MACHINES_S3_BUCKET or MACHINES_S3_BUCKET").option("--prefix <prefix>", "S3 key prefix; defaults to HASNA_MACHINES_S3_PREFIX, MACHINES_S3_PREFIX, or machines").option("--apply", "Execute backup commands instead of previewing the plan", false).option("--yes", "Confirm execution when using --apply", false).option("-j, --json", "Print JSON output", false).action((options) => {
14337
+ program2.command("backup").description("Create and optionally upload a machine backup archive").option("--bucket <name>", "S3 bucket name; defaults to HASNA_MACHINES_S3_BUCKET or MACHINES_S3_BUCKET").option("--prefix <prefix>", "S3 key prefix; defaults to HASNA_MACHINES_S3_PREFIX, MACHINES_S3_PREFIX, or machines").option("--apply", "Execute backup commands instead of previewing the plan", false).option("--yes", "Confirm execution when using --apply", false).option("--approval-token <token>", "Scoped mutation approval token").option("-j, --json", "Print JSON output", false).action((options) => {
14338
+ if (options.apply) {
14339
+ const target = resolveBackupTarget({ bucket: options.bucket, prefix: options.prefix });
14340
+ requireCliMutation("backup_apply", options.approvalToken, { resourceId: cliResourceId("backup", target.bucket, target.prefix), args: { bucket: target.bucket, prefix: target.prefix, yes: options.yes } });
14341
+ }
13024
14342
  const result = options.apply ? runBackup(options.bucket, options.prefix, { apply: true, yes: options.yes }) : buildBackupPlan(options.bucket, options.prefix);
13025
14343
  console.log(JSON.stringify(result, null, 2));
13026
14344
  });
13027
14345
  var certCommand = program2.command("cert").description("Manage mkcert-based local SSL certificates");
13028
- certCommand.command("issue").description("Plan or issue certificates for one or more domains").argument("<domains...>", "Domains to include in the certificate").option("--apply", "Execute certificate commands instead of previewing them", false).option("--yes", "Confirm execution when using --apply", false).option("-j, --json", "Print JSON output", false).action((domains, options) => {
14346
+ certCommand.command("issue").description("Plan or issue certificates for one or more domains").argument("<domains...>", "Domains to include in the certificate").option("--apply", "Execute certificate commands instead of previewing them", false).option("--yes", "Confirm execution when using --apply", false).option("--approval-token <token>", "Scoped mutation approval token").option("-j, --json", "Print JSON output", false).action((domains, options) => {
14347
+ if (options.apply)
14348
+ requireCliMutation("cert_apply", options.approvalToken, { resourceId: cliResourceId("cert", domains.join(",")), args: { domains, yes: options.yes } });
13029
14349
  const result = options.apply ? runCertPlan(domains, { apply: true, yes: options.yes }) : buildCertPlan(domains);
13030
14350
  console.log(JSON.stringify(result, null, 2));
13031
14351
  });
13032
14352
  var dnsCommand = program2.command("dns").description("Manage local domain mappings");
13033
- dnsCommand.command("add").description("Add or replace a local domain mapping").requiredOption("--domain <domain>", "Domain name").requiredOption("--port <port>", "Target port").option("--target-host <host>", "Target host", "127.0.0.1").option("-j, --json", "Print JSON output", false).action((options) => {
13034
- const result = addDomainMapping(options.domain, parseIntegerOption(options.port, "port", { min: 1, max: 65535 }), options.targetHost);
14353
+ dnsCommand.command("add").description("Add or replace a local domain mapping").requiredOption("--domain <domain>", "Domain name").requiredOption("--port <port>", "Target port").option("--target-host <host>", "Target host", "127.0.0.1").option("--approval-token <token>", "Scoped mutation approval token").option("-j, --json", "Print JSON output", false).action((options) => {
14354
+ const port = parseIntegerOption(options.port, "port", { min: 1, max: 65535 });
14355
+ requireCliMutation("dns_add", options.approvalToken, { resourceId: cliResourceId("dns", options.domain), args: { domain: options.domain, port, target_host: options.targetHost } });
14356
+ const result = addDomainMapping(options.domain, port, options.targetHost);
13035
14357
  console.log(JSON.stringify(result, null, 2));
13036
14358
  });
13037
14359
  dnsCommand.command("list").description("List saved local domain mappings").option("-j, --json", "Print JSON output", false).action(() => {
@@ -13040,43 +14362,122 @@ dnsCommand.command("list").description("List saved local domain mappings").optio
13040
14362
  dnsCommand.command("render").description("Render hosts/proxy configuration for a domain").argument("<domain>", "Domain name").option("-j, --json", "Print JSON output", false).action((domain) => {
13041
14363
  console.log(JSON.stringify(renderDomainMapping(domain), null, 2));
13042
14364
  });
13043
- notificationsCommand.command("add").description("Add or replace a notification channel").requiredOption("--id <id>", "Channel identifier").requiredOption("--type <type>", "email | webhook | command").requiredOption("--target <target>", "Email, webhook URL, or shell command").option("--event <event...>", "Events routed to this channel", ["setup_failed", "sync_failed"]).option("--disabled", "Create the channel in disabled state", false).option("-j, --json", "Print JSON output", false).action((options) => {
14365
+ var hostsCommand = program2.command("hosts").description("Sync fleet machine names into /etc/hosts so machine<NN>:port resolves on the LAN and tailnet");
14366
+ function printHostsResult(plan, applied, viaSudo = false) {
14367
+ console.log(`hosts file: ${plan.hostsPath}`);
14368
+ console.log(`local subnets: ${plan.localSubnets.join(", ") || "none"}`);
14369
+ for (const entry of plan.entries) {
14370
+ console.log(` ${entry.ip} ${entry.names.join(" ")} (${entry.source})`);
14371
+ }
14372
+ if (plan.unresolved.length > 0) {
14373
+ console.log(`unresolved: ${plan.unresolved.join(", ")}`);
14374
+ }
14375
+ if (plan.warnings.length > 0) {
14376
+ console.log(`warnings: ${plan.warnings.join(", ")}`);
14377
+ }
14378
+ console.log(applied ? `applied ${plan.entries.length} entries${viaSudo ? " (via sudo)" : ""}` : "dry run \u2014 re-run `machines hosts apply` to write");
14379
+ }
14380
+ hostsCommand.command("plan", { isDefault: true }).description("Preview the managed /etc/hosts block for the fleet (dry run)").option("-j, --json", "Print JSON output", false).option("--no-warm", "Skip establishing direct Tailscale paths to discover LAN endpoints").action((options) => {
14381
+ const plan = planFleetHosts({ warm: options.warm });
14382
+ if (options.json) {
14383
+ console.log(JSON.stringify(plan, null, 2));
14384
+ return;
14385
+ }
14386
+ printHostsResult(plan, false);
14387
+ });
14388
+ hostsCommand.command("apply").description("Write the managed fleet block into /etc/hosts (uses sudo when required)").option("-j, --json", "Print JSON output", false).option("--no-warm", "Skip establishing direct Tailscale paths to discover LAN endpoints").action((options) => {
14389
+ const result = applyFleetHosts({ warm: options.warm });
14390
+ if (options.json) {
14391
+ console.log(JSON.stringify(result, null, 2));
14392
+ return;
14393
+ }
14394
+ printHostsResult(result, true, result.viaSudo);
14395
+ });
14396
+ notificationsCommand.command("add").description("Add or replace a notification channel").requiredOption("--id <id>", "Channel identifier").requiredOption("--type <type>", "email | webhook | command").requiredOption("--target <target>", "Email, webhook URL, or command executable").option("--arg <arg...>", "Command argument for command transports", collectOptionValues, []).option("--event <event...>", "Events routed to this channel", ["setup_failed", "sync_failed"]).option("--disabled", "Create the channel in disabled state", false).option("--approval-token <token>", "Operator mutation approval token for command transports").option("-j, --json", "Print JSON output", false).action((options) => {
14397
+ const enabled = !options.disabled;
14398
+ const events = [...new Set(options.event)];
14399
+ const commandArgs = options.arg ?? [];
14400
+ requireCliMutation("notifications_add", options.approvalToken, { resourceId: cliResourceId("notification", options.id), args: { id: options.id, type: options.type, target: options.target, args: commandArgs, event: events, enabled } });
13044
14401
  const result = addNotificationChannel({
13045
14402
  id: options.id,
13046
14403
  type: options.type,
13047
14404
  target: options.target,
13048
- events: options.event,
13049
- enabled: !options.disabled
13050
- });
14405
+ commandArgs: options.type === "command" && commandArgs.length > 0 ? commandArgs : undefined,
14406
+ events,
14407
+ enabled
14408
+ }, { trustedApproval: trustedNotificationApproval2 });
13051
14409
  printJsonOrText(result, renderNotificationConfigResult(result), options.json);
13052
14410
  });
13053
14411
  notificationsCommand.command("list").description("List configured notification channels").option("-j, --json", "Print JSON output", false).action((options) => {
13054
14412
  const result = listNotificationChannels();
13055
14413
  printJsonOrText(result, renderNotificationConfigResult(result), options.json);
13056
14414
  });
13057
- notificationsCommand.command("test").description("Preview or execute a notification test").requiredOption("--channel <id>", "Channel identifier").option("--event <name>", "Event name", "manual.test").option("--message <message>", "Test message", "machines notification test").option("--apply", "Execute the notification test instead of previewing it", false).option("--yes", "Confirm execution when using --apply", false).option("-j, --json", "Print JSON output", false).action(async (options) => {
14415
+ notificationsCommand.command("test").description("Preview or execute a notification test").requiredOption("--channel <id>", "Channel identifier").option("--event <name>", "Event name", "manual.test").option("--message <message>", "Test message", "machines notification test").option("--apply", "Execute the notification test instead of previewing it", false).option("--yes", "Confirm execution when using --apply", false).option("--approval-token <token>", "Operator mutation approval token for command transports").option("-j, --json", "Print JSON output", false).action(async (options) => {
14416
+ if (options.apply)
14417
+ requireCliMutation("notifications_test", options.approvalToken, { resourceId: cliResourceId("notification-test", options.channel, options.event), args: { channel: options.channel, event: options.event, message: options.message, yes: options.yes } });
13058
14418
  const result = await testNotificationChannel(options.channel, options.event, options.message, {
13059
14419
  apply: options.apply,
13060
- yes: options.yes
14420
+ yes: options.yes,
14421
+ trustedApproval: options.apply === true ? trustedNotificationApproval2 : undefined
13061
14422
  });
13062
14423
  printJsonOrText(result, renderNotificationTestResult(result), options.json);
13063
14424
  });
13064
- notificationsCommand.command("dispatch").description("Dispatch an event to matching notification channels").requiredOption("--event <name>", "Event name").requiredOption("--message <message>", "Message body").option("--channel <id>", "Limit delivery to one channel").option("-j, --json", "Print JSON output", false).action(async (options) => {
13065
- const result = await dispatchNotificationEvent(options.event, options.message, { channelId: options.channel });
14425
+ notificationsCommand.command("dispatch").description("Dispatch an event to matching notification channels").requiredOption("--event <name>", "Event name").requiredOption("--message <message>", "Message body").option("--channel <id>", "Limit delivery to one channel").option("--approval-token <token>", "Operator mutation approval token for command transports").option("-j, --json", "Print JSON output", false).action(async (options) => {
14426
+ requireCliMutation("notifications_dispatch", options.approvalToken, { resourceId: cliResourceId("notification-dispatch", options.channel, options.event), args: { event: options.event, message: options.message, channel: options.channel } });
14427
+ const result = await dispatchNotificationEvent(options.event, options.message, { channelId: options.channel, trustedApproval: trustedNotificationApproval2 });
13066
14428
  printJsonOrText(result, renderNotificationDispatchResult(result), options.json);
13067
14429
  });
13068
- notificationsCommand.command("remove").description("Remove a notification channel").argument("<id>", "Channel identifier").option("-j, --json", "Print JSON output", false).action((id, options) => {
14430
+ notificationsCommand.command("remove").description("Remove a notification channel").argument("<id>", "Channel identifier").option("--approval-token <token>", "Scoped mutation approval token").option("-j, --json", "Print JSON output", false).action((id, options) => {
14431
+ requireCliMutation("notifications_remove", options.approvalToken, { resourceId: cliResourceId("notification", id), args: { id } });
13069
14432
  const result = removeNotificationChannel(id);
13070
14433
  printJsonOrText(result, renderNotificationConfigResult(result), options.json);
13071
14434
  });
13072
- runtimeCommand.command("tmux-watch").description("Watch a tmux pane and emit machines.tmux.pane_died if it disappears").argument("<target>", "tmux pane target, for example %1 or session:window.pane").option("--interval-ms <ms>", "Polling interval in milliseconds", "5000").option("--max-checks <n>", "Stop after N checks instead of watching forever").option("--once", "Probe once and emit machines.tmux.pane_missing when absent", false).option("--no-deliver", "Record the event without webhook delivery").option("-j, --json", "Print JSON output", false).action(async (target, options) => {
14435
+ runtimeCommand.command("tmux-hook-plan").description("Print a tmux pane-died hook command that emits machines events").option("--machines-command <command>", "machines CLI executable", "machines").option("--tmux-command <command>", "tmux executable").option("--deliver", "Deliver webhooks from the hook instead of recording only", false).option("--approval-token <token>", "Scoped mutation token for the generated events emit command").option("--trusted-local-mutation", "Include process-local trusted mutation env when no approval token is supplied", false).option("-j, --json", "Print JSON output", false).action((options) => {
14436
+ if (!options.approvalToken && options.trustedLocalMutation !== true) {
14437
+ throw new Error("tmux-hook-plan requires --approval-token or explicit --trusted-local-mutation.");
14438
+ }
14439
+ const result = buildTmuxPaneDiedHookPlan({
14440
+ machinesCommand: options.machinesCommand,
14441
+ tmuxCommand: options.tmuxCommand,
14442
+ deliver: options.deliver,
14443
+ approvalToken: options.approvalToken,
14444
+ trustedLocalMutation: options.trustedLocalMutation
14445
+ });
14446
+ printJsonOrText(result, result.shellCommand, options.json);
14447
+ });
14448
+ runtimeCommand.command("tmux-watch").description("Watch a tmux pane and emit machines.tmux.pane_died if it disappears").argument("<target>", "tmux pane target, for example %1 or session:window.pane").option("--interval-ms <ms>", "Polling interval in milliseconds", "5000").option("--max-checks <n>", "Stop after N checks instead of watching forever").option("--once", "Probe once and emit machines.tmux.pane_missing when absent", false).option("--no-deliver", "Record the event without webhook delivery").option("--approval-token <token>", "Scoped mutation approval token for event delivery").option("-j, --json", "Print JSON output", false).action(async (target, options) => {
14449
+ const normalizedTarget = target.trim();
14450
+ if (!normalizedTarget)
14451
+ throw new Error("tmux pane target is required");
14452
+ const intervalMs = parseIntegerOption(options.intervalMs ?? "5000", "interval-ms", { min: 0 });
13073
14453
  const maxChecks = options.once ? 1 : options.maxChecks ? parseIntegerOption(options.maxChecks, "max-checks", { min: 1 }) : undefined;
14454
+ const once = Boolean(options.once);
14455
+ const deliver = options.deliver !== false;
14456
+ const tmuxCommand = runtimeTmuxCommand();
14457
+ const eventTypes = runtimeTmuxEventTypes(once);
14458
+ const scopedIntervalMs = once ? undefined : intervalMs;
14459
+ if (deliver) {
14460
+ requireCliMutation("machines_runtime_tmux_watch_deliver", options.approvalToken, {
14461
+ resourceId: eventStoreResourceId2("runtime-tmux-watch", normalizedTarget, eventTypes.join(",")),
14462
+ args: withEventStoreScope2({
14463
+ target: normalizedTarget,
14464
+ event_types: eventTypes,
14465
+ interval_ms: scopedIntervalMs,
14466
+ max_checks: maxChecks,
14467
+ once,
14468
+ emit_initial_missing: once,
14469
+ deliver: true,
14470
+ tmux_command: tmuxCommand
14471
+ })
14472
+ });
14473
+ }
13074
14474
  const result = await watchTmuxPane({
13075
- target,
13076
- intervalMs: parseIntegerOption(options.intervalMs ?? "5000", "interval-ms", { min: 0 }),
14475
+ target: normalizedTarget,
14476
+ intervalMs,
13077
14477
  maxChecks,
13078
- emitInitialMissing: Boolean(options.once),
13079
- deliver: options.deliver !== false,
14478
+ emitInitialMissing: once,
14479
+ deliver,
14480
+ tmuxCommand,
13080
14481
  onProbe: options.json ? undefined : (probe) => {
13081
14482
  const status = probe.exists ? source_default.green("present") : source_default.yellow("missing");
13082
14483
  console.error(`tmux ${probe.target}: ${status}${probe.paneId ? ` ${probe.paneId}` : ""}`);
@@ -13145,7 +14546,7 @@ clipboardCommand.command("clear-history").description("Clear clipboard sync hist
13145
14546
  });
13146
14547
  clipboardCommand.command("key").description("Show or rotate the shared secret key").option("--rotate", "Generate a new key", false).option("-j, --json", "Print JSON output", false).action((options) => {
13147
14548
  if (options.rotate) {
13148
- rmSync2(getClipboardKeyPath(), { force: true });
14549
+ rmSync3(getClipboardKeyPath(), { force: true });
13149
14550
  }
13150
14551
  const key = getOrCreateClipboardKey();
13151
14552
  printJsonOrText({ key }, key, options.json);
@@ -13170,12 +14571,31 @@ installClaudeCommand.command("plan").description("Preview CLI install steps").op
13170
14571
  const result = buildClaudeInstallPlan(options.machine, options.tool);
13171
14572
  console.log(JSON.stringify(result, null, 2));
13172
14573
  });
13173
- installClaudeCommand.command("apply").description("Install or update the requested CLIs").option("--machine <id>", "Machine identifier").option("--tool <name...>", "CLI tools to install (claude, codex, gemini)").option("--yes", "Confirm execution when using apply", false).option("-j, --json", "Print JSON output", false).action((options) => {
13174
- const result = runClaudeInstall(options.machine, options.tool, { apply: true, yes: options.yes });
14574
+ installClaudeCommand.command("apply").description("Install or update the requested CLIs").option("--machine <id>", "Machine identifier").option("--tool <name...>", "CLI tools to install (claude, codex, gemini)").option("--yes", "Confirm execution when using apply", false).option("--approval-token <token>", "Scoped mutation approval token").option("-j, --json", "Print JSON output", false).action((options) => {
14575
+ const resolvedMachineId = cliMachineId(options.machine);
14576
+ const plan = buildClaudeInstallPlan(options.machine, options.tool);
14577
+ requireCliMutation("install_claude_apply", options.approvalToken, {
14578
+ machineId: resolvedMachineId,
14579
+ resourceId: cliPlanResourceId("install_claude_apply", resolvedMachineId, plan),
14580
+ args: cliPlanApprovalArgs({ machine_id: resolvedMachineId, tools: options.tool, yes: options.yes }, plan)
14581
+ });
14582
+ const result = runClaudeInstallPlan(plan, { apply: true, yes: options.yes });
13175
14583
  console.log(JSON.stringify(result, null, 2));
13176
14584
  });
13177
- program2.command("install-tailscale").description("Install Tailscale on a machine").option("--machine <id>", "Machine identifier").option("--apply", "Execute installation commands instead of previewing the plan", false).option("--yes", "Confirm execution when using --apply", false).option("-j, --json", "Print JSON output", false).action((options) => {
13178
- const result = options.apply ? runTailscaleInstall(options.machine, { apply: true, yes: options.yes }) : buildTailscaleInstallPlan(options.machine);
14585
+ program2.command("install-tailscale").description("Install Tailscale on a machine").option("--machine <id>", "Machine identifier").option("--apply", "Execute installation commands instead of previewing the plan", false).option("--yes", "Confirm execution when using --apply", false).option("--approval-token <token>", "Scoped mutation approval token").option("-j, --json", "Print JSON output", false).action((options) => {
14586
+ if (options.apply) {
14587
+ const resolvedMachineId = cliMachineId(options.machine);
14588
+ const plan = buildTailscaleInstallPlan(options.machine);
14589
+ requireCliMutation("install_tailscale_apply", options.approvalToken, {
14590
+ machineId: resolvedMachineId,
14591
+ resourceId: cliPlanResourceId("install_tailscale_apply", resolvedMachineId, plan),
14592
+ args: cliPlanApprovalArgs({ machine_id: resolvedMachineId, yes: options.yes }, plan)
14593
+ });
14594
+ const result2 = runTailscaleInstallPlan(plan, { apply: true, yes: options.yes });
14595
+ console.log(JSON.stringify(result2, null, 2));
14596
+ return;
14597
+ }
14598
+ const result = buildTailscaleInstallPlan(options.machine);
13179
14599
  console.log(JSON.stringify(result, null, 2));
13180
14600
  });
13181
14601
  program2.command("route").description("Resolve the best route for a machine").requiredOption("--machine <id>", "Machine identifier").option("--no-tailscale", "Skip tailscale status probing").option("--private-metadata", "Print private route targets", false).option("--cmd <command>", "Remote command to run").option("-j, --json", "Print JSON output", false).action((options) => {
@@ -13327,28 +14747,34 @@ storageCommand.command("status").description("Show storage sync status").option(
13327
14747
  ["tables", status.tables.join(", ")]
13328
14748
  ]), options.json);
13329
14749
  });
13330
- storageCommand.command("push").description("Push local machine runtime data to storage PostgreSQL").option("--tables <tables>", "Comma-separated table names").option("-j, --json", "Print JSON output", false).action(async (options) => {
14750
+ storageCommand.command("push").description("Push local machine runtime data to storage PostgreSQL").option("--tables <tables>", "Comma-separated table names").option("--approval-token <token>", "Scoped mutation approval token").option("-j, --json", "Print JSON output", false).action(async (options) => {
13331
14751
  try {
13332
- const { parseStorageTables: parseStorageTables2, storagePush: storagePush2 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
13333
- const results = await storagePush2({ tables: parseStorageTables2(options.tables) });
14752
+ const { parseStorageTables: parseStorageTables2, resolveTables: resolveTables2, storagePush: storagePush2 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
14753
+ const tables = resolveTables2(parseStorageTables2(options.tables));
14754
+ requireCliMutation("storage_push", options.approvalToken, { resourceId: cliResourceId("storage-push", tables.join(",")), args: { tables } });
14755
+ const results = await storagePush2({ tables });
13334
14756
  printStorageResults(results, options.json);
13335
14757
  } catch (error) {
13336
14758
  printStorageError(error);
13337
14759
  }
13338
14760
  });
13339
- storageCommand.command("pull").description("Pull machine runtime data from storage PostgreSQL to local SQLite").option("--tables <tables>", "Comma-separated table names").option("-j, --json", "Print JSON output", false).action(async (options) => {
14761
+ storageCommand.command("pull").description("Pull machine runtime data from storage PostgreSQL to local SQLite").option("--tables <tables>", "Comma-separated table names").option("--approval-token <token>", "Scoped mutation approval token").option("-j, --json", "Print JSON output", false).action(async (options) => {
13340
14762
  try {
13341
- const { parseStorageTables: parseStorageTables2, storagePull: storagePull2 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
13342
- const results = await storagePull2({ tables: parseStorageTables2(options.tables) });
14763
+ const { parseStorageTables: parseStorageTables2, resolveTables: resolveTables2, storagePull: storagePull2 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
14764
+ const tables = resolveTables2(parseStorageTables2(options.tables));
14765
+ requireCliMutation("storage_pull", options.approvalToken, { resourceId: cliResourceId("storage-pull", tables.join(",")), args: { tables } });
14766
+ const results = await storagePull2({ tables });
13343
14767
  printStorageResults(results, options.json);
13344
14768
  } catch (error) {
13345
14769
  printStorageError(error);
13346
14770
  }
13347
14771
  });
13348
- storageCommand.command("sync").description("Bidirectional storage sync: pull then push").option("--tables <tables>", "Comma-separated table names").option("-j, --json", "Print JSON output", false).action(async (options) => {
14772
+ storageCommand.command("sync").description("Bidirectional storage sync: pull then push").option("--tables <tables>", "Comma-separated table names").option("--approval-token <token>", "Scoped mutation approval token").option("-j, --json", "Print JSON output", false).action(async (options) => {
13349
14773
  try {
13350
- const { parseStorageTables: parseStorageTables2, storageSync: storageSync2 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
13351
- const result = await storageSync2({ tables: parseStorageTables2(options.tables) });
14774
+ const { parseStorageTables: parseStorageTables2, resolveTables: resolveTables2, storageSync: storageSync2 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
14775
+ const tables = resolveTables2(parseStorageTables2(options.tables));
14776
+ requireCliMutation("storage_sync", options.approvalToken, { resourceId: cliResourceId("storage-sync", tables.join(",")), args: { tables } });
14777
+ const result = await storageSync2({ tables });
13352
14778
  if (options.json) {
13353
14779
  console.log(JSON.stringify(result, null, 2));
13354
14780
  return;
@@ -13373,7 +14799,7 @@ program2.command("self-test").description("Run local package smoke checks").opti
13373
14799
  const result = runSelfTest();
13374
14800
  printJsonOrText(result, renderSelfTestResult(result), options.json);
13375
14801
  });
13376
- program2.command("serve").description("Serve a local fleet dashboard and JSON API").option("--host <host>", "Host interface to bind", "0.0.0.0").option("--port <port>", "Port to bind", "7676").option("-j, --json", "Print serve config and exit", false).action((options) => {
14802
+ program2.command("serve").description("Serve a local fleet dashboard and JSON API").option("--host <host>", "Host interface to bind", "127.0.0.1").option("--port <port>", "Port to bind", "7676").option("-j, --json", "Print serve config and exit", false).action((options) => {
13377
14803
  const info = getServeInfo({ host: options.host, port: parseIntegerOption(options.port, "port", { min: 1, max: 65535 }) });
13378
14804
  if (options.json) {
13379
14805
  console.log(JSON.stringify(info, null, 2));