@hasna/machines 0.0.14 → 0.0.16
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/LICENSE +2 -1
- package/README.md +87 -0
- package/dist/cli/index.js +1726 -179
- package/dist/commands/heal-daemon.d.ts +36 -0
- package/dist/commands/heal-daemon.d.ts.map +1 -0
- package/dist/commands/heal.d.ts +122 -0
- package/dist/commands/heal.d.ts.map +1 -0
- package/dist/compatibility.d.ts +55 -0
- package/dist/compatibility.d.ts.map +1 -0
- package/dist/db.d.ts +1 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1040 -185
- package/dist/mcp/http.d.ts +12 -0
- package/dist/mcp/http.d.ts.map +1 -0
- package/dist/mcp/index.js +954 -75
- package/dist/mcp/server.d.ts +2 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/pg-migrations.d.ts +7 -0
- package/dist/pg-migrations.d.ts.map +1 -0
- package/dist/remote-storage.d.ts +10 -0
- package/dist/remote-storage.d.ts.map +1 -0
- package/dist/remote.d.ts +5 -1
- package/dist/remote.d.ts.map +1 -1
- package/dist/storage-sync.d.ts +58 -0
- package/dist/storage-sync.d.ts.map +1 -0
- package/dist/storage.d.ts +5 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +557 -0
- package/dist/topology.d.ts +55 -0
- package/dist/topology.d.ts.map +1 -0
- package/dist/types.d.ts +3 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +8 -2
package/dist/mcp/index.js
CHANGED
|
@@ -16,11 +16,30 @@ var __export = (target, all) => {
|
|
|
16
16
|
};
|
|
17
17
|
|
|
18
18
|
// src/mcp/index.ts
|
|
19
|
-
import { readFileSync as readFileSync6 } from "fs";
|
|
20
|
-
import { dirname as dirname4, join as join6 } from "path";
|
|
21
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
22
19
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
23
20
|
|
|
21
|
+
// src/version.ts
|
|
22
|
+
import { existsSync, readFileSync } from "fs";
|
|
23
|
+
import { dirname, join } from "path";
|
|
24
|
+
import { fileURLToPath } from "url";
|
|
25
|
+
function getPackageVersion() {
|
|
26
|
+
try {
|
|
27
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
const candidates = [join(here, "..", "package.json"), join(here, "..", "..", "package.json")];
|
|
29
|
+
const pkgPath = candidates.find((candidate) => existsSync(candidate));
|
|
30
|
+
if (!pkgPath) {
|
|
31
|
+
return "0.0.0";
|
|
32
|
+
}
|
|
33
|
+
return JSON.parse(readFileSync(pkgPath, "utf8")).version || "0.0.0";
|
|
34
|
+
} catch {
|
|
35
|
+
return "0.0.0";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/mcp/http.ts
|
|
40
|
+
import { createServer } from "http";
|
|
41
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
42
|
+
|
|
24
43
|
// src/mcp/server.ts
|
|
25
44
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
26
45
|
|
|
@@ -3999,20 +4018,20 @@ var coerce = {
|
|
|
3999
4018
|
var NEVER = INVALID;
|
|
4000
4019
|
// src/commands/backup.ts
|
|
4001
4020
|
import { homedir } from "os";
|
|
4002
|
-
import { join } from "path";
|
|
4021
|
+
import { join as join2 } from "path";
|
|
4003
4022
|
function quote(value) {
|
|
4004
4023
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
4005
4024
|
}
|
|
4006
4025
|
function defaultBackupSources() {
|
|
4007
4026
|
const home = homedir();
|
|
4008
4027
|
return [
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4028
|
+
join2(home, ".hasna"),
|
|
4029
|
+
join2(home, ".ssh"),
|
|
4030
|
+
join2(home, ".secrets")
|
|
4012
4031
|
];
|
|
4013
4032
|
}
|
|
4014
4033
|
function buildBackupPlan(bucket, prefix = "machines") {
|
|
4015
|
-
const archivePath =
|
|
4034
|
+
const archivePath = join2(homedir(), ".hasna", "machines", "backup.tgz");
|
|
4016
4035
|
const sources = defaultBackupSources();
|
|
4017
4036
|
const steps = [
|
|
4018
4037
|
{
|
|
@@ -4063,33 +4082,33 @@ function runBackup(bucket, prefix = "machines", options = {}) {
|
|
|
4063
4082
|
}
|
|
4064
4083
|
|
|
4065
4084
|
// src/manifests.ts
|
|
4066
|
-
import { existsSync as
|
|
4085
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
4067
4086
|
import { arch, homedir as homedir2, hostname, platform, userInfo } from "os";
|
|
4068
|
-
import { dirname as
|
|
4087
|
+
import { dirname as dirname3 } from "path";
|
|
4069
4088
|
|
|
4070
4089
|
// src/paths.ts
|
|
4071
|
-
import { existsSync, mkdirSync } from "fs";
|
|
4072
|
-
import { dirname, join as
|
|
4090
|
+
import { existsSync as existsSync2, mkdirSync } from "fs";
|
|
4091
|
+
import { dirname as dirname2, join as join3, resolve } from "path";
|
|
4073
4092
|
function homeDir() {
|
|
4074
4093
|
return process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
4075
4094
|
}
|
|
4076
4095
|
function getDataDir() {
|
|
4077
|
-
return process.env["HASNA_MACHINES_DIR"] ||
|
|
4096
|
+
return process.env["HASNA_MACHINES_DIR"] || join3(homeDir(), ".hasna", "machines");
|
|
4078
4097
|
}
|
|
4079
4098
|
function getDbPath() {
|
|
4080
|
-
return process.env["HASNA_MACHINES_DB_PATH"] ||
|
|
4099
|
+
return process.env["HASNA_MACHINES_DB_PATH"] || join3(getDataDir(), "machines.db");
|
|
4081
4100
|
}
|
|
4082
4101
|
function getManifestPath() {
|
|
4083
|
-
return process.env["HASNA_MACHINES_MANIFEST_PATH"] ||
|
|
4102
|
+
return process.env["HASNA_MACHINES_MANIFEST_PATH"] || join3(getDataDir(), "machines.json");
|
|
4084
4103
|
}
|
|
4085
4104
|
function getNotificationsPath() {
|
|
4086
|
-
return process.env["HASNA_MACHINES_NOTIFICATIONS_PATH"] ||
|
|
4105
|
+
return process.env["HASNA_MACHINES_NOTIFICATIONS_PATH"] || join3(getDataDir(), "notifications.json");
|
|
4087
4106
|
}
|
|
4088
4107
|
function ensureParentDir(filePath) {
|
|
4089
4108
|
if (filePath === ":memory:")
|
|
4090
4109
|
return;
|
|
4091
|
-
const dir =
|
|
4092
|
-
if (!
|
|
4110
|
+
const dir = dirname2(resolve(filePath));
|
|
4111
|
+
if (!existsSync2(dir)) {
|
|
4093
4112
|
mkdirSync(dir, { recursive: true });
|
|
4094
4113
|
}
|
|
4095
4114
|
}
|
|
@@ -4154,10 +4173,10 @@ function getDefaultManifest() {
|
|
|
4154
4173
|
};
|
|
4155
4174
|
}
|
|
4156
4175
|
function readManifest(path = getManifestPath()) {
|
|
4157
|
-
if (!
|
|
4176
|
+
if (!existsSync3(path)) {
|
|
4158
4177
|
return getDefaultManifest();
|
|
4159
4178
|
}
|
|
4160
|
-
const raw = JSON.parse(
|
|
4179
|
+
const raw = JSON.parse(readFileSync2(path, "utf8"));
|
|
4161
4180
|
return fleetSchema.parse(raw);
|
|
4162
4181
|
}
|
|
4163
4182
|
function validateManifest(path = getManifestPath()) {
|
|
@@ -4181,7 +4200,7 @@ function getManifestMachine(machineId, path = getManifestPath()) {
|
|
|
4181
4200
|
function detectCurrentMachineManifest() {
|
|
4182
4201
|
const machineId = process.env["HASNA_MACHINES_MACHINE_ID"] || hostname();
|
|
4183
4202
|
const user = userInfo().username;
|
|
4184
|
-
const bunDir =
|
|
4203
|
+
const bunDir = dirname3(process.execPath);
|
|
4185
4204
|
return {
|
|
4186
4205
|
id: machineId,
|
|
4187
4206
|
hostname: hostname(),
|
|
@@ -4199,6 +4218,7 @@ function detectCurrentMachineManifest() {
|
|
|
4199
4218
|
|
|
4200
4219
|
// src/remote.ts
|
|
4201
4220
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
4221
|
+
import { hostname as hostname3 } from "os";
|
|
4202
4222
|
|
|
4203
4223
|
// src/db.ts
|
|
4204
4224
|
import { Database } from "bun:sqlite";
|
|
@@ -4343,18 +4363,37 @@ function buildSshCommand(machineId, remoteCommand) {
|
|
|
4343
4363
|
}
|
|
4344
4364
|
|
|
4345
4365
|
// src/remote.ts
|
|
4366
|
+
function shellQuote(value) {
|
|
4367
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
4368
|
+
}
|
|
4369
|
+
function machineIsLocal(machineId, localMachineId) {
|
|
4370
|
+
return machineId === "local" || machineId === "localhost" || machineId === localMachineId || machineId === hostname3();
|
|
4371
|
+
}
|
|
4372
|
+
function resolveMachineCommand(machineId, command, localMachineId = getLocalMachineId()) {
|
|
4373
|
+
if (machineIsLocal(machineId, localMachineId)) {
|
|
4374
|
+
return { source: "local", shellCommand: command };
|
|
4375
|
+
}
|
|
4376
|
+
try {
|
|
4377
|
+
return {
|
|
4378
|
+
source: resolveSshTarget(machineId).route,
|
|
4379
|
+
shellCommand: buildSshCommand(machineId, command)
|
|
4380
|
+
};
|
|
4381
|
+
} catch (error) {
|
|
4382
|
+
if (String(error.message ?? error).includes("Machine not found in manifest")) {
|
|
4383
|
+
return { source: "ssh", shellCommand: `ssh ${shellQuote(machineId)} ${shellQuote(command)}` };
|
|
4384
|
+
}
|
|
4385
|
+
throw error;
|
|
4386
|
+
}
|
|
4387
|
+
}
|
|
4346
4388
|
function runMachineCommand(machineId, command) {
|
|
4347
|
-
const
|
|
4348
|
-
const
|
|
4349
|
-
const route = isLocal ? "local" : resolveSshTarget(machineId).route;
|
|
4350
|
-
const shellCommand = isLocal ? command : buildSshCommand(machineId, command);
|
|
4351
|
-
const result = spawnSync2("bash", ["-lc", shellCommand], {
|
|
4389
|
+
const resolved = resolveMachineCommand(machineId, command);
|
|
4390
|
+
const result = spawnSync2("bash", ["-c", resolved.shellCommand], {
|
|
4352
4391
|
encoding: "utf8",
|
|
4353
4392
|
env: process.env
|
|
4354
4393
|
});
|
|
4355
4394
|
return {
|
|
4356
4395
|
machineId,
|
|
4357
|
-
source:
|
|
4396
|
+
source: resolved.source,
|
|
4358
4397
|
stdout: result.stdout || "",
|
|
4359
4398
|
stderr: result.stderr || "",
|
|
4360
4399
|
exitCode: result.status ?? 1
|
|
@@ -4374,7 +4413,7 @@ function getAppManager(machine, app) {
|
|
|
4374
4413
|
return "winget";
|
|
4375
4414
|
return "apt";
|
|
4376
4415
|
}
|
|
4377
|
-
function
|
|
4416
|
+
function shellQuote2(value) {
|
|
4378
4417
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
4379
4418
|
}
|
|
4380
4419
|
function buildAppCommand(machine, app) {
|
|
@@ -4395,7 +4434,7 @@ function buildAppCommand(machine, app) {
|
|
|
4395
4434
|
return `sudo apt-get install -y ${packageName}`;
|
|
4396
4435
|
}
|
|
4397
4436
|
function buildAppProbeCommand(machine, app) {
|
|
4398
|
-
const packageName =
|
|
4437
|
+
const packageName = shellQuote2(getPackageName(app));
|
|
4399
4438
|
const manager = getAppManager(machine, app);
|
|
4400
4439
|
if (manager === "custom") {
|
|
4401
4440
|
return `if command -v ${packageName} >/dev/null 2>&1; then printf 'installed=1\\nversion=custom\\n'; else printf 'installed=0\\n'; fi`;
|
|
@@ -4501,20 +4540,20 @@ function runAppsInstall(machineId, options = {}) {
|
|
|
4501
4540
|
|
|
4502
4541
|
// src/commands/cert.ts
|
|
4503
4542
|
import { homedir as homedir3, platform as platform2 } from "os";
|
|
4504
|
-
import { join as
|
|
4543
|
+
import { join as join4 } from "path";
|
|
4505
4544
|
function quote2(value) {
|
|
4506
4545
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
4507
4546
|
}
|
|
4508
4547
|
function certDir() {
|
|
4509
|
-
return
|
|
4548
|
+
return join4(homedir3(), ".hasna", "machines", "certs");
|
|
4510
4549
|
}
|
|
4511
4550
|
function buildCertPlan(domains) {
|
|
4512
4551
|
if (domains.length === 0) {
|
|
4513
4552
|
throw new Error("At least one domain is required.");
|
|
4514
4553
|
}
|
|
4515
4554
|
const primary = domains[0];
|
|
4516
|
-
const certPath =
|
|
4517
|
-
const keyPath =
|
|
4555
|
+
const certPath = join4(certDir(), `${primary}.pem`);
|
|
4556
|
+
const keyPath = join4(certDir(), `${primary}-key.pem`);
|
|
4518
4557
|
const steps = [];
|
|
4519
4558
|
if (platform2() === "darwin") {
|
|
4520
4559
|
steps.push({
|
|
@@ -4578,16 +4617,16 @@ function runCertPlan(domains, options = {}) {
|
|
|
4578
4617
|
}
|
|
4579
4618
|
|
|
4580
4619
|
// src/commands/dns.ts
|
|
4581
|
-
import { existsSync as
|
|
4582
|
-
import { join as
|
|
4620
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
4621
|
+
import { join as join5 } from "path";
|
|
4583
4622
|
function getDnsPath() {
|
|
4584
|
-
return
|
|
4623
|
+
return join5(getDataDir(), "dns.json");
|
|
4585
4624
|
}
|
|
4586
4625
|
function readMappings() {
|
|
4587
4626
|
const path = getDnsPath();
|
|
4588
|
-
if (!
|
|
4627
|
+
if (!existsSync4(path))
|
|
4589
4628
|
return [];
|
|
4590
|
-
return JSON.parse(
|
|
4629
|
+
return JSON.parse(readFileSync3(path, "utf8"));
|
|
4591
4630
|
}
|
|
4592
4631
|
function writeMappings(mappings) {
|
|
4593
4632
|
const path = getDnsPath();
|
|
@@ -4614,10 +4653,10 @@ function renderDomainMapping(domain) {
|
|
|
4614
4653
|
hostsEntry: `${entry.targetHost} ${entry.domain}`,
|
|
4615
4654
|
caddySnippet: `${entry.domain} {
|
|
4616
4655
|
reverse_proxy 127.0.0.1:${entry.port}
|
|
4617
|
-
tls ${
|
|
4656
|
+
tls ${join5(getDataDir(), "certs", `${entry.domain}.pem`)} ${join5(getDataDir(), "certs", `${entry.domain}-key.pem`)}
|
|
4618
4657
|
}`,
|
|
4619
|
-
certPath:
|
|
4620
|
-
keyPath:
|
|
4658
|
+
certPath: join5(getDataDir(), "certs", `${entry.domain}.pem`),
|
|
4659
|
+
keyPath: join5(getDataDir(), "certs", `${entry.domain}-key.pem`)
|
|
4621
4660
|
};
|
|
4622
4661
|
}
|
|
4623
4662
|
|
|
@@ -4889,7 +4928,7 @@ function runTailscaleInstall(machineId, options = {}) {
|
|
|
4889
4928
|
}
|
|
4890
4929
|
|
|
4891
4930
|
// src/commands/notifications.ts
|
|
4892
|
-
import { existsSync as
|
|
4931
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
4893
4932
|
var notificationChannelSchema = exports_external.object({
|
|
4894
4933
|
id: exports_external.string(),
|
|
4895
4934
|
type: exports_external.enum(["email", "webhook", "command"]),
|
|
@@ -4905,7 +4944,7 @@ var notificationConfigSchema = exports_external.object({
|
|
|
4905
4944
|
function sortChannels(channels) {
|
|
4906
4945
|
return [...channels].sort((left, right) => left.id.localeCompare(right.id));
|
|
4907
4946
|
}
|
|
4908
|
-
function
|
|
4947
|
+
function shellQuote3(value) {
|
|
4909
4948
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
4910
4949
|
}
|
|
4911
4950
|
function hasCommand(binary) {
|
|
@@ -4952,7 +4991,7 @@ ${message}
|
|
|
4952
4991
|
};
|
|
4953
4992
|
}
|
|
4954
4993
|
if (hasCommand("mail")) {
|
|
4955
|
-
const command = `printf %s ${
|
|
4994
|
+
const command = `printf %s ${shellQuote3(message)} | mail -s ${shellQuote3(subject)} ${shellQuote3(channel.target)}`;
|
|
4956
4995
|
const result = Bun.spawnSync(["bash", "-lc", command], {
|
|
4957
4996
|
stdout: "pipe",
|
|
4958
4997
|
stderr: "pipe",
|
|
@@ -5045,10 +5084,10 @@ function getDefaultNotificationConfig() {
|
|
|
5045
5084
|
};
|
|
5046
5085
|
}
|
|
5047
5086
|
function readNotificationConfig(path = getNotificationsPath()) {
|
|
5048
|
-
if (!
|
|
5087
|
+
if (!existsSync5(path)) {
|
|
5049
5088
|
return getDefaultNotificationConfig();
|
|
5050
5089
|
}
|
|
5051
|
-
return notificationConfigSchema.parse(JSON.parse(
|
|
5090
|
+
return notificationConfigSchema.parse(JSON.parse(readFileSync4(path, "utf8")));
|
|
5052
5091
|
}
|
|
5053
5092
|
function writeNotificationConfig(config, path = getNotificationsPath()) {
|
|
5054
5093
|
ensureParentDir(path);
|
|
@@ -5220,24 +5259,6 @@ function manifestValidate() {
|
|
|
5220
5259
|
return validateManifest(getManifestPath());
|
|
5221
5260
|
}
|
|
5222
5261
|
|
|
5223
|
-
// src/version.ts
|
|
5224
|
-
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
5225
|
-
import { dirname as dirname3, join as join5 } from "path";
|
|
5226
|
-
import { fileURLToPath } from "url";
|
|
5227
|
-
function getPackageVersion() {
|
|
5228
|
-
try {
|
|
5229
|
-
const here = dirname3(fileURLToPath(import.meta.url));
|
|
5230
|
-
const candidates = [join5(here, "..", "package.json"), join5(here, "..", "..", "package.json")];
|
|
5231
|
-
const pkgPath = candidates.find((candidate) => existsSync5(candidate));
|
|
5232
|
-
if (!pkgPath) {
|
|
5233
|
-
return "0.0.0";
|
|
5234
|
-
}
|
|
5235
|
-
return JSON.parse(readFileSync4(pkgPath, "utf8")).version || "0.0.0";
|
|
5236
|
-
} catch {
|
|
5237
|
-
return "0.0.0";
|
|
5238
|
-
}
|
|
5239
|
-
}
|
|
5240
|
-
|
|
5241
5262
|
// src/commands/status.ts
|
|
5242
5263
|
function getStatus() {
|
|
5243
5264
|
const manifest = readManifest();
|
|
@@ -5756,7 +5777,742 @@ function getAgentStatus(machineId = getLocalMachineId()) {
|
|
|
5756
5777
|
}));
|
|
5757
5778
|
}
|
|
5758
5779
|
|
|
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
|
+
// src/compatibility.ts
|
|
5958
|
+
var DEFAULT_COMMANDS = [
|
|
5959
|
+
{ command: "bun", required: true },
|
|
5960
|
+
{ command: "machines", required: true }
|
|
5961
|
+
];
|
|
5962
|
+
function defaultPackages() {
|
|
5963
|
+
return [{ name: "@hasna/machines", command: "machines", expectedVersion: getPackageVersion(), required: true }];
|
|
5964
|
+
}
|
|
5965
|
+
function shellQuote4(value) {
|
|
5966
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
5967
|
+
}
|
|
5968
|
+
function commandId(value) {
|
|
5969
|
+
return value.replace(/[^a-zA-Z0-9_.@/-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
5970
|
+
}
|
|
5971
|
+
function packageCommand(name) {
|
|
5972
|
+
if (name === "@hasna/knowledge")
|
|
5973
|
+
return "knowledge";
|
|
5974
|
+
if (name === "@hasna/machines")
|
|
5975
|
+
return "machines";
|
|
5976
|
+
return name.split("/").pop() ?? name;
|
|
5977
|
+
}
|
|
5978
|
+
function firstLine(value) {
|
|
5979
|
+
return value.trim().split(/\r?\n/).find(Boolean) ?? "";
|
|
5980
|
+
}
|
|
5981
|
+
function extractVersion(value) {
|
|
5982
|
+
const match = value.match(/\b\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?\b/);
|
|
5983
|
+
return match?.[0] ?? null;
|
|
5984
|
+
}
|
|
5985
|
+
function statusFor(required, ok) {
|
|
5986
|
+
if (ok)
|
|
5987
|
+
return "ok";
|
|
5988
|
+
return required === false ? "warn" : "fail";
|
|
5989
|
+
}
|
|
5990
|
+
function makeCheck2(input) {
|
|
5991
|
+
return {
|
|
5992
|
+
id: input.id,
|
|
5993
|
+
kind: input.kind,
|
|
5994
|
+
status: input.status,
|
|
5995
|
+
target: input.target,
|
|
5996
|
+
expected: input.expected ?? null,
|
|
5997
|
+
actual: input.actual ?? null,
|
|
5998
|
+
detail: input.detail,
|
|
5999
|
+
source: input.source
|
|
6000
|
+
};
|
|
6001
|
+
}
|
|
6002
|
+
function parseKeyValue(stdout) {
|
|
6003
|
+
const result = {};
|
|
6004
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
6005
|
+
const idx = line.indexOf("=");
|
|
6006
|
+
if (idx <= 0)
|
|
6007
|
+
continue;
|
|
6008
|
+
result[line.slice(0, idx)] = line.slice(idx + 1);
|
|
6009
|
+
}
|
|
6010
|
+
return result;
|
|
6011
|
+
}
|
|
6012
|
+
function defaultRunner2(machineId, command) {
|
|
6013
|
+
return runMachineCommand(machineId, command);
|
|
6014
|
+
}
|
|
6015
|
+
function inspectCommand(machineId, spec, runner) {
|
|
6016
|
+
const command = shellQuote4(spec.command);
|
|
6017
|
+
const versionArgs = spec.versionArgs ?? "--version";
|
|
6018
|
+
const script = [
|
|
6019
|
+
`cmd=${command}`,
|
|
6020
|
+
'path="$(command -v "$cmd" 2>/dev/null || true)"',
|
|
6021
|
+
'printf "path=%s\\n" "$path"',
|
|
6022
|
+
'if [ -n "$path" ]; then version="$("$cmd" ' + versionArgs + ' 2>/dev/null || true)"; printf "version=%s\\n" "$version"; fi'
|
|
6023
|
+
].join("; ");
|
|
6024
|
+
const result = runner(machineId, script);
|
|
6025
|
+
const parsed = parseKeyValue(result.stdout);
|
|
6026
|
+
return {
|
|
6027
|
+
path: parsed.path || null,
|
|
6028
|
+
version: parsed.version ? firstLine(parsed.version) : null,
|
|
6029
|
+
exitCode: result.exitCode,
|
|
6030
|
+
source: result.source,
|
|
6031
|
+
stderr: result.stderr
|
|
6032
|
+
};
|
|
6033
|
+
}
|
|
6034
|
+
function fieldCommand(field) {
|
|
6035
|
+
const regex = field === "name" ? String.raw`s/.*"name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p` : String.raw`s/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p`;
|
|
6036
|
+
return [
|
|
6037
|
+
`if command -v bun >/dev/null 2>&1; then bun -e "const p=JSON.parse(await Bun.file(process.argv[1]).text()); console.log(p.${field} ?? '')" "$pkg" 2>/dev/null`,
|
|
6038
|
+
`elif command -v node >/dev/null 2>&1; then node -e "const fs=require('fs'); const p=JSON.parse(fs.readFileSync(process.argv[1], 'utf8')); console.log(p.${field} || '')" "$pkg" 2>/dev/null`,
|
|
6039
|
+
`else sed -n '${regex}' "$pkg" | head -n 1`,
|
|
6040
|
+
"fi"
|
|
6041
|
+
].join("; ");
|
|
6042
|
+
}
|
|
6043
|
+
function inspectWorkspace(machineId, spec, runner) {
|
|
6044
|
+
const script = [
|
|
6045
|
+
`path=${shellQuote4(spec.path)}`,
|
|
6046
|
+
'printf "exists=%s\\n" "$(test -d "$path" && printf yes || printf no)"',
|
|
6047
|
+
'pkg="$path/package.json"',
|
|
6048
|
+
'printf "package_json=%s\\n" "$(test -f "$pkg" && printf yes || printf no)"',
|
|
6049
|
+
`if [ -f "$pkg" ]; then printf "package_name=%s\\n" "$(${fieldCommand("name")})"; printf "version=%s\\n" "$(${fieldCommand("version")})"; fi`
|
|
6050
|
+
].join("; ");
|
|
6051
|
+
const result = runner(machineId, script);
|
|
6052
|
+
const parsed = parseKeyValue(result.stdout);
|
|
6053
|
+
return {
|
|
6054
|
+
exists: parsed.exists === "yes",
|
|
6055
|
+
packageJson: parsed.package_json === "yes",
|
|
6056
|
+
packageName: parsed.package_name || null,
|
|
6057
|
+
version: parsed.version || null,
|
|
6058
|
+
source: result.source,
|
|
6059
|
+
stderr: result.stderr
|
|
6060
|
+
};
|
|
6061
|
+
}
|
|
6062
|
+
function commandCheck(machineId, spec, runner) {
|
|
6063
|
+
const inspection = inspectCommand(machineId, spec, runner);
|
|
6064
|
+
const found = Boolean(inspection.path);
|
|
6065
|
+
const checks = [
|
|
6066
|
+
makeCheck2({
|
|
6067
|
+
id: `command:${commandId(spec.command)}:path`,
|
|
6068
|
+
kind: "command",
|
|
6069
|
+
status: statusFor(spec.required, found),
|
|
6070
|
+
target: spec.command,
|
|
6071
|
+
expected: "available",
|
|
6072
|
+
actual: inspection.path ?? "missing",
|
|
6073
|
+
detail: found ? `found at ${inspection.path}` : inspection.stderr || "command missing",
|
|
6074
|
+
source: inspection.source
|
|
6075
|
+
})
|
|
6076
|
+
];
|
|
6077
|
+
if (spec.expectedVersion) {
|
|
6078
|
+
const actualVersion = extractVersion(inspection.version ?? "");
|
|
6079
|
+
checks.push(makeCheck2({
|
|
6080
|
+
id: `command:${commandId(spec.command)}:version`,
|
|
6081
|
+
kind: "command",
|
|
6082
|
+
status: actualVersion === spec.expectedVersion ? "ok" : statusFor(spec.required, false),
|
|
6083
|
+
target: spec.command,
|
|
6084
|
+
expected: spec.expectedVersion,
|
|
6085
|
+
actual: actualVersion ?? inspection.version ?? "missing",
|
|
6086
|
+
detail: actualVersion ? `version output: ${inspection.version}` : "version unavailable",
|
|
6087
|
+
source: inspection.source
|
|
6088
|
+
}));
|
|
6089
|
+
}
|
|
6090
|
+
return checks;
|
|
6091
|
+
}
|
|
6092
|
+
function packageCheck(machineId, spec, runner) {
|
|
6093
|
+
const command = spec.command ?? packageCommand(spec.name);
|
|
6094
|
+
const inspection = inspectCommand(machineId, { command, expectedVersion: spec.expectedVersion, required: spec.required }, runner);
|
|
6095
|
+
const found = Boolean(inspection.path);
|
|
6096
|
+
const checks = [
|
|
6097
|
+
makeCheck2({
|
|
6098
|
+
id: `package:${commandId(spec.name)}:command`,
|
|
6099
|
+
kind: "package",
|
|
6100
|
+
status: statusFor(spec.required, found),
|
|
6101
|
+
target: spec.name,
|
|
6102
|
+
expected: command,
|
|
6103
|
+
actual: inspection.path ?? "missing",
|
|
6104
|
+
detail: found ? `${command} found at ${inspection.path}` : `${command} command missing`,
|
|
6105
|
+
source: inspection.source
|
|
6106
|
+
})
|
|
6107
|
+
];
|
|
6108
|
+
if (spec.expectedVersion) {
|
|
6109
|
+
const actualVersion = extractVersion(inspection.version ?? "");
|
|
6110
|
+
checks.push(makeCheck2({
|
|
6111
|
+
id: `package:${commandId(spec.name)}:version`,
|
|
6112
|
+
kind: "package",
|
|
6113
|
+
status: actualVersion === spec.expectedVersion ? "ok" : statusFor(spec.required, false),
|
|
6114
|
+
target: spec.name,
|
|
6115
|
+
expected: spec.expectedVersion,
|
|
6116
|
+
actual: actualVersion ?? inspection.version ?? "missing",
|
|
6117
|
+
detail: actualVersion ? `version output: ${inspection.version}` : "version unavailable",
|
|
6118
|
+
source: inspection.source
|
|
6119
|
+
}));
|
|
6120
|
+
}
|
|
6121
|
+
return checks;
|
|
6122
|
+
}
|
|
6123
|
+
function workspaceCheck(machineId, spec, runner) {
|
|
6124
|
+
const inspection = inspectWorkspace(machineId, spec, runner);
|
|
6125
|
+
const target = spec.label ?? spec.path;
|
|
6126
|
+
const checks = [
|
|
6127
|
+
makeCheck2({
|
|
6128
|
+
id: `workspace:${commandId(target)}:path`,
|
|
6129
|
+
kind: "workspace",
|
|
6130
|
+
status: statusFor(spec.required, inspection.exists),
|
|
6131
|
+
target,
|
|
6132
|
+
expected: spec.path,
|
|
6133
|
+
actual: inspection.exists ? "exists" : "missing",
|
|
6134
|
+
detail: inspection.exists ? `workspace exists at ${spec.path}` : inspection.stderr || `workspace missing at ${spec.path}`,
|
|
6135
|
+
source: inspection.source
|
|
6136
|
+
})
|
|
6137
|
+
];
|
|
6138
|
+
if (spec.expectedPackageName) {
|
|
6139
|
+
checks.push(makeCheck2({
|
|
6140
|
+
id: `workspace:${commandId(target)}:package-name`,
|
|
6141
|
+
kind: "workspace",
|
|
6142
|
+
status: inspection.packageName === spec.expectedPackageName ? "ok" : statusFor(spec.required, false),
|
|
6143
|
+
target,
|
|
6144
|
+
expected: spec.expectedPackageName,
|
|
6145
|
+
actual: inspection.packageName ?? (inspection.packageJson ? "missing-name" : "missing-package-json"),
|
|
6146
|
+
detail: inspection.packageJson ? "package.json inspected" : "package.json missing",
|
|
6147
|
+
source: inspection.source
|
|
6148
|
+
}));
|
|
6149
|
+
}
|
|
6150
|
+
if (spec.expectedVersion) {
|
|
6151
|
+
checks.push(makeCheck2({
|
|
6152
|
+
id: `workspace:${commandId(target)}:version`,
|
|
6153
|
+
kind: "workspace",
|
|
6154
|
+
status: inspection.version === spec.expectedVersion ? "ok" : statusFor(spec.required, false),
|
|
6155
|
+
target,
|
|
6156
|
+
expected: spec.expectedVersion,
|
|
6157
|
+
actual: inspection.version ?? (inspection.packageJson ? "missing-version" : "missing-package-json"),
|
|
6158
|
+
detail: inspection.packageJson ? "package.json inspected" : "package.json missing",
|
|
6159
|
+
source: inspection.source
|
|
6160
|
+
}));
|
|
6161
|
+
}
|
|
6162
|
+
return checks;
|
|
6163
|
+
}
|
|
6164
|
+
function checkMachineCompatibility(options = {}) {
|
|
6165
|
+
const machineId = options.machineId ?? getLocalMachineId();
|
|
6166
|
+
const runner = options.runner ?? defaultRunner2;
|
|
6167
|
+
const commands = options.commands ?? DEFAULT_COMMANDS;
|
|
6168
|
+
const packages = options.packages ?? defaultPackages();
|
|
6169
|
+
const workspaces = options.workspaces ?? [];
|
|
6170
|
+
const checks = [];
|
|
6171
|
+
for (const spec of commands)
|
|
6172
|
+
checks.push(...commandCheck(machineId, spec, runner));
|
|
6173
|
+
for (const spec of packages)
|
|
6174
|
+
checks.push(...packageCheck(machineId, spec, runner));
|
|
6175
|
+
for (const spec of workspaces)
|
|
6176
|
+
checks.push(...workspaceCheck(machineId, spec, runner));
|
|
6177
|
+
const summary = {
|
|
6178
|
+
ok: checks.filter((check2) => check2.status === "ok").length,
|
|
6179
|
+
warn: checks.filter((check2) => check2.status === "warn").length,
|
|
6180
|
+
fail: checks.filter((check2) => check2.status === "fail").length
|
|
6181
|
+
};
|
|
6182
|
+
return {
|
|
6183
|
+
ok: summary.fail === 0,
|
|
6184
|
+
machine_id: machineId,
|
|
6185
|
+
source: checks[0]?.source ?? "local",
|
|
6186
|
+
generated_at: (options.now ?? new Date).toISOString(),
|
|
6187
|
+
checks,
|
|
6188
|
+
summary
|
|
6189
|
+
};
|
|
6190
|
+
}
|
|
6191
|
+
|
|
6192
|
+
// src/pg-migrations.ts
|
|
6193
|
+
var PG_MIGRATIONS = [
|
|
6194
|
+
`
|
|
6195
|
+
CREATE TABLE IF NOT EXISTS agent_heartbeats (
|
|
6196
|
+
machine_id TEXT NOT NULL,
|
|
6197
|
+
pid INTEGER NOT NULL,
|
|
6198
|
+
status TEXT NOT NULL,
|
|
6199
|
+
updated_at TIMESTAMPTZ NOT NULL,
|
|
6200
|
+
PRIMARY KEY (machine_id, pid)
|
|
6201
|
+
);
|
|
6202
|
+
|
|
6203
|
+
CREATE TABLE IF NOT EXISTS setup_runs (
|
|
6204
|
+
id TEXT PRIMARY KEY,
|
|
6205
|
+
machine_id TEXT NOT NULL,
|
|
6206
|
+
status TEXT NOT NULL,
|
|
6207
|
+
details_json TEXT NOT NULL DEFAULT '[]',
|
|
6208
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
6209
|
+
updated_at TIMESTAMPTZ NOT NULL
|
|
6210
|
+
);
|
|
6211
|
+
|
|
6212
|
+
CREATE TABLE IF NOT EXISTS sync_runs (
|
|
6213
|
+
id TEXT PRIMARY KEY,
|
|
6214
|
+
machine_id TEXT NOT NULL,
|
|
6215
|
+
status TEXT NOT NULL,
|
|
6216
|
+
actions_json TEXT NOT NULL DEFAULT '[]',
|
|
6217
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
6218
|
+
updated_at TIMESTAMPTZ NOT NULL
|
|
6219
|
+
);
|
|
6220
|
+
`
|
|
6221
|
+
];
|
|
6222
|
+
|
|
6223
|
+
// src/remote-storage.ts
|
|
6224
|
+
import pg from "pg";
|
|
6225
|
+
function translatePlaceholders(sql) {
|
|
6226
|
+
let index = 0;
|
|
6227
|
+
return sql.replace(/\?/g, () => `$${++index}`);
|
|
6228
|
+
}
|
|
6229
|
+
function normalizeParams(params) {
|
|
6230
|
+
const flat = params.length === 1 && Array.isArray(params[0]) ? params[0] : params;
|
|
6231
|
+
return flat.map((value) => value === undefined ? null : value);
|
|
6232
|
+
}
|
|
6233
|
+
function sslConfigFor(connectionString) {
|
|
6234
|
+
return connectionString.includes("sslmode=require") || connectionString.includes("ssl=true") ? { rejectUnauthorized: false } : undefined;
|
|
6235
|
+
}
|
|
6236
|
+
|
|
6237
|
+
class PgAdapterAsync {
|
|
6238
|
+
pool;
|
|
6239
|
+
constructor(connectionString) {
|
|
6240
|
+
this.pool = new pg.Pool({ connectionString, ssl: sslConfigFor(connectionString) });
|
|
6241
|
+
}
|
|
6242
|
+
async run(sql, ...params) {
|
|
6243
|
+
const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
|
|
6244
|
+
return { changes: result.rowCount ?? 0 };
|
|
6245
|
+
}
|
|
6246
|
+
async all(sql, ...params) {
|
|
6247
|
+
const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
|
|
6248
|
+
return result.rows;
|
|
6249
|
+
}
|
|
6250
|
+
async close() {
|
|
6251
|
+
await this.pool.end();
|
|
6252
|
+
}
|
|
6253
|
+
}
|
|
6254
|
+
|
|
6255
|
+
// src/storage-sync.ts
|
|
6256
|
+
var STORAGE_TABLES = [
|
|
6257
|
+
"agent_heartbeats",
|
|
6258
|
+
"setup_runs",
|
|
6259
|
+
"sync_runs"
|
|
6260
|
+
];
|
|
6261
|
+
var MACHINES_STORAGE_ENV = "HASNA_MACHINES_DATABASE_URL";
|
|
6262
|
+
var MACHINES_STORAGE_FALLBACK_ENV = "MACHINES_DATABASE_URL";
|
|
6263
|
+
var MACHINES_STORAGE_MODE_ENV = "HASNA_MACHINES_STORAGE_MODE";
|
|
6264
|
+
var MACHINES_STORAGE_MODE_FALLBACK_ENV = "MACHINES_STORAGE_MODE";
|
|
6265
|
+
var STORAGE_DATABASE_ENV = [MACHINES_STORAGE_ENV, MACHINES_STORAGE_FALLBACK_ENV];
|
|
6266
|
+
var PRIMARY_KEYS = {
|
|
6267
|
+
agent_heartbeats: ["machine_id", "pid"],
|
|
6268
|
+
setup_runs: ["id"],
|
|
6269
|
+
sync_runs: ["id"]
|
|
6270
|
+
};
|
|
6271
|
+
function readEnv(name) {
|
|
6272
|
+
const value = process.env[name]?.trim();
|
|
6273
|
+
return value || undefined;
|
|
6274
|
+
}
|
|
6275
|
+
function normalizeStorageMode(value) {
|
|
6276
|
+
const normalized = value?.trim().toLowerCase();
|
|
6277
|
+
if (normalized === "local" || normalized === "hybrid" || normalized === "remote")
|
|
6278
|
+
return normalized;
|
|
6279
|
+
return;
|
|
6280
|
+
}
|
|
6281
|
+
function getStorageDatabaseEnvName() {
|
|
6282
|
+
for (const name of STORAGE_DATABASE_ENV) {
|
|
6283
|
+
if (readEnv(name))
|
|
6284
|
+
return name;
|
|
6285
|
+
}
|
|
6286
|
+
return null;
|
|
6287
|
+
}
|
|
6288
|
+
function getStorageDatabaseEnv() {
|
|
6289
|
+
const name = getStorageDatabaseEnvName();
|
|
6290
|
+
return name ? { name } : null;
|
|
6291
|
+
}
|
|
6292
|
+
function getStorageDatabaseUrl() {
|
|
6293
|
+
const env = getStorageDatabaseEnv();
|
|
6294
|
+
return env ? readEnv(env.name) ?? null : null;
|
|
6295
|
+
}
|
|
6296
|
+
function getStorageMode() {
|
|
6297
|
+
const mode = normalizeStorageMode(readEnv(MACHINES_STORAGE_MODE_ENV)) ?? normalizeStorageMode(readEnv(MACHINES_STORAGE_MODE_FALLBACK_ENV));
|
|
6298
|
+
if (mode)
|
|
6299
|
+
return mode;
|
|
6300
|
+
return getStorageDatabaseUrl() ? "hybrid" : "local";
|
|
6301
|
+
}
|
|
6302
|
+
async function getStoragePg() {
|
|
6303
|
+
const url = getStorageDatabaseUrl();
|
|
6304
|
+
if (!url) {
|
|
6305
|
+
throw new Error("Missing HASNA_MACHINES_DATABASE_URL or MACHINES_DATABASE_URL");
|
|
6306
|
+
}
|
|
6307
|
+
return new PgAdapterAsync(url);
|
|
6308
|
+
}
|
|
6309
|
+
async function runStorageMigrations(remote) {
|
|
6310
|
+
for (const sql of PG_MIGRATIONS)
|
|
6311
|
+
await remote.run(sql);
|
|
6312
|
+
}
|
|
6313
|
+
async function storagePush(options) {
|
|
6314
|
+
const remote = await getStoragePg();
|
|
6315
|
+
const db = getDb();
|
|
6316
|
+
try {
|
|
6317
|
+
await runStorageMigrations(remote);
|
|
6318
|
+
const results = [];
|
|
6319
|
+
for (const table of resolveTables(options?.tables)) {
|
|
6320
|
+
results.push(await pushTable(db, remote, table));
|
|
6321
|
+
}
|
|
6322
|
+
recordSyncMeta(db, "push", results);
|
|
6323
|
+
return results;
|
|
6324
|
+
} finally {
|
|
6325
|
+
await remote.close();
|
|
6326
|
+
}
|
|
6327
|
+
}
|
|
6328
|
+
async function storagePull(options) {
|
|
6329
|
+
const remote = await getStoragePg();
|
|
6330
|
+
const db = getDb();
|
|
6331
|
+
try {
|
|
6332
|
+
await runStorageMigrations(remote);
|
|
6333
|
+
const results = [];
|
|
6334
|
+
for (const table of resolveTables(options?.tables)) {
|
|
6335
|
+
results.push(await pullTable(remote, db, table));
|
|
6336
|
+
}
|
|
6337
|
+
recordSyncMeta(db, "pull", results);
|
|
6338
|
+
return results;
|
|
6339
|
+
} finally {
|
|
6340
|
+
await remote.close();
|
|
6341
|
+
}
|
|
6342
|
+
}
|
|
6343
|
+
async function storageSync(options) {
|
|
6344
|
+
const pull = await storagePull(options);
|
|
6345
|
+
const push = await storagePush(options);
|
|
6346
|
+
return { pull, push };
|
|
6347
|
+
}
|
|
6348
|
+
function getSyncMetaAll() {
|
|
6349
|
+
const db = getDb();
|
|
6350
|
+
ensureSyncMetaTable(db);
|
|
6351
|
+
return db.query("SELECT table_name, last_synced_at, direction FROM _machines_sync_meta ORDER BY table_name, direction").all();
|
|
6352
|
+
}
|
|
6353
|
+
function getStorageStatus() {
|
|
6354
|
+
const activeEnv = getStorageDatabaseEnv();
|
|
6355
|
+
return {
|
|
6356
|
+
configured: Boolean(activeEnv),
|
|
6357
|
+
mode: getStorageMode(),
|
|
6358
|
+
env: STORAGE_DATABASE_ENV,
|
|
6359
|
+
activeEnv: activeEnv?.name ?? null,
|
|
6360
|
+
service: "machines",
|
|
6361
|
+
tables: STORAGE_TABLES,
|
|
6362
|
+
sync: getSyncMetaAll()
|
|
6363
|
+
};
|
|
6364
|
+
}
|
|
6365
|
+
function resolveTables(tables) {
|
|
6366
|
+
if (!tables || tables.length === 0)
|
|
6367
|
+
return [...STORAGE_TABLES];
|
|
6368
|
+
const allowed = new Set(STORAGE_TABLES);
|
|
6369
|
+
const requested = tables.map((table) => table.trim()).filter(Boolean);
|
|
6370
|
+
const invalid = requested.filter((table) => !allowed.has(table));
|
|
6371
|
+
if (invalid.length > 0)
|
|
6372
|
+
throw new Error(`Unknown machines storage table(s): ${invalid.join(", ")}`);
|
|
6373
|
+
return requested;
|
|
6374
|
+
}
|
|
6375
|
+
async function pushTable(db, remote, table) {
|
|
6376
|
+
const result = { table, rowsRead: 0, rowsWritten: 0, errors: [] };
|
|
6377
|
+
try {
|
|
6378
|
+
if (!tableExists(db, table))
|
|
6379
|
+
return result;
|
|
6380
|
+
const rows = db.query(`SELECT * FROM ${quoteIdent(table)}`).all();
|
|
6381
|
+
result.rowsRead = rows.length;
|
|
6382
|
+
if (rows.length === 0)
|
|
6383
|
+
return result;
|
|
6384
|
+
const remoteColumns = await getRemoteColumns(remote, table);
|
|
6385
|
+
const columns = filterRemoteColumns(remoteColumns, Object.keys(rows[0]));
|
|
6386
|
+
result.rowsWritten = await upsertPg(remote, table, columns, rows);
|
|
6387
|
+
} catch (error) {
|
|
6388
|
+
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
6389
|
+
}
|
|
6390
|
+
return result;
|
|
6391
|
+
}
|
|
6392
|
+
async function pullTable(remote, db, table) {
|
|
6393
|
+
const result = { table, rowsRead: 0, rowsWritten: 0, errors: [] };
|
|
6394
|
+
try {
|
|
6395
|
+
if (!tableExists(db, table))
|
|
6396
|
+
return result;
|
|
6397
|
+
const rows = await remote.all(`SELECT * FROM ${quoteIdent(table)}`);
|
|
6398
|
+
result.rowsRead = rows.length;
|
|
6399
|
+
if (rows.length === 0)
|
|
6400
|
+
return result;
|
|
6401
|
+
const columns = filterLocalColumns(db, table, Object.keys(rows[0]));
|
|
6402
|
+
result.rowsWritten = upsertSqlite(db, table, columns, rows);
|
|
6403
|
+
} catch (error) {
|
|
6404
|
+
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
6405
|
+
}
|
|
6406
|
+
return result;
|
|
6407
|
+
}
|
|
6408
|
+
async function getRemoteColumns(remote, table) {
|
|
6409
|
+
const rows = await remote.all("SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = ?", table);
|
|
6410
|
+
return new Set(rows.map((row) => row.column_name));
|
|
6411
|
+
}
|
|
6412
|
+
function filterRemoteColumns(remoteColumns, columns) {
|
|
6413
|
+
if (remoteColumns.size === 0)
|
|
6414
|
+
return columns;
|
|
6415
|
+
return columns.filter((column) => remoteColumns.has(column));
|
|
6416
|
+
}
|
|
6417
|
+
function filterLocalColumns(db, table, columns) {
|
|
6418
|
+
const rows = db.query(`PRAGMA table_info(${quoteIdent(table)})`).all();
|
|
6419
|
+
const allowed = new Set(rows.map((row) => row.name));
|
|
6420
|
+
return columns.filter((column) => allowed.has(column));
|
|
6421
|
+
}
|
|
6422
|
+
async function upsertPg(remote, table, columns, rows) {
|
|
6423
|
+
if (columns.length === 0)
|
|
6424
|
+
return 0;
|
|
6425
|
+
const primaryKeys = PRIMARY_KEYS[table];
|
|
6426
|
+
const columnList = columns.map(quoteIdent).join(", ");
|
|
6427
|
+
const placeholders = columns.map(() => "?").join(", ");
|
|
6428
|
+
const keyList = primaryKeys.map(quoteIdent).join(", ");
|
|
6429
|
+
const updateColumns = columns.filter((column) => !primaryKeys.includes(column));
|
|
6430
|
+
const fallbackKey = primaryKeys[0];
|
|
6431
|
+
const setClause = updateColumns.length > 0 ? updateColumns.map((column) => `${quoteIdent(column)} = EXCLUDED.${quoteIdent(column)}`).join(", ") : `${quoteIdent(fallbackKey)} = EXCLUDED.${quoteIdent(fallbackKey)}`;
|
|
6432
|
+
for (const row of rows) {
|
|
6433
|
+
await remote.run(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES (${placeholders})
|
|
6434
|
+
ON CONFLICT (${keyList}) DO UPDATE SET ${setClause}`, ...columns.map((column) => coerceForPg(row[column])));
|
|
6435
|
+
}
|
|
6436
|
+
return rows.length;
|
|
6437
|
+
}
|
|
6438
|
+
function upsertSqlite(db, table, columns, rows) {
|
|
6439
|
+
if (columns.length === 0)
|
|
6440
|
+
return 0;
|
|
6441
|
+
const primaryKeys = PRIMARY_KEYS[table];
|
|
6442
|
+
const columnList = columns.map(quoteIdent).join(", ");
|
|
6443
|
+
const placeholders = columns.map(() => "?").join(", ");
|
|
6444
|
+
const keyList = primaryKeys.map(quoteIdent).join(", ");
|
|
6445
|
+
const updateColumns = columns.filter((column) => !primaryKeys.includes(column));
|
|
6446
|
+
const fallbackKey = primaryKeys[0];
|
|
6447
|
+
const setClause = updateColumns.length > 0 ? updateColumns.map((column) => `${quoteIdent(column)} = excluded.${quoteIdent(column)}`).join(", ") : `${quoteIdent(fallbackKey)} = excluded.${quoteIdent(fallbackKey)}`;
|
|
6448
|
+
const statement = db.query(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES (${placeholders})
|
|
6449
|
+
ON CONFLICT (${keyList}) DO UPDATE SET ${setClause}`);
|
|
6450
|
+
const insert = db.transaction((batch) => {
|
|
6451
|
+
for (const row of batch)
|
|
6452
|
+
statement.run(...columns.map((column) => coerceForSqlite(row[column])));
|
|
6453
|
+
});
|
|
6454
|
+
insert(rows);
|
|
6455
|
+
return rows.length;
|
|
6456
|
+
}
|
|
6457
|
+
function recordSyncMeta(db, direction, results) {
|
|
6458
|
+
ensureSyncMetaTable(db);
|
|
6459
|
+
const now = new Date().toISOString();
|
|
6460
|
+
const statement = db.query(`
|
|
6461
|
+
INSERT INTO _machines_sync_meta (table_name, last_synced_at, direction)
|
|
6462
|
+
VALUES (?, ?, ?)
|
|
6463
|
+
ON CONFLICT(table_name, direction) DO UPDATE SET last_synced_at = excluded.last_synced_at
|
|
6464
|
+
`);
|
|
6465
|
+
for (const result of results) {
|
|
6466
|
+
if (result.errors.length > 0)
|
|
6467
|
+
continue;
|
|
6468
|
+
statement.run(result.table, now, direction);
|
|
6469
|
+
}
|
|
6470
|
+
}
|
|
6471
|
+
function ensureSyncMetaTable(db) {
|
|
6472
|
+
db.exec(`
|
|
6473
|
+
CREATE TABLE IF NOT EXISTS _machines_sync_meta (
|
|
6474
|
+
table_name TEXT NOT NULL,
|
|
6475
|
+
last_synced_at TEXT,
|
|
6476
|
+
direction TEXT NOT NULL CHECK(direction IN ('push', 'pull')),
|
|
6477
|
+
PRIMARY KEY (table_name, direction)
|
|
6478
|
+
)
|
|
6479
|
+
`);
|
|
6480
|
+
}
|
|
6481
|
+
function tableExists(db, table) {
|
|
6482
|
+
const row = db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(table);
|
|
6483
|
+
return Boolean(row);
|
|
6484
|
+
}
|
|
6485
|
+
function quoteIdent(identifier) {
|
|
6486
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
6487
|
+
}
|
|
6488
|
+
function coerceForPg(value) {
|
|
6489
|
+
if (value === undefined || value === null)
|
|
6490
|
+
return null;
|
|
6491
|
+
if (value instanceof Date)
|
|
6492
|
+
return value.toISOString();
|
|
6493
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array)
|
|
6494
|
+
return value;
|
|
6495
|
+
if (typeof value === "object")
|
|
6496
|
+
return JSON.stringify(value);
|
|
6497
|
+
return value;
|
|
6498
|
+
}
|
|
6499
|
+
function coerceForSqlite(value) {
|
|
6500
|
+
if (value === undefined || value === null)
|
|
6501
|
+
return null;
|
|
6502
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean")
|
|
6503
|
+
return value;
|
|
6504
|
+
if (value instanceof Date)
|
|
6505
|
+
return value.toISOString();
|
|
6506
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array)
|
|
6507
|
+
return value;
|
|
6508
|
+
if (typeof value === "object")
|
|
6509
|
+
return JSON.stringify(value);
|
|
6510
|
+
return String(value);
|
|
6511
|
+
}
|
|
5759
6512
|
// src/mcp/server.ts
|
|
6513
|
+
function buildServer(version = getPackageVersion()) {
|
|
6514
|
+
return createMcpServer(version);
|
|
6515
|
+
}
|
|
5760
6516
|
function createMcpServer(version) {
|
|
5761
6517
|
const server = new McpServer({ name: "machines", version });
|
|
5762
6518
|
server.tool("machines_status", "Return local machine fleet status paths and machine identity.", {}, async () => ({
|
|
@@ -5789,6 +6545,33 @@ function createMcpServer(version) {
|
|
|
5789
6545
|
server.tool("machines_setup_apply", "Execute setup actions 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(runSetup(machine_id, { apply: true, yes }), null, 2) }] }));
|
|
5790
6546
|
server.tool("machines_sync_preview", "Preview sync actions for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(buildSyncPlan(machine_id), null, 2) }] }));
|
|
5791
6547
|
server.tool("machines_sync_apply", "Execute sync actions 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(runSync(machine_id, { apply: true, yes }), null, 2) }] }));
|
|
6548
|
+
server.tool("machines_topology", "Discover local, manifest, heartbeat, SSH, and Tailscale machine topology.", { include_tailscale: exports_external.boolean().optional().describe("Whether to probe tailscale status --json") }, async ({ include_tailscale }) => ({
|
|
6549
|
+
content: [{ type: "text", text: JSON.stringify(discoverMachineTopology({ includeTailscale: include_tailscale !== false }), null, 2) }]
|
|
6550
|
+
}));
|
|
6551
|
+
server.tool("machines_compatibility", "Check remote package, command, and workspace compatibility for open-* consumers.", {
|
|
6552
|
+
machine_id: exports_external.string().optional().describe("Machine identifier"),
|
|
6553
|
+
commands: exports_external.array(exports_external.object({
|
|
6554
|
+
command: exports_external.string(),
|
|
6555
|
+
expectedVersion: exports_external.string().optional(),
|
|
6556
|
+
versionArgs: exports_external.string().optional(),
|
|
6557
|
+
required: exports_external.boolean().optional()
|
|
6558
|
+
})).optional().describe("Commands to check"),
|
|
6559
|
+
packages: exports_external.array(exports_external.object({
|
|
6560
|
+
name: exports_external.string(),
|
|
6561
|
+
command: exports_external.string().optional(),
|
|
6562
|
+
expectedVersion: exports_external.string().optional(),
|
|
6563
|
+
required: exports_external.boolean().optional()
|
|
6564
|
+
})).optional().describe("Package-backed CLI checks"),
|
|
6565
|
+
workspaces: exports_external.array(exports_external.object({
|
|
6566
|
+
path: exports_external.string(),
|
|
6567
|
+
label: exports_external.string().optional(),
|
|
6568
|
+
expectedPackageName: exports_external.string().optional(),
|
|
6569
|
+
expectedVersion: exports_external.string().optional(),
|
|
6570
|
+
required: exports_external.boolean().optional()
|
|
6571
|
+
})).optional().describe("Workspace paths and package metadata to check")
|
|
6572
|
+
}, async ({ machine_id, commands, packages, workspaces }) => ({
|
|
6573
|
+
content: [{ type: "text", text: JSON.stringify(checkMachineCompatibility({ machineId: machine_id, commands, packages, workspaces }), null, 2) }]
|
|
6574
|
+
}));
|
|
5792
6575
|
server.tool("machines_diff", "Show manifest differences between two machines.", {
|
|
5793
6576
|
left_machine_id: exports_external.string().describe("Left machine identifier"),
|
|
5794
6577
|
right_machine_id: exports_external.string().optional().describe("Right machine identifier")
|
|
@@ -5850,24 +6633,116 @@ function createMcpServer(version) {
|
|
|
5850
6633
|
server.tool("machines_serve_dashboard", "Render the current dashboard HTML.", {}, async () => ({
|
|
5851
6634
|
content: [{ type: "text", text: renderDashboardHtml() }]
|
|
5852
6635
|
}));
|
|
6636
|
+
server.tool("storage_status", "Show machines storage sync configuration and local sync history.", {}, async () => ({
|
|
6637
|
+
content: [{ type: "text", text: JSON.stringify(getStorageStatus(), null, 2) }]
|
|
6638
|
+
}));
|
|
6639
|
+
server.tool("storage_push", "Push local machine runtime data to storage PostgreSQL.", { tables: exports_external.array(exports_external.string()).optional().describe("Optional table list to push") }, async ({ tables }) => ({ content: [{ type: "text", text: JSON.stringify(await storagePush(tables ? { tables } : undefined), null, 2) }] }));
|
|
6640
|
+
server.tool("storage_pull", "Pull machine runtime data from storage PostgreSQL to local SQLite.", { tables: exports_external.array(exports_external.string()).optional().describe("Optional table list to pull") }, async ({ tables }) => ({ content: [{ type: "text", text: JSON.stringify(await storagePull(tables ? { tables } : undefined), null, 2) }] }));
|
|
6641
|
+
server.tool("storage_sync", "Bidirectional machines storage sync: pull then push.", { tables: exports_external.array(exports_external.string()).optional().describe("Optional table list to sync") }, async ({ tables }) => ({ content: [{ type: "text", text: JSON.stringify(await storageSync(tables ? { tables } : undefined), null, 2) }] }));
|
|
5853
6642
|
return server;
|
|
5854
6643
|
}
|
|
5855
6644
|
|
|
5856
|
-
// src/mcp/
|
|
5857
|
-
|
|
6645
|
+
// src/mcp/http.ts
|
|
6646
|
+
var DEFAULT_HTTP_PORT = 8821;
|
|
6647
|
+
var HTTP_NAME = "machines";
|
|
6648
|
+
function isHttpMode(args = process.argv.slice(2)) {
|
|
6649
|
+
return args.includes("--http") || process.env.MCP_HTTP === "1";
|
|
6650
|
+
}
|
|
6651
|
+
function resolveHttpPort(args = process.argv.slice(2)) {
|
|
6652
|
+
for (let i = 0;i < args.length; i++) {
|
|
6653
|
+
const arg = args[i];
|
|
6654
|
+
if (arg === "--port" && args[i + 1]) {
|
|
6655
|
+
return parsePort(args[i + 1]);
|
|
6656
|
+
}
|
|
6657
|
+
if (arg.startsWith("--port=")) {
|
|
6658
|
+
return parsePort(arg.slice("--port=".length));
|
|
6659
|
+
}
|
|
6660
|
+
}
|
|
6661
|
+
const envPort = process.env.MCP_HTTP_PORT;
|
|
6662
|
+
if (envPort) {
|
|
6663
|
+
return parsePort(envPort);
|
|
6664
|
+
}
|
|
6665
|
+
return DEFAULT_HTTP_PORT;
|
|
6666
|
+
}
|
|
6667
|
+
function parsePort(raw) {
|
|
6668
|
+
const port = Number.parseInt(raw, 10);
|
|
6669
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
6670
|
+
throw new Error(`Invalid port: ${raw}`);
|
|
6671
|
+
}
|
|
6672
|
+
return port;
|
|
6673
|
+
}
|
|
6674
|
+
function pathnameFromRequest(req) {
|
|
6675
|
+
return new URL(req.url ?? "/", "http://127.0.0.1").pathname;
|
|
6676
|
+
}
|
|
6677
|
+
async function readRequestBody(req) {
|
|
6678
|
+
if (req.method !== "POST" && req.method !== "DELETE") {
|
|
6679
|
+
return;
|
|
6680
|
+
}
|
|
6681
|
+
const chunks = [];
|
|
6682
|
+
for await (const chunk of req) {
|
|
6683
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
6684
|
+
}
|
|
6685
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
6686
|
+
if (!text) {
|
|
6687
|
+
return;
|
|
6688
|
+
}
|
|
6689
|
+
return JSON.parse(text);
|
|
6690
|
+
}
|
|
6691
|
+
async function handleMcpRequest(req, res) {
|
|
6692
|
+
const server = buildServer();
|
|
6693
|
+
const transport = new StreamableHTTPServerTransport({
|
|
6694
|
+
sessionIdGenerator: undefined
|
|
6695
|
+
});
|
|
6696
|
+
await server.connect(transport);
|
|
5858
6697
|
try {
|
|
5859
|
-
const
|
|
5860
|
-
|
|
5861
|
-
}
|
|
5862
|
-
|
|
6698
|
+
const body = await readRequestBody(req);
|
|
6699
|
+
await transport.handleRequest(req, res, body);
|
|
6700
|
+
} finally {
|
|
6701
|
+
res.on("close", () => {
|
|
6702
|
+
transport.close().catch(() => {
|
|
6703
|
+
return;
|
|
6704
|
+
});
|
|
6705
|
+
server.close().catch(() => {
|
|
6706
|
+
return;
|
|
6707
|
+
});
|
|
6708
|
+
});
|
|
5863
6709
|
}
|
|
5864
6710
|
}
|
|
6711
|
+
function startHttpServer(options = {}) {
|
|
6712
|
+
const host = options.host ?? "127.0.0.1";
|
|
6713
|
+
const port = options.port ?? resolveHttpPort();
|
|
6714
|
+
const name = options.name ?? HTTP_NAME;
|
|
6715
|
+
const httpServer = createServer(async (req, res) => {
|
|
6716
|
+
const path = pathnameFromRequest(req);
|
|
6717
|
+
if (req.method === "GET" && path === "/health") {
|
|
6718
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
6719
|
+
res.end(JSON.stringify({ status: "ok", name }));
|
|
6720
|
+
return;
|
|
6721
|
+
}
|
|
6722
|
+
if (path === "/mcp") {
|
|
6723
|
+
await handleMcpRequest(req, res);
|
|
6724
|
+
return;
|
|
6725
|
+
}
|
|
6726
|
+
res.writeHead(404, { "content-type": "application/json" });
|
|
6727
|
+
res.end(JSON.stringify({ error: "Not found" }));
|
|
6728
|
+
});
|
|
6729
|
+
httpServer.listen(port, host, () => {
|
|
6730
|
+
const address = httpServer.address();
|
|
6731
|
+
const boundPort = typeof address === "object" && address ? address.port : port;
|
|
6732
|
+
console.error(`machines-mcp HTTP listening on http://${host}:${boundPort}`);
|
|
6733
|
+
});
|
|
6734
|
+
return httpServer;
|
|
6735
|
+
}
|
|
6736
|
+
|
|
6737
|
+
// src/mcp/index.ts
|
|
5865
6738
|
function printHelp() {
|
|
5866
6739
|
console.log(`Usage: machines-mcp [options]
|
|
5867
6740
|
|
|
5868
|
-
MCP server for machine fleet management tools (stdio transport)
|
|
6741
|
+
MCP server for machine fleet management tools (stdio transport by default)
|
|
5869
6742
|
|
|
5870
6743
|
Options:
|
|
6744
|
+
--http Start Streamable HTTP transport on 127.0.0.1 (or MCP_HTTP=1)
|
|
6745
|
+
--port <n> HTTP port (default: 8821, or MCP_HTTP_PORT env)
|
|
5871
6746
|
-V, --version output the version number
|
|
5872
6747
|
-h, --help display help for command`);
|
|
5873
6748
|
}
|
|
@@ -5877,9 +6752,13 @@ if (args.includes("--help") || args.includes("-h")) {
|
|
|
5877
6752
|
process.exit(0);
|
|
5878
6753
|
}
|
|
5879
6754
|
if (args.includes("--version") || args.includes("-V")) {
|
|
5880
|
-
console.log(
|
|
6755
|
+
console.log(getPackageVersion());
|
|
5881
6756
|
process.exit(0);
|
|
5882
6757
|
}
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
6758
|
+
if (isHttpMode(args)) {
|
|
6759
|
+
startHttpServer({ port: resolveHttpPort(args) });
|
|
6760
|
+
} else {
|
|
6761
|
+
const server = buildServer();
|
|
6762
|
+
const transport = new StdioServerTransport;
|
|
6763
|
+
await server.connect(transport);
|
|
6764
|
+
}
|