@hasna/machines 0.0.16 → 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/README.md +17 -5
- package/dist/cli/index.js +387 -217
- package/dist/commands/ssh.d.ts +6 -3
- package/dist/commands/ssh.d.ts.map +1 -1
- package/dist/compatibility.d.ts +4 -0
- package/dist/compatibility.d.ts.map +1 -1
- package/dist/consumer.d.ts +10 -0
- package/dist/consumer.d.ts.map +1 -0
- package/dist/consumer.js +4983 -0
- package/dist/index.js +223 -64
- package/dist/mcp/index.js +370 -217
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/remote.d.ts.map +1 -1
- package/dist/topology.d.ts +45 -1
- package/dist/topology.d.ts.map +1 -1
- package/package.json +6 -2
package/dist/cli/index.js
CHANGED
|
@@ -7573,48 +7573,365 @@ 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
|
|
7576
|
+
import { hostname as hostname4 } from "os";
|
|
7577
7577
|
|
|
7578
|
-
// src/
|
|
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
|
|
7671
|
+
function manifestHostReachable(target) {
|
|
7585
7672
|
const overrides = envReachableHosts();
|
|
7586
|
-
if (overrides.size
|
|
7587
|
-
return
|
|
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
|
-
|
|
7590
|
-
|
|
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
|
-
|
|
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
|
|
7595
|
-
const
|
|
7596
|
-
|
|
7597
|
-
|
|
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
|
|
7600
|
-
if (
|
|
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
|
-
|
|
7603
|
-
|
|
7604
|
-
|
|
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
|
|
7608
|
-
const
|
|
7609
|
-
const
|
|
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
|
-
|
|
7612
|
-
|
|
7613
|
-
|
|
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
|
|
7614
7931
|
};
|
|
7615
7932
|
}
|
|
7616
|
-
function buildSshCommand(machineId, remoteCommand) {
|
|
7617
|
-
const resolved = resolveSshTarget(machineId);
|
|
7933
|
+
function buildSshCommand(machineId, remoteCommand, options = {}) {
|
|
7934
|
+
const resolved = resolveSshTarget(machineId, options);
|
|
7618
7935
|
return remoteCommand ? `ssh ${resolved.target} ${JSON.stringify(remoteCommand)}` : `ssh ${resolved.target}`;
|
|
7619
7936
|
}
|
|
7620
7937
|
|
|
@@ -7623,7 +7940,7 @@ function shellQuote(value) {
|
|
|
7623
7940
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
7624
7941
|
}
|
|
7625
7942
|
function machineIsLocal(machineId, localMachineId) {
|
|
7626
|
-
return machineId === "local" || machineId === "localhost" || machineId === localMachineId || machineId ===
|
|
7943
|
+
return machineId === "local" || machineId === "localhost" || machineId === localMachineId || machineId === hostname4();
|
|
7627
7944
|
}
|
|
7628
7945
|
function resolveMachineCommand(machineId, command, localMachineId = getLocalMachineId()) {
|
|
7629
7946
|
if (machineIsLocal(machineId, localMachineId)) {
|
|
@@ -7635,7 +7952,8 @@ function resolveMachineCommand(machineId, command, localMachineId = getLocalMach
|
|
|
7635
7952
|
shellCommand: buildSshCommand(machineId, command)
|
|
7636
7953
|
};
|
|
7637
7954
|
} catch (error) {
|
|
7638
|
-
|
|
7955
|
+
const message = String(error.message ?? error);
|
|
7956
|
+
if (message.includes("Machine route not found") || message.includes("Machine not found in manifest")) {
|
|
7639
7957
|
return { source: "ssh", shellCommand: `ssh ${shellQuote(machineId)} ${shellQuote(command)}` };
|
|
7640
7958
|
}
|
|
7641
7959
|
throw error;
|
|
@@ -7968,7 +8286,7 @@ function runTailscaleInstall(machineId, options = {}) {
|
|
|
7968
8286
|
}
|
|
7969
8287
|
|
|
7970
8288
|
// src/commands/notifications.ts
|
|
7971
|
-
import { existsSync as
|
|
8289
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
7972
8290
|
init_paths();
|
|
7973
8291
|
var notificationChannelSchema = exports_external.object({
|
|
7974
8292
|
id: exports_external.string(),
|
|
@@ -7988,7 +8306,7 @@ function sortChannels(channels) {
|
|
|
7988
8306
|
function shellQuote3(value) {
|
|
7989
8307
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
7990
8308
|
}
|
|
7991
|
-
function
|
|
8309
|
+
function hasCommand2(binary) {
|
|
7992
8310
|
const result = Bun.spawnSync(["bash", "-lc", `command -v ${binary} >/dev/null 2>&1`], {
|
|
7993
8311
|
stdout: "ignore",
|
|
7994
8312
|
stderr: "ignore",
|
|
@@ -8013,7 +8331,7 @@ Content-Type: text/plain; charset=utf-8
|
|
|
8013
8331
|
|
|
8014
8332
|
${message}
|
|
8015
8333
|
`;
|
|
8016
|
-
if (
|
|
8334
|
+
if (hasCommand2("sendmail")) {
|
|
8017
8335
|
const result = Bun.spawnSync(["bash", "-lc", "sendmail -t"], {
|
|
8018
8336
|
stdin: new TextEncoder().encode(body),
|
|
8019
8337
|
stdout: "pipe",
|
|
@@ -8031,7 +8349,7 @@ ${message}
|
|
|
8031
8349
|
detail: `Delivered via sendmail to ${channel.target}`
|
|
8032
8350
|
};
|
|
8033
8351
|
}
|
|
8034
|
-
if (
|
|
8352
|
+
if (hasCommand2("mail")) {
|
|
8035
8353
|
const command = `printf %s ${shellQuote3(message)} | mail -s ${shellQuote3(subject)} ${shellQuote3(channel.target)}`;
|
|
8036
8354
|
const result = Bun.spawnSync(["bash", "-lc", command], {
|
|
8037
8355
|
stdout: "pipe",
|
|
@@ -8125,7 +8443,7 @@ function getDefaultNotificationConfig() {
|
|
|
8125
8443
|
};
|
|
8126
8444
|
}
|
|
8127
8445
|
function readNotificationConfig(path = getNotificationsPath()) {
|
|
8128
|
-
if (!
|
|
8446
|
+
if (!existsSync6(path)) {
|
|
8129
8447
|
return getDefaultNotificationConfig();
|
|
8130
8448
|
}
|
|
8131
8449
|
return notificationConfigSchema.parse(JSON.parse(readFileSync4(path, "utf8")));
|
|
@@ -8271,7 +8589,7 @@ function listPorts(machineId) {
|
|
|
8271
8589
|
}
|
|
8272
8590
|
|
|
8273
8591
|
// src/commands/sync.ts
|
|
8274
|
-
import { existsSync as
|
|
8592
|
+
import { existsSync as existsSync7, lstatSync, readFileSync as readFileSync5, symlinkSync, copyFileSync } from "fs";
|
|
8275
8593
|
init_paths();
|
|
8276
8594
|
init_db();
|
|
8277
8595
|
function quote4(value) {
|
|
@@ -8321,8 +8639,8 @@ function detectPackageActions(machine) {
|
|
|
8321
8639
|
}
|
|
8322
8640
|
function detectFileActions(machine) {
|
|
8323
8641
|
return (machine.files || []).map((file, index) => {
|
|
8324
|
-
const sourceExists =
|
|
8325
|
-
const targetExists =
|
|
8642
|
+
const sourceExists = existsSync7(file.source);
|
|
8643
|
+
const targetExists = existsSync7(file.target);
|
|
8326
8644
|
let status = "missing";
|
|
8327
8645
|
if (sourceExists && targetExists) {
|
|
8328
8646
|
if (file.mode === "symlink") {
|
|
@@ -8458,185 +8776,6 @@ function getStatus() {
|
|
|
8458
8776
|
};
|
|
8459
8777
|
}
|
|
8460
8778
|
|
|
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
8779
|
// src/compatibility.ts
|
|
8641
8780
|
init_db();
|
|
8642
8781
|
var DEFAULT_COMMANDS = [
|
|
@@ -8864,6 +9003,17 @@ function checkMachineCompatibility(options = {}) {
|
|
|
8864
9003
|
fail: checks.filter((check) => check.status === "fail").length
|
|
8865
9004
|
};
|
|
8866
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
|
+
},
|
|
8867
9017
|
ok: summary.fail === 0,
|
|
8868
9018
|
machine_id: machineId,
|
|
8869
9019
|
source: checks[0]?.source ?? "local",
|
|
@@ -10333,10 +10483,13 @@ function parsePackageSpec(value) {
|
|
|
10333
10483
|
};
|
|
10334
10484
|
}
|
|
10335
10485
|
function parseWorkspaceSpec(value) {
|
|
10336
|
-
const [label,
|
|
10486
|
+
const [label, rest] = value.includes("=") ? value.split(/=(.*)/s).filter(Boolean) : ["workspace", value];
|
|
10487
|
+
const [path, expectedPackageName, expectedVersion] = rest.split(":");
|
|
10337
10488
|
return {
|
|
10338
10489
|
label,
|
|
10339
10490
|
path,
|
|
10491
|
+
expectedPackageName: expectedPackageName || undefined,
|
|
10492
|
+
expectedVersion: expectedVersion || undefined,
|
|
10340
10493
|
required: true
|
|
10341
10494
|
};
|
|
10342
10495
|
}
|
|
@@ -10497,7 +10650,7 @@ program2.command("topology").description("Discover local, manifest, heartbeat, S
|
|
|
10497
10650
|
console.log(`${machine.machine_id.padEnd(18)} ${String(machine.platform || "unknown").padEnd(8)} ${machine.heartbeat_status.padEnd(8)} ${route}`);
|
|
10498
10651
|
}
|
|
10499
10652
|
});
|
|
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) => {
|
|
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) => {
|
|
10501
10654
|
const result = checkMachineCompatibility({
|
|
10502
10655
|
machineId: options.machine,
|
|
10503
10656
|
commands: options.command?.map(parseCommandSpec),
|
|
@@ -10650,9 +10803,26 @@ program2.command("install-tailscale").description("Install Tailscale on a machin
|
|
|
10650
10803
|
const result = options.apply ? runTailscaleInstall(options.machine, { apply: true, yes: options.yes }) : buildTailscaleInstallPlan(options.machine);
|
|
10651
10804
|
console.log(JSON.stringify(result, null, 2));
|
|
10652
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
|
+
});
|
|
10653
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) => {
|
|
10654
10823
|
if (options.json) {
|
|
10655
|
-
|
|
10824
|
+
const resolved = resolveMachineRoute(options.machine);
|
|
10825
|
+
console.log(JSON.stringify({ resolved, command: resolved.ok ? buildSshCommand(options.machine, options.cmd) : null }, null, 2));
|
|
10656
10826
|
return;
|
|
10657
10827
|
}
|
|
10658
10828
|
console.log(buildSshCommand(options.machine, options.cmd));
|