svamp-cli 0.2.346 → 0.2.356

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.
Files changed (30) hide show
  1. package/bin/skills/artifact/SKILL.md +19 -5
  2. package/bin/skills/crew/SKILL.md +42 -11
  3. package/dist/{acpBackend-BZ1MJhQV.mjs → acpBackend-UaBD4zL3.mjs} +3 -0
  4. package/dist/{agentCommands-B2Nl7Fof.mjs → agentCommands-D91RcjqQ.mjs} +4 -4
  5. package/dist/{claudeAuth-iAerdaMr.mjs → claudeAuth-C0wc1GTM.mjs} +45 -1
  6. package/dist/cli.mjs +78 -69
  7. package/dist/{commands-D1pZq3d4.mjs → commands-B-yczgz_.mjs} +14 -10
  8. package/dist/{commands-BOpoVBOM.mjs → commands-BMeDrpSv.mjs} +3 -3
  9. package/dist/{commands-H4PsTGn8.mjs → commands-BbmypSgx.mjs} +5 -2
  10. package/dist/{commands-C2aODeVc.mjs → commands-BkdbelFo.mjs} +26 -11
  11. package/dist/{commands-BB1Kcyt-.mjs → commands-C13-yE2R.mjs} +32 -3
  12. package/dist/{commands-D69mnkYu.mjs → commands-CoOc4qiw.mjs} +2 -2
  13. package/dist/{commands-CiRCwjkJ.mjs → commands-sPvRcG3J.mjs} +9 -9
  14. package/dist/{detectIsolation-CzoDCEnh.mjs → detectIsolation-CrV8IFDv.mjs} +36 -16
  15. package/dist/{fleet-DM_Q7w69.mjs → fleet-Ca-UjJNL.mjs} +40 -15
  16. package/dist/{hookSettings-DYMTUgVA.mjs → hookSettings-FFc1pi8j.mjs} +174 -27
  17. package/dist/index.mjs +8 -8
  18. package/dist/{loopVerify-B47sSjxe.mjs → loopVerify-DALtTk2V.mjs} +1 -1
  19. package/dist/{notifyCommands-D-IKIxiq.mjs → notifyCommands-CCUE18Q-.mjs} +1 -1
  20. package/dist/package-BrsiNutS.mjs +64 -0
  21. package/dist/{rpc-BB6RF4-F.mjs → rpc-CBkSkzsI.mjs} +8 -8
  22. package/dist/{rpc-Cf0grcru.mjs → rpc-ccY7xvlo.mjs} +6 -3
  23. package/dist/{run-ChdsgaSD.mjs → run-DCq3aEiF.mjs} +3 -3
  24. package/dist/{run-DkjwF-zA.mjs → run-DDmeV9K3.mjs} +190 -59
  25. package/dist/{scheduler-BY30ZrB9.mjs → scheduler-BkVJ27Ki.mjs} +8 -8
  26. package/dist/{serveAuth-DbVLRCRR.mjs → serveAuth-4M0vwMyj.mjs} +53 -11
  27. package/dist/{serveCommands-CUHUiYnZ.mjs → serveCommands-CLhkccJM.mjs} +8 -8
  28. package/dist/{store-Bn0ByYev.mjs → store-B4E6NzG6.mjs} +10 -1
  29. package/package.json +2 -2
  30. package/dist/package-DtuINqf5.mjs +0 -64
@@ -1,16 +1,16 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { join, resolve } from 'node:path';
3
- import { connectAndGetMachine, resolveSessionId, createWorktree, connectAndResolveSession } from './commands-D1pZq3d4.mjs';
3
+ import { connectAndGetMachine, resolveSessionId, createWorktree, connectAndResolveSession } from './commands-B-yczgz_.mjs';
4
4
  import { execFileSync } from 'node:child_process';
5
5
  import { w as withFileLock } from './fileLock-BMPwCrB6.mjs';
6
- import { u as updateIssue, f as addComment, h as addIssue, n as resolveCrewIssueOnMerge } from './store-Bn0ByYev.mjs';
6
+ import { u as updateIssue, h as addComment, i as addIssue, o as resolveCrewIssueOnMerge } from './store-B4E6NzG6.mjs';
7
7
  import { s as shortId } from './friendlyName-1UZcyDhu.mjs';
8
8
  import { w as wantsHelp } from './wantsHelp-6bMOeNUz.mjs';
9
9
  import 'node:os';
10
10
  import './hyphaClient-ybivOjZo.mjs';
11
11
  import './types-57J3Re0n.mjs';
12
12
  import './securityContext-Cm5UTCuD.mjs';
13
- import './loopVerify-B47sSjxe.mjs';
13
+ import './loopVerify-DALtTk2V.mjs';
14
14
  import './instance-CeHz5C8v.mjs';
15
15
  import 'fs';
16
16
  import 'path';
@@ -306,6 +306,7 @@ async function featureStart(brief, opts = {}) {
306
306
  const standalone = !!opts.noWorktree;
307
307
  let childDir;
308
308
  let base = "";
309
+ const leadDir = opts.directory || lead.directory;
309
310
  if (standalone) {
310
311
  const dir = opts.directory;
311
312
  if (!dir) {
@@ -319,15 +320,14 @@ async function featureStart(brief, opts = {}) {
319
320
  }
320
321
  console.log(`Standalone child folder: ${childDir} (no worktree)`);
321
322
  } else {
322
- const leadDir = opts.directory || lead.directory;
323
323
  if (!leadDir) {
324
324
  console.error("Could not determine the lead project directory.");
325
325
  process.exit(1);
326
326
  }
327
- const projectRoot2 = canonicalProjectRoot(leadDir);
328
- base = opts.baseBranch || currentBranch(projectRoot2) || "main";
327
+ const worktreeRoot = canonicalProjectRoot(leadDir);
328
+ base = opts.baseBranch || currentBranch(worktreeRoot) || "main";
329
329
  try {
330
- wt = createWorktree(projectRoot2);
330
+ wt = createWorktree(worktreeRoot);
331
331
  } catch (e) {
332
332
  console.error(`Failed to create worktree: ${e.message}`);
333
333
  process.exit(1);
@@ -335,7 +335,7 @@ async function featureStart(brief, opts = {}) {
335
335
  childDir = wt.path;
336
336
  console.log(`Worktree: ${wt.branch} \u2192 ${wt.path}`);
337
337
  }
338
- const projectRoot = canonicalProjectRoot(lead.directory || opts.directory || childDir);
338
+ const projectRoot = canonicalProjectRoot(leadDir || childDir);
339
339
  const result = await machine.spawnSession({
340
340
  directory: childDir,
341
341
  agent: "claude",
@@ -629,7 +629,14 @@ async function featureMerge(childPartial, opts = {}) {
629
629
  notifyLead,
630
630
  childId,
631
631
  `<crew-freeze>Your lead is merging branch ${branch} into ${base}. Stop editing files now; do not commit until told.</crew-freeze>`,
632
- "crew: freeze for merge"
632
+ "crew: freeze for merge",
633
+ // #1619: URGENT. This is a pre-emption notice, and the state it must interrupt —
634
+ // a child mid-work — is by definition NOT idle, so at 'normal' urgency
635
+ // classifyInbound QUEUES it until the child stops on its own. waitForChildIdle
636
+ // below is supposed to observe the child halting BECAUSE of this message; a queued
637
+ // message causes no halt, so the wait could only ever time out, and mergeBack can
638
+ // then reach `git worktree remove --force` on a tree the child is still writing.
639
+ "urgent"
633
640
  );
634
641
  } catch {
635
642
  }
@@ -649,7 +656,10 @@ async function featureMerge(childPartial, opts = {}) {
649
656
  Merge was not applied: ${r.detail}
650
657
  Resolve and run \`svamp feature done\` again.
651
658
  </crew-rework>`,
652
- "crew: rework needed"
659
+ "crew: rework needed",
660
+ // #1619: URGENT — the comment above says 're-wake the child', which at
661
+ // 'normal' urgency is exactly what did not happen.
662
+ "urgent"
653
663
  );
654
664
  } catch {
655
665
  }
@@ -665,7 +675,12 @@ Resolve and run \`svamp feature done\` again.
665
675
  Merge could not be applied (${r.stage}): ${r.detail}
666
676
  Stop editing and hold \u2014 do not keep working. Once the blocker is resolved, the lead can retry \`svamp feature merge\`.
667
677
  </crew-blocked>`,
668
- "crew: merge blocked"
678
+ "crew: merge blocked",
679
+ // #1619: URGENT. #0353's whole point is to STOP a child that is 'silently
680
+ // LOOPING against an un-merged branch, burning tokens toward the cost
681
+ // ceiling'. A looping child is never idle, so a queued STOP notice arrived
682
+ // only once it was no longer needed.
683
+ "urgent"
669
684
  );
670
685
  } catch {
671
686
  }
@@ -1,4 +1,4 @@
1
- import { connectAndResolveSession } from './commands-D1pZq3d4.mjs';
1
+ import { connectAndResolveSession } from './commands-B-yczgz_.mjs';
2
2
  import { w as wantsHelp } from './wantsHelp-6bMOeNUz.mjs';
3
3
  import 'node:fs';
4
4
  import 'node:child_process';
@@ -9,12 +9,12 @@ import './friendlyName-1UZcyDhu.mjs';
9
9
  import 'node:crypto';
10
10
  import './types-57J3Re0n.mjs';
11
11
  import './securityContext-Cm5UTCuD.mjs';
12
- import './loopVerify-B47sSjxe.mjs';
12
+ import './loopVerify-DALtTk2V.mjs';
13
13
  import './instance-CeHz5C8v.mjs';
14
14
  import 'fs';
15
15
  import 'path';
16
16
  import 'os';
17
- import './store-Bn0ByYev.mjs';
17
+ import './store-B4E6NzG6.mjs';
18
18
  import './fileLock-BMPwCrB6.mjs';
19
19
  import './flushExit-C-i6SGjj.mjs';
20
20
 
@@ -111,6 +111,27 @@ async function outpostInstall(args) {
111
111
  if (info.downloadBase) console.log("Direct download: " + info.downloadBase);
112
112
  }
113
113
  }
114
+ async function outpostRotate(args) {
115
+ const dl = argVal(args, "--download-base");
116
+ if (!args.includes("--yes") && !args.includes("-y")) {
117
+ console.log("This REVOKES this session's outpost token and mints a new one.");
118
+ console.log("Every currently-connected outpost will stop working until it re-runs the new");
119
+ console.log("install command. Re-run with --yes to proceed.");
120
+ process.exit(1);
121
+ }
122
+ const info = await withSession((m, id) => m.sessionRPC(id, "outpostRotateToken", dl ? { params: { downloadBase: dl } } : {}));
123
+ if (!info || info.error) {
124
+ console.error(`Could not rotate the outpost token: ${info?.error || "unknown error"}`);
125
+ process.exit(1);
126
+ }
127
+ console.log("\u2713 outpost token rotated \u2014 the previous token no longer works.\n");
128
+ console.log("Any machine that was connected must re-run the install command below:\n");
129
+ const gen = info.install || {};
130
+ if (gen.unix) console.log(" macOS / Linux:\n " + gen.unix + "\n");
131
+ if (gen.windows) console.log(" Windows (PowerShell):\n " + gen.windows + "\n");
132
+ if (info.pairingCode) console.log(` \u2026or the short pairing code: ${info.pairingCode}
133
+ `);
134
+ }
114
135
  function usage() {
115
136
  console.log(`svamp outpost \u2014 drive a connected remote machine (FDE outpost)
116
137
 
@@ -121,6 +142,10 @@ function usage() {
121
142
  stdout/stderr/exit code synchronously (like a local command).
122
143
  With one outpost connected, --machine is optional.
123
144
  svamp outpost install [--os <os>] Reprint the install one-liner(s) for this session's outpost
145
+ svamp outpost rotate [--yes] REVOKE this session's outpost token and mint a new one.
146
+ Use after the install one-liner has been shared somewhere it
147
+ should not have been. Every currently-connected outpost stops
148
+ working and must re-run the new install command.
124
149
 
125
150
  With several machines connected, target each by id, e.g.:
126
151
  svamp outpost exec --machine build-mac "uname -a"
@@ -149,6 +174,10 @@ async function outpostCommand(args) {
149
174
  await outpostInstall(args.slice(1));
150
175
  return;
151
176
  }
177
+ if (sub === "rotate" || sub === "revoke") {
178
+ await outpostRotate(args.slice(1));
179
+ return;
180
+ }
152
181
  console.error(`unknown outpost command: ${sub}`);
153
182
  usage();
154
183
  process.exit(1);
@@ -1,4 +1,4 @@
1
- import { r as resolveProjectRoot } from './store-Bn0ByYev.mjs';
1
+ import { r as resolveProjectRoot } from './store-B4E6NzG6.mjs';
2
2
  import { w as wantsHelp } from './wantsHelp-6bMOeNUz.mjs';
3
3
  import { s as setWorkflowEnabled, i as isWorkflowEnabled, r as removeWorkflow, g as getWorkflow, l as listWorkflows, c as workflowSteps, a as saveWorkflow, b as rawWorkflow } from './store-fi0f-ff1.mjs';
4
4
  import { v as validateCronExpr, d as describeNextFire } from './cron-hHGdb5kU.mjs';
@@ -147,7 +147,7 @@ async function workflowCommand(args) {
147
147
  console.error(`Workflow not found: ${name}`);
148
148
  process.exit(1);
149
149
  }
150
- const { runWorkflow } = await import('./run-DkjwF-zA.mjs').then(function (n) { return n.e; });
150
+ const { runWorkflow } = await import('./run-DDmeV9K3.mjs').then(function (n) { return n.e; });
151
151
  const run = await runWorkflow(root, wf, { trigger: "manual", actor: process.env.SVAMP_SESSION_ID || null });
152
152
  console.log(`\u25B6 ran workflow "${wf.name}" (${run.steps.length} step${run.steps.length === 1 ? "" : "s"}) \u2192 ${run.status}`);
153
153
  for (const st of run.steps) {
@@ -61,7 +61,7 @@ async function serviceExpose(args) {
61
61
  process.exit(1);
62
62
  }
63
63
  if (foreground) {
64
- const { runFrpcTunnel } = await import('./hookSettings-DYMTUgVA.mjs').then(function (n) { return n.w; });
64
+ const { runFrpcTunnel } = await import('./hookSettings-FFc1pi8j.mjs').then(function (n) { return n.w; });
65
65
  await runFrpcTunnel(name, ports, void 0, {
66
66
  group,
67
67
  groupKey,
@@ -71,7 +71,7 @@ async function serviceExpose(args) {
71
71
  });
72
72
  return;
73
73
  }
74
- const { connectAndGetMachine } = await import('./commands-D1pZq3d4.mjs');
74
+ const { connectAndGetMachine } = await import('./commands-B-yczgz_.mjs');
75
75
  const { server, machine } = await connectAndGetMachine();
76
76
  try {
77
77
  const status = await machine.tunnelStart({
@@ -93,7 +93,7 @@ async function serviceExpose(args) {
93
93
  console.log(` port ${port}: ${url}`);
94
94
  }
95
95
  if (process.env.SVAMP_SESSION_ID) {
96
- const { autoAddSessionLink } = await import('./agentCommands-B2Nl7Fof.mjs');
96
+ const { autoAddSessionLink } = await import('./agentCommands-D91RcjqQ.mjs');
97
97
  let added = 0;
98
98
  for (const [port, url] of urlEntries) {
99
99
  const label = urlEntries.length > 1 ? `${name}:${port}` : name;
@@ -129,8 +129,8 @@ async function serviceServe(args) {
129
129
  const resolvedDir = path.resolve(directory);
130
130
  console.log(`Serving ${resolvedDir}`);
131
131
  const http = await import('http');
132
- const { serveStaticMount } = await import('./hookSettings-DYMTUgVA.mjs').then(function (n) { return n.x; });
133
- const { getFreePort } = await import('./hookSettings-DYMTUgVA.mjs').then(function (n) { return n.w; });
132
+ const { serveStaticMount } = await import('./hookSettings-FFc1pi8j.mjs').then(function (n) { return n.x; });
133
+ const { getFreePort } = await import('./hookSettings-FFc1pi8j.mjs').then(function (n) { return n.w; });
134
134
  const server = http.createServer((req, res) => {
135
135
  const u = new URL(req.url || "/", "http://127.0.0.1");
136
136
  let rel = u.pathname;
@@ -153,7 +153,7 @@ async function serviceServe(args) {
153
153
  resolve(18080);
154
154
  });
155
155
  });
156
- const { runFrpcTunnel } = await import('./hookSettings-DYMTUgVA.mjs').then(function (n) { return n.w; });
156
+ const { runFrpcTunnel } = await import('./hookSettings-FFc1pi8j.mjs').then(function (n) { return n.w; });
157
157
  void server;
158
158
  await runFrpcTunnel(name, [servePort]);
159
159
  } catch (err) {
@@ -163,7 +163,7 @@ async function serviceServe(args) {
163
163
  }
164
164
  async function serviceList(_args) {
165
165
  try {
166
- const { connectAndGetMachine } = await import('./commands-D1pZq3d4.mjs');
166
+ const { connectAndGetMachine } = await import('./commands-B-yczgz_.mjs');
167
167
  const { server, machine } = await connectAndGetMachine();
168
168
  try {
169
169
  const tunnels = await machine.tunnelList({});
@@ -203,11 +203,11 @@ async function serviceDelete(args) {
203
203
  process.exit(1);
204
204
  }
205
205
  try {
206
- const { connectAndGetMachine } = await import('./commands-D1pZq3d4.mjs');
206
+ const { connectAndGetMachine } = await import('./commands-B-yczgz_.mjs');
207
207
  const { server, machine } = await connectAndGetMachine();
208
208
  try {
209
209
  await machine.tunnelStop({ name });
210
- const { removeSessionLinkByService } = await import('./agentCommands-B2Nl7Fof.mjs');
210
+ const { removeSessionLinkByService } = await import('./agentCommands-D91RcjqQ.mjs');
211
211
  removeSessionLinkByService("tunnel", name);
212
212
  console.log(`Stopped tunnel '${name}'.`);
213
213
  } catch (err) {
@@ -1,5 +1,5 @@
1
1
  import { execFile } from 'node:child_process';
2
- import { mkdir, rm, readFile, chmod, access, mkdtemp } from 'node:fs/promises';
2
+ import { mkdir, rm, readFile, mkdtemp, access, chmod, rename } from 'node:fs/promises';
3
3
  import { homedir } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
  import { promisify } from 'node:util';
@@ -65,6 +65,16 @@ function getNonoAssetName(version) {
65
65
  }
66
66
  return null;
67
67
  }
68
+ async function probeNonoRuns(binaryPath) {
69
+ try {
70
+ const { stdout } = await execFileAsync(binaryPath, ["--version"], { timeout: 5e3 });
71
+ return { ok: true, version: stdout.trim().split("\n")[0] };
72
+ } catch (e) {
73
+ const stderr = typeof e?.stderr === "string" ? e.stderr.trim() : "";
74
+ const first = stderr.split("\n").find((l) => l.trim()) || e?.message || "unknown error";
75
+ return { ok: false, reason: first.trim() };
76
+ }
77
+ }
68
78
  async function installNono() {
69
79
  const platform = process.platform;
70
80
  if (platform !== "darwin" && platform !== "linux") {
@@ -116,23 +126,33 @@ async function installNono() {
116
126
  return false;
117
127
  }
118
128
  console.log(`[isolation] nono ${version} checksum verified (${actual.slice(0, 12)}\u2026)`);
119
- await execFileAsync("tar", [
120
- "xzf",
121
- tarball,
122
- "-C",
123
- SVAMP_BIN_DIR
124
- ], { timeout: 15e3 });
125
- await rm(tarball, { force: true }).catch(() => {
126
- });
127
129
  const nonoPath = join(SVAMP_BIN_DIR, "nono");
128
- await chmod(nonoPath, 493);
130
+ const stageDir = await mkdtemp(join(SVAMP_BIN_DIR, ".nono-stage-"));
129
131
  try {
130
- await access(nonoPath);
131
- console.log(`[isolation] nono ${version} installed at ${nonoPath}`);
132
+ await execFileAsync("tar", ["xzf", tarball, "-C", stageDir], { timeout: 15e3 });
133
+ await rm(tarball, { force: true }).catch(() => {
134
+ });
135
+ const staged = join(stageDir, "nono");
136
+ try {
137
+ await access(staged);
138
+ } catch {
139
+ console.warn("[isolation] Download completed but nono binary not found in the archive");
140
+ return false;
141
+ }
142
+ await chmod(staged, 493);
143
+ const probe = await probeNonoRuns(staged);
144
+ if (!probe.ok) {
145
+ console.error(
146
+ `[isolation] nono ${version} downloaded and checksum-verified, but it CANNOT RUN on this host: ${probe.reason}. Refusing to install \u2014 the previous binary (if any) is untouched. This host needs a nono build compatible with its libc; sessions will fall back to docker/podman, where --allow-write and --allow-domain are NOT enforced (#0878).`
147
+ );
148
+ return false;
149
+ }
150
+ await rename(staged, nonoPath);
151
+ console.log(`[isolation] nono ${version} installed at ${nonoPath} (verified runnable: ${probe.version || "ok"})`);
132
152
  return true;
133
- } catch {
134
- console.warn("[isolation] Download completed but nono binary not found");
135
- return false;
153
+ } finally {
154
+ await rm(stageDir, { recursive: true, force: true }).catch(() => {
155
+ });
136
156
  }
137
157
  } catch (e) {
138
158
  console.warn(`[isolation] Failed to install nono: ${e.message}`);
@@ -316,4 +336,4 @@ async function detectIsolationCapabilities() {
316
336
  return { available, preferred, details };
317
337
  }
318
338
 
319
- export { detectIsolationCapabilities, installNono, isolationDisabledByEnv, verifyWithHardTimeout };
339
+ export { detectIsolationCapabilities, installNono, isolationDisabledByEnv, probeNonoRuns, verifyWithHardTimeout };
@@ -37,6 +37,26 @@ function readDaemonState() {
37
37
  function getSelfMachineId() {
38
38
  return readDaemonState()?.machineId || null;
39
39
  }
40
+ function rowDetail(result) {
41
+ const one = (t) => (t || "").trim().replace(/\s+/g, " ");
42
+ const err = one(result.stderr).slice(-200);
43
+ if (result.exitCode === 124 || /timed out and was killed/i.test(err)) {
44
+ return `TIMED OUT (may have partially completed) \u2014 ${err || "no stderr"}`;
45
+ }
46
+ const last = one((result.stdout || "").trim().split("\n").slice(-1)[0]);
47
+ return last || err || "";
48
+ }
49
+ const INSTALL_TIMEOUT_MS = 10 * 60 * 1e3;
50
+ const SAFE_SKILL_NAME = /^[a-z0-9][a-z0-9._-]*$/i;
51
+ const SAFE_VERSION = /^[0-9][0-9a-zA-Z.+-]*$/;
52
+ function assertSafeFleetArg(value, kind, pattern) {
53
+ if (typeof value !== "string" || !pattern.test(value)) {
54
+ throw new Error(
55
+ `refusing to fan out: ${kind} ${JSON.stringify(value)} is not a plain ${kind} (expected ${pattern.source}). This string would be interpolated into a shell command run on every machine.`
56
+ );
57
+ }
58
+ return value;
59
+ }
40
60
  function partitionSelf(machines, selfId) {
41
61
  if (!selfId) return { targets: machines, skipped: [] };
42
62
  return {
@@ -234,8 +254,9 @@ async function fleetUpgradeClaude(opts) {
234
254
  `);
235
255
  try {
236
256
  printHeader(["MACHINE ID", "LABEL", "STATE", "DETAIL"], [20, 24, 6, 60]);
257
+ assertSafeFleetArg(version, "version", SAFE_VERSION);
237
258
  const cmd = `claude install ${version} 2>&1 || claude update 2>&1`;
238
- const rows = await fanOut(machines, async (m) => await m.rpc.bash(cmd, void 0));
259
+ const rows = await fanOut(machines, async (m) => await m.rpc.bash(cmd, void 0, INSTALL_TIMEOUT_MS));
239
260
  let failures = 0;
240
261
  for (const r of rows) {
241
262
  if (!r.ok) {
@@ -244,7 +265,7 @@ async function fleetUpgradeClaude(opts) {
244
265
  continue;
245
266
  }
246
267
  const ok = r.result.exitCode === 0;
247
- const detail = (r.result.stdout?.trim().split("\n").slice(-1)[0] || r.result.stderr?.trim().slice(-200) || "").replace(/\s+/g, " ");
268
+ const detail = rowDetail(r.result);
248
269
  printResultRow(r.machine, ok ? "OK" : "FAIL", detail);
249
270
  if (!ok) failures += 1;
250
271
  }
@@ -271,8 +292,9 @@ async function fleetUpgradeCodex(opts) {
271
292
  `);
272
293
  try {
273
294
  printHeader(["MACHINE ID", "LABEL", "STATE", "DETAIL"], [20, 24, 6, 60]);
295
+ assertSafeFleetArg(version, "version", SAFE_VERSION);
274
296
  const cmd = `bash -lc '${NPM_PREFIX_RESOLVER}; npm install -g $PREFIX_ARG @openai/codex@${version} 2>&1 | tail -5'`;
275
- const rows = await fanOut(machines, async (m) => await m.rpc.bash(cmd, void 0));
297
+ const rows = await fanOut(machines, async (m) => await m.rpc.bash(cmd, void 0, INSTALL_TIMEOUT_MS));
276
298
  let failures = 0;
277
299
  for (const r of rows) {
278
300
  if (!r.ok) {
@@ -281,7 +303,7 @@ async function fleetUpgradeCodex(opts) {
281
303
  continue;
282
304
  }
283
305
  const ok = r.result.exitCode === 0;
284
- const detail = (r.result.stdout?.trim().split("\n").slice(-1)[0] || r.result.stderr?.trim().slice(-200) || "").replace(/\s+/g, " ");
306
+ const detail = rowDetail(r.result);
285
307
  printResultRow(r.machine, ok ? "OK" : "FAIL", detail);
286
308
  if (!ok) failures += 1;
287
309
  }
@@ -318,7 +340,7 @@ async function fleetUpgradeNono(opts) {
318
340
  try {
319
341
  printHeader(["MACHINE ID", "LABEL", "STATE", "DETAIL"], [20, 24, 6, 60]);
320
342
  const cmd = `svamp daemon install-nono 2>&1 | tail -5`;
321
- const rows = await fanOut(machines, async (m) => await m.rpc.bash(cmd, void 0));
343
+ const rows = await fanOut(machines, async (m) => await m.rpc.bash(cmd, void 0, INSTALL_TIMEOUT_MS));
322
344
  let failures = 0;
323
345
  for (const r of rows) {
324
346
  if (!r.ok) {
@@ -327,7 +349,7 @@ async function fleetUpgradeNono(opts) {
327
349
  continue;
328
350
  }
329
351
  const ok = r.result.exitCode === 0;
330
- const detail = (r.result.stdout?.trim().split("\n").slice(-1)[0] || r.result.stderr?.trim().slice(-200) || "").replace(/\s+/g, " ");
352
+ const detail = rowDetail(r.result);
331
353
  printResultRow(r.machine, ok ? "OK" : "FAIL", detail);
332
354
  if (!ok) failures += 1;
333
355
  }
@@ -347,6 +369,7 @@ async function fleetUpgradeKimi(opts) {
347
369
  await server.disconnect();
348
370
  return;
349
371
  }
372
+ assertSafeFleetArg(version, "version", SAFE_VERSION);
350
373
  console.log(`Reconciling Kimi to ${version} on ${machines.length} machine(s)...
351
374
  `);
352
375
  console.log(`Note: Kimi is a global npm package (${KIMI_NPM_PACKAGE}); machines that never use it can ignore this.`);
@@ -355,7 +378,7 @@ async function fleetUpgradeKimi(opts) {
355
378
  try {
356
379
  printHeader(["MACHINE ID", "LABEL", "STATE", "DETAIL"], [20, 24, 6, 60]);
357
380
  const cmd = `bash -lc '${NPM_PREFIX_RESOLVER}; npm install -g $PREFIX_ARG ${KIMI_NPM_PACKAGE}@${version} 2>&1 | tail -5'`;
358
- const rows = await fanOut(machines, async (m) => await m.rpc.bash(cmd, void 0));
381
+ const rows = await fanOut(machines, async (m) => await m.rpc.bash(cmd, void 0, INSTALL_TIMEOUT_MS));
359
382
  let failures = 0;
360
383
  for (const r of rows) {
361
384
  if (!r.ok) {
@@ -364,7 +387,7 @@ async function fleetUpgradeKimi(opts) {
364
387
  continue;
365
388
  }
366
389
  const ok = r.result.exitCode === 0;
367
- const detail = (r.result.stdout?.trim().split("\n").slice(-1)[0] || r.result.stderr?.trim().slice(-200) || "").replace(/\s+/g, " ");
390
+ const detail = rowDetail(r.result);
368
391
  printResultRow(r.machine, ok ? "OK" : "FAIL", detail);
369
392
  if (!ok) failures += 1;
370
393
  }
@@ -395,8 +418,9 @@ async function fleetUpgradeSvamp(opts) {
395
418
  try {
396
419
  printHeader(["MACHINE ID", "LABEL", "STATE", "DETAIL"], [20, 24, 6, 60]);
397
420
  for (const m of skipped) printResultRow(m, "SKIP", 'self \u2014 restart separately with "svamp daemon restart"');
421
+ assertSafeFleetArg(version, "version", SAFE_VERSION);
398
422
  const cmd = `bash -lc '${NPM_PREFIX_RESOLVER}; npm install -g $PREFIX_ARG svamp-cli@${version} 2>&1 | tail -5 && ${restartCmd} 2>&1 | tail -5'`;
399
- const rows = await fanOut(targets, async (m) => await m.rpc.bash(cmd, void 0));
423
+ const rows = await fanOut(targets, async (m) => await m.rpc.bash(cmd, void 0, INSTALL_TIMEOUT_MS));
400
424
  let failures = 0;
401
425
  for (const r of rows) {
402
426
  if (!r.ok) {
@@ -405,7 +429,7 @@ async function fleetUpgradeSvamp(opts) {
405
429
  continue;
406
430
  }
407
431
  const ok = r.result.exitCode === 0;
408
- const detail = (r.result.stdout?.trim().split("\n").slice(-1)[0] || r.result.stderr?.trim().slice(-200) || "").replace(/\s+/g, " ");
432
+ const detail = rowDetail(r.result);
409
433
  printResultRow(r.machine, ok ? "OK" : "FAIL", detail);
410
434
  if (!ok) failures += 1;
411
435
  }
@@ -445,7 +469,7 @@ async function fleetDaemonRestart(opts) {
445
469
  console.log(`Restarting daemons on ${machines.length} machine(s) \u2014 ${mode}...
446
470
  `);
447
471
  printHeader(["MACHINE ID", "LABEL", "STATE", "DETAIL"], [20, 24, 6, 60]);
448
- const rows = await fanOut(machines, async (m) => await m.rpc.bash(cmd, void 0));
472
+ const rows = await fanOut(machines, async (m) => await m.rpc.bash(cmd, void 0, INSTALL_TIMEOUT_MS));
449
473
  let failures = 0;
450
474
  for (const r of rows) {
451
475
  if (!r.ok) {
@@ -454,7 +478,7 @@ async function fleetDaemonRestart(opts) {
454
478
  continue;
455
479
  }
456
480
  const ok = r.result.exitCode === 0;
457
- const detail = (r.result.stdout?.trim().split("\n").slice(-1)[0] || r.result.stderr?.trim().slice(-200) || "").replace(/\s+/g, " ");
481
+ const detail = rowDetail(r.result);
458
482
  printResultRow(r.machine, ok ? "OK" : "FAIL", detail);
459
483
  if (!ok) failures += 1;
460
484
  }
@@ -480,8 +504,9 @@ async function fleetPushSkill(name) {
480
504
  `);
481
505
  try {
482
506
  printHeader(["MACHINE ID", "LABEL", "STATE", "DETAIL"], [20, 24, 6, 60]);
507
+ assertSafeFleetArg(name, "skill name", SAFE_SKILL_NAME);
483
508
  const cmd = `svamp skills install ${name} --force 2>&1`;
484
- const rows = await fanOut(machines, async (m) => await m.rpc.bash(cmd, void 0));
509
+ const rows = await fanOut(machines, async (m) => await m.rpc.bash(cmd, void 0, INSTALL_TIMEOUT_MS));
485
510
  let failures = 0;
486
511
  for (const r of rows) {
487
512
  if (!r.ok) {
@@ -490,7 +515,7 @@ async function fleetPushSkill(name) {
490
515
  continue;
491
516
  }
492
517
  const ok = r.result.exitCode === 0;
493
- const detail = (r.result.stdout?.trim().split("\n").slice(-1)[0] || r.result.stderr?.trim().slice(-200) || "").replace(/\s+/g, " ");
518
+ const detail = rowDetail(r.result);
494
519
  printResultRow(r.machine, ok ? "OK" : "FAIL", detail);
495
520
  if (!ok) failures += 1;
496
521
  }
@@ -502,4 +527,4 @@ ${rows.length - failures}/${rows.length} updated.`);
502
527
  }
503
528
  }
504
529
 
505
- export { fleetDaemonRestart, fleetDaemonRestartCommand, fleetExec, fleetPushSkill, fleetStatus, fleetUpgradeClaude, fleetUpgradeCodex, fleetUpgradeKimi, fleetUpgradeNono, fleetUpgradeSvamp, parseVersionOutput, partitionSelf };
530
+ export { assertSafeFleetArg, fleetDaemonRestart, fleetDaemonRestartCommand, fleetExec, fleetPushSkill, fleetStatus, fleetUpgradeClaude, fleetUpgradeCodex, fleetUpgradeKimi, fleetUpgradeNono, fleetUpgradeSvamp, parseVersionOutput, partitionSelf, rowDetail };