@hasna/machines 0.0.16 → 0.0.18

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,57 +7573,377 @@ 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 hostname3 } from "os";
7576
+ import { hostname as hostname4 } from "os";
7577
7577
 
7578
- // 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";
7579
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
+ }
7580
7667
  function envReachableHosts() {
7581
7668
  const raw = process.env["HASNA_MACHINES_REACHABLE_HOSTS"];
7582
7669
  return new Set((raw || "").split(",").map((value) => value.trim()).filter(Boolean));
7583
7670
  }
7584
- function isReachable(host) {
7671
+ function manifestHostReachable(target) {
7585
7672
  const overrides = envReachableHosts();
7586
- if (overrides.size > 0) {
7587
- 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) });
7687
+ }
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 });
7588
7691
  }
7589
- const probe = spawnSync("bash", ["-lc", `getent hosts ${host} >/dev/null 2>&1 || ping -c 1 -W 1 ${host} >/dev/null 2>&1`], {
7590
- stdio: "ignore"
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
7591
7724
  });
7592
- 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
+ };
7593
7753
  }
7594
- function resolveSshTarget(machineId) {
7595
- const machine = getManifestMachine(machineId);
7596
- if (!machine) {
7597
- 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
+ };
7598
7824
  }
7599
- const current = detectCurrentMachineManifest();
7600
- 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}`);
7601
7864
  return {
7602
- machineId,
7603
- target: "localhost",
7604
- 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
7605
7886
  };
7606
7887
  }
7607
- const lanTarget = machine.sshAddress || machine.hostname || machine.id;
7608
- const tailscaleTarget = machine.tailscaleName || machine.hostname || machine.id;
7609
- 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;
7610
7891
  return {
7611
- machineId,
7612
- target: route === "lan" ? lanTarget : tailscaleTarget,
7613
- 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 shellQuote(value) {
7918
+ return `'${value.replace(/'/g, "'\\''")}'`;
7919
+ }
7920
+ function resolveSshTarget(machineId, options = {}) {
7921
+ const resolved = resolveMachineRoute(machineId, options);
7922
+ if (!resolved.ok || !resolved.target) {
7923
+ throw new Error(`Machine route not found: ${machineId}`);
7924
+ }
7925
+ if (resolved.route !== "local" && resolved.route !== "lan" && resolved.route !== "tailscale" && resolved.route !== "ssh") {
7926
+ throw new Error(`Machine route is not SSH-capable: ${machineId}`);
7927
+ }
7928
+ return {
7929
+ machineId: resolved.machine_id ?? machineId,
7930
+ target: resolved.target,
7931
+ route: resolved.route,
7932
+ confidence: resolved.confidence,
7933
+ warnings: resolved.warnings
7614
7934
  };
7615
7935
  }
7616
- function buildSshCommand(machineId, remoteCommand) {
7617
- const resolved = resolveSshTarget(machineId);
7618
- return remoteCommand ? `ssh ${resolved.target} ${JSON.stringify(remoteCommand)}` : `ssh ${resolved.target}`;
7936
+ function buildSshCommand(machineId, remoteCommand, options = {}) {
7937
+ const resolved = resolveSshTarget(machineId, options);
7938
+ return remoteCommand ? `ssh ${resolved.target} ${shellQuote(remoteCommand)}` : `ssh ${resolved.target}`;
7619
7939
  }
7620
7940
 
7621
7941
  // src/remote.ts
7622
- function shellQuote(value) {
7942
+ function shellQuote2(value) {
7623
7943
  return `'${value.replace(/'/g, "'\\''")}'`;
7624
7944
  }
7625
7945
  function machineIsLocal(machineId, localMachineId) {
7626
- return machineId === "local" || machineId === "localhost" || machineId === localMachineId || machineId === hostname3();
7946
+ return machineId === "local" || machineId === "localhost" || machineId === localMachineId || machineId === hostname4();
7627
7947
  }
7628
7948
  function resolveMachineCommand(machineId, command, localMachineId = getLocalMachineId()) {
7629
7949
  if (machineIsLocal(machineId, localMachineId)) {
@@ -7635,8 +7955,9 @@ function resolveMachineCommand(machineId, command, localMachineId = getLocalMach
7635
7955
  shellCommand: buildSshCommand(machineId, command)
7636
7956
  };
7637
7957
  } catch (error) {
7638
- if (String(error.message ?? error).includes("Machine not found in manifest")) {
7639
- return { source: "ssh", shellCommand: `ssh ${shellQuote(machineId)} ${shellQuote(command)}` };
7958
+ const message = String(error.message ?? error);
7959
+ if (message.includes("Machine route not found") || message.includes("Machine not found in manifest")) {
7960
+ return { source: "ssh", shellCommand: `ssh ${shellQuote2(machineId)} ${shellQuote2(command)}` };
7640
7961
  }
7641
7962
  throw error;
7642
7963
  }
@@ -7669,7 +7990,7 @@ function getAppManager(machine, app) {
7669
7990
  return "winget";
7670
7991
  return "apt";
7671
7992
  }
7672
- function shellQuote2(value) {
7993
+ function shellQuote3(value) {
7673
7994
  return `'${value.replace(/'/g, `'\\''`)}'`;
7674
7995
  }
7675
7996
  function buildAppCommand(machine, app) {
@@ -7690,7 +8011,7 @@ function buildAppCommand(machine, app) {
7690
8011
  return `sudo apt-get install -y ${packageName}`;
7691
8012
  }
7692
8013
  function buildAppProbeCommand(machine, app) {
7693
- const packageName = shellQuote2(getPackageName(app));
8014
+ const packageName = shellQuote3(getPackageName(app));
7694
8015
  const manager = getAppManager(machine, app);
7695
8016
  if (manager === "custom") {
7696
8017
  return `if command -v ${packageName} >/dev/null 2>&1; then printf 'installed=1\\nversion=custom\\n'; else printf 'installed=0\\n'; fi`;
@@ -7968,7 +8289,7 @@ function runTailscaleInstall(machineId, options = {}) {
7968
8289
  }
7969
8290
 
7970
8291
  // src/commands/notifications.ts
7971
- import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
8292
+ import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
7972
8293
  init_paths();
7973
8294
  var notificationChannelSchema = exports_external.object({
7974
8295
  id: exports_external.string(),
@@ -7985,10 +8306,10 @@ var notificationConfigSchema = exports_external.object({
7985
8306
  function sortChannels(channels) {
7986
8307
  return [...channels].sort((left, right) => left.id.localeCompare(right.id));
7987
8308
  }
7988
- function shellQuote3(value) {
8309
+ function shellQuote4(value) {
7989
8310
  return `'${value.replace(/'/g, `'\\''`)}'`;
7990
8311
  }
7991
- function hasCommand(binary) {
8312
+ function hasCommand2(binary) {
7992
8313
  const result = Bun.spawnSync(["bash", "-lc", `command -v ${binary} >/dev/null 2>&1`], {
7993
8314
  stdout: "ignore",
7994
8315
  stderr: "ignore",
@@ -8013,7 +8334,7 @@ Content-Type: text/plain; charset=utf-8
8013
8334
 
8014
8335
  ${message}
8015
8336
  `;
8016
- if (hasCommand("sendmail")) {
8337
+ if (hasCommand2("sendmail")) {
8017
8338
  const result = Bun.spawnSync(["bash", "-lc", "sendmail -t"], {
8018
8339
  stdin: new TextEncoder().encode(body),
8019
8340
  stdout: "pipe",
@@ -8031,8 +8352,8 @@ ${message}
8031
8352
  detail: `Delivered via sendmail to ${channel.target}`
8032
8353
  };
8033
8354
  }
8034
- if (hasCommand("mail")) {
8035
- const command = `printf %s ${shellQuote3(message)} | mail -s ${shellQuote3(subject)} ${shellQuote3(channel.target)}`;
8355
+ if (hasCommand2("mail")) {
8356
+ const command = `printf %s ${shellQuote4(message)} | mail -s ${shellQuote4(subject)} ${shellQuote4(channel.target)}`;
8036
8357
  const result = Bun.spawnSync(["bash", "-lc", command], {
8037
8358
  stdout: "pipe",
8038
8359
  stderr: "pipe",
@@ -8125,7 +8446,7 @@ function getDefaultNotificationConfig() {
8125
8446
  };
8126
8447
  }
8127
8448
  function readNotificationConfig(path = getNotificationsPath()) {
8128
- if (!existsSync5(path)) {
8449
+ if (!existsSync6(path)) {
8129
8450
  return getDefaultNotificationConfig();
8130
8451
  }
8131
8452
  return notificationConfigSchema.parse(JSON.parse(readFileSync4(path, "utf8")));
@@ -8271,7 +8592,7 @@ function listPorts(machineId) {
8271
8592
  }
8272
8593
 
8273
8594
  // src/commands/sync.ts
8274
- import { existsSync as existsSync6, lstatSync, readFileSync as readFileSync5, symlinkSync, copyFileSync } from "fs";
8595
+ import { existsSync as existsSync7, lstatSync, readFileSync as readFileSync5, symlinkSync, copyFileSync } from "fs";
8275
8596
  init_paths();
8276
8597
  init_db();
8277
8598
  function quote4(value) {
@@ -8321,8 +8642,8 @@ function detectPackageActions(machine) {
8321
8642
  }
8322
8643
  function detectFileActions(machine) {
8323
8644
  return (machine.files || []).map((file, index) => {
8324
- const sourceExists = existsSync6(file.source);
8325
- const targetExists = existsSync6(file.target);
8645
+ const sourceExists = existsSync7(file.source);
8646
+ const targetExists = existsSync7(file.target);
8326
8647
  let status = "missing";
8327
8648
  if (sourceExists && targetExists) {
8328
8649
  if (file.mode === "symlink") {
@@ -8458,185 +8779,6 @@ function getStatus() {
8458
8779
  };
8459
8780
  }
8460
8781
 
8461
- // src/topology.ts
8462
- init_db();
8463
- import { existsSync as existsSync7 } from "fs";
8464
- import { arch as arch2, hostname as hostname4, platform as platform3, userInfo as userInfo2 } from "os";
8465
- import { spawnSync as spawnSync4 } from "child_process";
8466
- init_paths();
8467
- function normalizePlatform2(value = platform3()) {
8468
- const normalized = value.toLowerCase();
8469
- if (normalized === "darwin" || normalized === "macos")
8470
- return "macos";
8471
- if (normalized === "win32" || normalized === "windows")
8472
- return "windows";
8473
- if (normalized === "linux")
8474
- return "linux";
8475
- return value;
8476
- }
8477
- function defaultRunner(command) {
8478
- const result = spawnSync4("bash", ["-c", command], {
8479
- encoding: "utf8",
8480
- env: process.env
8481
- });
8482
- return {
8483
- stdout: result.stdout || "",
8484
- stderr: result.stderr || "",
8485
- exitCode: result.status ?? 1
8486
- };
8487
- }
8488
- function hasCommand2(command, runner) {
8489
- return runner(`command -v ${command} >/dev/null 2>&1`).exitCode === 0;
8490
- }
8491
- function parseTailscaleStatus(raw) {
8492
- try {
8493
- const parsed = JSON.parse(raw);
8494
- if (!parsed || typeof parsed !== "object")
8495
- return null;
8496
- return parsed;
8497
- } catch {
8498
- return null;
8499
- }
8500
- }
8501
- function loadTailscalePeers(runner, warnings) {
8502
- const peers = new Map;
8503
- if (!hasCommand2("tailscale", runner)) {
8504
- warnings.push("tailscale_not_available");
8505
- return peers;
8506
- }
8507
- const result = runner("tailscale status --json");
8508
- if (result.exitCode !== 0) {
8509
- warnings.push(`tailscale_status_failed:${result.stderr.trim() || result.exitCode}`);
8510
- return peers;
8511
- }
8512
- const status = parseTailscaleStatus(result.stdout);
8513
- if (!status) {
8514
- warnings.push("tailscale_status_invalid_json");
8515
- return peers;
8516
- }
8517
- const addPeer = (peer) => {
8518
- if (!peer)
8519
- return;
8520
- const id = peer.HostName || peer.DNSName?.split(".")[0];
8521
- if (id)
8522
- peers.set(id, peer);
8523
- };
8524
- addPeer(status.Self);
8525
- for (const peer of Object.values(status.Peer ?? {}))
8526
- addPeer(peer);
8527
- return peers;
8528
- }
8529
- function machineKeys(machine) {
8530
- return [
8531
- machine.id,
8532
- machine.hostname,
8533
- machine.tailscaleName?.split(".")[0],
8534
- machine.tailscaleName,
8535
- machine.sshAddress?.split("@").pop()
8536
- ].filter((value) => Boolean(value));
8537
- }
8538
- function findTailscalePeer(machine, machineId, peers) {
8539
- if (machine) {
8540
- for (const key of machineKeys(machine)) {
8541
- const peer = peers.get(key) ?? peers.get(key.replace(/\.$/, ""));
8542
- if (peer)
8543
- return peer;
8544
- }
8545
- }
8546
- return peers.get(machineId) ?? null;
8547
- }
8548
- function routeHints(input) {
8549
- const hints = [];
8550
- if (input.machineId === input.localMachineId) {
8551
- hints.push({ kind: "local", target: "localhost", reachable: true });
8552
- }
8553
- if (input.manifest?.sshAddress) {
8554
- hints.push({ kind: "ssh", target: input.manifest.sshAddress, reachable: null });
8555
- }
8556
- if (input.manifest?.hostname) {
8557
- hints.push({ kind: "lan", target: input.manifest.hostname, reachable: null });
8558
- }
8559
- const tailscaleTarget = input.manifest?.tailscaleName ?? input.peer?.DNSName ?? input.peer?.TailscaleIPs?.[0];
8560
- if (tailscaleTarget) {
8561
- hints.push({ kind: "tailscale", target: tailscaleTarget.replace(/\.$/, ""), reachable: input.peer?.Online ?? null });
8562
- }
8563
- return hints;
8564
- }
8565
- function buildEntry(input) {
8566
- const manifest = input.manifest;
8567
- const peer = input.peer;
8568
- const hints = routeHints({
8569
- machineId: input.machineId,
8570
- localMachineId: input.localMachineId,
8571
- manifest,
8572
- peer
8573
- });
8574
- 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");
8575
- const route = selectedRoute?.kind === "ssh" ? "lan" : selectedRoute?.kind ?? "unknown";
8576
- return {
8577
- machine_id: input.machineId,
8578
- hostname: manifest?.hostname ?? peer?.HostName ?? null,
8579
- platform: manifest?.platform ?? (peer?.OS ? normalizePlatform2(peer.OS) : null),
8580
- os: peer?.OS ?? null,
8581
- user: typeof manifest?.metadata?.user === "string" ? manifest.metadata.user : null,
8582
- workspace_path: manifest?.workspacePath ?? null,
8583
- manifest_declared: Boolean(manifest),
8584
- heartbeat_status: input.heartbeat?.status ?? "unknown",
8585
- last_heartbeat_at: input.heartbeat?.updated_at ?? null,
8586
- tailscale: {
8587
- dns_name: manifest?.tailscaleName ?? peer?.DNSName?.replace(/\.$/, "") ?? null,
8588
- ips: peer?.TailscaleIPs ?? [],
8589
- online: peer?.Online ?? null,
8590
- active: peer?.Active ?? null,
8591
- last_seen: peer?.LastSeen ?? null
8592
- },
8593
- ssh: {
8594
- address: manifest?.sshAddress ?? null,
8595
- route,
8596
- command_target: selectedRoute?.target ?? null
8597
- },
8598
- route_hints: hints,
8599
- tags: manifest?.tags ?? [],
8600
- metadata: manifest?.metadata ?? {}
8601
- };
8602
- }
8603
- function discoverMachineTopology(options = {}) {
8604
- const now = options.now ?? new Date;
8605
- const runner = options.runner ?? defaultRunner;
8606
- const warnings = [];
8607
- const manifest = readManifest();
8608
- const heartbeats = listHeartbeats();
8609
- const heartbeatByMachine = new Map(heartbeats.map((heartbeat) => [heartbeat.machine_id, heartbeat]));
8610
- const localMachineId = getLocalMachineId();
8611
- const peers = options.includeTailscale === false ? new Map : loadTailscalePeers(runner, warnings);
8612
- const machineIds = new Set([
8613
- localMachineId,
8614
- ...manifest.machines.map((machine) => machine.id),
8615
- ...heartbeats.map((heartbeat) => heartbeat.machine_id),
8616
- ...peers.keys()
8617
- ]);
8618
- const manifestById = new Map(manifest.machines.map((machine) => [machine.id, machine]));
8619
- const machines = [...machineIds].sort().map((machineId) => {
8620
- const manifestMachine = manifestById.get(machineId);
8621
- return buildEntry({
8622
- machineId,
8623
- localMachineId,
8624
- manifest: manifestMachine,
8625
- peer: findTailscalePeer(manifestMachine ?? null, machineId, peers),
8626
- heartbeat: heartbeatByMachine.get(machineId)
8627
- });
8628
- });
8629
- return {
8630
- generated_at: now.toISOString(),
8631
- local_machine_id: localMachineId,
8632
- local_hostname: hostname4(),
8633
- current_platform: normalizePlatform2(),
8634
- manifest_path_known: existsSync7(getManifestPath()),
8635
- machines,
8636
- warnings
8637
- };
8638
- }
8639
-
8640
8782
  // src/compatibility.ts
8641
8783
  init_db();
8642
8784
  var DEFAULT_COMMANDS = [
@@ -8646,7 +8788,7 @@ var DEFAULT_COMMANDS = [
8646
8788
  function defaultPackages() {
8647
8789
  return [{ name: "@hasna/machines", command: "machines", expectedVersion: getPackageVersion(), required: true }];
8648
8790
  }
8649
- function shellQuote4(value) {
8791
+ function shellQuote5(value) {
8650
8792
  return `'${value.replace(/'/g, "'\\''")}'`;
8651
8793
  }
8652
8794
  function commandId(value) {
@@ -8697,7 +8839,7 @@ function defaultRunner2(machineId, command) {
8697
8839
  return runMachineCommand(machineId, command);
8698
8840
  }
8699
8841
  function inspectCommand(machineId, spec, runner) {
8700
- const command = shellQuote4(spec.command);
8842
+ const command = shellQuote5(spec.command);
8701
8843
  const versionArgs = spec.versionArgs ?? "--version";
8702
8844
  const script = [
8703
8845
  `cmd=${command}`,
@@ -8726,7 +8868,7 @@ function fieldCommand(field) {
8726
8868
  }
8727
8869
  function inspectWorkspace(machineId, spec, runner) {
8728
8870
  const script = [
8729
- `path=${shellQuote4(spec.path)}`,
8871
+ `path=${shellQuote5(spec.path)}`,
8730
8872
  'printf "exists=%s\\n" "$(test -d "$path" && printf yes || printf no)"',
8731
8873
  'pkg="$path/package.json"',
8732
8874
  'printf "package_json=%s\\n" "$(test -f "$pkg" && printf yes || printf no)"',
@@ -8864,6 +9006,17 @@ function checkMachineCompatibility(options = {}) {
8864
9006
  fail: checks.filter((check) => check.status === "fail").length
8865
9007
  };
8866
9008
  return {
9009
+ schema_version: MACHINES_CONSUMER_CONTRACT_VERSION,
9010
+ package: {
9011
+ name: MACHINES_PACKAGE_NAME,
9012
+ version: getPackageVersion()
9013
+ },
9014
+ capabilities: {
9015
+ topology: true,
9016
+ compatibility: true,
9017
+ route_resolution: true,
9018
+ cli_json_fallback: true
9019
+ },
8867
9020
  ok: summary.fail === 0,
8868
9021
  machine_id: machineId,
8869
9022
  source: checks[0]?.source ?? "local",
@@ -10333,10 +10486,13 @@ function parsePackageSpec(value) {
10333
10486
  };
10334
10487
  }
10335
10488
  function parseWorkspaceSpec(value) {
10336
- const [label, path] = value.includes("=") ? value.split(/=(.*)/s).filter(Boolean) : ["workspace", value];
10489
+ const [label, rest] = value.includes("=") ? value.split(/=(.*)/s).filter(Boolean) : ["workspace", value];
10490
+ const [path, expectedPackageName, expectedVersion] = rest.split(":");
10337
10491
  return {
10338
10492
  label,
10339
10493
  path,
10494
+ expectedPackageName: expectedPackageName || undefined,
10495
+ expectedVersion: expectedVersion || undefined,
10340
10496
  required: true
10341
10497
  };
10342
10498
  }
@@ -10497,7 +10653,7 @@ program2.command("topology").description("Discover local, manifest, heartbeat, S
10497
10653
  console.log(`${machine.machine_id.padEnd(18)} ${String(machine.platform || "unknown").padEnd(8)} ${machine.heartbeat_status.padEnd(8)} ${route}`);
10498
10654
  }
10499
10655
  });
10500
- 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) => {
10656
+ 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) => {
10501
10657
  const result = checkMachineCompatibility({
10502
10658
  machineId: options.machine,
10503
10659
  commands: options.command?.map(parseCommandSpec),
@@ -10650,9 +10806,26 @@ program2.command("install-tailscale").description("Install Tailscale on a machin
10650
10806
  const result = options.apply ? runTailscaleInstall(options.machine, { apply: true, yes: options.yes }) : buildTailscaleInstallPlan(options.machine);
10651
10807
  console.log(JSON.stringify(result, null, 2));
10652
10808
  });
10809
+ 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) => {
10810
+ const topology = discoverMachineTopology({ includeTailscale: options.tailscale !== false });
10811
+ const resolved = resolveMachineRoute(options.machine, { topology });
10812
+ const command = resolved.ok && resolved.target ? resolved.route === "local" ? options.cmd ?? null : buildSshCommand(options.machine, options.cmd, { topology }) : null;
10813
+ const payload = { ...resolved, command };
10814
+ if (options.json) {
10815
+ console.log(JSON.stringify(payload, null, 2));
10816
+ return;
10817
+ }
10818
+ if (!resolved.ok) {
10819
+ console.error(source_default.red(resolved.warnings.join("; ") || `No route found for ${options.machine}`));
10820
+ process.exitCode = 1;
10821
+ return;
10822
+ }
10823
+ console.log(command ?? `${resolved.route}:${resolved.target}`);
10824
+ });
10653
10825
  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) => {
10654
10826
  if (options.json) {
10655
- console.log(JSON.stringify(resolveSshTarget(options.machine), null, 2));
10827
+ const resolved = resolveMachineRoute(options.machine);
10828
+ console.log(JSON.stringify({ resolved, command: resolved.ok ? buildSshCommand(options.machine, options.cmd) : null }, null, 2));
10656
10829
  return;
10657
10830
  }
10658
10831
  console.log(buildSshCommand(options.machine, options.cmd));