pi-better-background-tasks 0.1.18 → 0.2.0

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.
@@ -0,0 +1,631 @@
1
+ import { runCommandOnce, spawnCommand } from "./process.js";
2
+ import type { SpawnedProcess } from "./process.js";
3
+ import type {
4
+ CommandResult,
5
+ CommandSpec,
6
+ RemoteTaskParams,
7
+ ResolvedRemoteTaskMetadata,
8
+ ResolvedSshIdentity,
9
+ SshConnectionParams,
10
+ } from "./types.js";
11
+
12
+ export const DEFAULT_SSH_CONNECT_TIMEOUT_SECONDS = 10;
13
+ export const DEFAULT_TMUX_BOOTSTRAP_TIMEOUT_MS = 120_000;
14
+
15
+ export interface RemoteRunner {
16
+ spawn(spec: CommandSpec, logPath: string, detached: boolean): SpawnedProcess;
17
+ runOnce(spec: CommandSpec, maxBufferBytes?: number, timeoutMs?: number): Promise<CommandResult>;
18
+ }
19
+
20
+ export interface SshRemoteTaskIntent {
21
+ operation: "spawn" | "watch";
22
+ taskId?: string;
23
+ sessionName?: string;
24
+ command?: string;
25
+ cwd?: string;
26
+ env?: Record<string, string>;
27
+ ssh: SshConnectionParams;
28
+ remote?: RemoteTaskParams;
29
+ }
30
+
31
+ export type TmuxPackageManager = "apt-get" | "dnf" | "yum" | "apk" | "pacman" | "zypper" | "brew";
32
+
33
+ type TmuxBootstrapCapability = {
34
+ target: string;
35
+ tmuxPath: string;
36
+ tmuxVersion: string;
37
+ message: string;
38
+ };
39
+
40
+ type TmuxBootstrapGuidance = {
41
+ target: string;
42
+ mutated: boolean;
43
+ verifyCommand: string;
44
+ message: string;
45
+ };
46
+
47
+ export type TmuxBootstrapResult =
48
+ | (TmuxBootstrapCapability & {
49
+ status: "present";
50
+ mutated: false;
51
+ })
52
+ | (TmuxBootstrapCapability & {
53
+ status: "installed";
54
+ packageManager: TmuxPackageManager;
55
+ mutated: true;
56
+ installCommand: string;
57
+ verifyCommand: string;
58
+ })
59
+ | (TmuxBootstrapGuidance & {
60
+ status: "needs_user";
61
+ reason: "passwordless_sudo_unavailable" | "install_disabled";
62
+ packageManager: TmuxPackageManager;
63
+ mutated: false;
64
+ installCommand: string;
65
+ })
66
+ | (TmuxBootstrapGuidance & {
67
+ status: "unknown_package_manager";
68
+ mutated: false;
69
+ })
70
+ | (TmuxBootstrapGuidance & {
71
+ status: "install_failed";
72
+ packageManager?: TmuxPackageManager;
73
+ exitCode: number | null;
74
+ installCommand?: string;
75
+ })
76
+ | (TmuxBootstrapGuidance & {
77
+ status: "timed_out";
78
+ packageManager?: TmuxPackageManager;
79
+ installCommand?: string;
80
+ });
81
+
82
+ export interface TmuxBootstrapOptions {
83
+ timeoutMs?: number;
84
+ }
85
+
86
+ export interface TmuxSessionPollResult {
87
+ status: "running" | "missing" | "timed_out" | number;
88
+ logSize: number;
89
+ output: string;
90
+ commandResult: CommandResult;
91
+ }
92
+
93
+ export interface ResolvedSshRemoteTask {
94
+ commandSpec: CommandSpec;
95
+ metadata: {
96
+ ssh: ResolvedSshIdentity;
97
+ remote: ResolvedRemoteTaskMetadata;
98
+ };
99
+ bootstrapTmux(options?: TmuxBootstrapOptions): Promise<TmuxBootstrapResult>;
100
+ startTmuxSession(tmuxPath: string): Promise<CommandResult>;
101
+ pollTmuxSession(logOffset: number, timeoutMs?: number): Promise<TmuxSessionPollResult>;
102
+ killTmuxSession(): Promise<CommandResult>;
103
+ spawn(logPath: string, detached: boolean): SpawnedProcess;
104
+ runOnce(maxBufferBytes?: number, timeoutMs?: number): Promise<CommandResult>;
105
+ }
106
+
107
+ const processRemoteRunner: RemoteRunner = {
108
+ spawn: spawnCommand,
109
+ runOnce: runCommandOnce,
110
+ };
111
+
112
+ const REQUIRED_SSH_OPTIONS = new Set(["batchmode", "connecttimeout", "requesttty"]);
113
+ const DIRECT_STOP_WARNING = "Direct SSH mode has weak stop semantics: stopping the local SSH client may leave the remote process running.";
114
+ const TMUX_STATUS_PREFIX = "__PI_BG_STATUS__=";
115
+ const TMUX_SIZE_PREFIX = "__PI_BG_SIZE__=";
116
+ const TMUX_CAPTURE_CHUNK_BYTES = 256 * 1024;
117
+ const TMUX_PROBE_COMMAND = "tmux_path=$(command -v tmux) || exit 127; printf '%s\\n' \"$tmux_path\" && \"$tmux_path\" -V";
118
+ const TMUX_PACKAGE_MANAGERS: TmuxPackageManager[] = ["apt-get", "dnf", "yum", "apk", "pacman", "zypper", "brew"];
119
+ const TMUX_INSTALL_COMMANDS: Record<TmuxPackageManager, string> = {
120
+ "apt-get": "apt-get update && apt-get install -y tmux",
121
+ dnf: "dnf install -y tmux",
122
+ yum: "yum install -y tmux",
123
+ apk: "apk add --no-cache tmux",
124
+ pacman: "pacman -Sy --noconfirm tmux",
125
+ zypper: "zypper --non-interactive install tmux",
126
+ brew: "brew install tmux",
127
+ };
128
+ const TMUX_DETECT_COMMAND = [
129
+ "remote_user=$(id -un) || exit 1",
130
+ "remote_uid=$(id -u) || exit 1",
131
+ "package_manager=''",
132
+ `for candidate in ${TMUX_PACKAGE_MANAGERS.join(" ")}; do if command -v \"$candidate\" >/dev/null 2>&1; then package_manager=$candidate; break; fi; done`,
133
+ "if [ \"$package_manager\" = brew ]; then privilege=direct; elif [ \"$remote_uid\" -eq 0 ]; then privilege=root; elif command -v sudo >/dev/null 2>&1 && sudo -n true >/dev/null 2>&1; then privilege=sudo; else privilege=needs_user; fi",
134
+ "printf 'user=%s\\nuid=%s\\npm=%s\\nprivilege=%s\\n' \"$remote_user\" \"$remote_uid\" \"$package_manager\" \"$privilege\"",
135
+ ].join("; ");
136
+
137
+ export function expandSshRemoteTaskPreset(
138
+ intent: SshRemoteTaskIntent,
139
+ runner: RemoteRunner = processRemoteRunner,
140
+ ): ResolvedSshRemoteTask {
141
+ const command = requireValue(intent.command, "command is required when ssh is set");
142
+ const host = requireToken(intent.ssh.host, "ssh.host is required");
143
+ const user = optionalToken(intent.ssh.user, "ssh.user must not be empty or contain whitespace");
144
+ const target = user ? `${user}@${host}` : host;
145
+ const argv = [
146
+ "ssh",
147
+ "-o", "BatchMode=yes",
148
+ "-o", `ConnectTimeout=${DEFAULT_SSH_CONNECT_TIMEOUT_SECONDS}`,
149
+ "-T",
150
+ ];
151
+
152
+ if (intent.ssh.port !== undefined) {
153
+ if (!Number.isInteger(intent.ssh.port) || intent.ssh.port < 1 || intent.ssh.port > 65_535) {
154
+ throw new Error("ssh.port must be an integer between 1 and 65535");
155
+ }
156
+ argv.push("-p", String(intent.ssh.port));
157
+ }
158
+
159
+ const identityFile = optionalValue(intent.ssh.identity_file, "ssh.identity_file must not be empty");
160
+ if (identityFile) argv.push("-i", identityFile);
161
+ const jump = optionalValue(intent.ssh.jump, "ssh.jump must not be empty");
162
+ if (jump) argv.push("-J", jump);
163
+
164
+ for (const [rawKey, rawValue] of Object.entries(intent.ssh.options ?? {})) {
165
+ const key = requireToken(rawKey, "ssh option names must not be empty or contain whitespace");
166
+ if (REQUIRED_SSH_OPTIONS.has(key.toLowerCase())) continue;
167
+ argv.push("-o", `${key}=${String(rawValue)}`);
168
+ }
169
+ argv.push("--", target, command);
170
+
171
+ const commandSpec: CommandSpec = {
172
+ command,
173
+ argv,
174
+ shell: false,
175
+ cwd: intent.cwd,
176
+ env: intent.env,
177
+ };
178
+ const ssh: ResolvedSshIdentity = {
179
+ host,
180
+ ...(user ? { user } : {}),
181
+ ...(intent.ssh.port !== undefined ? { port: intent.ssh.port } : {}),
182
+ ...(identityFile ? { identityFile } : {}),
183
+ ...(jump ? { jump } : {}),
184
+ ...(intent.ssh.options ? { options: { ...intent.ssh.options } } : {}),
185
+ target,
186
+ };
187
+ const session = intent.operation === "spawn" ? intent.remote?.session ?? "tmux" : "direct";
188
+ const installTmux = intent.operation === "spawn" && session === "tmux"
189
+ ? intent.remote?.install_tmux !== false
190
+ : false;
191
+ const sessionName = session === "tmux"
192
+ ? intent.sessionName ?? (intent.taskId ? sessionNameForTask(intent.taskId) : undefined)
193
+ : undefined;
194
+ const remote: ResolvedRemoteTaskMetadata = {
195
+ command,
196
+ session,
197
+ installTmux,
198
+ ...(intent.remote?.workdir !== undefined ? { workdir: intent.remote.workdir } : {}),
199
+ ...(sessionName ? { sessionName, bootstrapStatus: "pending", sessionStarted: false } : {}),
200
+ ...(session === "direct" && intent.operation === "spawn" ? { warning: DIRECT_STOP_WARNING } : {}),
201
+ };
202
+
203
+ const requireSessionName = (): string => {
204
+ if (!sessionName) throw new Error("taskId is required for tmux-backed SSH spawn lifecycle operations");
205
+ return sessionName;
206
+ };
207
+
208
+ return {
209
+ commandSpec,
210
+ metadata: { ssh, remote },
211
+ bootstrapTmux: async (options) => bootstrapTmux(
212
+ commandSpec,
213
+ target,
214
+ runner,
215
+ installTmux,
216
+ resolveBootstrapTimeoutMs(options?.timeoutMs),
217
+ ),
218
+ startTmuxSession: (tmuxPath) => runner.runOnce(withRemoteCommand(
219
+ commandSpec,
220
+ tmuxStartCommand(tmuxPath, requireSessionName(), command, intent.remote?.workdir),
221
+ )),
222
+ pollTmuxSession: async (logOffset, timeoutMs) => parseTmuxPollResult(await runner.runOnce(withRemoteCommand(
223
+ commandSpec,
224
+ tmuxPollCommand(requireSessionName(), logOffset),
225
+ ), undefined, timeoutMs), logOffset),
226
+ killTmuxSession: () => runner.runOnce(withRemoteCommand(
227
+ commandSpec,
228
+ `tmux kill-session -t ${shellQuote(requireSessionName())}`,
229
+ )),
230
+ spawn: (logPath, detached) => runner.spawn(commandSpec, logPath, detached),
231
+ runOnce: (maxBufferBytes, timeoutMs) => runner.runOnce(commandSpec, maxBufferBytes, timeoutMs),
232
+ };
233
+ }
234
+
235
+ function sessionNameForTask(taskId: string): string {
236
+ return `pi-bg-${taskId.replaceAll(/[^A-Za-z0-9_-]/g, "-")}`;
237
+ }
238
+
239
+ function tmuxLogPath(sessionName: string): string {
240
+ return `/tmp/${sessionName}.log`;
241
+ }
242
+
243
+ function tmuxExitPath(sessionName: string): string {
244
+ return `/tmp/${sessionName}.exit`;
245
+ }
246
+
247
+ function tmuxStartCommand(tmuxPath: string, sessionName: string, command: string, workdir?: string): string {
248
+ const logPath = tmuxLogPath(sessionName);
249
+ const exitPath = tmuxExitPath(sessionName);
250
+ const script = [
251
+ ...(workdir ? [`cd -- ${shellQuote(workdir)}`] : []),
252
+ `sh -lc ${shellQuote(command)} >${shellQuote(logPath)} 2>&1`,
253
+ "exit_code=$?",
254
+ `printf '%s\\n' "$exit_code" >${shellQuote(exitPath)}`,
255
+ "exit \"$exit_code\"",
256
+ ].join("; ");
257
+ return [
258
+ `rm -f ${shellQuote(logPath)} ${shellQuote(exitPath)}`,
259
+ `${shellQuote(tmuxPath)} new-session -d -s ${shellQuote(sessionName)} sh -lc ${shellQuote(script)}`,
260
+ ].join("; ");
261
+ }
262
+
263
+ function tmuxPollCommand(sessionName: string, logOffset: number): string {
264
+ const normalizedOffset = Math.max(0, Math.floor(logOffset));
265
+ const logPath = tmuxLogPath(sessionName);
266
+ const exitPath = tmuxExitPath(sessionName);
267
+ return [
268
+ "status=running",
269
+ `if test -f ${shellQuote(exitPath)}; then status=$(cat ${shellQuote(exitPath)}); elif ! tmux has-session -t ${shellQuote(sessionName)} 2>/dev/null; then status=missing; fi`,
270
+ `size=$(wc -c < ${shellQuote(logPath)} 2>/dev/null || printf '0')`,
271
+ "size=$(printf '%s' \"$size\" | tr -d '[:space:]')",
272
+ `next_offset=$(( ${normalizedOffset} + ${TMUX_CAPTURE_CHUNK_BYTES} ))`,
273
+ "if test \"$next_offset\" -gt \"$size\"; then next_offset=$size; fi",
274
+ "reported_status=$status",
275
+ "if test \"$next_offset\" -lt \"$size\"; then reported_status=running; fi",
276
+ `printf '${TMUX_STATUS_PREFIX}%s\\n${TMUX_SIZE_PREFIX}%s\\n' "$reported_status" "$next_offset"`,
277
+ `if test "$next_offset" -gt ${normalizedOffset}; then tail -c +${normalizedOffset + 1} ${shellQuote(logPath)} | head -c $(( next_offset - ${normalizedOffset} )); fi`,
278
+ ].join("; ");
279
+ }
280
+
281
+ function parseTmuxPollResult(result: CommandResult, logOffset: number): TmuxSessionPollResult {
282
+ if (result.timedOut) {
283
+ return {
284
+ status: "timed_out",
285
+ logSize: Math.max(0, Math.floor(logOffset)),
286
+ output: result.stdout,
287
+ commandResult: result,
288
+ };
289
+ }
290
+ const [statusLine = "", sizeLine = "", ...outputLines] = result.stdout.split("\n");
291
+ if (!statusLine.startsWith(TMUX_STATUS_PREFIX) || !sizeLine.startsWith(TMUX_SIZE_PREFIX)) {
292
+ throw new Error(`remote tmux supervision returned an invalid response${result.stderr.trim() ? `: ${result.stderr.trim()}` : ""}`);
293
+ }
294
+ const rawStatus = statusLine.slice(TMUX_STATUS_PREFIX.length);
295
+ const rawSize = sizeLine.slice(TMUX_SIZE_PREFIX.length);
296
+ const logSize = Number(rawSize);
297
+ if (!Number.isSafeInteger(logSize) || logSize < 0) {
298
+ throw new Error(`remote tmux supervision returned invalid log size ${JSON.stringify(rawSize)}`);
299
+ }
300
+ const status = rawStatus === "running" || rawStatus === "missing"
301
+ ? rawStatus
302
+ : Number(rawStatus);
303
+ if (typeof status === "number" && !Number.isInteger(status)) {
304
+ throw new Error(`remote tmux supervision returned invalid status ${JSON.stringify(rawStatus)}`);
305
+ }
306
+ return {
307
+ status,
308
+ logSize,
309
+ output: outputLines.join("\n"),
310
+ commandResult: result,
311
+ };
312
+ }
313
+
314
+ async function bootstrapTmux(
315
+ commandSpec: CommandSpec,
316
+ target: string,
317
+ runner: RemoteRunner,
318
+ installEnabled: boolean,
319
+ timeoutMs: number,
320
+ ): Promise<TmuxBootstrapResult> {
321
+ const deadlineAt = Date.now() + timeoutMs;
322
+ let timeoutContext: TmuxBootstrapTimeoutContext = {
323
+ target,
324
+ mutated: false,
325
+ verifyCommand: sshGuidanceCommand(commandSpec, target, "command -v tmux && tmux -V"),
326
+ stage: "probing tmux",
327
+ };
328
+ const run = async (remoteCommand: string): Promise<CommandResult> => {
329
+ const remainingMs = deadlineAt - Date.now();
330
+ if (remainingMs <= 0) throw new TmuxBootstrapTimeoutError();
331
+ const result = await runner.runOnce(withRemoteCommand(commandSpec, remoteCommand), undefined, remainingMs);
332
+ if (result.timedOut) throw new TmuxBootstrapTimeoutError();
333
+ return result;
334
+ };
335
+
336
+ try {
337
+ return await bootstrapTmuxWithinDeadline(run, commandSpec, target, installEnabled, timeoutContext, (context) => {
338
+ timeoutContext = context;
339
+ });
340
+ } catch (error) {
341
+ if (!(error instanceof TmuxBootstrapTimeoutError)) throw error;
342
+ const remediation = timeoutContext.installCommand
343
+ ? `Run: ${timeoutContext.installCommand} Then verify: ${timeoutContext.verifyCommand}`
344
+ : `Install tmux manually if needed, then verify: ${timeoutContext.verifyCommand}`;
345
+ return {
346
+ status: "timed_out",
347
+ target: timeoutContext.target,
348
+ ...(timeoutContext.packageManager ? { packageManager: timeoutContext.packageManager } : {}),
349
+ mutated: timeoutContext.mutated,
350
+ ...(timeoutContext.installCommand ? { installCommand: timeoutContext.installCommand } : {}),
351
+ verifyCommand: timeoutContext.verifyCommand,
352
+ message: `tmux bootstrap timed out on ${timeoutContext.target} while ${timeoutContext.stage}. ${remediation}`,
353
+ };
354
+ }
355
+ }
356
+
357
+ interface TmuxBootstrapTimeoutContext {
358
+ target: string;
359
+ packageManager?: TmuxPackageManager;
360
+ mutated: boolean;
361
+ installCommand?: string;
362
+ verifyCommand: string;
363
+ stage: string;
364
+ }
365
+
366
+ class TmuxBootstrapTimeoutError extends Error {}
367
+
368
+ async function bootstrapTmuxWithinDeadline(
369
+ run: (remoteCommand: string) => Promise<CommandResult>,
370
+ commandSpec: CommandSpec,
371
+ target: string,
372
+ installEnabled: boolean,
373
+ initialTimeoutContext: TmuxBootstrapTimeoutContext,
374
+ setTimeoutContext: (context: TmuxBootstrapTimeoutContext) => void,
375
+ ): Promise<TmuxBootstrapResult> {
376
+ setTimeoutContext(initialTimeoutContext);
377
+ const probe = await run(TMUX_PROBE_COMMAND);
378
+ const capability = parseTmuxCapability(probe);
379
+ if (capability) {
380
+ return {
381
+ status: "present",
382
+ target,
383
+ ...capability,
384
+ mutated: false,
385
+ message: `${capability.tmuxVersion} is available at ${capability.tmuxPath} on ${target}.`,
386
+ };
387
+ }
388
+ const initialVerifyCommand = sshGuidanceCommand(commandSpec, target, "command -v tmux && tmux -V");
389
+ if (probe.exitCode !== 127) {
390
+ const detail = resultDetail(probe);
391
+ return {
392
+ status: "install_failed",
393
+ target,
394
+ exitCode: probe.exitCode,
395
+ mutated: false,
396
+ verifyCommand: initialVerifyCommand,
397
+ message: `tmux bootstrap could not probe ${target} (exit ${probe.exitCode ?? "unknown"})${detail ? `: ${detail}` : "."} Install tmux manually if needed, then verify: ${initialVerifyCommand}`,
398
+ };
399
+ }
400
+
401
+ setTimeoutContext({ ...initialTimeoutContext, stage: "detecting the package manager" });
402
+ const detection = await run(TMUX_DETECT_COMMAND);
403
+ if (detection.exitCode !== 0) {
404
+ const detail = resultDetail(detection);
405
+ return {
406
+ status: "install_failed",
407
+ target,
408
+ exitCode: detection.exitCode,
409
+ mutated: false,
410
+ verifyCommand: initialVerifyCommand,
411
+ message: `tmux bootstrap could not inspect ${target} for an installer (exit ${detection.exitCode ?? "unknown"})${detail ? `: ${detail}` : "."} Install tmux manually if needed, then verify: ${initialVerifyCommand}`,
412
+ };
413
+ }
414
+ const detected = parseDetection(detection);
415
+ const guidanceTarget = detected && !target.includes("@") ? `${detected.user}@${target}` : target;
416
+ const verifyCommand = sshGuidanceCommand(commandSpec, guidanceTarget, "command -v tmux && tmux -V");
417
+ if (!detected || !isTmuxPackageManager(detected.packageManager)) {
418
+ return {
419
+ status: "unknown_package_manager",
420
+ target: guidanceTarget,
421
+ mutated: false,
422
+ verifyCommand,
423
+ message: `tmux is missing on ${guidanceTarget}, but none of ${formatPackageManagerList()} was found. Install tmux manually, then verify: ${verifyCommand}`,
424
+ };
425
+ }
426
+
427
+ const packageManager = detected.packageManager;
428
+ const baseInstallCommand = TMUX_INSTALL_COMMANDS[packageManager];
429
+ const needsSudo = detected.privilege === "sudo" || detected.privilege === "needs_user";
430
+ const humanInstallCommand = needsSudo ? withInteractiveSudo(baseInstallCommand) : baseInstallCommand;
431
+ const installCommand = sshGuidanceCommand(commandSpec, guidanceTarget, humanInstallCommand, needsSudo);
432
+ if (!installEnabled) {
433
+ return {
434
+ status: "needs_user",
435
+ reason: "install_disabled",
436
+ target: guidanceTarget,
437
+ packageManager,
438
+ mutated: false,
439
+ installCommand,
440
+ verifyCommand,
441
+ message: `Automatic tmux installation is disabled for ${guidanceTarget}. Run: ${installCommand} Then verify: ${verifyCommand}`,
442
+ };
443
+ }
444
+ if (detected.privilege === "needs_user") {
445
+ return {
446
+ status: "needs_user",
447
+ reason: "passwordless_sudo_unavailable",
448
+ target: guidanceTarget,
449
+ packageManager,
450
+ mutated: false,
451
+ installCommand,
452
+ verifyCommand,
453
+ message: `tmux is missing on ${guidanceTarget} and automatic installation cannot use passwordless sudo. Run: ${installCommand} Then verify: ${verifyCommand}`,
454
+ };
455
+ }
456
+
457
+ const machineInstallCommand = detected.privilege === "sudo"
458
+ ? withNonInteractiveSudo(baseInstallCommand)
459
+ : baseInstallCommand;
460
+ setTimeoutContext({
461
+ target: guidanceTarget,
462
+ packageManager,
463
+ mutated: false,
464
+ installCommand,
465
+ verifyCommand,
466
+ stage: `running the ${packageManager} install`,
467
+ });
468
+ const install = await run(machineInstallCommand);
469
+ if (install.exitCode !== 0) {
470
+ const detail = resultDetail(install);
471
+ return {
472
+ status: "install_failed",
473
+ target: guidanceTarget,
474
+ packageManager,
475
+ exitCode: install.exitCode,
476
+ mutated: false,
477
+ installCommand,
478
+ verifyCommand,
479
+ message: `Automatic tmux install with ${packageManager} failed on ${guidanceTarget} (exit ${install.exitCode ?? "unknown"})${detail ? `: ${detail}` : "."} Run: ${installCommand} Then verify: ${verifyCommand}`,
480
+ };
481
+ }
482
+
483
+ setTimeoutContext({
484
+ target: guidanceTarget,
485
+ packageManager,
486
+ mutated: true,
487
+ installCommand,
488
+ verifyCommand,
489
+ stage: "verifying the installed tmux",
490
+ });
491
+ const reprobe = await run(TMUX_PROBE_COMMAND);
492
+ const installedCapability = parseTmuxCapability(reprobe);
493
+ if (!installedCapability) {
494
+ return {
495
+ status: "install_failed",
496
+ target: guidanceTarget,
497
+ packageManager,
498
+ exitCode: reprobe.exitCode,
499
+ mutated: true,
500
+ installCommand,
501
+ verifyCommand,
502
+ message: `The ${packageManager} install command completed but tmux did not pass verification on ${guidanceTarget}${resultDetail(reprobe) ? `: ${resultDetail(reprobe)}` : "."} Run: ${installCommand} Then verify: ${verifyCommand}`,
503
+ };
504
+ }
505
+ return {
506
+ status: "installed",
507
+ target: guidanceTarget,
508
+ packageManager,
509
+ ...installedCapability,
510
+ mutated: true,
511
+ installCommand,
512
+ verifyCommand,
513
+ message: `Installed tmux with ${packageManager} on ${guidanceTarget}; ${installedCapability.tmuxVersion} is available at ${installedCapability.tmuxPath}.`,
514
+ };
515
+ }
516
+
517
+ function resolveBootstrapTimeoutMs(timeoutMs: number | undefined): number {
518
+ if (timeoutMs === undefined) return DEFAULT_TMUX_BOOTSTRAP_TIMEOUT_MS;
519
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
520
+ throw new Error("tmux bootstrap timeoutMs must be a positive finite number");
521
+ }
522
+ return Math.floor(timeoutMs);
523
+ }
524
+
525
+ function parseTmuxCapability(result: CommandResult): { tmuxPath: string; tmuxVersion: string } | undefined {
526
+ const [tmuxPath, tmuxVersion] = result.stdout.trim().split("\n");
527
+ if (result.exitCode !== 0 || !tmuxPath || !tmuxVersion) return undefined;
528
+ return { tmuxPath, tmuxVersion };
529
+ }
530
+
531
+ interface TmuxDetection {
532
+ user: string;
533
+ packageManager: string;
534
+ privilege: "root" | "sudo" | "direct" | "needs_user";
535
+ }
536
+
537
+ function parseDetection(result: CommandResult): TmuxDetection | undefined {
538
+ if (result.exitCode !== 0) return undefined;
539
+ const values = Object.fromEntries(result.stdout.trim().split("\n").map((line) => {
540
+ const separator = line.indexOf("=");
541
+ return separator < 0 ? [line, ""] : [line.slice(0, separator), line.slice(separator + 1)];
542
+ }));
543
+ if (!values.user || !/^(root|sudo|direct|needs_user)$/.test(values.privilege ?? "")) return undefined;
544
+ return {
545
+ user: values.user,
546
+ packageManager: values.pm ?? "",
547
+ privilege: values.privilege as TmuxDetection["privilege"],
548
+ };
549
+ }
550
+
551
+ function isTmuxPackageManager(value: string | undefined): value is TmuxPackageManager {
552
+ return TMUX_PACKAGE_MANAGERS.some((candidate) => candidate === value);
553
+ }
554
+
555
+ function formatPackageManagerList(): string {
556
+ return `${TMUX_PACKAGE_MANAGERS.slice(0, -1).join(", ")}, or ${TMUX_PACKAGE_MANAGERS.at(-1)}`;
557
+ }
558
+
559
+ function resultDetail(result: CommandResult): string {
560
+ return (result.stderr.trim() || result.stdout.trim()).replaceAll(/\s+/g, " ").slice(0, 500);
561
+ }
562
+
563
+ function withNonInteractiveSudo(command: string): string {
564
+ return command.split(" && ").map((part) => `sudo -n ${part}`).join(" && ");
565
+ }
566
+
567
+ function withInteractiveSudo(command: string): string {
568
+ return command.split(" && ").map((part) => `sudo ${part}`).join(" && ");
569
+ }
570
+
571
+ function sshGuidanceCommand(
572
+ spec: CommandSpec,
573
+ target: string,
574
+ remoteCommand: string,
575
+ tty = false,
576
+ ): string {
577
+ const argv = spec.argv ?? [];
578
+ const separator = argv.lastIndexOf("--");
579
+ const connectionArgs: string[] = [];
580
+ for (let index = 1; index >= 0 && index < separator; index += 1) {
581
+ const option = argv[index];
582
+ if (option === "-T") continue;
583
+ const value = argv[index + 1];
584
+ if ((option === "-p" || option === "-i" || option === "-J") && value) {
585
+ connectionArgs.push(option, shellQuote(value));
586
+ index += 1;
587
+ continue;
588
+ }
589
+ if (option === "-o" && value) {
590
+ index += 1;
591
+ const key = value.slice(0, value.indexOf("=")).toLowerCase();
592
+ if (REQUIRED_SSH_OPTIONS.has(key)) continue;
593
+ connectionArgs.push(option, shellQuote(value));
594
+ }
595
+ }
596
+ const renderedArgs = connectionArgs.length > 0 ? ` ${connectionArgs.join(" ")}` : "";
597
+ return `ssh${tty ? " -t" : ""}${renderedArgs} ${shellQuote(target)} ${shellQuote(remoteCommand)}`;
598
+ }
599
+
600
+ function shellQuote(value: string): string {
601
+ return `'${value.replaceAll("'", `'\"'\"'`)}'`;
602
+ }
603
+
604
+ function withRemoteCommand(spec: CommandSpec, remoteCommand: string): CommandSpec {
605
+ return {
606
+ ...spec,
607
+ command: remoteCommand,
608
+ argv: [...(spec.argv?.slice(0, -1) ?? []), remoteCommand],
609
+ };
610
+ }
611
+
612
+ function requireValue(value: string | undefined, message: string): string {
613
+ if (!value || value.trim().length === 0) throw new Error(message);
614
+ return value;
615
+ }
616
+
617
+ function requireToken(value: string | undefined, message: string): string {
618
+ const resolved = requireValue(value, message).trim();
619
+ if (/\s/.test(resolved)) throw new Error(message);
620
+ return resolved;
621
+ }
622
+
623
+ function optionalValue(value: string | undefined, message: string): string | undefined {
624
+ if (value === undefined) return undefined;
625
+ return requireValue(value, message);
626
+ }
627
+
628
+ function optionalToken(value: string | undefined, message: string): string | undefined {
629
+ if (value === undefined) return undefined;
630
+ return requireToken(value, message);
631
+ }