@hasna/machines 0.0.15 → 0.0.17

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
@@ -7573,63 +7573,401 @@ function diffMachines(leftMachineId, rightMachineId) {
7573
7573
  // src/remote.ts
7574
7574
  init_db();
7575
7575
  import { spawnSync as spawnSync2 } from "child_process";
7576
+ import { hostname as hostname4 } from "os";
7576
7577
 
7577
- // src/commands/ssh.ts
7578
+ // src/topology.ts
7579
+ init_db();
7580
+ import { existsSync as existsSync5 } from "fs";
7581
+ import { arch as arch2, hostname as hostname3, platform as platform3, userInfo as userInfo2 } from "os";
7578
7582
  import { spawnSync } from "child_process";
7583
+ init_paths();
7584
+ var MACHINES_CONSUMER_CONTRACT_VERSION = 1;
7585
+ var MACHINES_PACKAGE_NAME = "@hasna/machines";
7586
+ function normalizePlatform2(value = platform3()) {
7587
+ const normalized = value.toLowerCase();
7588
+ if (normalized === "darwin" || normalized === "macos")
7589
+ return "macos";
7590
+ if (normalized === "win32" || normalized === "windows")
7591
+ return "windows";
7592
+ if (normalized === "linux")
7593
+ return "linux";
7594
+ return value;
7595
+ }
7596
+ function defaultRunner(command) {
7597
+ const result = spawnSync("bash", ["-c", command], {
7598
+ encoding: "utf8",
7599
+ env: process.env
7600
+ });
7601
+ return {
7602
+ stdout: result.stdout || "",
7603
+ stderr: result.stderr || "",
7604
+ exitCode: result.status ?? 1
7605
+ };
7606
+ }
7607
+ function hasCommand(command, runner) {
7608
+ return runner(`command -v ${command} >/dev/null 2>&1`).exitCode === 0;
7609
+ }
7610
+ function parseTailscaleStatus(raw) {
7611
+ try {
7612
+ const parsed = JSON.parse(raw);
7613
+ if (!parsed || typeof parsed !== "object")
7614
+ return null;
7615
+ return parsed;
7616
+ } catch {
7617
+ return null;
7618
+ }
7619
+ }
7620
+ function loadTailscalePeers(runner, warnings) {
7621
+ const peers = new Map;
7622
+ if (!hasCommand("tailscale", runner)) {
7623
+ warnings.push("tailscale_not_available");
7624
+ return peers;
7625
+ }
7626
+ const result = runner("tailscale status --json");
7627
+ if (result.exitCode !== 0) {
7628
+ warnings.push(`tailscale_status_failed:${result.stderr.trim() || result.exitCode}`);
7629
+ return peers;
7630
+ }
7631
+ const status = parseTailscaleStatus(result.stdout);
7632
+ if (!status) {
7633
+ warnings.push("tailscale_status_invalid_json");
7634
+ return peers;
7635
+ }
7636
+ const addPeer = (peer) => {
7637
+ if (!peer)
7638
+ return;
7639
+ const id = peer.HostName || peer.DNSName?.split(".")[0];
7640
+ if (id)
7641
+ peers.set(id, peer);
7642
+ };
7643
+ addPeer(status.Self);
7644
+ for (const peer of Object.values(status.Peer ?? {}))
7645
+ addPeer(peer);
7646
+ return peers;
7647
+ }
7648
+ function machineKeys(machine) {
7649
+ return [
7650
+ machine.id,
7651
+ machine.hostname,
7652
+ machine.tailscaleName?.split(".")[0],
7653
+ machine.tailscaleName,
7654
+ machine.sshAddress?.split("@").pop()
7655
+ ].filter((value) => Boolean(value));
7656
+ }
7657
+ function findTailscalePeer(machine, machineId, peers) {
7658
+ if (machine) {
7659
+ for (const key of machineKeys(machine)) {
7660
+ const peer = peers.get(key) ?? peers.get(key.replace(/\.$/, ""));
7661
+ if (peer)
7662
+ return peer;
7663
+ }
7664
+ }
7665
+ return peers.get(machineId) ?? null;
7666
+ }
7579
7667
  function envReachableHosts() {
7580
7668
  const raw = process.env["HASNA_MACHINES_REACHABLE_HOSTS"];
7581
7669
  return new Set((raw || "").split(",").map((value) => value.trim()).filter(Boolean));
7582
7670
  }
7583
- function isReachable(host) {
7671
+ function manifestHostReachable(target) {
7584
7672
  const overrides = envReachableHosts();
7585
- if (overrides.size > 0) {
7586
- return overrides.has(host);
7673
+ if (overrides.size === 0)
7674
+ return null;
7675
+ return overrides.has(target);
7676
+ }
7677
+ function routeHints(input) {
7678
+ const hints = [];
7679
+ if (input.machineId === input.localMachineId) {
7680
+ hints.push({ kind: "local", target: "localhost", reachable: true });
7681
+ }
7682
+ if (input.manifest?.sshAddress) {
7683
+ hints.push({ kind: "ssh", target: input.manifest.sshAddress, reachable: manifestHostReachable(input.manifest.sshAddress) });
7684
+ }
7685
+ if (input.manifest?.hostname) {
7686
+ hints.push({ kind: "lan", target: input.manifest.hostname, reachable: manifestHostReachable(input.manifest.hostname) });
7587
7687
  }
7588
- const probe = spawnSync("bash", ["-lc", `getent hosts ${host} >/dev/null 2>&1 || ping -c 1 -W 1 ${host} >/dev/null 2>&1`], {
7589
- stdio: "ignore"
7688
+ const tailscaleTarget = input.manifest?.tailscaleName ?? input.peer?.DNSName ?? input.peer?.TailscaleIPs?.[0];
7689
+ if (tailscaleTarget) {
7690
+ hints.push({ kind: "tailscale", target: tailscaleTarget.replace(/\.$/, ""), reachable: input.peer?.Online ?? null });
7691
+ }
7692
+ return hints;
7693
+ }
7694
+ function routeRank(hint) {
7695
+ if (hint.kind === "local")
7696
+ return 0;
7697
+ if (hint.reachable === true && hint.kind === "ssh")
7698
+ return 1;
7699
+ if (hint.reachable === true && hint.kind === "lan")
7700
+ return 2;
7701
+ if (hint.reachable === true && hint.kind === "tailscale")
7702
+ return 3;
7703
+ if (hint.reachable === false)
7704
+ return 8;
7705
+ if (hint.kind === "ssh")
7706
+ return 4;
7707
+ if (hint.kind === "lan")
7708
+ return 5;
7709
+ if (hint.kind === "tailscale")
7710
+ return 6;
7711
+ return 9;
7712
+ }
7713
+ function selectRouteHint(hints) {
7714
+ return [...hints].sort((left, right) => routeRank(left) - routeRank(right))[0] ?? null;
7715
+ }
7716
+ function buildEntry(input) {
7717
+ const manifest = input.manifest;
7718
+ const peer = input.peer;
7719
+ const hints = routeHints({
7720
+ machineId: input.machineId,
7721
+ localMachineId: input.localMachineId,
7722
+ manifest,
7723
+ peer
7590
7724
  });
7591
- return probe.status === 0;
7725
+ const selectedRoute = selectRouteHint(hints);
7726
+ const route = selectedRoute?.kind === "ssh" ? "ssh" : selectedRoute?.kind ?? "unknown";
7727
+ return {
7728
+ machine_id: input.machineId,
7729
+ hostname: manifest?.hostname ?? peer?.HostName ?? null,
7730
+ platform: manifest?.platform ?? (peer?.OS ? normalizePlatform2(peer.OS) : null),
7731
+ os: peer?.OS ?? null,
7732
+ user: typeof manifest?.metadata?.user === "string" ? manifest.metadata.user : null,
7733
+ workspace_path: manifest?.workspacePath ?? null,
7734
+ manifest_declared: Boolean(manifest),
7735
+ heartbeat_status: input.heartbeat?.status ?? "unknown",
7736
+ last_heartbeat_at: input.heartbeat?.updated_at ?? null,
7737
+ tailscale: {
7738
+ dns_name: manifest?.tailscaleName ?? peer?.DNSName?.replace(/\.$/, "") ?? null,
7739
+ ips: peer?.TailscaleIPs ?? [],
7740
+ online: peer?.Online ?? null,
7741
+ active: peer?.Active ?? null,
7742
+ last_seen: peer?.LastSeen ?? null
7743
+ },
7744
+ ssh: {
7745
+ address: manifest?.sshAddress ?? null,
7746
+ route,
7747
+ command_target: selectedRoute?.target ?? null
7748
+ },
7749
+ route_hints: hints,
7750
+ tags: manifest?.tags ?? [],
7751
+ metadata: manifest?.metadata ?? {}
7752
+ };
7592
7753
  }
7593
- function resolveSshTarget(machineId) {
7594
- const machine = getManifestMachine(machineId);
7595
- if (!machine) {
7596
- throw new Error(`Machine not found in manifest: ${machineId}`);
7754
+ function discoverMachineTopology(options = {}) {
7755
+ const now = options.now ?? new Date;
7756
+ const runner = options.runner ?? defaultRunner;
7757
+ const warnings = [];
7758
+ const manifest = readManifest();
7759
+ const heartbeats = listHeartbeats();
7760
+ const heartbeatByMachine = new Map(heartbeats.map((heartbeat) => [heartbeat.machine_id, heartbeat]));
7761
+ const localMachineId = getLocalMachineId();
7762
+ const peers = options.includeTailscale === false ? new Map : loadTailscalePeers(runner, warnings);
7763
+ const machineIds = new Set([
7764
+ localMachineId,
7765
+ ...manifest.machines.map((machine) => machine.id),
7766
+ ...heartbeats.map((heartbeat) => heartbeat.machine_id),
7767
+ ...peers.keys()
7768
+ ]);
7769
+ const manifestById = new Map(manifest.machines.map((machine) => [machine.id, machine]));
7770
+ const machines = [...machineIds].sort().map((machineId) => {
7771
+ const manifestMachine = manifestById.get(machineId);
7772
+ return buildEntry({
7773
+ machineId,
7774
+ localMachineId,
7775
+ manifest: manifestMachine,
7776
+ peer: findTailscalePeer(manifestMachine ?? null, machineId, peers),
7777
+ heartbeat: heartbeatByMachine.get(machineId)
7778
+ });
7779
+ });
7780
+ return {
7781
+ schema_version: MACHINES_CONSUMER_CONTRACT_VERSION,
7782
+ package: {
7783
+ name: MACHINES_PACKAGE_NAME,
7784
+ version: getPackageVersion()
7785
+ },
7786
+ capabilities: {
7787
+ topology: true,
7788
+ compatibility: true,
7789
+ route_resolution: true,
7790
+ cli_json_fallback: true
7791
+ },
7792
+ generated_at: now.toISOString(),
7793
+ local_machine_id: localMachineId,
7794
+ local_hostname: hostname3(),
7795
+ current_platform: normalizePlatform2(),
7796
+ manifest_path_known: existsSync5(getManifestPath()),
7797
+ machines,
7798
+ warnings
7799
+ };
7800
+ }
7801
+ function normalizeMachineAlias(value) {
7802
+ return value.trim().replace(/\.$/, "").toLowerCase();
7803
+ }
7804
+ function routeTargetMatches(machine, requested) {
7805
+ const normalized = normalizeMachineAlias(requested);
7806
+ const values = [
7807
+ machine.ssh.address,
7808
+ machine.ssh.command_target,
7809
+ machine.tailscale.dns_name,
7810
+ machine.tailscale.dns_name?.split(".")[0],
7811
+ ...machine.tailscale.ips,
7812
+ ...machine.route_hints.map((hint) => hint.target),
7813
+ ...machine.route_hints.map((hint) => hint.target.split("@").pop() ?? hint.target)
7814
+ ].filter((value) => Boolean(value));
7815
+ return values.some((value) => normalizeMachineAlias(value) === normalized);
7816
+ }
7817
+ function findRouteMachine(topology, requestedMachineId) {
7818
+ const requested = normalizeMachineAlias(requestedMachineId);
7819
+ if (requested === "local" || requested === "localhost" || requested === normalizeMachineAlias(hostname3()) || requested === normalizeMachineAlias(topology.local_machine_id)) {
7820
+ return {
7821
+ machine: topology.machines.find((machine) => machine.machine_id === topology.local_machine_id) ?? null,
7822
+ matchedBy: "local_alias"
7823
+ };
7597
7824
  }
7598
- const current = detectCurrentMachineManifest();
7599
- if (machine.id === current.id) {
7825
+ const machineIdMatch = topology.machines.find((machine) => normalizeMachineAlias(machine.machine_id) === requested);
7826
+ if (machineIdMatch)
7827
+ return { machine: machineIdMatch, matchedBy: "machine_id" };
7828
+ const hostnameMatch = topology.machines.find((machine) => machine.hostname && normalizeMachineAlias(machine.hostname) === requested);
7829
+ if (hostnameMatch)
7830
+ return { machine: hostnameMatch, matchedBy: "hostname" };
7831
+ const tailscaleMatch = topology.machines.find((machine) => {
7832
+ if (!machine.tailscale.dns_name)
7833
+ return false;
7834
+ const dns = normalizeMachineAlias(machine.tailscale.dns_name);
7835
+ return dns === requested || dns.split(".")[0] === requested;
7836
+ });
7837
+ if (tailscaleMatch)
7838
+ return { machine: tailscaleMatch, matchedBy: "tailscale" };
7839
+ const routeMatch = topology.machines.find((machine) => routeTargetMatches(machine, requestedMachineId));
7840
+ if (routeMatch)
7841
+ return { machine: routeMatch, matchedBy: "route_target" };
7842
+ return { machine: null, matchedBy: null };
7843
+ }
7844
+ function routeConfidence(input) {
7845
+ if (input.matchedBy === "local_alias")
7846
+ return "exact";
7847
+ if (input.hint?.kind === "local")
7848
+ return "exact";
7849
+ if (input.hint?.reachable === true)
7850
+ return "high";
7851
+ if (input.machine.manifest_declared && (input.hint?.kind === "ssh" || input.hint?.kind === "lan"))
7852
+ return "medium";
7853
+ if (input.hint)
7854
+ return "low";
7855
+ return "none";
7856
+ }
7857
+ function resolveMachineRoute(machineId, options = {}) {
7858
+ const topology = options.topology ?? discoverMachineTopology(options);
7859
+ const warnings = [...topology.warnings];
7860
+ const { machine, matchedBy } = findRouteMachine(topology, machineId);
7861
+ const generatedAt = (options.now ?? new Date).toISOString();
7862
+ if (!machine) {
7863
+ warnings.push(`machine_not_found:${machineId}`);
7600
7864
  return {
7601
- machineId,
7602
- target: "localhost",
7603
- route: "local"
7865
+ schema_version: MACHINES_CONSUMER_CONTRACT_VERSION,
7866
+ package: { name: MACHINES_PACKAGE_NAME, version: getPackageVersion() },
7867
+ ok: false,
7868
+ machine_id: null,
7869
+ requested_machine_id: machineId,
7870
+ generated_at: generatedAt,
7871
+ route: "unknown",
7872
+ source: "unknown",
7873
+ target: null,
7874
+ command_target: null,
7875
+ confidence: "none",
7876
+ local: false,
7877
+ evidence: {
7878
+ topology: true,
7879
+ matched_by: null,
7880
+ manifest_declared: null,
7881
+ heartbeat_status: null,
7882
+ tailscale_online: null,
7883
+ selected_hint: null
7884
+ },
7885
+ warnings
7604
7886
  };
7605
7887
  }
7606
- const lanTarget = machine.sshAddress || machine.hostname || machine.id;
7607
- const tailscaleTarget = machine.tailscaleName || machine.hostname || machine.id;
7608
- const route = isReachable(lanTarget) ? "lan" : "tailscale";
7888
+ const selectedHint = selectRouteHint(machine.route_hints);
7889
+ const route = selectedHint?.kind ?? machine.ssh.route ?? "unknown";
7890
+ const local = route === "local" || machine.machine_id === topology.local_machine_id;
7609
7891
  return {
7610
- machineId,
7611
- target: route === "lan" ? lanTarget : tailscaleTarget,
7612
- route
7892
+ schema_version: MACHINES_CONSUMER_CONTRACT_VERSION,
7893
+ package: topology.package,
7894
+ ok: Boolean(selectedHint?.target),
7895
+ machine_id: machine.machine_id,
7896
+ requested_machine_id: machineId,
7897
+ generated_at: generatedAt,
7898
+ route,
7899
+ source: route,
7900
+ target: selectedHint?.target ?? null,
7901
+ command_target: selectedHint?.target ?? null,
7902
+ confidence: routeConfidence({ machine, hint: selectedHint, matchedBy }),
7903
+ local,
7904
+ evidence: {
7905
+ topology: true,
7906
+ matched_by: matchedBy,
7907
+ manifest_declared: machine.manifest_declared,
7908
+ heartbeat_status: machine.heartbeat_status,
7909
+ tailscale_online: machine.tailscale.online,
7910
+ selected_hint: selectedHint
7911
+ },
7912
+ warnings
7913
+ };
7914
+ }
7915
+
7916
+ // src/commands/ssh.ts
7917
+ function resolveSshTarget(machineId, options = {}) {
7918
+ const resolved = resolveMachineRoute(machineId, options);
7919
+ if (!resolved.ok || !resolved.target) {
7920
+ throw new Error(`Machine route not found: ${machineId}`);
7921
+ }
7922
+ if (resolved.route !== "local" && resolved.route !== "lan" && resolved.route !== "tailscale" && resolved.route !== "ssh") {
7923
+ throw new Error(`Machine route is not SSH-capable: ${machineId}`);
7924
+ }
7925
+ return {
7926
+ machineId: resolved.machine_id ?? machineId,
7927
+ target: resolved.target,
7928
+ route: resolved.route,
7929
+ confidence: resolved.confidence,
7930
+ warnings: resolved.warnings
7613
7931
  };
7614
7932
  }
7615
- function buildSshCommand(machineId, remoteCommand) {
7616
- const resolved = resolveSshTarget(machineId);
7933
+ function buildSshCommand(machineId, remoteCommand, options = {}) {
7934
+ const resolved = resolveSshTarget(machineId, options);
7617
7935
  return remoteCommand ? `ssh ${resolved.target} ${JSON.stringify(remoteCommand)}` : `ssh ${resolved.target}`;
7618
7936
  }
7619
7937
 
7620
7938
  // src/remote.ts
7939
+ function shellQuote(value) {
7940
+ return `'${value.replace(/'/g, "'\\''")}'`;
7941
+ }
7942
+ function machineIsLocal(machineId, localMachineId) {
7943
+ return machineId === "local" || machineId === "localhost" || machineId === localMachineId || machineId === hostname4();
7944
+ }
7945
+ function resolveMachineCommand(machineId, command, localMachineId = getLocalMachineId()) {
7946
+ if (machineIsLocal(machineId, localMachineId)) {
7947
+ return { source: "local", shellCommand: command };
7948
+ }
7949
+ try {
7950
+ return {
7951
+ source: resolveSshTarget(machineId).route,
7952
+ shellCommand: buildSshCommand(machineId, command)
7953
+ };
7954
+ } catch (error) {
7955
+ const message = String(error.message ?? error);
7956
+ if (message.includes("Machine route not found") || message.includes("Machine not found in manifest")) {
7957
+ return { source: "ssh", shellCommand: `ssh ${shellQuote(machineId)} ${shellQuote(command)}` };
7958
+ }
7959
+ throw error;
7960
+ }
7961
+ }
7621
7962
  function runMachineCommand(machineId, command) {
7622
- const localMachineId = getLocalMachineId();
7623
- const isLocal = machineId === localMachineId;
7624
- const route = isLocal ? "local" : resolveSshTarget(machineId).route;
7625
- const shellCommand = isLocal ? command : buildSshCommand(machineId, command);
7626
- const result = spawnSync2("bash", ["-c", shellCommand], {
7963
+ const resolved = resolveMachineCommand(machineId, command);
7964
+ const result = spawnSync2("bash", ["-c", resolved.shellCommand], {
7627
7965
  encoding: "utf8",
7628
7966
  env: process.env
7629
7967
  });
7630
7968
  return {
7631
7969
  machineId,
7632
- source: route,
7970
+ source: resolved.source,
7633
7971
  stdout: result.stdout || "",
7634
7972
  stderr: result.stderr || "",
7635
7973
  exitCode: result.status ?? 1
@@ -7649,7 +7987,7 @@ function getAppManager(machine, app) {
7649
7987
  return "winget";
7650
7988
  return "apt";
7651
7989
  }
7652
- function shellQuote(value) {
7990
+ function shellQuote2(value) {
7653
7991
  return `'${value.replace(/'/g, `'\\''`)}'`;
7654
7992
  }
7655
7993
  function buildAppCommand(machine, app) {
@@ -7670,7 +8008,7 @@ function buildAppCommand(machine, app) {
7670
8008
  return `sudo apt-get install -y ${packageName}`;
7671
8009
  }
7672
8010
  function buildAppProbeCommand(machine, app) {
7673
- const packageName = shellQuote(getPackageName(app));
8011
+ const packageName = shellQuote2(getPackageName(app));
7674
8012
  const manager = getAppManager(machine, app);
7675
8013
  if (manager === "custom") {
7676
8014
  return `if command -v ${packageName} >/dev/null 2>&1; then printf 'installed=1\\nversion=custom\\n'; else printf 'installed=0\\n'; fi`;
@@ -7948,7 +8286,7 @@ function runTailscaleInstall(machineId, options = {}) {
7948
8286
  }
7949
8287
 
7950
8288
  // src/commands/notifications.ts
7951
- import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
8289
+ import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
7952
8290
  init_paths();
7953
8291
  var notificationChannelSchema = exports_external.object({
7954
8292
  id: exports_external.string(),
@@ -7965,10 +8303,10 @@ var notificationConfigSchema = exports_external.object({
7965
8303
  function sortChannels(channels) {
7966
8304
  return [...channels].sort((left, right) => left.id.localeCompare(right.id));
7967
8305
  }
7968
- function shellQuote2(value) {
8306
+ function shellQuote3(value) {
7969
8307
  return `'${value.replace(/'/g, `'\\''`)}'`;
7970
8308
  }
7971
- function hasCommand(binary) {
8309
+ function hasCommand2(binary) {
7972
8310
  const result = Bun.spawnSync(["bash", "-lc", `command -v ${binary} >/dev/null 2>&1`], {
7973
8311
  stdout: "ignore",
7974
8312
  stderr: "ignore",
@@ -7993,7 +8331,7 @@ Content-Type: text/plain; charset=utf-8
7993
8331
 
7994
8332
  ${message}
7995
8333
  `;
7996
- if (hasCommand("sendmail")) {
8334
+ if (hasCommand2("sendmail")) {
7997
8335
  const result = Bun.spawnSync(["bash", "-lc", "sendmail -t"], {
7998
8336
  stdin: new TextEncoder().encode(body),
7999
8337
  stdout: "pipe",
@@ -8011,8 +8349,8 @@ ${message}
8011
8349
  detail: `Delivered via sendmail to ${channel.target}`
8012
8350
  };
8013
8351
  }
8014
- if (hasCommand("mail")) {
8015
- const command = `printf %s ${shellQuote2(message)} | mail -s ${shellQuote2(subject)} ${shellQuote2(channel.target)}`;
8352
+ if (hasCommand2("mail")) {
8353
+ const command = `printf %s ${shellQuote3(message)} | mail -s ${shellQuote3(subject)} ${shellQuote3(channel.target)}`;
8016
8354
  const result = Bun.spawnSync(["bash", "-lc", command], {
8017
8355
  stdout: "pipe",
8018
8356
  stderr: "pipe",
@@ -8105,7 +8443,7 @@ function getDefaultNotificationConfig() {
8105
8443
  };
8106
8444
  }
8107
8445
  function readNotificationConfig(path = getNotificationsPath()) {
8108
- if (!existsSync5(path)) {
8446
+ if (!existsSync6(path)) {
8109
8447
  return getDefaultNotificationConfig();
8110
8448
  }
8111
8449
  return notificationConfigSchema.parse(JSON.parse(readFileSync4(path, "utf8")));
@@ -8251,7 +8589,7 @@ function listPorts(machineId) {
8251
8589
  }
8252
8590
 
8253
8591
  // src/commands/sync.ts
8254
- import { existsSync as existsSync6, lstatSync, readFileSync as readFileSync5, symlinkSync, copyFileSync } from "fs";
8592
+ import { existsSync as existsSync7, lstatSync, readFileSync as readFileSync5, symlinkSync, copyFileSync } from "fs";
8255
8593
  init_paths();
8256
8594
  init_db();
8257
8595
  function quote4(value) {
@@ -8301,8 +8639,8 @@ function detectPackageActions(machine) {
8301
8639
  }
8302
8640
  function detectFileActions(machine) {
8303
8641
  return (machine.files || []).map((file, index) => {
8304
- const sourceExists = existsSync6(file.source);
8305
- const targetExists = existsSync6(file.target);
8642
+ const sourceExists = existsSync7(file.source);
8643
+ const targetExists = existsSync7(file.target);
8306
8644
  let status = "missing";
8307
8645
  if (sourceExists && targetExists) {
8308
8646
  if (file.mode === "symlink") {
@@ -8438,185 +8776,6 @@ function getStatus() {
8438
8776
  };
8439
8777
  }
8440
8778
 
8441
- // src/topology.ts
8442
- init_db();
8443
- import { existsSync as existsSync7 } from "fs";
8444
- import { arch as arch2, hostname as hostname3, platform as platform3, userInfo as userInfo2 } from "os";
8445
- import { spawnSync as spawnSync4 } from "child_process";
8446
- init_paths();
8447
- function normalizePlatform2(value = platform3()) {
8448
- const normalized = value.toLowerCase();
8449
- if (normalized === "darwin" || normalized === "macos")
8450
- return "macos";
8451
- if (normalized === "win32" || normalized === "windows")
8452
- return "windows";
8453
- if (normalized === "linux")
8454
- return "linux";
8455
- return value;
8456
- }
8457
- function defaultRunner(command) {
8458
- const result = spawnSync4("bash", ["-c", command], {
8459
- encoding: "utf8",
8460
- env: process.env
8461
- });
8462
- return {
8463
- stdout: result.stdout || "",
8464
- stderr: result.stderr || "",
8465
- exitCode: result.status ?? 1
8466
- };
8467
- }
8468
- function hasCommand2(command, runner) {
8469
- return runner(`command -v ${command} >/dev/null 2>&1`).exitCode === 0;
8470
- }
8471
- function parseTailscaleStatus(raw) {
8472
- try {
8473
- const parsed = JSON.parse(raw);
8474
- if (!parsed || typeof parsed !== "object")
8475
- return null;
8476
- return parsed;
8477
- } catch {
8478
- return null;
8479
- }
8480
- }
8481
- function loadTailscalePeers(runner, warnings) {
8482
- const peers = new Map;
8483
- if (!hasCommand2("tailscale", runner)) {
8484
- warnings.push("tailscale_not_available");
8485
- return peers;
8486
- }
8487
- const result = runner("tailscale status --json");
8488
- if (result.exitCode !== 0) {
8489
- warnings.push(`tailscale_status_failed:${result.stderr.trim() || result.exitCode}`);
8490
- return peers;
8491
- }
8492
- const status = parseTailscaleStatus(result.stdout);
8493
- if (!status) {
8494
- warnings.push("tailscale_status_invalid_json");
8495
- return peers;
8496
- }
8497
- const addPeer = (peer) => {
8498
- if (!peer)
8499
- return;
8500
- const id = peer.HostName || peer.DNSName?.split(".")[0];
8501
- if (id)
8502
- peers.set(id, peer);
8503
- };
8504
- addPeer(status.Self);
8505
- for (const peer of Object.values(status.Peer ?? {}))
8506
- addPeer(peer);
8507
- return peers;
8508
- }
8509
- function machineKeys(machine) {
8510
- return [
8511
- machine.id,
8512
- machine.hostname,
8513
- machine.tailscaleName?.split(".")[0],
8514
- machine.tailscaleName,
8515
- machine.sshAddress?.split("@").pop()
8516
- ].filter((value) => Boolean(value));
8517
- }
8518
- function findTailscalePeer(machine, machineId, peers) {
8519
- if (machine) {
8520
- for (const key of machineKeys(machine)) {
8521
- const peer = peers.get(key) ?? peers.get(key.replace(/\.$/, ""));
8522
- if (peer)
8523
- return peer;
8524
- }
8525
- }
8526
- return peers.get(machineId) ?? null;
8527
- }
8528
- function routeHints(input) {
8529
- const hints = [];
8530
- if (input.machineId === input.localMachineId) {
8531
- hints.push({ kind: "local", target: "localhost", reachable: true });
8532
- }
8533
- if (input.manifest?.sshAddress) {
8534
- hints.push({ kind: "ssh", target: input.manifest.sshAddress, reachable: null });
8535
- }
8536
- if (input.manifest?.hostname) {
8537
- hints.push({ kind: "lan", target: input.manifest.hostname, reachable: null });
8538
- }
8539
- const tailscaleTarget = input.manifest?.tailscaleName ?? input.peer?.DNSName ?? input.peer?.TailscaleIPs?.[0];
8540
- if (tailscaleTarget) {
8541
- hints.push({ kind: "tailscale", target: tailscaleTarget.replace(/\.$/, ""), reachable: input.peer?.Online ?? null });
8542
- }
8543
- return hints;
8544
- }
8545
- function buildEntry(input) {
8546
- const manifest = input.manifest;
8547
- const peer = input.peer;
8548
- const hints = routeHints({
8549
- machineId: input.machineId,
8550
- localMachineId: input.localMachineId,
8551
- manifest,
8552
- peer
8553
- });
8554
- const selectedRoute = hints.find((hint) => hint.kind === "local") ?? hints.find((hint) => hint.kind === "ssh") ?? hints.find((hint) => hint.kind === "lan") ?? hints.find((hint) => hint.kind === "tailscale");
8555
- const route = selectedRoute?.kind === "ssh" ? "lan" : selectedRoute?.kind ?? "unknown";
8556
- return {
8557
- machine_id: input.machineId,
8558
- hostname: manifest?.hostname ?? peer?.HostName ?? null,
8559
- platform: manifest?.platform ?? (peer?.OS ? normalizePlatform2(peer.OS) : null),
8560
- os: peer?.OS ?? null,
8561
- user: typeof manifest?.metadata?.user === "string" ? manifest.metadata.user : null,
8562
- workspace_path: manifest?.workspacePath ?? null,
8563
- manifest_declared: Boolean(manifest),
8564
- heartbeat_status: input.heartbeat?.status ?? "unknown",
8565
- last_heartbeat_at: input.heartbeat?.updated_at ?? null,
8566
- tailscale: {
8567
- dns_name: manifest?.tailscaleName ?? peer?.DNSName?.replace(/\.$/, "") ?? null,
8568
- ips: peer?.TailscaleIPs ?? [],
8569
- online: peer?.Online ?? null,
8570
- active: peer?.Active ?? null,
8571
- last_seen: peer?.LastSeen ?? null
8572
- },
8573
- ssh: {
8574
- address: manifest?.sshAddress ?? null,
8575
- route,
8576
- command_target: selectedRoute?.target ?? null
8577
- },
8578
- route_hints: hints,
8579
- tags: manifest?.tags ?? [],
8580
- metadata: manifest?.metadata ?? {}
8581
- };
8582
- }
8583
- function discoverMachineTopology(options = {}) {
8584
- const now = options.now ?? new Date;
8585
- const runner = options.runner ?? defaultRunner;
8586
- const warnings = [];
8587
- const manifest = readManifest();
8588
- const heartbeats = listHeartbeats();
8589
- const heartbeatByMachine = new Map(heartbeats.map((heartbeat) => [heartbeat.machine_id, heartbeat]));
8590
- const localMachineId = getLocalMachineId();
8591
- const peers = options.includeTailscale === false ? new Map : loadTailscalePeers(runner, warnings);
8592
- const machineIds = new Set([
8593
- localMachineId,
8594
- ...manifest.machines.map((machine) => machine.id),
8595
- ...heartbeats.map((heartbeat) => heartbeat.machine_id),
8596
- ...peers.keys()
8597
- ]);
8598
- const manifestById = new Map(manifest.machines.map((machine) => [machine.id, machine]));
8599
- const machines = [...machineIds].sort().map((machineId) => {
8600
- const manifestMachine = manifestById.get(machineId);
8601
- return buildEntry({
8602
- machineId,
8603
- localMachineId,
8604
- manifest: manifestMachine,
8605
- peer: findTailscalePeer(manifestMachine ?? null, machineId, peers),
8606
- heartbeat: heartbeatByMachine.get(machineId)
8607
- });
8608
- });
8609
- return {
8610
- generated_at: now.toISOString(),
8611
- local_machine_id: localMachineId,
8612
- local_hostname: hostname3(),
8613
- current_platform: normalizePlatform2(),
8614
- manifest_path_known: existsSync7(getManifestPath()),
8615
- machines,
8616
- warnings
8617
- };
8618
- }
8619
-
8620
8779
  // src/compatibility.ts
8621
8780
  init_db();
8622
8781
  var DEFAULT_COMMANDS = [
@@ -8626,7 +8785,7 @@ var DEFAULT_COMMANDS = [
8626
8785
  function defaultPackages() {
8627
8786
  return [{ name: "@hasna/machines", command: "machines", expectedVersion: getPackageVersion(), required: true }];
8628
8787
  }
8629
- function shellQuote3(value) {
8788
+ function shellQuote4(value) {
8630
8789
  return `'${value.replace(/'/g, "'\\''")}'`;
8631
8790
  }
8632
8791
  function commandId(value) {
@@ -8677,7 +8836,7 @@ function defaultRunner2(machineId, command) {
8677
8836
  return runMachineCommand(machineId, command);
8678
8837
  }
8679
8838
  function inspectCommand(machineId, spec, runner) {
8680
- const command = shellQuote3(spec.command);
8839
+ const command = shellQuote4(spec.command);
8681
8840
  const versionArgs = spec.versionArgs ?? "--version";
8682
8841
  const script = [
8683
8842
  `cmd=${command}`,
@@ -8706,7 +8865,7 @@ function fieldCommand(field) {
8706
8865
  }
8707
8866
  function inspectWorkspace(machineId, spec, runner) {
8708
8867
  const script = [
8709
- `path=${shellQuote3(spec.path)}`,
8868
+ `path=${shellQuote4(spec.path)}`,
8710
8869
  'printf "exists=%s\\n" "$(test -d "$path" && printf yes || printf no)"',
8711
8870
  'pkg="$path/package.json"',
8712
8871
  'printf "package_json=%s\\n" "$(test -f "$pkg" && printf yes || printf no)"',
@@ -8844,6 +9003,17 @@ function checkMachineCompatibility(options = {}) {
8844
9003
  fail: checks.filter((check) => check.status === "fail").length
8845
9004
  };
8846
9005
  return {
9006
+ schema_version: MACHINES_CONSUMER_CONTRACT_VERSION,
9007
+ package: {
9008
+ name: MACHINES_PACKAGE_NAME,
9009
+ version: getPackageVersion()
9010
+ },
9011
+ capabilities: {
9012
+ topology: true,
9013
+ compatibility: true,
9014
+ route_resolution: true,
9015
+ cli_json_fallback: true
9016
+ },
8847
9017
  ok: summary.fail === 0,
8848
9018
  machine_id: machineId,
8849
9019
  source: checks[0]?.source ?? "local",
@@ -10313,10 +10483,13 @@ function parsePackageSpec(value) {
10313
10483
  };
10314
10484
  }
10315
10485
  function parseWorkspaceSpec(value) {
10316
- const [label, path] = value.includes("=") ? value.split(/=(.*)/s).filter(Boolean) : ["workspace", value];
10486
+ const [label, rest] = value.includes("=") ? value.split(/=(.*)/s).filter(Boolean) : ["workspace", value];
10487
+ const [path, expectedPackageName, expectedVersion] = rest.split(":");
10317
10488
  return {
10318
10489
  label,
10319
10490
  path,
10491
+ expectedPackageName: expectedPackageName || undefined,
10492
+ expectedVersion: expectedVersion || undefined,
10320
10493
  required: true
10321
10494
  };
10322
10495
  }
@@ -10477,7 +10650,7 @@ program2.command("topology").description("Discover local, manifest, heartbeat, S
10477
10650
  console.log(`${machine.machine_id.padEnd(18)} ${String(machine.platform || "unknown").padEnd(8)} ${machine.heartbeat_status.padEnd(8)} ${route}`);
10478
10651
  }
10479
10652
  });
10480
- program2.command("compatibility").description("Check remote package, command, and workspace compatibility for open-* consumers").option("--machine <id>", "Machine identifier").option("--command <command...>", "Required command or command:expectedVersion").option("--package <spec...>", "Required package as name[:command[:expectedVersion]]").option("--workspace <spec...>", "Required workspace as label=/path or /path").option("-j, --json", "Print JSON output", false).action((options) => {
10653
+ program2.command("compatibility").description("Check remote package, command, and workspace compatibility for open-* consumers").option("--machine <id>", "Machine identifier").option("--command <command...>", "Required command or command:expectedVersion").option("--package <spec...>", "Required package as name[:command[:expectedVersion]]").option("--workspace <spec...>", "Required workspace as label=/path[:expectedPackageName[:expectedVersion]] or /path[:expectedPackageName[:expectedVersion]]").option("-j, --json", "Print JSON output", false).action((options) => {
10481
10654
  const result = checkMachineCompatibility({
10482
10655
  machineId: options.machine,
10483
10656
  commands: options.command?.map(parseCommandSpec),
@@ -10630,9 +10803,26 @@ program2.command("install-tailscale").description("Install Tailscale on a machin
10630
10803
  const result = options.apply ? runTailscaleInstall(options.machine, { apply: true, yes: options.yes }) : buildTailscaleInstallPlan(options.machine);
10631
10804
  console.log(JSON.stringify(result, null, 2));
10632
10805
  });
10806
+ 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) => {
10807
+ const topology = discoverMachineTopology({ includeTailscale: options.tailscale !== false });
10808
+ const resolved = resolveMachineRoute(options.machine, { topology });
10809
+ const command = resolved.ok && resolved.target ? resolved.route === "local" ? options.cmd ?? null : buildSshCommand(options.machine, options.cmd, { topology }) : null;
10810
+ const payload = { ...resolved, command };
10811
+ if (options.json) {
10812
+ console.log(JSON.stringify(payload, null, 2));
10813
+ return;
10814
+ }
10815
+ if (!resolved.ok) {
10816
+ console.error(source_default.red(resolved.warnings.join("; ") || `No route found for ${options.machine}`));
10817
+ process.exitCode = 1;
10818
+ return;
10819
+ }
10820
+ console.log(command ?? `${resolved.route}:${resolved.target}`);
10821
+ });
10633
10822
  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) => {
10634
10823
  if (options.json) {
10635
- console.log(JSON.stringify(resolveSshTarget(options.machine), null, 2));
10824
+ const resolved = resolveMachineRoute(options.machine);
10825
+ console.log(JSON.stringify({ resolved, command: resolved.ok ? buildSshCommand(options.machine, options.cmd) : null }, null, 2));
10636
10826
  return;
10637
10827
  }
10638
10828
  console.log(buildSshCommand(options.machine, options.cmd));