@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/README.md +17 -5
- package/dist/cli/index.js +400 -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 +4986 -0
- package/dist/index.js +235 -73
- package/dist/mcp/index.js +383 -227
- 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,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
|
|
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 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} ${
|
|
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
|
|
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 ===
|
|
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
|
-
|
|
7639
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
8309
|
+
function shellQuote4(value) {
|
|
7989
8310
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
7990
8311
|
}
|
|
7991
|
-
function
|
|
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 (
|
|
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 (
|
|
8035
|
-
const command = `printf %s ${
|
|
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 (!
|
|
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
|
|
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 =
|
|
8325
|
-
const targetExists =
|
|
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
|
|
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 =
|
|
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=${
|
|
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,
|
|
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
|
-
|
|
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));
|