@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/README.md +17 -5
- package/dist/cli/index.js +417 -227
- 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 +256 -77
- package/dist/mcp/index.js +400 -227
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/remote.d.ts +5 -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/dist/types.d.ts +3 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -2
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/
|
|
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
|
|
7671
|
+
function manifestHostReachable(target) {
|
|
7584
7672
|
const overrides = envReachableHosts();
|
|
7585
|
-
if (overrides.size
|
|
7586
|
-
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) });
|
|
7587
7687
|
}
|
|
7588
|
-
const
|
|
7589
|
-
|
|
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
|
-
|
|
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
|
|
7594
|
-
const
|
|
7595
|
-
|
|
7596
|
-
|
|
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
|
|
7599
|
-
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}`);
|
|
7600
7864
|
return {
|
|
7601
|
-
|
|
7602
|
-
|
|
7603
|
-
|
|
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
|
|
7607
|
-
const
|
|
7608
|
-
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;
|
|
7609
7891
|
return {
|
|
7610
|
-
|
|
7611
|
-
|
|
7612
|
-
|
|
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
|
|
7623
|
-
const
|
|
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:
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
8306
|
+
function shellQuote3(value) {
|
|
7969
8307
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
7970
8308
|
}
|
|
7971
|
-
function
|
|
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 (
|
|
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 (
|
|
8015
|
-
const command = `printf %s ${
|
|
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 (!
|
|
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
|
|
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 =
|
|
8305
|
-
const targetExists =
|
|
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
|
|
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 =
|
|
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=${
|
|
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,
|
|
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
|
-
|
|
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));
|