@hasna/machines 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -6599,6 +6599,12 @@ function getManifestPath() {
6599
6599
  function getNotificationsPath() {
6600
6600
  return process.env["HASNA_MACHINES_NOTIFICATIONS_PATH"] || join2(getDataDir(), "notifications.json");
6601
6601
  }
6602
+ function getClipboardKeyPath() {
6603
+ return process.env["HASNA_MACHINES_CLIPBOARD_KEY_PATH"] || join2(getDataDir(), "clipboard.key");
6604
+ }
6605
+ function getClipboardHistoryPath() {
6606
+ return process.env["HASNA_MACHINES_CLIPBOARD_HISTORY_PATH"] || join2(getDataDir(), "clipboard-history.json");
6607
+ }
6602
6608
  function ensureParentDir(filePath) {
6603
6609
  if (filePath === ":memory:")
6604
6610
  return;
@@ -16698,17 +16704,95 @@ function diffMachines(leftMachineId, rightMachineId) {
16698
16704
  };
16699
16705
  }
16700
16706
 
16707
+ // src/remote.ts
16708
+ import { spawnSync as spawnSync2 } from "child_process";
16709
+
16710
+ // src/commands/ssh.ts
16711
+ import { spawnSync } from "child_process";
16712
+ function envReachableHosts() {
16713
+ const raw = process.env["HASNA_MACHINES_REACHABLE_HOSTS"];
16714
+ return new Set((raw || "").split(",").map((value) => value.trim()).filter(Boolean));
16715
+ }
16716
+ function isReachable(host) {
16717
+ const overrides = envReachableHosts();
16718
+ if (overrides.size > 0) {
16719
+ return overrides.has(host);
16720
+ }
16721
+ const probe = spawnSync("bash", ["-lc", `getent hosts ${host} >/dev/null 2>&1 || ping -c 1 -W 1 ${host} >/dev/null 2>&1`], {
16722
+ stdio: "ignore"
16723
+ });
16724
+ return probe.status === 0;
16725
+ }
16726
+ function resolveSshTarget(machineId) {
16727
+ const machine = getManifestMachine(machineId);
16728
+ if (!machine) {
16729
+ throw new Error(`Machine not found in manifest: ${machineId}`);
16730
+ }
16731
+ const current = detectCurrentMachineManifest();
16732
+ if (machine.id === current.id) {
16733
+ return {
16734
+ machineId,
16735
+ target: "localhost",
16736
+ route: "local"
16737
+ };
16738
+ }
16739
+ const lanTarget = machine.sshAddress || machine.hostname || machine.id;
16740
+ const tailscaleTarget = machine.tailscaleName || machine.hostname || machine.id;
16741
+ const route = isReachable(lanTarget) ? "lan" : "tailscale";
16742
+ return {
16743
+ machineId,
16744
+ target: route === "lan" ? lanTarget : tailscaleTarget,
16745
+ route
16746
+ };
16747
+ }
16748
+ function buildSshCommand(machineId, remoteCommand) {
16749
+ const resolved = resolveSshTarget(machineId);
16750
+ return remoteCommand ? `ssh ${resolved.target} ${JSON.stringify(remoteCommand)}` : `ssh ${resolved.target}`;
16751
+ }
16752
+
16753
+ // src/remote.ts
16754
+ function runMachineCommand(machineId, command) {
16755
+ const localMachineId = getLocalMachineId();
16756
+ const isLocal = machineId === localMachineId;
16757
+ const route = isLocal ? "local" : resolveSshTarget(machineId).route;
16758
+ const shellCommand = isLocal ? command : buildSshCommand(machineId, command);
16759
+ const result = spawnSync2("bash", ["-lc", shellCommand], {
16760
+ encoding: "utf8",
16761
+ env: process.env
16762
+ });
16763
+ return {
16764
+ machineId,
16765
+ source: route,
16766
+ stdout: result.stdout || "",
16767
+ stderr: result.stderr || "",
16768
+ exitCode: result.status ?? 1
16769
+ };
16770
+ }
16771
+
16701
16772
  // src/commands/apps.ts
16702
16773
  function getPackageName(app) {
16703
16774
  return app.packageName || app.name;
16704
16775
  }
16776
+ function getAppManager(machine, app) {
16777
+ if (app.manager)
16778
+ return app.manager;
16779
+ if (machine.platform === "macos")
16780
+ return "brew";
16781
+ if (machine.platform === "windows")
16782
+ return "winget";
16783
+ return "apt";
16784
+ }
16785
+ function shellQuote(value) {
16786
+ return `'${value.replace(/'/g, `'\\''`)}'`;
16787
+ }
16705
16788
  function buildAppCommand(machine, app) {
16706
16789
  const packageName = getPackageName(app);
16707
- if (app.manager === "custom") {
16790
+ const manager = getAppManager(machine, app);
16791
+ if (manager === "custom") {
16708
16792
  return packageName;
16709
16793
  }
16710
16794
  if (machine.platform === "macos") {
16711
- if (app.manager === "cask") {
16795
+ if (manager === "cask") {
16712
16796
  return `brew install --cask ${packageName}`;
16713
16797
  }
16714
16798
  return `brew install ${packageName}`;
@@ -16718,18 +16802,48 @@ function buildAppCommand(machine, app) {
16718
16802
  }
16719
16803
  return `sudo apt-get install -y ${packageName}`;
16720
16804
  }
16805
+ function buildAppProbeCommand(machine, app) {
16806
+ const packageName = shellQuote(getPackageName(app));
16807
+ const manager = getAppManager(machine, app);
16808
+ if (manager === "custom") {
16809
+ return `if command -v ${packageName} >/dev/null 2>&1; then printf 'installed=1\\nversion=custom\\n'; else printf 'installed=0\\n'; fi`;
16810
+ }
16811
+ if (machine.platform === "macos") {
16812
+ if (manager === "cask") {
16813
+ return `if brew list --cask ${packageName} >/dev/null 2>&1; then printf 'installed=1\\nversion=installed\\n'; else printf 'installed=0\\n'; fi`;
16814
+ }
16815
+ 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`;
16816
+ }
16817
+ if (machine.platform === "windows") {
16818
+ return `if winget list --id ${packageName} --exact >/dev/null 2>&1; then printf 'installed=1\\nversion=installed\\n'; else printf 'installed=0\\n'; fi`;
16819
+ }
16820
+ 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`;
16821
+ }
16721
16822
  function buildAppSteps(machine) {
16722
16823
  return (machine.apps || []).map((app) => ({
16723
16824
  id: `app-${app.name}`,
16724
16825
  title: `Install ${app.name} on ${machine.id}`,
16725
16826
  command: buildAppCommand(machine, app),
16726
- manager: app.manager === "custom" ? "custom" : machine.platform === "macos" ? "brew" : machine.platform === "windows" ? "custom" : "apt",
16827
+ manager: getAppManager(machine, app) === "custom" ? "custom" : machine.platform === "macos" ? "brew" : machine.platform === "windows" ? "custom" : "apt",
16727
16828
  privileged: machine.platform === "linux"
16728
16829
  }));
16729
16830
  }
16730
16831
  function resolveMachine(machineId) {
16731
16832
  return (machineId ? getManifestMachine(machineId) : null) || detectCurrentMachineManifest();
16732
16833
  }
16834
+ function parseProbeOutput(app, machine, stdout) {
16835
+ const lines = stdout.trim().split(`
16836
+ `).filter(Boolean);
16837
+ const installedLine = lines.find((line) => line.startsWith("installed="));
16838
+ const versionLine = lines.find((line) => line.startsWith("version="));
16839
+ return {
16840
+ name: app.name,
16841
+ packageName: getPackageName(app),
16842
+ manager: getAppManager(machine, app),
16843
+ installed: installedLine === "installed=1",
16844
+ version: versionLine?.slice("version=".length) || undefined
16845
+ };
16846
+ }
16733
16847
  function listApps(machineId) {
16734
16848
  const machine = resolveMachine(machineId);
16735
16849
  return {
@@ -16746,6 +16860,26 @@ function buildAppsPlan(machineId) {
16746
16860
  executed: 0
16747
16861
  };
16748
16862
  }
16863
+ function getAppsStatus(machineId) {
16864
+ const machine = resolveMachine(machineId);
16865
+ const apps = (machine.apps || []).map((app) => {
16866
+ const probe = runMachineCommand(machine.id, buildAppProbeCommand(machine, app));
16867
+ return parseProbeOutput(app, machine, probe.stdout);
16868
+ });
16869
+ return {
16870
+ machineId: machine.id,
16871
+ source: apps.length > 0 ? runMachineCommand(machine.id, "true").source : machine.id === detectCurrentMachineManifest().id ? "local" : runMachineCommand(machine.id, "true").source,
16872
+ apps
16873
+ };
16874
+ }
16875
+ function diffApps(machineId) {
16876
+ const status = getAppsStatus(machineId);
16877
+ return {
16878
+ ...status,
16879
+ missing: status.apps.filter((app) => !app.installed).map((app) => app.name),
16880
+ installed: status.apps.filter((app) => app.installed).map((app) => app.name)
16881
+ };
16882
+ }
16749
16883
  function runAppsInstall(machineId, options = {}) {
16750
16884
  const plan = buildAppsPlan(machineId);
16751
16885
  if (!options.apply)
@@ -16779,6 +16913,13 @@ var AI_CLI_PACKAGES = {
16779
16913
  codex: "@openai/codex",
16780
16914
  gemini: "@google/gemini-cli"
16781
16915
  };
16916
+ function getToolBinary(tool) {
16917
+ if (tool === "claude")
16918
+ return process.env["HASNA_MACHINES_CLAUDE_BINARY"] || "claude";
16919
+ if (tool === "codex")
16920
+ return process.env["HASNA_MACHINES_CODEX_BINARY"] || "codex";
16921
+ return process.env["HASNA_MACHINES_GEMINI_BINARY"] || "gemini";
16922
+ }
16782
16923
  function normalizeTools(tools) {
16783
16924
  if (!tools || tools.length === 0) {
16784
16925
  return ["claude", "codex", "gemini"];
@@ -16798,8 +16939,27 @@ function buildInstallSteps(machine, tools) {
16798
16939
  manager: "bun"
16799
16940
  }));
16800
16941
  }
16942
+ function resolveMachine2(machineId) {
16943
+ return (machineId ? getManifestMachine(machineId) : null) || detectCurrentMachineManifest();
16944
+ }
16945
+ function buildProbeCommand(tool) {
16946
+ const binary = getToolBinary(tool);
16947
+ 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`;
16948
+ }
16949
+ function parseProbe(tool, stdout) {
16950
+ const lines = stdout.trim().split(`
16951
+ `).filter(Boolean);
16952
+ const installedLine = lines.find((line) => line.startsWith("installed="));
16953
+ const versionLine = lines.find((line) => line.startsWith("version="));
16954
+ return {
16955
+ tool,
16956
+ packageName: AI_CLI_PACKAGES[tool],
16957
+ installed: installedLine === "installed=1",
16958
+ version: versionLine?.slice("version=".length) || undefined
16959
+ };
16960
+ }
16801
16961
  function buildClaudeInstallPlan(machineId, tools) {
16802
- const machine = (machineId ? getManifestMachine(machineId) : null) || detectCurrentMachineManifest();
16962
+ const machine = resolveMachine2(machineId);
16803
16963
  return {
16804
16964
  machineId: machine.id,
16805
16965
  mode: "plan",
@@ -16807,6 +16967,24 @@ function buildClaudeInstallPlan(machineId, tools) {
16807
16967
  executed: 0
16808
16968
  };
16809
16969
  }
16970
+ function getClaudeCliStatus(machineId, tools) {
16971
+ const machine = resolveMachine2(machineId);
16972
+ const normalizedTools = normalizeTools(tools);
16973
+ const route = runMachineCommand(machine.id, "true").source;
16974
+ return {
16975
+ machineId: machine.id,
16976
+ source: route,
16977
+ tools: normalizedTools.map((tool) => parseProbe(tool, runMachineCommand(machine.id, buildProbeCommand(tool)).stdout))
16978
+ };
16979
+ }
16980
+ function diffClaudeCli(machineId, tools) {
16981
+ const status = getClaudeCliStatus(machineId, tools);
16982
+ return {
16983
+ ...status,
16984
+ missing: status.tools.filter((tool) => !tool.installed).map((tool) => tool.tool),
16985
+ installed: status.tools.filter((tool) => tool.installed).map((tool) => tool.tool)
16986
+ };
16987
+ }
16810
16988
  function runClaudeInstall(machineId, tools, options = {}) {
16811
16989
  const plan = buildClaudeInstallPlan(machineId, tools);
16812
16990
  if (!options.apply)
@@ -16919,6 +17097,138 @@ var notificationConfigSchema = exports_external.object({
16919
17097
  function sortChannels(channels) {
16920
17098
  return [...channels].sort((left, right) => left.id.localeCompare(right.id));
16921
17099
  }
17100
+ function shellQuote2(value) {
17101
+ return `'${value.replace(/'/g, `'\\''`)}'`;
17102
+ }
17103
+ function hasCommand(binary) {
17104
+ const result = Bun.spawnSync(["bash", "-lc", `command -v ${binary} >/dev/null 2>&1`], {
17105
+ stdout: "ignore",
17106
+ stderr: "ignore",
17107
+ env: process.env
17108
+ });
17109
+ return result.exitCode === 0;
17110
+ }
17111
+ function buildNotificationPreview(channel, event, message) {
17112
+ if (channel.type === "email") {
17113
+ return `send email to ${channel.target}: [${event}] ${message}`;
17114
+ }
17115
+ if (channel.type === "webhook") {
17116
+ return `POST ${channel.target} with payload {"event":"${event}","message":"${message}"}`;
17117
+ }
17118
+ return `${channel.target} --event ${event} --message ${JSON.stringify(message)}`;
17119
+ }
17120
+ async function dispatchEmail(channel, event, message) {
17121
+ const subject = `[${event}] machines notification`;
17122
+ const body = `To: ${channel.target}
17123
+ Subject: ${subject}
17124
+ Content-Type: text/plain; charset=utf-8
17125
+
17126
+ ${message}
17127
+ `;
17128
+ if (hasCommand("sendmail")) {
17129
+ const result = Bun.spawnSync(["bash", "-lc", "sendmail -t"], {
17130
+ stdin: new TextEncoder().encode(body),
17131
+ stdout: "pipe",
17132
+ stderr: "pipe",
17133
+ env: process.env
17134
+ });
17135
+ if (result.exitCode !== 0) {
17136
+ throw new Error(result.stderr.toString().trim() || `sendmail exited with ${result.exitCode}`);
17137
+ }
17138
+ return {
17139
+ channelId: channel.id,
17140
+ event,
17141
+ delivered: true,
17142
+ transport: channel.type,
17143
+ detail: `Delivered via sendmail to ${channel.target}`
17144
+ };
17145
+ }
17146
+ if (hasCommand("mail")) {
17147
+ const command = `printf %s ${shellQuote2(message)} | mail -s ${shellQuote2(subject)} ${shellQuote2(channel.target)}`;
17148
+ const result = Bun.spawnSync(["bash", "-lc", command], {
17149
+ stdout: "pipe",
17150
+ stderr: "pipe",
17151
+ env: process.env
17152
+ });
17153
+ if (result.exitCode !== 0) {
17154
+ throw new Error(result.stderr.toString().trim() || `mail exited with ${result.exitCode}`);
17155
+ }
17156
+ return {
17157
+ channelId: channel.id,
17158
+ event,
17159
+ delivered: true,
17160
+ transport: channel.type,
17161
+ detail: `Delivered via mail to ${channel.target}`
17162
+ };
17163
+ }
17164
+ throw new Error("No local email transport available. Install sendmail or mail.");
17165
+ }
17166
+ async function dispatchWebhook(channel, event, message) {
17167
+ const response = await fetch(channel.target, {
17168
+ method: "POST",
17169
+ headers: {
17170
+ "content-type": "application/json"
17171
+ },
17172
+ body: JSON.stringify({
17173
+ channelId: channel.id,
17174
+ event,
17175
+ message,
17176
+ sentAt: new Date().toISOString()
17177
+ })
17178
+ });
17179
+ if (!response.ok) {
17180
+ const text = await response.text();
17181
+ throw new Error(`Webhook responded ${response.status}: ${text || response.statusText}`);
17182
+ }
17183
+ return {
17184
+ channelId: channel.id,
17185
+ event,
17186
+ delivered: true,
17187
+ transport: channel.type,
17188
+ detail: `Webhook accepted with HTTP ${response.status}`
17189
+ };
17190
+ }
17191
+ async function dispatchCommand(channel, event, message) {
17192
+ const result = Bun.spawnSync(["bash", "-lc", channel.target], {
17193
+ stdout: "pipe",
17194
+ stderr: "pipe",
17195
+ env: {
17196
+ ...process.env,
17197
+ HASNA_MACHINES_NOTIFICATION_CHANNEL: channel.id,
17198
+ HASNA_MACHINES_NOTIFICATION_EVENT: event,
17199
+ HASNA_MACHINES_NOTIFICATION_MESSAGE: message
17200
+ }
17201
+ });
17202
+ if (result.exitCode !== 0) {
17203
+ throw new Error(result.stderr.toString().trim() || `command exited with ${result.exitCode}`);
17204
+ }
17205
+ const stdout = result.stdout.toString().trim();
17206
+ return {
17207
+ channelId: channel.id,
17208
+ event,
17209
+ delivered: true,
17210
+ transport: channel.type,
17211
+ detail: stdout || "Command completed successfully"
17212
+ };
17213
+ }
17214
+ async function dispatchChannel(channel, event, message) {
17215
+ if (!channel.enabled) {
17216
+ return {
17217
+ channelId: channel.id,
17218
+ event,
17219
+ delivered: false,
17220
+ transport: channel.type,
17221
+ detail: "Channel is disabled"
17222
+ };
17223
+ }
17224
+ if (channel.type === "email") {
17225
+ return dispatchEmail(channel, event, message);
17226
+ }
17227
+ if (channel.type === "webhook") {
17228
+ return dispatchWebhook(channel, event, message);
17229
+ }
17230
+ return dispatchCommand(channel, event, message);
17231
+ }
16922
17232
  function getDefaultNotificationConfig() {
16923
17233
  return {
16924
17234
  version: 1,
@@ -16962,16 +17272,34 @@ function removeNotificationChannel(channelId) {
16962
17272
  channels: config.channels.filter((channel) => channel.id !== channelId)
16963
17273
  });
16964
17274
  }
16965
- function buildNotificationPreview(channel, event, message) {
16966
- if (channel.type === "email") {
16967
- return `send email to ${channel.target}: [${event}] ${message}`;
16968
- }
16969
- if (channel.type === "webhook") {
16970
- return `POST ${channel.target} with payload {"event":"${event}","message":"${message}"}`;
17275
+ async function dispatchNotificationEvent(event, message, options = {}) {
17276
+ const channels = readNotificationConfig().channels.filter((channel) => {
17277
+ if (options.channelId && channel.id !== options.channelId) {
17278
+ return false;
17279
+ }
17280
+ return channel.events.includes(event) || event === "manual.test";
17281
+ });
17282
+ const deliveries = [];
17283
+ for (const channel of channels) {
17284
+ try {
17285
+ deliveries.push(await dispatchChannel(channel, event, message));
17286
+ } catch (error) {
17287
+ deliveries.push({
17288
+ channelId: channel.id,
17289
+ event,
17290
+ delivered: false,
17291
+ transport: channel.type,
17292
+ detail: error instanceof Error ? error.message : String(error)
17293
+ });
17294
+ }
16971
17295
  }
16972
- return `${channel.target} --event ${event} --message ${JSON.stringify(message)}`;
17296
+ return {
17297
+ event,
17298
+ message,
17299
+ deliveries
17300
+ };
16973
17301
  }
16974
- function testNotificationChannel(channelId, event = "manual.test", message = "machines notification test", options = {}) {
17302
+ async function testNotificationChannel(channelId, event = "manual.test", message = "machines notification test", options = {}) {
16975
17303
  const channel = readNotificationConfig().channels.find((entry) => entry.id === channelId);
16976
17304
  if (!channel) {
16977
17305
  throw new Error(`Notification channel not found: ${channelId}`);
@@ -16982,77 +17310,25 @@ function testNotificationChannel(channelId, event = "manual.test", message = "ma
16982
17310
  channelId,
16983
17311
  mode: "plan",
16984
17312
  delivered: false,
16985
- preview
17313
+ preview,
17314
+ detail: "Preview only"
16986
17315
  };
16987
17316
  }
16988
17317
  if (!options.yes) {
16989
17318
  throw new Error("Notification test execution requires --yes.");
16990
17319
  }
16991
- if (channel.type === "command") {
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
- }
17320
+ const delivery = await dispatchChannel(channel, event, message);
17001
17321
  return {
17002
17322
  channelId,
17003
17323
  mode: "apply",
17004
- delivered: channel.enabled,
17005
- preview
17324
+ delivered: delivery.delivered,
17325
+ preview,
17326
+ detail: delivery.detail
17006
17327
  };
17007
17328
  }
17008
17329
 
17009
17330
  // src/commands/ports.ts
17010
- import { spawnSync as spawnSync2 } from "child_process";
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
17331
+ import { spawnSync as spawnSync3 } from "child_process";
17056
17332
  function parseSsOutput(output) {
17057
17333
  return output.trim().split(`
17058
17334
  `).map((line) => line.trim()).filter(Boolean).map((line) => {
@@ -17094,7 +17370,7 @@ function listPorts(machineId) {
17094
17370
  const isLocal = targetMachineId === getLocalMachineId();
17095
17371
  const localCommand = "if command -v ss >/dev/null 2>&1; then ss -ltnpH; else lsof -nP -iTCP -sTCP:LISTEN; fi";
17096
17372
  const command = isLocal ? localCommand : buildSshCommand(targetMachineId, localCommand);
17097
- const result = spawnSync2("bash", ["-lc", command], { encoding: "utf8" });
17373
+ const result = spawnSync3("bash", ["-lc", command], { encoding: "utf8" });
17098
17374
  if (result.status !== 0) {
17099
17375
  throw new Error(result.stderr || `Failed to list ports for ${targetMachineId}`);
17100
17376
  }
@@ -17289,6 +17565,59 @@ function getStatus() {
17289
17565
  };
17290
17566
  }
17291
17567
 
17568
+ // src/commands/doctor.ts
17569
+ function makeCheck(id, status, summary, detail) {
17570
+ return { id, status, summary, detail };
17571
+ }
17572
+ function parseKeyValueOutput(stdout) {
17573
+ return Object.fromEntries(stdout.trim().split(`
17574
+ `).map((line) => line.split("=")).filter((parts) => parts.length === 2).map(([key, value]) => [key, value]));
17575
+ }
17576
+ function buildDoctorCommand() {
17577
+ return [
17578
+ 'data_dir="${HASNA_MACHINES_DIR:-$HOME/.hasna/machines}"',
17579
+ 'manifest_path="${HASNA_MACHINES_MANIFEST_PATH:-$data_dir/machines.json}"',
17580
+ 'db_path="${HASNA_MACHINES_DB_PATH:-$data_dir/machines.db}"',
17581
+ 'notifications_path="${HASNA_MACHINES_NOTIFICATIONS_PATH:-$data_dir/notifications.json}"',
17582
+ `printf 'manifest_path=%s\\n' "$manifest_path"`,
17583
+ `printf 'db_path=%s\\n' "$db_path"`,
17584
+ `printf 'notifications_path=%s\\n' "$notifications_path"`,
17585
+ `printf 'manifest_exists=%s\\n' "$(test -e "$manifest_path" && printf yes || printf no)"`,
17586
+ `printf 'db_exists=%s\\n' "$(test -e "$db_path" && printf yes || printf no)"`,
17587
+ `printf 'notifications_exists=%s\\n' "$(test -e "$notifications_path" && printf yes || printf no)"`,
17588
+ `printf 'bun=%s\\n' "$(bun --version 2>/dev/null || printf missing)"`,
17589
+ `printf 'ssh=%s\\n' "$(command -v ssh >/dev/null 2>&1 && printf ok || printf missing)"`,
17590
+ `printf 'machines=%s\\n' "$(command -v machines 2>/dev/null || printf missing)"`,
17591
+ `printf 'machines_agent=%s\\n' "$(command -v machines-agent 2>/dev/null || printf missing)"`,
17592
+ `printf 'machines_mcp=%s\\n' "$(command -v machines-mcp 2>/dev/null || printf missing)"`
17593
+ ].join("; ");
17594
+ }
17595
+ function runDoctor(machineId = getLocalMachineId()) {
17596
+ const manifest = readManifest();
17597
+ const commandChecks = runMachineCommand(machineId, buildDoctorCommand());
17598
+ const details = parseKeyValueOutput(commandChecks.stdout);
17599
+ const machineInManifest = manifest.machines.find((machine) => machine.id === machineId);
17600
+ const checks = [
17601
+ makeCheck("manifest-entry", machineInManifest ? "ok" : "warn", machineInManifest ? "Machine exists in manifest" : "Machine missing from manifest", machineInManifest ? JSON.stringify(machineInManifest) : `No manifest entry for ${machineId}`),
17602
+ makeCheck("manifest-path", details["manifest_exists"] === "yes" ? "ok" : "warn", "Manifest path check", `${details["manifest_path"] || "unknown"} ${details["manifest_exists"] === "yes" ? "exists" : "missing"}`),
17603
+ makeCheck("db-path", details["db_exists"] === "yes" ? "ok" : "warn", "DB path check", `${details["db_path"] || "unknown"} ${details["db_exists"] === "yes" ? "exists" : "missing"}`),
17604
+ makeCheck("notifications-path", details["notifications_exists"] === "yes" ? "ok" : "warn", "Notifications path check", `${details["notifications_path"] || "unknown"} ${details["notifications_exists"] === "yes" ? "exists" : "missing"}`),
17605
+ makeCheck("bun", details["bun"] && details["bun"] !== "missing" ? "ok" : "fail", "Bun availability", details["bun"] || "missing"),
17606
+ makeCheck("machines-cli", details["machines"] && details["machines"] !== "missing" ? "ok" : "warn", "machines CLI availability", details["machines"] || "missing"),
17607
+ makeCheck("machines-agent-cli", details["machines_agent"] && details["machines_agent"] !== "missing" ? "ok" : "warn", "machines-agent availability", details["machines_agent"] || "missing"),
17608
+ makeCheck("machines-mcp-cli", details["machines_mcp"] && details["machines_mcp"] !== "missing" ? "ok" : "warn", "machines-mcp availability", details["machines_mcp"] || "missing"),
17609
+ makeCheck("ssh", details["ssh"] === "ok" ? "ok" : "warn", "SSH availability", details["ssh"] || "missing")
17610
+ ];
17611
+ return {
17612
+ machineId,
17613
+ source: commandChecks.source,
17614
+ manifestPath: details["manifest_path"],
17615
+ dbPath: details["db_path"],
17616
+ notificationsPath: details["notifications_path"],
17617
+ checks
17618
+ };
17619
+ }
17620
+
17292
17621
  // src/commands/serve.ts
17293
17622
  function escapeHtml(value) {
17294
17623
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
@@ -17300,13 +17629,27 @@ function getServeInfo(options = {}) {
17300
17629
  host,
17301
17630
  port,
17302
17631
  url: `http://${host}:${port}`,
17303
- routes: ["/", "/health", "/api/status", "/api/manifest", "/api/notifications"]
17632
+ routes: [
17633
+ "/",
17634
+ "/health",
17635
+ "/api/status",
17636
+ "/api/manifest",
17637
+ "/api/notifications",
17638
+ "/api/doctor",
17639
+ "/api/self-test",
17640
+ "/api/apps/status",
17641
+ "/api/apps/diff",
17642
+ "/api/install-claude/status",
17643
+ "/api/install-claude/diff",
17644
+ "/api/notifications/test"
17645
+ ]
17304
17646
  };
17305
17647
  }
17306
17648
  function renderDashboardHtml() {
17307
17649
  const status = getStatus();
17308
17650
  const manifest = manifestList();
17309
17651
  const notifications = listNotificationChannels();
17652
+ const doctor = runDoctor();
17310
17653
  return `<!doctype html>
17311
17654
  <html lang="en">
17312
17655
  <head>
@@ -17322,21 +17665,26 @@ function renderDashboardHtml() {
17322
17665
  .card { background: #121933; border: 1px solid #243057; border-radius: 16px; padding: 20px; }
17323
17666
  .stat { font-size: 32px; font-weight: 700; margin-top: 8px; }
17324
17667
  table { width: 100%; border-collapse: collapse; }
17325
- th, td { text-align: left; padding: 10px 8px; border-bottom: 1px solid #243057; }
17668
+ th, td { text-align: left; padding: 10px 8px; border-bottom: 1px solid #243057; vertical-align: top; }
17326
17669
  code { color: #9ed0ff; }
17327
17670
  .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; }
17671
+ .online, .ok { background: #12351f; color: #74f0a7; }
17672
+ .offline, .fail { background: #3b1a1a; color: #ff8c8c; }
17673
+ .unknown, .warn { background: #2f2b16; color: #ffd76a; }
17674
+ ul { margin: 8px 0 0; padding-left: 18px; }
17675
+ .muted { color: #9fb0d9; }
17676
+ .refresh { font-size: 12px; color: #6b7fa3; margin-left: auto; }
17677
+ .updated { transition: opacity 0.3s; }
17331
17678
  </style>
17332
17679
  </head>
17333
17680
  <body>
17334
17681
  <main>
17335
- <h1>Machines Dashboard</h1>
17682
+ <h1>Machines Dashboard <span class="refresh" id="last-updated"></span></h1>
17336
17683
  <div class="grid">
17337
17684
  <section class="card"><div>Manifest machines</div><div class="stat">${status.manifestMachineCount}</div></section>
17338
17685
  <section class="card"><div>Heartbeats</div><div class="stat">${status.heartbeatCount}</div></section>
17339
17686
  <section class="card"><div>Notification channels</div><div class="stat">${notifications.channels.length}</div></section>
17687
+ <section class="card"><div>Doctor warnings</div><div class="stat">${doctor.checks.filter((entry) => entry.status !== "ok").length}</div></section>
17340
17688
  </div>
17341
17689
 
17342
17690
  <section class="card" style="margin-top:16px">
@@ -17354,21 +17702,119 @@ function renderDashboardHtml() {
17354
17702
  </table>
17355
17703
  </section>
17356
17704
 
17705
+ <section class="card" style="margin-top:16px">
17706
+ <h2>Doctor</h2>
17707
+ <table>
17708
+ <thead><tr><th>Check</th><th>Status</th><th>Detail</th></tr></thead>
17709
+ <tbody id="doctor-tbody">
17710
+ ${doctor.checks.map((entry) => `<tr>
17711
+ <td>${escapeHtml(entry.summary)}</td>
17712
+ <td><span class="badge ${escapeHtml(entry.status)}">${escapeHtml(entry.status)}</span></td>
17713
+ <td class="muted">${escapeHtml(entry.detail)}</td>
17714
+ </tr>`).join("")}
17715
+ </tbody>
17716
+ </table>
17717
+ </section>
17718
+
17719
+ <section class="card" style="margin-top:16px">
17720
+ <h2>Apps</h2>
17721
+ <p class="muted">Use <code>/api/apps/status</code> for the full app inventory payload.</p>
17722
+ </section>
17723
+
17724
+ <section class="card" style="margin-top:16px">
17725
+ <h2>AI CLIs</h2>
17726
+ <p class="muted">Use <code>/api/install-claude/status</code> for the full CLI inventory payload.</p>
17727
+ </section>
17728
+
17729
+ <section class="card" style="margin-top:16px">
17730
+ <h2>Self Test</h2>
17731
+ <p class="muted">Use <code>/api/self-test</code> for the full smoke-check payload.</p>
17732
+ </section>
17733
+
17357
17734
  <section class="card" style="margin-top:16px">
17358
17735
  <h2>Manifest</h2>
17359
- <pre>${escapeHtml(JSON.stringify(manifest, null, 2))}</pre>
17736
+ <pre id="manifest-json">${escapeHtml(JSON.stringify(manifest, null, 2))}</pre>
17360
17737
  </section>
17361
17738
  </main>
17739
+ <script>
17740
+ // Auto-refresh dashboard data every 15s
17741
+ const REFRESH_INTERVAL = 15000;
17742
+ async function refreshData() {
17743
+ try {
17744
+ const [statusRes, doctorRes] = await Promise.all([
17745
+ fetch("/api/status"),
17746
+ fetch("/api/doctor"),
17747
+ ]);
17748
+ const status = await statusRes.json();
17749
+ const doctor = await doctorRes.json();
17750
+
17751
+ // Update stat cards
17752
+ const stats = document.querySelectorAll(".stat");
17753
+ if (stats[0]) stats[0].textContent = status.manifestMachineCount;
17754
+ if (stats[1]) stats[1].textContent = status.heartbeatCount;
17755
+
17756
+ // Update machine table
17757
+ const tbody = document.querySelector("tbody");
17758
+ if (tbody && status.machines) {
17759
+ tbody.innerHTML = status.machines
17760
+ .map((m) =>
17761
+ "<tr>" +
17762
+ "<td><code>" + m.machineId + "</code></td>" +
17763
+ "<td>" + (m.platform || "unknown") + "</td>" +
17764
+ '<td><span class="badge ' + m.heartbeatStatus + '">' + m.heartbeatStatus + '</span></td>' +
17765
+ "<td>" + (m.lastHeartbeatAt || "\\u2014") + "</td>" +
17766
+ "</tr>"
17767
+ )
17768
+ .join("");
17769
+ }
17770
+
17771
+ // Update doctor table
17772
+ const doctorTbody = document.getElementById("doctor-tbody");
17773
+ if (doctorTbody && doctor.checks) {
17774
+ doctorTbody.innerHTML = doctor.checks
17775
+ .map((c) =>
17776
+ "<tr>" +
17777
+ "<td>" + c.summary + "</td>" +
17778
+ '<td><span class="badge ' + c.status + '">' + c.status + '</span></td>' +
17779
+ '<td class="muted">' + c.detail + "</td>" +
17780
+ "</tr>"
17781
+ )
17782
+ .join("");
17783
+ }
17784
+
17785
+ // Update timestamp
17786
+ document.getElementById("last-updated").textContent =
17787
+ "updated " + new Date().toLocaleTimeString();
17788
+ } catch (e) {
17789
+ // Silently ignore fetch errors during page unload
17790
+ }
17791
+ }
17792
+ document.getElementById("last-updated").textContent =
17793
+ "updated " + new Date().toLocaleTimeString();
17794
+ setInterval(refreshData, REFRESH_INTERVAL);
17795
+ </script>
17362
17796
  </body>
17363
17797
  </html>`;
17364
17798
  }
17799
+ async function parseJsonBody(request) {
17800
+ try {
17801
+ return await request.json();
17802
+ } catch {
17803
+ return {};
17804
+ }
17805
+ }
17806
+ function jsonError(message, status = 400) {
17807
+ return Response.json({ error: message }, { status });
17808
+ }
17365
17809
  function startDashboardServer(options = {}) {
17366
17810
  const info = getServeInfo(options);
17367
17811
  return Bun.serve({
17368
17812
  hostname: info.host,
17369
17813
  port: info.port,
17370
- fetch(request) {
17814
+ async fetch(request) {
17371
17815
  const url = new URL(request.url);
17816
+ const machineId = url.searchParams.get("machine") || undefined;
17817
+ const tools = url.searchParams.get("tools")?.split(",").map((value) => value.trim()).filter(Boolean);
17372
17818
  if (url.pathname === "/health") {
17373
17819
  return Response.json({ ok: true, ...getServeInfo(options) });
17374
17820
  }
@@ -17381,6 +17827,43 @@ function startDashboardServer(options = {}) {
17381
17827
  if (url.pathname === "/api/notifications") {
17382
17828
  return Response.json(listNotificationChannels());
17383
17829
  }
17830
+ if (url.pathname === "/api/doctor") {
17831
+ return Response.json(runDoctor(machineId));
17832
+ }
17833
+ if (url.pathname === "/api/self-test") {
17834
+ return Response.json(runSelfTest());
17835
+ }
17836
+ if (url.pathname === "/api/apps/status") {
17837
+ return Response.json(getAppsStatus(machineId));
17838
+ }
17839
+ if (url.pathname === "/api/apps/diff") {
17840
+ return Response.json(diffApps(machineId));
17841
+ }
17842
+ if (url.pathname === "/api/install-claude/status") {
17843
+ return Response.json(getClaudeCliStatus(machineId, tools));
17844
+ }
17845
+ if (url.pathname === "/api/install-claude/diff") {
17846
+ return Response.json(diffClaudeCli(machineId, tools));
17847
+ }
17848
+ if (url.pathname === "/api/notifications/test") {
17849
+ if (request.method !== "POST") {
17850
+ return jsonError("Use POST for notification tests.", 405);
17851
+ }
17852
+ const body = await parseJsonBody(request);
17853
+ const channelId = typeof body["channelId"] === "string" ? body["channelId"] : undefined;
17854
+ if (!channelId) {
17855
+ return jsonError("channelId is required.");
17856
+ }
17857
+ const event = typeof body["event"] === "string" ? body["event"] : undefined;
17858
+ const message = typeof body["message"] === "string" ? body["message"] : undefined;
17859
+ const apply = body["apply"] === true;
17860
+ const yes = body["yes"] === true;
17861
+ try {
17862
+ return Response.json(await testNotificationChannel(channelId, event, message, { apply, yes }));
17863
+ } catch (error) {
17864
+ return jsonError(error instanceof Error ? error.message : String(error));
17865
+ }
17866
+ }
17384
17867
  return new Response(renderDashboardHtml(), {
17385
17868
  headers: {
17386
17869
  "content-type": "text/html; charset=utf-8"
@@ -17390,11 +17873,279 @@ function startDashboardServer(options = {}) {
17390
17873
  });
17391
17874
  }
17392
17875
 
17876
+ // src/commands/self-test.ts
17877
+ function check(id, status, summary, detail) {
17878
+ return { id, status, summary, detail };
17879
+ }
17880
+ function runSelfTest() {
17881
+ const version = getPackageVersion();
17882
+ const status = getStatus();
17883
+ const doctor = runDoctor();
17884
+ const serveInfo = getServeInfo();
17885
+ const html = renderDashboardHtml();
17886
+ const notifications = listNotificationChannels();
17887
+ const apps = listApps(status.machineId);
17888
+ const appsDiff = diffApps(status.machineId);
17889
+ const cliPlan = buildClaudeInstallPlan(status.machineId);
17890
+ return {
17891
+ machineId: getLocalMachineId(),
17892
+ checks: [
17893
+ check("package-version", version === "0.0.0" ? "fail" : "ok", "Package version resolves", version),
17894
+ check("status", "ok", "Status loads", JSON.stringify({ machines: status.manifestMachineCount, heartbeats: status.heartbeatCount })),
17895
+ check("doctor", doctor.checks.some((entry) => entry.status === "fail") ? "warn" : "ok", "Doctor completed", `${doctor.checks.length} checks`),
17896
+ check("serve-info", "ok", "Dashboard info renders", `${serveInfo.url} routes=${serveInfo.routes.length}`),
17897
+ check("dashboard-html", html.includes("Machines Dashboard") ? "ok" : "fail", "Dashboard HTML renders", html.slice(0, 80)),
17898
+ check("notifications", "ok", "Notifications config loads", `${notifications.channels.length} channels`),
17899
+ check("apps", "ok", "Apps manifest loads", `${apps.apps.length} apps`),
17900
+ check("apps-diff", appsDiff.missing.length === 0 ? "ok" : "warn", "Apps diff computed", `missing=${appsDiff.missing.length} installed=${appsDiff.installed.length}`),
17901
+ check("install-claude-plan", cliPlan.steps.length > 0 ? "ok" : "warn", "Install plan renders", `${cliPlan.steps.length} steps`)
17902
+ ]
17903
+ };
17904
+ }
17905
+
17906
+ // src/commands/clipboard.ts
17907
+ import { createHash } from "crypto";
17908
+ import { existsSync as existsSync8, readFileSync as readFileSync7, rmSync, writeFileSync as writeFileSync5 } from "fs";
17909
+ import { join as join9 } from "path";
17910
+ var DEFAULT_CONFIG = {
17911
+ version: 1,
17912
+ enabled: true,
17913
+ port: 19452,
17914
+ maxHistory: 500,
17915
+ maxSizeBytes: 10 * 1024 * 1024,
17916
+ skipPatterns: [
17917
+ "password",
17918
+ "secret",
17919
+ "token",
17920
+ "-----BEGIN",
17921
+ "AKIA"
17922
+ ]
17923
+ };
17924
+ function resolveConfigPath(configPath) {
17925
+ if (configPath)
17926
+ return configPath;
17927
+ return join9(getDataDir(), "clipboard-config.json");
17928
+ }
17929
+ function resolveHistoryPath(historyPath) {
17930
+ if (historyPath)
17931
+ return historyPath;
17932
+ return getClipboardHistoryPath();
17933
+ }
17934
+ function getDefaultConfig() {
17935
+ return { ...DEFAULT_CONFIG, skipPatterns: [...DEFAULT_CONFIG.skipPatterns] };
17936
+ }
17937
+ function readConfig(configPath) {
17938
+ const path = resolveConfigPath(configPath);
17939
+ if (!existsSync8(path)) {
17940
+ return getDefaultConfig();
17941
+ }
17942
+ const parsed = JSON.parse(readFileSync7(path, "utf8"));
17943
+ return { ...getDefaultConfig(), ...parsed };
17944
+ }
17945
+ function writeConfig(config, configPath) {
17946
+ const path = resolveConfigPath(configPath);
17947
+ ensureParentDir(path);
17948
+ writeFileSync5(path, `${JSON.stringify(config, null, 2)}
17949
+ `, "utf8");
17950
+ }
17951
+ function readHistory(historyPath) {
17952
+ const path = resolveHistoryPath(historyPath);
17953
+ if (!existsSync8(path)) {
17954
+ return [];
17955
+ }
17956
+ try {
17957
+ return JSON.parse(readFileSync7(path, "utf8"));
17958
+ } catch {
17959
+ return [];
17960
+ }
17961
+ }
17962
+ function getOrCreateClipboardKey() {
17963
+ const keyPath = getClipboardKeyPath();
17964
+ if (existsSync8(keyPath)) {
17965
+ return readFileSync7(keyPath, "utf8").trim();
17966
+ }
17967
+ const key = createHash("sha256").update(crypto.randomUUID()).digest("hex").slice(0, 32);
17968
+ ensureParentDir(keyPath);
17969
+ writeFileSync5(keyPath, `${key}
17970
+ `, "utf8");
17971
+ return key;
17972
+ }
17973
+ function getDefaultClipboardConfig() {
17974
+ return getDefaultConfig();
17975
+ }
17976
+ function getConfigPath2(configPath) {
17977
+ return resolveConfigPath(configPath);
17978
+ }
17979
+ function readClipboardConfig(configPath) {
17980
+ return readConfig(configPath);
17981
+ }
17982
+ function writeClipboardConfig(config, configPath) {
17983
+ writeConfig(config, configPath);
17984
+ }
17985
+ function readClipboardHistory(historyPath) {
17986
+ return readHistory(historyPath);
17987
+ }
17988
+ function clearClipboardHistory(historyPath) {
17989
+ const path = resolveHistoryPath(historyPath);
17990
+ if (existsSync8(path)) {
17991
+ rmSync(path);
17992
+ }
17993
+ }
17994
+ function getClipboardStatus(historyPath) {
17995
+ const history = readHistory(historyPath);
17996
+ const config = readConfig();
17997
+ return {
17998
+ running: false,
17999
+ port: config.port,
18000
+ historyCount: history.length
18001
+ };
18002
+ }
18003
+
18004
+ // src/cli-utils.ts
18005
+ function parseIntegerOption(value, label, constraints = {}) {
18006
+ const parsed = Number.parseInt(value, 10);
18007
+ const { min, max } = constraints;
18008
+ if (!Number.isFinite(parsed) || !/^-?\d+$/.test(value.trim())) {
18009
+ throw new Error(`Invalid value for ${label}: ${value}`);
18010
+ }
18011
+ if (min !== undefined && parsed < min) {
18012
+ throw new Error(`Invalid value for ${label}: ${value}. Expected >= ${min}.`);
18013
+ }
18014
+ if (max !== undefined && parsed > max) {
18015
+ throw new Error(`Invalid value for ${label}: ${value}. Expected <= ${max}.`);
18016
+ }
18017
+ return parsed;
18018
+ }
18019
+ function renderKeyValueTable(entries) {
18020
+ const width = entries.reduce((max, [key]) => Math.max(max, key.length), 0);
18021
+ return entries.map(([key, value]) => `${key.padEnd(width)} ${value}`).join(`
18022
+ `);
18023
+ }
18024
+ function renderList(title, items) {
18025
+ if (items.length === 0) {
18026
+ return `${title}: none`;
18027
+ }
18028
+ return `${title}:
18029
+ ${items.map((item) => `- ${item}`).join(`
18030
+ `)}`;
18031
+ }
18032
+
17393
18033
  // src/cli/index.ts
18034
+ import { rmSync as rmSync2 } from "fs";
18035
+ import { readFileSync as readFileSync8 } from "fs";
17394
18036
  var program2 = new Command;
17395
- program2.name("machines").description("Machine fleet management CLI + MCP for developers").version(getPackageVersion());
18037
+ function printJsonOrText(data, text, json = false) {
18038
+ if (json || program2.opts().quiet) {
18039
+ console.log(JSON.stringify(data, null, 2));
18040
+ return;
18041
+ }
18042
+ console.log(text);
18043
+ }
18044
+ function renderAppsListResult(result) {
18045
+ return [
18046
+ `machine: ${result.machineId}`,
18047
+ renderList("apps", result.apps.map((app) => `${app.name}${app.manager ? ` (${app.manager})` : ""}`))
18048
+ ].join(`
18049
+ `);
18050
+ }
18051
+ function renderAppsStatusResult(result) {
18052
+ const lines = result.apps.map((app) => {
18053
+ const state = app.installed ? source_default.green("installed") : source_default.yellow("missing");
18054
+ return `${app.name.padEnd(18)} ${state} ${app.version ? `v${app.version}` : ""}`.trimEnd();
18055
+ });
18056
+ return [`machine: ${result.machineId} (${result.source})`, ...lines].join(`
18057
+ `);
18058
+ }
18059
+ function renderAppsDiffResult(result) {
18060
+ return [
18061
+ `machine: ${result.machineId} (${result.source})`,
18062
+ renderList("missing", result.missing),
18063
+ renderList("installed", result.installed)
18064
+ ].join(`
18065
+ `);
18066
+ }
18067
+ function renderClaudeStatusResult(result) {
18068
+ const lines = result.tools.map((tool) => {
18069
+ const state = tool.installed ? source_default.green("installed") : source_default.yellow("missing");
18070
+ return `${tool.tool.padEnd(8)} ${state} ${tool.version || ""}`.trimEnd();
18071
+ });
18072
+ return [`machine: ${result.machineId} (${result.source})`, ...lines].join(`
18073
+ `);
18074
+ }
18075
+ function renderClaudeDiffResult(result) {
18076
+ return [
18077
+ `machine: ${result.machineId} (${result.source})`,
18078
+ renderList("missing", result.missing),
18079
+ renderList("installed", result.installed)
18080
+ ].join(`
18081
+ `);
18082
+ }
18083
+ function renderNotificationConfigResult(config) {
18084
+ if (config.channels.length === 0) {
18085
+ return "notification channels: none";
18086
+ }
18087
+ return config.channels.map((channel) => `${channel.id} ${channel.enabled ? source_default.green("enabled") : source_default.yellow("disabled")} ${channel.type} -> ${channel.target}`).join(`
18088
+ `);
18089
+ }
18090
+ function renderNotificationTestResult(result) {
18091
+ return renderKeyValueTable([
18092
+ ["channel", result.channelId],
18093
+ ["mode", result.mode],
18094
+ ["delivered", String(result.delivered)],
18095
+ ["detail", result.detail],
18096
+ ["preview", result.preview]
18097
+ ]);
18098
+ }
18099
+ function renderNotificationDispatchResult(result) {
18100
+ return [
18101
+ `event: ${result.event}`,
18102
+ `message: ${result.message}`,
18103
+ ...result.deliveries.map((delivery) => `${delivery.channelId} ${delivery.delivered ? source_default.green("delivered") : source_default.red("failed")} ${delivery.transport} ${delivery.detail}`)
18104
+ ].join(`
18105
+ `);
18106
+ }
18107
+ function renderDoctorResult(report) {
18108
+ const header = `machine: ${report.machineId} (${report.source})`;
18109
+ const lines = report.checks.map((check2) => {
18110
+ const status = check2.status === "ok" ? source_default.green(check2.status) : check2.status === "warn" ? source_default.yellow(check2.status) : source_default.red(check2.status);
18111
+ return `${check2.id.padEnd(20)} ${status} ${check2.detail}`;
18112
+ });
18113
+ return [header, ...lines].join(`
18114
+ `);
18115
+ }
18116
+ function renderSelfTestResult(result) {
18117
+ return [
18118
+ `machine: ${result.machineId}`,
18119
+ ...result.checks.map((check2) => {
18120
+ const status = check2.status === "ok" ? source_default.green(check2.status) : check2.status === "warn" ? source_default.yellow(check2.status) : source_default.red(check2.status);
18121
+ return `${check2.id.padEnd(20)} ${status} ${check2.detail}`;
18122
+ })
18123
+ ].join(`
18124
+ `);
18125
+ }
18126
+ function renderFleetStatus(status) {
18127
+ return [
18128
+ renderKeyValueTable([
18129
+ ["machine", status.machineId],
18130
+ ["manifest", status.manifestPath],
18131
+ ["db", status.dbPath],
18132
+ ["notifications", status.notificationsPath],
18133
+ ["manifest machines", String(status.manifestMachineCount)],
18134
+ ["heartbeats", String(status.heartbeatCount)],
18135
+ ["setup runs", String(status.recentSetupRuns)],
18136
+ ["sync runs", String(status.recentSyncRuns)]
18137
+ ]),
18138
+ "",
18139
+ ...status.machines.map((machine) => `${machine.machineId.padEnd(18)} ${machine.platform || "unknown"} ${machine.heartbeatStatus} ${machine.lastHeartbeatAt || "\u2014"}`)
18140
+ ].join(`
18141
+ `);
18142
+ }
18143
+ program2.name("machines").description("Machine fleet management CLI + MCP for developers").version(getPackageVersion()).option("-q, --quiet", "Suppress non-essential output");
17396
18144
  var manifestCommand = program2.command("manifest").description("Manage the fleet manifest");
17397
18145
  var appsCommand = program2.command("apps").description("Manage installed applications per machine");
18146
+ var notificationsCommand = program2.command("notifications").description("Manage fleet alert delivery channels");
18147
+ var clipboardCommand = program2.command("clipboard").description("Real-time clipboard sync across fleet machines");
18148
+ var installClaudeCommand = program2.command("install-claude").description("Install or inspect Claude, Codex, and Gemini CLIs");
17398
18149
  manifestCommand.command("init").description("Create an empty fleet manifest").action(() => {
17399
18150
  console.log(manifestInit());
17400
18151
  });
@@ -17422,16 +18173,22 @@ manifestCommand.command("get").description("Print a single machine from the mani
17422
18173
  manifestCommand.command("remove").description("Remove a machine from the manifest").argument("<id>", "Machine identifier").action((id) => {
17423
18174
  console.log(JSON.stringify(manifestRemove(id), null, 2));
17424
18175
  });
17425
- manifestCommand.command("add").description("Add or replace a machine in the fleet manifest").requiredOption("--id <id>", "Machine identifier").requiredOption("--platform <platform>", "linux | macos | windows").requiredOption("--workspace-path <path>", "Primary workspace path").option("--hostname <hostname>", "Machine hostname").option("--ssh-address <sshAddress>", "Machine SSH address").option("--tailscale-name <tailscaleName>", "Machine Tailscale DNS name").option("--connection <connection>", "local | ssh | tailscale").option("--bun-path <path>", "Bun executable directory").option("--tag <tag...>", "Machine tags").option("--package <name...>", "Desired packages").option("--app <spec...>", "Desired apps as name[:manager[:packageName]]").option("--file <spec...>", "File sync spec source:target[:copy|symlink]").action((options) => {
18176
+ manifestCommand.command("add").description("Add or replace a machine in the fleet manifest").option("--id <id>", "Machine identifier").option("--platform <platform>", "linux | macos | windows").option("--workspace-path <path>", "Primary workspace path").option("--hostname <hostname>", "Machine hostname").option("--ssh-address <sshAddress>", "Machine SSH address").option("--tailscale-name <tailscaleName>", "Machine Tailscale DNS name").option("--connection <connection>", "local | ssh | tailscale").option("--bun-path <path>", "Bun executable directory").option("--tag <tag...>", "Machine tags").option("--package <name...>", "Desired packages").option("--app <spec...>", "Desired apps as name[:manager[:packageName]]").option("--file <spec...>", "File sync spec source:target[:copy|symlink]").option("--metadata <json>", "Machine metadata as JSON").option("--from-stdin", "Read the full MachineManifest JSON from stdin").action((options) => {
18177
+ if (options["from-stdin"]) {
18178
+ if (process.stdin.isTTY) {
18179
+ console.error("error: --from-stdin requires piped input");
18180
+ process.exit(1);
18181
+ }
18182
+ const input = readFileSync8(0, "utf8");
18183
+ const machine2 = JSON.parse(input);
18184
+ console.log(JSON.stringify(manifestAdd(machine2), null, 2));
18185
+ return;
18186
+ }
17426
18187
  const packages = Array.isArray(options["package"]) ? options["package"].map((name) => ({ name: String(name) })) : undefined;
17427
18188
  const files = Array.isArray(options["file"]) ? options["file"].map((value) => {
17428
18189
  const [source, target, mode] = String(value).split(":");
17429
18190
  const normalizedMode = mode === "symlink" ? "symlink" : mode === "copy" ? "copy" : undefined;
17430
- return {
17431
- source,
17432
- target,
17433
- mode: normalizedMode
17434
- };
18191
+ return { source, target, mode: normalizedMode };
17435
18192
  }) : undefined;
17436
18193
  const apps = Array.isArray(options["app"]) ? options["app"].map((value) => {
17437
18194
  const [name, manager, packageName] = String(value).split(":");
@@ -17441,6 +18198,7 @@ manifestCommand.command("add").description("Add or replace a machine in the flee
17441
18198
  packageName
17442
18199
  };
17443
18200
  }) : undefined;
18201
+ const metadata = typeof options["metadata"] === "string" ? JSON.parse(options["metadata"]) : undefined;
17444
18202
  const machine = {
17445
18203
  id: String(options["id"]),
17446
18204
  hostname: options["hostname"] ? String(options["hostname"]) : undefined,
@@ -17451,6 +18209,7 @@ manifestCommand.command("add").description("Add or replace a machine in the flee
17451
18209
  workspacePath: String(options["workspacePath"]),
17452
18210
  bunPath: options["bunPath"] ? String(options["bunPath"]) : undefined,
17453
18211
  tags: Array.isArray(options["tag"]) ? options["tag"].map(String) : undefined,
18212
+ metadata,
17454
18213
  packages,
17455
18214
  apps,
17456
18215
  files
@@ -17459,65 +18218,55 @@ manifestCommand.command("add").description("Add or replace a machine in the flee
17459
18218
  });
17460
18219
  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
18220
  const result = listApps(options.machine);
17462
- const rendered = JSON.stringify(result, null, 2);
17463
- console.log(options.json ? rendered : rendered);
18221
+ printJsonOrText(result, renderAppsListResult(result), options.json);
18222
+ });
18223
+ 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) => {
18224
+ const result = getAppsStatus(options.machine);
18225
+ printJsonOrText(result, renderAppsStatusResult(result), options.json);
18226
+ });
18227
+ 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) => {
18228
+ const result = diffApps(options.machine);
18229
+ printJsonOrText(result, renderAppsDiffResult(result), options.json);
17464
18230
  });
17465
- 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) => {
18231
+ appsCommand.command("plan").description("Preview app install steps for a machine").option("--machine <id>", "Machine identifier").action((options) => {
17466
18232
  const result = buildAppsPlan(options.machine);
17467
- const rendered = JSON.stringify(result, null, 2);
17468
- console.log(options.json ? rendered : rendered);
18233
+ console.log(JSON.stringify(result, null, 2));
17469
18234
  });
17470
- 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) => {
18235
+ appsCommand.command("apply").description("Install manifest-managed apps for a machine").option("--machine <id>", "Machine identifier").option("--yes", "Confirm execution", false).action((options) => {
17471
18236
  const result = runAppsInstall(options.machine, { apply: true, yes: options.yes });
17472
- const rendered = JSON.stringify(result, null, 2);
17473
- console.log(options.json ? rendered : rendered);
18237
+ console.log(JSON.stringify(result, null, 2));
17474
18238
  });
17475
18239
  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
18240
  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
18241
  console.log(JSON.stringify(result, null, 2));
17482
18242
  });
17483
18243
  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
18244
  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
18245
  console.log(JSON.stringify(result, null, 2));
17490
18246
  });
17491
18247
  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
18248
  const result = diffMachines(options.left, options.right);
17493
- if (options.json) {
17494
- console.log(JSON.stringify(result, null, 2));
17495
- return;
17496
- }
17497
18249
  console.log(JSON.stringify(result, null, 2));
17498
18250
  });
17499
18251
  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
18252
  const result = options.apply ? runBackup(options.bucket, options.prefix, { apply: true, yes: options.yes }) : buildBackupPlan(options.bucket, options.prefix);
17501
- const rendered = JSON.stringify(result, null, 2);
17502
- console.log(options.json ? rendered : rendered);
18253
+ console.log(JSON.stringify(result, null, 2));
17503
18254
  });
17504
18255
  var certCommand = program2.command("cert").description("Manage mkcert-based local SSL certificates");
17505
18256
  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
18257
  const result = options.apply ? runCertPlan(domains, { apply: true, yes: options.yes }) : buildCertPlan(domains);
17507
- const rendered = JSON.stringify(result, null, 2);
17508
- console.log(options.json ? rendered : rendered);
18258
+ console.log(JSON.stringify(result, null, 2));
17509
18259
  });
17510
18260
  var dnsCommand = program2.command("dns").description("Manage local domain mappings");
17511
- var notificationsCommand = program2.command("notifications").description("Manage fleet alert delivery channels");
17512
18261
  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, Number.parseInt(options.port, 10), options.targetHost);
17514
- const rendered = JSON.stringify(result, null, 2);
17515
- console.log(options.json ? rendered : rendered);
18262
+ const result = addDomainMapping(options.domain, parseIntegerOption(options.port, "port", { min: 1, max: 65535 }), options.targetHost);
18263
+ console.log(JSON.stringify(result, null, 2));
17516
18264
  });
17517
- dnsCommand.command("list").description("List saved local domain mappings").option("-j, --json", "Print JSON output", false).action((options) => {
17518
- const result = listDomainMappings();
17519
- const rendered = JSON.stringify(result, null, 2);
17520
- console.log(options.json ? rendered : rendered);
18265
+ dnsCommand.command("list").description("List saved local domain mappings").option("-j, --json", "Print JSON output", false).action(() => {
18266
+ console.log(JSON.stringify(listDomainMappings(), null, 2));
18267
+ });
18268
+ dnsCommand.command("render").description("Render hosts/proxy configuration for a domain").argument("<domain>", "Domain name").option("-j, --json", "Print JSON output", false).action((domain) => {
18269
+ console.log(JSON.stringify(renderDomainMapping(domain), null, 2));
17521
18270
  });
17522
18271
  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
18272
  const result = addNotificationChannel({
@@ -17527,41 +18276,107 @@ notificationsCommand.command("add").description("Add or replace a notification c
17527
18276
  events: options.event,
17528
18277
  enabled: !options.disabled
17529
18278
  });
17530
- const rendered = JSON.stringify(result, null, 2);
17531
- console.log(options.json ? rendered : rendered);
18279
+ printJsonOrText(result, renderNotificationConfigResult(result), options.json);
17532
18280
  });
17533
18281
  notificationsCommand.command("list").description("List configured notification channels").option("-j, --json", "Print JSON output", false).action((options) => {
17534
18282
  const result = listNotificationChannels();
17535
- const rendered = JSON.stringify(result, null, 2);
17536
- console.log(options.json ? rendered : rendered);
18283
+ printJsonOrText(result, renderNotificationConfigResult(result), options.json);
17537
18284
  });
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, {
18285
+ 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) => {
18286
+ const result = await testNotificationChannel(options.channel, options.event, options.message, {
17540
18287
  apply: options.apply,
17541
18288
  yes: options.yes
17542
18289
  });
17543
- const rendered = JSON.stringify(result, null, 2);
17544
- console.log(options.json ? rendered : rendered);
18290
+ printJsonOrText(result, renderNotificationTestResult(result), options.json);
18291
+ });
18292
+ 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) => {
18293
+ const result = await dispatchNotificationEvent(options.event, options.message, { channelId: options.channel });
18294
+ printJsonOrText(result, renderNotificationDispatchResult(result), options.json);
17545
18295
  });
17546
18296
  notificationsCommand.command("remove").description("Remove a notification channel").argument("<id>", "Channel identifier").option("-j, --json", "Print JSON output", false).action((id, options) => {
17547
18297
  const result = removeNotificationChannel(id);
17548
- const rendered = JSON.stringify(result, null, 2);
17549
- console.log(options.json ? rendered : rendered);
18298
+ printJsonOrText(result, renderNotificationConfigResult(result), options.json);
18299
+ });
18300
+ clipboardCommand.command("init").description("Initialize clipboard sync (generate shared secret)").option("-j, --json", "Print JSON output", false).action((options) => {
18301
+ const key = getOrCreateClipboardKey();
18302
+ const config = getDefaultClipboardConfig();
18303
+ writeClipboardConfig(config);
18304
+ const result = { keyPath: getClipboardKeyPath(), key, configPath: getConfigPath2(), config };
18305
+ printJsonOrText(result, `clipboard initialized
18306
+ key: ${key}
18307
+ port: ${config.port}`, options.json);
17550
18308
  });
17551
- dnsCommand.command("render").description("Render hosts/proxy configuration for a domain").argument("<domain>", "Domain name").option("-j, --json", "Print JSON output", false).action((domain, options) => {
17552
- const result = renderDomainMapping(domain);
17553
- const rendered = JSON.stringify(result, null, 2);
17554
- console.log(options.json ? rendered : rendered);
18309
+ clipboardCommand.command("status").description("Check clipboard sync status").option("-j, --json", "Print JSON output", false).action((options) => {
18310
+ const status = getClipboardStatus();
18311
+ const config = readClipboardConfig();
18312
+ const result = { ...status, enabled: config.enabled };
18313
+ printJsonOrText(result, `clipboard sync ${config.enabled ? source_default.green("enabled") : source_default.yellow("disabled")} (port ${status.port}, ${status.historyCount} entries)`, options.json);
17555
18314
  });
17556
- program2.command("install-claude").description("Install or update Claude, Codex, and Gemini CLIs across the fleet").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
- const result = options.apply ? runClaudeInstall(options.machine, options.tool, { apply: true, yes: options.yes }) : buildClaudeInstallPlan(options.machine, options.tool);
17558
- const rendered = JSON.stringify(result, null, 2);
17559
- console.log(options.json ? rendered : rendered);
18315
+ clipboardCommand.command("config").description("View or set clipboard sync config").option("--set <json>", "Set config values as JSON").option("-j, --json", "Print JSON output", false).action((options) => {
18316
+ if (options.set) {
18317
+ const partial = JSON.parse(options.set);
18318
+ const config2 = { ...readClipboardConfig(), ...partial };
18319
+ writeClipboardConfig(config2);
18320
+ }
18321
+ const config = readClipboardConfig();
18322
+ printJsonOrText(config, renderKeyValueTable([
18323
+ ["enabled", String(config.enabled)],
18324
+ ["port", String(config.port)],
18325
+ ["maxHistory", String(config.maxHistory)],
18326
+ ["maxSizeBytes", `${config.maxSizeBytes} bytes`],
18327
+ ["skipPatterns", config.skipPatterns.join(", ")]
18328
+ ]), options.json);
18329
+ });
18330
+ clipboardCommand.command("history").description("Show clipboard sync history").option("-j, --json", "Print JSON output", false).option("--limit <n>", "Show only the last N entries", "20").action((options) => {
18331
+ const limit = parseIntegerOption(options.limit, "limit", { min: 1, max: 100 });
18332
+ const entries = readClipboardHistory().slice(0, limit);
18333
+ if (options.json) {
18334
+ console.log(JSON.stringify(entries, null, 2));
18335
+ return;
18336
+ }
18337
+ if (entries.length === 0) {
18338
+ console.log("clipboard history: empty");
18339
+ return;
18340
+ }
18341
+ for (const entry of entries) {
18342
+ const preview = entry.content.length > 80 ? `${entry.content.slice(0, 80)}...` : entry.content;
18343
+ console.log(`${source_default.dim(entry.timestamp)} ${entry.sourceMachine.padEnd(12)} ${entry.contentType.padEnd(5)} ${preview.replace(/\n/g, " ")}`);
18344
+ }
18345
+ });
18346
+ clipboardCommand.command("clear-history").description("Clear clipboard sync history").option("--yes", "Confirm without prompt", false).action((options) => {
18347
+ if (!options.yes) {
18348
+ console.error("error: this command requires --yes");
18349
+ process.exit(1);
18350
+ }
18351
+ clearClipboardHistory();
18352
+ console.log("clipboard history cleared");
18353
+ });
18354
+ clipboardCommand.command("key").description("Show or rotate the shared secret key").option("--rotate", "Generate a new key", false).option("-j, --json", "Print JSON output", false).action((options) => {
18355
+ if (options.rotate) {
18356
+ rmSync2(getClipboardKeyPath(), { force: true });
18357
+ }
18358
+ const key = getOrCreateClipboardKey();
18359
+ printJsonOrText({ key }, key, options.json);
18360
+ });
18361
+ 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) => {
18362
+ const result = getClaudeCliStatus(options.machine, options.tool);
18363
+ printJsonOrText(result, renderClaudeStatusResult(result), options.json);
18364
+ });
18365
+ 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) => {
18366
+ const result = diffClaudeCli(options.machine, options.tool);
18367
+ printJsonOrText(result, renderClaudeDiffResult(result), options.json);
18368
+ });
18369
+ 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) => {
18370
+ const result = buildClaudeInstallPlan(options.machine, options.tool);
18371
+ console.log(JSON.stringify(result, null, 2));
18372
+ });
18373
+ 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) => {
18374
+ const result = runClaudeInstall(options.machine, options.tool, { apply: true, yes: options.yes });
18375
+ console.log(JSON.stringify(result, null, 2));
17560
18376
  });
17561
18377
  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
18378
  const result = options.apply ? runTailscaleInstall(options.machine, { apply: true, yes: options.yes }) : buildTailscaleInstallPlan(options.machine);
17563
- const rendered = JSON.stringify(result, null, 2);
17564
- console.log(options.json ? rendered : rendered);
18379
+ console.log(JSON.stringify(result, null, 2));
17565
18380
  });
17566
18381
  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
18382
  if (options.json) {
@@ -17572,27 +18387,27 @@ program2.command("ssh").description("Choose the best SSH route for a machine").r
17572
18387
  });
17573
18388
  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
18389
  const result = listPorts(options.machine);
17575
- const rendered = JSON.stringify(result, null, 2);
17576
- console.log(options.json ? rendered : rendered);
18390
+ console.log(JSON.stringify(result, null, 2));
17577
18391
  });
17578
18392
  program2.command("status").description("Print local machine and storage status").option("-j, --json", "Print JSON output", false).action((options) => {
17579
18393
  const status = getStatus();
17580
- const rendered = JSON.stringify(status, null, 2);
17581
- console.log(options.json ? rendered : source_default.cyan(rendered));
18394
+ printJsonOrText(status, renderFleetStatus(status), options.json);
18395
+ });
18396
+ program2.command("doctor").description("Run machine preflight checks").option("--machine <id>", "Machine identifier").option("-j, --json", "Print JSON output", false).action((options) => {
18397
+ const result = runDoctor(options.machine);
18398
+ printJsonOrText(result, renderDoctorResult(result), options.json);
18399
+ });
18400
+ program2.command("self-test").description("Run local package smoke checks").option("-j, --json", "Print JSON output", false).action((options) => {
18401
+ const result = runSelfTest();
18402
+ printJsonOrText(result, renderSelfTestResult(result), options.json);
17582
18403
  });
17583
18404
  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
- });
18405
+ const info = getServeInfo({ host: options.host, port: parseIntegerOption(options.port, "port", { min: 1, max: 65535 }) });
17588
18406
  if (options.json) {
17589
18407
  console.log(JSON.stringify(info, null, 2));
17590
18408
  return;
17591
18409
  }
17592
- const server = startDashboardServer({
17593
- host: info.host,
17594
- port: info.port
17595
- });
18410
+ const server = startDashboardServer({ host: info.host, port: info.port });
17596
18411
  console.log(source_default.green(`machines dashboard listening on http://${server.hostname}:${server.port}`));
17597
18412
  });
17598
18413
  await program2.parseAsync(process.argv);