@hasna/machines 0.0.16 → 0.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -5
- package/dist/cli/index.js +387 -217
- package/dist/commands/ssh.d.ts +6 -3
- package/dist/commands/ssh.d.ts.map +1 -1
- package/dist/compatibility.d.ts +4 -0
- package/dist/compatibility.d.ts.map +1 -1
- package/dist/consumer.d.ts +10 -0
- package/dist/consumer.d.ts.map +1 -0
- package/dist/consumer.js +4983 -0
- package/dist/index.js +223 -64
- package/dist/mcp/index.js +370 -217
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/remote.d.ts.map +1 -1
- package/dist/topology.d.ts +45 -1
- package/dist/topology.d.ts.map +1 -1
- package/package.json +6 -2
package/dist/mcp/index.js
CHANGED
|
@@ -4218,7 +4218,7 @@ function detectCurrentMachineManifest() {
|
|
|
4218
4218
|
|
|
4219
4219
|
// src/remote.ts
|
|
4220
4220
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
4221
|
-
import { hostname as
|
|
4221
|
+
import { hostname as hostname4 } from "os";
|
|
4222
4222
|
|
|
4223
4223
|
// src/db.ts
|
|
4224
4224
|
import { Database } from "bun:sqlite";
|
|
@@ -4319,46 +4319,361 @@ function recordSyncRun(machineId, status, actions) {
|
|
|
4319
4319
|
VALUES (?, ?, ?, ?, ?, ?)`).run(crypto.randomUUID(), machineId, status, JSON.stringify(actions), now, now);
|
|
4320
4320
|
}
|
|
4321
4321
|
|
|
4322
|
-
// src/
|
|
4322
|
+
// src/topology.ts
|
|
4323
|
+
import { existsSync as existsSync4 } from "fs";
|
|
4324
|
+
import { arch as arch2, hostname as hostname3, platform as platform2, userInfo as userInfo2 } from "os";
|
|
4323
4325
|
import { spawnSync } from "child_process";
|
|
4326
|
+
var MACHINES_CONSUMER_CONTRACT_VERSION = 1;
|
|
4327
|
+
var MACHINES_PACKAGE_NAME = "@hasna/machines";
|
|
4328
|
+
function normalizePlatform2(value = platform2()) {
|
|
4329
|
+
const normalized = value.toLowerCase();
|
|
4330
|
+
if (normalized === "darwin" || normalized === "macos")
|
|
4331
|
+
return "macos";
|
|
4332
|
+
if (normalized === "win32" || normalized === "windows")
|
|
4333
|
+
return "windows";
|
|
4334
|
+
if (normalized === "linux")
|
|
4335
|
+
return "linux";
|
|
4336
|
+
return value;
|
|
4337
|
+
}
|
|
4338
|
+
function defaultRunner(command) {
|
|
4339
|
+
const result = spawnSync("bash", ["-c", command], {
|
|
4340
|
+
encoding: "utf8",
|
|
4341
|
+
env: process.env
|
|
4342
|
+
});
|
|
4343
|
+
return {
|
|
4344
|
+
stdout: result.stdout || "",
|
|
4345
|
+
stderr: result.stderr || "",
|
|
4346
|
+
exitCode: result.status ?? 1
|
|
4347
|
+
};
|
|
4348
|
+
}
|
|
4349
|
+
function hasCommand(command, runner) {
|
|
4350
|
+
return runner(`command -v ${command} >/dev/null 2>&1`).exitCode === 0;
|
|
4351
|
+
}
|
|
4352
|
+
function parseTailscaleStatus(raw) {
|
|
4353
|
+
try {
|
|
4354
|
+
const parsed = JSON.parse(raw);
|
|
4355
|
+
if (!parsed || typeof parsed !== "object")
|
|
4356
|
+
return null;
|
|
4357
|
+
return parsed;
|
|
4358
|
+
} catch {
|
|
4359
|
+
return null;
|
|
4360
|
+
}
|
|
4361
|
+
}
|
|
4362
|
+
function loadTailscalePeers(runner, warnings) {
|
|
4363
|
+
const peers = new Map;
|
|
4364
|
+
if (!hasCommand("tailscale", runner)) {
|
|
4365
|
+
warnings.push("tailscale_not_available");
|
|
4366
|
+
return peers;
|
|
4367
|
+
}
|
|
4368
|
+
const result = runner("tailscale status --json");
|
|
4369
|
+
if (result.exitCode !== 0) {
|
|
4370
|
+
warnings.push(`tailscale_status_failed:${result.stderr.trim() || result.exitCode}`);
|
|
4371
|
+
return peers;
|
|
4372
|
+
}
|
|
4373
|
+
const status = parseTailscaleStatus(result.stdout);
|
|
4374
|
+
if (!status) {
|
|
4375
|
+
warnings.push("tailscale_status_invalid_json");
|
|
4376
|
+
return peers;
|
|
4377
|
+
}
|
|
4378
|
+
const addPeer = (peer) => {
|
|
4379
|
+
if (!peer)
|
|
4380
|
+
return;
|
|
4381
|
+
const id = peer.HostName || peer.DNSName?.split(".")[0];
|
|
4382
|
+
if (id)
|
|
4383
|
+
peers.set(id, peer);
|
|
4384
|
+
};
|
|
4385
|
+
addPeer(status.Self);
|
|
4386
|
+
for (const peer of Object.values(status.Peer ?? {}))
|
|
4387
|
+
addPeer(peer);
|
|
4388
|
+
return peers;
|
|
4389
|
+
}
|
|
4390
|
+
function machineKeys(machine) {
|
|
4391
|
+
return [
|
|
4392
|
+
machine.id,
|
|
4393
|
+
machine.hostname,
|
|
4394
|
+
machine.tailscaleName?.split(".")[0],
|
|
4395
|
+
machine.tailscaleName,
|
|
4396
|
+
machine.sshAddress?.split("@").pop()
|
|
4397
|
+
].filter((value) => Boolean(value));
|
|
4398
|
+
}
|
|
4399
|
+
function findTailscalePeer(machine, machineId, peers) {
|
|
4400
|
+
if (machine) {
|
|
4401
|
+
for (const key of machineKeys(machine)) {
|
|
4402
|
+
const peer = peers.get(key) ?? peers.get(key.replace(/\.$/, ""));
|
|
4403
|
+
if (peer)
|
|
4404
|
+
return peer;
|
|
4405
|
+
}
|
|
4406
|
+
}
|
|
4407
|
+
return peers.get(machineId) ?? null;
|
|
4408
|
+
}
|
|
4324
4409
|
function envReachableHosts() {
|
|
4325
4410
|
const raw = process.env["HASNA_MACHINES_REACHABLE_HOSTS"];
|
|
4326
4411
|
return new Set((raw || "").split(",").map((value) => value.trim()).filter(Boolean));
|
|
4327
4412
|
}
|
|
4328
|
-
function
|
|
4413
|
+
function manifestHostReachable(target) {
|
|
4329
4414
|
const overrides = envReachableHosts();
|
|
4330
|
-
if (overrides.size
|
|
4331
|
-
return
|
|
4415
|
+
if (overrides.size === 0)
|
|
4416
|
+
return null;
|
|
4417
|
+
return overrides.has(target);
|
|
4418
|
+
}
|
|
4419
|
+
function routeHints(input) {
|
|
4420
|
+
const hints = [];
|
|
4421
|
+
if (input.machineId === input.localMachineId) {
|
|
4422
|
+
hints.push({ kind: "local", target: "localhost", reachable: true });
|
|
4423
|
+
}
|
|
4424
|
+
if (input.manifest?.sshAddress) {
|
|
4425
|
+
hints.push({ kind: "ssh", target: input.manifest.sshAddress, reachable: manifestHostReachable(input.manifest.sshAddress) });
|
|
4426
|
+
}
|
|
4427
|
+
if (input.manifest?.hostname) {
|
|
4428
|
+
hints.push({ kind: "lan", target: input.manifest.hostname, reachable: manifestHostReachable(input.manifest.hostname) });
|
|
4332
4429
|
}
|
|
4333
|
-
const
|
|
4334
|
-
|
|
4430
|
+
const tailscaleTarget = input.manifest?.tailscaleName ?? input.peer?.DNSName ?? input.peer?.TailscaleIPs?.[0];
|
|
4431
|
+
if (tailscaleTarget) {
|
|
4432
|
+
hints.push({ kind: "tailscale", target: tailscaleTarget.replace(/\.$/, ""), reachable: input.peer?.Online ?? null });
|
|
4433
|
+
}
|
|
4434
|
+
return hints;
|
|
4435
|
+
}
|
|
4436
|
+
function routeRank(hint) {
|
|
4437
|
+
if (hint.kind === "local")
|
|
4438
|
+
return 0;
|
|
4439
|
+
if (hint.reachable === true && hint.kind === "ssh")
|
|
4440
|
+
return 1;
|
|
4441
|
+
if (hint.reachable === true && hint.kind === "lan")
|
|
4442
|
+
return 2;
|
|
4443
|
+
if (hint.reachable === true && hint.kind === "tailscale")
|
|
4444
|
+
return 3;
|
|
4445
|
+
if (hint.reachable === false)
|
|
4446
|
+
return 8;
|
|
4447
|
+
if (hint.kind === "ssh")
|
|
4448
|
+
return 4;
|
|
4449
|
+
if (hint.kind === "lan")
|
|
4450
|
+
return 5;
|
|
4451
|
+
if (hint.kind === "tailscale")
|
|
4452
|
+
return 6;
|
|
4453
|
+
return 9;
|
|
4454
|
+
}
|
|
4455
|
+
function selectRouteHint(hints) {
|
|
4456
|
+
return [...hints].sort((left, right) => routeRank(left) - routeRank(right))[0] ?? null;
|
|
4457
|
+
}
|
|
4458
|
+
function buildEntry(input) {
|
|
4459
|
+
const manifest = input.manifest;
|
|
4460
|
+
const peer = input.peer;
|
|
4461
|
+
const hints = routeHints({
|
|
4462
|
+
machineId: input.machineId,
|
|
4463
|
+
localMachineId: input.localMachineId,
|
|
4464
|
+
manifest,
|
|
4465
|
+
peer
|
|
4335
4466
|
});
|
|
4336
|
-
|
|
4467
|
+
const selectedRoute = selectRouteHint(hints);
|
|
4468
|
+
const route = selectedRoute?.kind === "ssh" ? "ssh" : selectedRoute?.kind ?? "unknown";
|
|
4469
|
+
return {
|
|
4470
|
+
machine_id: input.machineId,
|
|
4471
|
+
hostname: manifest?.hostname ?? peer?.HostName ?? null,
|
|
4472
|
+
platform: manifest?.platform ?? (peer?.OS ? normalizePlatform2(peer.OS) : null),
|
|
4473
|
+
os: peer?.OS ?? null,
|
|
4474
|
+
user: typeof manifest?.metadata?.user === "string" ? manifest.metadata.user : null,
|
|
4475
|
+
workspace_path: manifest?.workspacePath ?? null,
|
|
4476
|
+
manifest_declared: Boolean(manifest),
|
|
4477
|
+
heartbeat_status: input.heartbeat?.status ?? "unknown",
|
|
4478
|
+
last_heartbeat_at: input.heartbeat?.updated_at ?? null,
|
|
4479
|
+
tailscale: {
|
|
4480
|
+
dns_name: manifest?.tailscaleName ?? peer?.DNSName?.replace(/\.$/, "") ?? null,
|
|
4481
|
+
ips: peer?.TailscaleIPs ?? [],
|
|
4482
|
+
online: peer?.Online ?? null,
|
|
4483
|
+
active: peer?.Active ?? null,
|
|
4484
|
+
last_seen: peer?.LastSeen ?? null
|
|
4485
|
+
},
|
|
4486
|
+
ssh: {
|
|
4487
|
+
address: manifest?.sshAddress ?? null,
|
|
4488
|
+
route,
|
|
4489
|
+
command_target: selectedRoute?.target ?? null
|
|
4490
|
+
},
|
|
4491
|
+
route_hints: hints,
|
|
4492
|
+
tags: manifest?.tags ?? [],
|
|
4493
|
+
metadata: manifest?.metadata ?? {}
|
|
4494
|
+
};
|
|
4337
4495
|
}
|
|
4338
|
-
function
|
|
4339
|
-
const
|
|
4340
|
-
|
|
4341
|
-
|
|
4496
|
+
function discoverMachineTopology(options = {}) {
|
|
4497
|
+
const now = options.now ?? new Date;
|
|
4498
|
+
const runner = options.runner ?? defaultRunner;
|
|
4499
|
+
const warnings = [];
|
|
4500
|
+
const manifest = readManifest();
|
|
4501
|
+
const heartbeats = listHeartbeats();
|
|
4502
|
+
const heartbeatByMachine = new Map(heartbeats.map((heartbeat) => [heartbeat.machine_id, heartbeat]));
|
|
4503
|
+
const localMachineId = getLocalMachineId();
|
|
4504
|
+
const peers = options.includeTailscale === false ? new Map : loadTailscalePeers(runner, warnings);
|
|
4505
|
+
const machineIds = new Set([
|
|
4506
|
+
localMachineId,
|
|
4507
|
+
...manifest.machines.map((machine) => machine.id),
|
|
4508
|
+
...heartbeats.map((heartbeat) => heartbeat.machine_id),
|
|
4509
|
+
...peers.keys()
|
|
4510
|
+
]);
|
|
4511
|
+
const manifestById = new Map(manifest.machines.map((machine) => [machine.id, machine]));
|
|
4512
|
+
const machines = [...machineIds].sort().map((machineId) => {
|
|
4513
|
+
const manifestMachine = manifestById.get(machineId);
|
|
4514
|
+
return buildEntry({
|
|
4515
|
+
machineId,
|
|
4516
|
+
localMachineId,
|
|
4517
|
+
manifest: manifestMachine,
|
|
4518
|
+
peer: findTailscalePeer(manifestMachine ?? null, machineId, peers),
|
|
4519
|
+
heartbeat: heartbeatByMachine.get(machineId)
|
|
4520
|
+
});
|
|
4521
|
+
});
|
|
4522
|
+
return {
|
|
4523
|
+
schema_version: MACHINES_CONSUMER_CONTRACT_VERSION,
|
|
4524
|
+
package: {
|
|
4525
|
+
name: MACHINES_PACKAGE_NAME,
|
|
4526
|
+
version: getPackageVersion()
|
|
4527
|
+
},
|
|
4528
|
+
capabilities: {
|
|
4529
|
+
topology: true,
|
|
4530
|
+
compatibility: true,
|
|
4531
|
+
route_resolution: true,
|
|
4532
|
+
cli_json_fallback: true
|
|
4533
|
+
},
|
|
4534
|
+
generated_at: now.toISOString(),
|
|
4535
|
+
local_machine_id: localMachineId,
|
|
4536
|
+
local_hostname: hostname3(),
|
|
4537
|
+
current_platform: normalizePlatform2(),
|
|
4538
|
+
manifest_path_known: existsSync4(getManifestPath()),
|
|
4539
|
+
machines,
|
|
4540
|
+
warnings
|
|
4541
|
+
};
|
|
4542
|
+
}
|
|
4543
|
+
function normalizeMachineAlias(value) {
|
|
4544
|
+
return value.trim().replace(/\.$/, "").toLowerCase();
|
|
4545
|
+
}
|
|
4546
|
+
function routeTargetMatches(machine, requested) {
|
|
4547
|
+
const normalized = normalizeMachineAlias(requested);
|
|
4548
|
+
const values = [
|
|
4549
|
+
machine.ssh.address,
|
|
4550
|
+
machine.ssh.command_target,
|
|
4551
|
+
machine.tailscale.dns_name,
|
|
4552
|
+
machine.tailscale.dns_name?.split(".")[0],
|
|
4553
|
+
...machine.tailscale.ips,
|
|
4554
|
+
...machine.route_hints.map((hint) => hint.target),
|
|
4555
|
+
...machine.route_hints.map((hint) => hint.target.split("@").pop() ?? hint.target)
|
|
4556
|
+
].filter((value) => Boolean(value));
|
|
4557
|
+
return values.some((value) => normalizeMachineAlias(value) === normalized);
|
|
4558
|
+
}
|
|
4559
|
+
function findRouteMachine(topology, requestedMachineId) {
|
|
4560
|
+
const requested = normalizeMachineAlias(requestedMachineId);
|
|
4561
|
+
if (requested === "local" || requested === "localhost" || requested === normalizeMachineAlias(hostname3()) || requested === normalizeMachineAlias(topology.local_machine_id)) {
|
|
4562
|
+
return {
|
|
4563
|
+
machine: topology.machines.find((machine) => machine.machine_id === topology.local_machine_id) ?? null,
|
|
4564
|
+
matchedBy: "local_alias"
|
|
4565
|
+
};
|
|
4342
4566
|
}
|
|
4343
|
-
const
|
|
4344
|
-
if (
|
|
4567
|
+
const machineIdMatch = topology.machines.find((machine) => normalizeMachineAlias(machine.machine_id) === requested);
|
|
4568
|
+
if (machineIdMatch)
|
|
4569
|
+
return { machine: machineIdMatch, matchedBy: "machine_id" };
|
|
4570
|
+
const hostnameMatch = topology.machines.find((machine) => machine.hostname && normalizeMachineAlias(machine.hostname) === requested);
|
|
4571
|
+
if (hostnameMatch)
|
|
4572
|
+
return { machine: hostnameMatch, matchedBy: "hostname" };
|
|
4573
|
+
const tailscaleMatch = topology.machines.find((machine) => {
|
|
4574
|
+
if (!machine.tailscale.dns_name)
|
|
4575
|
+
return false;
|
|
4576
|
+
const dns = normalizeMachineAlias(machine.tailscale.dns_name);
|
|
4577
|
+
return dns === requested || dns.split(".")[0] === requested;
|
|
4578
|
+
});
|
|
4579
|
+
if (tailscaleMatch)
|
|
4580
|
+
return { machine: tailscaleMatch, matchedBy: "tailscale" };
|
|
4581
|
+
const routeMatch = topology.machines.find((machine) => routeTargetMatches(machine, requestedMachineId));
|
|
4582
|
+
if (routeMatch)
|
|
4583
|
+
return { machine: routeMatch, matchedBy: "route_target" };
|
|
4584
|
+
return { machine: null, matchedBy: null };
|
|
4585
|
+
}
|
|
4586
|
+
function routeConfidence(input) {
|
|
4587
|
+
if (input.matchedBy === "local_alias")
|
|
4588
|
+
return "exact";
|
|
4589
|
+
if (input.hint?.kind === "local")
|
|
4590
|
+
return "exact";
|
|
4591
|
+
if (input.hint?.reachable === true)
|
|
4592
|
+
return "high";
|
|
4593
|
+
if (input.machine.manifest_declared && (input.hint?.kind === "ssh" || input.hint?.kind === "lan"))
|
|
4594
|
+
return "medium";
|
|
4595
|
+
if (input.hint)
|
|
4596
|
+
return "low";
|
|
4597
|
+
return "none";
|
|
4598
|
+
}
|
|
4599
|
+
function resolveMachineRoute(machineId, options = {}) {
|
|
4600
|
+
const topology = options.topology ?? discoverMachineTopology(options);
|
|
4601
|
+
const warnings = [...topology.warnings];
|
|
4602
|
+
const { machine, matchedBy } = findRouteMachine(topology, machineId);
|
|
4603
|
+
const generatedAt = (options.now ?? new Date).toISOString();
|
|
4604
|
+
if (!machine) {
|
|
4605
|
+
warnings.push(`machine_not_found:${machineId}`);
|
|
4345
4606
|
return {
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4607
|
+
schema_version: MACHINES_CONSUMER_CONTRACT_VERSION,
|
|
4608
|
+
package: { name: MACHINES_PACKAGE_NAME, version: getPackageVersion() },
|
|
4609
|
+
ok: false,
|
|
4610
|
+
machine_id: null,
|
|
4611
|
+
requested_machine_id: machineId,
|
|
4612
|
+
generated_at: generatedAt,
|
|
4613
|
+
route: "unknown",
|
|
4614
|
+
source: "unknown",
|
|
4615
|
+
target: null,
|
|
4616
|
+
command_target: null,
|
|
4617
|
+
confidence: "none",
|
|
4618
|
+
local: false,
|
|
4619
|
+
evidence: {
|
|
4620
|
+
topology: true,
|
|
4621
|
+
matched_by: null,
|
|
4622
|
+
manifest_declared: null,
|
|
4623
|
+
heartbeat_status: null,
|
|
4624
|
+
tailscale_online: null,
|
|
4625
|
+
selected_hint: null
|
|
4626
|
+
},
|
|
4627
|
+
warnings
|
|
4349
4628
|
};
|
|
4350
4629
|
}
|
|
4351
|
-
const
|
|
4352
|
-
const
|
|
4353
|
-
const
|
|
4630
|
+
const selectedHint = selectRouteHint(machine.route_hints);
|
|
4631
|
+
const route = selectedHint?.kind ?? machine.ssh.route ?? "unknown";
|
|
4632
|
+
const local = route === "local" || machine.machine_id === topology.local_machine_id;
|
|
4354
4633
|
return {
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4634
|
+
schema_version: MACHINES_CONSUMER_CONTRACT_VERSION,
|
|
4635
|
+
package: topology.package,
|
|
4636
|
+
ok: Boolean(selectedHint?.target),
|
|
4637
|
+
machine_id: machine.machine_id,
|
|
4638
|
+
requested_machine_id: machineId,
|
|
4639
|
+
generated_at: generatedAt,
|
|
4640
|
+
route,
|
|
4641
|
+
source: route,
|
|
4642
|
+
target: selectedHint?.target ?? null,
|
|
4643
|
+
command_target: selectedHint?.target ?? null,
|
|
4644
|
+
confidence: routeConfidence({ machine, hint: selectedHint, matchedBy }),
|
|
4645
|
+
local,
|
|
4646
|
+
evidence: {
|
|
4647
|
+
topology: true,
|
|
4648
|
+
matched_by: matchedBy,
|
|
4649
|
+
manifest_declared: machine.manifest_declared,
|
|
4650
|
+
heartbeat_status: machine.heartbeat_status,
|
|
4651
|
+
tailscale_online: machine.tailscale.online,
|
|
4652
|
+
selected_hint: selectedHint
|
|
4653
|
+
},
|
|
4654
|
+
warnings
|
|
4655
|
+
};
|
|
4656
|
+
}
|
|
4657
|
+
|
|
4658
|
+
// src/commands/ssh.ts
|
|
4659
|
+
function resolveSshTarget(machineId, options = {}) {
|
|
4660
|
+
const resolved = resolveMachineRoute(machineId, options);
|
|
4661
|
+
if (!resolved.ok || !resolved.target) {
|
|
4662
|
+
throw new Error(`Machine route not found: ${machineId}`);
|
|
4663
|
+
}
|
|
4664
|
+
if (resolved.route !== "local" && resolved.route !== "lan" && resolved.route !== "tailscale" && resolved.route !== "ssh") {
|
|
4665
|
+
throw new Error(`Machine route is not SSH-capable: ${machineId}`);
|
|
4666
|
+
}
|
|
4667
|
+
return {
|
|
4668
|
+
machineId: resolved.machine_id ?? machineId,
|
|
4669
|
+
target: resolved.target,
|
|
4670
|
+
route: resolved.route,
|
|
4671
|
+
confidence: resolved.confidence,
|
|
4672
|
+
warnings: resolved.warnings
|
|
4358
4673
|
};
|
|
4359
4674
|
}
|
|
4360
|
-
function buildSshCommand(machineId, remoteCommand) {
|
|
4361
|
-
const resolved = resolveSshTarget(machineId);
|
|
4675
|
+
function buildSshCommand(machineId, remoteCommand, options = {}) {
|
|
4676
|
+
const resolved = resolveSshTarget(machineId, options);
|
|
4362
4677
|
return remoteCommand ? `ssh ${resolved.target} ${JSON.stringify(remoteCommand)}` : `ssh ${resolved.target}`;
|
|
4363
4678
|
}
|
|
4364
4679
|
|
|
@@ -4367,7 +4682,7 @@ function shellQuote(value) {
|
|
|
4367
4682
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
4368
4683
|
}
|
|
4369
4684
|
function machineIsLocal(machineId, localMachineId) {
|
|
4370
|
-
return machineId === "local" || machineId === "localhost" || machineId === localMachineId || machineId ===
|
|
4685
|
+
return machineId === "local" || machineId === "localhost" || machineId === localMachineId || machineId === hostname4();
|
|
4371
4686
|
}
|
|
4372
4687
|
function resolveMachineCommand(machineId, command, localMachineId = getLocalMachineId()) {
|
|
4373
4688
|
if (machineIsLocal(machineId, localMachineId)) {
|
|
@@ -4379,7 +4694,8 @@ function resolveMachineCommand(machineId, command, localMachineId = getLocalMach
|
|
|
4379
4694
|
shellCommand: buildSshCommand(machineId, command)
|
|
4380
4695
|
};
|
|
4381
4696
|
} catch (error) {
|
|
4382
|
-
|
|
4697
|
+
const message = String(error.message ?? error);
|
|
4698
|
+
if (message.includes("Machine route not found") || message.includes("Machine not found in manifest")) {
|
|
4383
4699
|
return { source: "ssh", shellCommand: `ssh ${shellQuote(machineId)} ${shellQuote(command)}` };
|
|
4384
4700
|
}
|
|
4385
4701
|
throw error;
|
|
@@ -4539,7 +4855,7 @@ function runAppsInstall(machineId, options = {}) {
|
|
|
4539
4855
|
}
|
|
4540
4856
|
|
|
4541
4857
|
// src/commands/cert.ts
|
|
4542
|
-
import { homedir as homedir3, platform as
|
|
4858
|
+
import { homedir as homedir3, platform as platform3 } from "os";
|
|
4543
4859
|
import { join as join4 } from "path";
|
|
4544
4860
|
function quote2(value) {
|
|
4545
4861
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
@@ -4555,7 +4871,7 @@ function buildCertPlan(domains) {
|
|
|
4555
4871
|
const certPath = join4(certDir(), `${primary}.pem`);
|
|
4556
4872
|
const keyPath = join4(certDir(), `${primary}-key.pem`);
|
|
4557
4873
|
const steps = [];
|
|
4558
|
-
if (
|
|
4874
|
+
if (platform3() === "darwin") {
|
|
4559
4875
|
steps.push({
|
|
4560
4876
|
id: "mkcert-install-macos",
|
|
4561
4877
|
title: "Install mkcert on macOS",
|
|
@@ -4617,14 +4933,14 @@ function runCertPlan(domains, options = {}) {
|
|
|
4617
4933
|
}
|
|
4618
4934
|
|
|
4619
4935
|
// src/commands/dns.ts
|
|
4620
|
-
import { existsSync as
|
|
4936
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
4621
4937
|
import { join as join5 } from "path";
|
|
4622
4938
|
function getDnsPath() {
|
|
4623
4939
|
return join5(getDataDir(), "dns.json");
|
|
4624
4940
|
}
|
|
4625
4941
|
function readMappings() {
|
|
4626
4942
|
const path = getDnsPath();
|
|
4627
|
-
if (!
|
|
4943
|
+
if (!existsSync5(path))
|
|
4628
4944
|
return [];
|
|
4629
4945
|
return JSON.parse(readFileSync3(path, "utf8"));
|
|
4630
4946
|
}
|
|
@@ -4928,7 +5244,7 @@ function runTailscaleInstall(machineId, options = {}) {
|
|
|
4928
5244
|
}
|
|
4929
5245
|
|
|
4930
5246
|
// src/commands/notifications.ts
|
|
4931
|
-
import { existsSync as
|
|
5247
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
4932
5248
|
var notificationChannelSchema = exports_external.object({
|
|
4933
5249
|
id: exports_external.string(),
|
|
4934
5250
|
type: exports_external.enum(["email", "webhook", "command"]),
|
|
@@ -4947,7 +5263,7 @@ function sortChannels(channels) {
|
|
|
4947
5263
|
function shellQuote3(value) {
|
|
4948
5264
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
4949
5265
|
}
|
|
4950
|
-
function
|
|
5266
|
+
function hasCommand2(binary) {
|
|
4951
5267
|
const result = Bun.spawnSync(["bash", "-lc", `command -v ${binary} >/dev/null 2>&1`], {
|
|
4952
5268
|
stdout: "ignore",
|
|
4953
5269
|
stderr: "ignore",
|
|
@@ -4972,7 +5288,7 @@ Content-Type: text/plain; charset=utf-8
|
|
|
4972
5288
|
|
|
4973
5289
|
${message}
|
|
4974
5290
|
`;
|
|
4975
|
-
if (
|
|
5291
|
+
if (hasCommand2("sendmail")) {
|
|
4976
5292
|
const result = Bun.spawnSync(["bash", "-lc", "sendmail -t"], {
|
|
4977
5293
|
stdin: new TextEncoder().encode(body),
|
|
4978
5294
|
stdout: "pipe",
|
|
@@ -4990,7 +5306,7 @@ ${message}
|
|
|
4990
5306
|
detail: `Delivered via sendmail to ${channel.target}`
|
|
4991
5307
|
};
|
|
4992
5308
|
}
|
|
4993
|
-
if (
|
|
5309
|
+
if (hasCommand2("mail")) {
|
|
4994
5310
|
const command = `printf %s ${shellQuote3(message)} | mail -s ${shellQuote3(subject)} ${shellQuote3(channel.target)}`;
|
|
4995
5311
|
const result = Bun.spawnSync(["bash", "-lc", command], {
|
|
4996
5312
|
stdout: "pipe",
|
|
@@ -5084,7 +5400,7 @@ function getDefaultNotificationConfig() {
|
|
|
5084
5400
|
};
|
|
5085
5401
|
}
|
|
5086
5402
|
function readNotificationConfig(path = getNotificationsPath()) {
|
|
5087
|
-
if (!
|
|
5403
|
+
if (!existsSync6(path)) {
|
|
5088
5404
|
return getDefaultNotificationConfig();
|
|
5089
5405
|
}
|
|
5090
5406
|
return notificationConfigSchema.parse(JSON.parse(readFileSync4(path, "utf8")));
|
|
@@ -5616,7 +5932,7 @@ function runSetup(machineId, options = {}) {
|
|
|
5616
5932
|
}
|
|
5617
5933
|
|
|
5618
5934
|
// src/commands/sync.ts
|
|
5619
|
-
import { existsSync as
|
|
5935
|
+
import { existsSync as existsSync7, lstatSync, readFileSync as readFileSync5, symlinkSync, copyFileSync } from "fs";
|
|
5620
5936
|
function quote4(value) {
|
|
5621
5937
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
5622
5938
|
}
|
|
@@ -5664,8 +5980,8 @@ function detectPackageActions(machine) {
|
|
|
5664
5980
|
}
|
|
5665
5981
|
function detectFileActions(machine) {
|
|
5666
5982
|
return (machine.files || []).map((file, index) => {
|
|
5667
|
-
const sourceExists =
|
|
5668
|
-
const targetExists =
|
|
5983
|
+
const sourceExists = existsSync7(file.source);
|
|
5984
|
+
const targetExists = existsSync7(file.target);
|
|
5669
5985
|
let status = "missing";
|
|
5670
5986
|
if (sourceExists && targetExists) {
|
|
5671
5987
|
if (file.mode === "symlink") {
|
|
@@ -5777,183 +6093,6 @@ function getAgentStatus(machineId = getLocalMachineId()) {
|
|
|
5777
6093
|
}));
|
|
5778
6094
|
}
|
|
5779
6095
|
|
|
5780
|
-
// src/topology.ts
|
|
5781
|
-
import { existsSync as existsSync7 } from "fs";
|
|
5782
|
-
import { arch as arch2, hostname as hostname4, platform as platform3, userInfo as userInfo2 } from "os";
|
|
5783
|
-
import { spawnSync as spawnSync4 } from "child_process";
|
|
5784
|
-
function normalizePlatform2(value = platform3()) {
|
|
5785
|
-
const normalized = value.toLowerCase();
|
|
5786
|
-
if (normalized === "darwin" || normalized === "macos")
|
|
5787
|
-
return "macos";
|
|
5788
|
-
if (normalized === "win32" || normalized === "windows")
|
|
5789
|
-
return "windows";
|
|
5790
|
-
if (normalized === "linux")
|
|
5791
|
-
return "linux";
|
|
5792
|
-
return value;
|
|
5793
|
-
}
|
|
5794
|
-
function defaultRunner(command) {
|
|
5795
|
-
const result = spawnSync4("bash", ["-c", command], {
|
|
5796
|
-
encoding: "utf8",
|
|
5797
|
-
env: process.env
|
|
5798
|
-
});
|
|
5799
|
-
return {
|
|
5800
|
-
stdout: result.stdout || "",
|
|
5801
|
-
stderr: result.stderr || "",
|
|
5802
|
-
exitCode: result.status ?? 1
|
|
5803
|
-
};
|
|
5804
|
-
}
|
|
5805
|
-
function hasCommand2(command, runner) {
|
|
5806
|
-
return runner(`command -v ${command} >/dev/null 2>&1`).exitCode === 0;
|
|
5807
|
-
}
|
|
5808
|
-
function parseTailscaleStatus(raw) {
|
|
5809
|
-
try {
|
|
5810
|
-
const parsed = JSON.parse(raw);
|
|
5811
|
-
if (!parsed || typeof parsed !== "object")
|
|
5812
|
-
return null;
|
|
5813
|
-
return parsed;
|
|
5814
|
-
} catch {
|
|
5815
|
-
return null;
|
|
5816
|
-
}
|
|
5817
|
-
}
|
|
5818
|
-
function loadTailscalePeers(runner, warnings) {
|
|
5819
|
-
const peers = new Map;
|
|
5820
|
-
if (!hasCommand2("tailscale", runner)) {
|
|
5821
|
-
warnings.push("tailscale_not_available");
|
|
5822
|
-
return peers;
|
|
5823
|
-
}
|
|
5824
|
-
const result = runner("tailscale status --json");
|
|
5825
|
-
if (result.exitCode !== 0) {
|
|
5826
|
-
warnings.push(`tailscale_status_failed:${result.stderr.trim() || result.exitCode}`);
|
|
5827
|
-
return peers;
|
|
5828
|
-
}
|
|
5829
|
-
const status = parseTailscaleStatus(result.stdout);
|
|
5830
|
-
if (!status) {
|
|
5831
|
-
warnings.push("tailscale_status_invalid_json");
|
|
5832
|
-
return peers;
|
|
5833
|
-
}
|
|
5834
|
-
const addPeer = (peer) => {
|
|
5835
|
-
if (!peer)
|
|
5836
|
-
return;
|
|
5837
|
-
const id = peer.HostName || peer.DNSName?.split(".")[0];
|
|
5838
|
-
if (id)
|
|
5839
|
-
peers.set(id, peer);
|
|
5840
|
-
};
|
|
5841
|
-
addPeer(status.Self);
|
|
5842
|
-
for (const peer of Object.values(status.Peer ?? {}))
|
|
5843
|
-
addPeer(peer);
|
|
5844
|
-
return peers;
|
|
5845
|
-
}
|
|
5846
|
-
function machineKeys(machine) {
|
|
5847
|
-
return [
|
|
5848
|
-
machine.id,
|
|
5849
|
-
machine.hostname,
|
|
5850
|
-
machine.tailscaleName?.split(".")[0],
|
|
5851
|
-
machine.tailscaleName,
|
|
5852
|
-
machine.sshAddress?.split("@").pop()
|
|
5853
|
-
].filter((value) => Boolean(value));
|
|
5854
|
-
}
|
|
5855
|
-
function findTailscalePeer(machine, machineId, peers) {
|
|
5856
|
-
if (machine) {
|
|
5857
|
-
for (const key of machineKeys(machine)) {
|
|
5858
|
-
const peer = peers.get(key) ?? peers.get(key.replace(/\.$/, ""));
|
|
5859
|
-
if (peer)
|
|
5860
|
-
return peer;
|
|
5861
|
-
}
|
|
5862
|
-
}
|
|
5863
|
-
return peers.get(machineId) ?? null;
|
|
5864
|
-
}
|
|
5865
|
-
function routeHints(input) {
|
|
5866
|
-
const hints = [];
|
|
5867
|
-
if (input.machineId === input.localMachineId) {
|
|
5868
|
-
hints.push({ kind: "local", target: "localhost", reachable: true });
|
|
5869
|
-
}
|
|
5870
|
-
if (input.manifest?.sshAddress) {
|
|
5871
|
-
hints.push({ kind: "ssh", target: input.manifest.sshAddress, reachable: null });
|
|
5872
|
-
}
|
|
5873
|
-
if (input.manifest?.hostname) {
|
|
5874
|
-
hints.push({ kind: "lan", target: input.manifest.hostname, reachable: null });
|
|
5875
|
-
}
|
|
5876
|
-
const tailscaleTarget = input.manifest?.tailscaleName ?? input.peer?.DNSName ?? input.peer?.TailscaleIPs?.[0];
|
|
5877
|
-
if (tailscaleTarget) {
|
|
5878
|
-
hints.push({ kind: "tailscale", target: tailscaleTarget.replace(/\.$/, ""), reachable: input.peer?.Online ?? null });
|
|
5879
|
-
}
|
|
5880
|
-
return hints;
|
|
5881
|
-
}
|
|
5882
|
-
function buildEntry(input) {
|
|
5883
|
-
const manifest = input.manifest;
|
|
5884
|
-
const peer = input.peer;
|
|
5885
|
-
const hints = routeHints({
|
|
5886
|
-
machineId: input.machineId,
|
|
5887
|
-
localMachineId: input.localMachineId,
|
|
5888
|
-
manifest,
|
|
5889
|
-
peer
|
|
5890
|
-
});
|
|
5891
|
-
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");
|
|
5892
|
-
const route = selectedRoute?.kind === "ssh" ? "lan" : selectedRoute?.kind ?? "unknown";
|
|
5893
|
-
return {
|
|
5894
|
-
machine_id: input.machineId,
|
|
5895
|
-
hostname: manifest?.hostname ?? peer?.HostName ?? null,
|
|
5896
|
-
platform: manifest?.platform ?? (peer?.OS ? normalizePlatform2(peer.OS) : null),
|
|
5897
|
-
os: peer?.OS ?? null,
|
|
5898
|
-
user: typeof manifest?.metadata?.user === "string" ? manifest.metadata.user : null,
|
|
5899
|
-
workspace_path: manifest?.workspacePath ?? null,
|
|
5900
|
-
manifest_declared: Boolean(manifest),
|
|
5901
|
-
heartbeat_status: input.heartbeat?.status ?? "unknown",
|
|
5902
|
-
last_heartbeat_at: input.heartbeat?.updated_at ?? null,
|
|
5903
|
-
tailscale: {
|
|
5904
|
-
dns_name: manifest?.tailscaleName ?? peer?.DNSName?.replace(/\.$/, "") ?? null,
|
|
5905
|
-
ips: peer?.TailscaleIPs ?? [],
|
|
5906
|
-
online: peer?.Online ?? null,
|
|
5907
|
-
active: peer?.Active ?? null,
|
|
5908
|
-
last_seen: peer?.LastSeen ?? null
|
|
5909
|
-
},
|
|
5910
|
-
ssh: {
|
|
5911
|
-
address: manifest?.sshAddress ?? null,
|
|
5912
|
-
route,
|
|
5913
|
-
command_target: selectedRoute?.target ?? null
|
|
5914
|
-
},
|
|
5915
|
-
route_hints: hints,
|
|
5916
|
-
tags: manifest?.tags ?? [],
|
|
5917
|
-
metadata: manifest?.metadata ?? {}
|
|
5918
|
-
};
|
|
5919
|
-
}
|
|
5920
|
-
function discoverMachineTopology(options = {}) {
|
|
5921
|
-
const now = options.now ?? new Date;
|
|
5922
|
-
const runner = options.runner ?? defaultRunner;
|
|
5923
|
-
const warnings = [];
|
|
5924
|
-
const manifest = readManifest();
|
|
5925
|
-
const heartbeats = listHeartbeats();
|
|
5926
|
-
const heartbeatByMachine = new Map(heartbeats.map((heartbeat) => [heartbeat.machine_id, heartbeat]));
|
|
5927
|
-
const localMachineId = getLocalMachineId();
|
|
5928
|
-
const peers = options.includeTailscale === false ? new Map : loadTailscalePeers(runner, warnings);
|
|
5929
|
-
const machineIds = new Set([
|
|
5930
|
-
localMachineId,
|
|
5931
|
-
...manifest.machines.map((machine) => machine.id),
|
|
5932
|
-
...heartbeats.map((heartbeat) => heartbeat.machine_id),
|
|
5933
|
-
...peers.keys()
|
|
5934
|
-
]);
|
|
5935
|
-
const manifestById = new Map(manifest.machines.map((machine) => [machine.id, machine]));
|
|
5936
|
-
const machines = [...machineIds].sort().map((machineId) => {
|
|
5937
|
-
const manifestMachine = manifestById.get(machineId);
|
|
5938
|
-
return buildEntry({
|
|
5939
|
-
machineId,
|
|
5940
|
-
localMachineId,
|
|
5941
|
-
manifest: manifestMachine,
|
|
5942
|
-
peer: findTailscalePeer(manifestMachine ?? null, machineId, peers),
|
|
5943
|
-
heartbeat: heartbeatByMachine.get(machineId)
|
|
5944
|
-
});
|
|
5945
|
-
});
|
|
5946
|
-
return {
|
|
5947
|
-
generated_at: now.toISOString(),
|
|
5948
|
-
local_machine_id: localMachineId,
|
|
5949
|
-
local_hostname: hostname4(),
|
|
5950
|
-
current_platform: normalizePlatform2(),
|
|
5951
|
-
manifest_path_known: existsSync7(getManifestPath()),
|
|
5952
|
-
machines,
|
|
5953
|
-
warnings
|
|
5954
|
-
};
|
|
5955
|
-
}
|
|
5956
|
-
|
|
5957
6096
|
// src/compatibility.ts
|
|
5958
6097
|
var DEFAULT_COMMANDS = [
|
|
5959
6098
|
{ command: "bun", required: true },
|
|
@@ -6180,6 +6319,17 @@ function checkMachineCompatibility(options = {}) {
|
|
|
6180
6319
|
fail: checks.filter((check2) => check2.status === "fail").length
|
|
6181
6320
|
};
|
|
6182
6321
|
return {
|
|
6322
|
+
schema_version: MACHINES_CONSUMER_CONTRACT_VERSION,
|
|
6323
|
+
package: {
|
|
6324
|
+
name: MACHINES_PACKAGE_NAME,
|
|
6325
|
+
version: getPackageVersion()
|
|
6326
|
+
},
|
|
6327
|
+
capabilities: {
|
|
6328
|
+
topology: true,
|
|
6329
|
+
compatibility: true,
|
|
6330
|
+
route_resolution: true,
|
|
6331
|
+
cli_json_fallback: true
|
|
6332
|
+
},
|
|
6183
6333
|
ok: summary.fail === 0,
|
|
6184
6334
|
machine_id: machineId,
|
|
6185
6335
|
source: checks[0]?.source ?? "local",
|
|
@@ -6599,8 +6749,11 @@ function createMcpServer(version) {
|
|
|
6599
6749
|
}));
|
|
6600
6750
|
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) }] }));
|
|
6601
6751
|
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) }] }));
|
|
6752
|
+
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 }) => ({
|
|
6753
|
+
content: [{ type: "text", text: JSON.stringify(resolveMachineRoute(machine_id, { includeTailscale: include_tailscale !== false }), null, 2) }]
|
|
6754
|
+
}));
|
|
6602
6755
|
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 }) => ({
|
|
6603
|
-
content: [{ type: "text", text: JSON.stringify({ resolved:
|
|
6756
|
+
content: [{ type: "text", text: JSON.stringify({ resolved: resolveMachineRoute(machine_id), command: buildSshCommand(machine_id, remote_command) }, null, 2) }]
|
|
6604
6757
|
}));
|
|
6605
6758
|
server.tool("machines_ports", "List listening ports on a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({
|
|
6606
6759
|
content: [{ type: "text", text: JSON.stringify(listPorts(machine_id), null, 2) }]
|