svamp-cli 0.2.224 → 0.2.225

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.
@@ -1,356 +0,0 @@
1
- import { existsSync, readFileSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import os from 'node:os';
4
- import { c as connectToHypha } from './run-C-9ZtEi4.mjs';
5
- import { PINNED_CLAUDE_CODE_VERSION } from './pinnedClaudeCode-VIupR1NK.mjs';
6
- import 'os';
7
- import 'fs/promises';
8
- import 'fs';
9
- import 'path';
10
- import 'url';
11
- import 'child_process';
12
- import 'crypto';
13
- import 'node:crypto';
14
- import 'node:child_process';
15
- import 'util';
16
- import 'node:events';
17
- import '@agentclientprotocol/sdk';
18
- import '@modelcontextprotocol/sdk/client/index.js';
19
- import '@modelcontextprotocol/sdk/client/stdio.js';
20
- import '@modelcontextprotocol/sdk/types.js';
21
- import 'zod';
22
- import 'node:fs/promises';
23
- import 'node:util';
24
-
25
- const SVAMP_HOME = process.env.SVAMP_HOME || join(os.homedir(), ".svamp");
26
- const DAEMON_STATE_FILE = join(SVAMP_HOME, "daemon.state.json");
27
- const ENV_FILE = join(SVAMP_HOME, ".env");
28
- function loadDotEnv() {
29
- if (!existsSync(ENV_FILE)) return;
30
- for (const raw of readFileSync(ENV_FILE, "utf-8").split("\n")) {
31
- const line = raw.trim();
32
- if (!line || line.startsWith("#")) continue;
33
- const eq = line.indexOf("=");
34
- if (eq === -1) continue;
35
- const k = line.slice(0, eq).trim();
36
- const v = line.slice(eq + 1).trim().replace(/^["']|["']$/g, "");
37
- if (!process.env[k]) process.env[k] = v;
38
- }
39
- }
40
- function readDaemonState() {
41
- if (!existsSync(DAEMON_STATE_FILE)) return null;
42
- try {
43
- return JSON.parse(readFileSync(DAEMON_STATE_FILE, "utf-8"));
44
- } catch {
45
- return null;
46
- }
47
- }
48
- function getSelfMachineId() {
49
- return readDaemonState()?.machineId || null;
50
- }
51
- function partitionSelf(machines, selfId) {
52
- if (!selfId) return { targets: machines, skipped: [] };
53
- return {
54
- targets: machines.filter((m) => m.machineId !== selfId),
55
- skipped: machines.filter((m) => m.machineId === selfId)
56
- };
57
- }
58
- function suppressHyphaLogs() {
59
- const ol = console.log, ow = console.warn, oi = console.info, oe = console.error;
60
- const sw = process.stdout.write.bind(process.stdout);
61
- const ew = process.stderr.write.bind(process.stderr);
62
- const isLog = (chunk) => typeof chunk === "string" && (chunk.includes("WebSocket connection") || chunk.includes("Connection established") || chunk.includes("registering service built-in") || chunk.includes("registered service") || chunk.includes("registered all") || chunk.includes("Subscribing to client_") || chunk.includes("subscribed to client_") || chunk.includes("subscribe to client_") || chunk.includes("Cleaning up all sessions") || chunk.includes("WebSocket connection disconnected") || chunk.includes("local RPC disconnection"));
63
- console.log = () => {
64
- };
65
- console.warn = () => {
66
- };
67
- console.info = () => {
68
- };
69
- console.error = (...args) => {
70
- if (args.some((a) => isLog(a))) return;
71
- oe(...args);
72
- };
73
- process.stdout.write = (c, ...a) => isLog(c) ? true : sw(c, ...a);
74
- process.stderr.write = (c, ...a) => isLog(c) ? true : ew(c, ...a);
75
- return () => {
76
- console.log = ol;
77
- console.warn = ow;
78
- console.info = oi;
79
- console.error = oe;
80
- process.stdout.write = sw;
81
- process.stderr.write = ew;
82
- };
83
- }
84
- function truncate(s, n) {
85
- if (!s) return s;
86
- return s.length <= n ? s : s.slice(0, Math.max(1, n - 1)) + "\u2026";
87
- }
88
- async function discoverMachines() {
89
- loadDotEnv();
90
- const state = readDaemonState();
91
- const serverUrl = process.env.HYPHA_SERVER_URL || state?.hyphaServerUrl;
92
- const token = process.env.HYPHA_TOKEN;
93
- if (!serverUrl) {
94
- console.error('No Hypha server URL. Run "svamp login <url>" first.');
95
- process.exit(1);
96
- }
97
- const restore = suppressHyphaLogs();
98
- let server;
99
- try {
100
- server = await connectToHypha({ serverUrl, token, name: "svamp-fleet-cli" });
101
- } catch (err) {
102
- restore();
103
- console.error(`Failed to connect to Hypha: ${err.message}`);
104
- process.exit(1);
105
- }
106
- const services = await server.listServices({ query: { type: "svamp-machine" }, include_unlisted: true, _rkwargs: true });
107
- restore();
108
- const machines = await Promise.all(services.map(async (svc) => {
109
- const serviceId = svc.id || svc.name;
110
- try {
111
- const rpc = await server.getService(serviceId);
112
- const info = await rpc.getMachineInfo();
113
- const label = info.metadata?.displayName || info.metadata?.host || info.metadata?.hostname || serviceId;
114
- return { serviceId, machineId: info.machineId || serviceId, label, rpc };
115
- } catch {
116
- return { serviceId, machineId: serviceId, label: "-", rpc: null };
117
- }
118
- }));
119
- return { server, machines };
120
- }
121
- async function fanOut(machines, fn) {
122
- return Promise.all(machines.map(async (m) => {
123
- if (!m.rpc) return { machine: m, ok: false, error: "unreachable" };
124
- try {
125
- const result = await fn(m);
126
- return { machine: m, ok: true, result };
127
- } catch (e) {
128
- return { machine: m, ok: false, error: e?.message || String(e) };
129
- }
130
- }));
131
- }
132
- function printResultRow(m, status, detail) {
133
- const id = truncate(m.machineId, 18).padEnd(20);
134
- const label = truncate(m.label, 22).padEnd(24);
135
- const color = status === "OK" ? "\x1B[32m" : status === "SKIP" ? "\x1B[33m" : "\x1B[31m";
136
- console.log(`${id} ${label} ${color}${status.padEnd(5)}\x1B[0m ${truncate(detail, 60)}`);
137
- }
138
- function printHeader(headers, widths) {
139
- const line = headers.map((h, i) => h.padEnd(widths[i])).join(" ");
140
- console.log(line);
141
- console.log("-".repeat(line.length));
142
- }
143
- async function fleetStatus() {
144
- const { server, machines } = await discoverMachines();
145
- if (machines.length === 0) {
146
- console.log("No machines found.");
147
- await server.disconnect();
148
- return;
149
- }
150
- try {
151
- printHeader(["MACHINE ID", "LABEL", "CLAUDE", "SVAMP", "STATUS"], [20, 24, 12, 10, 12]);
152
- const rows = await fanOut(machines, async (m) => {
153
- const [claudeRes, svampRes] = await Promise.all([
154
- m.rpc.bash("claude --version 2>/dev/null || echo unknown").catch(() => null),
155
- m.rpc.bash("svamp --version 2>/dev/null || echo unknown").catch(() => null)
156
- ]);
157
- const claude = (claudeRes?.stdout || "unknown").trim().split(/\s+/)[0] || "unknown";
158
- const svamp = (svampRes?.stdout || "unknown").trim().split(/\s+/)[0] || "unknown";
159
- const info = await m.rpc.getMachineInfo();
160
- return { claude, svamp, status: info.daemonState?.status || "unknown" };
161
- });
162
- for (const r of rows) {
163
- const id = truncate(r.machine.machineId, 18).padEnd(20);
164
- const label = truncate(r.machine.label, 22).padEnd(24);
165
- const claude = (r.ok ? r.result.claude : "-").padEnd(12);
166
- const svamp = (r.ok ? r.result.svamp : "-").padEnd(10);
167
- const status = r.ok ? r.result.status : r.error || "fail";
168
- const color = r.ok && r.result.status === "running" ? "\x1B[32m" : "\x1B[31m";
169
- console.log(`${id} ${label} ${claude} ${svamp} ${color}${status}\x1B[0m`);
170
- }
171
- console.log(`
172
- ${machines.length} machine(s).`);
173
- } finally {
174
- await server.disconnect();
175
- }
176
- }
177
- async function fleetExec(command, opts) {
178
- if (!command) {
179
- console.error('Usage: svamp machine exec "<command>" --all');
180
- process.exit(1);
181
- }
182
- const { server, machines } = await discoverMachines();
183
- if (machines.length === 0) {
184
- console.log("No machines found.");
185
- await server.disconnect();
186
- return;
187
- }
188
- try {
189
- printHeader(["MACHINE ID", "LABEL", "EXIT", "OUTPUT"], [20, 24, 6, 60]);
190
- const rows = await fanOut(machines, async (m) => {
191
- return await m.rpc.bash(command, opts?.cwd || void 0);
192
- });
193
- let failures = 0;
194
- for (const r of rows) {
195
- const id = truncate(r.machine.machineId, 18).padEnd(20);
196
- const label = truncate(r.machine.label, 22).padEnd(24);
197
- if (!r.ok) {
198
- console.log(`${id} ${label} ${"-".padEnd(6)} \x1B[31m${truncate(r.error || "error", 60)}\x1B[0m`);
199
- failures += 1;
200
- continue;
201
- }
202
- const exit = String(r.result.exitCode).padEnd(6);
203
- const out = (r.result.stdout?.trim() || r.result.stderr?.trim() || "").replace(/\s+/g, " ");
204
- const color = r.result.exitCode === 0 ? "" : "\x1B[31m";
205
- const reset = color ? "\x1B[0m" : "";
206
- console.log(`${id} ${label} ${exit} ${color}${truncate(out, 60)}${reset}`);
207
- if (r.result.exitCode !== 0) failures += 1;
208
- }
209
- console.log(`
210
- ${rows.length - failures}/${rows.length} succeeded.`);
211
- if (failures > 0) process.exitCode = 1;
212
- } finally {
213
- await server.disconnect();
214
- }
215
- }
216
- async function fleetUpgradeClaude(opts) {
217
- const version = opts?.version || PINNED_CLAUDE_CODE_VERSION;
218
- const { server, machines } = await discoverMachines();
219
- if (machines.length === 0) {
220
- console.log("No machines found.");
221
- await server.disconnect();
222
- return;
223
- }
224
- console.log(`Upgrading Claude Code to ${version} on ${machines.length} machine(s)...
225
- `);
226
- try {
227
- printHeader(["MACHINE ID", "LABEL", "STATE", "DETAIL"], [20, 24, 6, 60]);
228
- const cmd = `claude install ${version} 2>&1 || claude update 2>&1`;
229
- const rows = await fanOut(machines, async (m) => await m.rpc.bash(cmd, void 0));
230
- let failures = 0;
231
- for (const r of rows) {
232
- if (!r.ok) {
233
- printResultRow(r.machine, "FAIL", r.error || "");
234
- failures += 1;
235
- continue;
236
- }
237
- const ok = r.result.exitCode === 0;
238
- const detail = (r.result.stdout?.trim().split("\n").slice(-1)[0] || r.result.stderr?.trim().slice(-200) || "").replace(/\s+/g, " ");
239
- printResultRow(r.machine, ok ? "OK" : "FAIL", detail);
240
- if (!ok) failures += 1;
241
- }
242
- console.log(`
243
- ${rows.length - failures}/${rows.length} machines on ${version}.`);
244
- if (failures > 0) process.exitCode = 1;
245
- } finally {
246
- await server.disconnect();
247
- }
248
- }
249
- async function fleetUpgradeSvamp(opts) {
250
- const version = opts?.version || "latest";
251
- const { server, machines } = await discoverMachines();
252
- if (machines.length === 0) {
253
- console.log("No machines found.");
254
- await server.disconnect();
255
- return;
256
- }
257
- const selfId = opts?.excludeSelf ? getSelfMachineId() : null;
258
- if (opts?.excludeSelf && !selfId) {
259
- console.log("\u26A0 --exclude-self: could not determine this machine's ID (no local daemon state) \u2014 nothing excluded.");
260
- }
261
- const { targets, skipped } = partitionSelf(machines, selfId);
262
- console.log(`Upgrading svamp-cli to ${version} on ${targets.length} machine(s)${selfId ? ` (excluding self: ${selfId})` : ""}...
263
- `);
264
- try {
265
- printHeader(["MACHINE ID", "LABEL", "STATE", "DETAIL"], [20, 24, 6, 60]);
266
- for (const m of skipped) printResultRow(m, "SKIP", 'self \u2014 restart separately with "svamp daemon restart"');
267
- const cmd = `bash -lc 'npm install -g svamp-cli@${version} 2>&1 | tail -5 && svamp daemon restart 2>&1 | tail -5'`;
268
- const rows = await fanOut(targets, async (m) => await m.rpc.bash(cmd, void 0));
269
- let failures = 0;
270
- for (const r of rows) {
271
- if (!r.ok) {
272
- printResultRow(r.machine, "FAIL", r.error || "");
273
- failures += 1;
274
- continue;
275
- }
276
- const ok = r.result.exitCode === 0;
277
- const detail = (r.result.stdout?.trim().split("\n").slice(-1)[0] || r.result.stderr?.trim().slice(-200) || "").replace(/\s+/g, " ");
278
- printResultRow(r.machine, ok ? "OK" : "FAIL", detail);
279
- if (!ok) failures += 1;
280
- }
281
- console.log(`
282
- ${rows.length - failures}/${rows.length} machines upgraded.`);
283
- console.log("Note: daemons will auto-converge Claude Code to the pinned version on restart.");
284
- if (failures > 0) process.exitCode = 1;
285
- } finally {
286
- await server.disconnect();
287
- }
288
- }
289
- async function fleetDaemonRestart(opts) {
290
- const { server, machines } = await discoverMachines();
291
- if (machines.length === 0) {
292
- console.log("No machines found.");
293
- await server.disconnect();
294
- return;
295
- }
296
- try {
297
- printHeader(["MACHINE ID", "LABEL", "STATE", "DETAIL"], [20, 24, 6, 60]);
298
- const cmd = opts?.graceful === false ? `svamp daemon stop --cleanup 2>&1 && svamp daemon start 2>&1` : `svamp daemon restart 2>&1`;
299
- const rows = await fanOut(machines, async (m) => await m.rpc.bash(cmd, void 0));
300
- let failures = 0;
301
- for (const r of rows) {
302
- if (!r.ok) {
303
- printResultRow(r.machine, "FAIL", r.error || "");
304
- failures += 1;
305
- continue;
306
- }
307
- const ok = r.result.exitCode === 0;
308
- const detail = (r.result.stdout?.trim().split("\n").slice(-1)[0] || r.result.stderr?.trim().slice(-200) || "").replace(/\s+/g, " ");
309
- printResultRow(r.machine, ok ? "OK" : "FAIL", detail);
310
- if (!ok) failures += 1;
311
- }
312
- console.log(`
313
- ${rows.length - failures}/${rows.length} restarted.`);
314
- if (failures > 0) process.exitCode = 1;
315
- } finally {
316
- await server.disconnect();
317
- }
318
- }
319
- async function fleetPushSkill(name) {
320
- if (!name) {
321
- console.error("Usage: svamp fleet push-skill <name>");
322
- process.exit(1);
323
- }
324
- const { server, machines } = await discoverMachines();
325
- if (machines.length === 0) {
326
- console.log("No machines found.");
327
- await server.disconnect();
328
- return;
329
- }
330
- console.log(`Pushing skill "${name}" to ${machines.length} machine(s)...
331
- `);
332
- try {
333
- printHeader(["MACHINE ID", "LABEL", "STATE", "DETAIL"], [20, 24, 6, 60]);
334
- const cmd = `svamp skills install ${name} --force 2>&1`;
335
- const rows = await fanOut(machines, async (m) => await m.rpc.bash(cmd, void 0));
336
- let failures = 0;
337
- for (const r of rows) {
338
- if (!r.ok) {
339
- printResultRow(r.machine, "FAIL", r.error || "");
340
- failures += 1;
341
- continue;
342
- }
343
- const ok = r.result.exitCode === 0;
344
- const detail = (r.result.stdout?.trim().split("\n").slice(-1)[0] || r.result.stderr?.trim().slice(-200) || "").replace(/\s+/g, " ");
345
- printResultRow(r.machine, ok ? "OK" : "FAIL", detail);
346
- if (!ok) failures += 1;
347
- }
348
- console.log(`
349
- ${rows.length - failures}/${rows.length} updated.`);
350
- if (failures > 0) process.exitCode = 1;
351
- } finally {
352
- await server.disconnect();
353
- }
354
- }
355
-
356
- export { fleetDaemonRestart, fleetExec, fleetPushSkill, fleetStatus, fleetUpgradeClaude, fleetUpgradeSvamp, partitionSelf };