@hasna/machines 0.0.46 → 0.0.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -418,6 +418,32 @@ to be scoped to a short-lived approval token, or pass
418
418
  `--trusted-local-mutation` to generate a process-local
419
419
  `HASNA_MACHINES_ALLOW_MUTATIONS=1` prefix for local event recording.
420
420
 
421
+ ## Fleet hostnames (`machines hosts`)
422
+
423
+ Make every fleet machine reachable by its bare name from any other machine —
424
+ `curl http://machine001:3000` works the same on every box — without depending on
425
+ Tailscale MagicDNS being configured. `machines hosts` writes a managed block into
426
+ `/etc/hosts` for each machine in the manifest, choosing the best address:
427
+
428
+ 1. `metadata.lanAddress` from the manifest, when it is on the local machine's `/24`
429
+ 2. the peer's live direct Tailscale LAN endpoint (`CurAddr`) on the local `/24`
430
+ 3. the peer's tailnet IP (`100.64.0.0/10`) — always routable, auto-routed over the
431
+ LAN when co-located
432
+
433
+ ```bash
434
+ machines hosts # dry-run plan (default)
435
+ machines hosts plan -j # JSON plan
436
+ machines hosts apply # write /etc/hosts (uses sudo when the file is root-owned)
437
+ machines hosts plan --no-warm # skip discovering LAN endpoints (faster, tailnet IPs)
438
+ ```
439
+
440
+ By default the command first runs `tailscale ping` against online peers so their
441
+ LAN endpoints become visible and same-LAN machines resolve to their `192.168.x.x`
442
+ address (true LAN-direct) instead of the tailnet IP. Off-LAN or offline peers fall
443
+ back to the tailnet IP. The local machine is skipped. The managed block is delimited
444
+ by markers, so re-running `apply` only rewrites that block and leaves the rest of
445
+ `/etc/hosts` untouched.
446
+
421
447
  ## Dashboard
422
448
 
423
449
  ```bash
@@ -992,7 +992,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
992
992
  this._exitCallback = (err) => {
993
993
  if (err.code !== "commander.executeSubCommandAsync") {
994
994
  throw err;
995
- }
995
+ } else {}
996
996
  };
997
997
  }
998
998
  return this;
package/dist/cli/index.js CHANGED
@@ -993,7 +993,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
993
993
  this._exitCallback = (err) => {
994
994
  if (err.code !== "commander.executeSubCommandAsync") {
995
995
  throw err;
996
- }
996
+ } else {}
997
997
  };
998
998
  }
999
999
  return this;
@@ -2714,7 +2714,7 @@ import {
2714
2714
  sanitizeChannelsForOutput as sanitizeChannelsForOutput2
2715
2715
  } from "@hasna/events";
2716
2716
  import { execFileSync as execFileSync2 } from "child_process";
2717
- import { existsSync as existsSync13, readFileSync as readFileSync13, rmSync as rmSync3 } from "fs";
2717
+ import { existsSync as existsSync14, readFileSync as readFileSync14, rmSync as rmSync3 } from "fs";
2718
2718
  import { join as join12, resolve as resolve4 } from "path";
2719
2719
 
2720
2720
  // node_modules/chalk/source/vendor/ansi-styles/index.js
@@ -9448,6 +9448,310 @@ function renderDomainMapping(domain) {
9448
9448
  };
9449
9449
  }
9450
9450
 
9451
+ // src/commands/hosts.ts
9452
+ import { spawnSync as spawnSync3 } from "child_process";
9453
+ import { existsSync as existsSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
9454
+ import { networkInterfaces, platform as osPlatform } from "os";
9455
+ var HOSTS_BLOCK_BEGIN = "# >>> hasna machines fleet >>>";
9456
+ var HOSTS_BLOCK_END = "# <<< hasna machines fleet <<<";
9457
+ function defaultRunner2(command) {
9458
+ const result = spawnSync3("bash", ["-c", command], { encoding: "utf8", env: process.env });
9459
+ return { stdout: result.stdout || "", exitCode: result.status ?? 1 };
9460
+ }
9461
+ function getHostsPath() {
9462
+ const override = process.env["HASNA_MACHINES_HOSTS_PATH"];
9463
+ if (override)
9464
+ return override;
9465
+ if (osPlatform() === "win32") {
9466
+ return "C:\\Windows\\System32\\drivers\\etc\\hosts";
9467
+ }
9468
+ return "/etc/hosts";
9469
+ }
9470
+ function isIpv4(value) {
9471
+ const parts = value.split(".");
9472
+ if (parts.length !== 4)
9473
+ return false;
9474
+ return parts.every((part) => {
9475
+ if (!/^\d{1,3}$/.test(part))
9476
+ return false;
9477
+ const num = Number(part);
9478
+ return num >= 0 && num <= 255;
9479
+ });
9480
+ }
9481
+ function isPrivateIpv4(value) {
9482
+ if (!isIpv4(value))
9483
+ return false;
9484
+ const [a, b] = value.split(".").map(Number);
9485
+ if (a === 10)
9486
+ return true;
9487
+ if (a === 192 && b === 168)
9488
+ return true;
9489
+ if (a === 172 && b >= 16 && b <= 31)
9490
+ return true;
9491
+ return false;
9492
+ }
9493
+ function isTailnetIpv4(value) {
9494
+ if (!isIpv4(value))
9495
+ return false;
9496
+ const [a, b] = value.split(".").map(Number);
9497
+ return a === 100 && b >= 64 && b <= 127;
9498
+ }
9499
+ function subnet24(value) {
9500
+ if (!isIpv4(value))
9501
+ return null;
9502
+ return value.split(".").slice(0, 3).join(".");
9503
+ }
9504
+ function ipFromEndpoint(endpoint) {
9505
+ if (!endpoint)
9506
+ return null;
9507
+ const host = endpoint.replace(/:\d+$/, "").trim();
9508
+ return isIpv4(host) ? host : null;
9509
+ }
9510
+ function localPrivateSubnets() {
9511
+ const subnets = new Set;
9512
+ const interfaces = networkInterfaces();
9513
+ for (const addrs of Object.values(interfaces)) {
9514
+ for (const addr of addrs ?? []) {
9515
+ if (addr.family !== "IPv4" || addr.internal)
9516
+ continue;
9517
+ if (!isPrivateIpv4(addr.address))
9518
+ continue;
9519
+ const subnet = subnet24(addr.address);
9520
+ if (subnet)
9521
+ subnets.add(subnet);
9522
+ }
9523
+ }
9524
+ return [...subnets];
9525
+ }
9526
+ function shortName(value) {
9527
+ if (!value)
9528
+ return null;
9529
+ const trimmed = value.replace(/\.$/, "").trim();
9530
+ if (!trimmed)
9531
+ return null;
9532
+ return trimmed.split(".")[0]?.toLowerCase() || null;
9533
+ }
9534
+ function machineNames(machine) {
9535
+ const names = [];
9536
+ for (const candidate of [machine.id, machine.hostname, shortName(machine.tailscaleName)]) {
9537
+ const name = shortName(candidate);
9538
+ if (name && !names.includes(name))
9539
+ names.push(name);
9540
+ }
9541
+ return names;
9542
+ }
9543
+ function peerKeysForMachine(machine) {
9544
+ return [
9545
+ machine.id,
9546
+ machine.hostname,
9547
+ shortName(machine.tailscaleName),
9548
+ machine.tailscaleName,
9549
+ machine.sshAddress?.split("@").pop()
9550
+ ].filter((value) => Boolean(value));
9551
+ }
9552
+ function indexPeers(tailscale) {
9553
+ const peers = new Map;
9554
+ if (!tailscale)
9555
+ return peers;
9556
+ const add = (peer) => {
9557
+ if (!peer)
9558
+ return;
9559
+ const id = shortName(peer.HostName) || shortName(peer.DNSName);
9560
+ if (id)
9561
+ peers.set(id, peer);
9562
+ };
9563
+ add(tailscale.Self);
9564
+ for (const peer of Object.values(tailscale.Peer ?? {}))
9565
+ add(peer);
9566
+ return peers;
9567
+ }
9568
+ function findPeer(machine, peers) {
9569
+ for (const key of peerKeysForMachine(machine)) {
9570
+ const peer = peers.get(shortName(key) || key);
9571
+ if (peer)
9572
+ return peer;
9573
+ }
9574
+ return null;
9575
+ }
9576
+ function tailnetIp(peer) {
9577
+ if (!peer)
9578
+ return null;
9579
+ for (const ip of peer.TailscaleIPs ?? []) {
9580
+ if (isTailnetIpv4(ip))
9581
+ return ip;
9582
+ }
9583
+ return null;
9584
+ }
9585
+ function metadataString2(metadata, key) {
9586
+ if (!metadata)
9587
+ return null;
9588
+ const value = metadata[key];
9589
+ return typeof value === "string" && value.trim() ? value.trim() : null;
9590
+ }
9591
+ function buildFleetHostEntries(input) {
9592
+ const { manifest, tailscale, localSubnets, localMachineId } = input;
9593
+ const peers = indexPeers(tailscale);
9594
+ const subnetSet = new Set(localSubnets);
9595
+ const entries = [];
9596
+ const unresolved = [];
9597
+ const warnings = [];
9598
+ for (const machine of manifest.machines) {
9599
+ if (localMachineId && machine.id === localMachineId)
9600
+ continue;
9601
+ const names = machineNames(machine);
9602
+ if (names.length === 0)
9603
+ continue;
9604
+ const peer = findPeer(machine, peers);
9605
+ const lanAddress = metadataString2(machine.metadata, "lanAddress");
9606
+ const explicitIp = metadataString2(machine.metadata, "ipAddress");
9607
+ const curLanIp = ipFromEndpoint(peer?.CurAddr);
9608
+ const tnet = tailnetIp(peer);
9609
+ let ip = null;
9610
+ let source = null;
9611
+ if (lanAddress && isPrivateIpv4(lanAddress) && subnetSet.has(subnet24(lanAddress) ?? "")) {
9612
+ ip = lanAddress;
9613
+ source = "manifest_lan";
9614
+ } else if (curLanIp && isPrivateIpv4(curLanIp) && subnetSet.has(subnet24(curLanIp) ?? "")) {
9615
+ ip = curLanIp;
9616
+ source = "tailscale_lan";
9617
+ } else if (tnet) {
9618
+ ip = tnet;
9619
+ source = "tailscale";
9620
+ } else if (explicitIp && isIpv4(explicitIp)) {
9621
+ ip = explicitIp;
9622
+ source = "manifest_ip";
9623
+ }
9624
+ if (!ip || !source) {
9625
+ unresolved.push(machine.id);
9626
+ continue;
9627
+ }
9628
+ entries.push({ id: machine.id, ip, names, source });
9629
+ }
9630
+ entries.sort((left, right) => left.id.localeCompare(right.id));
9631
+ return { entries, unresolved, warnings };
9632
+ }
9633
+ function renderHostsBlock(entries) {
9634
+ const lines = [
9635
+ HOSTS_BLOCK_BEGIN,
9636
+ "# Managed by `machines hosts apply`. Do not edit between these markers.",
9637
+ ...entries.map((entry) => `${entry.ip} ${entry.names.join(" ")}`),
9638
+ HOSTS_BLOCK_END
9639
+ ];
9640
+ return lines.join(`
9641
+ `);
9642
+ }
9643
+ function mergeHostsContent(existing, block) {
9644
+ const beginIndex = existing.indexOf(HOSTS_BLOCK_BEGIN);
9645
+ const endIndex = existing.indexOf(HOSTS_BLOCK_END);
9646
+ if (beginIndex !== -1 && endIndex !== -1 && endIndex > beginIndex) {
9647
+ const before = existing.slice(0, beginIndex).replace(/\n+$/, "");
9648
+ const after = existing.slice(endIndex + HOSTS_BLOCK_END.length).replace(/^\n+/, "");
9649
+ const head = before ? `${before}
9650
+ ` : "";
9651
+ const tail = after ? `
9652
+ ${after}` : `
9653
+ `;
9654
+ return `${head}${block}${tail}`;
9655
+ }
9656
+ const base = existing.replace(/\n+$/, "");
9657
+ const prefix = base ? `${base}
9658
+
9659
+ ` : "";
9660
+ return `${prefix}${block}
9661
+ `;
9662
+ }
9663
+ function loadTailscale(runner, warnings) {
9664
+ if (runner("command -v tailscale >/dev/null 2>&1").exitCode !== 0) {
9665
+ warnings.push("tailscale_not_available");
9666
+ return null;
9667
+ }
9668
+ const result = runner("tailscale status --json");
9669
+ if (result.exitCode !== 0) {
9670
+ warnings.push("tailscale_status_failed");
9671
+ return null;
9672
+ }
9673
+ try {
9674
+ return JSON.parse(result.stdout);
9675
+ } catch {
9676
+ warnings.push("tailscale_status_invalid_json");
9677
+ return null;
9678
+ }
9679
+ }
9680
+ function collectPingTargets(tailscale, localSubnets) {
9681
+ if (!tailscale)
9682
+ return [];
9683
+ const subnetSet = new Set(localSubnets);
9684
+ const targets = [];
9685
+ for (const peer of Object.values(tailscale.Peer ?? {})) {
9686
+ if (!peer.Online)
9687
+ continue;
9688
+ const cur = ipFromEndpoint(peer.CurAddr);
9689
+ if (cur && isPrivateIpv4(cur) && subnetSet.has(subnet24(cur) ?? ""))
9690
+ continue;
9691
+ const tnet = tailnetIp(peer);
9692
+ if (tnet && !targets.includes(tnet))
9693
+ targets.push(tnet);
9694
+ }
9695
+ return targets;
9696
+ }
9697
+ function warmDirectPaths(runner, targets, timeoutSeconds = 2) {
9698
+ for (const target of targets) {
9699
+ runner(`tailscale ping --c 1 --timeout ${timeoutSeconds}s ${target} >/dev/null 2>&1 || true`);
9700
+ }
9701
+ }
9702
+ function resolveLocalMachineId(tailscale, explicit) {
9703
+ if (explicit)
9704
+ return explicit;
9705
+ return shortName(tailscale?.Self?.HostName) || shortName(process.env["HOSTNAME"]) || null;
9706
+ }
9707
+ function planFleetHosts(options = {}) {
9708
+ const runner = options.runner ?? defaultRunner2;
9709
+ const warnings = [];
9710
+ let tailscale = loadTailscale(runner, warnings);
9711
+ const manifest = readManifest();
9712
+ const localSubnets = options.localSubnets ?? localPrivateSubnets();
9713
+ if (options.warm !== false && tailscale && localSubnets.length > 0) {
9714
+ const targets = collectPingTargets(tailscale, localSubnets);
9715
+ if (targets.length > 0) {
9716
+ warmDirectPaths(runner, targets, options.warmTimeoutSeconds);
9717
+ tailscale = loadTailscale(runner, warnings) ?? tailscale;
9718
+ }
9719
+ }
9720
+ const localMachineId = resolveLocalMachineId(tailscale, options.localMachineId);
9721
+ const built = buildFleetHostEntries({ manifest, tailscale, localSubnets, localMachineId });
9722
+ const block = renderHostsBlock(built.entries);
9723
+ return {
9724
+ hostsPath: getHostsPath(),
9725
+ entries: built.entries,
9726
+ unresolved: built.unresolved,
9727
+ warnings: [...warnings, ...built.warnings],
9728
+ block,
9729
+ localSubnets
9730
+ };
9731
+ }
9732
+ function applyFleetHosts(options = {}) {
9733
+ const plan = planFleetHosts(options);
9734
+ const hostsPath = plan.hostsPath;
9735
+ const existing = existsSync7(hostsPath) ? readFileSync5(hostsPath, "utf8") : "";
9736
+ const merged = mergeHostsContent(existing, plan.block);
9737
+ let viaSudo = false;
9738
+ try {
9739
+ writeFileSync3(hostsPath, merged, "utf8");
9740
+ } catch (error) {
9741
+ const code = error?.code;
9742
+ if (code !== "EACCES" && code !== "EPERM")
9743
+ throw error;
9744
+ const runner = options.runner ?? defaultRunner2;
9745
+ const encoded = Buffer.from(merged, "utf8").toString("base64");
9746
+ const result = runner(`printf %s '${encoded}' | base64 -d | sudo tee ${hostsPath} >/dev/null`);
9747
+ if (result.exitCode !== 0) {
9748
+ throw new Error(`Failed to write ${hostsPath} (need sudo). Re-run with elevated privileges.`);
9749
+ }
9750
+ viaSudo = true;
9751
+ }
9752
+ return { ...plan, written: true, viaSudo };
9753
+ }
9754
+
9451
9755
  // src/commands/diff.ts
9452
9756
  function packageNames(machine) {
9453
9757
  return (machine.packages || []).map((pkg) => pkg.name).sort();
@@ -9801,7 +10105,7 @@ function runTailscaleInstallPlan(plan, options = {}, runner = runMachineCommand)
9801
10105
  }
9802
10106
 
9803
10107
  // src/commands/notifications.ts
9804
- import { accessSync, constants, existsSync as existsSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
10108
+ import { accessSync, constants, existsSync as existsSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
9805
10109
  import { delimiter, isAbsolute, join as join7 } from "path";
9806
10110
  init_paths();
9807
10111
  var notificationChannelSchema = exports_external.object({
@@ -9986,10 +10290,10 @@ function getDefaultNotificationConfig() {
9986
10290
  };
9987
10291
  }
9988
10292
  function readNotificationConfig(path = getNotificationsPath()) {
9989
- if (!existsSync7(path)) {
10293
+ if (!existsSync8(path)) {
9990
10294
  return getDefaultNotificationConfig();
9991
10295
  }
9992
- return notificationConfigSchema.parse(JSON.parse(readFileSync5(path, "utf8")));
10296
+ return notificationConfigSchema.parse(JSON.parse(readFileSync6(path, "utf8")));
9993
10297
  }
9994
10298
  function writeNotificationConfig(config, path = getNotificationsPath()) {
9995
10299
  ensureParentDir(path);
@@ -9998,7 +10302,7 @@ function writeNotificationConfig(config, path = getNotificationsPath()) {
9998
10302
  updatedAt: new Date().toISOString(),
9999
10303
  channels: sortChannels(config.channels)
10000
10304
  };
10001
- writeFileSync3(path, `${JSON.stringify(nextConfig, null, 2)}
10305
+ writeFileSync4(path, `${JSON.stringify(nextConfig, null, 2)}
10002
10306
  `, "utf8");
10003
10307
  return nextConfig;
10004
10308
  }
@@ -10087,7 +10391,7 @@ async function testNotificationChannel(channelId, event = "manual.test", message
10087
10391
 
10088
10392
  // src/commands/ports.ts
10089
10393
  init_db();
10090
- import { spawnSync as spawnSync3 } from "child_process";
10394
+ import { spawnSync as spawnSync4 } from "child_process";
10091
10395
  function parseSsOutput(output) {
10092
10396
  return output.trim().split(`
10093
10397
  `).map((line) => line.trim()).filter(Boolean).map((line) => {
@@ -10129,7 +10433,7 @@ function listPorts(machineId) {
10129
10433
  const isLocal = targetMachineId === getLocalMachineId();
10130
10434
  const localCommand = "if command -v ss >/dev/null 2>&1; then ss -ltnpH; else lsof -nP -iTCP -sTCP:LISTEN; fi";
10131
10435
  const command = isLocal ? localCommand : buildSshCommand(targetMachineId, localCommand);
10132
- const result = spawnSync3("bash", ["-lc", command], { encoding: "utf8" });
10436
+ const result = spawnSync4("bash", ["-lc", command], { encoding: "utf8" });
10133
10437
  if (result.status !== 0) {
10134
10438
  throw new Error(result.stderr || `Failed to list ports for ${targetMachineId}`);
10135
10439
  }
@@ -10141,7 +10445,7 @@ function listPorts(machineId) {
10141
10445
  }
10142
10446
 
10143
10447
  // src/commands/runtime.ts
10144
- import { spawnSync as spawnSync4 } from "child_process";
10448
+ import { spawnSync as spawnSync5 } from "child_process";
10145
10449
  import { setTimeout as sleep } from "timers/promises";
10146
10450
  import { EventsClient } from "@hasna/events";
10147
10451
  function shellQuote5(value) {
@@ -10186,7 +10490,7 @@ function buildTmuxPaneDiedHookPlan(options = {}) {
10186
10490
  }
10187
10491
  function probeTmuxPane(target, tmuxCommand = process.env["HASNA_MACHINES_TMUX_BIN"] || "tmux") {
10188
10492
  const checkedAt = new Date().toISOString();
10189
- const result = spawnSync4(tmuxCommand, ["display-message", "-p", "-t", target, "#{pane_id}"], {
10493
+ const result = spawnSync5(tmuxCommand, ["display-message", "-p", "-t", target, "#{pane_id}"], {
10190
10494
  encoding: "utf8",
10191
10495
  timeout: 5000
10192
10496
  });
@@ -10278,7 +10582,7 @@ function shellQuote6(value) {
10278
10582
  function shellCommand2(command) {
10279
10583
  return command.map(shellQuote6).join(" ");
10280
10584
  }
10281
- function metadataString2(metadata, keys) {
10585
+ function metadataString3(metadata, keys) {
10282
10586
  if (!metadata)
10283
10587
  return null;
10284
10588
  for (const key of keys) {
@@ -10328,8 +10632,8 @@ function resolveScreenCredentials(machineId, options = {}) {
10328
10632
  const screen = resolveScreenTarget(machineId, { ...options, topology });
10329
10633
  const entry = topology.machines.find((machine) => machine.machine_id === screen.machineId);
10330
10634
  const metadata = entry?.metadata;
10331
- const metadataUser = metadataString2(metadata, ["screenUser", "screen_user", "user", "username"]);
10332
- const metadataPasswordSecret = metadataString2(metadata, [
10635
+ const metadataUser = metadataString3(metadata, ["screenUser", "screen_user", "user", "username"]);
10636
+ const metadataPasswordSecret = metadataString3(metadata, [
10333
10637
  "screenPasswordSecret",
10334
10638
  "screen_password_secret",
10335
10639
  "screenVncPasswordSecret",
@@ -10386,7 +10690,7 @@ function buildScreenEnableCommand(machineId, options = {}) {
10386
10690
  }
10387
10691
 
10388
10692
  // src/commands/sync.ts
10389
- import { existsSync as existsSync8, lstatSync, readFileSync as readFileSync6, symlinkSync, copyFileSync } from "fs";
10693
+ import { existsSync as existsSync9, lstatSync, readFileSync as readFileSync7, symlinkSync, copyFileSync } from "fs";
10390
10694
  import { homedir as homedir5 } from "os";
10391
10695
  init_paths();
10392
10696
  init_db();
@@ -10441,15 +10745,15 @@ function detectFileActions(machine) {
10441
10745
  throw new Error(`Remote file sync planning is not supported for ${machine.id}; refusing to inspect or apply local paths as remote state.`);
10442
10746
  }
10443
10747
  return (machine.files || []).map((file, index) => {
10444
- const sourceExists = existsSync8(file.source);
10445
- const targetExists = existsSync8(file.target);
10748
+ const sourceExists = existsSync9(file.source);
10749
+ const targetExists = existsSync9(file.target);
10446
10750
  let status = "missing";
10447
10751
  if (sourceExists && targetExists) {
10448
10752
  if (file.mode === "symlink") {
10449
10753
  status = lstatSync(file.target).isSymbolicLink() ? "ok" : "drifted";
10450
10754
  } else {
10451
- const source = readFileSync6(file.source, "utf8");
10452
- const target = readFileSync6(file.target, "utf8");
10755
+ const source = readFileSync7(file.source, "utf8");
10756
+ const target = readFileSync7(file.target, "utf8");
10453
10757
  status = source === target ? "ok" : "drifted";
10454
10758
  }
10455
10759
  }
@@ -10802,7 +11106,7 @@ function parseKeyValue(stdout) {
10802
11106
  }
10803
11107
  return result;
10804
11108
  }
10805
- function defaultRunner2(machineId, command) {
11109
+ function defaultRunner3(machineId, command) {
10806
11110
  return runMachineCommand(machineId, command);
10807
11111
  }
10808
11112
  function inspectCommand(machineId, spec, runner) {
@@ -10956,7 +11260,7 @@ function workspaceCheck(machineId, spec, runner) {
10956
11260
  }
10957
11261
  function checkMachineCompatibility(options = {}) {
10958
11262
  const machineId = options.machineId ?? getLocalMachineId();
10959
- const runner = options.runner ?? defaultRunner2;
11263
+ const runner = options.runner ?? defaultRunner3;
10960
11264
  const commands = options.commands ?? DEFAULT_COMMANDS;
10961
11265
  const packages = options.packages ?? defaultPackages();
10962
11266
  const workspaces = options.workspaces ?? [];
@@ -11176,9 +11480,9 @@ function runDoctor(machineId, options = {}) {
11176
11480
 
11177
11481
  // src/commands/daemon.ts
11178
11482
  import { execFileSync } from "child_process";
11179
- import { chmodSync, existsSync as existsSync9, readFileSync as readFileSync7, statSync, mkdirSync as mkdirSync2, writeFileSync as writeFileSync4 } from "fs";
11483
+ import { chmodSync, existsSync as existsSync10, readFileSync as readFileSync8, statSync, mkdirSync as mkdirSync2, writeFileSync as writeFileSync5 } from "fs";
11180
11484
  import { delimiter as delimiter2, dirname as dirname4 } from "path";
11181
- import { platform as osPlatform } from "os";
11485
+ import { platform as osPlatform2 } from "os";
11182
11486
  var DEFAULT_SERVICE_NAME = "machines-agent";
11183
11487
  var DEFAULT_EXECUTABLE = "/usr/local/bin/machines-agent";
11184
11488
  var DEFAULT_INTERVAL_MS = 30000;
@@ -11235,7 +11539,7 @@ function runDaemonServicePlan(plan, options = {}) {
11235
11539
  };
11236
11540
  }
11237
11541
  mkdirSync2(dirname4(path), { recursive: true });
11238
- writeFileSync4(path, content, "utf8");
11542
+ writeFileSync5(path, content, "utf8");
11239
11543
  chmodSync(path, Number.parseInt(file.mode, 8));
11240
11544
  filesWritten.push(path);
11241
11545
  }
@@ -11330,7 +11634,7 @@ function normalizeServiceName(value, warnings) {
11330
11634
  return DEFAULT_SERVICE_NAME;
11331
11635
  }
11332
11636
  function normalizePlatform3(value, warnings) {
11333
- const raw = value ?? osPlatform();
11637
+ const raw = value ?? osPlatform2();
11334
11638
  if (raw === "darwin" || raw === "macos")
11335
11639
  return "macos";
11336
11640
  if (raw === "linux")
@@ -11591,7 +11895,7 @@ function bunRuntimeCandidates(executable) {
11591
11895
  }
11592
11896
  function isBunShebangScript(executable) {
11593
11897
  try {
11594
- const content = readFileSync7(executable, "utf8").slice(0, 256);
11898
+ const content = readFileSync8(executable, "utf8").slice(0, 256);
11595
11899
  const firstLine2 = content.split(/\r?\n/, 1)[0] ?? "";
11596
11900
  return /^#!.*\bbun\b/.test(firstLine2);
11597
11901
  } catch {
@@ -11599,7 +11903,7 @@ function isBunShebangScript(executable) {
11599
11903
  }
11600
11904
  }
11601
11905
  function isExecutableFile(path) {
11602
- if (!existsSync9(path))
11906
+ if (!existsSync10(path))
11603
11907
  return false;
11604
11908
  try {
11605
11909
  const stats = statSync(path);
@@ -12222,7 +12526,7 @@ function runSelfTest() {
12222
12526
  // src/commands/clipboard.ts
12223
12527
  init_paths();
12224
12528
  import { createHash as createHash2 } from "crypto";
12225
- import { existsSync as existsSync10, readFileSync as readFileSync8, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
12529
+ import { existsSync as existsSync11, readFileSync as readFileSync9, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
12226
12530
  import { join as join8 } from "path";
12227
12531
  var DEFAULT_CONFIG = {
12228
12532
  version: 1,
@@ -12253,25 +12557,25 @@ function getDefaultConfig() {
12253
12557
  }
12254
12558
  function readConfig(configPath) {
12255
12559
  const path = resolveConfigPath(configPath);
12256
- if (!existsSync10(path)) {
12560
+ if (!existsSync11(path)) {
12257
12561
  return getDefaultConfig();
12258
12562
  }
12259
- const parsed = JSON.parse(readFileSync8(path, "utf8"));
12563
+ const parsed = JSON.parse(readFileSync9(path, "utf8"));
12260
12564
  return { ...getDefaultConfig(), ...parsed };
12261
12565
  }
12262
12566
  function writeConfig(config, configPath) {
12263
12567
  const path = resolveConfigPath(configPath);
12264
12568
  ensureParentDir(path);
12265
- writeFileSync5(path, `${JSON.stringify(config, null, 2)}
12569
+ writeFileSync6(path, `${JSON.stringify(config, null, 2)}
12266
12570
  `, "utf8");
12267
12571
  }
12268
12572
  function readHistory(historyPath) {
12269
12573
  const path = resolveHistoryPath(historyPath);
12270
- if (!existsSync10(path)) {
12574
+ if (!existsSync11(path)) {
12271
12575
  return [];
12272
12576
  }
12273
12577
  try {
12274
- return JSON.parse(readFileSync8(path, "utf8"));
12578
+ return JSON.parse(readFileSync9(path, "utf8"));
12275
12579
  } catch {
12276
12580
  return [];
12277
12581
  }
@@ -12279,7 +12583,7 @@ function readHistory(historyPath) {
12279
12583
  function writeHistory(entries, historyPath) {
12280
12584
  const path = resolveHistoryPath(historyPath);
12281
12585
  ensureParentDir(path);
12282
- writeFileSync5(path, `${JSON.stringify(entries, null, 2)}
12586
+ writeFileSync6(path, `${JSON.stringify(entries, null, 2)}
12283
12587
  `, "utf8");
12284
12588
  }
12285
12589
  function computeHash(content) {
@@ -12300,12 +12604,12 @@ function sanitizeClipboardForRead(content, maxSizeBytes, skipPatterns) {
12300
12604
  }
12301
12605
  function getOrCreateClipboardKey() {
12302
12606
  const keyPath = getClipboardKeyPath();
12303
- if (existsSync10(keyPath)) {
12304
- return readFileSync8(keyPath, "utf8").trim();
12607
+ if (existsSync11(keyPath)) {
12608
+ return readFileSync9(keyPath, "utf8").trim();
12305
12609
  }
12306
12610
  const key = createHash2("sha256").update(crypto.randomUUID()).digest("hex").slice(0, 32);
12307
12611
  ensureParentDir(keyPath);
12308
- writeFileSync5(keyPath, `${key}
12612
+ writeFileSync6(keyPath, `${key}
12309
12613
  `, "utf8");
12310
12614
  return key;
12311
12615
  }
@@ -12340,7 +12644,7 @@ function addClipboardEntry(entry, historyPath) {
12340
12644
  }
12341
12645
  function clearClipboardHistory(historyPath) {
12342
12646
  const path = resolveHistoryPath(historyPath);
12343
- if (existsSync10(path)) {
12647
+ if (existsSync11(path)) {
12344
12648
  rmSync2(path);
12345
12649
  }
12346
12650
  }
@@ -12356,7 +12660,7 @@ function getClipboardStatus(historyPath) {
12356
12660
 
12357
12661
  // src/commands/clipboard-daemon.ts
12358
12662
  init_paths();
12359
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "fs";
12663
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync7 } from "fs";
12360
12664
  import { join as join9 } from "path";
12361
12665
  import { createHash as createHash4 } from "crypto";
12362
12666
 
@@ -12364,7 +12668,7 @@ import { createHash as createHash4 } from "crypto";
12364
12668
  init_paths();
12365
12669
  import { createServer } from "http";
12366
12670
  import { createHash as createHash3 } from "crypto";
12367
- import { readFileSync as readFileSync9 } from "fs";
12671
+ import { readFileSync as readFileSync10 } from "fs";
12368
12672
  function readLocalClipboardSync() {
12369
12673
  const platform5 = process.platform;
12370
12674
  if (platform5 === "darwin") {
@@ -12410,7 +12714,7 @@ function hasCommand3(binary) {
12410
12714
  function loadSharedSecret() {
12411
12715
  const keyPath = getClipboardKeyPath();
12412
12716
  try {
12413
- return readFileSync9(keyPath, "utf8").trim();
12717
+ return readFileSync10(keyPath, "utf8").trim();
12414
12718
  } catch {
12415
12719
  return "";
12416
12720
  }
@@ -12577,18 +12881,18 @@ function computeHash2(content) {
12577
12881
  }
12578
12882
  function loadSharedSecret2() {
12579
12883
  try {
12580
- return readFileSync10(getClipboardKeyPath(), "utf8").trim();
12884
+ return readFileSync11(getClipboardKeyPath(), "utf8").trim();
12581
12885
  } catch {
12582
12886
  return "";
12583
12887
  }
12584
12888
  }
12585
12889
  function writePid(pid) {
12586
- writeFileSync6(DAEMON_PID_PATH, `${pid}
12890
+ writeFileSync7(DAEMON_PID_PATH, `${pid}
12587
12891
  `);
12588
12892
  }
12589
12893
  function readPid() {
12590
12894
  try {
12591
- const pid = Number.parseInt(readFileSync10(DAEMON_PID_PATH, "utf8").trim());
12895
+ const pid = Number.parseInt(readFileSync11(DAEMON_PID_PATH, "utf8").trim());
12592
12896
  return Number.isFinite(pid) ? pid : null;
12593
12897
  } catch {
12594
12898
  return null;
@@ -12689,7 +12993,7 @@ async function discoverPeers() {
12689
12993
 
12690
12994
  // src/commands/heal.ts
12691
12995
  init_paths();
12692
- import { existsSync as existsSync11, readFileSync as readFileSync11, writeFileSync as writeFileSync7 } from "fs";
12996
+ import { existsSync as existsSync12, readFileSync as readFileSync12, writeFileSync as writeFileSync8 } from "fs";
12693
12997
  import { join as join10 } from "path";
12694
12998
  var DEFAULT_THRESHOLDS = {
12695
12999
  reconnect: 3,
@@ -12741,9 +13045,9 @@ function getHealStatePath() {
12741
13045
  }
12742
13046
  function readHealConfig(path) {
12743
13047
  const p = path || getHealConfigPath();
12744
- if (!existsSync11(p))
13048
+ if (!existsSync12(p))
12745
13049
  return { ...DEFAULT_HEAL_CONFIG, thresholds: { ...DEFAULT_THRESHOLDS } };
12746
- const parsed = JSON.parse(readFileSync11(p, "utf8"));
13050
+ const parsed = JSON.parse(readFileSync12(p, "utf8"));
12747
13051
  return {
12748
13052
  ...DEFAULT_HEAL_CONFIG,
12749
13053
  ...parsed,
@@ -12754,15 +13058,15 @@ function readHealConfig(path) {
12754
13058
  function writeHealConfig(config, path) {
12755
13059
  const p = path || getHealConfigPath();
12756
13060
  ensureParentDir(p);
12757
- writeFileSync7(p, `${JSON.stringify(config, null, 2)}
13061
+ writeFileSync8(p, `${JSON.stringify(config, null, 2)}
12758
13062
  `, "utf8");
12759
13063
  }
12760
13064
  function readHealState(path) {
12761
13065
  const p = path || getHealStatePath();
12762
- if (!existsSync11(p))
13066
+ if (!existsSync12(p))
12763
13067
  return defaultHealState();
12764
13068
  try {
12765
- return { ...defaultHealState(), ...JSON.parse(readFileSync11(p, "utf8")) };
13069
+ return { ...defaultHealState(), ...JSON.parse(readFileSync12(p, "utf8")) };
12766
13070
  } catch {
12767
13071
  return defaultHealState();
12768
13072
  }
@@ -12770,7 +13074,7 @@ function readHealState(path) {
12770
13074
  function writeHealState(state, path) {
12771
13075
  const p = path || getHealStatePath();
12772
13076
  ensureParentDir(p);
12773
- writeFileSync7(p, `${JSON.stringify(state, null, 2)}
13077
+ writeFileSync8(p, `${JSON.stringify(state, null, 2)}
12774
13078
  `, "utf8");
12775
13079
  }
12776
13080
  function evaluateHealth(probe, config, state) {
@@ -12889,7 +13193,7 @@ function sh(cmd, timeoutMs = 8000) {
12889
13193
  }
12890
13194
  function getCurrentBootId() {
12891
13195
  try {
12892
- return readFileSync11("/proc/sys/kernel/random/boot_id", "utf8").trim();
13196
+ return readFileSync12("/proc/sys/kernel/random/boot_id", "utf8").trim();
12893
13197
  } catch {
12894
13198
  return "";
12895
13199
  }
@@ -12975,7 +13279,7 @@ function executeAction(action, config) {
12975
13279
 
12976
13280
  // src/commands/heal-daemon.ts
12977
13281
  init_paths();
12978
- import { existsSync as existsSync12, readFileSync as readFileSync12, writeFileSync as writeFileSync8 } from "fs";
13282
+ import { existsSync as existsSync13, readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "fs";
12979
13283
  import { join as join11 } from "path";
12980
13284
  var DAEMON_PID_PATH2 = join11(getDataDir(), "heal-daemon.pid");
12981
13285
  var SERVICE_PATH = "/etc/systemd/system/machines-heal.service";
@@ -13018,12 +13322,12 @@ function runHealOnce(config, opts = {}) {
13018
13322
  return result;
13019
13323
  }
13020
13324
  function writePid2(pid) {
13021
- writeFileSync8(DAEMON_PID_PATH2, `${pid}
13325
+ writeFileSync9(DAEMON_PID_PATH2, `${pid}
13022
13326
  `);
13023
13327
  }
13024
13328
  function readPid2() {
13025
13329
  try {
13026
- const pid = Number.parseInt(readFileSync12(DAEMON_PID_PATH2, "utf8").trim());
13330
+ const pid = Number.parseInt(readFileSync13(DAEMON_PID_PATH2, "utf8").trim());
13027
13331
  return Number.isFinite(pid) ? pid : null;
13028
13332
  } catch {
13029
13333
  return null;
@@ -13095,9 +13399,9 @@ function applyDeterminism(config) {
13095
13399
  }
13096
13400
  function enableHardwareWatchdog() {
13097
13401
  const log2 = [];
13098
- if (!existsSync12(SYSTEM_CONF))
13402
+ if (!existsSync13(SYSTEM_CONF))
13099
13403
  return ["/etc/systemd/system.conf not found; skipping hardware watchdog"];
13100
- let conf = readFileSync12(SYSTEM_CONF, "utf8");
13404
+ let conf = readFileSync13(SYSTEM_CONF, "utf8");
13101
13405
  const set = (key, value) => {
13102
13406
  const re = new RegExp(`^#?\\s*${key}=.*$`, "m");
13103
13407
  if (re.test(conf))
@@ -13109,7 +13413,7 @@ ${key}=${value}
13109
13413
  };
13110
13414
  set("RuntimeWatchdogSec", "20s");
13111
13415
  set("RebootWatchdogSec", "2min");
13112
- writeFileSync8(SYSTEM_CONF, conf);
13416
+ writeFileSync9(SYSTEM_CONF, conf);
13113
13417
  sh2("systemctl daemon-reexec");
13114
13418
  log2.push("hardware watchdog: RuntimeWatchdogSec=20s RebootWatchdogSec=2min");
13115
13419
  return log2;
@@ -13127,7 +13431,7 @@ function binPath() {
13127
13431
  candidates.push(`${home}/.bun/bin/machines`);
13128
13432
  candidates.push("/root/.bun/bin/machines", "/usr/local/bin/machines");
13129
13433
  for (const c of candidates) {
13130
- if (c && existsSync12(c))
13434
+ if (c && existsSync13(c))
13131
13435
  return c;
13132
13436
  }
13133
13437
  return "machines";
@@ -13154,7 +13458,7 @@ Environment=PATH=${binDir}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sb
13154
13458
  [Install]
13155
13459
  WantedBy=multi-user.target
13156
13460
  `;
13157
- writeFileSync8(SERVICE_PATH, unit);
13461
+ writeFileSync9(SERVICE_PATH, unit);
13158
13462
  sh2("systemctl daemon-reload");
13159
13463
  sh2("systemctl enable --now machines-heal.service");
13160
13464
  log2.push(`installed + enabled ${SERVICE_PATH} (ExecStart=${exec} heal daemon)`);
@@ -13163,7 +13467,7 @@ WantedBy=multi-user.target
13163
13467
  function uninstallHealService() {
13164
13468
  const log2 = [];
13165
13469
  sh2("systemctl disable --now machines-heal.service 2>/dev/null || true");
13166
- if (existsSync12(SERVICE_PATH)) {
13470
+ if (existsSync13(SERVICE_PATH)) {
13167
13471
  sh2(`rm -f ${SERVICE_PATH}`);
13168
13472
  sh2("systemctl daemon-reload");
13169
13473
  log2.push(`removed ${SERVICE_PATH}`);
@@ -13174,7 +13478,7 @@ function uninstallHealService() {
13174
13478
  }
13175
13479
  function healServiceStatus() {
13176
13480
  return {
13177
- installed: existsSync12(SERVICE_PATH),
13481
+ installed: existsSync13(SERVICE_PATH),
13178
13482
  active: sh2("systemctl is-active machines-heal.service").out === "active",
13179
13483
  enabled: sh2("systemctl is-enabled machines-heal.service 2>/dev/null").out === "enabled"
13180
13484
  };
@@ -13538,9 +13842,9 @@ function withEventStoreScope2(args) {
13538
13842
  return { event_store_dir: eventStoreDir2(), ...args };
13539
13843
  }
13540
13844
  function readJsonArrayFile(path) {
13541
- if (!existsSync13(path))
13845
+ if (!existsSync14(path))
13542
13846
  return [];
13543
- const raw = readFileSync13(path, "utf8").trim();
13847
+ const raw = readFileSync14(path, "utf8").trim();
13544
13848
  if (!raw)
13545
13849
  return [];
13546
13850
  const parsed = JSON.parse(raw);
@@ -13831,7 +14135,7 @@ manifestCommand.command("add").description("Add or replace a machine in the flee
13831
14135
  console.error("error: --from-stdin requires piped input");
13832
14136
  process.exit(1);
13833
14137
  }
13834
- const input = readFileSync13(0, "utf8");
14138
+ const input = readFileSync14(0, "utf8");
13835
14139
  const machine2 = JSON.parse(input);
13836
14140
  requireCliMutation("manifest_add", typeof options["approvalToken"] === "string" ? options["approvalToken"] : undefined, { machineId: machine2.id, args: machine2 });
13837
14141
  console.log(JSON.stringify(manifestAdd(machine2), null, 2));
@@ -14058,6 +14362,37 @@ dnsCommand.command("list").description("List saved local domain mappings").optio
14058
14362
  dnsCommand.command("render").description("Render hosts/proxy configuration for a domain").argument("<domain>", "Domain name").option("-j, --json", "Print JSON output", false).action((domain) => {
14059
14363
  console.log(JSON.stringify(renderDomainMapping(domain), null, 2));
14060
14364
  });
14365
+ var hostsCommand = program2.command("hosts").description("Sync fleet machine names into /etc/hosts so machine<NN>:port resolves on the LAN and tailnet");
14366
+ function printHostsResult(plan, applied, viaSudo = false) {
14367
+ console.log(`hosts file: ${plan.hostsPath}`);
14368
+ console.log(`local subnets: ${plan.localSubnets.join(", ") || "none"}`);
14369
+ for (const entry of plan.entries) {
14370
+ console.log(` ${entry.ip} ${entry.names.join(" ")} (${entry.source})`);
14371
+ }
14372
+ if (plan.unresolved.length > 0) {
14373
+ console.log(`unresolved: ${plan.unresolved.join(", ")}`);
14374
+ }
14375
+ if (plan.warnings.length > 0) {
14376
+ console.log(`warnings: ${plan.warnings.join(", ")}`);
14377
+ }
14378
+ console.log(applied ? `applied ${plan.entries.length} entries${viaSudo ? " (via sudo)" : ""}` : "dry run \u2014 re-run `machines hosts apply` to write");
14379
+ }
14380
+ hostsCommand.command("plan", { isDefault: true }).description("Preview the managed /etc/hosts block for the fleet (dry run)").option("-j, --json", "Print JSON output", false).option("--no-warm", "Skip establishing direct Tailscale paths to discover LAN endpoints").action((options) => {
14381
+ const plan = planFleetHosts({ warm: options.warm });
14382
+ if (options.json) {
14383
+ console.log(JSON.stringify(plan, null, 2));
14384
+ return;
14385
+ }
14386
+ printHostsResult(plan, false);
14387
+ });
14388
+ hostsCommand.command("apply").description("Write the managed fleet block into /etc/hosts (uses sudo when required)").option("-j, --json", "Print JSON output", false).option("--no-warm", "Skip establishing direct Tailscale paths to discover LAN endpoints").action((options) => {
14389
+ const result = applyFleetHosts({ warm: options.warm });
14390
+ if (options.json) {
14391
+ console.log(JSON.stringify(result, null, 2));
14392
+ return;
14393
+ }
14394
+ printHostsResult(result, true, result.viaSudo);
14395
+ });
14061
14396
  notificationsCommand.command("add").description("Add or replace a notification channel").requiredOption("--id <id>", "Channel identifier").requiredOption("--type <type>", "email | webhook | command").requiredOption("--target <target>", "Email, webhook URL, or command executable").option("--arg <arg...>", "Command argument for command transports", collectOptionValues, []).option("--event <event...>", "Events routed to this channel", ["setup_failed", "sync_failed"]).option("--disabled", "Create the channel in disabled state", false).option("--approval-token <token>", "Operator mutation approval token for command transports").option("-j, --json", "Print JSON output", false).action((options) => {
14062
14397
  const enabled = !options.disabled;
14063
14398
  const events = [...new Set(options.event)];
@@ -0,0 +1,81 @@
1
+ import type { FleetManifest } from "../types.js";
2
+ export declare const HOSTS_BLOCK_BEGIN = "# >>> hasna machines fleet >>>";
3
+ export declare const HOSTS_BLOCK_END = "# <<< hasna machines fleet <<<";
4
+ /**
5
+ * Where each fleet host entry's IP came from, in descending preference:
6
+ * - manifest_lan : a `metadata.lanAddress` on the same /24 as this machine
7
+ * - tailscale_lan : the peer's live direct LAN endpoint (CurAddr) on this /24
8
+ * - tailscale : the peer's tailnet (100.64.0.0/10) IP — always routable
9
+ * - manifest_ip : an explicit `metadata.ipAddress` override
10
+ */
11
+ export type HostEntrySource = "manifest_lan" | "tailscale_lan" | "tailscale" | "manifest_ip";
12
+ export interface FleetHostEntry {
13
+ id: string;
14
+ ip: string;
15
+ names: string[];
16
+ source: HostEntrySource;
17
+ }
18
+ export interface RawTailscalePeer {
19
+ HostName?: string;
20
+ DNSName?: string;
21
+ TailscaleIPs?: string[];
22
+ CurAddr?: string;
23
+ Online?: boolean;
24
+ }
25
+ export interface RawTailscaleStatus {
26
+ Self?: RawTailscalePeer;
27
+ Peer?: Record<string, RawTailscalePeer>;
28
+ }
29
+ export interface HostsCommandResult {
30
+ stdout: string;
31
+ exitCode: number;
32
+ }
33
+ export type HostsCommandRunner = (command: string) => HostsCommandResult;
34
+ export interface BuildFleetHostEntriesInput {
35
+ manifest: FleetManifest;
36
+ tailscale: RawTailscaleStatus | null;
37
+ localSubnets: string[];
38
+ localMachineId?: string | null;
39
+ }
40
+ export interface BuildFleetHostEntriesResult {
41
+ entries: FleetHostEntry[];
42
+ unresolved: string[];
43
+ warnings: string[];
44
+ }
45
+ export interface FleetHostsPlan extends BuildFleetHostEntriesResult {
46
+ hostsPath: string;
47
+ block: string;
48
+ localSubnets: string[];
49
+ }
50
+ export interface FleetHostsOptions {
51
+ runner?: HostsCommandRunner;
52
+ localSubnets?: string[];
53
+ localMachineId?: string | null;
54
+ /**
55
+ * Establish direct Tailscale paths to online peers first so their LAN
56
+ * endpoints (CurAddr) become visible and can be preferred over tailnet IPs.
57
+ * Defaults to true.
58
+ */
59
+ warm?: boolean;
60
+ warmTimeoutSeconds?: number;
61
+ }
62
+ export interface ApplyFleetHostsResult extends FleetHostsPlan {
63
+ written: boolean;
64
+ viaSudo: boolean;
65
+ }
66
+ export declare function getHostsPath(): string;
67
+ export declare function isPrivateIpv4(value: string): boolean;
68
+ export declare function subnet24(value: string): string | null;
69
+ export declare function localPrivateSubnets(): string[];
70
+ export declare function buildFleetHostEntries(input: BuildFleetHostEntriesInput): BuildFleetHostEntriesResult;
71
+ export declare function renderHostsBlock(entries: FleetHostEntry[]): string;
72
+ export declare function mergeHostsContent(existing: string, block: string): string;
73
+ /**
74
+ * Online peers that do not yet expose a same-subnet LAN endpoint but do have a
75
+ * tailnet IP — pinging these establishes a direct path so their LAN address
76
+ * becomes resolvable.
77
+ */
78
+ export declare function collectPingTargets(tailscale: RawTailscaleStatus | null, localSubnets: string[]): string[];
79
+ export declare function warmDirectPaths(runner: HostsCommandRunner, targets: string[], timeoutSeconds?: number): void;
80
+ export declare function planFleetHosts(options?: FleetHostsOptions): FleetHostsPlan;
81
+ export declare function applyFleetHosts(options?: FleetHostsOptions): ApplyFleetHostsResult;
package/dist/index.js CHANGED
@@ -19926,7 +19926,7 @@ class JSONSchemaGenerator {
19926
19926
  if (val === undefined) {
19927
19927
  if (this.unrepresentable === "throw") {
19928
19928
  throw new Error("Literal `undefined` cannot be represented in JSON Schema");
19929
- }
19929
+ } else {}
19930
19930
  } else if (typeof val === "bigint") {
19931
19931
  if (this.unrepresentable === "throw") {
19932
19932
  throw new Error("BigInt literals cannot be represented in JSON Schema");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/machines",
3
- "version": "0.0.46",
3
+ "version": "0.0.47",
4
4
  "description": "Machine fleet management CLI + MCP for developers",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",