@hasna/machines 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -8
- package/dist/cli/index.js +714 -144
- package/dist/cli-utils.d.ts +7 -0
- package/dist/cli-utils.d.ts.map +1 -0
- package/dist/commands/apps.d.ts +3 -1
- package/dist/commands/apps.d.ts.map +1 -1
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/install-claude.d.ts +3 -1
- package/dist/commands/install-claude.d.ts.map +1 -1
- package/dist/commands/notifications.d.ts +5 -2
- package/dist/commands/notifications.d.ts.map +1 -1
- package/dist/commands/self-test.d.ts +3 -0
- package/dist/commands/self-test.d.ts.map +1 -0
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +668 -315
- package/dist/mcp/index.js +2055 -1747
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/remote.d.ts +9 -0
- package/dist/remote.d.ts.map +1 -0
- package/dist/types.d.ts +68 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -16698,17 +16698,95 @@ function diffMachines(leftMachineId, rightMachineId) {
|
|
|
16698
16698
|
};
|
|
16699
16699
|
}
|
|
16700
16700
|
|
|
16701
|
+
// src/remote.ts
|
|
16702
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
16703
|
+
|
|
16704
|
+
// src/commands/ssh.ts
|
|
16705
|
+
import { spawnSync } from "child_process";
|
|
16706
|
+
function envReachableHosts() {
|
|
16707
|
+
const raw = process.env["HASNA_MACHINES_REACHABLE_HOSTS"];
|
|
16708
|
+
return new Set((raw || "").split(",").map((value) => value.trim()).filter(Boolean));
|
|
16709
|
+
}
|
|
16710
|
+
function isReachable(host) {
|
|
16711
|
+
const overrides = envReachableHosts();
|
|
16712
|
+
if (overrides.size > 0) {
|
|
16713
|
+
return overrides.has(host);
|
|
16714
|
+
}
|
|
16715
|
+
const probe = spawnSync("bash", ["-lc", `getent hosts ${host} >/dev/null 2>&1 || ping -c 1 -W 1 ${host} >/dev/null 2>&1`], {
|
|
16716
|
+
stdio: "ignore"
|
|
16717
|
+
});
|
|
16718
|
+
return probe.status === 0;
|
|
16719
|
+
}
|
|
16720
|
+
function resolveSshTarget(machineId) {
|
|
16721
|
+
const machine = getManifestMachine(machineId);
|
|
16722
|
+
if (!machine) {
|
|
16723
|
+
throw new Error(`Machine not found in manifest: ${machineId}`);
|
|
16724
|
+
}
|
|
16725
|
+
const current = detectCurrentMachineManifest();
|
|
16726
|
+
if (machine.id === current.id) {
|
|
16727
|
+
return {
|
|
16728
|
+
machineId,
|
|
16729
|
+
target: "localhost",
|
|
16730
|
+
route: "local"
|
|
16731
|
+
};
|
|
16732
|
+
}
|
|
16733
|
+
const lanTarget = machine.sshAddress || machine.hostname || machine.id;
|
|
16734
|
+
const tailscaleTarget = machine.tailscaleName || machine.hostname || machine.id;
|
|
16735
|
+
const route = isReachable(lanTarget) ? "lan" : "tailscale";
|
|
16736
|
+
return {
|
|
16737
|
+
machineId,
|
|
16738
|
+
target: route === "lan" ? lanTarget : tailscaleTarget,
|
|
16739
|
+
route
|
|
16740
|
+
};
|
|
16741
|
+
}
|
|
16742
|
+
function buildSshCommand(machineId, remoteCommand) {
|
|
16743
|
+
const resolved = resolveSshTarget(machineId);
|
|
16744
|
+
return remoteCommand ? `ssh ${resolved.target} ${JSON.stringify(remoteCommand)}` : `ssh ${resolved.target}`;
|
|
16745
|
+
}
|
|
16746
|
+
|
|
16747
|
+
// src/remote.ts
|
|
16748
|
+
function runMachineCommand(machineId, command) {
|
|
16749
|
+
const localMachineId = getLocalMachineId();
|
|
16750
|
+
const isLocal = machineId === localMachineId;
|
|
16751
|
+
const route = isLocal ? "local" : resolveSshTarget(machineId).route;
|
|
16752
|
+
const shellCommand = isLocal ? command : buildSshCommand(machineId, command);
|
|
16753
|
+
const result = spawnSync2("bash", ["-lc", shellCommand], {
|
|
16754
|
+
encoding: "utf8",
|
|
16755
|
+
env: process.env
|
|
16756
|
+
});
|
|
16757
|
+
return {
|
|
16758
|
+
machineId,
|
|
16759
|
+
source: route,
|
|
16760
|
+
stdout: result.stdout || "",
|
|
16761
|
+
stderr: result.stderr || "",
|
|
16762
|
+
exitCode: result.status ?? 1
|
|
16763
|
+
};
|
|
16764
|
+
}
|
|
16765
|
+
|
|
16701
16766
|
// src/commands/apps.ts
|
|
16702
16767
|
function getPackageName(app) {
|
|
16703
16768
|
return app.packageName || app.name;
|
|
16704
16769
|
}
|
|
16770
|
+
function getAppManager(machine, app) {
|
|
16771
|
+
if (app.manager)
|
|
16772
|
+
return app.manager;
|
|
16773
|
+
if (machine.platform === "macos")
|
|
16774
|
+
return "brew";
|
|
16775
|
+
if (machine.platform === "windows")
|
|
16776
|
+
return "winget";
|
|
16777
|
+
return "apt";
|
|
16778
|
+
}
|
|
16779
|
+
function shellQuote(value) {
|
|
16780
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
16781
|
+
}
|
|
16705
16782
|
function buildAppCommand(machine, app) {
|
|
16706
16783
|
const packageName = getPackageName(app);
|
|
16707
|
-
|
|
16784
|
+
const manager = getAppManager(machine, app);
|
|
16785
|
+
if (manager === "custom") {
|
|
16708
16786
|
return packageName;
|
|
16709
16787
|
}
|
|
16710
16788
|
if (machine.platform === "macos") {
|
|
16711
|
-
if (
|
|
16789
|
+
if (manager === "cask") {
|
|
16712
16790
|
return `brew install --cask ${packageName}`;
|
|
16713
16791
|
}
|
|
16714
16792
|
return `brew install ${packageName}`;
|
|
@@ -16718,18 +16796,48 @@ function buildAppCommand(machine, app) {
|
|
|
16718
16796
|
}
|
|
16719
16797
|
return `sudo apt-get install -y ${packageName}`;
|
|
16720
16798
|
}
|
|
16799
|
+
function buildAppProbeCommand(machine, app) {
|
|
16800
|
+
const packageName = shellQuote(getPackageName(app));
|
|
16801
|
+
const manager = getAppManager(machine, app);
|
|
16802
|
+
if (manager === "custom") {
|
|
16803
|
+
return `if command -v ${packageName} >/dev/null 2>&1; then printf 'installed=1\\nversion=custom\\n'; else printf 'installed=0\\n'; fi`;
|
|
16804
|
+
}
|
|
16805
|
+
if (machine.platform === "macos") {
|
|
16806
|
+
if (manager === "cask") {
|
|
16807
|
+
return `if brew list --cask ${packageName} >/dev/null 2>&1; then printf 'installed=1\\nversion=installed\\n'; else printf 'installed=0\\n'; fi`;
|
|
16808
|
+
}
|
|
16809
|
+
return `if brew list --versions ${packageName} >/dev/null 2>&1; then printf 'installed=1\\nversion='; brew list --versions ${packageName} | awk '{print $2}'; printf '\\n'; else printf 'installed=0\\n'; fi`;
|
|
16810
|
+
}
|
|
16811
|
+
if (machine.platform === "windows") {
|
|
16812
|
+
return `if winget list --id ${packageName} --exact >/dev/null 2>&1; then printf 'installed=1\\nversion=installed\\n'; else printf 'installed=0\\n'; fi`;
|
|
16813
|
+
}
|
|
16814
|
+
return `if dpkg-query -W -f='${"${Version}"}' ${packageName} >/tmp/machines-app-version 2>/dev/null; then printf 'installed=1\\nversion='; cat /tmp/machines-app-version; printf '\\n'; rm -f /tmp/machines-app-version; else printf 'installed=0\\n'; fi`;
|
|
16815
|
+
}
|
|
16721
16816
|
function buildAppSteps(machine) {
|
|
16722
16817
|
return (machine.apps || []).map((app) => ({
|
|
16723
16818
|
id: `app-${app.name}`,
|
|
16724
16819
|
title: `Install ${app.name} on ${machine.id}`,
|
|
16725
16820
|
command: buildAppCommand(machine, app),
|
|
16726
|
-
manager: app
|
|
16821
|
+
manager: getAppManager(machine, app) === "custom" ? "custom" : machine.platform === "macos" ? "brew" : machine.platform === "windows" ? "custom" : "apt",
|
|
16727
16822
|
privileged: machine.platform === "linux"
|
|
16728
16823
|
}));
|
|
16729
16824
|
}
|
|
16730
16825
|
function resolveMachine(machineId) {
|
|
16731
16826
|
return (machineId ? getManifestMachine(machineId) : null) || detectCurrentMachineManifest();
|
|
16732
16827
|
}
|
|
16828
|
+
function parseProbeOutput(app, machine, stdout) {
|
|
16829
|
+
const lines = stdout.trim().split(`
|
|
16830
|
+
`).filter(Boolean);
|
|
16831
|
+
const installedLine = lines.find((line) => line.startsWith("installed="));
|
|
16832
|
+
const versionLine = lines.find((line) => line.startsWith("version="));
|
|
16833
|
+
return {
|
|
16834
|
+
name: app.name,
|
|
16835
|
+
packageName: getPackageName(app),
|
|
16836
|
+
manager: getAppManager(machine, app),
|
|
16837
|
+
installed: installedLine === "installed=1",
|
|
16838
|
+
version: versionLine?.slice("version=".length) || undefined
|
|
16839
|
+
};
|
|
16840
|
+
}
|
|
16733
16841
|
function listApps(machineId) {
|
|
16734
16842
|
const machine = resolveMachine(machineId);
|
|
16735
16843
|
return {
|
|
@@ -16746,6 +16854,26 @@ function buildAppsPlan(machineId) {
|
|
|
16746
16854
|
executed: 0
|
|
16747
16855
|
};
|
|
16748
16856
|
}
|
|
16857
|
+
function getAppsStatus(machineId) {
|
|
16858
|
+
const machine = resolveMachine(machineId);
|
|
16859
|
+
const apps = (machine.apps || []).map((app) => {
|
|
16860
|
+
const probe = runMachineCommand(machine.id, buildAppProbeCommand(machine, app));
|
|
16861
|
+
return parseProbeOutput(app, machine, probe.stdout);
|
|
16862
|
+
});
|
|
16863
|
+
return {
|
|
16864
|
+
machineId: machine.id,
|
|
16865
|
+
source: apps.length > 0 ? runMachineCommand(machine.id, "true").source : machine.id === detectCurrentMachineManifest().id ? "local" : runMachineCommand(machine.id, "true").source,
|
|
16866
|
+
apps
|
|
16867
|
+
};
|
|
16868
|
+
}
|
|
16869
|
+
function diffApps(machineId) {
|
|
16870
|
+
const status = getAppsStatus(machineId);
|
|
16871
|
+
return {
|
|
16872
|
+
...status,
|
|
16873
|
+
missing: status.apps.filter((app) => !app.installed).map((app) => app.name),
|
|
16874
|
+
installed: status.apps.filter((app) => app.installed).map((app) => app.name)
|
|
16875
|
+
};
|
|
16876
|
+
}
|
|
16749
16877
|
function runAppsInstall(machineId, options = {}) {
|
|
16750
16878
|
const plan = buildAppsPlan(machineId);
|
|
16751
16879
|
if (!options.apply)
|
|
@@ -16779,6 +16907,13 @@ var AI_CLI_PACKAGES = {
|
|
|
16779
16907
|
codex: "@openai/codex",
|
|
16780
16908
|
gemini: "@google/gemini-cli"
|
|
16781
16909
|
};
|
|
16910
|
+
function getToolBinary(tool) {
|
|
16911
|
+
if (tool === "claude")
|
|
16912
|
+
return process.env["HASNA_MACHINES_CLAUDE_BINARY"] || "claude";
|
|
16913
|
+
if (tool === "codex")
|
|
16914
|
+
return process.env["HASNA_MACHINES_CODEX_BINARY"] || "codex";
|
|
16915
|
+
return process.env["HASNA_MACHINES_GEMINI_BINARY"] || "gemini";
|
|
16916
|
+
}
|
|
16782
16917
|
function normalizeTools(tools) {
|
|
16783
16918
|
if (!tools || tools.length === 0) {
|
|
16784
16919
|
return ["claude", "codex", "gemini"];
|
|
@@ -16798,8 +16933,27 @@ function buildInstallSteps(machine, tools) {
|
|
|
16798
16933
|
manager: "bun"
|
|
16799
16934
|
}));
|
|
16800
16935
|
}
|
|
16936
|
+
function resolveMachine2(machineId) {
|
|
16937
|
+
return (machineId ? getManifestMachine(machineId) : null) || detectCurrentMachineManifest();
|
|
16938
|
+
}
|
|
16939
|
+
function buildProbeCommand(tool) {
|
|
16940
|
+
const binary = getToolBinary(tool);
|
|
16941
|
+
return `if command -v ${binary} >/dev/null 2>&1; then printf 'installed=1\\nversion='; ${binary} --version 2>/dev/null | head -n 1; printf '\\n'; else printf 'installed=0\\n'; fi`;
|
|
16942
|
+
}
|
|
16943
|
+
function parseProbe(tool, stdout) {
|
|
16944
|
+
const lines = stdout.trim().split(`
|
|
16945
|
+
`).filter(Boolean);
|
|
16946
|
+
const installedLine = lines.find((line) => line.startsWith("installed="));
|
|
16947
|
+
const versionLine = lines.find((line) => line.startsWith("version="));
|
|
16948
|
+
return {
|
|
16949
|
+
tool,
|
|
16950
|
+
packageName: AI_CLI_PACKAGES[tool],
|
|
16951
|
+
installed: installedLine === "installed=1",
|
|
16952
|
+
version: versionLine?.slice("version=".length) || undefined
|
|
16953
|
+
};
|
|
16954
|
+
}
|
|
16801
16955
|
function buildClaudeInstallPlan(machineId, tools) {
|
|
16802
|
-
const machine = (machineId
|
|
16956
|
+
const machine = resolveMachine2(machineId);
|
|
16803
16957
|
return {
|
|
16804
16958
|
machineId: machine.id,
|
|
16805
16959
|
mode: "plan",
|
|
@@ -16807,6 +16961,24 @@ function buildClaudeInstallPlan(machineId, tools) {
|
|
|
16807
16961
|
executed: 0
|
|
16808
16962
|
};
|
|
16809
16963
|
}
|
|
16964
|
+
function getClaudeCliStatus(machineId, tools) {
|
|
16965
|
+
const machine = resolveMachine2(machineId);
|
|
16966
|
+
const normalizedTools = normalizeTools(tools);
|
|
16967
|
+
const route = runMachineCommand(machine.id, "true").source;
|
|
16968
|
+
return {
|
|
16969
|
+
machineId: machine.id,
|
|
16970
|
+
source: route,
|
|
16971
|
+
tools: normalizedTools.map((tool) => parseProbe(tool, runMachineCommand(machine.id, buildProbeCommand(tool)).stdout))
|
|
16972
|
+
};
|
|
16973
|
+
}
|
|
16974
|
+
function diffClaudeCli(machineId, tools) {
|
|
16975
|
+
const status = getClaudeCliStatus(machineId, tools);
|
|
16976
|
+
return {
|
|
16977
|
+
...status,
|
|
16978
|
+
missing: status.tools.filter((tool) => !tool.installed).map((tool) => tool.tool),
|
|
16979
|
+
installed: status.tools.filter((tool) => tool.installed).map((tool) => tool.tool)
|
|
16980
|
+
};
|
|
16981
|
+
}
|
|
16810
16982
|
function runClaudeInstall(machineId, tools, options = {}) {
|
|
16811
16983
|
const plan = buildClaudeInstallPlan(machineId, tools);
|
|
16812
16984
|
if (!options.apply)
|
|
@@ -16919,6 +17091,138 @@ var notificationConfigSchema = exports_external.object({
|
|
|
16919
17091
|
function sortChannels(channels) {
|
|
16920
17092
|
return [...channels].sort((left, right) => left.id.localeCompare(right.id));
|
|
16921
17093
|
}
|
|
17094
|
+
function shellQuote2(value) {
|
|
17095
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
17096
|
+
}
|
|
17097
|
+
function hasCommand(binary) {
|
|
17098
|
+
const result = Bun.spawnSync(["bash", "-lc", `command -v ${binary} >/dev/null 2>&1`], {
|
|
17099
|
+
stdout: "ignore",
|
|
17100
|
+
stderr: "ignore",
|
|
17101
|
+
env: process.env
|
|
17102
|
+
});
|
|
17103
|
+
return result.exitCode === 0;
|
|
17104
|
+
}
|
|
17105
|
+
function buildNotificationPreview(channel, event, message) {
|
|
17106
|
+
if (channel.type === "email") {
|
|
17107
|
+
return `send email to ${channel.target}: [${event}] ${message}`;
|
|
17108
|
+
}
|
|
17109
|
+
if (channel.type === "webhook") {
|
|
17110
|
+
return `POST ${channel.target} with payload {"event":"${event}","message":"${message}"}`;
|
|
17111
|
+
}
|
|
17112
|
+
return `${channel.target} --event ${event} --message ${JSON.stringify(message)}`;
|
|
17113
|
+
}
|
|
17114
|
+
async function dispatchEmail(channel, event, message) {
|
|
17115
|
+
const subject = `[${event}] machines notification`;
|
|
17116
|
+
const body = `To: ${channel.target}
|
|
17117
|
+
Subject: ${subject}
|
|
17118
|
+
Content-Type: text/plain; charset=utf-8
|
|
17119
|
+
|
|
17120
|
+
${message}
|
|
17121
|
+
`;
|
|
17122
|
+
if (hasCommand("sendmail")) {
|
|
17123
|
+
const result = Bun.spawnSync(["bash", "-lc", "sendmail -t"], {
|
|
17124
|
+
stdin: new TextEncoder().encode(body),
|
|
17125
|
+
stdout: "pipe",
|
|
17126
|
+
stderr: "pipe",
|
|
17127
|
+
env: process.env
|
|
17128
|
+
});
|
|
17129
|
+
if (result.exitCode !== 0) {
|
|
17130
|
+
throw new Error(result.stderr.toString().trim() || `sendmail exited with ${result.exitCode}`);
|
|
17131
|
+
}
|
|
17132
|
+
return {
|
|
17133
|
+
channelId: channel.id,
|
|
17134
|
+
event,
|
|
17135
|
+
delivered: true,
|
|
17136
|
+
transport: channel.type,
|
|
17137
|
+
detail: `Delivered via sendmail to ${channel.target}`
|
|
17138
|
+
};
|
|
17139
|
+
}
|
|
17140
|
+
if (hasCommand("mail")) {
|
|
17141
|
+
const command = `printf %s ${shellQuote2(message)} | mail -s ${shellQuote2(subject)} ${shellQuote2(channel.target)}`;
|
|
17142
|
+
const result = Bun.spawnSync(["bash", "-lc", command], {
|
|
17143
|
+
stdout: "pipe",
|
|
17144
|
+
stderr: "pipe",
|
|
17145
|
+
env: process.env
|
|
17146
|
+
});
|
|
17147
|
+
if (result.exitCode !== 0) {
|
|
17148
|
+
throw new Error(result.stderr.toString().trim() || `mail exited with ${result.exitCode}`);
|
|
17149
|
+
}
|
|
17150
|
+
return {
|
|
17151
|
+
channelId: channel.id,
|
|
17152
|
+
event,
|
|
17153
|
+
delivered: true,
|
|
17154
|
+
transport: channel.type,
|
|
17155
|
+
detail: `Delivered via mail to ${channel.target}`
|
|
17156
|
+
};
|
|
17157
|
+
}
|
|
17158
|
+
throw new Error("No local email transport available. Install sendmail or mail.");
|
|
17159
|
+
}
|
|
17160
|
+
async function dispatchWebhook(channel, event, message) {
|
|
17161
|
+
const response = await fetch(channel.target, {
|
|
17162
|
+
method: "POST",
|
|
17163
|
+
headers: {
|
|
17164
|
+
"content-type": "application/json"
|
|
17165
|
+
},
|
|
17166
|
+
body: JSON.stringify({
|
|
17167
|
+
channelId: channel.id,
|
|
17168
|
+
event,
|
|
17169
|
+
message,
|
|
17170
|
+
sentAt: new Date().toISOString()
|
|
17171
|
+
})
|
|
17172
|
+
});
|
|
17173
|
+
if (!response.ok) {
|
|
17174
|
+
const text = await response.text();
|
|
17175
|
+
throw new Error(`Webhook responded ${response.status}: ${text || response.statusText}`);
|
|
17176
|
+
}
|
|
17177
|
+
return {
|
|
17178
|
+
channelId: channel.id,
|
|
17179
|
+
event,
|
|
17180
|
+
delivered: true,
|
|
17181
|
+
transport: channel.type,
|
|
17182
|
+
detail: `Webhook accepted with HTTP ${response.status}`
|
|
17183
|
+
};
|
|
17184
|
+
}
|
|
17185
|
+
async function dispatchCommand(channel, event, message) {
|
|
17186
|
+
const result = Bun.spawnSync(["bash", "-lc", channel.target], {
|
|
17187
|
+
stdout: "pipe",
|
|
17188
|
+
stderr: "pipe",
|
|
17189
|
+
env: {
|
|
17190
|
+
...process.env,
|
|
17191
|
+
HASNA_MACHINES_NOTIFICATION_CHANNEL: channel.id,
|
|
17192
|
+
HASNA_MACHINES_NOTIFICATION_EVENT: event,
|
|
17193
|
+
HASNA_MACHINES_NOTIFICATION_MESSAGE: message
|
|
17194
|
+
}
|
|
17195
|
+
});
|
|
17196
|
+
if (result.exitCode !== 0) {
|
|
17197
|
+
throw new Error(result.stderr.toString().trim() || `command exited with ${result.exitCode}`);
|
|
17198
|
+
}
|
|
17199
|
+
const stdout = result.stdout.toString().trim();
|
|
17200
|
+
return {
|
|
17201
|
+
channelId: channel.id,
|
|
17202
|
+
event,
|
|
17203
|
+
delivered: true,
|
|
17204
|
+
transport: channel.type,
|
|
17205
|
+
detail: stdout || "Command completed successfully"
|
|
17206
|
+
};
|
|
17207
|
+
}
|
|
17208
|
+
async function dispatchChannel(channel, event, message) {
|
|
17209
|
+
if (!channel.enabled) {
|
|
17210
|
+
return {
|
|
17211
|
+
channelId: channel.id,
|
|
17212
|
+
event,
|
|
17213
|
+
delivered: false,
|
|
17214
|
+
transport: channel.type,
|
|
17215
|
+
detail: "Channel is disabled"
|
|
17216
|
+
};
|
|
17217
|
+
}
|
|
17218
|
+
if (channel.type === "email") {
|
|
17219
|
+
return dispatchEmail(channel, event, message);
|
|
17220
|
+
}
|
|
17221
|
+
if (channel.type === "webhook") {
|
|
17222
|
+
return dispatchWebhook(channel, event, message);
|
|
17223
|
+
}
|
|
17224
|
+
return dispatchCommand(channel, event, message);
|
|
17225
|
+
}
|
|
16922
17226
|
function getDefaultNotificationConfig() {
|
|
16923
17227
|
return {
|
|
16924
17228
|
version: 1,
|
|
@@ -16962,16 +17266,34 @@ function removeNotificationChannel(channelId) {
|
|
|
16962
17266
|
channels: config.channels.filter((channel) => channel.id !== channelId)
|
|
16963
17267
|
});
|
|
16964
17268
|
}
|
|
16965
|
-
function
|
|
16966
|
-
|
|
16967
|
-
|
|
16968
|
-
|
|
16969
|
-
|
|
16970
|
-
return
|
|
17269
|
+
async function dispatchNotificationEvent(event, message, options = {}) {
|
|
17270
|
+
const channels = readNotificationConfig().channels.filter((channel) => {
|
|
17271
|
+
if (options.channelId && channel.id !== options.channelId) {
|
|
17272
|
+
return false;
|
|
17273
|
+
}
|
|
17274
|
+
return channel.events.includes(event) || event === "manual.test";
|
|
17275
|
+
});
|
|
17276
|
+
const deliveries = [];
|
|
17277
|
+
for (const channel of channels) {
|
|
17278
|
+
try {
|
|
17279
|
+
deliveries.push(await dispatchChannel(channel, event, message));
|
|
17280
|
+
} catch (error) {
|
|
17281
|
+
deliveries.push({
|
|
17282
|
+
channelId: channel.id,
|
|
17283
|
+
event,
|
|
17284
|
+
delivered: false,
|
|
17285
|
+
transport: channel.type,
|
|
17286
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
17287
|
+
});
|
|
17288
|
+
}
|
|
16971
17289
|
}
|
|
16972
|
-
return
|
|
17290
|
+
return {
|
|
17291
|
+
event,
|
|
17292
|
+
message,
|
|
17293
|
+
deliveries
|
|
17294
|
+
};
|
|
16973
17295
|
}
|
|
16974
|
-
function testNotificationChannel(channelId, event = "manual.test", message = "machines notification test", options = {}) {
|
|
17296
|
+
async function testNotificationChannel(channelId, event = "manual.test", message = "machines notification test", options = {}) {
|
|
16975
17297
|
const channel = readNotificationConfig().channels.find((entry) => entry.id === channelId);
|
|
16976
17298
|
if (!channel) {
|
|
16977
17299
|
throw new Error(`Notification channel not found: ${channelId}`);
|
|
@@ -16982,77 +17304,25 @@ function testNotificationChannel(channelId, event = "manual.test", message = "ma
|
|
|
16982
17304
|
channelId,
|
|
16983
17305
|
mode: "plan",
|
|
16984
17306
|
delivered: false,
|
|
16985
|
-
preview
|
|
17307
|
+
preview,
|
|
17308
|
+
detail: "Preview only"
|
|
16986
17309
|
};
|
|
16987
17310
|
}
|
|
16988
17311
|
if (!options.yes) {
|
|
16989
17312
|
throw new Error("Notification test execution requires --yes.");
|
|
16990
17313
|
}
|
|
16991
|
-
|
|
16992
|
-
const result = Bun.spawnSync(["bash", "-lc", preview], {
|
|
16993
|
-
stdout: "pipe",
|
|
16994
|
-
stderr: "pipe",
|
|
16995
|
-
env: process.env
|
|
16996
|
-
});
|
|
16997
|
-
if (result.exitCode !== 0) {
|
|
16998
|
-
throw new Error(`Notification command failed (${channel.id}): ${result.stderr.toString().trim()}`);
|
|
16999
|
-
}
|
|
17000
|
-
}
|
|
17314
|
+
const delivery = await dispatchChannel(channel, event, message);
|
|
17001
17315
|
return {
|
|
17002
17316
|
channelId,
|
|
17003
17317
|
mode: "apply",
|
|
17004
|
-
delivered:
|
|
17005
|
-
preview
|
|
17318
|
+
delivered: delivery.delivered,
|
|
17319
|
+
preview,
|
|
17320
|
+
detail: delivery.detail
|
|
17006
17321
|
};
|
|
17007
17322
|
}
|
|
17008
17323
|
|
|
17009
17324
|
// src/commands/ports.ts
|
|
17010
|
-
import { spawnSync as
|
|
17011
|
-
|
|
17012
|
-
// src/commands/ssh.ts
|
|
17013
|
-
import { spawnSync } from "child_process";
|
|
17014
|
-
function envReachableHosts() {
|
|
17015
|
-
const raw = process.env["HASNA_MACHINES_REACHABLE_HOSTS"];
|
|
17016
|
-
return new Set((raw || "").split(",").map((value) => value.trim()).filter(Boolean));
|
|
17017
|
-
}
|
|
17018
|
-
function isReachable(host) {
|
|
17019
|
-
const overrides = envReachableHosts();
|
|
17020
|
-
if (overrides.size > 0) {
|
|
17021
|
-
return overrides.has(host);
|
|
17022
|
-
}
|
|
17023
|
-
const probe = spawnSync("bash", ["-lc", `getent hosts ${host} >/dev/null 2>&1 || ping -c 1 -W 1 ${host} >/dev/null 2>&1`], {
|
|
17024
|
-
stdio: "ignore"
|
|
17025
|
-
});
|
|
17026
|
-
return probe.status === 0;
|
|
17027
|
-
}
|
|
17028
|
-
function resolveSshTarget(machineId) {
|
|
17029
|
-
const machine = getManifestMachine(machineId);
|
|
17030
|
-
if (!machine) {
|
|
17031
|
-
throw new Error(`Machine not found in manifest: ${machineId}`);
|
|
17032
|
-
}
|
|
17033
|
-
const current = detectCurrentMachineManifest();
|
|
17034
|
-
if (machine.id === current.id) {
|
|
17035
|
-
return {
|
|
17036
|
-
machineId,
|
|
17037
|
-
target: "localhost",
|
|
17038
|
-
route: "local"
|
|
17039
|
-
};
|
|
17040
|
-
}
|
|
17041
|
-
const lanTarget = machine.sshAddress || machine.hostname || machine.id;
|
|
17042
|
-
const tailscaleTarget = machine.tailscaleName || machine.hostname || machine.id;
|
|
17043
|
-
const route = isReachable(lanTarget) ? "lan" : "tailscale";
|
|
17044
|
-
return {
|
|
17045
|
-
machineId,
|
|
17046
|
-
target: route === "lan" ? lanTarget : tailscaleTarget,
|
|
17047
|
-
route
|
|
17048
|
-
};
|
|
17049
|
-
}
|
|
17050
|
-
function buildSshCommand(machineId, remoteCommand) {
|
|
17051
|
-
const resolved = resolveSshTarget(machineId);
|
|
17052
|
-
return remoteCommand ? `ssh ${resolved.target} ${JSON.stringify(remoteCommand)}` : `ssh ${resolved.target}`;
|
|
17053
|
-
}
|
|
17054
|
-
|
|
17055
|
-
// src/commands/ports.ts
|
|
17325
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
17056
17326
|
function parseSsOutput(output) {
|
|
17057
17327
|
return output.trim().split(`
|
|
17058
17328
|
`).map((line) => line.trim()).filter(Boolean).map((line) => {
|
|
@@ -17094,7 +17364,7 @@ function listPorts(machineId) {
|
|
|
17094
17364
|
const isLocal = targetMachineId === getLocalMachineId();
|
|
17095
17365
|
const localCommand = "if command -v ss >/dev/null 2>&1; then ss -ltnpH; else lsof -nP -iTCP -sTCP:LISTEN; fi";
|
|
17096
17366
|
const command = isLocal ? localCommand : buildSshCommand(targetMachineId, localCommand);
|
|
17097
|
-
const result =
|
|
17367
|
+
const result = spawnSync3("bash", ["-lc", command], { encoding: "utf8" });
|
|
17098
17368
|
if (result.status !== 0) {
|
|
17099
17369
|
throw new Error(result.stderr || `Failed to list ports for ${targetMachineId}`);
|
|
17100
17370
|
}
|
|
@@ -17289,6 +17559,59 @@ function getStatus() {
|
|
|
17289
17559
|
};
|
|
17290
17560
|
}
|
|
17291
17561
|
|
|
17562
|
+
// src/commands/doctor.ts
|
|
17563
|
+
function makeCheck(id, status, summary, detail) {
|
|
17564
|
+
return { id, status, summary, detail };
|
|
17565
|
+
}
|
|
17566
|
+
function parseKeyValueOutput(stdout) {
|
|
17567
|
+
return Object.fromEntries(stdout.trim().split(`
|
|
17568
|
+
`).map((line) => line.split("=")).filter((parts) => parts.length === 2).map(([key, value]) => [key, value]));
|
|
17569
|
+
}
|
|
17570
|
+
function buildDoctorCommand() {
|
|
17571
|
+
return [
|
|
17572
|
+
'data_dir="${HASNA_MACHINES_DIR:-$HOME/.hasna/machines}"',
|
|
17573
|
+
'manifest_path="${HASNA_MACHINES_MANIFEST_PATH:-$data_dir/machines.json}"',
|
|
17574
|
+
'db_path="${HASNA_MACHINES_DB_PATH:-$data_dir/machines.db}"',
|
|
17575
|
+
'notifications_path="${HASNA_MACHINES_NOTIFICATIONS_PATH:-$data_dir/notifications.json}"',
|
|
17576
|
+
`printf 'manifest_path=%s\\n' "$manifest_path"`,
|
|
17577
|
+
`printf 'db_path=%s\\n' "$db_path"`,
|
|
17578
|
+
`printf 'notifications_path=%s\\n' "$notifications_path"`,
|
|
17579
|
+
`printf 'manifest_exists=%s\\n' "$(test -e "$manifest_path" && printf yes || printf no)"`,
|
|
17580
|
+
`printf 'db_exists=%s\\n' "$(test -e "$db_path" && printf yes || printf no)"`,
|
|
17581
|
+
`printf 'notifications_exists=%s\\n' "$(test -e "$notifications_path" && printf yes || printf no)"`,
|
|
17582
|
+
`printf 'bun=%s\\n' "$(bun --version 2>/dev/null || printf missing)"`,
|
|
17583
|
+
`printf 'ssh=%s\\n' "$(command -v ssh >/dev/null 2>&1 && printf ok || printf missing)"`,
|
|
17584
|
+
`printf 'machines=%s\\n' "$(command -v machines 2>/dev/null || printf missing)"`,
|
|
17585
|
+
`printf 'machines_agent=%s\\n' "$(command -v machines-agent 2>/dev/null || printf missing)"`,
|
|
17586
|
+
`printf 'machines_mcp=%s\\n' "$(command -v machines-mcp 2>/dev/null || printf missing)"`
|
|
17587
|
+
].join("; ");
|
|
17588
|
+
}
|
|
17589
|
+
function runDoctor(machineId = getLocalMachineId()) {
|
|
17590
|
+
const manifest = readManifest();
|
|
17591
|
+
const commandChecks = runMachineCommand(machineId, buildDoctorCommand());
|
|
17592
|
+
const details = parseKeyValueOutput(commandChecks.stdout);
|
|
17593
|
+
const machineInManifest = manifest.machines.find((machine) => machine.id === machineId);
|
|
17594
|
+
const checks = [
|
|
17595
|
+
makeCheck("manifest-entry", machineInManifest ? "ok" : "warn", machineInManifest ? "Machine exists in manifest" : "Machine missing from manifest", machineInManifest ? JSON.stringify(machineInManifest) : `No manifest entry for ${machineId}`),
|
|
17596
|
+
makeCheck("manifest-path", details["manifest_exists"] === "yes" ? "ok" : "warn", "Manifest path check", `${details["manifest_path"] || "unknown"} ${details["manifest_exists"] === "yes" ? "exists" : "missing"}`),
|
|
17597
|
+
makeCheck("db-path", details["db_exists"] === "yes" ? "ok" : "warn", "DB path check", `${details["db_path"] || "unknown"} ${details["db_exists"] === "yes" ? "exists" : "missing"}`),
|
|
17598
|
+
makeCheck("notifications-path", details["notifications_exists"] === "yes" ? "ok" : "warn", "Notifications path check", `${details["notifications_path"] || "unknown"} ${details["notifications_exists"] === "yes" ? "exists" : "missing"}`),
|
|
17599
|
+
makeCheck("bun", details["bun"] && details["bun"] !== "missing" ? "ok" : "fail", "Bun availability", details["bun"] || "missing"),
|
|
17600
|
+
makeCheck("machines-cli", details["machines"] && details["machines"] !== "missing" ? "ok" : "warn", "machines CLI availability", details["machines"] || "missing"),
|
|
17601
|
+
makeCheck("machines-agent-cli", details["machines_agent"] && details["machines_agent"] !== "missing" ? "ok" : "warn", "machines-agent availability", details["machines_agent"] || "missing"),
|
|
17602
|
+
makeCheck("machines-mcp-cli", details["machines_mcp"] && details["machines_mcp"] !== "missing" ? "ok" : "warn", "machines-mcp availability", details["machines_mcp"] || "missing"),
|
|
17603
|
+
makeCheck("ssh", details["ssh"] === "ok" ? "ok" : "warn", "SSH availability", details["ssh"] || "missing")
|
|
17604
|
+
];
|
|
17605
|
+
return {
|
|
17606
|
+
machineId,
|
|
17607
|
+
source: commandChecks.source,
|
|
17608
|
+
manifestPath: details["manifest_path"],
|
|
17609
|
+
dbPath: details["db_path"],
|
|
17610
|
+
notificationsPath: details["notifications_path"],
|
|
17611
|
+
checks
|
|
17612
|
+
};
|
|
17613
|
+
}
|
|
17614
|
+
|
|
17292
17615
|
// src/commands/serve.ts
|
|
17293
17616
|
function escapeHtml(value) {
|
|
17294
17617
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
@@ -17300,13 +17623,27 @@ function getServeInfo(options = {}) {
|
|
|
17300
17623
|
host,
|
|
17301
17624
|
port,
|
|
17302
17625
|
url: `http://${host}:${port}`,
|
|
17303
|
-
routes: [
|
|
17626
|
+
routes: [
|
|
17627
|
+
"/",
|
|
17628
|
+
"/health",
|
|
17629
|
+
"/api/status",
|
|
17630
|
+
"/api/manifest",
|
|
17631
|
+
"/api/notifications",
|
|
17632
|
+
"/api/doctor",
|
|
17633
|
+
"/api/self-test",
|
|
17634
|
+
"/api/apps/status",
|
|
17635
|
+
"/api/apps/diff",
|
|
17636
|
+
"/api/install-claude/status",
|
|
17637
|
+
"/api/install-claude/diff",
|
|
17638
|
+
"/api/notifications/test"
|
|
17639
|
+
]
|
|
17304
17640
|
};
|
|
17305
17641
|
}
|
|
17306
17642
|
function renderDashboardHtml() {
|
|
17307
17643
|
const status = getStatus();
|
|
17308
17644
|
const manifest = manifestList();
|
|
17309
17645
|
const notifications = listNotificationChannels();
|
|
17646
|
+
const doctor = runDoctor();
|
|
17310
17647
|
return `<!doctype html>
|
|
17311
17648
|
<html lang="en">
|
|
17312
17649
|
<head>
|
|
@@ -17322,12 +17659,14 @@ function renderDashboardHtml() {
|
|
|
17322
17659
|
.card { background: #121933; border: 1px solid #243057; border-radius: 16px; padding: 20px; }
|
|
17323
17660
|
.stat { font-size: 32px; font-weight: 700; margin-top: 8px; }
|
|
17324
17661
|
table { width: 100%; border-collapse: collapse; }
|
|
17325
|
-
th, td { text-align: left; padding: 10px 8px; border-bottom: 1px solid #243057; }
|
|
17662
|
+
th, td { text-align: left; padding: 10px 8px; border-bottom: 1px solid #243057; vertical-align: top; }
|
|
17326
17663
|
code { color: #9ed0ff; }
|
|
17327
17664
|
.badge { display: inline-block; border-radius: 999px; padding: 4px 10px; font-size: 12px; }
|
|
17328
|
-
.online { background: #12351f; color: #74f0a7; }
|
|
17329
|
-
.offline { background: #3b1a1a; color: #ff8c8c; }
|
|
17330
|
-
.unknown { background: #2f2b16; color: #ffd76a; }
|
|
17665
|
+
.online, .ok { background: #12351f; color: #74f0a7; }
|
|
17666
|
+
.offline, .fail { background: #3b1a1a; color: #ff8c8c; }
|
|
17667
|
+
.unknown, .warn { background: #2f2b16; color: #ffd76a; }
|
|
17668
|
+
ul { margin: 8px 0 0; padding-left: 18px; }
|
|
17669
|
+
.muted { color: #9fb0d9; }
|
|
17331
17670
|
</style>
|
|
17332
17671
|
</head>
|
|
17333
17672
|
<body>
|
|
@@ -17337,6 +17676,7 @@ function renderDashboardHtml() {
|
|
|
17337
17676
|
<section class="card"><div>Manifest machines</div><div class="stat">${status.manifestMachineCount}</div></section>
|
|
17338
17677
|
<section class="card"><div>Heartbeats</div><div class="stat">${status.heartbeatCount}</div></section>
|
|
17339
17678
|
<section class="card"><div>Notification channels</div><div class="stat">${notifications.channels.length}</div></section>
|
|
17679
|
+
<section class="card"><div>Doctor warnings</div><div class="stat">${doctor.checks.filter((entry) => entry.status !== "ok").length}</div></section>
|
|
17340
17680
|
</div>
|
|
17341
17681
|
|
|
17342
17682
|
<section class="card" style="margin-top:16px">
|
|
@@ -17354,6 +17694,25 @@ function renderDashboardHtml() {
|
|
|
17354
17694
|
</table>
|
|
17355
17695
|
</section>
|
|
17356
17696
|
|
|
17697
|
+
<section class="card" style="margin-top:16px">
|
|
17698
|
+
<h2>Doctor</h2>
|
|
17699
|
+
<table>
|
|
17700
|
+
<thead><tr><th>Check</th><th>Status</th><th>Detail</th></tr></thead>
|
|
17701
|
+
<tbody>
|
|
17702
|
+
${doctor.checks.map((entry) => `<tr>
|
|
17703
|
+
<td>${escapeHtml(entry.summary)}</td>
|
|
17704
|
+
<td><span class="badge ${escapeHtml(entry.status)}">${escapeHtml(entry.status)}</span></td>
|
|
17705
|
+
<td class="muted">${escapeHtml(entry.detail)}</td>
|
|
17706
|
+
</tr>`).join("")}
|
|
17707
|
+
</tbody>
|
|
17708
|
+
</table>
|
|
17709
|
+
</section>
|
|
17710
|
+
|
|
17711
|
+
<section class="card" style="margin-top:16px">
|
|
17712
|
+
<h2>Self Test</h2>
|
|
17713
|
+
<p class="muted">Use <code>/api/self-test</code> for the full smoke-check payload.</p>
|
|
17714
|
+
</section>
|
|
17715
|
+
|
|
17357
17716
|
<section class="card" style="margin-top:16px">
|
|
17358
17717
|
<h2>Manifest</h2>
|
|
17359
17718
|
<pre>${escapeHtml(JSON.stringify(manifest, null, 2))}</pre>
|
|
@@ -17362,13 +17721,25 @@ function renderDashboardHtml() {
|
|
|
17362
17721
|
</body>
|
|
17363
17722
|
</html>`;
|
|
17364
17723
|
}
|
|
17724
|
+
async function parseJsonBody(request) {
|
|
17725
|
+
try {
|
|
17726
|
+
return await request.json();
|
|
17727
|
+
} catch {
|
|
17728
|
+
return {};
|
|
17729
|
+
}
|
|
17730
|
+
}
|
|
17731
|
+
function jsonError(message, status = 400) {
|
|
17732
|
+
return Response.json({ error: message }, { status });
|
|
17733
|
+
}
|
|
17365
17734
|
function startDashboardServer(options = {}) {
|
|
17366
17735
|
const info = getServeInfo(options);
|
|
17367
17736
|
return Bun.serve({
|
|
17368
17737
|
hostname: info.host,
|
|
17369
17738
|
port: info.port,
|
|
17370
|
-
fetch(request) {
|
|
17739
|
+
async fetch(request) {
|
|
17371
17740
|
const url = new URL(request.url);
|
|
17741
|
+
const machineId = url.searchParams.get("machine") || undefined;
|
|
17742
|
+
const tools = url.searchParams.get("tools")?.split(",").map((value) => value.trim()).filter(Boolean);
|
|
17372
17743
|
if (url.pathname === "/health") {
|
|
17373
17744
|
return Response.json({ ok: true, ...getServeInfo(options) });
|
|
17374
17745
|
}
|
|
@@ -17381,6 +17752,43 @@ function startDashboardServer(options = {}) {
|
|
|
17381
17752
|
if (url.pathname === "/api/notifications") {
|
|
17382
17753
|
return Response.json(listNotificationChannels());
|
|
17383
17754
|
}
|
|
17755
|
+
if (url.pathname === "/api/doctor") {
|
|
17756
|
+
return Response.json(runDoctor(machineId));
|
|
17757
|
+
}
|
|
17758
|
+
if (url.pathname === "/api/self-test") {
|
|
17759
|
+
return Response.json(runSelfTest());
|
|
17760
|
+
}
|
|
17761
|
+
if (url.pathname === "/api/apps/status") {
|
|
17762
|
+
return Response.json(getAppsStatus(machineId));
|
|
17763
|
+
}
|
|
17764
|
+
if (url.pathname === "/api/apps/diff") {
|
|
17765
|
+
return Response.json(diffApps(machineId));
|
|
17766
|
+
}
|
|
17767
|
+
if (url.pathname === "/api/install-claude/status") {
|
|
17768
|
+
return Response.json(getClaudeCliStatus(machineId, tools));
|
|
17769
|
+
}
|
|
17770
|
+
if (url.pathname === "/api/install-claude/diff") {
|
|
17771
|
+
return Response.json(diffClaudeCli(machineId, tools));
|
|
17772
|
+
}
|
|
17773
|
+
if (url.pathname === "/api/notifications/test") {
|
|
17774
|
+
if (request.method !== "POST") {
|
|
17775
|
+
return jsonError("Use POST for notification tests.", 405);
|
|
17776
|
+
}
|
|
17777
|
+
const body = await parseJsonBody(request);
|
|
17778
|
+
const channelId = typeof body["channelId"] === "string" ? body["channelId"] : undefined;
|
|
17779
|
+
if (!channelId) {
|
|
17780
|
+
return jsonError("channelId is required.");
|
|
17781
|
+
}
|
|
17782
|
+
const event = typeof body["event"] === "string" ? body["event"] : undefined;
|
|
17783
|
+
const message = typeof body["message"] === "string" ? body["message"] : undefined;
|
|
17784
|
+
const apply = body["apply"] === true;
|
|
17785
|
+
const yes = body["yes"] === true;
|
|
17786
|
+
try {
|
|
17787
|
+
return Response.json(await testNotificationChannel(channelId, event, message, { apply, yes }));
|
|
17788
|
+
} catch (error) {
|
|
17789
|
+
return jsonError(error instanceof Error ? error.message : String(error));
|
|
17790
|
+
}
|
|
17791
|
+
}
|
|
17384
17792
|
return new Response(renderDashboardHtml(), {
|
|
17385
17793
|
headers: {
|
|
17386
17794
|
"content-type": "text/html; charset=utf-8"
|
|
@@ -17390,11 +17798,178 @@ function startDashboardServer(options = {}) {
|
|
|
17390
17798
|
});
|
|
17391
17799
|
}
|
|
17392
17800
|
|
|
17801
|
+
// src/commands/self-test.ts
|
|
17802
|
+
function check(id, status, summary, detail) {
|
|
17803
|
+
return { id, status, summary, detail };
|
|
17804
|
+
}
|
|
17805
|
+
function runSelfTest() {
|
|
17806
|
+
const version = getPackageVersion();
|
|
17807
|
+
const status = getStatus();
|
|
17808
|
+
const doctor = runDoctor();
|
|
17809
|
+
const serveInfo = getServeInfo();
|
|
17810
|
+
const html = renderDashboardHtml();
|
|
17811
|
+
const notifications = listNotificationChannels();
|
|
17812
|
+
const apps = listApps(status.machineId);
|
|
17813
|
+
const appsDiff = diffApps(status.machineId);
|
|
17814
|
+
const cliPlan = buildClaudeInstallPlan(status.machineId);
|
|
17815
|
+
return {
|
|
17816
|
+
machineId: getLocalMachineId(),
|
|
17817
|
+
checks: [
|
|
17818
|
+
check("package-version", version === "0.0.0" ? "fail" : "ok", "Package version resolves", version),
|
|
17819
|
+
check("status", "ok", "Status loads", JSON.stringify({ machines: status.manifestMachineCount, heartbeats: status.heartbeatCount })),
|
|
17820
|
+
check("doctor", doctor.checks.some((entry) => entry.status === "fail") ? "warn" : "ok", "Doctor completed", `${doctor.checks.length} checks`),
|
|
17821
|
+
check("serve-info", "ok", "Dashboard info renders", `${serveInfo.url} routes=${serveInfo.routes.length}`),
|
|
17822
|
+
check("dashboard-html", html.includes("Machines Dashboard") ? "ok" : "fail", "Dashboard HTML renders", html.slice(0, 80)),
|
|
17823
|
+
check("notifications", "ok", "Notifications config loads", `${notifications.channels.length} channels`),
|
|
17824
|
+
check("apps", "ok", "Apps manifest loads", `${apps.apps.length} apps`),
|
|
17825
|
+
check("apps-diff", appsDiff.missing.length === 0 ? "ok" : "warn", "Apps diff computed", `missing=${appsDiff.missing.length} installed=${appsDiff.installed.length}`),
|
|
17826
|
+
check("install-claude-plan", cliPlan.steps.length > 0 ? "ok" : "warn", "Install plan renders", `${cliPlan.steps.length} steps`)
|
|
17827
|
+
]
|
|
17828
|
+
};
|
|
17829
|
+
}
|
|
17830
|
+
|
|
17831
|
+
// src/cli-utils.ts
|
|
17832
|
+
function parseIntegerOption(value, label, constraints = {}) {
|
|
17833
|
+
const parsed = Number.parseInt(value, 10);
|
|
17834
|
+
const { min, max } = constraints;
|
|
17835
|
+
if (!Number.isFinite(parsed) || !/^-?\d+$/.test(value.trim())) {
|
|
17836
|
+
throw new Error(`Invalid value for ${label}: ${value}`);
|
|
17837
|
+
}
|
|
17838
|
+
if (min !== undefined && parsed < min) {
|
|
17839
|
+
throw new Error(`Invalid value for ${label}: ${value}. Expected >= ${min}.`);
|
|
17840
|
+
}
|
|
17841
|
+
if (max !== undefined && parsed > max) {
|
|
17842
|
+
throw new Error(`Invalid value for ${label}: ${value}. Expected <= ${max}.`);
|
|
17843
|
+
}
|
|
17844
|
+
return parsed;
|
|
17845
|
+
}
|
|
17846
|
+
function renderKeyValueTable(entries) {
|
|
17847
|
+
const width = entries.reduce((max, [key]) => Math.max(max, key.length), 0);
|
|
17848
|
+
return entries.map(([key, value]) => `${key.padEnd(width)} ${value}`).join(`
|
|
17849
|
+
`);
|
|
17850
|
+
}
|
|
17851
|
+
function renderList(title, items) {
|
|
17852
|
+
if (items.length === 0) {
|
|
17853
|
+
return `${title}: none`;
|
|
17854
|
+
}
|
|
17855
|
+
return `${title}:
|
|
17856
|
+
${items.map((item) => `- ${item}`).join(`
|
|
17857
|
+
`)}`;
|
|
17858
|
+
}
|
|
17859
|
+
|
|
17393
17860
|
// src/cli/index.ts
|
|
17394
17861
|
var program2 = new Command;
|
|
17862
|
+
function printJsonOrText(data, text, json = false) {
|
|
17863
|
+
if (json) {
|
|
17864
|
+
console.log(JSON.stringify(data, null, 2));
|
|
17865
|
+
return;
|
|
17866
|
+
}
|
|
17867
|
+
console.log(text);
|
|
17868
|
+
}
|
|
17869
|
+
function renderAppsListResult(result) {
|
|
17870
|
+
return [
|
|
17871
|
+
`machine: ${result.machineId}`,
|
|
17872
|
+
renderList("apps", result.apps.map((app) => `${app.name}${app.manager ? ` (${app.manager})` : ""}`))
|
|
17873
|
+
].join(`
|
|
17874
|
+
`);
|
|
17875
|
+
}
|
|
17876
|
+
function renderAppsStatusResult(result) {
|
|
17877
|
+
const lines = result.apps.map((app) => {
|
|
17878
|
+
const state = app.installed ? source_default.green("installed") : source_default.yellow("missing");
|
|
17879
|
+
return `${app.name.padEnd(18)} ${state} ${app.version ? `v${app.version}` : ""}`.trimEnd();
|
|
17880
|
+
});
|
|
17881
|
+
return [`machine: ${result.machineId} (${result.source})`, ...lines].join(`
|
|
17882
|
+
`);
|
|
17883
|
+
}
|
|
17884
|
+
function renderAppsDiffResult(result) {
|
|
17885
|
+
return [
|
|
17886
|
+
`machine: ${result.machineId} (${result.source})`,
|
|
17887
|
+
renderList("missing", result.missing),
|
|
17888
|
+
renderList("installed", result.installed)
|
|
17889
|
+
].join(`
|
|
17890
|
+
`);
|
|
17891
|
+
}
|
|
17892
|
+
function renderClaudeStatusResult(result) {
|
|
17893
|
+
const lines = result.tools.map((tool) => {
|
|
17894
|
+
const state = tool.installed ? source_default.green("installed") : source_default.yellow("missing");
|
|
17895
|
+
return `${tool.tool.padEnd(8)} ${state} ${tool.version || ""}`.trimEnd();
|
|
17896
|
+
});
|
|
17897
|
+
return [`machine: ${result.machineId} (${result.source})`, ...lines].join(`
|
|
17898
|
+
`);
|
|
17899
|
+
}
|
|
17900
|
+
function renderClaudeDiffResult(result) {
|
|
17901
|
+
return [
|
|
17902
|
+
`machine: ${result.machineId} (${result.source})`,
|
|
17903
|
+
renderList("missing", result.missing),
|
|
17904
|
+
renderList("installed", result.installed)
|
|
17905
|
+
].join(`
|
|
17906
|
+
`);
|
|
17907
|
+
}
|
|
17908
|
+
function renderNotificationConfigResult(config) {
|
|
17909
|
+
if (config.channels.length === 0) {
|
|
17910
|
+
return "notification channels: none";
|
|
17911
|
+
}
|
|
17912
|
+
return config.channels.map((channel) => `${channel.id} ${channel.enabled ? source_default.green("enabled") : source_default.yellow("disabled")} ${channel.type} -> ${channel.target}`).join(`
|
|
17913
|
+
`);
|
|
17914
|
+
}
|
|
17915
|
+
function renderNotificationTestResult(result) {
|
|
17916
|
+
return renderKeyValueTable([
|
|
17917
|
+
["channel", result.channelId],
|
|
17918
|
+
["mode", result.mode],
|
|
17919
|
+
["delivered", String(result.delivered)],
|
|
17920
|
+
["detail", result.detail],
|
|
17921
|
+
["preview", result.preview]
|
|
17922
|
+
]);
|
|
17923
|
+
}
|
|
17924
|
+
function renderNotificationDispatchResult(result) {
|
|
17925
|
+
return [
|
|
17926
|
+
`event: ${result.event}`,
|
|
17927
|
+
`message: ${result.message}`,
|
|
17928
|
+
...result.deliveries.map((delivery) => `${delivery.channelId} ${delivery.delivered ? source_default.green("delivered") : source_default.red("failed")} ${delivery.transport} ${delivery.detail}`)
|
|
17929
|
+
].join(`
|
|
17930
|
+
`);
|
|
17931
|
+
}
|
|
17932
|
+
function renderDoctorResult(report) {
|
|
17933
|
+
const header = `machine: ${report.machineId} (${report.source})`;
|
|
17934
|
+
const lines = report.checks.map((check2) => {
|
|
17935
|
+
const status = check2.status === "ok" ? source_default.green(check2.status) : check2.status === "warn" ? source_default.yellow(check2.status) : source_default.red(check2.status);
|
|
17936
|
+
return `${check2.id.padEnd(20)} ${status} ${check2.detail}`;
|
|
17937
|
+
});
|
|
17938
|
+
return [header, ...lines].join(`
|
|
17939
|
+
`);
|
|
17940
|
+
}
|
|
17941
|
+
function renderSelfTestResult(result) {
|
|
17942
|
+
return [
|
|
17943
|
+
`machine: ${result.machineId}`,
|
|
17944
|
+
...result.checks.map((check2) => {
|
|
17945
|
+
const status = check2.status === "ok" ? source_default.green(check2.status) : check2.status === "warn" ? source_default.yellow(check2.status) : source_default.red(check2.status);
|
|
17946
|
+
return `${check2.id.padEnd(20)} ${status} ${check2.detail}`;
|
|
17947
|
+
})
|
|
17948
|
+
].join(`
|
|
17949
|
+
`);
|
|
17950
|
+
}
|
|
17951
|
+
function renderFleetStatus(status) {
|
|
17952
|
+
return [
|
|
17953
|
+
renderKeyValueTable([
|
|
17954
|
+
["machine", status.machineId],
|
|
17955
|
+
["manifest", status.manifestPath],
|
|
17956
|
+
["db", status.dbPath],
|
|
17957
|
+
["notifications", status.notificationsPath],
|
|
17958
|
+
["manifest machines", String(status.manifestMachineCount)],
|
|
17959
|
+
["heartbeats", String(status.heartbeatCount)],
|
|
17960
|
+
["setup runs", String(status.recentSetupRuns)],
|
|
17961
|
+
["sync runs", String(status.recentSyncRuns)]
|
|
17962
|
+
]),
|
|
17963
|
+
"",
|
|
17964
|
+
...status.machines.map((machine) => `${machine.machineId.padEnd(18)} ${machine.platform || "unknown"} ${machine.heartbeatStatus} ${machine.lastHeartbeatAt || "\u2014"}`)
|
|
17965
|
+
].join(`
|
|
17966
|
+
`);
|
|
17967
|
+
}
|
|
17395
17968
|
program2.name("machines").description("Machine fleet management CLI + MCP for developers").version(getPackageVersion());
|
|
17396
17969
|
var manifestCommand = program2.command("manifest").description("Manage the fleet manifest");
|
|
17397
17970
|
var appsCommand = program2.command("apps").description("Manage installed applications per machine");
|
|
17971
|
+
var notificationsCommand = program2.command("notifications").description("Manage fleet alert delivery channels");
|
|
17972
|
+
var installClaudeCommand = program2.command("install-claude").description("Install or inspect Claude, Codex, and Gemini CLIs");
|
|
17398
17973
|
manifestCommand.command("init").description("Create an empty fleet manifest").action(() => {
|
|
17399
17974
|
console.log(manifestInit());
|
|
17400
17975
|
});
|
|
@@ -17427,11 +18002,7 @@ manifestCommand.command("add").description("Add or replace a machine in the flee
|
|
|
17427
18002
|
const files = Array.isArray(options["file"]) ? options["file"].map((value) => {
|
|
17428
18003
|
const [source, target, mode] = String(value).split(":");
|
|
17429
18004
|
const normalizedMode = mode === "symlink" ? "symlink" : mode === "copy" ? "copy" : undefined;
|
|
17430
|
-
return {
|
|
17431
|
-
source,
|
|
17432
|
-
target,
|
|
17433
|
-
mode: normalizedMode
|
|
17434
|
-
};
|
|
18005
|
+
return { source, target, mode: normalizedMode };
|
|
17435
18006
|
}) : undefined;
|
|
17436
18007
|
const apps = Array.isArray(options["app"]) ? options["app"].map((value) => {
|
|
17437
18008
|
const [name, manager, packageName] = String(value).split(":");
|
|
@@ -17459,65 +18030,55 @@ manifestCommand.command("add").description("Add or replace a machine in the flee
|
|
|
17459
18030
|
});
|
|
17460
18031
|
appsCommand.command("list").description("List manifest-managed apps for a machine").option("--machine <id>", "Machine identifier").option("-j, --json", "Print JSON output", false).action((options) => {
|
|
17461
18032
|
const result = listApps(options.machine);
|
|
17462
|
-
|
|
17463
|
-
|
|
18033
|
+
printJsonOrText(result, renderAppsListResult(result), options.json);
|
|
18034
|
+
});
|
|
18035
|
+
appsCommand.command("status").description("Check installed state for manifest-managed apps").option("--machine <id>", "Machine identifier").option("-j, --json", "Print JSON output", false).action((options) => {
|
|
18036
|
+
const result = getAppsStatus(options.machine);
|
|
18037
|
+
printJsonOrText(result, renderAppsStatusResult(result), options.json);
|
|
18038
|
+
});
|
|
18039
|
+
appsCommand.command("diff").description("Show missing and installed manifest-managed apps").option("--machine <id>", "Machine identifier").option("-j, --json", "Print JSON output", false).action((options) => {
|
|
18040
|
+
const result = diffApps(options.machine);
|
|
18041
|
+
printJsonOrText(result, renderAppsDiffResult(result), options.json);
|
|
17464
18042
|
});
|
|
17465
18043
|
appsCommand.command("plan").description("Preview app install steps for a machine").option("--machine <id>", "Machine identifier").option("-j, --json", "Print JSON output", false).action((options) => {
|
|
17466
18044
|
const result = buildAppsPlan(options.machine);
|
|
17467
|
-
|
|
17468
|
-
console.log(options.json ? rendered : rendered);
|
|
18045
|
+
console.log(options.json ? JSON.stringify(result, null, 2) : JSON.stringify(result, null, 2));
|
|
17469
18046
|
});
|
|
17470
18047
|
appsCommand.command("apply").description("Install manifest-managed apps for a machine").option("--machine <id>", "Machine identifier").option("--yes", "Confirm execution", false).option("-j, --json", "Print JSON output", false).action((options) => {
|
|
17471
18048
|
const result = runAppsInstall(options.machine, { apply: true, yes: options.yes });
|
|
17472
|
-
|
|
17473
|
-
console.log(options.json ? rendered : rendered);
|
|
18049
|
+
console.log(options.json ? JSON.stringify(result, null, 2) : JSON.stringify(result, null, 2));
|
|
17474
18050
|
});
|
|
17475
18051
|
program2.command("setup").description("Prepare a machine from the fleet manifest").option("--machine <id>", "Machine identifier").option("--apply", "Execute provisioning commands instead of previewing the plan", false).option("--yes", "Confirm execution when using --apply", false).option("-j, --json", "Print JSON output", false).action((options) => {
|
|
17476
18052
|
const result = options.apply ? runSetup(options.machine, { apply: true, yes: options.yes }) : buildSetupPlan(options.machine);
|
|
17477
|
-
if (options.json) {
|
|
17478
|
-
console.log(JSON.stringify(result, null, 2));
|
|
17479
|
-
return;
|
|
17480
|
-
}
|
|
17481
18053
|
console.log(JSON.stringify(result, null, 2));
|
|
17482
18054
|
});
|
|
17483
18055
|
program2.command("sync").description("Reconcile a machine against the fleet manifest").option("--machine <id>", "Machine identifier").option("--apply", "Execute reconciliation commands instead of previewing the plan", false).option("--yes", "Confirm execution when using --apply", false).option("-j, --json", "Print JSON output", false).action((options) => {
|
|
17484
18056
|
const result = options.apply ? runSync(options.machine, { apply: true, yes: options.yes }) : buildSyncPlan(options.machine);
|
|
17485
|
-
if (options.json) {
|
|
17486
|
-
console.log(JSON.stringify(result, null, 2));
|
|
17487
|
-
return;
|
|
17488
|
-
}
|
|
17489
18057
|
console.log(JSON.stringify(result, null, 2));
|
|
17490
18058
|
});
|
|
17491
18059
|
program2.command("diff").description("Show manifest differences between two machines").requiredOption("--left <id>", "Left machine identifier").option("--right <id>", "Right machine identifier (defaults to current machine)").option("-j, --json", "Print JSON output", false).action((options) => {
|
|
17492
18060
|
const result = diffMachines(options.left, options.right);
|
|
17493
|
-
if (options.json) {
|
|
17494
|
-
console.log(JSON.stringify(result, null, 2));
|
|
17495
|
-
return;
|
|
17496
|
-
}
|
|
17497
18061
|
console.log(JSON.stringify(result, null, 2));
|
|
17498
18062
|
});
|
|
17499
18063
|
program2.command("backup").description("Create and optionally upload a machine backup archive").requiredOption("--bucket <name>", "S3 bucket name").option("--prefix <prefix>", "S3 key prefix", "machines").option("--apply", "Execute backup commands instead of previewing the plan", false).option("--yes", "Confirm execution when using --apply", false).option("-j, --json", "Print JSON output", false).action((options) => {
|
|
17500
18064
|
const result = options.apply ? runBackup(options.bucket, options.prefix, { apply: true, yes: options.yes }) : buildBackupPlan(options.bucket, options.prefix);
|
|
17501
|
-
|
|
17502
|
-
console.log(options.json ? rendered : rendered);
|
|
18065
|
+
console.log(JSON.stringify(result, null, 2));
|
|
17503
18066
|
});
|
|
17504
18067
|
var certCommand = program2.command("cert").description("Manage mkcert-based local SSL certificates");
|
|
17505
18068
|
certCommand.command("issue").description("Plan or issue certificates for one or more domains").argument("<domains...>", "Domains to include in the certificate").option("--apply", "Execute certificate commands instead of previewing them", false).option("--yes", "Confirm execution when using --apply", false).option("-j, --json", "Print JSON output", false).action((domains, options) => {
|
|
17506
18069
|
const result = options.apply ? runCertPlan(domains, { apply: true, yes: options.yes }) : buildCertPlan(domains);
|
|
17507
|
-
|
|
17508
|
-
console.log(options.json ? rendered : rendered);
|
|
18070
|
+
console.log(JSON.stringify(result, null, 2));
|
|
17509
18071
|
});
|
|
17510
18072
|
var dnsCommand = program2.command("dns").description("Manage local domain mappings");
|
|
17511
|
-
var notificationsCommand = program2.command("notifications").description("Manage fleet alert delivery channels");
|
|
17512
18073
|
dnsCommand.command("add").description("Add or replace a local domain mapping").requiredOption("--domain <domain>", "Domain name").requiredOption("--port <port>", "Target port").option("--target-host <host>", "Target host", "127.0.0.1").option("-j, --json", "Print JSON output", false).action((options) => {
|
|
17513
|
-
const result = addDomainMapping(options.domain,
|
|
17514
|
-
|
|
17515
|
-
|
|
18074
|
+
const result = addDomainMapping(options.domain, parseIntegerOption(options.port, "port", { min: 1, max: 65535 }), options.targetHost);
|
|
18075
|
+
console.log(JSON.stringify(result, null, 2));
|
|
18076
|
+
});
|
|
18077
|
+
dnsCommand.command("list").description("List saved local domain mappings").option("-j, --json", "Print JSON output", false).action(() => {
|
|
18078
|
+
console.log(JSON.stringify(listDomainMappings(), null, 2));
|
|
17516
18079
|
});
|
|
17517
|
-
dnsCommand.command("
|
|
17518
|
-
|
|
17519
|
-
const rendered = JSON.stringify(result, null, 2);
|
|
17520
|
-
console.log(options.json ? rendered : rendered);
|
|
18080
|
+
dnsCommand.command("render").description("Render hosts/proxy configuration for a domain").argument("<domain>", "Domain name").option("-j, --json", "Print JSON output", false).action((domain) => {
|
|
18081
|
+
console.log(JSON.stringify(renderDomainMapping(domain), null, 2));
|
|
17521
18082
|
});
|
|
17522
18083
|
notificationsCommand.command("add").description("Add or replace a notification channel").requiredOption("--id <id>", "Channel identifier").requiredOption("--type <type>", "email | webhook | command").requiredOption("--target <target>", "Email, webhook URL, or shell command").option("--event <event...>", "Events routed to this channel", ["setup_failed", "sync_failed"]).option("--disabled", "Create the channel in disabled state", false).option("-j, --json", "Print JSON output", false).action((options) => {
|
|
17523
18084
|
const result = addNotificationChannel({
|
|
@@ -17527,41 +18088,50 @@ notificationsCommand.command("add").description("Add or replace a notification c
|
|
|
17527
18088
|
events: options.event,
|
|
17528
18089
|
enabled: !options.disabled
|
|
17529
18090
|
});
|
|
17530
|
-
|
|
17531
|
-
console.log(options.json ? rendered : rendered);
|
|
18091
|
+
printJsonOrText(result, renderNotificationConfigResult(result), options.json);
|
|
17532
18092
|
});
|
|
17533
18093
|
notificationsCommand.command("list").description("List configured notification channels").option("-j, --json", "Print JSON output", false).action((options) => {
|
|
17534
18094
|
const result = listNotificationChannels();
|
|
17535
|
-
|
|
17536
|
-
console.log(options.json ? rendered : rendered);
|
|
18095
|
+
printJsonOrText(result, renderNotificationConfigResult(result), options.json);
|
|
17537
18096
|
});
|
|
17538
|
-
notificationsCommand.command("test").description("Preview or execute a notification test").requiredOption("--channel <id>", "Channel identifier").option("--event <name>", "Event name", "manual.test").option("--message <message>", "Test message", "machines notification test").option("--apply", "Execute the notification test instead of previewing it", false).option("--yes", "Confirm execution when using --apply", false).option("-j, --json", "Print JSON output", false).action((options) => {
|
|
17539
|
-
const result = testNotificationChannel(options.channel, options.event, options.message, {
|
|
18097
|
+
notificationsCommand.command("test").description("Preview or execute a notification test").requiredOption("--channel <id>", "Channel identifier").option("--event <name>", "Event name", "manual.test").option("--message <message>", "Test message", "machines notification test").option("--apply", "Execute the notification test instead of previewing it", false).option("--yes", "Confirm execution when using --apply", false).option("-j, --json", "Print JSON output", false).action(async (options) => {
|
|
18098
|
+
const result = await testNotificationChannel(options.channel, options.event, options.message, {
|
|
17540
18099
|
apply: options.apply,
|
|
17541
18100
|
yes: options.yes
|
|
17542
18101
|
});
|
|
17543
|
-
|
|
17544
|
-
|
|
18102
|
+
printJsonOrText(result, renderNotificationTestResult(result), options.json);
|
|
18103
|
+
});
|
|
18104
|
+
notificationsCommand.command("dispatch").description("Dispatch an event to matching notification channels").requiredOption("--event <name>", "Event name").requiredOption("--message <message>", "Message body").option("--channel <id>", "Limit delivery to one channel").option("-j, --json", "Print JSON output", false).action(async (options) => {
|
|
18105
|
+
const result = await dispatchNotificationEvent(options.event, options.message, { channelId: options.channel });
|
|
18106
|
+
printJsonOrText(result, renderNotificationDispatchResult(result), options.json);
|
|
17545
18107
|
});
|
|
17546
18108
|
notificationsCommand.command("remove").description("Remove a notification channel").argument("<id>", "Channel identifier").option("-j, --json", "Print JSON output", false).action((id, options) => {
|
|
17547
18109
|
const result = removeNotificationChannel(id);
|
|
17548
|
-
|
|
17549
|
-
|
|
18110
|
+
printJsonOrText(result, renderNotificationConfigResult(result), options.json);
|
|
18111
|
+
});
|
|
18112
|
+
installClaudeCommand.command("status").description("Check installed state for Claude, Codex, and Gemini CLIs").option("--machine <id>", "Machine identifier").option("--tool <name...>", "CLI tools to inspect (claude, codex, gemini)").option("-j, --json", "Print JSON output", false).action((options) => {
|
|
18113
|
+
const result = getClaudeCliStatus(options.machine, options.tool);
|
|
18114
|
+
printJsonOrText(result, renderClaudeStatusResult(result), options.json);
|
|
18115
|
+
});
|
|
18116
|
+
installClaudeCommand.command("diff").description("Show missing and installed Claude, Codex, and Gemini CLIs").option("--machine <id>", "Machine identifier").option("--tool <name...>", "CLI tools to inspect (claude, codex, gemini)").option("-j, --json", "Print JSON output", false).action((options) => {
|
|
18117
|
+
const result = diffClaudeCli(options.machine, options.tool);
|
|
18118
|
+
printJsonOrText(result, renderClaudeDiffResult(result), options.json);
|
|
17550
18119
|
});
|
|
17551
|
-
|
|
17552
|
-
const result =
|
|
17553
|
-
|
|
17554
|
-
|
|
18120
|
+
installClaudeCommand.command("plan").description("Preview CLI install steps").option("--machine <id>", "Machine identifier").option("--tool <name...>", "CLI tools to install (claude, codex, gemini)").option("-j, --json", "Print JSON output", false).action((options) => {
|
|
18121
|
+
const result = buildClaudeInstallPlan(options.machine, options.tool);
|
|
18122
|
+
console.log(JSON.stringify(result, null, 2));
|
|
18123
|
+
});
|
|
18124
|
+
installClaudeCommand.command("apply").description("Install or update the requested CLIs").option("--machine <id>", "Machine identifier").option("--tool <name...>", "CLI tools to install (claude, codex, gemini)").option("--yes", "Confirm execution when using apply", false).option("-j, --json", "Print JSON output", false).action((options) => {
|
|
18125
|
+
const result = runClaudeInstall(options.machine, options.tool, { apply: true, yes: options.yes });
|
|
18126
|
+
console.log(JSON.stringify(result, null, 2));
|
|
17555
18127
|
});
|
|
17556
|
-
|
|
18128
|
+
installClaudeCommand.option("--machine <id>", "Machine identifier").option("--tool <name...>", "CLI tools to install (claude, codex, gemini)").option("--apply", "Execute installation commands instead of previewing the plan", false).option("--yes", "Confirm execution when using --apply", false).option("-j, --json", "Print JSON output", false).action((options) => {
|
|
17557
18129
|
const result = options.apply ? runClaudeInstall(options.machine, options.tool, { apply: true, yes: options.yes }) : buildClaudeInstallPlan(options.machine, options.tool);
|
|
17558
|
-
|
|
17559
|
-
console.log(options.json ? rendered : rendered);
|
|
18130
|
+
console.log(JSON.stringify(result, null, 2));
|
|
17560
18131
|
});
|
|
17561
18132
|
program2.command("install-tailscale").description("Install Tailscale on a machine").option("--machine <id>", "Machine identifier").option("--apply", "Execute installation commands instead of previewing the plan", false).option("--yes", "Confirm execution when using --apply", false).option("-j, --json", "Print JSON output", false).action((options) => {
|
|
17562
18133
|
const result = options.apply ? runTailscaleInstall(options.machine, { apply: true, yes: options.yes }) : buildTailscaleInstallPlan(options.machine);
|
|
17563
|
-
|
|
17564
|
-
console.log(options.json ? rendered : rendered);
|
|
18134
|
+
console.log(JSON.stringify(result, null, 2));
|
|
17565
18135
|
});
|
|
17566
18136
|
program2.command("ssh").description("Choose the best SSH route for a machine").requiredOption("--machine <id>", "Machine identifier").option("--cmd <command>", "Remote command to run").option("-j, --json", "Print JSON output", false).action((options) => {
|
|
17567
18137
|
if (options.json) {
|
|
@@ -17572,27 +18142,27 @@ program2.command("ssh").description("Choose the best SSH route for a machine").r
|
|
|
17572
18142
|
});
|
|
17573
18143
|
program2.command("ports").description("List listening ports on a machine").option("--machine <id>", "Machine identifier").option("-j, --json", "Print JSON output", false).action((options) => {
|
|
17574
18144
|
const result = listPorts(options.machine);
|
|
17575
|
-
|
|
17576
|
-
console.log(options.json ? rendered : rendered);
|
|
18145
|
+
console.log(JSON.stringify(result, null, 2));
|
|
17577
18146
|
});
|
|
17578
18147
|
program2.command("status").description("Print local machine and storage status").option("-j, --json", "Print JSON output", false).action((options) => {
|
|
17579
18148
|
const status = getStatus();
|
|
17580
|
-
|
|
17581
|
-
|
|
18149
|
+
printJsonOrText(status, renderFleetStatus(status), options.json);
|
|
18150
|
+
});
|
|
18151
|
+
program2.command("doctor").description("Run machine preflight checks").option("--machine <id>", "Machine identifier").option("-j, --json", "Print JSON output", false).action((options) => {
|
|
18152
|
+
const result = runDoctor(options.machine);
|
|
18153
|
+
printJsonOrText(result, renderDoctorResult(result), options.json);
|
|
18154
|
+
});
|
|
18155
|
+
program2.command("self-test").description("Run local package smoke checks").option("-j, --json", "Print JSON output", false).action((options) => {
|
|
18156
|
+
const result = runSelfTest();
|
|
18157
|
+
printJsonOrText(result, renderSelfTestResult(result), options.json);
|
|
17582
18158
|
});
|
|
17583
18159
|
program2.command("serve").description("Serve a local fleet dashboard and JSON API").option("--host <host>", "Host interface to bind", "0.0.0.0").option("--port <port>", "Port to bind", "7676").option("-j, --json", "Print serve config and exit", false).action((options) => {
|
|
17584
|
-
const info = getServeInfo({
|
|
17585
|
-
host: options.host,
|
|
17586
|
-
port: Number.parseInt(options.port, 10)
|
|
17587
|
-
});
|
|
18160
|
+
const info = getServeInfo({ host: options.host, port: parseIntegerOption(options.port, "port", { min: 1, max: 65535 }) });
|
|
17588
18161
|
if (options.json) {
|
|
17589
18162
|
console.log(JSON.stringify(info, null, 2));
|
|
17590
18163
|
return;
|
|
17591
18164
|
}
|
|
17592
|
-
const server = startDashboardServer({
|
|
17593
|
-
host: info.host,
|
|
17594
|
-
port: info.port
|
|
17595
|
-
});
|
|
18165
|
+
const server = startDashboardServer({ host: info.host, port: info.port });
|
|
17596
18166
|
console.log(source_default.green(`machines dashboard listening on http://${server.hostname}:${server.port}`));
|
|
17597
18167
|
});
|
|
17598
18168
|
await program2.parseAsync(process.argv);
|