@hasna/machines 0.0.15 → 0.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp/index.js CHANGED
@@ -4218,6 +4218,7 @@ function detectCurrentMachineManifest() {
4218
4218
 
4219
4219
  // src/remote.ts
4220
4220
  import { spawnSync as spawnSync2 } from "child_process";
4221
+ import { hostname as hostname4 } from "os";
4221
4222
 
4222
4223
  // src/db.ts
4223
4224
  import { Database } from "bun:sqlite";
@@ -4318,62 +4319,397 @@ function recordSyncRun(machineId, status, actions) {
4318
4319
  VALUES (?, ?, ?, ?, ?, ?)`).run(crypto.randomUUID(), machineId, status, JSON.stringify(actions), now, now);
4319
4320
  }
4320
4321
 
4321
- // src/commands/ssh.ts
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";
4322
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
+ }
4323
4409
  function envReachableHosts() {
4324
4410
  const raw = process.env["HASNA_MACHINES_REACHABLE_HOSTS"];
4325
4411
  return new Set((raw || "").split(",").map((value) => value.trim()).filter(Boolean));
4326
4412
  }
4327
- function isReachable(host) {
4413
+ function manifestHostReachable(target) {
4328
4414
  const overrides = envReachableHosts();
4329
- if (overrides.size > 0) {
4330
- return overrides.has(host);
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) });
4331
4429
  }
4332
- const probe = spawnSync("bash", ["-lc", `getent hosts ${host} >/dev/null 2>&1 || ping -c 1 -W 1 ${host} >/dev/null 2>&1`], {
4333
- stdio: "ignore"
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
4334
4466
  });
4335
- return probe.status === 0;
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
+ };
4336
4495
  }
4337
- function resolveSshTarget(machineId) {
4338
- const machine = getManifestMachine(machineId);
4339
- if (!machine) {
4340
- throw new Error(`Machine not found in manifest: ${machineId}`);
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
+ };
4341
4566
  }
4342
- const current = detectCurrentMachineManifest();
4343
- if (machine.id === current.id) {
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}`);
4344
4606
  return {
4345
- machineId,
4346
- target: "localhost",
4347
- route: "local"
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
4348
4628
  };
4349
4629
  }
4350
- const lanTarget = machine.sshAddress || machine.hostname || machine.id;
4351
- const tailscaleTarget = machine.tailscaleName || machine.hostname || machine.id;
4352
- const route = isReachable(lanTarget) ? "lan" : "tailscale";
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;
4353
4633
  return {
4354
- machineId,
4355
- target: route === "lan" ? lanTarget : tailscaleTarget,
4356
- route
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
4357
4673
  };
4358
4674
  }
4359
- function buildSshCommand(machineId, remoteCommand) {
4360
- const resolved = resolveSshTarget(machineId);
4675
+ function buildSshCommand(machineId, remoteCommand, options = {}) {
4676
+ const resolved = resolveSshTarget(machineId, options);
4361
4677
  return remoteCommand ? `ssh ${resolved.target} ${JSON.stringify(remoteCommand)}` : `ssh ${resolved.target}`;
4362
4678
  }
4363
4679
 
4364
4680
  // src/remote.ts
4681
+ function shellQuote(value) {
4682
+ return `'${value.replace(/'/g, "'\\''")}'`;
4683
+ }
4684
+ function machineIsLocal(machineId, localMachineId) {
4685
+ return machineId === "local" || machineId === "localhost" || machineId === localMachineId || machineId === hostname4();
4686
+ }
4687
+ function resolveMachineCommand(machineId, command, localMachineId = getLocalMachineId()) {
4688
+ if (machineIsLocal(machineId, localMachineId)) {
4689
+ return { source: "local", shellCommand: command };
4690
+ }
4691
+ try {
4692
+ return {
4693
+ source: resolveSshTarget(machineId).route,
4694
+ shellCommand: buildSshCommand(machineId, command)
4695
+ };
4696
+ } catch (error) {
4697
+ const message = String(error.message ?? error);
4698
+ if (message.includes("Machine route not found") || message.includes("Machine not found in manifest")) {
4699
+ return { source: "ssh", shellCommand: `ssh ${shellQuote(machineId)} ${shellQuote(command)}` };
4700
+ }
4701
+ throw error;
4702
+ }
4703
+ }
4365
4704
  function runMachineCommand(machineId, command) {
4366
- const localMachineId = getLocalMachineId();
4367
- const isLocal = machineId === localMachineId;
4368
- const route = isLocal ? "local" : resolveSshTarget(machineId).route;
4369
- const shellCommand = isLocal ? command : buildSshCommand(machineId, command);
4370
- const result = spawnSync2("bash", ["-c", shellCommand], {
4705
+ const resolved = resolveMachineCommand(machineId, command);
4706
+ const result = spawnSync2("bash", ["-c", resolved.shellCommand], {
4371
4707
  encoding: "utf8",
4372
4708
  env: process.env
4373
4709
  });
4374
4710
  return {
4375
4711
  machineId,
4376
- source: route,
4712
+ source: resolved.source,
4377
4713
  stdout: result.stdout || "",
4378
4714
  stderr: result.stderr || "",
4379
4715
  exitCode: result.status ?? 1
@@ -4393,7 +4729,7 @@ function getAppManager(machine, app) {
4393
4729
  return "winget";
4394
4730
  return "apt";
4395
4731
  }
4396
- function shellQuote(value) {
4732
+ function shellQuote2(value) {
4397
4733
  return `'${value.replace(/'/g, `'\\''`)}'`;
4398
4734
  }
4399
4735
  function buildAppCommand(machine, app) {
@@ -4414,7 +4750,7 @@ function buildAppCommand(machine, app) {
4414
4750
  return `sudo apt-get install -y ${packageName}`;
4415
4751
  }
4416
4752
  function buildAppProbeCommand(machine, app) {
4417
- const packageName = shellQuote(getPackageName(app));
4753
+ const packageName = shellQuote2(getPackageName(app));
4418
4754
  const manager = getAppManager(machine, app);
4419
4755
  if (manager === "custom") {
4420
4756
  return `if command -v ${packageName} >/dev/null 2>&1; then printf 'installed=1\\nversion=custom\\n'; else printf 'installed=0\\n'; fi`;
@@ -4519,7 +4855,7 @@ function runAppsInstall(machineId, options = {}) {
4519
4855
  }
4520
4856
 
4521
4857
  // src/commands/cert.ts
4522
- import { homedir as homedir3, platform as platform2 } from "os";
4858
+ import { homedir as homedir3, platform as platform3 } from "os";
4523
4859
  import { join as join4 } from "path";
4524
4860
  function quote2(value) {
4525
4861
  return `'${value.replace(/'/g, `'\\''`)}'`;
@@ -4535,7 +4871,7 @@ function buildCertPlan(domains) {
4535
4871
  const certPath = join4(certDir(), `${primary}.pem`);
4536
4872
  const keyPath = join4(certDir(), `${primary}-key.pem`);
4537
4873
  const steps = [];
4538
- if (platform2() === "darwin") {
4874
+ if (platform3() === "darwin") {
4539
4875
  steps.push({
4540
4876
  id: "mkcert-install-macos",
4541
4877
  title: "Install mkcert on macOS",
@@ -4597,14 +4933,14 @@ function runCertPlan(domains, options = {}) {
4597
4933
  }
4598
4934
 
4599
4935
  // src/commands/dns.ts
4600
- import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
4936
+ import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
4601
4937
  import { join as join5 } from "path";
4602
4938
  function getDnsPath() {
4603
4939
  return join5(getDataDir(), "dns.json");
4604
4940
  }
4605
4941
  function readMappings() {
4606
4942
  const path = getDnsPath();
4607
- if (!existsSync4(path))
4943
+ if (!existsSync5(path))
4608
4944
  return [];
4609
4945
  return JSON.parse(readFileSync3(path, "utf8"));
4610
4946
  }
@@ -4908,7 +5244,7 @@ function runTailscaleInstall(machineId, options = {}) {
4908
5244
  }
4909
5245
 
4910
5246
  // src/commands/notifications.ts
4911
- import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
5247
+ import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
4912
5248
  var notificationChannelSchema = exports_external.object({
4913
5249
  id: exports_external.string(),
4914
5250
  type: exports_external.enum(["email", "webhook", "command"]),
@@ -4924,10 +5260,10 @@ var notificationConfigSchema = exports_external.object({
4924
5260
  function sortChannels(channels) {
4925
5261
  return [...channels].sort((left, right) => left.id.localeCompare(right.id));
4926
5262
  }
4927
- function shellQuote2(value) {
5263
+ function shellQuote3(value) {
4928
5264
  return `'${value.replace(/'/g, `'\\''`)}'`;
4929
5265
  }
4930
- function hasCommand(binary) {
5266
+ function hasCommand2(binary) {
4931
5267
  const result = Bun.spawnSync(["bash", "-lc", `command -v ${binary} >/dev/null 2>&1`], {
4932
5268
  stdout: "ignore",
4933
5269
  stderr: "ignore",
@@ -4952,7 +5288,7 @@ Content-Type: text/plain; charset=utf-8
4952
5288
 
4953
5289
  ${message}
4954
5290
  `;
4955
- if (hasCommand("sendmail")) {
5291
+ if (hasCommand2("sendmail")) {
4956
5292
  const result = Bun.spawnSync(["bash", "-lc", "sendmail -t"], {
4957
5293
  stdin: new TextEncoder().encode(body),
4958
5294
  stdout: "pipe",
@@ -4970,8 +5306,8 @@ ${message}
4970
5306
  detail: `Delivered via sendmail to ${channel.target}`
4971
5307
  };
4972
5308
  }
4973
- if (hasCommand("mail")) {
4974
- const command = `printf %s ${shellQuote2(message)} | mail -s ${shellQuote2(subject)} ${shellQuote2(channel.target)}`;
5309
+ if (hasCommand2("mail")) {
5310
+ const command = `printf %s ${shellQuote3(message)} | mail -s ${shellQuote3(subject)} ${shellQuote3(channel.target)}`;
4975
5311
  const result = Bun.spawnSync(["bash", "-lc", command], {
4976
5312
  stdout: "pipe",
4977
5313
  stderr: "pipe",
@@ -5064,7 +5400,7 @@ function getDefaultNotificationConfig() {
5064
5400
  };
5065
5401
  }
5066
5402
  function readNotificationConfig(path = getNotificationsPath()) {
5067
- if (!existsSync5(path)) {
5403
+ if (!existsSync6(path)) {
5068
5404
  return getDefaultNotificationConfig();
5069
5405
  }
5070
5406
  return notificationConfigSchema.parse(JSON.parse(readFileSync4(path, "utf8")));
@@ -5596,7 +5932,7 @@ function runSetup(machineId, options = {}) {
5596
5932
  }
5597
5933
 
5598
5934
  // src/commands/sync.ts
5599
- import { existsSync as existsSync6, lstatSync, readFileSync as readFileSync5, symlinkSync, copyFileSync } from "fs";
5935
+ import { existsSync as existsSync7, lstatSync, readFileSync as readFileSync5, symlinkSync, copyFileSync } from "fs";
5600
5936
  function quote4(value) {
5601
5937
  return `'${value.replace(/'/g, `'\\''`)}'`;
5602
5938
  }
@@ -5644,8 +5980,8 @@ function detectPackageActions(machine) {
5644
5980
  }
5645
5981
  function detectFileActions(machine) {
5646
5982
  return (machine.files || []).map((file, index) => {
5647
- const sourceExists = existsSync6(file.source);
5648
- const targetExists = existsSync6(file.target);
5983
+ const sourceExists = existsSync7(file.source);
5984
+ const targetExists = existsSync7(file.target);
5649
5985
  let status = "missing";
5650
5986
  if (sourceExists && targetExists) {
5651
5987
  if (file.mode === "symlink") {
@@ -5757,183 +6093,6 @@ function getAgentStatus(machineId = getLocalMachineId()) {
5757
6093
  }));
5758
6094
  }
5759
6095
 
5760
- // src/topology.ts
5761
- import { existsSync as existsSync7 } from "fs";
5762
- import { arch as arch2, hostname as hostname3, platform as platform3, userInfo as userInfo2 } from "os";
5763
- import { spawnSync as spawnSync4 } from "child_process";
5764
- function normalizePlatform2(value = platform3()) {
5765
- const normalized = value.toLowerCase();
5766
- if (normalized === "darwin" || normalized === "macos")
5767
- return "macos";
5768
- if (normalized === "win32" || normalized === "windows")
5769
- return "windows";
5770
- if (normalized === "linux")
5771
- return "linux";
5772
- return value;
5773
- }
5774
- function defaultRunner(command) {
5775
- const result = spawnSync4("bash", ["-c", command], {
5776
- encoding: "utf8",
5777
- env: process.env
5778
- });
5779
- return {
5780
- stdout: result.stdout || "",
5781
- stderr: result.stderr || "",
5782
- exitCode: result.status ?? 1
5783
- };
5784
- }
5785
- function hasCommand2(command, runner) {
5786
- return runner(`command -v ${command} >/dev/null 2>&1`).exitCode === 0;
5787
- }
5788
- function parseTailscaleStatus(raw) {
5789
- try {
5790
- const parsed = JSON.parse(raw);
5791
- if (!parsed || typeof parsed !== "object")
5792
- return null;
5793
- return parsed;
5794
- } catch {
5795
- return null;
5796
- }
5797
- }
5798
- function loadTailscalePeers(runner, warnings) {
5799
- const peers = new Map;
5800
- if (!hasCommand2("tailscale", runner)) {
5801
- warnings.push("tailscale_not_available");
5802
- return peers;
5803
- }
5804
- const result = runner("tailscale status --json");
5805
- if (result.exitCode !== 0) {
5806
- warnings.push(`tailscale_status_failed:${result.stderr.trim() || result.exitCode}`);
5807
- return peers;
5808
- }
5809
- const status = parseTailscaleStatus(result.stdout);
5810
- if (!status) {
5811
- warnings.push("tailscale_status_invalid_json");
5812
- return peers;
5813
- }
5814
- const addPeer = (peer) => {
5815
- if (!peer)
5816
- return;
5817
- const id = peer.HostName || peer.DNSName?.split(".")[0];
5818
- if (id)
5819
- peers.set(id, peer);
5820
- };
5821
- addPeer(status.Self);
5822
- for (const peer of Object.values(status.Peer ?? {}))
5823
- addPeer(peer);
5824
- return peers;
5825
- }
5826
- function machineKeys(machine) {
5827
- return [
5828
- machine.id,
5829
- machine.hostname,
5830
- machine.tailscaleName?.split(".")[0],
5831
- machine.tailscaleName,
5832
- machine.sshAddress?.split("@").pop()
5833
- ].filter((value) => Boolean(value));
5834
- }
5835
- function findTailscalePeer(machine, machineId, peers) {
5836
- if (machine) {
5837
- for (const key of machineKeys(machine)) {
5838
- const peer = peers.get(key) ?? peers.get(key.replace(/\.$/, ""));
5839
- if (peer)
5840
- return peer;
5841
- }
5842
- }
5843
- return peers.get(machineId) ?? null;
5844
- }
5845
- function routeHints(input) {
5846
- const hints = [];
5847
- if (input.machineId === input.localMachineId) {
5848
- hints.push({ kind: "local", target: "localhost", reachable: true });
5849
- }
5850
- if (input.manifest?.sshAddress) {
5851
- hints.push({ kind: "ssh", target: input.manifest.sshAddress, reachable: null });
5852
- }
5853
- if (input.manifest?.hostname) {
5854
- hints.push({ kind: "lan", target: input.manifest.hostname, reachable: null });
5855
- }
5856
- const tailscaleTarget = input.manifest?.tailscaleName ?? input.peer?.DNSName ?? input.peer?.TailscaleIPs?.[0];
5857
- if (tailscaleTarget) {
5858
- hints.push({ kind: "tailscale", target: tailscaleTarget.replace(/\.$/, ""), reachable: input.peer?.Online ?? null });
5859
- }
5860
- return hints;
5861
- }
5862
- function buildEntry(input) {
5863
- const manifest = input.manifest;
5864
- const peer = input.peer;
5865
- const hints = routeHints({
5866
- machineId: input.machineId,
5867
- localMachineId: input.localMachineId,
5868
- manifest,
5869
- peer
5870
- });
5871
- 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");
5872
- const route = selectedRoute?.kind === "ssh" ? "lan" : selectedRoute?.kind ?? "unknown";
5873
- return {
5874
- machine_id: input.machineId,
5875
- hostname: manifest?.hostname ?? peer?.HostName ?? null,
5876
- platform: manifest?.platform ?? (peer?.OS ? normalizePlatform2(peer.OS) : null),
5877
- os: peer?.OS ?? null,
5878
- user: typeof manifest?.metadata?.user === "string" ? manifest.metadata.user : null,
5879
- workspace_path: manifest?.workspacePath ?? null,
5880
- manifest_declared: Boolean(manifest),
5881
- heartbeat_status: input.heartbeat?.status ?? "unknown",
5882
- last_heartbeat_at: input.heartbeat?.updated_at ?? null,
5883
- tailscale: {
5884
- dns_name: manifest?.tailscaleName ?? peer?.DNSName?.replace(/\.$/, "") ?? null,
5885
- ips: peer?.TailscaleIPs ?? [],
5886
- online: peer?.Online ?? null,
5887
- active: peer?.Active ?? null,
5888
- last_seen: peer?.LastSeen ?? null
5889
- },
5890
- ssh: {
5891
- address: manifest?.sshAddress ?? null,
5892
- route,
5893
- command_target: selectedRoute?.target ?? null
5894
- },
5895
- route_hints: hints,
5896
- tags: manifest?.tags ?? [],
5897
- metadata: manifest?.metadata ?? {}
5898
- };
5899
- }
5900
- function discoverMachineTopology(options = {}) {
5901
- const now = options.now ?? new Date;
5902
- const runner = options.runner ?? defaultRunner;
5903
- const warnings = [];
5904
- const manifest = readManifest();
5905
- const heartbeats = listHeartbeats();
5906
- const heartbeatByMachine = new Map(heartbeats.map((heartbeat) => [heartbeat.machine_id, heartbeat]));
5907
- const localMachineId = getLocalMachineId();
5908
- const peers = options.includeTailscale === false ? new Map : loadTailscalePeers(runner, warnings);
5909
- const machineIds = new Set([
5910
- localMachineId,
5911
- ...manifest.machines.map((machine) => machine.id),
5912
- ...heartbeats.map((heartbeat) => heartbeat.machine_id),
5913
- ...peers.keys()
5914
- ]);
5915
- const manifestById = new Map(manifest.machines.map((machine) => [machine.id, machine]));
5916
- const machines = [...machineIds].sort().map((machineId) => {
5917
- const manifestMachine = manifestById.get(machineId);
5918
- return buildEntry({
5919
- machineId,
5920
- localMachineId,
5921
- manifest: manifestMachine,
5922
- peer: findTailscalePeer(manifestMachine ?? null, machineId, peers),
5923
- heartbeat: heartbeatByMachine.get(machineId)
5924
- });
5925
- });
5926
- return {
5927
- generated_at: now.toISOString(),
5928
- local_machine_id: localMachineId,
5929
- local_hostname: hostname3(),
5930
- current_platform: normalizePlatform2(),
5931
- manifest_path_known: existsSync7(getManifestPath()),
5932
- machines,
5933
- warnings
5934
- };
5935
- }
5936
-
5937
6096
  // src/compatibility.ts
5938
6097
  var DEFAULT_COMMANDS = [
5939
6098
  { command: "bun", required: true },
@@ -5942,7 +6101,7 @@ var DEFAULT_COMMANDS = [
5942
6101
  function defaultPackages() {
5943
6102
  return [{ name: "@hasna/machines", command: "machines", expectedVersion: getPackageVersion(), required: true }];
5944
6103
  }
5945
- function shellQuote3(value) {
6104
+ function shellQuote4(value) {
5946
6105
  return `'${value.replace(/'/g, "'\\''")}'`;
5947
6106
  }
5948
6107
  function commandId(value) {
@@ -5993,7 +6152,7 @@ function defaultRunner2(machineId, command) {
5993
6152
  return runMachineCommand(machineId, command);
5994
6153
  }
5995
6154
  function inspectCommand(machineId, spec, runner) {
5996
- const command = shellQuote3(spec.command);
6155
+ const command = shellQuote4(spec.command);
5997
6156
  const versionArgs = spec.versionArgs ?? "--version";
5998
6157
  const script = [
5999
6158
  `cmd=${command}`,
@@ -6022,7 +6181,7 @@ function fieldCommand(field) {
6022
6181
  }
6023
6182
  function inspectWorkspace(machineId, spec, runner) {
6024
6183
  const script = [
6025
- `path=${shellQuote3(spec.path)}`,
6184
+ `path=${shellQuote4(spec.path)}`,
6026
6185
  'printf "exists=%s\\n" "$(test -d "$path" && printf yes || printf no)"',
6027
6186
  'pkg="$path/package.json"',
6028
6187
  'printf "package_json=%s\\n" "$(test -f "$pkg" && printf yes || printf no)"',
@@ -6160,6 +6319,17 @@ function checkMachineCompatibility(options = {}) {
6160
6319
  fail: checks.filter((check2) => check2.status === "fail").length
6161
6320
  };
6162
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
+ },
6163
6333
  ok: summary.fail === 0,
6164
6334
  machine_id: machineId,
6165
6335
  source: checks[0]?.source ?? "local",
@@ -6579,8 +6749,11 @@ function createMcpServer(version) {
6579
6749
  }));
6580
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) }] }));
6581
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
+ }));
6582
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 }) => ({
6583
- content: [{ type: "text", text: JSON.stringify({ resolved: resolveSshTarget(machine_id), command: buildSshCommand(machine_id, remote_command) }, null, 2) }]
6756
+ content: [{ type: "text", text: JSON.stringify({ resolved: resolveMachineRoute(machine_id), command: buildSshCommand(machine_id, remote_command) }, null, 2) }]
6584
6757
  }));
6585
6758
  server.tool("machines_ports", "List listening ports on a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({
6586
6759
  content: [{ type: "text", text: JSON.stringify(listPorts(machine_id), null, 2) }]