@hasna/machines 0.0.36 → 0.0.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -2135,9 +2135,23 @@ function createTables(db) {
2135
2135
  pid INTEGER NOT NULL,
2136
2136
  status TEXT NOT NULL,
2137
2137
  updated_at TEXT NOT NULL,
2138
+ daemon_version TEXT,
2139
+ agent_mode TEXT,
2140
+ platform TEXT,
2141
+ os_version TEXT,
2142
+ os_build TEXT,
2143
+ arch TEXT,
2144
+ uptime_seconds INTEGER,
2145
+ tool_versions_json TEXT,
2146
+ tailscale_json TEXT,
2147
+ storage_sync_status TEXT,
2148
+ storage_sync_last_error TEXT,
2149
+ doctor_summary_json TEXT,
2150
+ private_metadata INTEGER NOT NULL DEFAULT 0,
2138
2151
  PRIMARY KEY (machine_id, pid)
2139
2152
  )
2140
2153
  `);
2154
+ migrateAgentHeartbeats(db);
2141
2155
  db.exec(`
2142
2156
  CREATE TABLE IF NOT EXISTS setup_runs (
2143
2157
  id TEXT PRIMARY KEY,
@@ -2159,6 +2173,15 @@ function createTables(db) {
2159
2173
  )
2160
2174
  `);
2161
2175
  }
2176
+ function migrateAgentHeartbeats(db) {
2177
+ const columns = db.query("PRAGMA table_info(agent_heartbeats)").all();
2178
+ const existing = new Set(columns.map((column) => column.name));
2179
+ for (const column of AGENT_HEARTBEAT_COLUMNS) {
2180
+ if (existing.has(column.name))
2181
+ continue;
2182
+ db.exec(`ALTER TABLE agent_heartbeats ADD COLUMN ${column.name} ${column.definition}`);
2183
+ }
2184
+ }
2162
2185
  function getAdapter(path = getDbPath()) {
2163
2186
  if (path === ":memory:") {
2164
2187
  const memoryAdapter = new SqliteAdapter(path);
@@ -2187,12 +2210,12 @@ function getLocalMachineId() {
2187
2210
  function listHeartbeats(machineId) {
2188
2211
  const db = getDb();
2189
2212
  if (machineId) {
2190
- return db.query(`SELECT machine_id, pid, status, updated_at
2213
+ return db.query(`SELECT *
2191
2214
  FROM agent_heartbeats
2192
2215
  WHERE machine_id = ?
2193
2216
  ORDER BY updated_at DESC`).all(machineId);
2194
2217
  }
2195
- return db.query(`SELECT machine_id, pid, status, updated_at
2218
+ return db.query(`SELECT *
2196
2219
  FROM agent_heartbeats
2197
2220
  ORDER BY updated_at DESC`).all();
2198
2221
  }
@@ -2213,9 +2236,24 @@ function recordSyncRun(machineId, status, actions) {
2213
2236
  db.query(`INSERT INTO sync_runs (id, machine_id, status, actions_json, created_at, updated_at)
2214
2237
  VALUES (?, ?, ?, ?, ?, ?)`).run(crypto.randomUUID(), machineId, status, JSON.stringify(actions), now, now);
2215
2238
  }
2216
- var adapter = null;
2239
+ var adapter = null, AGENT_HEARTBEAT_COLUMNS;
2217
2240
  var init_db = __esm(() => {
2218
2241
  init_paths();
2242
+ AGENT_HEARTBEAT_COLUMNS = [
2243
+ { name: "daemon_version", definition: "TEXT" },
2244
+ { name: "agent_mode", definition: "TEXT" },
2245
+ { name: "platform", definition: "TEXT" },
2246
+ { name: "os_version", definition: "TEXT" },
2247
+ { name: "os_build", definition: "TEXT" },
2248
+ { name: "arch", definition: "TEXT" },
2249
+ { name: "uptime_seconds", definition: "INTEGER" },
2250
+ { name: "tool_versions_json", definition: "TEXT" },
2251
+ { name: "tailscale_json", definition: "TEXT" },
2252
+ { name: "storage_sync_status", definition: "TEXT" },
2253
+ { name: "storage_sync_last_error", definition: "TEXT" },
2254
+ { name: "doctor_summary_json", definition: "TEXT" },
2255
+ { name: "private_metadata", definition: "INTEGER NOT NULL DEFAULT 0" }
2256
+ ];
2219
2257
  });
2220
2258
 
2221
2259
  // src/pg-migrations.ts
@@ -2228,9 +2266,36 @@ var init_pg_migrations = __esm(() => {
2228
2266
  pid INTEGER NOT NULL,
2229
2267
  status TEXT NOT NULL,
2230
2268
  updated_at TIMESTAMPTZ NOT NULL,
2269
+ daemon_version TEXT,
2270
+ agent_mode TEXT,
2271
+ platform TEXT,
2272
+ os_version TEXT,
2273
+ os_build TEXT,
2274
+ arch TEXT,
2275
+ uptime_seconds INTEGER,
2276
+ tool_versions_json TEXT,
2277
+ tailscale_json TEXT,
2278
+ storage_sync_status TEXT,
2279
+ storage_sync_last_error TEXT,
2280
+ doctor_summary_json TEXT,
2281
+ private_metadata INTEGER NOT NULL DEFAULT 0,
2231
2282
  PRIMARY KEY (machine_id, pid)
2232
2283
  );
2233
2284
 
2285
+ ALTER TABLE agent_heartbeats ADD COLUMN IF NOT EXISTS daemon_version TEXT;
2286
+ ALTER TABLE agent_heartbeats ADD COLUMN IF NOT EXISTS agent_mode TEXT;
2287
+ ALTER TABLE agent_heartbeats ADD COLUMN IF NOT EXISTS platform TEXT;
2288
+ ALTER TABLE agent_heartbeats ADD COLUMN IF NOT EXISTS os_version TEXT;
2289
+ ALTER TABLE agent_heartbeats ADD COLUMN IF NOT EXISTS os_build TEXT;
2290
+ ALTER TABLE agent_heartbeats ADD COLUMN IF NOT EXISTS arch TEXT;
2291
+ ALTER TABLE agent_heartbeats ADD COLUMN IF NOT EXISTS uptime_seconds INTEGER;
2292
+ ALTER TABLE agent_heartbeats ADD COLUMN IF NOT EXISTS tool_versions_json TEXT;
2293
+ ALTER TABLE agent_heartbeats ADD COLUMN IF NOT EXISTS tailscale_json TEXT;
2294
+ ALTER TABLE agent_heartbeats ADD COLUMN IF NOT EXISTS storage_sync_status TEXT;
2295
+ ALTER TABLE agent_heartbeats ADD COLUMN IF NOT EXISTS storage_sync_last_error TEXT;
2296
+ ALTER TABLE agent_heartbeats ADD COLUMN IF NOT EXISTS doctor_summary_json TEXT;
2297
+ ALTER TABLE agent_heartbeats ADD COLUMN IF NOT EXISTS private_metadata INTEGER NOT NULL DEFAULT 0;
2298
+
2234
2299
  CREATE TABLE IF NOT EXISTS setup_runs (
2235
2300
  id TEXT PRIMARY KEY,
2236
2301
  machine_id TEXT NOT NULL,
@@ -2263,7 +2328,20 @@ function normalizeParams(params) {
2263
2328
  return flat.map((value) => value === undefined ? null : value);
2264
2329
  }
2265
2330
  function sslConfigFor(connectionString) {
2266
- return connectionString.includes("sslmode=require") || connectionString.includes("ssl=true") ? { rejectUnauthorized: false } : undefined;
2331
+ let url;
2332
+ try {
2333
+ url = new URL(connectionString);
2334
+ } catch {
2335
+ return;
2336
+ }
2337
+ const sslMode = url.searchParams.get("sslmode")?.toLowerCase();
2338
+ const ssl = url.searchParams.get("ssl")?.toLowerCase();
2339
+ if (sslMode === "disable" || ssl === "false")
2340
+ return;
2341
+ if (sslMode === "no-verify" || process.env["HASNA_MACHINES_DATABASE_SSL_REJECT_UNAUTHORIZED"] === "0") {
2342
+ return { rejectUnauthorized: false };
2343
+ }
2344
+ return sslMode || ssl === "true" ? { rejectUnauthorized: true } : undefined;
2267
2345
  }
2268
2346
 
2269
2347
  class PgAdapterAsync {
@@ -2603,7 +2681,7 @@ var {
2603
2681
 
2604
2682
  // src/cli/index.ts
2605
2683
  import { registerEventCommands, registerWebhookCommands } from "@hasna/events/commander";
2606
- import { execFileSync } from "child_process";
2684
+ import { execFileSync as execFileSync2 } from "child_process";
2607
2685
 
2608
2686
  // node_modules/chalk/source/vendor/ansi-styles/index.js
2609
2687
  var ANSI_BACKGROUND_OFFSET = 10;
@@ -7095,6 +7173,9 @@ init_paths();
7095
7173
 
7096
7174
  // src/redaction.ts
7097
7175
  var REDACTED_VALUE = "[redacted]";
7176
+ var PRIVATE_OUTPUT_ENV = "HASNA_MACHINES_ALLOW_PRIVATE_OUTPUT";
7177
+ var PRIVATE_OUTPUT_FALLBACK_ENV = "MACHINES_ALLOW_PRIVATE_OUTPUT";
7178
+ var PRIVATE_OUTPUT_DENIED_WARNING = `private_output_denied:set ${PRIVATE_OUTPUT_ENV}=1 to allow private metadata output`;
7098
7179
  var SENSITIVE_KEY_PATTERN = /(password|passwd|token|credential|private[_-]?key|privateKey|api[_-]?key|github.*key|pem|secret)/i;
7099
7180
  var SECRET_REFERENCE_KEY_PATTERN = /(secret(ref(erence)?|key)?|secretRef|secretKey)$/i;
7100
7181
  var SENSITIVE_VALUE_PATTERNS = [
@@ -7105,9 +7186,17 @@ var SENSITIVE_VALUE_PATTERNS = [
7105
7186
  /\bAKIA[0-9A-Z]{16}\b/,
7106
7187
  /\bsk-[A-Za-z0-9_-]{20,}\b/
7107
7188
  ];
7189
+ var IPV4_PATTERN = /\b(?:10|127|169\.254|172\.(?:1[6-9]|2\d|3[0-1])|192\.168|100\.(?:6[4-9]|[7-9]\d|1[01]\d|12[0-7]))(?:\.\d{1,3}){2}\b/g;
7190
+ var IPV6_PATTERN = /\b(?:fc|fd|fe80)[0-9a-f:]*:[0-9a-f:]+\b/gi;
7191
+ var DATABASE_URL_PATTERN = /\b(?:postgres(?:ql)?|mysql|mariadb|redis|mongodb|s3):\/\/[^\s"'<>]+/gi;
7192
+ var PRIVATE_HOST_PATTERN = /\b(?:[A-Za-z0-9._%+-]+@)?[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*(?:\.tailnet(?:\.[A-Za-z0-9-]+)*|\.ts\.net|\.private(?:\.[A-Za-z0-9-]+)*|\.internal|\.local)\b/gi;
7108
7193
  function isSensitiveKey(key) {
7109
7194
  return SENSITIVE_KEY_PATTERN.test(key);
7110
7195
  }
7196
+ function isPrivateOutputEnabled(env2 = process.env) {
7197
+ const value = env2[PRIVATE_OUTPUT_ENV] ?? env2[PRIVATE_OUTPUT_FALLBACK_ENV];
7198
+ return ["1", "true", "yes", "on", "private"].includes(String(value ?? "").trim().toLowerCase());
7199
+ }
7111
7200
  function isSecretReferenceKey(key) {
7112
7201
  return SECRET_REFERENCE_KEY_PATTERN.test(key);
7113
7202
  }
@@ -7120,6 +7209,12 @@ function isRecord(value) {
7120
7209
  function redactPath(value) {
7121
7210
  return value.replace(/\/home\/[^/\s]+/g, "/home/<user>").replace(/\/Users\/[^/\s]+/g, "/Users/<user>").replace(/[A-Za-z]:\\Users\\[^\\\s]+/g, "C:\\Users\\<user>");
7122
7211
  }
7212
+ function redactErrorMessage(value) {
7213
+ return redactPath(value).replace(DATABASE_URL_PATTERN, (match) => {
7214
+ const scheme = match.match(/^([a-z][a-z0-9+.-]*:\/\/)/i)?.[1] ?? "";
7215
+ return `${scheme}${REDACTED_VALUE}`;
7216
+ }).replace(IPV4_PATTERN, REDACTED_VALUE).replace(IPV6_PATTERN, REDACTED_VALUE).replace(PRIVATE_HOST_PATTERN, REDACTED_VALUE);
7217
+ }
7123
7218
  function redactPrivateRef(value) {
7124
7219
  const trimmed = value.trim();
7125
7220
  const scheme = trimmed.match(/^([a-z][a-z0-9+.-]*:\/\/)/i);
@@ -7589,6 +7684,16 @@ function routeRank(hint) {
7589
7684
  function selectRouteHint(hints) {
7590
7685
  return [...hints].sort((left, right) => routeRank(left) - routeRank(right))[0] ?? null;
7591
7686
  }
7687
+ function parseHeartbeatJson(value) {
7688
+ if (!value)
7689
+ return null;
7690
+ try {
7691
+ const parsed = JSON.parse(value);
7692
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
7693
+ } catch {
7694
+ return null;
7695
+ }
7696
+ }
7592
7697
  function buildEntry(input) {
7593
7698
  const manifest = input.manifest;
7594
7699
  const peer = input.peer;
@@ -7611,6 +7716,22 @@ function buildEntry(input) {
7611
7716
  manifest_declared: Boolean(manifest),
7612
7717
  heartbeat_status: input.heartbeat?.status ?? "unknown",
7613
7718
  last_heartbeat_at: input.heartbeat?.updated_at ?? null,
7719
+ agent: {
7720
+ pid: input.heartbeat?.pid ?? null,
7721
+ daemon_version: input.heartbeat?.daemon_version ?? null,
7722
+ mode: input.heartbeat?.agent_mode ?? null,
7723
+ private_metadata: Boolean(input.heartbeat?.private_metadata),
7724
+ platform: input.heartbeat?.platform ?? null,
7725
+ os_version: input.heartbeat?.os_version ?? null,
7726
+ os_build: input.heartbeat?.os_build ?? null,
7727
+ arch: input.heartbeat?.arch ?? null,
7728
+ uptime_seconds: input.heartbeat?.uptime_seconds ?? null,
7729
+ tool_versions: parseHeartbeatJson(input.heartbeat?.tool_versions_json),
7730
+ tailscale: parseHeartbeatJson(input.heartbeat?.tailscale_json),
7731
+ storage_sync_status: input.heartbeat?.storage_sync_status ?? null,
7732
+ storage_sync_last_error: input.heartbeat?.storage_sync_last_error ?? null,
7733
+ doctor_summary: parseHeartbeatJson(input.heartbeat?.doctor_summary_json)
7734
+ },
7614
7735
  tailscale: {
7615
7736
  dns_name: manifest?.tailscaleName ?? peer?.DNSName?.replace(/\.$/, "") ?? null,
7616
7737
  ips: peer?.TailscaleIPs ?? [],
@@ -7670,6 +7791,72 @@ function discoverMachineTopology(options = {}) {
7670
7791
  warnings
7671
7792
  };
7672
7793
  }
7794
+ function redactFleetString(value) {
7795
+ if (!value)
7796
+ return value;
7797
+ return redactErrorMessage(value);
7798
+ }
7799
+ function redactPublicRecord(value) {
7800
+ if (!value)
7801
+ return null;
7802
+ const redacted = {};
7803
+ for (const [key, entry] of Object.entries(value)) {
7804
+ if (/(host|hostname|dns|ip|ips|user|username|serial|address|target|url|token|secret|password|credential)/i.test(key)) {
7805
+ redacted[key] = REDACTED_VALUE;
7806
+ continue;
7807
+ }
7808
+ if (typeof entry === "string") {
7809
+ redacted[key] = redactFleetString(entry);
7810
+ } else if (Array.isArray(entry)) {
7811
+ redacted[key] = entry.map((item) => {
7812
+ if (typeof item === "string")
7813
+ return redactFleetString(item);
7814
+ if (item && typeof item === "object")
7815
+ return redactPublicRecord(item);
7816
+ return item;
7817
+ });
7818
+ } else if (entry && typeof entry === "object") {
7819
+ redacted[key] = redactPublicRecord(entry);
7820
+ } else {
7821
+ redacted[key] = entry;
7822
+ }
7823
+ }
7824
+ return redactSensitiveValue(redacted);
7825
+ }
7826
+ function redactTopologyForOutput(topology, options = {}) {
7827
+ if (options.privateMetadata)
7828
+ return topology;
7829
+ return {
7830
+ ...topology,
7831
+ local_hostname: REDACTED_VALUE,
7832
+ warnings: topology.warnings.map(redactFleetString),
7833
+ machines: topology.machines.map((machine) => ({
7834
+ ...machine,
7835
+ hostname: machine.hostname ? REDACTED_VALUE : null,
7836
+ user: machine.user ? REDACTED_VALUE : null,
7837
+ tailscale: {
7838
+ ...machine.tailscale,
7839
+ dns_name: machine.tailscale.dns_name ? REDACTED_VALUE : null,
7840
+ ips: machine.tailscale.ips.map(() => REDACTED_VALUE)
7841
+ },
7842
+ ssh: {
7843
+ ...machine.ssh,
7844
+ address: machine.ssh.address ? REDACTED_VALUE : null,
7845
+ command_target: machine.ssh.command_target ? REDACTED_VALUE : null
7846
+ },
7847
+ route_hints: machine.route_hints.map((hint) => ({
7848
+ ...hint,
7849
+ target: REDACTED_VALUE
7850
+ })),
7851
+ agent: {
7852
+ ...machine.agent,
7853
+ tailscale: redactPublicRecord(machine.agent.tailscale),
7854
+ storage_sync_last_error: machine.agent.storage_sync_last_error ? redactFleetString(machine.agent.storage_sync_last_error) : null,
7855
+ doctor_summary: redactPublicRecord(machine.agent.doctor_summary)
7856
+ }
7857
+ }))
7858
+ };
7859
+ }
7673
7860
  function normalizeMachineAlias(value) {
7674
7861
  return value.trim().replace(/\.$/, "").toLowerCase();
7675
7862
  }
@@ -7863,6 +8050,20 @@ function resolveMachineRoute(machineId, options = {}) {
7863
8050
  warnings
7864
8051
  };
7865
8052
  }
8053
+ function redactRouteForOutput(route, options = {}) {
8054
+ if (options.privateMetadata)
8055
+ return route;
8056
+ return {
8057
+ ...route,
8058
+ target: route.target ? REDACTED_VALUE : null,
8059
+ command_target: route.command_target ? REDACTED_VALUE : null,
8060
+ warnings: route.warnings.map(redactFleetString),
8061
+ evidence: {
8062
+ ...route.evidence,
8063
+ selected_hint: route.evidence.selected_hint ? { ...route.evidence.selected_hint, target: REDACTED_VALUE } : null
8064
+ }
8065
+ };
8066
+ }
7866
8067
  function isRecord2(value) {
7867
8068
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
7868
8069
  }
@@ -9752,6 +9953,16 @@ function runSync(machineId, options = {}, runner = runMachineCommand) {
9752
9953
  // src/commands/status.ts
9753
9954
  init_db();
9754
9955
  init_paths();
9956
+ function parseJsonObject(value) {
9957
+ if (!value)
9958
+ return null;
9959
+ try {
9960
+ const parsed = JSON.parse(value);
9961
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
9962
+ } catch {
9963
+ return null;
9964
+ }
9965
+ }
9755
9966
  function getStatus() {
9756
9967
  const manifest = readManifest();
9757
9968
  const heartbeats = listHeartbeats();
@@ -9775,7 +9986,12 @@ function getStatus() {
9775
9986
  platform: declared?.platform,
9776
9987
  manifestDeclared: Boolean(declared),
9777
9988
  heartbeatStatus: heartbeat?.status || "unknown",
9778
- lastHeartbeatAt: heartbeat?.updated_at
9989
+ lastHeartbeatAt: heartbeat?.updated_at,
9990
+ daemonVersion: heartbeat?.daemon_version ?? null,
9991
+ agentMode: heartbeat?.agent_mode ?? null,
9992
+ storageSyncStatus: heartbeat?.storage_sync_status ?? null,
9993
+ doctorSummary: parseJsonObject(heartbeat?.doctor_summary_json),
9994
+ privateMetadata: Boolean(heartbeat?.private_metadata)
9779
9995
  };
9780
9996
  }),
9781
9997
  recentSetupRuns: countRuns("setup_runs"),
@@ -10266,14 +10482,20 @@ function runOptionalAdapterChecks(context, adapters) {
10266
10482
  }
10267
10483
  return checks;
10268
10484
  }
10269
- function runDoctor(machineId = getLocalMachineId(), options = {}) {
10485
+ function runDoctor(machineId, options = {}) {
10486
+ const implicitLocalMachine = !machineId;
10487
+ const requestedMachineId = machineId ?? getLocalMachineId();
10488
+ const reportedMachineId = implicitLocalMachine ? "local" : requestedMachineId;
10270
10489
  const now = options.now ?? new Date;
10271
10490
  const { manifest, info: manifestSource } = readManifestWithSource({ adapter: options.manifestAdapter ?? null });
10272
- const commandChecks = runMachineCommand(machineId, buildDoctorCommand());
10491
+ const commandChecks = runMachineCommand(requestedMachineId, buildDoctorCommand());
10273
10492
  const details = parseKeyValueOutput(commandChecks.stdout);
10274
- const machineInManifest = manifest.machines.find((machine) => machine.id === machineId);
10493
+ const machineInManifest = manifest.machines.find((machine) => machine.id === requestedMachineId);
10494
+ const diagnosticMachine = machineInManifest ? redactManifestForDiagnostics(machineInManifest) : null;
10495
+ if (implicitLocalMachine && diagnosticMachine)
10496
+ diagnosticMachine.id = reportedMachineId;
10275
10497
  const optionalAdapterChecks = options.includeOptionalAdapters === false ? [] : runOptionalAdapterChecks({
10276
- machineId,
10498
+ machineId: requestedMachineId,
10277
10499
  manifest,
10278
10500
  manifestSource,
10279
10501
  commandDetails: details,
@@ -10289,10 +10511,10 @@ function runDoctor(machineId = getLocalMachineId(), options = {}) {
10289
10511
  },
10290
10512
  remediation: manifestSource.warnings.length > 0 ? ["Provide a private manifest adapter or unset the private manifest ref to use the local manifest only."] : undefined
10291
10513
  }),
10292
- makeCheck2("manifest-entry", machineInManifest ? "ok" : "warn", machineInManifest ? "Machine exists in manifest" : "Machine missing from manifest", machineInManifest ? JSON.stringify(redactManifestForDiagnostics(machineInManifest)) : `No manifest entry for ${machineId}`, {
10514
+ makeCheck2("manifest-entry", machineInManifest ? "ok" : "warn", machineInManifest ? "Machine exists in manifest" : "Machine missing from manifest", diagnosticMachine ? JSON.stringify(diagnosticMachine) : `No manifest entry for ${reportedMachineId}`, {
10293
10515
  data: {
10294
10516
  declared: Boolean(machineInManifest),
10295
- machine: machineInManifest ? redactManifestForDiagnostics(machineInManifest) : null
10517
+ machine: diagnosticMachine
10296
10518
  }
10297
10519
  }),
10298
10520
  makeCheck2("data-dir", details["data_dir_exists"] === "yes" ? "ok" : "warn", "Data directory check", `${redactPath(details["data_dir"] || "unknown")} ${details["data_dir_exists"] === "yes" ? "exists" : "missing"}`, {
@@ -10343,7 +10565,7 @@ function runDoctor(machineId = getLocalMachineId(), options = {}) {
10343
10565
  ...optionalAdapterChecks
10344
10566
  ];
10345
10567
  return {
10346
- machineId,
10568
+ machineId: reportedMachineId,
10347
10569
  source: commandChecks.source,
10348
10570
  schemaVersion: 1,
10349
10571
  generatedAt: now.toISOString(),
@@ -10355,11 +10577,514 @@ function runDoctor(machineId = getLocalMachineId(), options = {}) {
10355
10577
  };
10356
10578
  }
10357
10579
 
10580
+ // src/commands/daemon.ts
10581
+ import { execFileSync } from "child_process";
10582
+ import { chmodSync, mkdirSync as mkdirSync2, writeFileSync as writeFileSync4 } from "fs";
10583
+ import { dirname as dirname4 } from "path";
10584
+ import { platform as osPlatform } from "os";
10585
+ var DEFAULT_SERVICE_NAME = "machines-agent";
10586
+ var DEFAULT_EXECUTABLE = "/usr/local/bin/machines-agent";
10587
+ var DEFAULT_INTERVAL_MS = 30000;
10588
+ var ENV_NAME_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
10589
+ var SERVICE_NAME_PATTERN = /^[A-Za-z0-9_.-]+$/;
10590
+ function buildDaemonServicePlan(options) {
10591
+ const resolved = resolveDaemonServiceOptions(options);
10592
+ const files = resolved.action === "install" ? [buildServiceFile(resolved)] : [];
10593
+ return {
10594
+ platform: resolved.platform,
10595
+ mode: resolved.mode,
10596
+ action: resolved.action,
10597
+ serviceName: resolved.serviceName,
10598
+ serviceId: resolved.serviceId,
10599
+ executable: resolved.executable,
10600
+ intervalMs: resolved.intervalMs,
10601
+ commands: buildActionCommands(resolved),
10602
+ files,
10603
+ warnings: resolved.warnings,
10604
+ manualSteps: buildManualSteps(resolved, files)
10605
+ };
10606
+ }
10607
+ function runDaemonServicePlan(plan, options = {}) {
10608
+ const apply = options.apply === true;
10609
+ const allowed = apply && options.yes === true;
10610
+ const warnings = [...plan.warnings];
10611
+ const filesWritten = [];
10612
+ const commands = [];
10613
+ if (apply && !allowed) {
10614
+ warnings.push("apply_requires_yes");
10615
+ }
10616
+ if (allowed) {
10617
+ for (const file of plan.files) {
10618
+ const path = expandShellPath(file.path);
10619
+ let content;
10620
+ try {
10621
+ content = materializePlaceholders(file.content);
10622
+ } catch (error) {
10623
+ warnings.push(error instanceof Error ? error.message : String(error));
10624
+ return {
10625
+ mode: "plan",
10626
+ applied: false,
10627
+ plan,
10628
+ filesWritten,
10629
+ commands: plan.commands.map((commandSpec) => ({
10630
+ id: commandSpec.id,
10631
+ command: renderCommand(commandSpec),
10632
+ skipped: true,
10633
+ exitCode: null,
10634
+ stdout: "",
10635
+ stderr: ""
10636
+ })),
10637
+ warnings
10638
+ };
10639
+ }
10640
+ mkdirSync2(dirname4(path), { recursive: true });
10641
+ writeFileSync4(path, content, "utf8");
10642
+ chmodSync(path, Number.parseInt(file.mode, 8));
10643
+ filesWritten.push(path);
10644
+ }
10645
+ }
10646
+ for (const commandSpec of plan.commands) {
10647
+ const commandLine = renderCommand(commandSpec);
10648
+ if (!allowed) {
10649
+ commands.push({
10650
+ id: commandSpec.id,
10651
+ command: commandLine,
10652
+ skipped: true,
10653
+ exitCode: null,
10654
+ stdout: "",
10655
+ stderr: ""
10656
+ });
10657
+ continue;
10658
+ }
10659
+ const program2 = commandLine[0];
10660
+ const args = commandLine.slice(1);
10661
+ try {
10662
+ const result = execFileSync(program2, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
10663
+ commands.push({
10664
+ id: commandSpec.id,
10665
+ command: commandLine,
10666
+ skipped: false,
10667
+ exitCode: 0,
10668
+ stdout: result,
10669
+ stderr: ""
10670
+ });
10671
+ } catch (error) {
10672
+ const maybe = error;
10673
+ if (commandSpec.allowFailure) {
10674
+ commands.push({
10675
+ id: commandSpec.id,
10676
+ command: commandLine,
10677
+ skipped: false,
10678
+ exitCode: typeof maybe.status === "number" ? maybe.status : 1,
10679
+ stdout: String(maybe.stdout ?? ""),
10680
+ stderr: String(maybe.stderr ?? ""),
10681
+ error: maybe.message ?? String(error)
10682
+ });
10683
+ continue;
10684
+ }
10685
+ commands.push({
10686
+ id: commandSpec.id,
10687
+ command: commandLine,
10688
+ skipped: false,
10689
+ exitCode: typeof maybe.status === "number" ? maybe.status : 1,
10690
+ stdout: String(maybe.stdout ?? ""),
10691
+ stderr: String(maybe.stderr ?? ""),
10692
+ error: maybe.message ?? String(error)
10693
+ });
10694
+ break;
10695
+ }
10696
+ }
10697
+ return {
10698
+ mode: allowed ? "apply" : "plan",
10699
+ applied: allowed,
10700
+ plan,
10701
+ filesWritten,
10702
+ commands,
10703
+ warnings
10704
+ };
10705
+ }
10706
+ function resolveDaemonServiceOptions(options) {
10707
+ const warnings = [];
10708
+ const serviceName = normalizeServiceName(options.serviceName, warnings);
10709
+ const platform4 = normalizePlatform3(options.platform, warnings);
10710
+ const mode = options.mode ?? "user";
10711
+ const intervalMs = normalizeIntervalMs(options.intervalMs, warnings);
10712
+ const executable = options.executable?.trim() || DEFAULT_EXECUTABLE;
10713
+ if (platform4 === "linux" && !executable.startsWith("/")) {
10714
+ warnings.push("systemd units should use an absolute executable path; install plan keeps the provided path unchanged.");
10715
+ }
10716
+ return {
10717
+ action: options.action,
10718
+ platform: platform4,
10719
+ mode,
10720
+ serviceName,
10721
+ serviceId: serviceName,
10722
+ executable,
10723
+ intervalMs,
10724
+ env: buildEnvironment(serviceName, options, warnings),
10725
+ warnings
10726
+ };
10727
+ }
10728
+ function normalizeServiceName(value, warnings) {
10729
+ const serviceName = value?.trim() || DEFAULT_SERVICE_NAME;
10730
+ if (SERVICE_NAME_PATTERN.test(serviceName))
10731
+ return serviceName;
10732
+ warnings.push(`Invalid serviceName "${serviceName}"; using ${DEFAULT_SERVICE_NAME}.`);
10733
+ return DEFAULT_SERVICE_NAME;
10734
+ }
10735
+ function normalizePlatform3(value, warnings) {
10736
+ const raw = value ?? osPlatform();
10737
+ if (raw === "darwin" || raw === "macos")
10738
+ return "macos";
10739
+ if (raw === "linux")
10740
+ return "linux";
10741
+ warnings.push(`Unsupported platform "${raw}"; using linux service planning.`);
10742
+ return "linux";
10743
+ }
10744
+ function normalizeIntervalMs(value, warnings) {
10745
+ if (value === undefined)
10746
+ return DEFAULT_INTERVAL_MS;
10747
+ if (Number.isInteger(value) && value > 0)
10748
+ return value;
10749
+ warnings.push(`Invalid intervalMs "${String(value)}"; using ${DEFAULT_INTERVAL_MS}.`);
10750
+ return DEFAULT_INTERVAL_MS;
10751
+ }
10752
+ function buildEnvironment(serviceName, options, warnings) {
10753
+ const env2 = {
10754
+ HASNA_MACHINES_AGENT_MODE: "daemon",
10755
+ HASNA_MACHINES_AGENT_SERVICE: serviceName
10756
+ };
10757
+ if (options.storagePush) {
10758
+ env2["HASNA_MACHINES_AGENT_STORAGE_PUSH"] = "1";
10759
+ env2["HASNA_MACHINES_AGENT_STORAGE_PUSH_BACKOFF_MS"] = "250";
10760
+ env2["HASNA_MACHINES_AGENT_STORAGE_PUSH_RETRIES"] = "2";
10761
+ env2["HASNA_MACHINES_STORAGE_MODE"] = "hybrid";
10762
+ env2["HASNA_MACHINES_DATABASE_URL"] = placeholderForEnv("HASNA_MACHINES_DATABASE_URL");
10763
+ warnings.push("storagePush is represented with env placeholders; no database URL is embedded in the plan.");
10764
+ }
10765
+ if (options.doctorSummary) {
10766
+ env2["HASNA_MACHINES_AGENT_DOCTOR_SUMMARY"] = "1";
10767
+ }
10768
+ if (options.privateMetadata === true) {
10769
+ env2["HASNA_MACHINES_PRIVATE_METADATA"] = "1";
10770
+ warnings.push("privateMetadata=true enables private host/network facts in heartbeat rows; do not share private-mode output publicly.");
10771
+ } else if (Array.isArray(options.privateMetadata)) {
10772
+ addEnvPlaceholders(env2, options.privateMetadata, warnings);
10773
+ }
10774
+ addEnvPlaceholders(env2, options.env ?? [], warnings);
10775
+ return Object.fromEntries(Object.entries(env2).sort(([left], [right]) => left.localeCompare(right)));
10776
+ }
10777
+ function addEnvPlaceholders(env2, names, warnings) {
10778
+ for (const rawName of names) {
10779
+ const name = rawName.trim();
10780
+ if (!ENV_NAME_PATTERN.test(name)) {
10781
+ warnings.push(`Invalid environment variable name "${rawName}"; skipped.`);
10782
+ continue;
10783
+ }
10784
+ env2[name] = placeholderForEnv(name);
10785
+ }
10786
+ }
10787
+ function placeholderForEnv(name) {
10788
+ return `<set:${name}>`;
10789
+ }
10790
+ function buildServiceFile(options) {
10791
+ if (options.platform === "macos") {
10792
+ return {
10793
+ id: "launchd-plist",
10794
+ description: "launchd property list for machines-agent",
10795
+ path: launchdPlistPath(options),
10796
+ mode: "0644",
10797
+ content: launchdPlist(options)
10798
+ };
10799
+ }
10800
+ return {
10801
+ id: "systemd-unit",
10802
+ description: "systemd unit for machines-agent",
10803
+ path: systemdUnitPath(options),
10804
+ mode: "0644",
10805
+ content: systemdUnit(options)
10806
+ };
10807
+ }
10808
+ function buildActionCommands(options) {
10809
+ if (options.platform === "macos")
10810
+ return buildLaunchdCommands(options);
10811
+ return buildSystemdCommands(options);
10812
+ }
10813
+ function buildLaunchdCommands(options) {
10814
+ const domain = launchdDomain(options);
10815
+ const serviceTarget = `${domain}/${options.serviceId}`;
10816
+ const plistPath = launchdPlistPath(options);
10817
+ const sudo = options.mode === "system";
10818
+ if (options.action === "install") {
10819
+ return [
10820
+ command("launchd-bootout-existing", "Unload any existing launchd job before bootstrap.", "launchctl", ["bootout", domain, plistPath], sudo, true, true),
10821
+ command("launchd-bootstrap", "Load the planned launchd plist.", "launchctl", ["bootstrap", domain, plistPath], sudo, true),
10822
+ command("launchd-enable", "Enable the launchd service.", "launchctl", ["enable", serviceTarget], sudo, true),
10823
+ command("launchd-kickstart", "Start or restart the launchd service.", "launchctl", ["kickstart", "-k", serviceTarget], sudo, true)
10824
+ ];
10825
+ }
10826
+ if (options.action === "uninstall") {
10827
+ return [
10828
+ command("launchd-bootout", "Unload the launchd job.", "launchctl", ["bootout", domain, plistPath], sudo, true),
10829
+ command("remove-launchd-plist", "Remove the planned launchd plist file.", "rm", ["-f", plistPath], sudo, true)
10830
+ ];
10831
+ }
10832
+ if (options.action === "restart") {
10833
+ return [command("launchd-kickstart", "Restart the launchd service.", "launchctl", ["kickstart", "-k", serviceTarget], sudo, true)];
10834
+ }
10835
+ if (options.action === "status") {
10836
+ return [command("launchd-print", "Print launchd service status.", "launchctl", ["print", serviceTarget], sudo, false)];
10837
+ }
10838
+ return [
10839
+ command("launchd-logs", "Stream logs for machines-agent.", "log", ["stream", "--style", "compact", "--predicate", `process == "${basename(options.executable)}" OR eventMessage CONTAINS "${options.serviceId}"`], false, false)
10840
+ ];
10841
+ }
10842
+ function buildSystemdCommands(options) {
10843
+ const userFlag = options.mode === "user" ? ["--user"] : [];
10844
+ const sudo = options.mode === "system";
10845
+ const unitName = systemdUnitName(options);
10846
+ const daemonReload = command("systemd-daemon-reload", "Reload systemd unit metadata.", "systemctl", [...userFlag, "daemon-reload"], sudo, true);
10847
+ if (options.action === "install") {
10848
+ return [
10849
+ daemonReload,
10850
+ command("systemd-enable-now", "Enable and start the systemd service.", "systemctl", [...userFlag, "enable", "--now", unitName], sudo, true)
10851
+ ];
10852
+ }
10853
+ if (options.action === "uninstall") {
10854
+ return [
10855
+ command("systemd-disable-now", "Stop and disable the systemd service.", "systemctl", [...userFlag, "disable", "--now", unitName], sudo, true),
10856
+ command("remove-systemd-unit", "Remove the planned systemd unit file.", "rm", ["-f", systemdUnitPath(options)], sudo, true),
10857
+ daemonReload
10858
+ ];
10859
+ }
10860
+ if (options.action === "restart") {
10861
+ return [command("systemd-restart", "Restart the systemd service.", "systemctl", [...userFlag, "restart", unitName], sudo, true)];
10862
+ }
10863
+ if (options.action === "status") {
10864
+ return [command("systemd-status", "Show systemd service status.", "systemctl", [...userFlag, "status", unitName, "--no-pager"], sudo, false)];
10865
+ }
10866
+ return [
10867
+ command("systemd-logs", "Follow journal logs for the service.", "journalctl", [...userFlag, "-u", unitName, "-f", "--no-pager"], sudo, false)
10868
+ ];
10869
+ }
10870
+ function buildManualSteps(options, files) {
10871
+ const steps = [];
10872
+ if (files[0])
10873
+ steps.push(`Write ${files[0].id} content to ${files[0].path} with mode ${files[0].mode}.`);
10874
+ if (options.mode === "system")
10875
+ steps.push("Run commands marked sudo with root privileges.");
10876
+ if (options.platform === "linux" && options.mode === "user") {
10877
+ steps.push("Run commands as the target user; enable lingering separately if the service must survive logout.");
10878
+ }
10879
+ if (options.action === "logs")
10880
+ steps.push("Stop the log command manually when finished.");
10881
+ return steps;
10882
+ }
10883
+ function command(id, description, program2, args, sudo, mutates, allowFailure = false) {
10884
+ return { id, description, program: program2, args, sudo, mutates, ...allowFailure ? { allowFailure: true } : {} };
10885
+ }
10886
+ function renderCommand(commandSpec) {
10887
+ const expanded = [commandSpec.program, ...commandSpec.args].map(expandShellPath);
10888
+ if (commandSpec.sudo)
10889
+ return ["sudo", ...expanded];
10890
+ return expanded;
10891
+ }
10892
+ function expandShellPath(path) {
10893
+ if (path.startsWith("$HOME/"))
10894
+ return `${process.env["HOME"] ?? ""}/${path.slice("$HOME/".length)}`;
10895
+ return path.replaceAll("$HOME", process.env["HOME"] ?? "").replaceAll("$UID", String(process.getuid ? process.getuid() : ""));
10896
+ }
10897
+ function materializePlaceholders(content) {
10898
+ return content.replace(/(?:<set:([A-Z_][A-Z0-9_]*)>|&lt;set:([A-Z_][A-Z0-9_]*)&gt;)/g, (_match, rawName, escapedName) => {
10899
+ const name = rawName ?? escapedName ?? "";
10900
+ const value = process.env[name];
10901
+ if (value === undefined || value === "") {
10902
+ throw new Error(`Missing environment variable required for service apply: ${name}`);
10903
+ }
10904
+ if (/[\u0000-\u001f\u007f]/.test(value)) {
10905
+ throw new Error(`Environment variable ${name} contains control characters; refusing to write service file.`);
10906
+ }
10907
+ return escapedName ? xmlEscape(value) : escapeSystemdEnvironmentValue(value);
10908
+ });
10909
+ }
10910
+ function escapeSystemdEnvironmentValue(value) {
10911
+ return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/%/g, "%%");
10912
+ }
10913
+ function launchdPlist(options) {
10914
+ const env2 = Object.entries(options.env).map(([name, value]) => ` <key>${xmlEscape(name)}</key>
10915
+ <string>${xmlEscape(value)}</string>`).join(`
10916
+ `);
10917
+ return `<?xml version="1.0" encoding="UTF-8"?>
10918
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
10919
+ <plist version="1.0">
10920
+ <dict>
10921
+ <key>Label</key>
10922
+ <string>${xmlEscape(options.serviceId)}</string>
10923
+ <key>ProgramArguments</key>
10924
+ <array>
10925
+ <string>${xmlEscape(options.executable)}</string>
10926
+ <string>--interval-ms</string>
10927
+ <string>${options.intervalMs}</string>
10928
+ </array>
10929
+ <key>EnvironmentVariables</key>
10930
+ <dict>
10931
+ ${env2}
10932
+ </dict>
10933
+ <key>KeepAlive</key>
10934
+ <true/>
10935
+ <key>RunAtLoad</key>
10936
+ <true/>
10937
+ <key>StandardOutPath</key>
10938
+ <string>${xmlEscape(launchdLogPath(options, "out"))}</string>
10939
+ <key>StandardErrorPath</key>
10940
+ <string>${xmlEscape(launchdLogPath(options, "err"))}</string>
10941
+ </dict>
10942
+ </plist>
10943
+ `;
10944
+ }
10945
+ function systemdUnit(options) {
10946
+ const env2 = Object.entries(options.env).map(([name, value]) => `Environment=${quoteSystemdEnvironment(name, value)}`).join(`
10947
+ `);
10948
+ return `[Unit]
10949
+ Description=Hasna machines agent
10950
+ After=network-online.target
10951
+ Wants=network-online.target
10952
+
10953
+ [Service]
10954
+ Type=simple
10955
+ ExecStart=${quoteSystemdExecArg(options.executable)} --interval-ms ${options.intervalMs}
10956
+ Restart=always
10957
+ RestartSec=10
10958
+ ${env2}
10959
+
10960
+ [Install]
10961
+ WantedBy=${options.mode === "system" ? "multi-user.target" : "default.target"}
10962
+ `;
10963
+ }
10964
+ function launchdDomain(options) {
10965
+ return options.mode === "system" ? "system" : "gui/$UID";
10966
+ }
10967
+ function launchdPlistPath(options) {
10968
+ if (options.mode === "system")
10969
+ return `/Library/LaunchDaemons/${options.serviceId}.plist`;
10970
+ return `$HOME/Library/LaunchAgents/${options.serviceId}.plist`;
10971
+ }
10972
+ function launchdLogPath(options, stream) {
10973
+ const fileName = `${options.serviceId}.${stream}.log`;
10974
+ if (options.mode === "system")
10975
+ return `/var/log/${fileName}`;
10976
+ return `$HOME/Library/Logs/${fileName}`;
10977
+ }
10978
+ function systemdUnitName(options) {
10979
+ return `${options.serviceId}.service`;
10980
+ }
10981
+ function systemdUnitPath(options) {
10982
+ if (options.mode === "system")
10983
+ return `/etc/systemd/system/${systemdUnitName(options)}`;
10984
+ return `$HOME/.config/systemd/user/${systemdUnitName(options)}`;
10985
+ }
10986
+ function xmlEscape(value) {
10987
+ return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
10988
+ }
10989
+ function quoteSystemdExecArg(value) {
10990
+ if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(value) && !value.includes("%"))
10991
+ return value;
10992
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/%/g, "%%")}"`;
10993
+ }
10994
+ function quoteSystemdEnvironment(name, value) {
10995
+ return `"${name}=${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/%/g, "%%")}"`;
10996
+ }
10997
+ function basename(path) {
10998
+ return path.split("/").filter(Boolean).at(-1) || path;
10999
+ }
11000
+
10358
11001
  // src/commands/self-test.ts
10359
11002
  init_db();
10360
11003
 
10361
11004
  // src/commands/serve.ts
10362
11005
  import { EventsClient as EventsClient2, sanitizeChannelsForOutput } from "@hasna/events";
11006
+
11007
+ // src/agent/runtime.ts
11008
+ import { arch as arch3, hostname as hostname6, platform as platform4, release, uptime, version as osVersion } from "os";
11009
+ init_db();
11010
+ init_storage_sync();
11011
+ function parseJsonObject2(value) {
11012
+ if (!value)
11013
+ return null;
11014
+ try {
11015
+ const parsed = JSON.parse(value);
11016
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
11017
+ } catch {
11018
+ return null;
11019
+ }
11020
+ }
11021
+ function heartbeatToStatus(heartbeat, options = {}) {
11022
+ const privateMetadata = options.privateMetadata === true;
11023
+ const tailscale = parseJsonObject2(heartbeat.tailscale_json);
11024
+ const doctorSummary = parseJsonObject2(heartbeat.doctor_summary_json);
11025
+ return {
11026
+ machineId: heartbeat.machine_id,
11027
+ pid: heartbeat.pid,
11028
+ status: heartbeat.status,
11029
+ updatedAt: heartbeat.updated_at,
11030
+ daemonVersion: heartbeat.daemon_version,
11031
+ agentMode: heartbeat.agent_mode,
11032
+ platform: heartbeat.platform,
11033
+ osVersion: heartbeat.os_version,
11034
+ osBuild: heartbeat.os_build,
11035
+ arch: heartbeat.arch,
11036
+ uptimeSeconds: heartbeat.uptime_seconds,
11037
+ toolVersions: sanitizeRecord(parseJsonObject2(heartbeat.tool_versions_json) ?? {}, privateMetadata),
11038
+ tailscale: tailscale ? sanitizeRecord(tailscale, privateMetadata) : null,
11039
+ storageSyncStatus: heartbeat.storage_sync_status,
11040
+ storageSyncLastError: heartbeat.storage_sync_last_error ? sanitizeStorageError(heartbeat.storage_sync_last_error, privateMetadata) : null,
11041
+ doctorSummary: doctorSummary ? sanitizeRecord(doctorSummary, privateMetadata) : null,
11042
+ privateMetadata: Boolean(heartbeat.private_metadata)
11043
+ };
11044
+ }
11045
+ function sanitizePublicString(value, privateMetadata = false) {
11046
+ if (privateMetadata)
11047
+ return value;
11048
+ let redacted = value;
11049
+ const localHostname = hostname6();
11050
+ const localUser = process.env["USER"] || process.env["LOGNAME"] || process.env["USERNAME"];
11051
+ if (localHostname)
11052
+ redacted = redacted.replaceAll(localHostname, "[redacted-host]");
11053
+ if (localUser)
11054
+ redacted = redacted.replaceAll(localUser, "[redacted-user]");
11055
+ return redactErrorMessage(redacted.replace(/[a-z][a-z0-9+.-]*:\/\/[^\s'")]+/gi, (match) => {
11056
+ const scheme = match.match(/^([a-z][a-z0-9+.-]*:\/\/)/i)?.[1] ?? "";
11057
+ return `${scheme}[redacted]`;
11058
+ }).replace(/\b10(?:\.\d{1,3}){3}\b/g, "[redacted-ip]").replace(/\b172\.(?:1[6-9]|2\d|3[01])(?:\.\d{1,3}){2}\b/g, "[redacted-ip]").replace(/\b192\.168(?:\.\d{1,3}){2}\b/g, "[redacted-ip]").replace(/\b100\.(?:6[4-9]|[7-9]\d|1[01]\d|12[0-7])(?:\.\d{1,3}){2}\b/g, "[redacted-ip]").replace(/\b(password|passwd|token|secret|api[_-]?key)=([^&\s]+)/gi, "$1=[redacted]"));
11059
+ }
11060
+ function sanitizeValue(value, privateMetadata) {
11061
+ if (typeof value === "string")
11062
+ return sanitizePublicString(value, privateMetadata);
11063
+ if (Array.isArray(value))
11064
+ return value.map((entry) => sanitizeValue(entry, privateMetadata));
11065
+ if (value && typeof value === "object")
11066
+ return sanitizeRecord(value, privateMetadata);
11067
+ return value;
11068
+ }
11069
+ function sanitizeRecord(value, privateMetadata) {
11070
+ const sanitized = {};
11071
+ for (const [key, entry] of Object.entries(value)) {
11072
+ if (!privateMetadata && /(hostname|hostName|user|username|serial|dnsName|ip|ips|databaseUrl|url|token|secret|password|credential)/i.test(key)) {
11073
+ sanitized[key] = "[redacted]";
11074
+ continue;
11075
+ }
11076
+ sanitized[key] = sanitizeValue(entry, privateMetadata);
11077
+ }
11078
+ return sanitized;
11079
+ }
11080
+ function sanitizeStorageError(message, privateMetadata) {
11081
+ return privateMetadata ? message : redactErrorMessage(message);
11082
+ }
11083
+ function getAgentStatus(machineId, options = {}) {
11084
+ return listHeartbeats(machineId).map((heartbeat) => heartbeatToStatus(heartbeat, options));
11085
+ }
11086
+
11087
+ // src/commands/serve.ts
10363
11088
  function escapeHtml(value) {
10364
11089
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
10365
11090
  }
@@ -10374,6 +11099,9 @@ function getServeInfo(options = {}) {
10374
11099
  "/",
10375
11100
  "/health",
10376
11101
  "/api/status",
11102
+ "/api/topology",
11103
+ "/api/routes",
11104
+ "/api/daemon/status",
10377
11105
  "/api/manifest",
10378
11106
  "/api/notifications",
10379
11107
  "/api/webhooks",
@@ -10391,6 +11119,7 @@ function getServeInfo(options = {}) {
10391
11119
  }
10392
11120
  function renderDashboardHtml() {
10393
11121
  const status = getStatus();
11122
+ const topology = discoverMachineTopology();
10394
11123
  const manifest = manifestList();
10395
11124
  const notifications = listNotificationChannels();
10396
11125
  const doctor = runDoctor();
@@ -10429,17 +11158,20 @@ function renderDashboardHtml() {
10429
11158
  <section class="card"><div>Heartbeats</div><div class="stat">${status.heartbeatCount}</div></section>
10430
11159
  <section class="card"><div>Notification channels</div><div class="stat">${notifications.channels.length}</div></section>
10431
11160
  <section class="card"><div>Doctor warnings</div><div class="stat">${doctor.checks.filter((entry) => entry.status !== "ok").length}</div></section>
11161
+ <section class="card"><div>Tailscale routes</div><div class="stat">${topology.machines.filter((machine) => machine.ssh.route === "tailscale").length}</div></section>
10432
11162
  </div>
10433
11163
 
10434
11164
  <section class="card" style="margin-top:16px">
10435
11165
  <h2>Machines</h2>
10436
11166
  <table>
10437
- <thead><tr><th>ID</th><th>Platform</th><th>Status</th><th>Last heartbeat</th></tr></thead>
11167
+ <thead><tr><th>ID</th><th>Platform</th><th>Status</th><th>Agent</th><th>Storage</th><th>Last heartbeat</th></tr></thead>
10438
11168
  <tbody>
10439
11169
  ${status.machines.map((machine) => `<tr>
10440
11170
  <td><code>${escapeHtml(machine.machineId)}</code></td>
10441
11171
  <td>${escapeHtml(machine.platform || "unknown")}</td>
10442
11172
  <td><span class="badge ${escapeHtml(machine.heartbeatStatus)}">${escapeHtml(machine.heartbeatStatus)}</span></td>
11173
+ <td>${escapeHtml(machine.agentMode || "unknown")} ${escapeHtml(machine.daemonVersion || "")}</td>
11174
+ <td>${escapeHtml(machine.storageSyncStatus || "unknown")}</td>
10443
11175
  <td>${escapeHtml(machine.lastHeartbeatAt || "\u2014")}</td>
10444
11176
  </tr>`).join("")}
10445
11177
  </tbody>
@@ -10483,6 +11215,14 @@ function renderDashboardHtml() {
10483
11215
  <script>
10484
11216
  // Auto-refresh dashboard data every 15s
10485
11217
  const REFRESH_INTERVAL = 15000;
11218
+ function escapeHtml(value) {
11219
+ return String(value ?? "")
11220
+ .replaceAll("&", "&amp;")
11221
+ .replaceAll("<", "&lt;")
11222
+ .replaceAll(">", "&gt;")
11223
+ .replaceAll('"', "&quot;")
11224
+ .replaceAll("'", "&#39;");
11225
+ }
10486
11226
  async function refreshData() {
10487
11227
  try {
10488
11228
  const [statusRes, doctorRes] = await Promise.all([
@@ -10503,10 +11243,12 @@ function renderDashboardHtml() {
10503
11243
  tbody.innerHTML = status.machines
10504
11244
  .map((m) =>
10505
11245
  "<tr>" +
10506
- "<td><code>" + m.machineId + "</code></td>" +
10507
- "<td>" + (m.platform || "unknown") + "</td>" +
10508
- '<td><span class="badge ' + m.heartbeatStatus + '">' + m.heartbeatStatus + '</span></td>' +
10509
- "<td>" + (m.lastHeartbeatAt || "\\u2014") + "</td>" +
11246
+ "<td><code>" + escapeHtml(m.machineId) + "</code></td>" +
11247
+ "<td>" + escapeHtml(m.platform || "unknown") + "</td>" +
11248
+ '<td><span class="badge ' + escapeHtml(m.heartbeatStatus) + '">' + escapeHtml(m.heartbeatStatus) + '</span></td>' +
11249
+ "<td>" + escapeHtml(m.agentMode || "unknown") + " " + escapeHtml(m.daemonVersion || "") + "</td>" +
11250
+ "<td>" + escapeHtml(m.storageSyncStatus || "unknown") + "</td>" +
11251
+ "<td>" + escapeHtml(m.lastHeartbeatAt || "\\u2014") + "</td>" +
10510
11252
  "</tr>"
10511
11253
  )
10512
11254
  .join("");
@@ -10518,9 +11260,9 @@ function renderDashboardHtml() {
10518
11260
  doctorTbody.innerHTML = doctor.checks
10519
11261
  .map((c) =>
10520
11262
  "<tr>" +
10521
- "<td>" + c.summary + "</td>" +
10522
- '<td><span class="badge ' + c.status + '">' + c.status + '</span></td>' +
10523
- '<td class="muted">' + c.detail + "</td>" +
11263
+ "<td>" + escapeHtml(c.summary) + "</td>" +
11264
+ '<td><span class="badge ' + escapeHtml(c.status) + '">' + escapeHtml(c.status) + '</span></td>' +
11265
+ '<td class="muted">' + escapeHtml(c.detail) + "</td>" +
10524
11266
  "</tr>"
10525
11267
  )
10526
11268
  .join("");
@@ -10550,6 +11292,14 @@ async function parseJsonBody(request) {
10550
11292
  function jsonError(message, status = 400) {
10551
11293
  return Response.json({ error: message }, { status });
10552
11294
  }
11295
+ function privateOutputWarnings(requested, allowed) {
11296
+ return requested && !allowed ? [PRIVATE_OUTPUT_DENIED_WARNING] : [];
11297
+ }
11298
+ function appendWarnings(payload, warnings) {
11299
+ if (warnings.length === 0)
11300
+ return payload;
11301
+ return { ...payload, warnings: [...payload.warnings ?? [], ...warnings] };
11302
+ }
10553
11303
  function startDashboardServer(options = {}) {
10554
11304
  const info = getServeInfo(options);
10555
11305
  const events = new EventsClient2;
@@ -10560,12 +11310,34 @@ function startDashboardServer(options = {}) {
10560
11310
  const url = new URL(request.url);
10561
11311
  const machineId = url.searchParams.get("machine") || undefined;
10562
11312
  const tools = url.searchParams.get("tools")?.split(",").map((value) => value.trim()).filter(Boolean);
11313
+ const privateMetadataRequested = url.searchParams.get("privateMetadata") === "true" || url.searchParams.get("private_metadata") === "true";
11314
+ const privateMetadata = privateMetadataRequested && isPrivateOutputEnabled();
11315
+ const privateWarnings = privateOutputWarnings(privateMetadataRequested, privateMetadata);
10563
11316
  if (url.pathname === "/health") {
10564
11317
  return Response.json({ ok: true, ...getServeInfo(options) });
10565
11318
  }
10566
11319
  if (url.pathname === "/api/status") {
10567
11320
  return Response.json(getStatus());
10568
11321
  }
11322
+ if (url.pathname === "/api/topology") {
11323
+ const topology = discoverMachineTopology({ includeTailscale: url.searchParams.get("tailscale") !== "false" });
11324
+ return Response.json(appendWarnings(redactTopologyForOutput(topology, { privateMetadata }), privateWarnings));
11325
+ }
11326
+ if (url.pathname === "/api/routes") {
11327
+ const topology = discoverMachineTopology({ includeTailscale: url.searchParams.get("tailscale") !== "false" });
11328
+ return Response.json({
11329
+ generated_at: topology.generated_at,
11330
+ routes: topology.machines.map((machine) => redactRouteForOutput(resolveMachineRoute(machine.machine_id, { topology }), { privateMetadata })),
11331
+ ...privateWarnings.length > 0 ? { warnings: privateWarnings } : {}
11332
+ });
11333
+ }
11334
+ if (url.pathname === "/api/daemon/status") {
11335
+ return Response.json({
11336
+ generated_at: new Date().toISOString(),
11337
+ agents: getAgentStatus(machineId, { privateMetadata }),
11338
+ ...privateWarnings.length > 0 ? { warnings: privateWarnings } : {}
11339
+ });
11340
+ }
10569
11341
  if (url.pathname === "/api/manifest") {
10570
11342
  return Response.json(manifestList());
10571
11343
  }
@@ -10709,7 +11481,7 @@ function runSelfTest() {
10709
11481
  // src/commands/clipboard.ts
10710
11482
  init_paths();
10711
11483
  import { createHash } from "crypto";
10712
- import { existsSync as existsSync8, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync4 } from "fs";
11484
+ import { existsSync as existsSync8, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync5 } from "fs";
10713
11485
  import { join as join6 } from "path";
10714
11486
  var DEFAULT_CONFIG = {
10715
11487
  version: 1,
@@ -10749,7 +11521,7 @@ function readConfig(configPath) {
10749
11521
  function writeConfig(config, configPath) {
10750
11522
  const path = resolveConfigPath(configPath);
10751
11523
  ensureParentDir(path);
10752
- writeFileSync4(path, `${JSON.stringify(config, null, 2)}
11524
+ writeFileSync5(path, `${JSON.stringify(config, null, 2)}
10753
11525
  `, "utf8");
10754
11526
  }
10755
11527
  function readHistory(historyPath) {
@@ -10766,7 +11538,7 @@ function readHistory(historyPath) {
10766
11538
  function writeHistory(entries, historyPath) {
10767
11539
  const path = resolveHistoryPath(historyPath);
10768
11540
  ensureParentDir(path);
10769
- writeFileSync4(path, `${JSON.stringify(entries, null, 2)}
11541
+ writeFileSync5(path, `${JSON.stringify(entries, null, 2)}
10770
11542
  `, "utf8");
10771
11543
  }
10772
11544
  function computeHash(content) {
@@ -10792,7 +11564,7 @@ function getOrCreateClipboardKey() {
10792
11564
  }
10793
11565
  const key = createHash("sha256").update(crypto.randomUUID()).digest("hex").slice(0, 32);
10794
11566
  ensureParentDir(keyPath);
10795
- writeFileSync4(keyPath, `${key}
11567
+ writeFileSync5(keyPath, `${key}
10796
11568
  `, "utf8");
10797
11569
  return key;
10798
11570
  }
@@ -10843,7 +11615,7 @@ function getClipboardStatus(historyPath) {
10843
11615
 
10844
11616
  // src/commands/clipboard-daemon.ts
10845
11617
  init_paths();
10846
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
11618
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "fs";
10847
11619
  import { join as join7 } from "path";
10848
11620
  import { createHash as createHash3 } from "crypto";
10849
11621
 
@@ -10853,12 +11625,12 @@ import { createServer } from "http";
10853
11625
  import { createHash as createHash2 } from "crypto";
10854
11626
  import { readFileSync as readFileSync7 } from "fs";
10855
11627
  function readLocalClipboardSync() {
10856
- const platform4 = process.platform;
10857
- if (platform4 === "darwin") {
11628
+ const platform5 = process.platform;
11629
+ if (platform5 === "darwin") {
10858
11630
  const result = Bun.spawnSync(["pbpaste"], { stdout: "pipe", stderr: "pipe" });
10859
11631
  return result.exitCode === 0 ? result.stdout.toString("utf8").trim() : "";
10860
11632
  }
10861
- if (platform4 === "linux") {
11633
+ if (platform5 === "linux") {
10862
11634
  if (hasCommand3("wl-paste")) {
10863
11635
  const result = Bun.spawnSync(["wl-paste"], { stdout: "pipe", stderr: "pipe" });
10864
11636
  return result.exitCode === 0 ? result.stdout.toString("utf8").trim() : "";
@@ -10872,12 +11644,12 @@ function readLocalClipboardSync() {
10872
11644
  return "";
10873
11645
  }
10874
11646
  function writeLocalClipboardSync(content) {
10875
- const platform4 = process.platform;
10876
- if (platform4 === "darwin") {
11647
+ const platform5 = process.platform;
11648
+ if (platform5 === "darwin") {
10877
11649
  const result = Bun.spawnSync(["pbcopy"], { stdin: new TextEncoder().encode(content), stdout: "ignore", stderr: "ignore" });
10878
11650
  return result.exitCode === 0;
10879
11651
  }
10880
- if (platform4 === "linux") {
11652
+ if (platform5 === "linux") {
10881
11653
  if (hasCommand3("wl-copy")) {
10882
11654
  const result = Bun.spawnSync(["wl-copy"], { stdin: new TextEncoder().encode(content), stdout: "ignore", stderr: "ignore" });
10883
11655
  return result.exitCode === 0;
@@ -11004,12 +11776,12 @@ function handleGetClipboard(response, config) {
11004
11776
  // src/commands/clipboard-daemon.ts
11005
11777
  var DAEMON_PID_PATH = join7(getDataDir(), "clipboard-daemon.pid");
11006
11778
  function readLocalClipboardSync2() {
11007
- const platform4 = process.platform;
11008
- if (platform4 === "darwin") {
11779
+ const platform5 = process.platform;
11780
+ if (platform5 === "darwin") {
11009
11781
  const result = Bun.spawnSync(["pbpaste"], { stdout: "pipe", stderr: "pipe" });
11010
11782
  return result.exitCode === 0 ? result.stdout.toString("utf8").trim() : "";
11011
11783
  }
11012
- if (platform4 === "linux") {
11784
+ if (platform5 === "linux") {
11013
11785
  if (hasCommand4("wl-paste")) {
11014
11786
  const result = Bun.spawnSync(["wl-paste"], { stdout: "pipe", stderr: "pipe" });
11015
11787
  return result.exitCode === 0 ? result.stdout.toString("utf8").trim() : "";
@@ -11070,7 +11842,7 @@ function loadSharedSecret2() {
11070
11842
  }
11071
11843
  }
11072
11844
  function writePid(pid) {
11073
- writeFileSync5(DAEMON_PID_PATH, `${pid}
11845
+ writeFileSync6(DAEMON_PID_PATH, `${pid}
11074
11846
  `);
11075
11847
  }
11076
11848
  function readPid() {
@@ -11165,7 +11937,7 @@ async function discoverPeers() {
11165
11937
  }
11166
11938
  }
11167
11939
  } catch {}
11168
- const knownPeers = ["100.82.44.120", "100.100.226.69", "100.71.123.34", "100.85.234.92"];
11940
+ const knownPeers = (process.env["HASNA_MACHINES_CLIPBOARD_PEERS"] || "").split(",").map((peer) => peer.trim()).filter(Boolean);
11169
11941
  for (const ip of knownPeers) {
11170
11942
  if (!peers.some((p) => p.host === ip)) {
11171
11943
  peers.push({ host: ip, port: config.port });
@@ -11176,7 +11948,7 @@ async function discoverPeers() {
11176
11948
 
11177
11949
  // src/commands/heal.ts
11178
11950
  init_paths();
11179
- import { existsSync as existsSync9, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "fs";
11951
+ import { existsSync as existsSync9, readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "fs";
11180
11952
  import { join as join8 } from "path";
11181
11953
  var DEFAULT_THRESHOLDS = {
11182
11954
  reconnect: 3,
@@ -11241,7 +12013,7 @@ function readHealConfig(path) {
11241
12013
  function writeHealConfig(config, path) {
11242
12014
  const p = path || getHealConfigPath();
11243
12015
  ensureParentDir(p);
11244
- writeFileSync6(p, `${JSON.stringify(config, null, 2)}
12016
+ writeFileSync7(p, `${JSON.stringify(config, null, 2)}
11245
12017
  `, "utf8");
11246
12018
  }
11247
12019
  function readHealState(path) {
@@ -11257,7 +12029,7 @@ function readHealState(path) {
11257
12029
  function writeHealState(state, path) {
11258
12030
  const p = path || getHealStatePath();
11259
12031
  ensureParentDir(p);
11260
- writeFileSync6(p, `${JSON.stringify(state, null, 2)}
12032
+ writeFileSync7(p, `${JSON.stringify(state, null, 2)}
11261
12033
  `, "utf8");
11262
12034
  }
11263
12035
  function evaluateHealth(probe, config, state) {
@@ -11462,7 +12234,7 @@ function executeAction(action, config) {
11462
12234
 
11463
12235
  // src/commands/heal-daemon.ts
11464
12236
  init_paths();
11465
- import { existsSync as existsSync10, readFileSync as readFileSync10, writeFileSync as writeFileSync7 } from "fs";
12237
+ import { existsSync as existsSync10, readFileSync as readFileSync10, writeFileSync as writeFileSync8 } from "fs";
11466
12238
  import { join as join9 } from "path";
11467
12239
  var DAEMON_PID_PATH2 = join9(getDataDir(), "heal-daemon.pid");
11468
12240
  var SERVICE_PATH = "/etc/systemd/system/machines-heal.service";
@@ -11505,7 +12277,7 @@ function runHealOnce(config, opts = {}) {
11505
12277
  return result;
11506
12278
  }
11507
12279
  function writePid2(pid) {
11508
- writeFileSync7(DAEMON_PID_PATH2, `${pid}
12280
+ writeFileSync8(DAEMON_PID_PATH2, `${pid}
11509
12281
  `);
11510
12282
  }
11511
12283
  function readPid2() {
@@ -11596,7 +12368,7 @@ ${key}=${value}
11596
12368
  };
11597
12369
  set("RuntimeWatchdogSec", "20s");
11598
12370
  set("RebootWatchdogSec", "2min");
11599
- writeFileSync7(SYSTEM_CONF, conf);
12371
+ writeFileSync8(SYSTEM_CONF, conf);
11600
12372
  sh2("systemctl daemon-reexec");
11601
12373
  log2.push("hardware watchdog: RuntimeWatchdogSec=20s RebootWatchdogSec=2min");
11602
12374
  return log2;
@@ -11641,7 +12413,7 @@ Environment=PATH=${binDir}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sb
11641
12413
  [Install]
11642
12414
  WantedBy=multi-user.target
11643
12415
  `;
11644
- writeFileSync7(SERVICE_PATH, unit);
12416
+ writeFileSync8(SERVICE_PATH, unit);
11645
12417
  sh2("systemctl daemon-reload");
11646
12418
  sh2("systemctl enable --now machines-heal.service");
11647
12419
  log2.push(`installed + enabled ${SERVICE_PATH} (ExecStart=${exec} heal daemon)`);
@@ -11822,18 +12594,18 @@ function checkSecretPresence(secretsCommand, key) {
11822
12594
  };
11823
12595
  }
11824
12596
  function parseCommandSpec(value) {
11825
- const [command, expectedVersion] = value.split(":");
12597
+ const [command2, expectedVersion] = value.split(":");
11826
12598
  return {
11827
- command,
12599
+ command: command2,
11828
12600
  expectedVersion: expectedVersion || undefined,
11829
12601
  required: true
11830
12602
  };
11831
12603
  }
11832
12604
  function parsePackageSpec(value) {
11833
- const [name, command, expectedVersion] = value.split(":");
12605
+ const [name, command2, expectedVersion] = value.split(":");
11834
12606
  return {
11835
12607
  name,
11836
- command: command || undefined,
12608
+ command: command2 || undefined,
11837
12609
  expectedVersion: expectedVersion || undefined,
11838
12610
  required: true
11839
12611
  };
@@ -11924,17 +12696,54 @@ function renderFleetStatus(status) {
11924
12696
  ["sync runs", String(status.recentSyncRuns)]
11925
12697
  ]),
11926
12698
  "",
11927
- ...status.machines.map((machine) => `${machine.machineId.padEnd(18)} ${machine.platform || "unknown"} ${machine.heartbeatStatus} ${machine.lastHeartbeatAt || "\u2014"}`)
12699
+ ...status.machines.map((machine) => `${machine.machineId.padEnd(18)} ${machine.platform || "unknown"} ${machine.heartbeatStatus} ${machine.agentMode || "agent:unknown"} ${machine.storageSyncStatus || "storage:unknown"} ${machine.lastHeartbeatAt || "\u2014"}`)
12700
+ ].join(`
12701
+ `);
12702
+ }
12703
+ function renderShellCommand(command2) {
12704
+ const parts = command2.sudo ? ["sudo", command2.program, ...command2.args] : [command2.program, ...command2.args];
12705
+ return parts.map((part) => /^[A-Za-z0-9_@%+=:,./$-]+$/.test(part) ? part : JSON.stringify(part)).join(" ");
12706
+ }
12707
+ function renderDaemonPlan(plan) {
12708
+ const files = plan.files.map((file) => `${file.path} (${file.mode})`);
12709
+ const commands = plan.commands.map((command2) => `${command2.mutates ? "apply" : "read"} ${command2.id}: ${renderShellCommand(command2)}`);
12710
+ return [
12711
+ renderKeyValueTable([
12712
+ ["action", plan.action],
12713
+ ["platform", plan.platform],
12714
+ ["mode", plan.mode],
12715
+ ["service", plan.serviceName],
12716
+ ["executable", plan.executable],
12717
+ ["interval", `${plan.intervalMs}ms`],
12718
+ ["warnings", plan.warnings.join(", ") || "none"]
12719
+ ]),
12720
+ renderList("files", files),
12721
+ renderList("commands", commands),
12722
+ renderList("manual steps", plan.manualSteps)
11928
12723
  ].join(`
11929
12724
  `);
11930
12725
  }
12726
+ function parseDaemonOptions(action, options) {
12727
+ return {
12728
+ action,
12729
+ platform: options.platform,
12730
+ mode: options.mode,
12731
+ serviceName: options.serviceName,
12732
+ executable: options.executable,
12733
+ intervalMs: options.intervalMs ? parseIntegerOption(options.intervalMs, "interval-ms", { min: 1 }) : undefined,
12734
+ storagePush: options.storagePush,
12735
+ doctorSummary: options.doctorSummary,
12736
+ privateMetadata: options.privateMetadata,
12737
+ env: options.env
12738
+ };
12739
+ }
11931
12740
  program2.name("machines").description("Machine fleet management CLI + MCP for developers").version(getPackageVersion()).option("-q, --quiet", "Suppress non-essential output");
11932
12741
  var manifestCommand = program2.command("manifest").description("Manage the fleet manifest");
11933
12742
  var appsCommand = program2.command("apps").description("Manage installed applications per machine");
11934
12743
  var notificationsCommand = program2.command("notifications").description("Manage fleet alert delivery channels");
11935
12744
  var eventWebhooksCommand = registerWebhookCommands(program2, { source: "machines" });
11936
12745
  eventWebhooksCommand.description("Manage shared event webhook subscriptions");
11937
- var webhookTestCommand = eventWebhooksCommand.commands.find((command) => command.name() === "test");
12746
+ var webhookTestCommand = eventWebhooksCommand.commands.find((command2) => command2.name() === "test");
11938
12747
  var webhookOptions = webhookTestCommand?.options ?? [];
11939
12748
  var webhookMessageOption = webhookOptions.find((option) => option.long === "--message");
11940
12749
  if (webhookMessageOption) {
@@ -11945,6 +12754,23 @@ eventsCommand.description("Emit, list, and replay shared events");
11945
12754
  var runtimeCommand = program2.command("runtime").description("Watch runtime conditions and emit shared events");
11946
12755
  var clipboardCommand = program2.command("clipboard").description("Real-time clipboard sync across fleet machines");
11947
12756
  var installClaudeCommand = program2.command("install-claude").description("Install or inspect Claude, Codex, and Gemini CLIs");
12757
+ var daemonCommand = program2.command("daemon").description("Install and inspect the machines-agent fleet daemon service");
12758
+ function addDaemonLifecycleCommand(action, description) {
12759
+ 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) => {
12760
+ const plan = buildDaemonServicePlan(parseDaemonOptions(action, options));
12761
+ const result = runDaemonServicePlan(plan, { apply: options.apply, yes: options.yes });
12762
+ if (options.json || options.apply) {
12763
+ console.log(JSON.stringify(result, null, 2));
12764
+ return;
12765
+ }
12766
+ console.log(renderDaemonPlan(plan));
12767
+ });
12768
+ }
12769
+ addDaemonLifecycleCommand("install", "Plan or install the machines-agent daemon service");
12770
+ addDaemonLifecycleCommand("uninstall", "Plan or uninstall the machines-agent daemon service");
12771
+ addDaemonLifecycleCommand("restart", "Plan or restart the machines-agent daemon service");
12772
+ addDaemonLifecycleCommand("status", "Plan a daemon service status command");
12773
+ addDaemonLifecycleCommand("logs", "Plan a daemon service log command");
11948
12774
  manifestCommand.command("init").description("Create an empty fleet manifest").action(() => {
11949
12775
  console.log(manifestInit());
11950
12776
  });
@@ -12049,8 +12875,9 @@ program2.command("sync").description("Reconcile a machine against the fleet mani
12049
12875
  const result = options.apply ? runSync(options.machine, { apply: true, yes: options.yes }) : buildSyncPlan(options.machine);
12050
12876
  console.log(JSON.stringify(result, null, 2));
12051
12877
  });
12052
- program2.command("topology").description("Discover local, manifest, heartbeat, SSH, and Tailscale machine topology").option("--no-tailscale", "Skip tailscale status probing").option("-j, --json", "Print JSON output", false).action((options) => {
12053
- const topology = discoverMachineTopology({ includeTailscale: options.tailscale !== false });
12878
+ 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) => {
12879
+ const rawTopology = discoverMachineTopology({ includeTailscale: options.tailscale !== false });
12880
+ const topology = redactTopologyForOutput(rawTopology, { privateMetadata: options.privateMetadata });
12054
12881
  if (options.json) {
12055
12882
  console.log(JSON.stringify(topology, null, 2));
12056
12883
  return;
@@ -12290,11 +13117,12 @@ program2.command("install-tailscale").description("Install Tailscale on a machin
12290
13117
  const result = options.apply ? runTailscaleInstall(options.machine, { apply: true, yes: options.yes }) : buildTailscaleInstallPlan(options.machine);
12291
13118
  console.log(JSON.stringify(result, null, 2));
12292
13119
  });
12293
- program2.command("route").description("Resolve the best route for a machine").requiredOption("--machine <id>", "Machine identifier").option("--no-tailscale", "Skip tailscale status probing").option("--cmd <command>", "Remote command to run").option("-j, --json", "Print JSON output", false).action((options) => {
13120
+ 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) => {
12294
13121
  const topology = discoverMachineTopology({ includeTailscale: options.tailscale !== false });
12295
13122
  const resolved = resolveMachineRoute(options.machine, { topology });
12296
- const command = resolved.ok && resolved.target ? resolved.route === "local" ? options.cmd ?? null : buildSshCommand(options.machine, options.cmd, { topology }) : null;
12297
- const payload = { ...resolved, command };
13123
+ const publicResolved = redactRouteForOutput(resolved, { privateMetadata: options.privateMetadata });
13124
+ const command2 = resolved.ok && resolved.target ? resolved.route === "local" ? options.cmd ?? null : buildSshCommand(options.machine, options.cmd, { topology }) : null;
13125
+ const payload = { ...publicResolved, command: options.privateMetadata ? command2 : command2 ? "[redacted]" : null };
12298
13126
  if (options.json) {
12299
13127
  console.log(JSON.stringify(payload, null, 2));
12300
13128
  return;
@@ -12304,7 +13132,7 @@ program2.command("route").description("Resolve the best route for a machine").re
12304
13132
  process.exitCode = 1;
12305
13133
  return;
12306
13134
  }
12307
- console.log(command ?? `${resolved.route}:${resolved.target}`);
13135
+ console.log(options.privateMetadata ? command2 ?? `${resolved.route}:${resolved.target}` : `${publicResolved.route}:${publicResolved.target ?? "unresolved"}`);
12308
13136
  });
12309
13137
  program2.command("ssh").description("Choose the best SSH route for a machine").requiredOption("--machine <id>", "Machine identifier").option("--cmd <command>", "Remote command to run").option("-j, --json", "Print JSON output", false).action((options) => {
12310
13138
  if (options.json) {
@@ -12332,7 +13160,7 @@ program2.command("screen").description("Open Screen Sharing (VNC) to a machine u
12332
13160
  for (const r of results) {
12333
13161
  if (r.ok && r.url) {
12334
13162
  if (!options.print)
12335
- execFileSync("open", [r.url], { stdio: "ignore" });
13163
+ execFileSync2("open", [r.url], { stdio: "ignore" });
12336
13164
  console.log(`${r.ok ? "\u2713" : "\u2717"} ${r.machine.padEnd(14)} ${r.url ?? r.error}`);
12337
13165
  } else {
12338
13166
  console.log(`\u2717 ${r.machine.padEnd(14)} ${r.error}`);
@@ -12355,7 +13183,7 @@ program2.command("screen").description("Open Screen Sharing (VNC) to a machine u
12355
13183
  console.log(resolved.url);
12356
13184
  return;
12357
13185
  }
12358
- execFileSync("open", [resolved.url], { stdio: "ignore" });
13186
+ execFileSync2("open", [resolved.url], { stdio: "ignore" });
12359
13187
  console.log(`Opening Screen Sharing \u2192 ${resolved.url} (route: ${resolved.route})`);
12360
13188
  });
12361
13189
  program2.command("screen-credentials").description("Inspect screen-sharing user and password secret references without printing secrets").option("--machine <id>", "Machine identifier").option("--all", "Inspect every discovered machine", false).option("--check-secret", "Check whether the password secret exists in the local secrets vault", false).option("--secrets-command <command>", "Secrets CLI command to inspect", "secrets").option("--no-tailscale", "Skip tailscale status probing").option("-j, --json", "Print JSON output", false).action((options) => {