@hasna/machines 0.0.37 → 0.0.39

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;
@@ -3095,12 +3173,12 @@ var chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
3095
3173
  var source_default = chalk;
3096
3174
 
3097
3175
  // src/version.ts
3098
- import { existsSync, readFileSync } from "fs";
3176
+ import { existsSync, readFileSync, realpathSync } from "fs";
3099
3177
  import { dirname, join } from "path";
3100
3178
  import { fileURLToPath } from "url";
3101
3179
  function getPackageVersion() {
3102
3180
  try {
3103
- const here = dirname(fileURLToPath(import.meta.url));
3181
+ const here = dirname(realpathSync(fileURLToPath(import.meta.url)));
3104
3182
  const candidates = [join(here, "..", "package.json"), join(here, "..", "..", "package.json")];
3105
3183
  const pkgPath = candidates.find((candidate) => existsSync(candidate));
3106
3184
  if (!pkgPath) {
@@ -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"),
@@ -10361,11 +10577,514 @@ function runDoctor(machineId, options = {}) {
10361
10577
  };
10362
10578
  }
10363
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
+
10364
11001
  // src/commands/self-test.ts
10365
11002
  init_db();
10366
11003
 
10367
11004
  // src/commands/serve.ts
10368
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
10369
11088
  function escapeHtml(value) {
10370
11089
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
10371
11090
  }
@@ -10380,6 +11099,9 @@ function getServeInfo(options = {}) {
10380
11099
  "/",
10381
11100
  "/health",
10382
11101
  "/api/status",
11102
+ "/api/topology",
11103
+ "/api/routes",
11104
+ "/api/daemon/status",
10383
11105
  "/api/manifest",
10384
11106
  "/api/notifications",
10385
11107
  "/api/webhooks",
@@ -10397,6 +11119,7 @@ function getServeInfo(options = {}) {
10397
11119
  }
10398
11120
  function renderDashboardHtml() {
10399
11121
  const status = getStatus();
11122
+ const topology = discoverMachineTopology();
10400
11123
  const manifest = manifestList();
10401
11124
  const notifications = listNotificationChannels();
10402
11125
  const doctor = runDoctor();
@@ -10435,17 +11158,20 @@ function renderDashboardHtml() {
10435
11158
  <section class="card"><div>Heartbeats</div><div class="stat">${status.heartbeatCount}</div></section>
10436
11159
  <section class="card"><div>Notification channels</div><div class="stat">${notifications.channels.length}</div></section>
10437
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>
10438
11162
  </div>
10439
11163
 
10440
11164
  <section class="card" style="margin-top:16px">
10441
11165
  <h2>Machines</h2>
10442
11166
  <table>
10443
- <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>
10444
11168
  <tbody>
10445
11169
  ${status.machines.map((machine) => `<tr>
10446
11170
  <td><code>${escapeHtml(machine.machineId)}</code></td>
10447
11171
  <td>${escapeHtml(machine.platform || "unknown")}</td>
10448
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>
10449
11175
  <td>${escapeHtml(machine.lastHeartbeatAt || "\u2014")}</td>
10450
11176
  </tr>`).join("")}
10451
11177
  </tbody>
@@ -10489,6 +11215,14 @@ function renderDashboardHtml() {
10489
11215
  <script>
10490
11216
  // Auto-refresh dashboard data every 15s
10491
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
+ }
10492
11226
  async function refreshData() {
10493
11227
  try {
10494
11228
  const [statusRes, doctorRes] = await Promise.all([
@@ -10509,10 +11243,12 @@ function renderDashboardHtml() {
10509
11243
  tbody.innerHTML = status.machines
10510
11244
  .map((m) =>
10511
11245
  "<tr>" +
10512
- "<td><code>" + m.machineId + "</code></td>" +
10513
- "<td>" + (m.platform || "unknown") + "</td>" +
10514
- '<td><span class="badge ' + m.heartbeatStatus + '">' + m.heartbeatStatus + '</span></td>' +
10515
- "<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>" +
10516
11252
  "</tr>"
10517
11253
  )
10518
11254
  .join("");
@@ -10524,9 +11260,9 @@ function renderDashboardHtml() {
10524
11260
  doctorTbody.innerHTML = doctor.checks
10525
11261
  .map((c) =>
10526
11262
  "<tr>" +
10527
- "<td>" + c.summary + "</td>" +
10528
- '<td><span class="badge ' + c.status + '">' + c.status + '</span></td>' +
10529
- '<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>" +
10530
11266
  "</tr>"
10531
11267
  )
10532
11268
  .join("");
@@ -10556,6 +11292,14 @@ async function parseJsonBody(request) {
10556
11292
  function jsonError(message, status = 400) {
10557
11293
  return Response.json({ error: message }, { status });
10558
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
+ }
10559
11303
  function startDashboardServer(options = {}) {
10560
11304
  const info = getServeInfo(options);
10561
11305
  const events = new EventsClient2;
@@ -10566,12 +11310,34 @@ function startDashboardServer(options = {}) {
10566
11310
  const url = new URL(request.url);
10567
11311
  const machineId = url.searchParams.get("machine") || undefined;
10568
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);
10569
11316
  if (url.pathname === "/health") {
10570
11317
  return Response.json({ ok: true, ...getServeInfo(options) });
10571
11318
  }
10572
11319
  if (url.pathname === "/api/status") {
10573
11320
  return Response.json(getStatus());
10574
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
+ }
10575
11341
  if (url.pathname === "/api/manifest") {
10576
11342
  return Response.json(manifestList());
10577
11343
  }
@@ -10715,7 +11481,7 @@ function runSelfTest() {
10715
11481
  // src/commands/clipboard.ts
10716
11482
  init_paths();
10717
11483
  import { createHash } from "crypto";
10718
- 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";
10719
11485
  import { join as join6 } from "path";
10720
11486
  var DEFAULT_CONFIG = {
10721
11487
  version: 1,
@@ -10755,7 +11521,7 @@ function readConfig(configPath) {
10755
11521
  function writeConfig(config, configPath) {
10756
11522
  const path = resolveConfigPath(configPath);
10757
11523
  ensureParentDir(path);
10758
- writeFileSync4(path, `${JSON.stringify(config, null, 2)}
11524
+ writeFileSync5(path, `${JSON.stringify(config, null, 2)}
10759
11525
  `, "utf8");
10760
11526
  }
10761
11527
  function readHistory(historyPath) {
@@ -10772,7 +11538,7 @@ function readHistory(historyPath) {
10772
11538
  function writeHistory(entries, historyPath) {
10773
11539
  const path = resolveHistoryPath(historyPath);
10774
11540
  ensureParentDir(path);
10775
- writeFileSync4(path, `${JSON.stringify(entries, null, 2)}
11541
+ writeFileSync5(path, `${JSON.stringify(entries, null, 2)}
10776
11542
  `, "utf8");
10777
11543
  }
10778
11544
  function computeHash(content) {
@@ -10798,7 +11564,7 @@ function getOrCreateClipboardKey() {
10798
11564
  }
10799
11565
  const key = createHash("sha256").update(crypto.randomUUID()).digest("hex").slice(0, 32);
10800
11566
  ensureParentDir(keyPath);
10801
- writeFileSync4(keyPath, `${key}
11567
+ writeFileSync5(keyPath, `${key}
10802
11568
  `, "utf8");
10803
11569
  return key;
10804
11570
  }
@@ -10849,7 +11615,7 @@ function getClipboardStatus(historyPath) {
10849
11615
 
10850
11616
  // src/commands/clipboard-daemon.ts
10851
11617
  init_paths();
10852
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
11618
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "fs";
10853
11619
  import { join as join7 } from "path";
10854
11620
  import { createHash as createHash3 } from "crypto";
10855
11621
 
@@ -10859,12 +11625,12 @@ import { createServer } from "http";
10859
11625
  import { createHash as createHash2 } from "crypto";
10860
11626
  import { readFileSync as readFileSync7 } from "fs";
10861
11627
  function readLocalClipboardSync() {
10862
- const platform4 = process.platform;
10863
- if (platform4 === "darwin") {
11628
+ const platform5 = process.platform;
11629
+ if (platform5 === "darwin") {
10864
11630
  const result = Bun.spawnSync(["pbpaste"], { stdout: "pipe", stderr: "pipe" });
10865
11631
  return result.exitCode === 0 ? result.stdout.toString("utf8").trim() : "";
10866
11632
  }
10867
- if (platform4 === "linux") {
11633
+ if (platform5 === "linux") {
10868
11634
  if (hasCommand3("wl-paste")) {
10869
11635
  const result = Bun.spawnSync(["wl-paste"], { stdout: "pipe", stderr: "pipe" });
10870
11636
  return result.exitCode === 0 ? result.stdout.toString("utf8").trim() : "";
@@ -10878,12 +11644,12 @@ function readLocalClipboardSync() {
10878
11644
  return "";
10879
11645
  }
10880
11646
  function writeLocalClipboardSync(content) {
10881
- const platform4 = process.platform;
10882
- if (platform4 === "darwin") {
11647
+ const platform5 = process.platform;
11648
+ if (platform5 === "darwin") {
10883
11649
  const result = Bun.spawnSync(["pbcopy"], { stdin: new TextEncoder().encode(content), stdout: "ignore", stderr: "ignore" });
10884
11650
  return result.exitCode === 0;
10885
11651
  }
10886
- if (platform4 === "linux") {
11652
+ if (platform5 === "linux") {
10887
11653
  if (hasCommand3("wl-copy")) {
10888
11654
  const result = Bun.spawnSync(["wl-copy"], { stdin: new TextEncoder().encode(content), stdout: "ignore", stderr: "ignore" });
10889
11655
  return result.exitCode === 0;
@@ -11010,12 +11776,12 @@ function handleGetClipboard(response, config) {
11010
11776
  // src/commands/clipboard-daemon.ts
11011
11777
  var DAEMON_PID_PATH = join7(getDataDir(), "clipboard-daemon.pid");
11012
11778
  function readLocalClipboardSync2() {
11013
- const platform4 = process.platform;
11014
- if (platform4 === "darwin") {
11779
+ const platform5 = process.platform;
11780
+ if (platform5 === "darwin") {
11015
11781
  const result = Bun.spawnSync(["pbpaste"], { stdout: "pipe", stderr: "pipe" });
11016
11782
  return result.exitCode === 0 ? result.stdout.toString("utf8").trim() : "";
11017
11783
  }
11018
- if (platform4 === "linux") {
11784
+ if (platform5 === "linux") {
11019
11785
  if (hasCommand4("wl-paste")) {
11020
11786
  const result = Bun.spawnSync(["wl-paste"], { stdout: "pipe", stderr: "pipe" });
11021
11787
  return result.exitCode === 0 ? result.stdout.toString("utf8").trim() : "";
@@ -11076,7 +11842,7 @@ function loadSharedSecret2() {
11076
11842
  }
11077
11843
  }
11078
11844
  function writePid(pid) {
11079
- writeFileSync5(DAEMON_PID_PATH, `${pid}
11845
+ writeFileSync6(DAEMON_PID_PATH, `${pid}
11080
11846
  `);
11081
11847
  }
11082
11848
  function readPid() {
@@ -11171,7 +11937,7 @@ async function discoverPeers() {
11171
11937
  }
11172
11938
  }
11173
11939
  } catch {}
11174
- 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);
11175
11941
  for (const ip of knownPeers) {
11176
11942
  if (!peers.some((p) => p.host === ip)) {
11177
11943
  peers.push({ host: ip, port: config.port });
@@ -11182,7 +11948,7 @@ async function discoverPeers() {
11182
11948
 
11183
11949
  // src/commands/heal.ts
11184
11950
  init_paths();
11185
- 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";
11186
11952
  import { join as join8 } from "path";
11187
11953
  var DEFAULT_THRESHOLDS = {
11188
11954
  reconnect: 3,
@@ -11247,7 +12013,7 @@ function readHealConfig(path) {
11247
12013
  function writeHealConfig(config, path) {
11248
12014
  const p = path || getHealConfigPath();
11249
12015
  ensureParentDir(p);
11250
- writeFileSync6(p, `${JSON.stringify(config, null, 2)}
12016
+ writeFileSync7(p, `${JSON.stringify(config, null, 2)}
11251
12017
  `, "utf8");
11252
12018
  }
11253
12019
  function readHealState(path) {
@@ -11263,7 +12029,7 @@ function readHealState(path) {
11263
12029
  function writeHealState(state, path) {
11264
12030
  const p = path || getHealStatePath();
11265
12031
  ensureParentDir(p);
11266
- writeFileSync6(p, `${JSON.stringify(state, null, 2)}
12032
+ writeFileSync7(p, `${JSON.stringify(state, null, 2)}
11267
12033
  `, "utf8");
11268
12034
  }
11269
12035
  function evaluateHealth(probe, config, state) {
@@ -11468,7 +12234,7 @@ function executeAction(action, config) {
11468
12234
 
11469
12235
  // src/commands/heal-daemon.ts
11470
12236
  init_paths();
11471
- 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";
11472
12238
  import { join as join9 } from "path";
11473
12239
  var DAEMON_PID_PATH2 = join9(getDataDir(), "heal-daemon.pid");
11474
12240
  var SERVICE_PATH = "/etc/systemd/system/machines-heal.service";
@@ -11511,7 +12277,7 @@ function runHealOnce(config, opts = {}) {
11511
12277
  return result;
11512
12278
  }
11513
12279
  function writePid2(pid) {
11514
- writeFileSync7(DAEMON_PID_PATH2, `${pid}
12280
+ writeFileSync8(DAEMON_PID_PATH2, `${pid}
11515
12281
  `);
11516
12282
  }
11517
12283
  function readPid2() {
@@ -11602,7 +12368,7 @@ ${key}=${value}
11602
12368
  };
11603
12369
  set("RuntimeWatchdogSec", "20s");
11604
12370
  set("RebootWatchdogSec", "2min");
11605
- writeFileSync7(SYSTEM_CONF, conf);
12371
+ writeFileSync8(SYSTEM_CONF, conf);
11606
12372
  sh2("systemctl daemon-reexec");
11607
12373
  log2.push("hardware watchdog: RuntimeWatchdogSec=20s RebootWatchdogSec=2min");
11608
12374
  return log2;
@@ -11647,7 +12413,7 @@ Environment=PATH=${binDir}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sb
11647
12413
  [Install]
11648
12414
  WantedBy=multi-user.target
11649
12415
  `;
11650
- writeFileSync7(SERVICE_PATH, unit);
12416
+ writeFileSync8(SERVICE_PATH, unit);
11651
12417
  sh2("systemctl daemon-reload");
11652
12418
  sh2("systemctl enable --now machines-heal.service");
11653
12419
  log2.push(`installed + enabled ${SERVICE_PATH} (ExecStart=${exec} heal daemon)`);
@@ -11828,18 +12594,18 @@ function checkSecretPresence(secretsCommand, key) {
11828
12594
  };
11829
12595
  }
11830
12596
  function parseCommandSpec(value) {
11831
- const [command, expectedVersion] = value.split(":");
12597
+ const [command2, expectedVersion] = value.split(":");
11832
12598
  return {
11833
- command,
12599
+ command: command2,
11834
12600
  expectedVersion: expectedVersion || undefined,
11835
12601
  required: true
11836
12602
  };
11837
12603
  }
11838
12604
  function parsePackageSpec(value) {
11839
- const [name, command, expectedVersion] = value.split(":");
12605
+ const [name, command2, expectedVersion] = value.split(":");
11840
12606
  return {
11841
12607
  name,
11842
- command: command || undefined,
12608
+ command: command2 || undefined,
11843
12609
  expectedVersion: expectedVersion || undefined,
11844
12610
  required: true
11845
12611
  };
@@ -11930,17 +12696,54 @@ function renderFleetStatus(status) {
11930
12696
  ["sync runs", String(status.recentSyncRuns)]
11931
12697
  ]),
11932
12698
  "",
11933
- ...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)
11934
12723
  ].join(`
11935
12724
  `);
11936
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
+ }
11937
12740
  program2.name("machines").description("Machine fleet management CLI + MCP for developers").version(getPackageVersion()).option("-q, --quiet", "Suppress non-essential output");
11938
12741
  var manifestCommand = program2.command("manifest").description("Manage the fleet manifest");
11939
12742
  var appsCommand = program2.command("apps").description("Manage installed applications per machine");
11940
12743
  var notificationsCommand = program2.command("notifications").description("Manage fleet alert delivery channels");
11941
12744
  var eventWebhooksCommand = registerWebhookCommands(program2, { source: "machines" });
11942
12745
  eventWebhooksCommand.description("Manage shared event webhook subscriptions");
11943
- var webhookTestCommand = eventWebhooksCommand.commands.find((command) => command.name() === "test");
12746
+ var webhookTestCommand = eventWebhooksCommand.commands.find((command2) => command2.name() === "test");
11944
12747
  var webhookOptions = webhookTestCommand?.options ?? [];
11945
12748
  var webhookMessageOption = webhookOptions.find((option) => option.long === "--message");
11946
12749
  if (webhookMessageOption) {
@@ -11951,6 +12754,23 @@ eventsCommand.description("Emit, list, and replay shared events");
11951
12754
  var runtimeCommand = program2.command("runtime").description("Watch runtime conditions and emit shared events");
11952
12755
  var clipboardCommand = program2.command("clipboard").description("Real-time clipboard sync across fleet machines");
11953
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");
11954
12774
  manifestCommand.command("init").description("Create an empty fleet manifest").action(() => {
11955
12775
  console.log(manifestInit());
11956
12776
  });
@@ -12055,8 +12875,9 @@ program2.command("sync").description("Reconcile a machine against the fleet mani
12055
12875
  const result = options.apply ? runSync(options.machine, { apply: true, yes: options.yes }) : buildSyncPlan(options.machine);
12056
12876
  console.log(JSON.stringify(result, null, 2));
12057
12877
  });
12058
- 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) => {
12059
- 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 });
12060
12881
  if (options.json) {
12061
12882
  console.log(JSON.stringify(topology, null, 2));
12062
12883
  return;
@@ -12296,11 +13117,12 @@ program2.command("install-tailscale").description("Install Tailscale on a machin
12296
13117
  const result = options.apply ? runTailscaleInstall(options.machine, { apply: true, yes: options.yes }) : buildTailscaleInstallPlan(options.machine);
12297
13118
  console.log(JSON.stringify(result, null, 2));
12298
13119
  });
12299
- 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) => {
12300
13121
  const topology = discoverMachineTopology({ includeTailscale: options.tailscale !== false });
12301
13122
  const resolved = resolveMachineRoute(options.machine, { topology });
12302
- const command = resolved.ok && resolved.target ? resolved.route === "local" ? options.cmd ?? null : buildSshCommand(options.machine, options.cmd, { topology }) : null;
12303
- 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 };
12304
13126
  if (options.json) {
12305
13127
  console.log(JSON.stringify(payload, null, 2));
12306
13128
  return;
@@ -12310,7 +13132,7 @@ program2.command("route").description("Resolve the best route for a machine").re
12310
13132
  process.exitCode = 1;
12311
13133
  return;
12312
13134
  }
12313
- console.log(command ?? `${resolved.route}:${resolved.target}`);
13135
+ console.log(options.privateMetadata ? command2 ?? `${resolved.route}:${resolved.target}` : `${publicResolved.route}:${publicResolved.target ?? "unresolved"}`);
12314
13136
  });
12315
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) => {
12316
13138
  if (options.json) {
@@ -12338,7 +13160,7 @@ program2.command("screen").description("Open Screen Sharing (VNC) to a machine u
12338
13160
  for (const r of results) {
12339
13161
  if (r.ok && r.url) {
12340
13162
  if (!options.print)
12341
- execFileSync("open", [r.url], { stdio: "ignore" });
13163
+ execFileSync2("open", [r.url], { stdio: "ignore" });
12342
13164
  console.log(`${r.ok ? "\u2713" : "\u2717"} ${r.machine.padEnd(14)} ${r.url ?? r.error}`);
12343
13165
  } else {
12344
13166
  console.log(`\u2717 ${r.machine.padEnd(14)} ${r.error}`);
@@ -12361,7 +13183,7 @@ program2.command("screen").description("Open Screen Sharing (VNC) to a machine u
12361
13183
  console.log(resolved.url);
12362
13184
  return;
12363
13185
  }
12364
- execFileSync("open", [resolved.url], { stdio: "ignore" });
13186
+ execFileSync2("open", [resolved.url], { stdio: "ignore" });
12365
13187
  console.log(`Opening Screen Sharing \u2192 ${resolved.url} (route: ${resolved.route})`);
12366
13188
  });
12367
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) => {