@hasna/machines 0.0.1 → 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/agent/index.js +9 -5
- package/dist/cli/index.js +732 -158
- 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 -311
- 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/dist/version.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -2586,13 +2586,17 @@ var chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
|
|
|
2586
2586
|
var source_default = chalk;
|
|
2587
2587
|
|
|
2588
2588
|
// src/version.ts
|
|
2589
|
-
import { readFileSync } from "fs";
|
|
2589
|
+
import { existsSync, readFileSync } from "fs";
|
|
2590
2590
|
import { dirname, join } from "path";
|
|
2591
2591
|
import { fileURLToPath } from "url";
|
|
2592
2592
|
function getPackageVersion() {
|
|
2593
2593
|
try {
|
|
2594
2594
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
2595
|
-
const
|
|
2595
|
+
const candidates = [join(here, "..", "package.json"), join(here, "..", "..", "package.json")];
|
|
2596
|
+
const pkgPath = candidates.find((candidate) => existsSync(candidate));
|
|
2597
|
+
if (!pkgPath) {
|
|
2598
|
+
return "0.0.0";
|
|
2599
|
+
}
|
|
2596
2600
|
return JSON.parse(readFileSync(pkgPath, "utf8")).version || "0.0.0";
|
|
2597
2601
|
} catch {
|
|
2598
2602
|
return "0.0.0";
|
|
@@ -2600,7 +2604,7 @@ function getPackageVersion() {
|
|
|
2600
2604
|
}
|
|
2601
2605
|
|
|
2602
2606
|
// src/manifests.ts
|
|
2603
|
-
import { existsSync as
|
|
2607
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
2604
2608
|
import { arch, homedir, hostname, platform, userInfo } from "os";
|
|
2605
2609
|
import { dirname as dirname3 } from "path";
|
|
2606
2610
|
|
|
@@ -6578,7 +6582,7 @@ var coerce = {
|
|
|
6578
6582
|
};
|
|
6579
6583
|
var NEVER = INVALID;
|
|
6580
6584
|
// src/paths.ts
|
|
6581
|
-
import { existsSync, mkdirSync } from "fs";
|
|
6585
|
+
import { existsSync as existsSync2, mkdirSync } from "fs";
|
|
6582
6586
|
import { dirname as dirname2, join as join2, resolve } from "path";
|
|
6583
6587
|
function homeDir() {
|
|
6584
6588
|
return process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
@@ -6599,7 +6603,7 @@ function ensureParentDir(filePath) {
|
|
|
6599
6603
|
if (filePath === ":memory:")
|
|
6600
6604
|
return;
|
|
6601
6605
|
const dir = dirname2(resolve(filePath));
|
|
6602
|
-
if (!
|
|
6606
|
+
if (!existsSync2(dir)) {
|
|
6603
6607
|
mkdirSync(dir, { recursive: true });
|
|
6604
6608
|
}
|
|
6605
6609
|
}
|
|
@@ -6664,7 +6668,7 @@ function getDefaultManifest() {
|
|
|
6664
6668
|
};
|
|
6665
6669
|
}
|
|
6666
6670
|
function readManifest(path = getManifestPath()) {
|
|
6667
|
-
if (!
|
|
6671
|
+
if (!existsSync3(path)) {
|
|
6668
6672
|
return getDefaultManifest();
|
|
6669
6673
|
}
|
|
6670
6674
|
const raw = JSON.parse(readFileSync2(path, "utf8"));
|
|
@@ -6748,7 +6752,7 @@ import { hostname as hostname2 } from "os";
|
|
|
6748
6752
|
import { createRequire } from "module";
|
|
6749
6753
|
import { Database } from "bun:sqlite";
|
|
6750
6754
|
import {
|
|
6751
|
-
existsSync as
|
|
6755
|
+
existsSync as existsSync4,
|
|
6752
6756
|
mkdirSync as mkdirSync2,
|
|
6753
6757
|
readdirSync,
|
|
6754
6758
|
copyFileSync
|
|
@@ -16610,14 +16614,14 @@ function runCertPlan(domains, options = {}) {
|
|
|
16610
16614
|
}
|
|
16611
16615
|
|
|
16612
16616
|
// src/commands/dns.ts
|
|
16613
|
-
import { existsSync as
|
|
16617
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
16614
16618
|
import { join as join8 } from "path";
|
|
16615
16619
|
function getDnsPath() {
|
|
16616
16620
|
return join8(getDataDir(), "dns.json");
|
|
16617
16621
|
}
|
|
16618
16622
|
function readMappings() {
|
|
16619
16623
|
const path = getDnsPath();
|
|
16620
|
-
if (!
|
|
16624
|
+
if (!existsSync5(path))
|
|
16621
16625
|
return [];
|
|
16622
16626
|
return JSON.parse(readFileSync4(path, "utf8"));
|
|
16623
16627
|
}
|
|
@@ -16694,17 +16698,95 @@ function diffMachines(leftMachineId, rightMachineId) {
|
|
|
16694
16698
|
};
|
|
16695
16699
|
}
|
|
16696
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
|
+
|
|
16697
16766
|
// src/commands/apps.ts
|
|
16698
16767
|
function getPackageName(app) {
|
|
16699
16768
|
return app.packageName || app.name;
|
|
16700
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
|
+
}
|
|
16701
16782
|
function buildAppCommand(machine, app) {
|
|
16702
16783
|
const packageName = getPackageName(app);
|
|
16703
|
-
|
|
16784
|
+
const manager = getAppManager(machine, app);
|
|
16785
|
+
if (manager === "custom") {
|
|
16704
16786
|
return packageName;
|
|
16705
16787
|
}
|
|
16706
16788
|
if (machine.platform === "macos") {
|
|
16707
|
-
if (
|
|
16789
|
+
if (manager === "cask") {
|
|
16708
16790
|
return `brew install --cask ${packageName}`;
|
|
16709
16791
|
}
|
|
16710
16792
|
return `brew install ${packageName}`;
|
|
@@ -16714,18 +16796,48 @@ function buildAppCommand(machine, app) {
|
|
|
16714
16796
|
}
|
|
16715
16797
|
return `sudo apt-get install -y ${packageName}`;
|
|
16716
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
|
+
}
|
|
16717
16816
|
function buildAppSteps(machine) {
|
|
16718
16817
|
return (machine.apps || []).map((app) => ({
|
|
16719
16818
|
id: `app-${app.name}`,
|
|
16720
16819
|
title: `Install ${app.name} on ${machine.id}`,
|
|
16721
16820
|
command: buildAppCommand(machine, app),
|
|
16722
|
-
manager: app
|
|
16821
|
+
manager: getAppManager(machine, app) === "custom" ? "custom" : machine.platform === "macos" ? "brew" : machine.platform === "windows" ? "custom" : "apt",
|
|
16723
16822
|
privileged: machine.platform === "linux"
|
|
16724
16823
|
}));
|
|
16725
16824
|
}
|
|
16726
16825
|
function resolveMachine(machineId) {
|
|
16727
16826
|
return (machineId ? getManifestMachine(machineId) : null) || detectCurrentMachineManifest();
|
|
16728
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
|
+
}
|
|
16729
16841
|
function listApps(machineId) {
|
|
16730
16842
|
const machine = resolveMachine(machineId);
|
|
16731
16843
|
return {
|
|
@@ -16742,6 +16854,26 @@ function buildAppsPlan(machineId) {
|
|
|
16742
16854
|
executed: 0
|
|
16743
16855
|
};
|
|
16744
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
|
+
}
|
|
16745
16877
|
function runAppsInstall(machineId, options = {}) {
|
|
16746
16878
|
const plan = buildAppsPlan(machineId);
|
|
16747
16879
|
if (!options.apply)
|
|
@@ -16775,6 +16907,13 @@ var AI_CLI_PACKAGES = {
|
|
|
16775
16907
|
codex: "@openai/codex",
|
|
16776
16908
|
gemini: "@google/gemini-cli"
|
|
16777
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
|
+
}
|
|
16778
16917
|
function normalizeTools(tools) {
|
|
16779
16918
|
if (!tools || tools.length === 0) {
|
|
16780
16919
|
return ["claude", "codex", "gemini"];
|
|
@@ -16794,8 +16933,27 @@ function buildInstallSteps(machine, tools) {
|
|
|
16794
16933
|
manager: "bun"
|
|
16795
16934
|
}));
|
|
16796
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
|
+
}
|
|
16797
16955
|
function buildClaudeInstallPlan(machineId, tools) {
|
|
16798
|
-
const machine = (machineId
|
|
16956
|
+
const machine = resolveMachine2(machineId);
|
|
16799
16957
|
return {
|
|
16800
16958
|
machineId: machine.id,
|
|
16801
16959
|
mode: "plan",
|
|
@@ -16803,6 +16961,24 @@ function buildClaudeInstallPlan(machineId, tools) {
|
|
|
16803
16961
|
executed: 0
|
|
16804
16962
|
};
|
|
16805
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
|
+
}
|
|
16806
16982
|
function runClaudeInstall(machineId, tools, options = {}) {
|
|
16807
16983
|
const plan = buildClaudeInstallPlan(machineId, tools);
|
|
16808
16984
|
if (!options.apply)
|
|
@@ -16899,7 +17075,7 @@ function runTailscaleInstall(machineId, options = {}) {
|
|
|
16899
17075
|
}
|
|
16900
17076
|
|
|
16901
17077
|
// src/commands/notifications.ts
|
|
16902
|
-
import { existsSync as
|
|
17078
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
16903
17079
|
var notificationChannelSchema = exports_external.object({
|
|
16904
17080
|
id: exports_external.string(),
|
|
16905
17081
|
type: exports_external.enum(["email", "webhook", "command"]),
|
|
@@ -16915,6 +17091,138 @@ var notificationConfigSchema = exports_external.object({
|
|
|
16915
17091
|
function sortChannels(channels) {
|
|
16916
17092
|
return [...channels].sort((left, right) => left.id.localeCompare(right.id));
|
|
16917
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
|
+
}
|
|
16918
17226
|
function getDefaultNotificationConfig() {
|
|
16919
17227
|
return {
|
|
16920
17228
|
version: 1,
|
|
@@ -16923,7 +17231,7 @@ function getDefaultNotificationConfig() {
|
|
|
16923
17231
|
};
|
|
16924
17232
|
}
|
|
16925
17233
|
function readNotificationConfig(path = getNotificationsPath()) {
|
|
16926
|
-
if (!
|
|
17234
|
+
if (!existsSync6(path)) {
|
|
16927
17235
|
return getDefaultNotificationConfig();
|
|
16928
17236
|
}
|
|
16929
17237
|
return notificationConfigSchema.parse(JSON.parse(readFileSync5(path, "utf8")));
|
|
@@ -16958,16 +17266,34 @@ function removeNotificationChannel(channelId) {
|
|
|
16958
17266
|
channels: config.channels.filter((channel) => channel.id !== channelId)
|
|
16959
17267
|
});
|
|
16960
17268
|
}
|
|
16961
|
-
function
|
|
16962
|
-
|
|
16963
|
-
|
|
16964
|
-
|
|
16965
|
-
|
|
16966
|
-
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
|
+
}
|
|
16967
17289
|
}
|
|
16968
|
-
return
|
|
17290
|
+
return {
|
|
17291
|
+
event,
|
|
17292
|
+
message,
|
|
17293
|
+
deliveries
|
|
17294
|
+
};
|
|
16969
17295
|
}
|
|
16970
|
-
function testNotificationChannel(channelId, event = "manual.test", message = "machines notification test", options = {}) {
|
|
17296
|
+
async function testNotificationChannel(channelId, event = "manual.test", message = "machines notification test", options = {}) {
|
|
16971
17297
|
const channel = readNotificationConfig().channels.find((entry) => entry.id === channelId);
|
|
16972
17298
|
if (!channel) {
|
|
16973
17299
|
throw new Error(`Notification channel not found: ${channelId}`);
|
|
@@ -16978,77 +17304,25 @@ function testNotificationChannel(channelId, event = "manual.test", message = "ma
|
|
|
16978
17304
|
channelId,
|
|
16979
17305
|
mode: "plan",
|
|
16980
17306
|
delivered: false,
|
|
16981
|
-
preview
|
|
17307
|
+
preview,
|
|
17308
|
+
detail: "Preview only"
|
|
16982
17309
|
};
|
|
16983
17310
|
}
|
|
16984
17311
|
if (!options.yes) {
|
|
16985
17312
|
throw new Error("Notification test execution requires --yes.");
|
|
16986
17313
|
}
|
|
16987
|
-
|
|
16988
|
-
const result = Bun.spawnSync(["bash", "-lc", preview], {
|
|
16989
|
-
stdout: "pipe",
|
|
16990
|
-
stderr: "pipe",
|
|
16991
|
-
env: process.env
|
|
16992
|
-
});
|
|
16993
|
-
if (result.exitCode !== 0) {
|
|
16994
|
-
throw new Error(`Notification command failed (${channel.id}): ${result.stderr.toString().trim()}`);
|
|
16995
|
-
}
|
|
16996
|
-
}
|
|
17314
|
+
const delivery = await dispatchChannel(channel, event, message);
|
|
16997
17315
|
return {
|
|
16998
17316
|
channelId,
|
|
16999
17317
|
mode: "apply",
|
|
17000
|
-
delivered:
|
|
17001
|
-
preview
|
|
17002
|
-
|
|
17003
|
-
}
|
|
17004
|
-
|
|
17005
|
-
// src/commands/ports.ts
|
|
17006
|
-
import { spawnSync as spawnSync2 } from "child_process";
|
|
17007
|
-
|
|
17008
|
-
// src/commands/ssh.ts
|
|
17009
|
-
import { spawnSync } from "child_process";
|
|
17010
|
-
function envReachableHosts() {
|
|
17011
|
-
const raw = process.env["HASNA_MACHINES_REACHABLE_HOSTS"];
|
|
17012
|
-
return new Set((raw || "").split(",").map((value) => value.trim()).filter(Boolean));
|
|
17013
|
-
}
|
|
17014
|
-
function isReachable(host) {
|
|
17015
|
-
const overrides = envReachableHosts();
|
|
17016
|
-
if (overrides.size > 0) {
|
|
17017
|
-
return overrides.has(host);
|
|
17018
|
-
}
|
|
17019
|
-
const probe = spawnSync("bash", ["-lc", `getent hosts ${host} >/dev/null 2>&1 || ping -c 1 -W 1 ${host} >/dev/null 2>&1`], {
|
|
17020
|
-
stdio: "ignore"
|
|
17021
|
-
});
|
|
17022
|
-
return probe.status === 0;
|
|
17023
|
-
}
|
|
17024
|
-
function resolveSshTarget(machineId) {
|
|
17025
|
-
const machine = getManifestMachine(machineId);
|
|
17026
|
-
if (!machine) {
|
|
17027
|
-
throw new Error(`Machine not found in manifest: ${machineId}`);
|
|
17028
|
-
}
|
|
17029
|
-
const current = detectCurrentMachineManifest();
|
|
17030
|
-
if (machine.id === current.id) {
|
|
17031
|
-
return {
|
|
17032
|
-
machineId,
|
|
17033
|
-
target: "localhost",
|
|
17034
|
-
route: "local"
|
|
17035
|
-
};
|
|
17036
|
-
}
|
|
17037
|
-
const lanTarget = machine.sshAddress || machine.hostname || machine.id;
|
|
17038
|
-
const tailscaleTarget = machine.tailscaleName || machine.hostname || machine.id;
|
|
17039
|
-
const route = isReachable(lanTarget) ? "lan" : "tailscale";
|
|
17040
|
-
return {
|
|
17041
|
-
machineId,
|
|
17042
|
-
target: route === "lan" ? lanTarget : tailscaleTarget,
|
|
17043
|
-
route
|
|
17318
|
+
delivered: delivery.delivered,
|
|
17319
|
+
preview,
|
|
17320
|
+
detail: delivery.detail
|
|
17044
17321
|
};
|
|
17045
17322
|
}
|
|
17046
|
-
function buildSshCommand(machineId, remoteCommand) {
|
|
17047
|
-
const resolved = resolveSshTarget(machineId);
|
|
17048
|
-
return remoteCommand ? `ssh ${resolved.target} ${JSON.stringify(remoteCommand)}` : `ssh ${resolved.target}`;
|
|
17049
|
-
}
|
|
17050
17323
|
|
|
17051
17324
|
// src/commands/ports.ts
|
|
17325
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
17052
17326
|
function parseSsOutput(output) {
|
|
17053
17327
|
return output.trim().split(`
|
|
17054
17328
|
`).map((line) => line.trim()).filter(Boolean).map((line) => {
|
|
@@ -17090,7 +17364,7 @@ function listPorts(machineId) {
|
|
|
17090
17364
|
const isLocal = targetMachineId === getLocalMachineId();
|
|
17091
17365
|
const localCommand = "if command -v ss >/dev/null 2>&1; then ss -ltnpH; else lsof -nP -iTCP -sTCP:LISTEN; fi";
|
|
17092
17366
|
const command = isLocal ? localCommand : buildSshCommand(targetMachineId, localCommand);
|
|
17093
|
-
const result =
|
|
17367
|
+
const result = spawnSync3("bash", ["-lc", command], { encoding: "utf8" });
|
|
17094
17368
|
if (result.status !== 0) {
|
|
17095
17369
|
throw new Error(result.stderr || `Failed to list ports for ${targetMachineId}`);
|
|
17096
17370
|
}
|
|
@@ -17102,7 +17376,7 @@ function listPorts(machineId) {
|
|
|
17102
17376
|
}
|
|
17103
17377
|
|
|
17104
17378
|
// src/commands/sync.ts
|
|
17105
|
-
import { existsSync as
|
|
17379
|
+
import { existsSync as existsSync7, lstatSync, readFileSync as readFileSync6, symlinkSync, copyFileSync as copyFileSync2 } from "fs";
|
|
17106
17380
|
function quote4(value) {
|
|
17107
17381
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
17108
17382
|
}
|
|
@@ -17150,8 +17424,8 @@ function detectPackageActions(machine) {
|
|
|
17150
17424
|
}
|
|
17151
17425
|
function detectFileActions(machine) {
|
|
17152
17426
|
return (machine.files || []).map((file, index) => {
|
|
17153
|
-
const sourceExists =
|
|
17154
|
-
const targetExists =
|
|
17427
|
+
const sourceExists = existsSync7(file.source);
|
|
17428
|
+
const targetExists = existsSync7(file.target);
|
|
17155
17429
|
let status = "missing";
|
|
17156
17430
|
if (sourceExists && targetExists) {
|
|
17157
17431
|
if (file.mode === "symlink") {
|
|
@@ -17285,6 +17559,59 @@ function getStatus() {
|
|
|
17285
17559
|
};
|
|
17286
17560
|
}
|
|
17287
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
|
+
|
|
17288
17615
|
// src/commands/serve.ts
|
|
17289
17616
|
function escapeHtml(value) {
|
|
17290
17617
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
@@ -17296,13 +17623,27 @@ function getServeInfo(options = {}) {
|
|
|
17296
17623
|
host,
|
|
17297
17624
|
port,
|
|
17298
17625
|
url: `http://${host}:${port}`,
|
|
17299
|
-
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
|
+
]
|
|
17300
17640
|
};
|
|
17301
17641
|
}
|
|
17302
17642
|
function renderDashboardHtml() {
|
|
17303
17643
|
const status = getStatus();
|
|
17304
17644
|
const manifest = manifestList();
|
|
17305
17645
|
const notifications = listNotificationChannels();
|
|
17646
|
+
const doctor = runDoctor();
|
|
17306
17647
|
return `<!doctype html>
|
|
17307
17648
|
<html lang="en">
|
|
17308
17649
|
<head>
|
|
@@ -17318,12 +17659,14 @@ function renderDashboardHtml() {
|
|
|
17318
17659
|
.card { background: #121933; border: 1px solid #243057; border-radius: 16px; padding: 20px; }
|
|
17319
17660
|
.stat { font-size: 32px; font-weight: 700; margin-top: 8px; }
|
|
17320
17661
|
table { width: 100%; border-collapse: collapse; }
|
|
17321
|
-
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; }
|
|
17322
17663
|
code { color: #9ed0ff; }
|
|
17323
17664
|
.badge { display: inline-block; border-radius: 999px; padding: 4px 10px; font-size: 12px; }
|
|
17324
|
-
.online { background: #12351f; color: #74f0a7; }
|
|
17325
|
-
.offline { background: #3b1a1a; color: #ff8c8c; }
|
|
17326
|
-
.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; }
|
|
17327
17670
|
</style>
|
|
17328
17671
|
</head>
|
|
17329
17672
|
<body>
|
|
@@ -17333,6 +17676,7 @@ function renderDashboardHtml() {
|
|
|
17333
17676
|
<section class="card"><div>Manifest machines</div><div class="stat">${status.manifestMachineCount}</div></section>
|
|
17334
17677
|
<section class="card"><div>Heartbeats</div><div class="stat">${status.heartbeatCount}</div></section>
|
|
17335
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>
|
|
17336
17680
|
</div>
|
|
17337
17681
|
|
|
17338
17682
|
<section class="card" style="margin-top:16px">
|
|
@@ -17350,6 +17694,25 @@ function renderDashboardHtml() {
|
|
|
17350
17694
|
</table>
|
|
17351
17695
|
</section>
|
|
17352
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
|
+
|
|
17353
17716
|
<section class="card" style="margin-top:16px">
|
|
17354
17717
|
<h2>Manifest</h2>
|
|
17355
17718
|
<pre>${escapeHtml(JSON.stringify(manifest, null, 2))}</pre>
|
|
@@ -17358,13 +17721,25 @@ function renderDashboardHtml() {
|
|
|
17358
17721
|
</body>
|
|
17359
17722
|
</html>`;
|
|
17360
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
|
+
}
|
|
17361
17734
|
function startDashboardServer(options = {}) {
|
|
17362
17735
|
const info = getServeInfo(options);
|
|
17363
17736
|
return Bun.serve({
|
|
17364
17737
|
hostname: info.host,
|
|
17365
17738
|
port: info.port,
|
|
17366
|
-
fetch(request) {
|
|
17739
|
+
async fetch(request) {
|
|
17367
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);
|
|
17368
17743
|
if (url.pathname === "/health") {
|
|
17369
17744
|
return Response.json({ ok: true, ...getServeInfo(options) });
|
|
17370
17745
|
}
|
|
@@ -17377,6 +17752,43 @@ function startDashboardServer(options = {}) {
|
|
|
17377
17752
|
if (url.pathname === "/api/notifications") {
|
|
17378
17753
|
return Response.json(listNotificationChannels());
|
|
17379
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
|
+
}
|
|
17380
17792
|
return new Response(renderDashboardHtml(), {
|
|
17381
17793
|
headers: {
|
|
17382
17794
|
"content-type": "text/html; charset=utf-8"
|
|
@@ -17386,11 +17798,178 @@ function startDashboardServer(options = {}) {
|
|
|
17386
17798
|
});
|
|
17387
17799
|
}
|
|
17388
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
|
+
|
|
17389
17860
|
// src/cli/index.ts
|
|
17390
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
|
+
}
|
|
17391
17968
|
program2.name("machines").description("Machine fleet management CLI + MCP for developers").version(getPackageVersion());
|
|
17392
17969
|
var manifestCommand = program2.command("manifest").description("Manage the fleet manifest");
|
|
17393
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");
|
|
17394
17973
|
manifestCommand.command("init").description("Create an empty fleet manifest").action(() => {
|
|
17395
17974
|
console.log(manifestInit());
|
|
17396
17975
|
});
|
|
@@ -17423,11 +18002,7 @@ manifestCommand.command("add").description("Add or replace a machine in the flee
|
|
|
17423
18002
|
const files = Array.isArray(options["file"]) ? options["file"].map((value) => {
|
|
17424
18003
|
const [source, target, mode] = String(value).split(":");
|
|
17425
18004
|
const normalizedMode = mode === "symlink" ? "symlink" : mode === "copy" ? "copy" : undefined;
|
|
17426
|
-
return {
|
|
17427
|
-
source,
|
|
17428
|
-
target,
|
|
17429
|
-
mode: normalizedMode
|
|
17430
|
-
};
|
|
18005
|
+
return { source, target, mode: normalizedMode };
|
|
17431
18006
|
}) : undefined;
|
|
17432
18007
|
const apps = Array.isArray(options["app"]) ? options["app"].map((value) => {
|
|
17433
18008
|
const [name, manager, packageName] = String(value).split(":");
|
|
@@ -17455,65 +18030,55 @@ manifestCommand.command("add").description("Add or replace a machine in the flee
|
|
|
17455
18030
|
});
|
|
17456
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) => {
|
|
17457
18032
|
const result = listApps(options.machine);
|
|
17458
|
-
|
|
17459
|
-
|
|
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);
|
|
17460
18042
|
});
|
|
17461
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) => {
|
|
17462
18044
|
const result = buildAppsPlan(options.machine);
|
|
17463
|
-
|
|
17464
|
-
console.log(options.json ? rendered : rendered);
|
|
18045
|
+
console.log(options.json ? JSON.stringify(result, null, 2) : JSON.stringify(result, null, 2));
|
|
17465
18046
|
});
|
|
17466
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) => {
|
|
17467
18048
|
const result = runAppsInstall(options.machine, { apply: true, yes: options.yes });
|
|
17468
|
-
|
|
17469
|
-
console.log(options.json ? rendered : rendered);
|
|
18049
|
+
console.log(options.json ? JSON.stringify(result, null, 2) : JSON.stringify(result, null, 2));
|
|
17470
18050
|
});
|
|
17471
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) => {
|
|
17472
18052
|
const result = options.apply ? runSetup(options.machine, { apply: true, yes: options.yes }) : buildSetupPlan(options.machine);
|
|
17473
|
-
if (options.json) {
|
|
17474
|
-
console.log(JSON.stringify(result, null, 2));
|
|
17475
|
-
return;
|
|
17476
|
-
}
|
|
17477
18053
|
console.log(JSON.stringify(result, null, 2));
|
|
17478
18054
|
});
|
|
17479
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) => {
|
|
17480
18056
|
const result = options.apply ? runSync(options.machine, { apply: true, yes: options.yes }) : buildSyncPlan(options.machine);
|
|
17481
|
-
if (options.json) {
|
|
17482
|
-
console.log(JSON.stringify(result, null, 2));
|
|
17483
|
-
return;
|
|
17484
|
-
}
|
|
17485
18057
|
console.log(JSON.stringify(result, null, 2));
|
|
17486
18058
|
});
|
|
17487
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) => {
|
|
17488
18060
|
const result = diffMachines(options.left, options.right);
|
|
17489
|
-
if (options.json) {
|
|
17490
|
-
console.log(JSON.stringify(result, null, 2));
|
|
17491
|
-
return;
|
|
17492
|
-
}
|
|
17493
18061
|
console.log(JSON.stringify(result, null, 2));
|
|
17494
18062
|
});
|
|
17495
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) => {
|
|
17496
18064
|
const result = options.apply ? runBackup(options.bucket, options.prefix, { apply: true, yes: options.yes }) : buildBackupPlan(options.bucket, options.prefix);
|
|
17497
|
-
|
|
17498
|
-
console.log(options.json ? rendered : rendered);
|
|
18065
|
+
console.log(JSON.stringify(result, null, 2));
|
|
17499
18066
|
});
|
|
17500
18067
|
var certCommand = program2.command("cert").description("Manage mkcert-based local SSL certificates");
|
|
17501
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) => {
|
|
17502
18069
|
const result = options.apply ? runCertPlan(domains, { apply: true, yes: options.yes }) : buildCertPlan(domains);
|
|
17503
|
-
|
|
17504
|
-
console.log(options.json ? rendered : rendered);
|
|
18070
|
+
console.log(JSON.stringify(result, null, 2));
|
|
17505
18071
|
});
|
|
17506
18072
|
var dnsCommand = program2.command("dns").description("Manage local domain mappings");
|
|
17507
|
-
var notificationsCommand = program2.command("notifications").description("Manage fleet alert delivery channels");
|
|
17508
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) => {
|
|
17509
|
-
const result = addDomainMapping(options.domain,
|
|
17510
|
-
|
|
17511
|
-
|
|
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));
|
|
17512
18079
|
});
|
|
17513
|
-
dnsCommand.command("
|
|
17514
|
-
|
|
17515
|
-
const rendered = JSON.stringify(result, null, 2);
|
|
17516
|
-
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));
|
|
17517
18082
|
});
|
|
17518
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) => {
|
|
17519
18084
|
const result = addNotificationChannel({
|
|
@@ -17523,41 +18088,50 @@ notificationsCommand.command("add").description("Add or replace a notification c
|
|
|
17523
18088
|
events: options.event,
|
|
17524
18089
|
enabled: !options.disabled
|
|
17525
18090
|
});
|
|
17526
|
-
|
|
17527
|
-
console.log(options.json ? rendered : rendered);
|
|
18091
|
+
printJsonOrText(result, renderNotificationConfigResult(result), options.json);
|
|
17528
18092
|
});
|
|
17529
18093
|
notificationsCommand.command("list").description("List configured notification channels").option("-j, --json", "Print JSON output", false).action((options) => {
|
|
17530
18094
|
const result = listNotificationChannels();
|
|
17531
|
-
|
|
17532
|
-
console.log(options.json ? rendered : rendered);
|
|
18095
|
+
printJsonOrText(result, renderNotificationConfigResult(result), options.json);
|
|
17533
18096
|
});
|
|
17534
|
-
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) => {
|
|
17535
|
-
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, {
|
|
17536
18099
|
apply: options.apply,
|
|
17537
18100
|
yes: options.yes
|
|
17538
18101
|
});
|
|
17539
|
-
|
|
17540
|
-
|
|
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);
|
|
17541
18107
|
});
|
|
17542
18108
|
notificationsCommand.command("remove").description("Remove a notification channel").argument("<id>", "Channel identifier").option("-j, --json", "Print JSON output", false).action((id, options) => {
|
|
17543
18109
|
const result = removeNotificationChannel(id);
|
|
17544
|
-
|
|
17545
|
-
|
|
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);
|
|
17546
18119
|
});
|
|
17547
|
-
|
|
17548
|
-
const result =
|
|
17549
|
-
|
|
17550
|
-
|
|
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));
|
|
17551
18127
|
});
|
|
17552
|
-
|
|
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) => {
|
|
17553
18129
|
const result = options.apply ? runClaudeInstall(options.machine, options.tool, { apply: true, yes: options.yes }) : buildClaudeInstallPlan(options.machine, options.tool);
|
|
17554
|
-
|
|
17555
|
-
console.log(options.json ? rendered : rendered);
|
|
18130
|
+
console.log(JSON.stringify(result, null, 2));
|
|
17556
18131
|
});
|
|
17557
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) => {
|
|
17558
18133
|
const result = options.apply ? runTailscaleInstall(options.machine, { apply: true, yes: options.yes }) : buildTailscaleInstallPlan(options.machine);
|
|
17559
|
-
|
|
17560
|
-
console.log(options.json ? rendered : rendered);
|
|
18134
|
+
console.log(JSON.stringify(result, null, 2));
|
|
17561
18135
|
});
|
|
17562
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) => {
|
|
17563
18137
|
if (options.json) {
|
|
@@ -17568,27 +18142,27 @@ program2.command("ssh").description("Choose the best SSH route for a machine").r
|
|
|
17568
18142
|
});
|
|
17569
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) => {
|
|
17570
18144
|
const result = listPorts(options.machine);
|
|
17571
|
-
|
|
17572
|
-
console.log(options.json ? rendered : rendered);
|
|
18145
|
+
console.log(JSON.stringify(result, null, 2));
|
|
17573
18146
|
});
|
|
17574
18147
|
program2.command("status").description("Print local machine and storage status").option("-j, --json", "Print JSON output", false).action((options) => {
|
|
17575
18148
|
const status = getStatus();
|
|
17576
|
-
|
|
17577
|
-
|
|
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);
|
|
17578
18158
|
});
|
|
17579
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) => {
|
|
17580
|
-
const info = getServeInfo({
|
|
17581
|
-
host: options.host,
|
|
17582
|
-
port: Number.parseInt(options.port, 10)
|
|
17583
|
-
});
|
|
18160
|
+
const info = getServeInfo({ host: options.host, port: parseIntegerOption(options.port, "port", { min: 1, max: 65535 }) });
|
|
17584
18161
|
if (options.json) {
|
|
17585
18162
|
console.log(JSON.stringify(info, null, 2));
|
|
17586
18163
|
return;
|
|
17587
18164
|
}
|
|
17588
|
-
const server = startDashboardServer({
|
|
17589
|
-
host: info.host,
|
|
17590
|
-
port: info.port
|
|
17591
|
-
});
|
|
18165
|
+
const server = startDashboardServer({ host: info.host, port: info.port });
|
|
17592
18166
|
console.log(source_default.green(`machines dashboard listening on http://${server.hostname}:${server.port}`));
|
|
17593
18167
|
});
|
|
17594
18168
|
await program2.parseAsync(process.argv);
|