@zswarm/core 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/assets/host-install.sh +36 -2
- package/assets/host-report.sh +15 -1
- package/dist/harness.d.ts +10 -0
- package/dist/harness.js +33 -1
- package/dist/host/cli.js +51 -5
- package/dist/host/crew.d.ts +2 -0
- package/dist/host/crew.js +13 -0
- package/dist/host/host.js +13 -0
- package/dist/host/relay.d.ts +63 -1
- package/dist/host/relay.js +241 -2
- package/dist/host/services.d.ts +9 -2
- package/dist/host/services.js +105 -9
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/ops/delivery.d.ts +51 -2
- package/dist/ops/delivery.js +162 -31
- package/dist/ops/dispatch.js +83 -10
- package/dist/schema.js +44 -3
- package/dist/zellij/client.d.ts +1 -0
- package/dist/zellij/client.js +16 -0
- package/dist/zellij/session.d.ts +11 -0
- package/dist/zellij/session.js +35 -3
- package/package.json +1 -1
package/dist/host/relay.js
CHANGED
|
@@ -11,6 +11,10 @@ const DEFAULT_DELAYS = [0, 30, 60, 120, 300, 600, 900, 1500];
|
|
|
11
11
|
/** Consecutive matches of the done regex without a passing check before UNCONFIRMED. */
|
|
12
12
|
const CONFIRM_TRIES = 3;
|
|
13
13
|
const SERVE_DOWN_TRIES = 3;
|
|
14
|
+
/** Consecutive identical failing checks before CHECK_BROKEN: a check that can never pass. */
|
|
15
|
+
const CHECK_BROKEN_TRIES = 10;
|
|
16
|
+
/** How long a relay may outlive `--timeout-min` when `--max-age` is not given. */
|
|
17
|
+
const DEFAULT_MAX_AGE_EXTRA_MIN = 60;
|
|
14
18
|
/** A wait that only finds `seen` lines returns at once; poll no faster than this. */
|
|
15
19
|
const STALE_MATCH_POLL_MS = 15_000;
|
|
16
20
|
export function relaysDir(env = process.env) {
|
|
@@ -78,6 +82,9 @@ export function fillMessage(template, fields) {
|
|
|
78
82
|
const DEFAULT_TEMPLATE = "RELAY {id} STATUS {status} pane {pane}\n{output}{note}\n";
|
|
79
83
|
const NOTES = {
|
|
80
84
|
TIMEOUT: "no done line or passing check before the deadline; dump the pane once, do not resend blindly.",
|
|
85
|
+
MAX_AGE: "the relay reached --max-age without its done line; check the target pane and --done before resending.",
|
|
86
|
+
CHECK_BROKEN: "the check script failed the same way ten times in a row; fix the check (or --done) before resending.",
|
|
87
|
+
CANCELLED: "the relay was cancelled before it finished; nothing was pasted.",
|
|
81
88
|
PANE_DIED: "the target pane is gone or exited; check the crew (zswarm crew status) before resending.",
|
|
82
89
|
SERVE_DOWN: "serve was unreachable for three checks in a row; run zswarm doctor against it.",
|
|
83
90
|
UNCONFIRMED: "the pane printed the done line but the check never passed; dump the pane once for the error.",
|
|
@@ -130,12 +137,20 @@ export async function relayLoop(dir, deps = {}) {
|
|
|
130
137
|
ZSWARM_RELAY_PANE: cfg.to,
|
|
131
138
|
ZSWARM_RELAY_DIR: dir,
|
|
132
139
|
});
|
|
133
|
-
|
|
140
|
+
// The timeout is the normal end; --max-age (default timeout + 60 min) is the
|
|
141
|
+
// hard cap that still ends the relay when a wait or the serve-down backoff
|
|
142
|
+
// overruns the timeout.
|
|
143
|
+
const startedAtMs = now();
|
|
144
|
+
const timeoutAt = startedAtMs + cfg.timeoutMin * 60_000;
|
|
145
|
+
const maxAgeAt = startedAtMs + (cfg.maxAgeMin ?? cfg.timeoutMin + DEFAULT_MAX_AGE_EXTRA_MIN) * 60_000;
|
|
146
|
+
const deadline = Math.min(timeoutAt, maxAgeAt);
|
|
134
147
|
let status = "TIMEOUT";
|
|
135
148
|
let output = "";
|
|
136
149
|
let match = "";
|
|
137
150
|
let confirmTries = 0;
|
|
138
151
|
let serveDown = 0;
|
|
152
|
+
let lastCheckFail;
|
|
153
|
+
let sameCheckFails = 0;
|
|
139
154
|
log(`relay start id=${cfg.id} to=${cfg.to} serve=${cfg.serve} done=${cfg.done}`);
|
|
140
155
|
while (now() < deadline) {
|
|
141
156
|
const started = now();
|
|
@@ -163,6 +178,20 @@ export async function relayLoop(dir, deps = {}) {
|
|
|
163
178
|
output = r.stdout;
|
|
164
179
|
break;
|
|
165
180
|
}
|
|
181
|
+
// The same failure over and over means the check can never pass for this
|
|
182
|
+
// pane: end as CHECK_BROKEN rather than polling to the timeout.
|
|
183
|
+
const sig = `${r.status}:${r.stdout}`;
|
|
184
|
+
if (sig === lastCheckFail)
|
|
185
|
+
sameCheckFails += 1;
|
|
186
|
+
else {
|
|
187
|
+
lastCheckFail = sig;
|
|
188
|
+
sameCheckFails = 1;
|
|
189
|
+
}
|
|
190
|
+
if (sameCheckFails >= CHECK_BROKEN_TRIES) {
|
|
191
|
+
status = "CHECK_BROKEN";
|
|
192
|
+
output = r.stdout;
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
166
195
|
}
|
|
167
196
|
else {
|
|
168
197
|
log(`wait ok=${res.ok} matched=${matched}${stale ? " (seen before start)" : ""}`);
|
|
@@ -207,6 +236,9 @@ export async function relayLoop(dir, deps = {}) {
|
|
|
207
236
|
if (matched)
|
|
208
237
|
await sleep(30_000); // the done line stays on screen; give the check time
|
|
209
238
|
}
|
|
239
|
+
if (status === "TIMEOUT" && maxAgeAt <= timeoutAt && now() >= maxAgeAt)
|
|
240
|
+
status = "MAX_AGE";
|
|
241
|
+
writeFinalStatus(dir, status);
|
|
210
242
|
let body;
|
|
211
243
|
if (status !== "DONE" && check) {
|
|
212
244
|
const r = runCheck(check, checkEnv(status, match));
|
|
@@ -269,6 +301,18 @@ function updateConfig(dir, patch) {
|
|
|
269
301
|
const cfg = { ...JSON.parse(readFileSync(path, "utf8")), ...patch };
|
|
270
302
|
writePrivate(path, `${JSON.stringify(cfg, null, 1)}\n`);
|
|
271
303
|
}
|
|
304
|
+
/** Persist the final status unless `--cancel` already marked the relay. */
|
|
305
|
+
function writeFinalStatus(dir, status) {
|
|
306
|
+
try {
|
|
307
|
+
const cfg = JSON.parse(readFileSync(join(dir, "config.json"), "utf8"));
|
|
308
|
+
if (cfg.cancelled || cfg.status === "CANCELLED")
|
|
309
|
+
return;
|
|
310
|
+
updateConfig(dir, { status });
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
// the directory is gone; nothing to record
|
|
314
|
+
}
|
|
315
|
+
}
|
|
272
316
|
/** Remove the token copy once the relay no longer needs it. */
|
|
273
317
|
export function scrubRelayToken(dir) {
|
|
274
318
|
const path = join(dir, "config.json");
|
|
@@ -293,10 +337,12 @@ export function launchRelay(dir, env = process.env) {
|
|
|
293
337
|
"submit", "-l", label, "-o", log, "-e", log, "--",
|
|
294
338
|
"/usr/bin/env", `PATH=${env.PATH ?? "/usr/bin:/bin"}`, `HOME=${env.HOME ?? ""}`,
|
|
295
339
|
...(env.ZSWARM_STATE_DIR ? [`ZSWARM_STATE_DIR=${env.ZSWARM_STATE_DIR}`] : []),
|
|
340
|
+
...(env.ZSWARM_RELAY_DELAYS ? [`ZSWARM_RELAY_DELAYS=${env.ZSWARM_RELAY_DELAYS}`] : []),
|
|
296
341
|
`ZSWARM_RELAY_LABEL=${label}`, ...argv,
|
|
297
342
|
], { encoding: "utf8" });
|
|
298
343
|
if (res.status !== 0)
|
|
299
344
|
throw new ZellijError("failed", `launchctl submit failed: ${(res.stderr ?? "").trim().slice(0, 200)}`);
|
|
345
|
+
updateConfig(dir, { job: { kind: "launchd", label } });
|
|
300
346
|
return `launchd job ${label}`;
|
|
301
347
|
}
|
|
302
348
|
if (process.platform === "linux" && spawnSync("systemctl", ["--user", "is-system-running"], { encoding: "utf8" }).status !== null) {
|
|
@@ -304,13 +350,18 @@ export function launchRelay(dir, env = process.env) {
|
|
|
304
350
|
const res = spawnSync("systemd-run", ["--user", "--collect", "--quiet", `--unit=${unit}`,
|
|
305
351
|
`--setenv=PATH=${env.PATH ?? "/usr/bin:/bin"}`,
|
|
306
352
|
...(env.ZSWARM_STATE_DIR ? [`--setenv=ZSWARM_STATE_DIR=${env.ZSWARM_STATE_DIR}`] : []),
|
|
353
|
+
...(env.ZSWARM_RELAY_DELAYS ? [`--setenv=ZSWARM_RELAY_DELAYS=${env.ZSWARM_RELAY_DELAYS}`] : []),
|
|
307
354
|
...argv], { encoding: "utf8" });
|
|
308
|
-
if (res.status === 0)
|
|
355
|
+
if (res.status === 0) {
|
|
356
|
+
updateConfig(dir, { job: { kind: "systemd", unit } });
|
|
309
357
|
return `systemd unit ${unit}`;
|
|
358
|
+
}
|
|
310
359
|
}
|
|
311
360
|
const out = openLog(log);
|
|
312
361
|
const child = spawn(argv[0], argv.slice(1), { detached: true, stdio: ["ignore", out, out], env, windowsHide: true });
|
|
313
362
|
child.unref();
|
|
363
|
+
if (child.pid)
|
|
364
|
+
updateConfig(dir, { job: { kind: "pid", pid: child.pid } });
|
|
314
365
|
return `pid ${child.pid}`;
|
|
315
366
|
}
|
|
316
367
|
/** A file descriptor the detached child can inherit. */
|
|
@@ -338,6 +389,7 @@ export async function startRelay(input) {
|
|
|
338
389
|
replySession: input.replySession?.trim() || env.ZSWARM_SESSION?.trim() || env.ZELLIJ_SESSION_NAME?.trim() || undefined,
|
|
339
390
|
from: input.from?.trim() || "zswarm-relay",
|
|
340
391
|
timeoutMin: input.timeoutMin ?? 240,
|
|
392
|
+
maxAgeMin: input.maxAgeMin ?? (input.timeoutMin ?? 240) + DEFAULT_MAX_AGE_EXTRA_MIN,
|
|
341
393
|
};
|
|
342
394
|
// Read the body before the relay directory (and its token copy) exists.
|
|
343
395
|
const body = input.bodyFile ? readFileSync(input.bodyFile, "utf8") : undefined;
|
|
@@ -452,6 +504,193 @@ export async function waitForRelay(id, opts = {}) {
|
|
|
452
504
|
await sleep(opts.pollMs ?? 15_000);
|
|
453
505
|
}
|
|
454
506
|
}
|
|
507
|
+
/** A pid's command line: `ps` on POSIX, CIM on Windows. */
|
|
508
|
+
function defaultCommandLine(pid) {
|
|
509
|
+
const res = process.platform === "win32"
|
|
510
|
+
? spawnSync("powershell.exe", ["-NoProfile", "-Command",
|
|
511
|
+
`(Get-CimInstance Win32_Process -Filter "ProcessId=${pid}").CommandLine`], { encoding: "utf8", windowsHide: true })
|
|
512
|
+
: spawnSync("ps", ["-o", "command=", "-p", String(pid)], { encoding: "utf8" });
|
|
513
|
+
const text = res.status === 0 ? (res.stdout ?? "").trim() : "";
|
|
514
|
+
return text || undefined;
|
|
515
|
+
}
|
|
516
|
+
const defaultJobRunner = (cmd, args) => {
|
|
517
|
+
const res = spawnSync(cmd, args, { encoding: "utf8" });
|
|
518
|
+
return { status: res.status ?? 1, stdout: res.stdout ?? "", stderr: res.stderr ?? (res.error?.message ?? "") };
|
|
519
|
+
};
|
|
520
|
+
/** The run directories under relaysDir, with the start time from each name. */
|
|
521
|
+
function runDirs(root) {
|
|
522
|
+
if (!existsSync(root))
|
|
523
|
+
return [];
|
|
524
|
+
const out = [];
|
|
525
|
+
for (const name of readdirSync(root)) {
|
|
526
|
+
if (name === "undelivered")
|
|
527
|
+
continue;
|
|
528
|
+
const path = join(root, name);
|
|
529
|
+
let isDir = false;
|
|
530
|
+
try {
|
|
531
|
+
isDir = statSync(path).isDirectory();
|
|
532
|
+
}
|
|
533
|
+
catch {
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
if (!isDir)
|
|
537
|
+
continue;
|
|
538
|
+
const m = /^(.*)-(\d+)$/.exec(name);
|
|
539
|
+
out.push({
|
|
540
|
+
id: m ? m[1] : name,
|
|
541
|
+
dir: path,
|
|
542
|
+
started: (m ? Number(m[2]) : Math.floor(statSync(path).mtimeMs / 1000)) * 1000,
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
return out;
|
|
546
|
+
}
|
|
547
|
+
function readRelayConfig(dir) {
|
|
548
|
+
try {
|
|
549
|
+
return JSON.parse(readFileSync(join(dir, "config.json"), "utf8"));
|
|
550
|
+
}
|
|
551
|
+
catch {
|
|
552
|
+
return undefined;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
/** The state a relay dir already records: a cancel marker, a final status, a message, else running. */
|
|
556
|
+
function relayState(dir, cfg) {
|
|
557
|
+
if (cfg?.cancelled || cfg?.status === "CANCELLED")
|
|
558
|
+
return "cancelled";
|
|
559
|
+
if (cfg?.status)
|
|
560
|
+
return cfg.status.toLowerCase();
|
|
561
|
+
const message = join(dir, "message.txt");
|
|
562
|
+
if (existsSync(message)) {
|
|
563
|
+
const m = /^\s*RELAY \S+ STATUS (\S+)/m.exec(readFileSync(message, "utf8"));
|
|
564
|
+
return (m?.[1] ?? "done").toLowerCase();
|
|
565
|
+
}
|
|
566
|
+
return "running";
|
|
567
|
+
}
|
|
568
|
+
/** The last `check=<n>` a relay logged, if any. */
|
|
569
|
+
function lastCheckResult(dir) {
|
|
570
|
+
try {
|
|
571
|
+
const matches = [...readFileSync(join(dir, "relay.log"), "utf8").matchAll(/check=(-?\d+)/g)];
|
|
572
|
+
const last = matches.at(-1)?.[1];
|
|
573
|
+
return last === undefined ? undefined : Number(last);
|
|
574
|
+
}
|
|
575
|
+
catch {
|
|
576
|
+
return undefined;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
function jobLabelText(job) {
|
|
580
|
+
if (job.kind === "launchd")
|
|
581
|
+
return `launchd job ${job.label}`;
|
|
582
|
+
if (job.kind === "systemd")
|
|
583
|
+
return `systemd unit ${job.unit}`;
|
|
584
|
+
return `pid ${job.pid}`;
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* True while the recorded job is still loaded (launchd/systemd), or while the
|
|
588
|
+
* pid is alive and its command line still names this relay's directory (a
|
|
589
|
+
* relay that died without a final status leaves a pid another process may reuse).
|
|
590
|
+
*/
|
|
591
|
+
function jobLoaded(job, deps, dir) {
|
|
592
|
+
const run = deps.run ?? defaultJobRunner;
|
|
593
|
+
if (job.kind === "launchd" && job.label)
|
|
594
|
+
return run("launchctl", ["list", job.label]).status === 0;
|
|
595
|
+
if (job.kind === "systemd" && job.unit) {
|
|
596
|
+
const res = run("systemctl", ["--user", "is-active", job.unit]);
|
|
597
|
+
return res.status === 0 && /^(active|activating|reloading)$/.test(res.stdout.trim());
|
|
598
|
+
}
|
|
599
|
+
if (job.kind === "pid" && job.pid) {
|
|
600
|
+
const alive = deps.alive ?? ((pid) => (process.kill(pid, 0), true));
|
|
601
|
+
try {
|
|
602
|
+
if (!alive(job.pid))
|
|
603
|
+
return false;
|
|
604
|
+
}
|
|
605
|
+
catch {
|
|
606
|
+
return false;
|
|
607
|
+
}
|
|
608
|
+
return (deps.commandLine ?? defaultCommandLine)(job.pid)?.includes(dir) ?? false;
|
|
609
|
+
}
|
|
610
|
+
return false;
|
|
611
|
+
}
|
|
612
|
+
/** One row per relay run directory; newest first. */
|
|
613
|
+
export function listRelays(deps = {}) {
|
|
614
|
+
const env = deps.env ?? process.env;
|
|
615
|
+
const now = deps.now ?? Date.now;
|
|
616
|
+
const rows = [];
|
|
617
|
+
for (const run of runDirs(relaysDir(env))) {
|
|
618
|
+
const cfg = readRelayConfig(run.dir);
|
|
619
|
+
if (!cfg)
|
|
620
|
+
continue;
|
|
621
|
+
const loaded = cfg.job ? jobLoaded(cfg.job, deps, run.dir) : false;
|
|
622
|
+
rows.push({
|
|
623
|
+
id: cfg.id || run.id,
|
|
624
|
+
dir: run.dir,
|
|
625
|
+
to: cfg.to,
|
|
626
|
+
session: cfg.session,
|
|
627
|
+
started: run.started,
|
|
628
|
+
age: Math.max(0, Math.floor((now() - run.started) / 1000)),
|
|
629
|
+
state: relayState(run.dir, cfg),
|
|
630
|
+
lastCheck: lastCheckResult(run.dir),
|
|
631
|
+
...(loaded && cfg.job ? { job: jobLabelText(cfg.job) } : {}),
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
return rows.sort((a, b) => b.started - a.started);
|
|
635
|
+
}
|
|
636
|
+
function stopJob(job, deps) {
|
|
637
|
+
const run = deps.run ?? defaultJobRunner;
|
|
638
|
+
if (job.kind === "launchd" && job.label) {
|
|
639
|
+
const res = run("launchctl", ["remove", job.label]);
|
|
640
|
+
if (res.status !== 0)
|
|
641
|
+
throw new ZellijError("failed", `launchctl remove ${job.label} failed: ${(res.stderr || res.stdout).trim().slice(0, 200)}`);
|
|
642
|
+
}
|
|
643
|
+
else if (job.kind === "systemd" && job.unit) {
|
|
644
|
+
const res = run("systemctl", ["--user", "stop", job.unit]);
|
|
645
|
+
if (res.status !== 0)
|
|
646
|
+
throw new ZellijError("failed", `systemctl --user stop ${job.unit} failed: ${(res.stderr || res.stdout).trim().slice(0, 200)}`);
|
|
647
|
+
}
|
|
648
|
+
else if (job.kind === "pid" && job.pid) {
|
|
649
|
+
const kill = deps.kill ?? ((pid) => { process.kill(pid, "SIGTERM"); });
|
|
650
|
+
kill(job.pid);
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
653
|
+
return "no job recorded; nothing to stop";
|
|
654
|
+
}
|
|
655
|
+
return `stopped ${jobLabelText(job)}`;
|
|
656
|
+
}
|
|
657
|
+
/** Stop a running relay's job, mark its directory CANCELLED and scrub its token. */
|
|
658
|
+
export function cancelRelay(id, deps = {}) {
|
|
659
|
+
const env = deps.env ?? process.env;
|
|
660
|
+
const now = deps.now ?? Date.now;
|
|
661
|
+
const match = runDirs(relaysDir(env))
|
|
662
|
+
.filter((r) => r.id === id)
|
|
663
|
+
.sort((a, b) => b.started - a.started)[0];
|
|
664
|
+
if (!match)
|
|
665
|
+
throw new ZellijError("usage", `no relay ${id}`);
|
|
666
|
+
const cfg = readRelayConfig(match.dir);
|
|
667
|
+
if (!cfg)
|
|
668
|
+
throw new ZellijError("failed", `relay ${id} has no config.json`);
|
|
669
|
+
const state = relayState(match.dir, cfg);
|
|
670
|
+
if (state !== "running") {
|
|
671
|
+
return { id, cancelled: false, state, reason: `relay ${id} is already ${state}; nothing to cancel`, actions: [] };
|
|
672
|
+
}
|
|
673
|
+
// Stop only a job that is still this relay's: a crashed relay's label may be
|
|
674
|
+
// gone and its pid reused. Either way the directory is marked cancelled.
|
|
675
|
+
const actions = [
|
|
676
|
+
!cfg.job
|
|
677
|
+
? "no job recorded; nothing to stop"
|
|
678
|
+
: jobLoaded(cfg.job, deps, match.dir)
|
|
679
|
+
? stopJob(cfg.job, deps)
|
|
680
|
+
: `${jobLabelText(cfg.job)} is no longer running this relay; nothing to stop`,
|
|
681
|
+
];
|
|
682
|
+
updateConfig(match.dir, { cancelled: true, status: "CANCELLED", cancelledAt: now() });
|
|
683
|
+
scrubRelayToken(match.dir);
|
|
684
|
+
actions.push(`${id} marked CANCELLED (nothing pasted)`);
|
|
685
|
+
return { id, cancelled: true, state: "cancelled", reason: `cancelled relay ${id}`, actions };
|
|
686
|
+
}
|
|
687
|
+
/** Cancel every running relay that targets `to`. */
|
|
688
|
+
export function cancelAllTo(to, deps = {}) {
|
|
689
|
+
const env = deps.env ?? process.env;
|
|
690
|
+
return listRelays({ ...deps, env })
|
|
691
|
+
.filter((row) => row.to === to && row.state === "running")
|
|
692
|
+
.map((row) => cancelRelay(row.id, { ...deps, env }));
|
|
693
|
+
}
|
|
455
694
|
/** Remove relay directories and undelivered messages older than `days`. */
|
|
456
695
|
export function pruneRelays(days = 14, env = process.env) {
|
|
457
696
|
const root = relaysDir(env);
|
package/dist/host/services.d.ts
CHANGED
|
@@ -48,8 +48,13 @@ export type Runner = (cmd: string, args: string[]) => RunResult;
|
|
|
48
48
|
export type ServiceState = {
|
|
49
49
|
name: string;
|
|
50
50
|
unit: string;
|
|
51
|
+
/** Human summary: `running (pid 42)`, `timer every 120 s, loaded`, `not loaded`. */
|
|
51
52
|
state: string;
|
|
52
53
|
active: boolean;
|
|
54
|
+
/** Main pid when the platform reports one (daemons). */
|
|
55
|
+
pid?: number;
|
|
56
|
+
/** Last exit status when the platform reports one. */
|
|
57
|
+
lastExit?: number;
|
|
53
58
|
};
|
|
54
59
|
export type ServiceHost = {
|
|
55
60
|
account: ServiceAccount;
|
|
@@ -61,6 +66,8 @@ export type ServiceHost = {
|
|
|
61
66
|
exists(path: string): boolean;
|
|
62
67
|
mkdir(path: string): void;
|
|
63
68
|
};
|
|
69
|
+
/** Synchronous sleep; injected in tests so the launchd wait is instant. */
|
|
70
|
+
sleep?: (ms: number) => void;
|
|
64
71
|
};
|
|
65
72
|
export declare const defaultRunner: Runner;
|
|
66
73
|
/** The account services are installed for: the current user, or `runAs` when root. */
|
|
@@ -87,7 +94,7 @@ export declare function ensureLinger(host: ServiceHost): {
|
|
|
87
94
|
note?: string;
|
|
88
95
|
};
|
|
89
96
|
export declare function launchdLabel(account: ServiceAccount, name: string): string;
|
|
90
|
-
export declare function launchdPlist(account: ServiceAccount, spec: ServiceSpec): string;
|
|
97
|
+
export declare function launchdPlist(account: ServiceAccount, spec: ServiceSpec, run?: Runner): string;
|
|
91
98
|
/** Write, (re)load and start the services. Returns what it did, one line per unit. */
|
|
92
99
|
export declare function installServices(host: ServiceHost, specs: ServiceSpec[]): {
|
|
93
100
|
lines: string[];
|
|
@@ -95,7 +102,7 @@ export declare function installServices(host: ServiceHost, specs: ServiceSpec[])
|
|
|
95
102
|
};
|
|
96
103
|
/** Stop and delete the services; missing ones are fine. */
|
|
97
104
|
export declare function removeServices(host: ServiceHost, specs: Array<Pick<ServiceSpec, "name" | "kind">>): string[];
|
|
98
|
-
export declare function serviceStates(host: ServiceHost, specs: Array<Pick<ServiceSpec, "name" | "kind">>): ServiceState[];
|
|
105
|
+
export declare function serviceStates(host: ServiceHost, specs: Array<Pick<ServiceSpec, "name" | "kind" | "intervalSec">>): ServiceState[];
|
|
99
106
|
/** Create dir and any missing parents, owned by the account when installing for it. */
|
|
100
107
|
export declare function ensureAccountDir(account: ServiceAccount, dir: string, mode?: number): void;
|
|
101
108
|
/** Write a mode-600 file for the account, creating its directory (700) as needed. */
|
package/dist/host/services.js
CHANGED
|
@@ -11,6 +11,14 @@ export const defaultRunner = (cmd, args) => {
|
|
|
11
11
|
const res = spawnSync(cmd, args, { encoding: "utf8" });
|
|
12
12
|
return { status: res.status ?? 1, stdout: res.stdout ?? "", stderr: res.stderr ?? (res.error?.message ?? "") };
|
|
13
13
|
};
|
|
14
|
+
/** A synchronous sleep for install paths (Node allows Atomics.wait on its main thread). */
|
|
15
|
+
function defaultSleep(ms) {
|
|
16
|
+
if (ms > 0)
|
|
17
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
18
|
+
}
|
|
19
|
+
function hostSleep(host, ms) {
|
|
20
|
+
(host.sleep ?? defaultSleep)(ms);
|
|
21
|
+
}
|
|
14
22
|
/** The account services are installed for: the current user, or `runAs` when root. */
|
|
15
23
|
export function serviceAccount(runAs, run = defaultRunner) {
|
|
16
24
|
const platform = process.platform;
|
|
@@ -181,11 +189,30 @@ function launchdPlistPath(account, label) {
|
|
|
181
189
|
function xml(text) {
|
|
182
190
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
183
191
|
}
|
|
184
|
-
|
|
192
|
+
/**
|
|
193
|
+
* The account's per-user temp dir. launchd gives daemons no TMPDIR, so Zellij
|
|
194
|
+
* would fall back to it anyway; naming it explicitly lets every caller (an SSH
|
|
195
|
+
* shell, a script that sets TMPDIR) agree on one Zellij socket dir. Resolved as
|
|
196
|
+
* the account so root writing a foreign LaunchDaemon gets that account's dir.
|
|
197
|
+
*/
|
|
198
|
+
function darwinUserTempDir(account, run) {
|
|
199
|
+
const argv = [...(account.foreign ? accountArgv(account) : []), "getconf", "DARWIN_USER_TEMP_DIR"];
|
|
200
|
+
const [cmd, ...args] = argv;
|
|
201
|
+
if (!cmd)
|
|
202
|
+
return undefined;
|
|
203
|
+
const res = run(cmd, args);
|
|
204
|
+
const dir = res.status === 0 ? res.stdout.trim() : "";
|
|
205
|
+
return dir || undefined;
|
|
206
|
+
}
|
|
207
|
+
export function launchdPlist(account, spec, run = defaultRunner) {
|
|
185
208
|
checkName(spec.name);
|
|
186
209
|
const label = launchdLabel(account, spec.name);
|
|
187
210
|
const log = join(account.home, "Library", "Logs", "zswarm", `${spec.name}.log`);
|
|
188
|
-
const env = { HOME: account.home, USER: account.user
|
|
211
|
+
const env = { HOME: account.home, USER: account.user };
|
|
212
|
+
const tmpdir = darwinUserTempDir(account, run);
|
|
213
|
+
if (tmpdir)
|
|
214
|
+
env.TMPDIR = tmpdir;
|
|
215
|
+
Object.assign(env, spec.env ?? {});
|
|
189
216
|
const lines = [
|
|
190
217
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
191
218
|
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
@@ -217,6 +244,36 @@ function launchdDomain(host) {
|
|
|
217
244
|
const gui = `gui/${a.uid}`;
|
|
218
245
|
return host.run("launchctl", ["print", gui]).status === 0 ? gui : `user/${a.uid}`;
|
|
219
246
|
}
|
|
247
|
+
/** launchd removes a booted-out job asynchronously; wait until print stops seeing it. */
|
|
248
|
+
const BOOTOUT_WAIT_MS = 10_000;
|
|
249
|
+
const BOOTOUT_POLL_MS = 250;
|
|
250
|
+
/** bootstrap racing that teardown fails with EIO (5) or 37; retry a couple of times. */
|
|
251
|
+
const BOOTSTRAP_RETRIES = 2;
|
|
252
|
+
const BOOTSTRAP_RETRY_MS = 1_000;
|
|
253
|
+
function waitForBootout(host, domain, label) {
|
|
254
|
+
const deadline = Date.now() + BOOTOUT_WAIT_MS;
|
|
255
|
+
for (;;) {
|
|
256
|
+
if (host.run("launchctl", ["print", `${domain}/${label}`]).status !== 0)
|
|
257
|
+
return;
|
|
258
|
+
const left = deadline - Date.now();
|
|
259
|
+
if (left <= 0)
|
|
260
|
+
return;
|
|
261
|
+
hostSleep(host, Math.min(BOOTOUT_POLL_MS, left));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function bootstrapLaunchd(host, domain, path, label) {
|
|
265
|
+
let last;
|
|
266
|
+
for (let attempt = 0; attempt <= BOOTSTRAP_RETRIES; attempt++) {
|
|
267
|
+
const res = host.run("launchctl", ["bootstrap", domain, path]);
|
|
268
|
+
if (res.status === 0)
|
|
269
|
+
return;
|
|
270
|
+
last = res;
|
|
271
|
+
if (!/Bootstrap failed: (5|37):/.test(res.stderr || res.stdout) || attempt === BOOTSTRAP_RETRIES)
|
|
272
|
+
break;
|
|
273
|
+
hostSleep(host, BOOTSTRAP_RETRY_MS);
|
|
274
|
+
}
|
|
275
|
+
must(last, `launchctl bootstrap ${domain} ${label}`);
|
|
276
|
+
}
|
|
220
277
|
// ------------------------------------------------------------------ common
|
|
221
278
|
/** Write, (re)load and start the services. Returns what it did, one line per unit. */
|
|
222
279
|
export function installServices(host, specs) {
|
|
@@ -257,9 +314,10 @@ export function installServices(host, specs) {
|
|
|
257
314
|
for (const spec of specs) {
|
|
258
315
|
const label = launchdLabel(a, spec.name);
|
|
259
316
|
const path = launchdPlistPath(a, label);
|
|
260
|
-
fs.write(path, launchdPlist(a, spec), 0o644);
|
|
317
|
+
fs.write(path, launchdPlist(a, spec, host.run), 0o644);
|
|
261
318
|
host.run("launchctl", ["bootout", `${domain}/${label}`]);
|
|
262
|
-
|
|
319
|
+
waitForBootout(host, domain, label);
|
|
320
|
+
bootstrapLaunchd(host, domain, path, label);
|
|
263
321
|
lines.push(`${label} loaded in ${domain}`);
|
|
264
322
|
}
|
|
265
323
|
return { lines, notes };
|
|
@@ -298,7 +356,16 @@ export function serviceStates(host, specs) {
|
|
|
298
356
|
return specs.map((spec) => {
|
|
299
357
|
const unit = systemdUnitName(spec.name, spec.kind);
|
|
300
358
|
const state = systemctl(host, ["is-active", unit]).stdout.trim() || "unknown";
|
|
301
|
-
|
|
359
|
+
const props = systemctl(host, ["show", unit, "-p", "MainPID", "-p", "ExecMainStatus"]).stdout;
|
|
360
|
+
const pid = Number(/^MainPID=(\d+)$/m.exec(props)?.[1] ?? NaN);
|
|
361
|
+
const exit = /^ExecMainStatus=(-?\d+)$/m.exec(props)?.[1];
|
|
362
|
+
const lastExit = exit === undefined ? undefined : Number(exit);
|
|
363
|
+
return {
|
|
364
|
+
name: spec.name,
|
|
365
|
+
unit,
|
|
366
|
+
active: state === "active",
|
|
367
|
+
...describeJob(spec, state, Number.isFinite(pid) && pid > 0 ? pid : undefined, lastExit),
|
|
368
|
+
};
|
|
302
369
|
});
|
|
303
370
|
}
|
|
304
371
|
const domain = launchdDomain(host);
|
|
@@ -307,12 +374,41 @@ export function serviceStates(host, specs) {
|
|
|
307
374
|
const res = host.run("launchctl", ["print", `${domain}/${label}`]);
|
|
308
375
|
if (res.status !== 0)
|
|
309
376
|
return { name: spec.name, unit: label, state: "not loaded", active: false };
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
const
|
|
313
|
-
|
|
377
|
+
const raw = /^\s*state = (.+)$/m.exec(res.stdout)?.[1]?.trim() ?? "loaded";
|
|
378
|
+
const pid = Number(/^\s*pid = (\d+)$/m.exec(res.stdout)?.[1] ?? NaN);
|
|
379
|
+
const exit = /last exit code = (-?\d+)/.exec(res.stdout)?.[1];
|
|
380
|
+
const lastExit = exit === undefined ? undefined : Number(exit);
|
|
381
|
+
const pidOrUndef = Number.isFinite(pid) && pid > 0 ? pid : undefined;
|
|
382
|
+
// Oneshot and interval jobs sit "not running" between runs; being loaded is up for them.
|
|
383
|
+
const active = spec.kind === "daemon" ? raw === "running" : true;
|
|
384
|
+
return {
|
|
385
|
+
name: spec.name,
|
|
386
|
+
unit: label,
|
|
387
|
+
active,
|
|
388
|
+
...describeJob(spec, spec.kind === "daemon" ? raw : "loaded", pidOrUndef, lastExit),
|
|
389
|
+
};
|
|
314
390
|
});
|
|
315
391
|
}
|
|
392
|
+
/**
|
|
393
|
+
* Word a service by what it is: a daemon is running with a pid, an interval
|
|
394
|
+
* job is a timer and a oneshot has run; the raw platform state reads like a
|
|
395
|
+
* failure between timer runs.
|
|
396
|
+
*/
|
|
397
|
+
function describeJob(spec, healthy, pid, lastExit) {
|
|
398
|
+
const parts = [];
|
|
399
|
+
if (spec.kind === "daemon") {
|
|
400
|
+
parts.push(pid === undefined ? healthy : `running (pid ${pid})`);
|
|
401
|
+
}
|
|
402
|
+
else if (spec.kind === "interval") {
|
|
403
|
+
parts.push(`timer every ${Math.max(30, Math.floor(spec.intervalSec ?? 120))} s`, healthy);
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
parts.push("oneshot", healthy);
|
|
407
|
+
}
|
|
408
|
+
if (lastExit !== undefined)
|
|
409
|
+
parts.push(`last exit ${lastExit}`);
|
|
410
|
+
return { state: parts.join(", "), pid, lastExit };
|
|
411
|
+
}
|
|
316
412
|
function must(res, what) {
|
|
317
413
|
if (res.status !== 0) {
|
|
318
414
|
throw new ZellijError("failed", `${what} failed: ${(res.stderr || res.stdout).trim().slice(0, 300)}`);
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export { createExec, NOT_FOUND_EXIT, type ExecFn, type ExecOptions, type ExecRes
|
|
|
4
4
|
export { createGitClient, defaultWorktreeRoot, normalizeRepoPath, parseWorktreeList, worktreeDirName, type GitClient, type GitClientOptions, type Worktree, } from "./git.js";
|
|
5
5
|
export { expandHomePath, looksLikeZswarmBinary, isZellijVersionOutput, resolveZellijBinary, resetZellijIdentityCache, sanitizeZellijEnv, validateSshDestination, validateSshMode, type ZellijExecFn, type ZellijExecResult, } from "./zellij/binary.js";
|
|
6
6
|
export { normalizePaneId, parsePaneList, resolvePane, type ZellijPane, } from "./zellij/panes.js";
|
|
7
|
-
export { liveSessionNames, parseSessionList, resolveSelfPaneId, type ZellijSession, type ZellijSessionResolve, } from "./zellij/session.js";
|
|
7
|
+
export { liveSessionNames, parseCreatedAge, parseSessionList, resolveSelfPaneId, type ZellijSession, type ZellijSessionResolve, } from "./zellij/session.js";
|
|
8
8
|
export { buildClosePaneArgs, buildDumpArgs, buildLaunchPluginArgs, buildListPanesArgs, buildNewPaneArgs, buildNewTabArgs, buildPasteArgs, buildPipeArgs, buildSendEnterArgs, buildSendKeysArgs, buildWriteCharsArgs, scrollbackPayload, waitPayload, type LaunchPluginInput, type NewPaneInput, type NewTabInput, type PaneDirection, type PipeInput, type ScrollbackRequest, type WaitRequest, } from "./zellij/args.js";
|
|
9
9
|
export { busPluginUrl, busToPanes, isBusPluginPane, nextConfigKey, parseBusReply, parseScrollbackReply, resolveBusPlugin, scrollbackToScreen, BUS_PIPE_NAME, DEFAULT_BUS_KEY, DEFAULT_BUS_TIMEOUT_MS, type BusMarker, type BusPane, type BusScrollback, type BusScrollbackPane, type BusSnapshot, } from "./zellij/bus.js";
|
|
10
10
|
export { busSnapshot, closeBusPluginPanes, planBus, resetBusCache, type BusPlan, } from "./ops/bus.js";
|
|
@@ -34,5 +34,5 @@ export { findPython, runSlot, slotScriptPath } from "./host/slot.js";
|
|
|
34
34
|
export { installServeService, serveServiceName, serveServiceSpec, serveTokenFromEnv, serveTokenPath, uninstallServeService, SERVE_AWAIT_SESSION_ENV, SERVE_TOKEN_FILE_ENV, type ServeServiceInput, } from "./host/serve-service.js";
|
|
35
35
|
export { accountArgv, installServices, launchdLabel, launchdPlist, removeServices, serviceAccount, serviceStates, systemdUnitName, systemdUnits, writeAccountFile, type Runner, type ServiceAccount, type ServiceHost, type ServiceKind, type ServiceSpec, type ServiceState, } from "./host/services.js";
|
|
36
36
|
export { crewCheck, crewDir, crewDown, crewServiceSpecs, crewStart, crewStatus, crewUp, firstLayoutPane, parseEnvFile, CREW_UNIT_ENV, CREW_WATCH_SEC, type CrewDeps, type CrewUpInput, } from "./host/crew.js";
|
|
37
|
-
export { deliverMessage, doneLines, fillMessage, freshDoneLine, launchRelay, prepareRelay, pruneRelays, relayLoop, relaysDir, runRelayDir, scrubRelayToken, startRelay, waitForRelay, type RelayConfig, type RelayDeps, type RelayStartInput, type RelayStatus, } from "./host/relay.js";
|
|
37
|
+
export { cancelAllTo, cancelRelay, deliverMessage, doneLines, fillMessage, freshDoneLine, launchRelay, listRelays, prepareRelay, pruneRelays, relayLoop, relaysDir, runRelayDir, scrubRelayToken, startRelay, waitForRelay, type RelayAdminDeps, type RelayCancelResult, type RelayConfig, type RelayDeps, type RelayJob, type RelayJobRunner, type RelayRow, type RelayStartInput, type RelayStatus, } from "./host/relay.js";
|
|
38
38
|
export { hostChecks, hostDoctor, hostInstall, hostInstallArgs, hostScriptPath, parseHostReport, type HostCheck, type HostDoctorInput, type HostInstallInput, type SshRunner, } from "./host/host.js";
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ export { createExec, NOT_FOUND_EXIT, } from "./exec.js";
|
|
|
4
4
|
export { createGitClient, defaultWorktreeRoot, normalizeRepoPath, parseWorktreeList, worktreeDirName, } from "./git.js";
|
|
5
5
|
export { expandHomePath, looksLikeZswarmBinary, isZellijVersionOutput, resolveZellijBinary, resetZellijIdentityCache, sanitizeZellijEnv, validateSshDestination, validateSshMode, } from "./zellij/binary.js";
|
|
6
6
|
export { normalizePaneId, parsePaneList, resolvePane, } from "./zellij/panes.js";
|
|
7
|
-
export { liveSessionNames, parseSessionList, resolveSelfPaneId, } from "./zellij/session.js";
|
|
7
|
+
export { liveSessionNames, parseCreatedAge, parseSessionList, resolveSelfPaneId, } from "./zellij/session.js";
|
|
8
8
|
export { buildClosePaneArgs, buildDumpArgs, buildLaunchPluginArgs, buildListPanesArgs, buildNewPaneArgs, buildNewTabArgs, buildPasteArgs, buildPipeArgs, buildSendEnterArgs, buildSendKeysArgs, buildWriteCharsArgs, scrollbackPayload, waitPayload, } from "./zellij/args.js";
|
|
9
9
|
export { busPluginUrl, busToPanes, isBusPluginPane, nextConfigKey, parseBusReply, parseScrollbackReply, resolveBusPlugin, scrollbackToScreen, BUS_PIPE_NAME, DEFAULT_BUS_KEY, DEFAULT_BUS_TIMEOUT_MS, } from "./zellij/bus.js";
|
|
10
10
|
export { busSnapshot, closeBusPluginPanes, planBus, resetBusCache, } from "./ops/bus.js";
|
|
@@ -32,5 +32,5 @@ export { findPython, runSlot, slotScriptPath } from "./host/slot.js";
|
|
|
32
32
|
export { installServeService, serveServiceName, serveServiceSpec, serveTokenFromEnv, serveTokenPath, uninstallServeService, SERVE_AWAIT_SESSION_ENV, SERVE_TOKEN_FILE_ENV, } from "./host/serve-service.js";
|
|
33
33
|
export { accountArgv, installServices, launchdLabel, launchdPlist, removeServices, serviceAccount, serviceStates, systemdUnitName, systemdUnits, writeAccountFile, } from "./host/services.js";
|
|
34
34
|
export { crewCheck, crewDir, crewDown, crewServiceSpecs, crewStart, crewStatus, crewUp, firstLayoutPane, parseEnvFile, CREW_UNIT_ENV, CREW_WATCH_SEC, } from "./host/crew.js";
|
|
35
|
-
export { deliverMessage, doneLines, fillMessage, freshDoneLine, launchRelay, prepareRelay, pruneRelays, relayLoop, relaysDir, runRelayDir, scrubRelayToken, startRelay, waitForRelay, } from "./host/relay.js";
|
|
35
|
+
export { cancelAllTo, cancelRelay, deliverMessage, doneLines, fillMessage, freshDoneLine, launchRelay, listRelays, prepareRelay, pruneRelays, relayLoop, relaysDir, runRelayDir, scrubRelayToken, startRelay, waitForRelay, } from "./host/relay.js";
|
|
36
36
|
export { hostChecks, hostDoctor, hostInstall, hostInstallArgs, hostScriptPath, parseHostReport, } from "./host/host.js";
|
package/dist/ops/delivery.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { StateStore } from "../state.js";
|
|
|
2
2
|
import type { ZellijClient } from "../zellij/client.js";
|
|
3
3
|
import type { ZellijPane } from "../zellij/panes.js";
|
|
4
4
|
import type { Clock } from "./types.js";
|
|
5
|
-
export type Submitted = boolean | "unverified";
|
|
5
|
+
export type Submitted = boolean | "queued" | "unverified" | "not-delivered";
|
|
6
6
|
export type DeliveryResult = {
|
|
7
7
|
to: string;
|
|
8
8
|
ok: boolean;
|
|
@@ -14,6 +14,36 @@ export type DeliveryResult = {
|
|
|
14
14
|
};
|
|
15
15
|
};
|
|
16
16
|
export type SubmitMode = "auto" | "double-enter" | "none";
|
|
17
|
+
/** Fixed send windows before adaptive scaling; explicit flags always win. */
|
|
18
|
+
export declare const SEND_BUDGETS: {
|
|
19
|
+
/** Pause before checking the paste landed. */
|
|
20
|
+
readonly settleMs: 300;
|
|
21
|
+
/** Pane-lookup retry window for an absent pane. */
|
|
22
|
+
readonly observeMs: 1000;
|
|
23
|
+
/** The scaled window never exceeds this multiple of the default. */
|
|
24
|
+
readonly maxScale: 5;
|
|
25
|
+
/** A first Zellij call at or under this many ms is not "slow". */
|
|
26
|
+
readonly slowMs: 1000;
|
|
27
|
+
readonly settleMaxMs: 5000;
|
|
28
|
+
readonly observeMaxMs: 30000;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Scale the fixed settle/observe windows by how slow the send's first Zellij
|
|
32
|
+
* call was. A call under `slowMs` keeps the defaults; five seconds or more pins
|
|
33
|
+
* the 5x cap. Explicit `--settle-ms` / `--observe-ms` are returned as given,
|
|
34
|
+
* and a scaled value is capped by the call's own timeout so it cannot outlast
|
|
35
|
+
* the observation budget.
|
|
36
|
+
*/
|
|
37
|
+
export declare function scaleBudgets(input: {
|
|
38
|
+
firstCallMs: number;
|
|
39
|
+
timeoutMs: number;
|
|
40
|
+
settleMs?: number;
|
|
41
|
+
observeMs?: number;
|
|
42
|
+
}): {
|
|
43
|
+
settleMs: number;
|
|
44
|
+
observeMs: number;
|
|
45
|
+
scale: number;
|
|
46
|
+
};
|
|
17
47
|
export type SenderSource = "arg" | "env" | "title" | "default";
|
|
18
48
|
export type SenderLabel = {
|
|
19
49
|
label: string;
|
|
@@ -54,14 +84,29 @@ export declare function resolveSubmitMode(args: Record<string, unknown>, pane: {
|
|
|
54
84
|
command?: string | null;
|
|
55
85
|
title?: string | null;
|
|
56
86
|
}): SubmitMode;
|
|
87
|
+
/**
|
|
88
|
+
* The body text to look for on screen: its first non-empty line, trimmed to 40
|
|
89
|
+
* chars. A prefix of the whole body (`body.slice(0, 40)`) can span a newline and
|
|
90
|
+
* then never matches any single screen line. A leading `[zswarm from=…]` header
|
|
91
|
+
* is skipped so we look for the caller's own first line.
|
|
92
|
+
*/
|
|
93
|
+
export declare function bodyNeedle(body: string): string;
|
|
57
94
|
/** True when the paste still sits in the composer (bottom of the screen). */
|
|
58
95
|
export declare function composerHolds(screen: string, body: string): boolean;
|
|
59
96
|
/**
|
|
60
97
|
* Compare the pane before vs after the paste.
|
|
61
98
|
* Composer clearing (pasted text left the bottom input region) is submission.
|
|
62
99
|
* `[Pasted text` in scrollback is not evidence.
|
|
100
|
+
*
|
|
101
|
+
* `not-delivered` is deliberately conservative: only when the screen is
|
|
102
|
+
* unchanged once the paste/composer itself is discounted and nothing is queued.
|
|
103
|
+
* A screen that changed while the body is gone is `unverified` — the pane may
|
|
104
|
+
* have taken the message and scrolled it away, and a false `not-delivered`
|
|
105
|
+
* makes the caller resend the whole message.
|
|
63
106
|
*/
|
|
64
|
-
export declare function classifySubmit(before: string, after: string, body: string
|
|
107
|
+
export declare function classifySubmit(before: string, after: string, body: string, opts?: {
|
|
108
|
+
queued?: (screen: string) => boolean;
|
|
109
|
+
}): Submitted;
|
|
65
110
|
/** Paste a body into one pane and record the attempt. */
|
|
66
111
|
export declare function deliverTo(client: ZellijClient, state: StateStore, args: Record<string, unknown>, input: {
|
|
67
112
|
session: string;
|
|
@@ -70,4 +115,8 @@ export declare function deliverTo(client: ZellijClient, state: StateStore, args:
|
|
|
70
115
|
op: string;
|
|
71
116
|
at: number;
|
|
72
117
|
clock: Clock;
|
|
118
|
+
/** How long the send's first Zellij call took, for adaptive budgets. */
|
|
119
|
+
firstCallMs?: number;
|
|
120
|
+
/** The observation timeout the scaled windows are capped by. */
|
|
121
|
+
timeoutMs?: number;
|
|
73
122
|
}): Promise<DeliveryResult>;
|