@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/index.js
CHANGED
|
@@ -11085,9 +11085,31 @@ function detectCurrentMachineManifest() {
|
|
|
11085
11085
|
};
|
|
11086
11086
|
}
|
|
11087
11087
|
// src/topology.ts
|
|
11088
|
-
import { existsSync as
|
|
11088
|
+
import { existsSync as existsSync4 } from "fs";
|
|
11089
11089
|
import { arch as arch2, hostname as hostname3, platform as platform2, userInfo as userInfo2 } from "os";
|
|
11090
11090
|
import { spawnSync } from "child_process";
|
|
11091
|
+
|
|
11092
|
+
// src/version.ts
|
|
11093
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
11094
|
+
import { dirname as dirname3, join as join2 } from "path";
|
|
11095
|
+
import { fileURLToPath } from "url";
|
|
11096
|
+
function getPackageVersion() {
|
|
11097
|
+
try {
|
|
11098
|
+
const here = dirname3(fileURLToPath(import.meta.url));
|
|
11099
|
+
const candidates = [join2(here, "..", "package.json"), join2(here, "..", "..", "package.json")];
|
|
11100
|
+
const pkgPath = candidates.find((candidate) => existsSync3(candidate));
|
|
11101
|
+
if (!pkgPath) {
|
|
11102
|
+
return "0.0.0";
|
|
11103
|
+
}
|
|
11104
|
+
return JSON.parse(readFileSync2(pkgPath, "utf8")).version || "0.0.0";
|
|
11105
|
+
} catch {
|
|
11106
|
+
return "0.0.0";
|
|
11107
|
+
}
|
|
11108
|
+
}
|
|
11109
|
+
|
|
11110
|
+
// src/topology.ts
|
|
11111
|
+
var MACHINES_CONSUMER_CONTRACT_VERSION = 1;
|
|
11112
|
+
var MACHINES_PACKAGE_NAME = "@hasna/machines";
|
|
11091
11113
|
function normalizePlatform2(value = platform2()) {
|
|
11092
11114
|
const normalized = value.toLowerCase();
|
|
11093
11115
|
if (normalized === "darwin" || normalized === "macos")
|
|
@@ -11169,16 +11191,26 @@ function findTailscalePeer(machine, machineId, peers) {
|
|
|
11169
11191
|
}
|
|
11170
11192
|
return peers.get(machineId) ?? null;
|
|
11171
11193
|
}
|
|
11194
|
+
function envReachableHosts() {
|
|
11195
|
+
const raw = process.env["HASNA_MACHINES_REACHABLE_HOSTS"];
|
|
11196
|
+
return new Set((raw || "").split(",").map((value) => value.trim()).filter(Boolean));
|
|
11197
|
+
}
|
|
11198
|
+
function manifestHostReachable(target) {
|
|
11199
|
+
const overrides = envReachableHosts();
|
|
11200
|
+
if (overrides.size === 0)
|
|
11201
|
+
return null;
|
|
11202
|
+
return overrides.has(target);
|
|
11203
|
+
}
|
|
11172
11204
|
function routeHints(input) {
|
|
11173
11205
|
const hints = [];
|
|
11174
11206
|
if (input.machineId === input.localMachineId) {
|
|
11175
11207
|
hints.push({ kind: "local", target: "localhost", reachable: true });
|
|
11176
11208
|
}
|
|
11177
11209
|
if (input.manifest?.sshAddress) {
|
|
11178
|
-
hints.push({ kind: "ssh", target: input.manifest.sshAddress, reachable:
|
|
11210
|
+
hints.push({ kind: "ssh", target: input.manifest.sshAddress, reachable: manifestHostReachable(input.manifest.sshAddress) });
|
|
11179
11211
|
}
|
|
11180
11212
|
if (input.manifest?.hostname) {
|
|
11181
|
-
hints.push({ kind: "lan", target: input.manifest.hostname, reachable:
|
|
11213
|
+
hints.push({ kind: "lan", target: input.manifest.hostname, reachable: manifestHostReachable(input.manifest.hostname) });
|
|
11182
11214
|
}
|
|
11183
11215
|
const tailscaleTarget = input.manifest?.tailscaleName ?? input.peer?.DNSName ?? input.peer?.TailscaleIPs?.[0];
|
|
11184
11216
|
if (tailscaleTarget) {
|
|
@@ -11186,6 +11218,28 @@ function routeHints(input) {
|
|
|
11186
11218
|
}
|
|
11187
11219
|
return hints;
|
|
11188
11220
|
}
|
|
11221
|
+
function routeRank(hint) {
|
|
11222
|
+
if (hint.kind === "local")
|
|
11223
|
+
return 0;
|
|
11224
|
+
if (hint.reachable === true && hint.kind === "ssh")
|
|
11225
|
+
return 1;
|
|
11226
|
+
if (hint.reachable === true && hint.kind === "lan")
|
|
11227
|
+
return 2;
|
|
11228
|
+
if (hint.reachable === true && hint.kind === "tailscale")
|
|
11229
|
+
return 3;
|
|
11230
|
+
if (hint.reachable === false)
|
|
11231
|
+
return 8;
|
|
11232
|
+
if (hint.kind === "ssh")
|
|
11233
|
+
return 4;
|
|
11234
|
+
if (hint.kind === "lan")
|
|
11235
|
+
return 5;
|
|
11236
|
+
if (hint.kind === "tailscale")
|
|
11237
|
+
return 6;
|
|
11238
|
+
return 9;
|
|
11239
|
+
}
|
|
11240
|
+
function selectRouteHint(hints) {
|
|
11241
|
+
return [...hints].sort((left, right) => routeRank(left) - routeRank(right))[0] ?? null;
|
|
11242
|
+
}
|
|
11189
11243
|
function buildEntry(input) {
|
|
11190
11244
|
const manifest = input.manifest;
|
|
11191
11245
|
const peer = input.peer;
|
|
@@ -11195,8 +11249,8 @@ function buildEntry(input) {
|
|
|
11195
11249
|
manifest,
|
|
11196
11250
|
peer
|
|
11197
11251
|
});
|
|
11198
|
-
const selectedRoute =
|
|
11199
|
-
const route = selectedRoute?.kind === "ssh" ? "
|
|
11252
|
+
const selectedRoute = selectRouteHint(hints);
|
|
11253
|
+
const route = selectedRoute?.kind === "ssh" ? "ssh" : selectedRoute?.kind ?? "unknown";
|
|
11200
11254
|
return {
|
|
11201
11255
|
machine_id: input.machineId,
|
|
11202
11256
|
hostname: manifest?.hostname ?? peer?.HostName ?? null,
|
|
@@ -11251,15 +11305,140 @@ function discoverMachineTopology(options = {}) {
|
|
|
11251
11305
|
});
|
|
11252
11306
|
});
|
|
11253
11307
|
return {
|
|
11308
|
+
schema_version: MACHINES_CONSUMER_CONTRACT_VERSION,
|
|
11309
|
+
package: {
|
|
11310
|
+
name: MACHINES_PACKAGE_NAME,
|
|
11311
|
+
version: getPackageVersion()
|
|
11312
|
+
},
|
|
11313
|
+
capabilities: {
|
|
11314
|
+
topology: true,
|
|
11315
|
+
compatibility: true,
|
|
11316
|
+
route_resolution: true,
|
|
11317
|
+
cli_json_fallback: true
|
|
11318
|
+
},
|
|
11254
11319
|
generated_at: now.toISOString(),
|
|
11255
11320
|
local_machine_id: localMachineId,
|
|
11256
11321
|
local_hostname: hostname3(),
|
|
11257
11322
|
current_platform: normalizePlatform2(),
|
|
11258
|
-
manifest_path_known:
|
|
11323
|
+
manifest_path_known: existsSync4(getManifestPath()),
|
|
11259
11324
|
machines,
|
|
11260
11325
|
warnings
|
|
11261
11326
|
};
|
|
11262
11327
|
}
|
|
11328
|
+
function normalizeMachineAlias(value) {
|
|
11329
|
+
return value.trim().replace(/\.$/, "").toLowerCase();
|
|
11330
|
+
}
|
|
11331
|
+
function routeTargetMatches(machine, requested) {
|
|
11332
|
+
const normalized = normalizeMachineAlias(requested);
|
|
11333
|
+
const values = [
|
|
11334
|
+
machine.ssh.address,
|
|
11335
|
+
machine.ssh.command_target,
|
|
11336
|
+
machine.tailscale.dns_name,
|
|
11337
|
+
machine.tailscale.dns_name?.split(".")[0],
|
|
11338
|
+
...machine.tailscale.ips,
|
|
11339
|
+
...machine.route_hints.map((hint) => hint.target),
|
|
11340
|
+
...machine.route_hints.map((hint) => hint.target.split("@").pop() ?? hint.target)
|
|
11341
|
+
].filter((value) => Boolean(value));
|
|
11342
|
+
return values.some((value) => normalizeMachineAlias(value) === normalized);
|
|
11343
|
+
}
|
|
11344
|
+
function findRouteMachine(topology, requestedMachineId) {
|
|
11345
|
+
const requested = normalizeMachineAlias(requestedMachineId);
|
|
11346
|
+
if (requested === "local" || requested === "localhost" || requested === normalizeMachineAlias(hostname3()) || requested === normalizeMachineAlias(topology.local_machine_id)) {
|
|
11347
|
+
return {
|
|
11348
|
+
machine: topology.machines.find((machine) => machine.machine_id === topology.local_machine_id) ?? null,
|
|
11349
|
+
matchedBy: "local_alias"
|
|
11350
|
+
};
|
|
11351
|
+
}
|
|
11352
|
+
const machineIdMatch = topology.machines.find((machine) => normalizeMachineAlias(machine.machine_id) === requested);
|
|
11353
|
+
if (machineIdMatch)
|
|
11354
|
+
return { machine: machineIdMatch, matchedBy: "machine_id" };
|
|
11355
|
+
const hostnameMatch = topology.machines.find((machine) => machine.hostname && normalizeMachineAlias(machine.hostname) === requested);
|
|
11356
|
+
if (hostnameMatch)
|
|
11357
|
+
return { machine: hostnameMatch, matchedBy: "hostname" };
|
|
11358
|
+
const tailscaleMatch = topology.machines.find((machine) => {
|
|
11359
|
+
if (!machine.tailscale.dns_name)
|
|
11360
|
+
return false;
|
|
11361
|
+
const dns = normalizeMachineAlias(machine.tailscale.dns_name);
|
|
11362
|
+
return dns === requested || dns.split(".")[0] === requested;
|
|
11363
|
+
});
|
|
11364
|
+
if (tailscaleMatch)
|
|
11365
|
+
return { machine: tailscaleMatch, matchedBy: "tailscale" };
|
|
11366
|
+
const routeMatch = topology.machines.find((machine) => routeTargetMatches(machine, requestedMachineId));
|
|
11367
|
+
if (routeMatch)
|
|
11368
|
+
return { machine: routeMatch, matchedBy: "route_target" };
|
|
11369
|
+
return { machine: null, matchedBy: null };
|
|
11370
|
+
}
|
|
11371
|
+
function routeConfidence(input) {
|
|
11372
|
+
if (input.matchedBy === "local_alias")
|
|
11373
|
+
return "exact";
|
|
11374
|
+
if (input.hint?.kind === "local")
|
|
11375
|
+
return "exact";
|
|
11376
|
+
if (input.hint?.reachable === true)
|
|
11377
|
+
return "high";
|
|
11378
|
+
if (input.machine.manifest_declared && (input.hint?.kind === "ssh" || input.hint?.kind === "lan"))
|
|
11379
|
+
return "medium";
|
|
11380
|
+
if (input.hint)
|
|
11381
|
+
return "low";
|
|
11382
|
+
return "none";
|
|
11383
|
+
}
|
|
11384
|
+
function resolveMachineRoute(machineId, options = {}) {
|
|
11385
|
+
const topology = options.topology ?? discoverMachineTopology(options);
|
|
11386
|
+
const warnings = [...topology.warnings];
|
|
11387
|
+
const { machine, matchedBy } = findRouteMachine(topology, machineId);
|
|
11388
|
+
const generatedAt = (options.now ?? new Date).toISOString();
|
|
11389
|
+
if (!machine) {
|
|
11390
|
+
warnings.push(`machine_not_found:${machineId}`);
|
|
11391
|
+
return {
|
|
11392
|
+
schema_version: MACHINES_CONSUMER_CONTRACT_VERSION,
|
|
11393
|
+
package: { name: MACHINES_PACKAGE_NAME, version: getPackageVersion() },
|
|
11394
|
+
ok: false,
|
|
11395
|
+
machine_id: null,
|
|
11396
|
+
requested_machine_id: machineId,
|
|
11397
|
+
generated_at: generatedAt,
|
|
11398
|
+
route: "unknown",
|
|
11399
|
+
source: "unknown",
|
|
11400
|
+
target: null,
|
|
11401
|
+
command_target: null,
|
|
11402
|
+
confidence: "none",
|
|
11403
|
+
local: false,
|
|
11404
|
+
evidence: {
|
|
11405
|
+
topology: true,
|
|
11406
|
+
matched_by: null,
|
|
11407
|
+
manifest_declared: null,
|
|
11408
|
+
heartbeat_status: null,
|
|
11409
|
+
tailscale_online: null,
|
|
11410
|
+
selected_hint: null
|
|
11411
|
+
},
|
|
11412
|
+
warnings
|
|
11413
|
+
};
|
|
11414
|
+
}
|
|
11415
|
+
const selectedHint = selectRouteHint(machine.route_hints);
|
|
11416
|
+
const route = selectedHint?.kind ?? machine.ssh.route ?? "unknown";
|
|
11417
|
+
const local = route === "local" || machine.machine_id === topology.local_machine_id;
|
|
11418
|
+
return {
|
|
11419
|
+
schema_version: MACHINES_CONSUMER_CONTRACT_VERSION,
|
|
11420
|
+
package: topology.package,
|
|
11421
|
+
ok: Boolean(selectedHint?.target),
|
|
11422
|
+
machine_id: machine.machine_id,
|
|
11423
|
+
requested_machine_id: machineId,
|
|
11424
|
+
generated_at: generatedAt,
|
|
11425
|
+
route,
|
|
11426
|
+
source: route,
|
|
11427
|
+
target: selectedHint?.target ?? null,
|
|
11428
|
+
command_target: selectedHint?.target ?? null,
|
|
11429
|
+
confidence: routeConfidence({ machine, hint: selectedHint, matchedBy }),
|
|
11430
|
+
local,
|
|
11431
|
+
evidence: {
|
|
11432
|
+
topology: true,
|
|
11433
|
+
matched_by: matchedBy,
|
|
11434
|
+
manifest_declared: machine.manifest_declared,
|
|
11435
|
+
heartbeat_status: machine.heartbeat_status,
|
|
11436
|
+
tailscale_online: machine.tailscale.online,
|
|
11437
|
+
selected_hint: selectedHint
|
|
11438
|
+
},
|
|
11439
|
+
warnings
|
|
11440
|
+
};
|
|
11441
|
+
}
|
|
11263
11442
|
function getLocalMachineTopology(options = {}) {
|
|
11264
11443
|
const topology = discoverMachineTopology(options);
|
|
11265
11444
|
return topology.machines.find((machine) => machine.machine_id === topology.local_machine_id) ?? {
|
|
@@ -11280,54 +11459,36 @@ function getLocalMachineTopology(options = {}) {
|
|
|
11280
11459
|
};
|
|
11281
11460
|
}
|
|
11282
11461
|
// src/remote.ts
|
|
11283
|
-
import { spawnSync as
|
|
11462
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
11284
11463
|
import { hostname as hostname4 } from "os";
|
|
11285
11464
|
|
|
11286
11465
|
// src/commands/ssh.ts
|
|
11287
|
-
|
|
11288
|
-
|
|
11289
|
-
const raw = process.env["HASNA_MACHINES_REACHABLE_HOSTS"];
|
|
11290
|
-
return new Set((raw || "").split(",").map((value) => value.trim()).filter(Boolean));
|
|
11291
|
-
}
|
|
11292
|
-
function isReachable(host) {
|
|
11293
|
-
const overrides = envReachableHosts();
|
|
11294
|
-
if (overrides.size > 0) {
|
|
11295
|
-
return overrides.has(host);
|
|
11296
|
-
}
|
|
11297
|
-
const probe = spawnSync2("bash", ["-lc", `getent hosts ${host} >/dev/null 2>&1 || ping -c 1 -W 1 ${host} >/dev/null 2>&1`], {
|
|
11298
|
-
stdio: "ignore"
|
|
11299
|
-
});
|
|
11300
|
-
return probe.status === 0;
|
|
11466
|
+
function shellQuote(value) {
|
|
11467
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
11301
11468
|
}
|
|
11302
|
-
function resolveSshTarget(machineId) {
|
|
11303
|
-
const
|
|
11304
|
-
if (!
|
|
11305
|
-
throw new Error(`Machine not found
|
|
11469
|
+
function resolveSshTarget(machineId, options = {}) {
|
|
11470
|
+
const resolved = resolveMachineRoute(machineId, options);
|
|
11471
|
+
if (!resolved.ok || !resolved.target) {
|
|
11472
|
+
throw new Error(`Machine route not found: ${machineId}`);
|
|
11306
11473
|
}
|
|
11307
|
-
|
|
11308
|
-
|
|
11309
|
-
return {
|
|
11310
|
-
machineId,
|
|
11311
|
-
target: "localhost",
|
|
11312
|
-
route: "local"
|
|
11313
|
-
};
|
|
11474
|
+
if (resolved.route !== "local" && resolved.route !== "lan" && resolved.route !== "tailscale" && resolved.route !== "ssh") {
|
|
11475
|
+
throw new Error(`Machine route is not SSH-capable: ${machineId}`);
|
|
11314
11476
|
}
|
|
11315
|
-
const lanTarget = machine.sshAddress || machine.hostname || machine.id;
|
|
11316
|
-
const tailscaleTarget = machine.tailscaleName || machine.hostname || machine.id;
|
|
11317
|
-
const route = isReachable(lanTarget) ? "lan" : "tailscale";
|
|
11318
11477
|
return {
|
|
11319
|
-
machineId,
|
|
11320
|
-
target:
|
|
11321
|
-
route
|
|
11478
|
+
machineId: resolved.machine_id ?? machineId,
|
|
11479
|
+
target: resolved.target,
|
|
11480
|
+
route: resolved.route,
|
|
11481
|
+
confidence: resolved.confidence,
|
|
11482
|
+
warnings: resolved.warnings
|
|
11322
11483
|
};
|
|
11323
11484
|
}
|
|
11324
|
-
function buildSshCommand(machineId, remoteCommand) {
|
|
11325
|
-
const resolved = resolveSshTarget(machineId);
|
|
11326
|
-
return remoteCommand ? `ssh ${resolved.target} ${
|
|
11485
|
+
function buildSshCommand(machineId, remoteCommand, options = {}) {
|
|
11486
|
+
const resolved = resolveSshTarget(machineId, options);
|
|
11487
|
+
return remoteCommand ? `ssh ${resolved.target} ${shellQuote(remoteCommand)}` : `ssh ${resolved.target}`;
|
|
11327
11488
|
}
|
|
11328
11489
|
|
|
11329
11490
|
// src/remote.ts
|
|
11330
|
-
function
|
|
11491
|
+
function shellQuote2(value) {
|
|
11331
11492
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
11332
11493
|
}
|
|
11333
11494
|
function machineIsLocal(machineId, localMachineId) {
|
|
@@ -11343,15 +11504,16 @@ function resolveMachineCommand(machineId, command, localMachineId = getLocalMach
|
|
|
11343
11504
|
shellCommand: buildSshCommand(machineId, command)
|
|
11344
11505
|
};
|
|
11345
11506
|
} catch (error) {
|
|
11346
|
-
|
|
11347
|
-
|
|
11507
|
+
const message = String(error.message ?? error);
|
|
11508
|
+
if (message.includes("Machine route not found") || message.includes("Machine not found in manifest")) {
|
|
11509
|
+
return { source: "ssh", shellCommand: `ssh ${shellQuote2(machineId)} ${shellQuote2(command)}` };
|
|
11348
11510
|
}
|
|
11349
11511
|
throw error;
|
|
11350
11512
|
}
|
|
11351
11513
|
}
|
|
11352
11514
|
function runMachineCommand(machineId, command) {
|
|
11353
11515
|
const resolved = resolveMachineCommand(machineId, command);
|
|
11354
|
-
const result =
|
|
11516
|
+
const result = spawnSync2("bash", ["-c", resolved.shellCommand], {
|
|
11355
11517
|
encoding: "utf8",
|
|
11356
11518
|
env: process.env
|
|
11357
11519
|
});
|
|
@@ -11364,24 +11526,6 @@ function runMachineCommand(machineId, command) {
|
|
|
11364
11526
|
};
|
|
11365
11527
|
}
|
|
11366
11528
|
|
|
11367
|
-
// src/version.ts
|
|
11368
|
-
import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
|
|
11369
|
-
import { dirname as dirname3, join as join2 } from "path";
|
|
11370
|
-
import { fileURLToPath } from "url";
|
|
11371
|
-
function getPackageVersion() {
|
|
11372
|
-
try {
|
|
11373
|
-
const here = dirname3(fileURLToPath(import.meta.url));
|
|
11374
|
-
const candidates = [join2(here, "..", "package.json"), join2(here, "..", "..", "package.json")];
|
|
11375
|
-
const pkgPath = candidates.find((candidate) => existsSync4(candidate));
|
|
11376
|
-
if (!pkgPath) {
|
|
11377
|
-
return "0.0.0";
|
|
11378
|
-
}
|
|
11379
|
-
return JSON.parse(readFileSync2(pkgPath, "utf8")).version || "0.0.0";
|
|
11380
|
-
} catch {
|
|
11381
|
-
return "0.0.0";
|
|
11382
|
-
}
|
|
11383
|
-
}
|
|
11384
|
-
|
|
11385
11529
|
// src/compatibility.ts
|
|
11386
11530
|
var DEFAULT_COMMANDS = [
|
|
11387
11531
|
{ command: "bun", required: true },
|
|
@@ -11390,7 +11534,7 @@ var DEFAULT_COMMANDS = [
|
|
|
11390
11534
|
function defaultPackages() {
|
|
11391
11535
|
return [{ name: "@hasna/machines", command: "machines", expectedVersion: getPackageVersion(), required: true }];
|
|
11392
11536
|
}
|
|
11393
|
-
function
|
|
11537
|
+
function shellQuote3(value) {
|
|
11394
11538
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
11395
11539
|
}
|
|
11396
11540
|
function commandId(value) {
|
|
@@ -11441,7 +11585,7 @@ function defaultRunner2(machineId, command) {
|
|
|
11441
11585
|
return runMachineCommand(machineId, command);
|
|
11442
11586
|
}
|
|
11443
11587
|
function inspectCommand(machineId, spec, runner) {
|
|
11444
|
-
const command =
|
|
11588
|
+
const command = shellQuote3(spec.command);
|
|
11445
11589
|
const versionArgs = spec.versionArgs ?? "--version";
|
|
11446
11590
|
const script = [
|
|
11447
11591
|
`cmd=${command}`,
|
|
@@ -11470,7 +11614,7 @@ function fieldCommand(field) {
|
|
|
11470
11614
|
}
|
|
11471
11615
|
function inspectWorkspace(machineId, spec, runner) {
|
|
11472
11616
|
const script = [
|
|
11473
|
-
`path=${
|
|
11617
|
+
`path=${shellQuote3(spec.path)}`,
|
|
11474
11618
|
'printf "exists=%s\\n" "$(test -d "$path" && printf yes || printf no)"',
|
|
11475
11619
|
'pkg="$path/package.json"',
|
|
11476
11620
|
'printf "package_json=%s\\n" "$(test -f "$pkg" && printf yes || printf no)"',
|
|
@@ -11608,6 +11752,17 @@ function checkMachineCompatibility(options = {}) {
|
|
|
11608
11752
|
fail: checks.filter((check) => check.status === "fail").length
|
|
11609
11753
|
};
|
|
11610
11754
|
return {
|
|
11755
|
+
schema_version: MACHINES_CONSUMER_CONTRACT_VERSION,
|
|
11756
|
+
package: {
|
|
11757
|
+
name: MACHINES_PACKAGE_NAME,
|
|
11758
|
+
version: getPackageVersion()
|
|
11759
|
+
},
|
|
11760
|
+
capabilities: {
|
|
11761
|
+
topology: true,
|
|
11762
|
+
compatibility: true,
|
|
11763
|
+
route_resolution: true,
|
|
11764
|
+
cli_json_fallback: true
|
|
11765
|
+
},
|
|
11611
11766
|
ok: summary.fail === 0,
|
|
11612
11767
|
machine_id: machineId,
|
|
11613
11768
|
source: checks[0]?.source ?? "local",
|
|
@@ -11715,7 +11870,7 @@ function getAppManager(machine, app) {
|
|
|
11715
11870
|
return "winget";
|
|
11716
11871
|
return "apt";
|
|
11717
11872
|
}
|
|
11718
|
-
function
|
|
11873
|
+
function shellQuote4(value) {
|
|
11719
11874
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
11720
11875
|
}
|
|
11721
11876
|
function buildAppCommand(machine, app) {
|
|
@@ -11736,7 +11891,7 @@ function buildAppCommand(machine, app) {
|
|
|
11736
11891
|
return `sudo apt-get install -y ${packageName}`;
|
|
11737
11892
|
}
|
|
11738
11893
|
function buildAppProbeCommand(machine, app) {
|
|
11739
|
-
const packageName =
|
|
11894
|
+
const packageName = shellQuote4(getPackageName(app));
|
|
11740
11895
|
const manager = getAppManager(machine, app);
|
|
11741
11896
|
if (manager === "custom") {
|
|
11742
11897
|
return `if command -v ${packageName} >/dev/null 2>&1; then printf 'installed=1\\nversion=custom\\n'; else printf 'installed=0\\n'; fi`;
|
|
@@ -12272,7 +12427,7 @@ var notificationConfigSchema = exports_external.object({
|
|
|
12272
12427
|
function sortChannels(channels) {
|
|
12273
12428
|
return [...channels].sort((left, right) => left.id.localeCompare(right.id));
|
|
12274
12429
|
}
|
|
12275
|
-
function
|
|
12430
|
+
function shellQuote5(value) {
|
|
12276
12431
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
12277
12432
|
}
|
|
12278
12433
|
function hasCommand2(binary) {
|
|
@@ -12319,7 +12474,7 @@ ${message}
|
|
|
12319
12474
|
};
|
|
12320
12475
|
}
|
|
12321
12476
|
if (hasCommand2("mail")) {
|
|
12322
|
-
const command = `printf %s ${
|
|
12477
|
+
const command = `printf %s ${shellQuote5(message)} | mail -s ${shellQuote5(subject)} ${shellQuote5(channel.target)}`;
|
|
12323
12478
|
const result = Bun.spawnSync(["bash", "-lc", command], {
|
|
12324
12479
|
stdout: "pipe",
|
|
12325
12480
|
stderr: "pipe",
|
|
@@ -12502,7 +12657,7 @@ async function testNotificationChannel(channelId, event = "manual.test", message
|
|
|
12502
12657
|
};
|
|
12503
12658
|
}
|
|
12504
12659
|
// src/commands/ports.ts
|
|
12505
|
-
import { spawnSync as
|
|
12660
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
12506
12661
|
function parseSsOutput(output) {
|
|
12507
12662
|
return output.trim().split(`
|
|
12508
12663
|
`).map((line) => line.trim()).filter(Boolean).map((line) => {
|
|
@@ -12544,7 +12699,7 @@ function listPorts(machineId) {
|
|
|
12544
12699
|
const isLocal = targetMachineId === getLocalMachineId();
|
|
12545
12700
|
const localCommand = "if command -v ss >/dev/null 2>&1; then ss -ltnpH; else lsof -nP -iTCP -sTCP:LISTEN; fi";
|
|
12546
12701
|
const command = isLocal ? localCommand : buildSshCommand(targetMachineId, localCommand);
|
|
12547
|
-
const result =
|
|
12702
|
+
const result = spawnSync3("bash", ["-lc", command], { encoding: "utf8" });
|
|
12548
12703
|
if (result.status !== 0) {
|
|
12549
12704
|
throw new Error(result.stderr || `Failed to list ports for ${targetMachineId}`);
|
|
12550
12705
|
}
|
|
@@ -22072,6 +22227,7 @@ var MACHINE_MCP_TOOL_NAMES = [
|
|
|
22072
22227
|
"machines_install_claude_diff",
|
|
22073
22228
|
"machines_install_claude_preview",
|
|
22074
22229
|
"machines_install_claude_apply",
|
|
22230
|
+
"machines_route_resolve",
|
|
22075
22231
|
"machines_ssh_resolve",
|
|
22076
22232
|
"machines_ports",
|
|
22077
22233
|
"machines_backup_preview",
|
|
@@ -22182,8 +22338,11 @@ function createMcpServer(version2) {
|
|
|
22182
22338
|
}));
|
|
22183
22339
|
server.tool("machines_install_tailscale_preview", "Preview Tailscale install steps for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(buildTailscaleInstallPlan(machine_id), null, 2) }] }));
|
|
22184
22340
|
server.tool("machines_install_tailscale_apply", "Execute Tailscale install steps for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier"), yes: exports_external.boolean().describe("Confirmation flag for execution") }, async ({ machine_id, yes }) => ({ content: [{ type: "text", text: JSON.stringify(runTailscaleInstall(machine_id, { apply: true, yes }), null, 2) }] }));
|
|
22341
|
+
server.tool("machines_route_resolve", "Resolve the best route for a machine using manifest, heartbeat, SSH, LAN, and Tailscale topology.", { machine_id: exports_external.string().describe("Machine identifier"), include_tailscale: exports_external.boolean().optional().describe("Whether to probe tailscale status --json") }, async ({ machine_id, include_tailscale }) => ({
|
|
22342
|
+
content: [{ type: "text", text: JSON.stringify(resolveMachineRoute(machine_id, { includeTailscale: include_tailscale !== false }), null, 2) }]
|
|
22343
|
+
}));
|
|
22185
22344
|
server.tool("machines_ssh_resolve", "Resolve the best SSH route for a machine.", { machine_id: exports_external.string().describe("Machine identifier"), remote_command: exports_external.string().optional().describe("Optional remote command") }, async ({ machine_id, remote_command }) => ({
|
|
22186
|
-
content: [{ type: "text", text: JSON.stringify({ resolved:
|
|
22345
|
+
content: [{ type: "text", text: JSON.stringify({ resolved: resolveMachineRoute(machine_id), command: buildSshCommand(machine_id, remote_command) }, null, 2) }]
|
|
22187
22346
|
}));
|
|
22188
22347
|
server.tool("machines_ports", "List listening ports on a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({
|
|
22189
22348
|
content: [{ type: "text", text: JSON.stringify(listPorts(machine_id), null, 2) }]
|
|
@@ -22248,6 +22407,7 @@ export {
|
|
|
22248
22407
|
runAppsInstall,
|
|
22249
22408
|
resolveTables,
|
|
22250
22409
|
resolveSshTarget,
|
|
22410
|
+
resolveMachineRoute,
|
|
22251
22411
|
renderDomainMapping,
|
|
22252
22412
|
renderDashboardHtml,
|
|
22253
22413
|
removeNotificationChannel,
|
|
@@ -22331,5 +22491,7 @@ export {
|
|
|
22331
22491
|
MACHINES_STORAGE_MODE_ENV,
|
|
22332
22492
|
MACHINES_STORAGE_FALLBACK_ENV,
|
|
22333
22493
|
MACHINES_STORAGE_ENV,
|
|
22494
|
+
MACHINES_PACKAGE_NAME,
|
|
22495
|
+
MACHINES_CONSUMER_CONTRACT_VERSION,
|
|
22334
22496
|
CROSSREFS_KEY
|
|
22335
22497
|
};
|