@hasna/machines 0.0.14 → 0.0.15
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 +1697 -170
- 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 +1020 -185
- package/dist/mcp/http.d.ts +12 -0
- package/dist/mcp/http.d.ts.map +1 -0
- package/dist/mcp/index.js +925 -66
- 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/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/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(),
|
|
@@ -4348,7 +4367,7 @@ function runMachineCommand(machineId, command) {
|
|
|
4348
4367
|
const isLocal = machineId === localMachineId;
|
|
4349
4368
|
const route = isLocal ? "local" : resolveSshTarget(machineId).route;
|
|
4350
4369
|
const shellCommand = isLocal ? command : buildSshCommand(machineId, command);
|
|
4351
|
-
const result = spawnSync2("bash", ["-
|
|
4370
|
+
const result = spawnSync2("bash", ["-c", shellCommand], {
|
|
4352
4371
|
encoding: "utf8",
|
|
4353
4372
|
env: process.env
|
|
4354
4373
|
});
|
|
@@ -4501,20 +4520,20 @@ function runAppsInstall(machineId, options = {}) {
|
|
|
4501
4520
|
|
|
4502
4521
|
// src/commands/cert.ts
|
|
4503
4522
|
import { homedir as homedir3, platform as platform2 } from "os";
|
|
4504
|
-
import { join as
|
|
4523
|
+
import { join as join4 } from "path";
|
|
4505
4524
|
function quote2(value) {
|
|
4506
4525
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
4507
4526
|
}
|
|
4508
4527
|
function certDir() {
|
|
4509
|
-
return
|
|
4528
|
+
return join4(homedir3(), ".hasna", "machines", "certs");
|
|
4510
4529
|
}
|
|
4511
4530
|
function buildCertPlan(domains) {
|
|
4512
4531
|
if (domains.length === 0) {
|
|
4513
4532
|
throw new Error("At least one domain is required.");
|
|
4514
4533
|
}
|
|
4515
4534
|
const primary = domains[0];
|
|
4516
|
-
const certPath =
|
|
4517
|
-
const keyPath =
|
|
4535
|
+
const certPath = join4(certDir(), `${primary}.pem`);
|
|
4536
|
+
const keyPath = join4(certDir(), `${primary}-key.pem`);
|
|
4518
4537
|
const steps = [];
|
|
4519
4538
|
if (platform2() === "darwin") {
|
|
4520
4539
|
steps.push({
|
|
@@ -4578,16 +4597,16 @@ function runCertPlan(domains, options = {}) {
|
|
|
4578
4597
|
}
|
|
4579
4598
|
|
|
4580
4599
|
// src/commands/dns.ts
|
|
4581
|
-
import { existsSync as
|
|
4582
|
-
import { join as
|
|
4600
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
4601
|
+
import { join as join5 } from "path";
|
|
4583
4602
|
function getDnsPath() {
|
|
4584
|
-
return
|
|
4603
|
+
return join5(getDataDir(), "dns.json");
|
|
4585
4604
|
}
|
|
4586
4605
|
function readMappings() {
|
|
4587
4606
|
const path = getDnsPath();
|
|
4588
|
-
if (!
|
|
4607
|
+
if (!existsSync4(path))
|
|
4589
4608
|
return [];
|
|
4590
|
-
return JSON.parse(
|
|
4609
|
+
return JSON.parse(readFileSync3(path, "utf8"));
|
|
4591
4610
|
}
|
|
4592
4611
|
function writeMappings(mappings) {
|
|
4593
4612
|
const path = getDnsPath();
|
|
@@ -4614,10 +4633,10 @@ function renderDomainMapping(domain) {
|
|
|
4614
4633
|
hostsEntry: `${entry.targetHost} ${entry.domain}`,
|
|
4615
4634
|
caddySnippet: `${entry.domain} {
|
|
4616
4635
|
reverse_proxy 127.0.0.1:${entry.port}
|
|
4617
|
-
tls ${
|
|
4636
|
+
tls ${join5(getDataDir(), "certs", `${entry.domain}.pem`)} ${join5(getDataDir(), "certs", `${entry.domain}-key.pem`)}
|
|
4618
4637
|
}`,
|
|
4619
|
-
certPath:
|
|
4620
|
-
keyPath:
|
|
4638
|
+
certPath: join5(getDataDir(), "certs", `${entry.domain}.pem`),
|
|
4639
|
+
keyPath: join5(getDataDir(), "certs", `${entry.domain}-key.pem`)
|
|
4621
4640
|
};
|
|
4622
4641
|
}
|
|
4623
4642
|
|
|
@@ -4889,7 +4908,7 @@ function runTailscaleInstall(machineId, options = {}) {
|
|
|
4889
4908
|
}
|
|
4890
4909
|
|
|
4891
4910
|
// src/commands/notifications.ts
|
|
4892
|
-
import { existsSync as
|
|
4911
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
4893
4912
|
var notificationChannelSchema = exports_external.object({
|
|
4894
4913
|
id: exports_external.string(),
|
|
4895
4914
|
type: exports_external.enum(["email", "webhook", "command"]),
|
|
@@ -5045,10 +5064,10 @@ function getDefaultNotificationConfig() {
|
|
|
5045
5064
|
};
|
|
5046
5065
|
}
|
|
5047
5066
|
function readNotificationConfig(path = getNotificationsPath()) {
|
|
5048
|
-
if (!
|
|
5067
|
+
if (!existsSync5(path)) {
|
|
5049
5068
|
return getDefaultNotificationConfig();
|
|
5050
5069
|
}
|
|
5051
|
-
return notificationConfigSchema.parse(JSON.parse(
|
|
5070
|
+
return notificationConfigSchema.parse(JSON.parse(readFileSync4(path, "utf8")));
|
|
5052
5071
|
}
|
|
5053
5072
|
function writeNotificationConfig(config, path = getNotificationsPath()) {
|
|
5054
5073
|
ensureParentDir(path);
|
|
@@ -5220,24 +5239,6 @@ function manifestValidate() {
|
|
|
5220
5239
|
return validateManifest(getManifestPath());
|
|
5221
5240
|
}
|
|
5222
5241
|
|
|
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
5242
|
// src/commands/status.ts
|
|
5242
5243
|
function getStatus() {
|
|
5243
5244
|
const manifest = readManifest();
|
|
@@ -5756,7 +5757,742 @@ function getAgentStatus(machineId = getLocalMachineId()) {
|
|
|
5756
5757
|
}));
|
|
5757
5758
|
}
|
|
5758
5759
|
|
|
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
|
+
// src/compatibility.ts
|
|
5938
|
+
var DEFAULT_COMMANDS = [
|
|
5939
|
+
{ command: "bun", required: true },
|
|
5940
|
+
{ command: "machines", required: true }
|
|
5941
|
+
];
|
|
5942
|
+
function defaultPackages() {
|
|
5943
|
+
return [{ name: "@hasna/machines", command: "machines", expectedVersion: getPackageVersion(), required: true }];
|
|
5944
|
+
}
|
|
5945
|
+
function shellQuote3(value) {
|
|
5946
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
5947
|
+
}
|
|
5948
|
+
function commandId(value) {
|
|
5949
|
+
return value.replace(/[^a-zA-Z0-9_.@/-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
5950
|
+
}
|
|
5951
|
+
function packageCommand(name) {
|
|
5952
|
+
if (name === "@hasna/knowledge")
|
|
5953
|
+
return "knowledge";
|
|
5954
|
+
if (name === "@hasna/machines")
|
|
5955
|
+
return "machines";
|
|
5956
|
+
return name.split("/").pop() ?? name;
|
|
5957
|
+
}
|
|
5958
|
+
function firstLine(value) {
|
|
5959
|
+
return value.trim().split(/\r?\n/).find(Boolean) ?? "";
|
|
5960
|
+
}
|
|
5961
|
+
function extractVersion(value) {
|
|
5962
|
+
const match = value.match(/\b\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?\b/);
|
|
5963
|
+
return match?.[0] ?? null;
|
|
5964
|
+
}
|
|
5965
|
+
function statusFor(required, ok) {
|
|
5966
|
+
if (ok)
|
|
5967
|
+
return "ok";
|
|
5968
|
+
return required === false ? "warn" : "fail";
|
|
5969
|
+
}
|
|
5970
|
+
function makeCheck2(input) {
|
|
5971
|
+
return {
|
|
5972
|
+
id: input.id,
|
|
5973
|
+
kind: input.kind,
|
|
5974
|
+
status: input.status,
|
|
5975
|
+
target: input.target,
|
|
5976
|
+
expected: input.expected ?? null,
|
|
5977
|
+
actual: input.actual ?? null,
|
|
5978
|
+
detail: input.detail,
|
|
5979
|
+
source: input.source
|
|
5980
|
+
};
|
|
5981
|
+
}
|
|
5982
|
+
function parseKeyValue(stdout) {
|
|
5983
|
+
const result = {};
|
|
5984
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
5985
|
+
const idx = line.indexOf("=");
|
|
5986
|
+
if (idx <= 0)
|
|
5987
|
+
continue;
|
|
5988
|
+
result[line.slice(0, idx)] = line.slice(idx + 1);
|
|
5989
|
+
}
|
|
5990
|
+
return result;
|
|
5991
|
+
}
|
|
5992
|
+
function defaultRunner2(machineId, command) {
|
|
5993
|
+
return runMachineCommand(machineId, command);
|
|
5994
|
+
}
|
|
5995
|
+
function inspectCommand(machineId, spec, runner) {
|
|
5996
|
+
const command = shellQuote3(spec.command);
|
|
5997
|
+
const versionArgs = spec.versionArgs ?? "--version";
|
|
5998
|
+
const script = [
|
|
5999
|
+
`cmd=${command}`,
|
|
6000
|
+
'path="$(command -v "$cmd" 2>/dev/null || true)"',
|
|
6001
|
+
'printf "path=%s\\n" "$path"',
|
|
6002
|
+
'if [ -n "$path" ]; then version="$("$cmd" ' + versionArgs + ' 2>/dev/null || true)"; printf "version=%s\\n" "$version"; fi'
|
|
6003
|
+
].join("; ");
|
|
6004
|
+
const result = runner(machineId, script);
|
|
6005
|
+
const parsed = parseKeyValue(result.stdout);
|
|
6006
|
+
return {
|
|
6007
|
+
path: parsed.path || null,
|
|
6008
|
+
version: parsed.version ? firstLine(parsed.version) : null,
|
|
6009
|
+
exitCode: result.exitCode,
|
|
6010
|
+
source: result.source,
|
|
6011
|
+
stderr: result.stderr
|
|
6012
|
+
};
|
|
6013
|
+
}
|
|
6014
|
+
function fieldCommand(field) {
|
|
6015
|
+
const regex = field === "name" ? String.raw`s/.*"name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p` : String.raw`s/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p`;
|
|
6016
|
+
return [
|
|
6017
|
+
`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`,
|
|
6018
|
+
`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`,
|
|
6019
|
+
`else sed -n '${regex}' "$pkg" | head -n 1`,
|
|
6020
|
+
"fi"
|
|
6021
|
+
].join("; ");
|
|
6022
|
+
}
|
|
6023
|
+
function inspectWorkspace(machineId, spec, runner) {
|
|
6024
|
+
const script = [
|
|
6025
|
+
`path=${shellQuote3(spec.path)}`,
|
|
6026
|
+
'printf "exists=%s\\n" "$(test -d "$path" && printf yes || printf no)"',
|
|
6027
|
+
'pkg="$path/package.json"',
|
|
6028
|
+
'printf "package_json=%s\\n" "$(test -f "$pkg" && printf yes || printf no)"',
|
|
6029
|
+
`if [ -f "$pkg" ]; then printf "package_name=%s\\n" "$(${fieldCommand("name")})"; printf "version=%s\\n" "$(${fieldCommand("version")})"; fi`
|
|
6030
|
+
].join("; ");
|
|
6031
|
+
const result = runner(machineId, script);
|
|
6032
|
+
const parsed = parseKeyValue(result.stdout);
|
|
6033
|
+
return {
|
|
6034
|
+
exists: parsed.exists === "yes",
|
|
6035
|
+
packageJson: parsed.package_json === "yes",
|
|
6036
|
+
packageName: parsed.package_name || null,
|
|
6037
|
+
version: parsed.version || null,
|
|
6038
|
+
source: result.source,
|
|
6039
|
+
stderr: result.stderr
|
|
6040
|
+
};
|
|
6041
|
+
}
|
|
6042
|
+
function commandCheck(machineId, spec, runner) {
|
|
6043
|
+
const inspection = inspectCommand(machineId, spec, runner);
|
|
6044
|
+
const found = Boolean(inspection.path);
|
|
6045
|
+
const checks = [
|
|
6046
|
+
makeCheck2({
|
|
6047
|
+
id: `command:${commandId(spec.command)}:path`,
|
|
6048
|
+
kind: "command",
|
|
6049
|
+
status: statusFor(spec.required, found),
|
|
6050
|
+
target: spec.command,
|
|
6051
|
+
expected: "available",
|
|
6052
|
+
actual: inspection.path ?? "missing",
|
|
6053
|
+
detail: found ? `found at ${inspection.path}` : inspection.stderr || "command missing",
|
|
6054
|
+
source: inspection.source
|
|
6055
|
+
})
|
|
6056
|
+
];
|
|
6057
|
+
if (spec.expectedVersion) {
|
|
6058
|
+
const actualVersion = extractVersion(inspection.version ?? "");
|
|
6059
|
+
checks.push(makeCheck2({
|
|
6060
|
+
id: `command:${commandId(spec.command)}:version`,
|
|
6061
|
+
kind: "command",
|
|
6062
|
+
status: actualVersion === spec.expectedVersion ? "ok" : statusFor(spec.required, false),
|
|
6063
|
+
target: spec.command,
|
|
6064
|
+
expected: spec.expectedVersion,
|
|
6065
|
+
actual: actualVersion ?? inspection.version ?? "missing",
|
|
6066
|
+
detail: actualVersion ? `version output: ${inspection.version}` : "version unavailable",
|
|
6067
|
+
source: inspection.source
|
|
6068
|
+
}));
|
|
6069
|
+
}
|
|
6070
|
+
return checks;
|
|
6071
|
+
}
|
|
6072
|
+
function packageCheck(machineId, spec, runner) {
|
|
6073
|
+
const command = spec.command ?? packageCommand(spec.name);
|
|
6074
|
+
const inspection = inspectCommand(machineId, { command, expectedVersion: spec.expectedVersion, required: spec.required }, runner);
|
|
6075
|
+
const found = Boolean(inspection.path);
|
|
6076
|
+
const checks = [
|
|
6077
|
+
makeCheck2({
|
|
6078
|
+
id: `package:${commandId(spec.name)}:command`,
|
|
6079
|
+
kind: "package",
|
|
6080
|
+
status: statusFor(spec.required, found),
|
|
6081
|
+
target: spec.name,
|
|
6082
|
+
expected: command,
|
|
6083
|
+
actual: inspection.path ?? "missing",
|
|
6084
|
+
detail: found ? `${command} found at ${inspection.path}` : `${command} command missing`,
|
|
6085
|
+
source: inspection.source
|
|
6086
|
+
})
|
|
6087
|
+
];
|
|
6088
|
+
if (spec.expectedVersion) {
|
|
6089
|
+
const actualVersion = extractVersion(inspection.version ?? "");
|
|
6090
|
+
checks.push(makeCheck2({
|
|
6091
|
+
id: `package:${commandId(spec.name)}:version`,
|
|
6092
|
+
kind: "package",
|
|
6093
|
+
status: actualVersion === spec.expectedVersion ? "ok" : statusFor(spec.required, false),
|
|
6094
|
+
target: spec.name,
|
|
6095
|
+
expected: spec.expectedVersion,
|
|
6096
|
+
actual: actualVersion ?? inspection.version ?? "missing",
|
|
6097
|
+
detail: actualVersion ? `version output: ${inspection.version}` : "version unavailable",
|
|
6098
|
+
source: inspection.source
|
|
6099
|
+
}));
|
|
6100
|
+
}
|
|
6101
|
+
return checks;
|
|
6102
|
+
}
|
|
6103
|
+
function workspaceCheck(machineId, spec, runner) {
|
|
6104
|
+
const inspection = inspectWorkspace(machineId, spec, runner);
|
|
6105
|
+
const target = spec.label ?? spec.path;
|
|
6106
|
+
const checks = [
|
|
6107
|
+
makeCheck2({
|
|
6108
|
+
id: `workspace:${commandId(target)}:path`,
|
|
6109
|
+
kind: "workspace",
|
|
6110
|
+
status: statusFor(spec.required, inspection.exists),
|
|
6111
|
+
target,
|
|
6112
|
+
expected: spec.path,
|
|
6113
|
+
actual: inspection.exists ? "exists" : "missing",
|
|
6114
|
+
detail: inspection.exists ? `workspace exists at ${spec.path}` : inspection.stderr || `workspace missing at ${spec.path}`,
|
|
6115
|
+
source: inspection.source
|
|
6116
|
+
})
|
|
6117
|
+
];
|
|
6118
|
+
if (spec.expectedPackageName) {
|
|
6119
|
+
checks.push(makeCheck2({
|
|
6120
|
+
id: `workspace:${commandId(target)}:package-name`,
|
|
6121
|
+
kind: "workspace",
|
|
6122
|
+
status: inspection.packageName === spec.expectedPackageName ? "ok" : statusFor(spec.required, false),
|
|
6123
|
+
target,
|
|
6124
|
+
expected: spec.expectedPackageName,
|
|
6125
|
+
actual: inspection.packageName ?? (inspection.packageJson ? "missing-name" : "missing-package-json"),
|
|
6126
|
+
detail: inspection.packageJson ? "package.json inspected" : "package.json missing",
|
|
6127
|
+
source: inspection.source
|
|
6128
|
+
}));
|
|
6129
|
+
}
|
|
6130
|
+
if (spec.expectedVersion) {
|
|
6131
|
+
checks.push(makeCheck2({
|
|
6132
|
+
id: `workspace:${commandId(target)}:version`,
|
|
6133
|
+
kind: "workspace",
|
|
6134
|
+
status: inspection.version === spec.expectedVersion ? "ok" : statusFor(spec.required, false),
|
|
6135
|
+
target,
|
|
6136
|
+
expected: spec.expectedVersion,
|
|
6137
|
+
actual: inspection.version ?? (inspection.packageJson ? "missing-version" : "missing-package-json"),
|
|
6138
|
+
detail: inspection.packageJson ? "package.json inspected" : "package.json missing",
|
|
6139
|
+
source: inspection.source
|
|
6140
|
+
}));
|
|
6141
|
+
}
|
|
6142
|
+
return checks;
|
|
6143
|
+
}
|
|
6144
|
+
function checkMachineCompatibility(options = {}) {
|
|
6145
|
+
const machineId = options.machineId ?? getLocalMachineId();
|
|
6146
|
+
const runner = options.runner ?? defaultRunner2;
|
|
6147
|
+
const commands = options.commands ?? DEFAULT_COMMANDS;
|
|
6148
|
+
const packages = options.packages ?? defaultPackages();
|
|
6149
|
+
const workspaces = options.workspaces ?? [];
|
|
6150
|
+
const checks = [];
|
|
6151
|
+
for (const spec of commands)
|
|
6152
|
+
checks.push(...commandCheck(machineId, spec, runner));
|
|
6153
|
+
for (const spec of packages)
|
|
6154
|
+
checks.push(...packageCheck(machineId, spec, runner));
|
|
6155
|
+
for (const spec of workspaces)
|
|
6156
|
+
checks.push(...workspaceCheck(machineId, spec, runner));
|
|
6157
|
+
const summary = {
|
|
6158
|
+
ok: checks.filter((check2) => check2.status === "ok").length,
|
|
6159
|
+
warn: checks.filter((check2) => check2.status === "warn").length,
|
|
6160
|
+
fail: checks.filter((check2) => check2.status === "fail").length
|
|
6161
|
+
};
|
|
6162
|
+
return {
|
|
6163
|
+
ok: summary.fail === 0,
|
|
6164
|
+
machine_id: machineId,
|
|
6165
|
+
source: checks[0]?.source ?? "local",
|
|
6166
|
+
generated_at: (options.now ?? new Date).toISOString(),
|
|
6167
|
+
checks,
|
|
6168
|
+
summary
|
|
6169
|
+
};
|
|
6170
|
+
}
|
|
6171
|
+
|
|
6172
|
+
// src/pg-migrations.ts
|
|
6173
|
+
var PG_MIGRATIONS = [
|
|
6174
|
+
`
|
|
6175
|
+
CREATE TABLE IF NOT EXISTS agent_heartbeats (
|
|
6176
|
+
machine_id TEXT NOT NULL,
|
|
6177
|
+
pid INTEGER NOT NULL,
|
|
6178
|
+
status TEXT NOT NULL,
|
|
6179
|
+
updated_at TIMESTAMPTZ NOT NULL,
|
|
6180
|
+
PRIMARY KEY (machine_id, pid)
|
|
6181
|
+
);
|
|
6182
|
+
|
|
6183
|
+
CREATE TABLE IF NOT EXISTS setup_runs (
|
|
6184
|
+
id TEXT PRIMARY KEY,
|
|
6185
|
+
machine_id TEXT NOT NULL,
|
|
6186
|
+
status TEXT NOT NULL,
|
|
6187
|
+
details_json TEXT NOT NULL DEFAULT '[]',
|
|
6188
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
6189
|
+
updated_at TIMESTAMPTZ NOT NULL
|
|
6190
|
+
);
|
|
6191
|
+
|
|
6192
|
+
CREATE TABLE IF NOT EXISTS sync_runs (
|
|
6193
|
+
id TEXT PRIMARY KEY,
|
|
6194
|
+
machine_id TEXT NOT NULL,
|
|
6195
|
+
status TEXT NOT NULL,
|
|
6196
|
+
actions_json TEXT NOT NULL DEFAULT '[]',
|
|
6197
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
6198
|
+
updated_at TIMESTAMPTZ NOT NULL
|
|
6199
|
+
);
|
|
6200
|
+
`
|
|
6201
|
+
];
|
|
6202
|
+
|
|
6203
|
+
// src/remote-storage.ts
|
|
6204
|
+
import pg from "pg";
|
|
6205
|
+
function translatePlaceholders(sql) {
|
|
6206
|
+
let index = 0;
|
|
6207
|
+
return sql.replace(/\?/g, () => `$${++index}`);
|
|
6208
|
+
}
|
|
6209
|
+
function normalizeParams(params) {
|
|
6210
|
+
const flat = params.length === 1 && Array.isArray(params[0]) ? params[0] : params;
|
|
6211
|
+
return flat.map((value) => value === undefined ? null : value);
|
|
6212
|
+
}
|
|
6213
|
+
function sslConfigFor(connectionString) {
|
|
6214
|
+
return connectionString.includes("sslmode=require") || connectionString.includes("ssl=true") ? { rejectUnauthorized: false } : undefined;
|
|
6215
|
+
}
|
|
6216
|
+
|
|
6217
|
+
class PgAdapterAsync {
|
|
6218
|
+
pool;
|
|
6219
|
+
constructor(connectionString) {
|
|
6220
|
+
this.pool = new pg.Pool({ connectionString, ssl: sslConfigFor(connectionString) });
|
|
6221
|
+
}
|
|
6222
|
+
async run(sql, ...params) {
|
|
6223
|
+
const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
|
|
6224
|
+
return { changes: result.rowCount ?? 0 };
|
|
6225
|
+
}
|
|
6226
|
+
async all(sql, ...params) {
|
|
6227
|
+
const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
|
|
6228
|
+
return result.rows;
|
|
6229
|
+
}
|
|
6230
|
+
async close() {
|
|
6231
|
+
await this.pool.end();
|
|
6232
|
+
}
|
|
6233
|
+
}
|
|
6234
|
+
|
|
6235
|
+
// src/storage-sync.ts
|
|
6236
|
+
var STORAGE_TABLES = [
|
|
6237
|
+
"agent_heartbeats",
|
|
6238
|
+
"setup_runs",
|
|
6239
|
+
"sync_runs"
|
|
6240
|
+
];
|
|
6241
|
+
var MACHINES_STORAGE_ENV = "HASNA_MACHINES_DATABASE_URL";
|
|
6242
|
+
var MACHINES_STORAGE_FALLBACK_ENV = "MACHINES_DATABASE_URL";
|
|
6243
|
+
var MACHINES_STORAGE_MODE_ENV = "HASNA_MACHINES_STORAGE_MODE";
|
|
6244
|
+
var MACHINES_STORAGE_MODE_FALLBACK_ENV = "MACHINES_STORAGE_MODE";
|
|
6245
|
+
var STORAGE_DATABASE_ENV = [MACHINES_STORAGE_ENV, MACHINES_STORAGE_FALLBACK_ENV];
|
|
6246
|
+
var PRIMARY_KEYS = {
|
|
6247
|
+
agent_heartbeats: ["machine_id", "pid"],
|
|
6248
|
+
setup_runs: ["id"],
|
|
6249
|
+
sync_runs: ["id"]
|
|
6250
|
+
};
|
|
6251
|
+
function readEnv(name) {
|
|
6252
|
+
const value = process.env[name]?.trim();
|
|
6253
|
+
return value || undefined;
|
|
6254
|
+
}
|
|
6255
|
+
function normalizeStorageMode(value) {
|
|
6256
|
+
const normalized = value?.trim().toLowerCase();
|
|
6257
|
+
if (normalized === "local" || normalized === "hybrid" || normalized === "remote")
|
|
6258
|
+
return normalized;
|
|
6259
|
+
return;
|
|
6260
|
+
}
|
|
6261
|
+
function getStorageDatabaseEnvName() {
|
|
6262
|
+
for (const name of STORAGE_DATABASE_ENV) {
|
|
6263
|
+
if (readEnv(name))
|
|
6264
|
+
return name;
|
|
6265
|
+
}
|
|
6266
|
+
return null;
|
|
6267
|
+
}
|
|
6268
|
+
function getStorageDatabaseEnv() {
|
|
6269
|
+
const name = getStorageDatabaseEnvName();
|
|
6270
|
+
return name ? { name } : null;
|
|
6271
|
+
}
|
|
6272
|
+
function getStorageDatabaseUrl() {
|
|
6273
|
+
const env = getStorageDatabaseEnv();
|
|
6274
|
+
return env ? readEnv(env.name) ?? null : null;
|
|
6275
|
+
}
|
|
6276
|
+
function getStorageMode() {
|
|
6277
|
+
const mode = normalizeStorageMode(readEnv(MACHINES_STORAGE_MODE_ENV)) ?? normalizeStorageMode(readEnv(MACHINES_STORAGE_MODE_FALLBACK_ENV));
|
|
6278
|
+
if (mode)
|
|
6279
|
+
return mode;
|
|
6280
|
+
return getStorageDatabaseUrl() ? "hybrid" : "local";
|
|
6281
|
+
}
|
|
6282
|
+
async function getStoragePg() {
|
|
6283
|
+
const url = getStorageDatabaseUrl();
|
|
6284
|
+
if (!url) {
|
|
6285
|
+
throw new Error("Missing HASNA_MACHINES_DATABASE_URL or MACHINES_DATABASE_URL");
|
|
6286
|
+
}
|
|
6287
|
+
return new PgAdapterAsync(url);
|
|
6288
|
+
}
|
|
6289
|
+
async function runStorageMigrations(remote) {
|
|
6290
|
+
for (const sql of PG_MIGRATIONS)
|
|
6291
|
+
await remote.run(sql);
|
|
6292
|
+
}
|
|
6293
|
+
async function storagePush(options) {
|
|
6294
|
+
const remote = await getStoragePg();
|
|
6295
|
+
const db = getDb();
|
|
6296
|
+
try {
|
|
6297
|
+
await runStorageMigrations(remote);
|
|
6298
|
+
const results = [];
|
|
6299
|
+
for (const table of resolveTables(options?.tables)) {
|
|
6300
|
+
results.push(await pushTable(db, remote, table));
|
|
6301
|
+
}
|
|
6302
|
+
recordSyncMeta(db, "push", results);
|
|
6303
|
+
return results;
|
|
6304
|
+
} finally {
|
|
6305
|
+
await remote.close();
|
|
6306
|
+
}
|
|
6307
|
+
}
|
|
6308
|
+
async function storagePull(options) {
|
|
6309
|
+
const remote = await getStoragePg();
|
|
6310
|
+
const db = getDb();
|
|
6311
|
+
try {
|
|
6312
|
+
await runStorageMigrations(remote);
|
|
6313
|
+
const results = [];
|
|
6314
|
+
for (const table of resolveTables(options?.tables)) {
|
|
6315
|
+
results.push(await pullTable(remote, db, table));
|
|
6316
|
+
}
|
|
6317
|
+
recordSyncMeta(db, "pull", results);
|
|
6318
|
+
return results;
|
|
6319
|
+
} finally {
|
|
6320
|
+
await remote.close();
|
|
6321
|
+
}
|
|
6322
|
+
}
|
|
6323
|
+
async function storageSync(options) {
|
|
6324
|
+
const pull = await storagePull(options);
|
|
6325
|
+
const push = await storagePush(options);
|
|
6326
|
+
return { pull, push };
|
|
6327
|
+
}
|
|
6328
|
+
function getSyncMetaAll() {
|
|
6329
|
+
const db = getDb();
|
|
6330
|
+
ensureSyncMetaTable(db);
|
|
6331
|
+
return db.query("SELECT table_name, last_synced_at, direction FROM _machines_sync_meta ORDER BY table_name, direction").all();
|
|
6332
|
+
}
|
|
6333
|
+
function getStorageStatus() {
|
|
6334
|
+
const activeEnv = getStorageDatabaseEnv();
|
|
6335
|
+
return {
|
|
6336
|
+
configured: Boolean(activeEnv),
|
|
6337
|
+
mode: getStorageMode(),
|
|
6338
|
+
env: STORAGE_DATABASE_ENV,
|
|
6339
|
+
activeEnv: activeEnv?.name ?? null,
|
|
6340
|
+
service: "machines",
|
|
6341
|
+
tables: STORAGE_TABLES,
|
|
6342
|
+
sync: getSyncMetaAll()
|
|
6343
|
+
};
|
|
6344
|
+
}
|
|
6345
|
+
function resolveTables(tables) {
|
|
6346
|
+
if (!tables || tables.length === 0)
|
|
6347
|
+
return [...STORAGE_TABLES];
|
|
6348
|
+
const allowed = new Set(STORAGE_TABLES);
|
|
6349
|
+
const requested = tables.map((table) => table.trim()).filter(Boolean);
|
|
6350
|
+
const invalid = requested.filter((table) => !allowed.has(table));
|
|
6351
|
+
if (invalid.length > 0)
|
|
6352
|
+
throw new Error(`Unknown machines storage table(s): ${invalid.join(", ")}`);
|
|
6353
|
+
return requested;
|
|
6354
|
+
}
|
|
6355
|
+
async function pushTable(db, remote, table) {
|
|
6356
|
+
const result = { table, rowsRead: 0, rowsWritten: 0, errors: [] };
|
|
6357
|
+
try {
|
|
6358
|
+
if (!tableExists(db, table))
|
|
6359
|
+
return result;
|
|
6360
|
+
const rows = db.query(`SELECT * FROM ${quoteIdent(table)}`).all();
|
|
6361
|
+
result.rowsRead = rows.length;
|
|
6362
|
+
if (rows.length === 0)
|
|
6363
|
+
return result;
|
|
6364
|
+
const remoteColumns = await getRemoteColumns(remote, table);
|
|
6365
|
+
const columns = filterRemoteColumns(remoteColumns, Object.keys(rows[0]));
|
|
6366
|
+
result.rowsWritten = await upsertPg(remote, table, columns, rows);
|
|
6367
|
+
} catch (error) {
|
|
6368
|
+
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
6369
|
+
}
|
|
6370
|
+
return result;
|
|
6371
|
+
}
|
|
6372
|
+
async function pullTable(remote, db, table) {
|
|
6373
|
+
const result = { table, rowsRead: 0, rowsWritten: 0, errors: [] };
|
|
6374
|
+
try {
|
|
6375
|
+
if (!tableExists(db, table))
|
|
6376
|
+
return result;
|
|
6377
|
+
const rows = await remote.all(`SELECT * FROM ${quoteIdent(table)}`);
|
|
6378
|
+
result.rowsRead = rows.length;
|
|
6379
|
+
if (rows.length === 0)
|
|
6380
|
+
return result;
|
|
6381
|
+
const columns = filterLocalColumns(db, table, Object.keys(rows[0]));
|
|
6382
|
+
result.rowsWritten = upsertSqlite(db, table, columns, rows);
|
|
6383
|
+
} catch (error) {
|
|
6384
|
+
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
6385
|
+
}
|
|
6386
|
+
return result;
|
|
6387
|
+
}
|
|
6388
|
+
async function getRemoteColumns(remote, table) {
|
|
6389
|
+
const rows = await remote.all("SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = ?", table);
|
|
6390
|
+
return new Set(rows.map((row) => row.column_name));
|
|
6391
|
+
}
|
|
6392
|
+
function filterRemoteColumns(remoteColumns, columns) {
|
|
6393
|
+
if (remoteColumns.size === 0)
|
|
6394
|
+
return columns;
|
|
6395
|
+
return columns.filter((column) => remoteColumns.has(column));
|
|
6396
|
+
}
|
|
6397
|
+
function filterLocalColumns(db, table, columns) {
|
|
6398
|
+
const rows = db.query(`PRAGMA table_info(${quoteIdent(table)})`).all();
|
|
6399
|
+
const allowed = new Set(rows.map((row) => row.name));
|
|
6400
|
+
return columns.filter((column) => allowed.has(column));
|
|
6401
|
+
}
|
|
6402
|
+
async function upsertPg(remote, table, columns, rows) {
|
|
6403
|
+
if (columns.length === 0)
|
|
6404
|
+
return 0;
|
|
6405
|
+
const primaryKeys = PRIMARY_KEYS[table];
|
|
6406
|
+
const columnList = columns.map(quoteIdent).join(", ");
|
|
6407
|
+
const placeholders = columns.map(() => "?").join(", ");
|
|
6408
|
+
const keyList = primaryKeys.map(quoteIdent).join(", ");
|
|
6409
|
+
const updateColumns = columns.filter((column) => !primaryKeys.includes(column));
|
|
6410
|
+
const fallbackKey = primaryKeys[0];
|
|
6411
|
+
const setClause = updateColumns.length > 0 ? updateColumns.map((column) => `${quoteIdent(column)} = EXCLUDED.${quoteIdent(column)}`).join(", ") : `${quoteIdent(fallbackKey)} = EXCLUDED.${quoteIdent(fallbackKey)}`;
|
|
6412
|
+
for (const row of rows) {
|
|
6413
|
+
await remote.run(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES (${placeholders})
|
|
6414
|
+
ON CONFLICT (${keyList}) DO UPDATE SET ${setClause}`, ...columns.map((column) => coerceForPg(row[column])));
|
|
6415
|
+
}
|
|
6416
|
+
return rows.length;
|
|
6417
|
+
}
|
|
6418
|
+
function upsertSqlite(db, table, columns, rows) {
|
|
6419
|
+
if (columns.length === 0)
|
|
6420
|
+
return 0;
|
|
6421
|
+
const primaryKeys = PRIMARY_KEYS[table];
|
|
6422
|
+
const columnList = columns.map(quoteIdent).join(", ");
|
|
6423
|
+
const placeholders = columns.map(() => "?").join(", ");
|
|
6424
|
+
const keyList = primaryKeys.map(quoteIdent).join(", ");
|
|
6425
|
+
const updateColumns = columns.filter((column) => !primaryKeys.includes(column));
|
|
6426
|
+
const fallbackKey = primaryKeys[0];
|
|
6427
|
+
const setClause = updateColumns.length > 0 ? updateColumns.map((column) => `${quoteIdent(column)} = excluded.${quoteIdent(column)}`).join(", ") : `${quoteIdent(fallbackKey)} = excluded.${quoteIdent(fallbackKey)}`;
|
|
6428
|
+
const statement = db.query(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES (${placeholders})
|
|
6429
|
+
ON CONFLICT (${keyList}) DO UPDATE SET ${setClause}`);
|
|
6430
|
+
const insert = db.transaction((batch) => {
|
|
6431
|
+
for (const row of batch)
|
|
6432
|
+
statement.run(...columns.map((column) => coerceForSqlite(row[column])));
|
|
6433
|
+
});
|
|
6434
|
+
insert(rows);
|
|
6435
|
+
return rows.length;
|
|
6436
|
+
}
|
|
6437
|
+
function recordSyncMeta(db, direction, results) {
|
|
6438
|
+
ensureSyncMetaTable(db);
|
|
6439
|
+
const now = new Date().toISOString();
|
|
6440
|
+
const statement = db.query(`
|
|
6441
|
+
INSERT INTO _machines_sync_meta (table_name, last_synced_at, direction)
|
|
6442
|
+
VALUES (?, ?, ?)
|
|
6443
|
+
ON CONFLICT(table_name, direction) DO UPDATE SET last_synced_at = excluded.last_synced_at
|
|
6444
|
+
`);
|
|
6445
|
+
for (const result of results) {
|
|
6446
|
+
if (result.errors.length > 0)
|
|
6447
|
+
continue;
|
|
6448
|
+
statement.run(result.table, now, direction);
|
|
6449
|
+
}
|
|
6450
|
+
}
|
|
6451
|
+
function ensureSyncMetaTable(db) {
|
|
6452
|
+
db.exec(`
|
|
6453
|
+
CREATE TABLE IF NOT EXISTS _machines_sync_meta (
|
|
6454
|
+
table_name TEXT NOT NULL,
|
|
6455
|
+
last_synced_at TEXT,
|
|
6456
|
+
direction TEXT NOT NULL CHECK(direction IN ('push', 'pull')),
|
|
6457
|
+
PRIMARY KEY (table_name, direction)
|
|
6458
|
+
)
|
|
6459
|
+
`);
|
|
6460
|
+
}
|
|
6461
|
+
function tableExists(db, table) {
|
|
6462
|
+
const row = db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(table);
|
|
6463
|
+
return Boolean(row);
|
|
6464
|
+
}
|
|
6465
|
+
function quoteIdent(identifier) {
|
|
6466
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
6467
|
+
}
|
|
6468
|
+
function coerceForPg(value) {
|
|
6469
|
+
if (value === undefined || value === null)
|
|
6470
|
+
return null;
|
|
6471
|
+
if (value instanceof Date)
|
|
6472
|
+
return value.toISOString();
|
|
6473
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array)
|
|
6474
|
+
return value;
|
|
6475
|
+
if (typeof value === "object")
|
|
6476
|
+
return JSON.stringify(value);
|
|
6477
|
+
return value;
|
|
6478
|
+
}
|
|
6479
|
+
function coerceForSqlite(value) {
|
|
6480
|
+
if (value === undefined || value === null)
|
|
6481
|
+
return null;
|
|
6482
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean")
|
|
6483
|
+
return value;
|
|
6484
|
+
if (value instanceof Date)
|
|
6485
|
+
return value.toISOString();
|
|
6486
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array)
|
|
6487
|
+
return value;
|
|
6488
|
+
if (typeof value === "object")
|
|
6489
|
+
return JSON.stringify(value);
|
|
6490
|
+
return String(value);
|
|
6491
|
+
}
|
|
5759
6492
|
// src/mcp/server.ts
|
|
6493
|
+
function buildServer(version = getPackageVersion()) {
|
|
6494
|
+
return createMcpServer(version);
|
|
6495
|
+
}
|
|
5760
6496
|
function createMcpServer(version) {
|
|
5761
6497
|
const server = new McpServer({ name: "machines", version });
|
|
5762
6498
|
server.tool("machines_status", "Return local machine fleet status paths and machine identity.", {}, async () => ({
|
|
@@ -5789,6 +6525,33 @@ function createMcpServer(version) {
|
|
|
5789
6525
|
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
6526
|
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
6527
|
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) }] }));
|
|
6528
|
+
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 }) => ({
|
|
6529
|
+
content: [{ type: "text", text: JSON.stringify(discoverMachineTopology({ includeTailscale: include_tailscale !== false }), null, 2) }]
|
|
6530
|
+
}));
|
|
6531
|
+
server.tool("machines_compatibility", "Check remote package, command, and workspace compatibility for open-* consumers.", {
|
|
6532
|
+
machine_id: exports_external.string().optional().describe("Machine identifier"),
|
|
6533
|
+
commands: exports_external.array(exports_external.object({
|
|
6534
|
+
command: exports_external.string(),
|
|
6535
|
+
expectedVersion: exports_external.string().optional(),
|
|
6536
|
+
versionArgs: exports_external.string().optional(),
|
|
6537
|
+
required: exports_external.boolean().optional()
|
|
6538
|
+
})).optional().describe("Commands to check"),
|
|
6539
|
+
packages: exports_external.array(exports_external.object({
|
|
6540
|
+
name: exports_external.string(),
|
|
6541
|
+
command: exports_external.string().optional(),
|
|
6542
|
+
expectedVersion: exports_external.string().optional(),
|
|
6543
|
+
required: exports_external.boolean().optional()
|
|
6544
|
+
})).optional().describe("Package-backed CLI checks"),
|
|
6545
|
+
workspaces: exports_external.array(exports_external.object({
|
|
6546
|
+
path: exports_external.string(),
|
|
6547
|
+
label: exports_external.string().optional(),
|
|
6548
|
+
expectedPackageName: exports_external.string().optional(),
|
|
6549
|
+
expectedVersion: exports_external.string().optional(),
|
|
6550
|
+
required: exports_external.boolean().optional()
|
|
6551
|
+
})).optional().describe("Workspace paths and package metadata to check")
|
|
6552
|
+
}, async ({ machine_id, commands, packages, workspaces }) => ({
|
|
6553
|
+
content: [{ type: "text", text: JSON.stringify(checkMachineCompatibility({ machineId: machine_id, commands, packages, workspaces }), null, 2) }]
|
|
6554
|
+
}));
|
|
5792
6555
|
server.tool("machines_diff", "Show manifest differences between two machines.", {
|
|
5793
6556
|
left_machine_id: exports_external.string().describe("Left machine identifier"),
|
|
5794
6557
|
right_machine_id: exports_external.string().optional().describe("Right machine identifier")
|
|
@@ -5850,24 +6613,116 @@ function createMcpServer(version) {
|
|
|
5850
6613
|
server.tool("machines_serve_dashboard", "Render the current dashboard HTML.", {}, async () => ({
|
|
5851
6614
|
content: [{ type: "text", text: renderDashboardHtml() }]
|
|
5852
6615
|
}));
|
|
6616
|
+
server.tool("storage_status", "Show machines storage sync configuration and local sync history.", {}, async () => ({
|
|
6617
|
+
content: [{ type: "text", text: JSON.stringify(getStorageStatus(), null, 2) }]
|
|
6618
|
+
}));
|
|
6619
|
+
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) }] }));
|
|
6620
|
+
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) }] }));
|
|
6621
|
+
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
6622
|
return server;
|
|
5854
6623
|
}
|
|
5855
6624
|
|
|
5856
|
-
// src/mcp/
|
|
5857
|
-
|
|
6625
|
+
// src/mcp/http.ts
|
|
6626
|
+
var DEFAULT_HTTP_PORT = 8821;
|
|
6627
|
+
var HTTP_NAME = "machines";
|
|
6628
|
+
function isHttpMode(args = process.argv.slice(2)) {
|
|
6629
|
+
return args.includes("--http") || process.env.MCP_HTTP === "1";
|
|
6630
|
+
}
|
|
6631
|
+
function resolveHttpPort(args = process.argv.slice(2)) {
|
|
6632
|
+
for (let i = 0;i < args.length; i++) {
|
|
6633
|
+
const arg = args[i];
|
|
6634
|
+
if (arg === "--port" && args[i + 1]) {
|
|
6635
|
+
return parsePort(args[i + 1]);
|
|
6636
|
+
}
|
|
6637
|
+
if (arg.startsWith("--port=")) {
|
|
6638
|
+
return parsePort(arg.slice("--port=".length));
|
|
6639
|
+
}
|
|
6640
|
+
}
|
|
6641
|
+
const envPort = process.env.MCP_HTTP_PORT;
|
|
6642
|
+
if (envPort) {
|
|
6643
|
+
return parsePort(envPort);
|
|
6644
|
+
}
|
|
6645
|
+
return DEFAULT_HTTP_PORT;
|
|
6646
|
+
}
|
|
6647
|
+
function parsePort(raw) {
|
|
6648
|
+
const port = Number.parseInt(raw, 10);
|
|
6649
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
6650
|
+
throw new Error(`Invalid port: ${raw}`);
|
|
6651
|
+
}
|
|
6652
|
+
return port;
|
|
6653
|
+
}
|
|
6654
|
+
function pathnameFromRequest(req) {
|
|
6655
|
+
return new URL(req.url ?? "/", "http://127.0.0.1").pathname;
|
|
6656
|
+
}
|
|
6657
|
+
async function readRequestBody(req) {
|
|
6658
|
+
if (req.method !== "POST" && req.method !== "DELETE") {
|
|
6659
|
+
return;
|
|
6660
|
+
}
|
|
6661
|
+
const chunks = [];
|
|
6662
|
+
for await (const chunk of req) {
|
|
6663
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
6664
|
+
}
|
|
6665
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
6666
|
+
if (!text) {
|
|
6667
|
+
return;
|
|
6668
|
+
}
|
|
6669
|
+
return JSON.parse(text);
|
|
6670
|
+
}
|
|
6671
|
+
async function handleMcpRequest(req, res) {
|
|
6672
|
+
const server = buildServer();
|
|
6673
|
+
const transport = new StreamableHTTPServerTransport({
|
|
6674
|
+
sessionIdGenerator: undefined
|
|
6675
|
+
});
|
|
6676
|
+
await server.connect(transport);
|
|
5858
6677
|
try {
|
|
5859
|
-
const
|
|
5860
|
-
|
|
5861
|
-
}
|
|
5862
|
-
|
|
6678
|
+
const body = await readRequestBody(req);
|
|
6679
|
+
await transport.handleRequest(req, res, body);
|
|
6680
|
+
} finally {
|
|
6681
|
+
res.on("close", () => {
|
|
6682
|
+
transport.close().catch(() => {
|
|
6683
|
+
return;
|
|
6684
|
+
});
|
|
6685
|
+
server.close().catch(() => {
|
|
6686
|
+
return;
|
|
6687
|
+
});
|
|
6688
|
+
});
|
|
5863
6689
|
}
|
|
5864
6690
|
}
|
|
6691
|
+
function startHttpServer(options = {}) {
|
|
6692
|
+
const host = options.host ?? "127.0.0.1";
|
|
6693
|
+
const port = options.port ?? resolveHttpPort();
|
|
6694
|
+
const name = options.name ?? HTTP_NAME;
|
|
6695
|
+
const httpServer = createServer(async (req, res) => {
|
|
6696
|
+
const path = pathnameFromRequest(req);
|
|
6697
|
+
if (req.method === "GET" && path === "/health") {
|
|
6698
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
6699
|
+
res.end(JSON.stringify({ status: "ok", name }));
|
|
6700
|
+
return;
|
|
6701
|
+
}
|
|
6702
|
+
if (path === "/mcp") {
|
|
6703
|
+
await handleMcpRequest(req, res);
|
|
6704
|
+
return;
|
|
6705
|
+
}
|
|
6706
|
+
res.writeHead(404, { "content-type": "application/json" });
|
|
6707
|
+
res.end(JSON.stringify({ error: "Not found" }));
|
|
6708
|
+
});
|
|
6709
|
+
httpServer.listen(port, host, () => {
|
|
6710
|
+
const address = httpServer.address();
|
|
6711
|
+
const boundPort = typeof address === "object" && address ? address.port : port;
|
|
6712
|
+
console.error(`machines-mcp HTTP listening on http://${host}:${boundPort}`);
|
|
6713
|
+
});
|
|
6714
|
+
return httpServer;
|
|
6715
|
+
}
|
|
6716
|
+
|
|
6717
|
+
// src/mcp/index.ts
|
|
5865
6718
|
function printHelp() {
|
|
5866
6719
|
console.log(`Usage: machines-mcp [options]
|
|
5867
6720
|
|
|
5868
|
-
MCP server for machine fleet management tools (stdio transport)
|
|
6721
|
+
MCP server for machine fleet management tools (stdio transport by default)
|
|
5869
6722
|
|
|
5870
6723
|
Options:
|
|
6724
|
+
--http Start Streamable HTTP transport on 127.0.0.1 (or MCP_HTTP=1)
|
|
6725
|
+
--port <n> HTTP port (default: 8821, or MCP_HTTP_PORT env)
|
|
5871
6726
|
-V, --version output the version number
|
|
5872
6727
|
-h, --help display help for command`);
|
|
5873
6728
|
}
|
|
@@ -5877,9 +6732,13 @@ if (args.includes("--help") || args.includes("-h")) {
|
|
|
5877
6732
|
process.exit(0);
|
|
5878
6733
|
}
|
|
5879
6734
|
if (args.includes("--version") || args.includes("-V")) {
|
|
5880
|
-
console.log(
|
|
6735
|
+
console.log(getPackageVersion());
|
|
5881
6736
|
process.exit(0);
|
|
5882
6737
|
}
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
6738
|
+
if (isHttpMode(args)) {
|
|
6739
|
+
startHttpServer({ port: resolveHttpPort(args) });
|
|
6740
|
+
} else {
|
|
6741
|
+
const server = buildServer();
|
|
6742
|
+
const transport = new StdioServerTransport;
|
|
6743
|
+
await server.connect(transport);
|
|
6744
|
+
}
|