@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/dist/index.js CHANGED
@@ -11085,9 +11085,31 @@ function detectCurrentMachineManifest() {
11085
11085
  };
11086
11086
  }
11087
11087
  // src/topology.ts
11088
- import { existsSync as existsSync3 } from "fs";
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: null });
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: null });
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 = hints.find((hint) => hint.kind === "local") ?? hints.find((hint) => hint.kind === "ssh") ?? hints.find((hint) => hint.kind === "lan") ?? hints.find((hint) => hint.kind === "tailscale");
11199
- const route = selectedRoute?.kind === "ssh" ? "lan" : selectedRoute?.kind ?? "unknown";
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: existsSync3(getManifestPath()),
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,49 +11459,28 @@ function getLocalMachineTopology(options = {}) {
11280
11459
  };
11281
11460
  }
11282
11461
  // src/remote.ts
11283
- import { spawnSync as spawnSync3 } from "child_process";
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
- import { spawnSync as spawnSync2 } from "child_process";
11288
- function envReachableHosts() {
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;
11301
- }
11302
- function resolveSshTarget(machineId) {
11303
- const machine = getManifestMachine(machineId);
11304
- if (!machine) {
11305
- throw new Error(`Machine not found in manifest: ${machineId}`);
11466
+ function resolveSshTarget(machineId, options = {}) {
11467
+ const resolved = resolveMachineRoute(machineId, options);
11468
+ if (!resolved.ok || !resolved.target) {
11469
+ throw new Error(`Machine route not found: ${machineId}`);
11306
11470
  }
11307
- const current = detectCurrentMachineManifest();
11308
- if (machine.id === current.id) {
11309
- return {
11310
- machineId,
11311
- target: "localhost",
11312
- route: "local"
11313
- };
11471
+ if (resolved.route !== "local" && resolved.route !== "lan" && resolved.route !== "tailscale" && resolved.route !== "ssh") {
11472
+ throw new Error(`Machine route is not SSH-capable: ${machineId}`);
11314
11473
  }
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
11474
  return {
11319
- machineId,
11320
- target: route === "lan" ? lanTarget : tailscaleTarget,
11321
- route
11475
+ machineId: resolved.machine_id ?? machineId,
11476
+ target: resolved.target,
11477
+ route: resolved.route,
11478
+ confidence: resolved.confidence,
11479
+ warnings: resolved.warnings
11322
11480
  };
11323
11481
  }
11324
- function buildSshCommand(machineId, remoteCommand) {
11325
- const resolved = resolveSshTarget(machineId);
11482
+ function buildSshCommand(machineId, remoteCommand, options = {}) {
11483
+ const resolved = resolveSshTarget(machineId, options);
11326
11484
  return remoteCommand ? `ssh ${resolved.target} ${JSON.stringify(remoteCommand)}` : `ssh ${resolved.target}`;
11327
11485
  }
11328
11486
 
@@ -11343,7 +11501,8 @@ function resolveMachineCommand(machineId, command, localMachineId = getLocalMach
11343
11501
  shellCommand: buildSshCommand(machineId, command)
11344
11502
  };
11345
11503
  } catch (error) {
11346
- if (String(error.message ?? error).includes("Machine not found in manifest")) {
11504
+ const message = String(error.message ?? error);
11505
+ if (message.includes("Machine route not found") || message.includes("Machine not found in manifest")) {
11347
11506
  return { source: "ssh", shellCommand: `ssh ${shellQuote(machineId)} ${shellQuote(command)}` };
11348
11507
  }
11349
11508
  throw error;
@@ -11351,7 +11510,7 @@ function resolveMachineCommand(machineId, command, localMachineId = getLocalMach
11351
11510
  }
11352
11511
  function runMachineCommand(machineId, command) {
11353
11512
  const resolved = resolveMachineCommand(machineId, command);
11354
- const result = spawnSync3("bash", ["-c", resolved.shellCommand], {
11513
+ const result = spawnSync2("bash", ["-c", resolved.shellCommand], {
11355
11514
  encoding: "utf8",
11356
11515
  env: process.env
11357
11516
  });
@@ -11364,24 +11523,6 @@ function runMachineCommand(machineId, command) {
11364
11523
  };
11365
11524
  }
11366
11525
 
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
11526
  // src/compatibility.ts
11386
11527
  var DEFAULT_COMMANDS = [
11387
11528
  { command: "bun", required: true },
@@ -11608,6 +11749,17 @@ function checkMachineCompatibility(options = {}) {
11608
11749
  fail: checks.filter((check) => check.status === "fail").length
11609
11750
  };
11610
11751
  return {
11752
+ schema_version: MACHINES_CONSUMER_CONTRACT_VERSION,
11753
+ package: {
11754
+ name: MACHINES_PACKAGE_NAME,
11755
+ version: getPackageVersion()
11756
+ },
11757
+ capabilities: {
11758
+ topology: true,
11759
+ compatibility: true,
11760
+ route_resolution: true,
11761
+ cli_json_fallback: true
11762
+ },
11611
11763
  ok: summary.fail === 0,
11612
11764
  machine_id: machineId,
11613
11765
  source: checks[0]?.source ?? "local",
@@ -12502,7 +12654,7 @@ async function testNotificationChannel(channelId, event = "manual.test", message
12502
12654
  };
12503
12655
  }
12504
12656
  // src/commands/ports.ts
12505
- import { spawnSync as spawnSync4 } from "child_process";
12657
+ import { spawnSync as spawnSync3 } from "child_process";
12506
12658
  function parseSsOutput(output) {
12507
12659
  return output.trim().split(`
12508
12660
  `).map((line) => line.trim()).filter(Boolean).map((line) => {
@@ -12544,7 +12696,7 @@ function listPorts(machineId) {
12544
12696
  const isLocal = targetMachineId === getLocalMachineId();
12545
12697
  const localCommand = "if command -v ss >/dev/null 2>&1; then ss -ltnpH; else lsof -nP -iTCP -sTCP:LISTEN; fi";
12546
12698
  const command = isLocal ? localCommand : buildSshCommand(targetMachineId, localCommand);
12547
- const result = spawnSync4("bash", ["-lc", command], { encoding: "utf8" });
12699
+ const result = spawnSync3("bash", ["-lc", command], { encoding: "utf8" });
12548
12700
  if (result.status !== 0) {
12549
12701
  throw new Error(result.stderr || `Failed to list ports for ${targetMachineId}`);
12550
12702
  }
@@ -22072,6 +22224,7 @@ var MACHINE_MCP_TOOL_NAMES = [
22072
22224
  "machines_install_claude_diff",
22073
22225
  "machines_install_claude_preview",
22074
22226
  "machines_install_claude_apply",
22227
+ "machines_route_resolve",
22075
22228
  "machines_ssh_resolve",
22076
22229
  "machines_ports",
22077
22230
  "machines_backup_preview",
@@ -22182,8 +22335,11 @@ function createMcpServer(version2) {
22182
22335
  }));
22183
22336
  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
22337
  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) }] }));
22338
+ 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 }) => ({
22339
+ content: [{ type: "text", text: JSON.stringify(resolveMachineRoute(machine_id, { includeTailscale: include_tailscale !== false }), null, 2) }]
22340
+ }));
22185
22341
  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: resolveSshTarget(machine_id), command: buildSshCommand(machine_id, remote_command) }, null, 2) }]
22342
+ content: [{ type: "text", text: JSON.stringify({ resolved: resolveMachineRoute(machine_id), command: buildSshCommand(machine_id, remote_command) }, null, 2) }]
22187
22343
  }));
22188
22344
  server.tool("machines_ports", "List listening ports on a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({
22189
22345
  content: [{ type: "text", text: JSON.stringify(listPorts(machine_id), null, 2) }]
@@ -22248,6 +22404,7 @@ export {
22248
22404
  runAppsInstall,
22249
22405
  resolveTables,
22250
22406
  resolveSshTarget,
22407
+ resolveMachineRoute,
22251
22408
  renderDomainMapping,
22252
22409
  renderDashboardHtml,
22253
22410
  removeNotificationChannel,
@@ -22331,5 +22488,7 @@ export {
22331
22488
  MACHINES_STORAGE_MODE_ENV,
22332
22489
  MACHINES_STORAGE_FALLBACK_ENV,
22333
22490
  MACHINES_STORAGE_ENV,
22491
+ MACHINES_PACKAGE_NAME,
22492
+ MACHINES_CONSUMER_CONTRACT_VERSION,
22334
22493
  CROSSREFS_KEY
22335
22494
  };