@zswarm/core 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/host-install.sh +36 -2
- package/assets/host-report.sh +15 -1
- package/assets/slot.py +35 -1
- package/dist/harness.d.ts +20 -0
- package/dist/harness.js +64 -1
- package/dist/host/cli.js +78 -4
- 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/lease.d.ts +68 -0
- package/dist/host/lease.js +138 -0
- package/dist/host/relay.d.ts +80 -1
- package/dist/host/relay.js +291 -9
- package/dist/host/services.d.ts +9 -2
- package/dist/host/services.js +105 -9
- package/dist/index.d.ts +5 -4
- package/dist/index.js +4 -3
- package/dist/ops/delivery.d.ts +51 -2
- package/dist/ops/delivery.js +162 -31
- package/dist/ops/dispatch.js +151 -14
- package/dist/ops/util.d.ts +9 -0
- package/dist/ops/util.js +24 -0
- package/dist/schema.js +50 -3
- package/dist/state.d.ts +20 -0
- package/dist/state.js +20 -0
- package/dist/zellij/args.d.ts +1 -1
- package/dist/zellij/args.js +3 -1
- package/dist/zellij/bus.js +1 -0
- package/dist/zellij/client.d.ts +2 -0
- package/dist/zellij/client.js +17 -1
- package/dist/zellij/panes.d.ts +1 -0
- package/dist/zellij/panes.js +3 -0
- package/dist/zellij/session.d.ts +11 -0
- package/dist/zellij/session.js +35 -3
- package/package.json +1 -1
package/dist/ops/dispatch.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { ZellijError } from "../errors.js";
|
|
2
2
|
import { createGitClient } from "../git.js";
|
|
3
|
+
import { busyPrompt, resolveHarness } from "../harness.js";
|
|
4
|
+
import { findLease } from "../host/lease.js";
|
|
3
5
|
import { normalizeKeys } from "../keys.js";
|
|
4
6
|
import { createZellijClient } from "../zellij/client.js";
|
|
7
|
+
import { parseCreatedAge } from "../zellij/session.js";
|
|
5
8
|
import { assertOpAllowed, assertPaneAllowed, loadPolicy, } from "../policy.js";
|
|
6
9
|
import { createStateStore } from "../state.js";
|
|
7
10
|
import { busToPanes } from "../zellij/bus.js";
|
|
@@ -9,7 +12,7 @@ import { broadcast } from "./broadcast.js";
|
|
|
9
12
|
import { busChanged, busOp, busScreens, busSnapshot, busWait, planBus } from "./bus.js";
|
|
10
13
|
import { dumpLayoutOp, focusTarget, listTabsOp, renameTarget, stackTargets, } from "./panes.js";
|
|
11
14
|
import { peerCheckpoint, peerDiff } from "./review.js";
|
|
12
|
-
import { attachKnownSender, deliverTo, withSenderLabel, selfPaneTitle } from "./delivery.js";
|
|
15
|
+
import { attachKnownSender, deliverTo, scaleBudgets, withSenderLabel, selfPaneTitle } from "./delivery.js";
|
|
13
16
|
import { assertNotPlugin, assertNotSelf, assertExpectedScreen, assertWritable, } from "./guards.js";
|
|
14
17
|
import { readDeliveryLog } from "./log.js";
|
|
15
18
|
import { awaitSignal, listSignals, postSignal } from "./signals.js";
|
|
@@ -19,7 +22,7 @@ import { invocationContext } from "./routing.js";
|
|
|
19
22
|
import { DEFAULT_STATUS_TIMEOUT_MS, peerStatus, } from "./status.js";
|
|
20
23
|
import { tailPane } from "./tail.js";
|
|
21
24
|
import { OP_NAMES } from "../schema.js";
|
|
22
|
-
import { dumpMaxChars, fail, isTrue, isVerbose, numberArg, optionalString, paneViewBus, paneViewFull, paneViewSlim, throwIfAborted, truncateDumpText, } from "./util.js";
|
|
25
|
+
import { dumpMaxChars, fail, isTrue, isVerbose, numberArg, optionalString, paneViewBus, paneViewFull, paneViewSlim, throwIfAborted, truncateDumpText, unfoldScreen, } from "./util.js";
|
|
23
26
|
import { waitForPane } from "./wait.js";
|
|
24
27
|
import { listPeerWorktrees, removePeerWorktree } from "./worktree.js";
|
|
25
28
|
import { DEFAULT_SERVE_INSTALL_TIMEOUT_MS, installServeLogon, uninstallServeLogon, } from "./serve-install.js";
|
|
@@ -100,18 +103,22 @@ async function resolveTarget(client, args, state, clock, env, policy, op, signal
|
|
|
100
103
|
const to = String(args.to ?? "").trim();
|
|
101
104
|
if (!to)
|
|
102
105
|
throw new ZellijError("missing_peer", "to required");
|
|
103
|
-
const
|
|
106
|
+
const timeoutMs = numberArg(args, "timeoutMs", 30_000, { min: 1, max: 900_000 });
|
|
107
|
+
const budget = observationBudget(clock, timeoutMs, signal);
|
|
108
|
+
const explicitObserve = args.observeMs !== undefined && args.observeMs !== null && args.observeMs !== "";
|
|
104
109
|
const observeMs = numberArg(args, "observeMs", 1_000, { min: 0, max: 30_000 });
|
|
105
110
|
const { session } = await client.resolveSession(optionalString(args.session), budget.require());
|
|
106
111
|
const bus = isVerbose(args) ? null : await busSnapshot(client, state, session, clock, env, "status", {
|
|
107
112
|
deadlineAt: budget.deadline, signal,
|
|
108
113
|
});
|
|
109
114
|
let panes = null;
|
|
115
|
+
let fromBus = false;
|
|
110
116
|
if (bus) {
|
|
111
117
|
const snapshot = busToPanes(bus.snapshot);
|
|
112
118
|
try {
|
|
113
119
|
client.resolvePane(snapshot, to);
|
|
114
120
|
panes = snapshot;
|
|
121
|
+
fromBus = true;
|
|
115
122
|
}
|
|
116
123
|
catch { /* An absent title/command/ID gets a fresh direct observation. */ }
|
|
117
124
|
}
|
|
@@ -121,13 +128,25 @@ async function resolveTarget(client, args, state, clock, env, policy, op, signal
|
|
|
121
128
|
let until = Number.POSITIVE_INFINITY;
|
|
122
129
|
let pane;
|
|
123
130
|
let fresh = isTrue(args.fresh) || Boolean(op);
|
|
131
|
+
let firstCallMs = 0;
|
|
132
|
+
let timed = false;
|
|
124
133
|
for (;;) {
|
|
125
134
|
const left = until === Number.POSITIVE_INFINITY || observeMs === 0
|
|
126
135
|
? budget.require()
|
|
127
136
|
: Math.min(budget.require(), Math.max(1, until - clock.now()));
|
|
137
|
+
const listingStart = clock.now();
|
|
138
|
+
const wantedListing = panes === null;
|
|
128
139
|
panes ??= await client.listPanes(session, left, { fresh });
|
|
129
|
-
if (
|
|
130
|
-
|
|
140
|
+
if (wantedListing && !timed) {
|
|
141
|
+
// The send's first Zellij call: a slow one scales the pane-observe and
|
|
142
|
+
// settle windows rather than making callers hand-tune --observe-ms.
|
|
143
|
+
firstCallMs = Math.max(0, clock.now() - listingStart);
|
|
144
|
+
timed = true;
|
|
145
|
+
}
|
|
146
|
+
if (until === Number.POSITIVE_INFINITY) {
|
|
147
|
+
const window = explicitObserve ? observeMs : scaleBudgets({ firstCallMs, timeoutMs }).observeMs;
|
|
148
|
+
until = Math.min(budget.deadline, clock.now() + window);
|
|
149
|
+
}
|
|
131
150
|
throwIfAborted(signal);
|
|
132
151
|
try {
|
|
133
152
|
pane = client.resolvePane(panes, to);
|
|
@@ -145,7 +164,48 @@ async function resolveTarget(client, args, state, clock, env, policy, op, signal
|
|
|
145
164
|
}
|
|
146
165
|
if (policy && op)
|
|
147
166
|
assertPaneAllowed(policy, pane, op);
|
|
148
|
-
return { session, panes, pane };
|
|
167
|
+
return { session, panes, pane, fromBus, firstCallMs, timeoutMs };
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* A pane's content width, for unfolding a glued dump. Only a bus-resolved pane
|
|
171
|
+
* reaches this: a direct listing already carried the field, and the manifest
|
|
172
|
+
* the bus pushes has no geometry. A single-line dump is the only caller.
|
|
173
|
+
*/
|
|
174
|
+
async function paneContentColumns(client, session, pane) {
|
|
175
|
+
const panes = await client.listPanes(session);
|
|
176
|
+
return panes.find((p) => p.id === pane.id)?.contentColumns ?? 0;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Zellij puts a newline before a row only when the row is marked canonical, and
|
|
180
|
+
* a full-screen TUI repaints by writing full-width rows back to back, so every
|
|
181
|
+
* row is a wrap continuation: `dump-screen` then hands back the whole viewport
|
|
182
|
+
* as one line. A pane whose rows are ended by a line feed reads normally and is
|
|
183
|
+
* left alone. The `--ansi` dump keeps the repainted rows full width, so it
|
|
184
|
+
* unfolds back into rows when it divides into the pane's column count exactly;
|
|
185
|
+
* anything else keeps the original text.
|
|
186
|
+
*/
|
|
187
|
+
async function unglueScreen(client, session, pane, text, full, fromBus) {
|
|
188
|
+
// The Zellij client prints the dump, so even a glued screen ends in one
|
|
189
|
+
// `println!` newline; only a newline *inside* the text separates rows.
|
|
190
|
+
const body = text.replace(/\n+$/, "");
|
|
191
|
+
if (!body || body.includes("\n"))
|
|
192
|
+
return text;
|
|
193
|
+
const columns = pane.contentColumns && pane.contentColumns > 1
|
|
194
|
+
? pane.contentColumns
|
|
195
|
+
: fromBus
|
|
196
|
+
? await paneContentColumns(client, session, pane)
|
|
197
|
+
: 0;
|
|
198
|
+
if (columns < 2 || body.length <= columns)
|
|
199
|
+
return text;
|
|
200
|
+
let ansi;
|
|
201
|
+
try {
|
|
202
|
+
ansi = (await client.dumpPane({ session, paneId: pane.id, full, ansi: true })).text;
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
// A Zellij too old for `--ansi` (or a failing call) must not break dump.
|
|
206
|
+
return text;
|
|
207
|
+
}
|
|
208
|
+
return unfoldScreen(ansi, columns) ?? text;
|
|
149
209
|
}
|
|
150
210
|
async function dispatchOperation(args, injected, deps, context, env) {
|
|
151
211
|
const op = String(args.op ?? "");
|
|
@@ -275,15 +335,25 @@ async function dispatchOperation(args, injected, deps, context, env) {
|
|
|
275
335
|
ipc.status === "cancelled")) {
|
|
276
336
|
throw new ZellijError("ipc_unreachable", `ZSWARM_TMP=auto could not discover the remote Zellij IPC directory (status=${ipc.status}). Set ZSWARM_TMP to the desktop TEMP, confirm ZSWARM_SSH_MODE=interactive with a logged-on user, or use zswarm serve. Refusing to treat EXITED rows as "no live crew".`);
|
|
277
337
|
}
|
|
338
|
+
if (isTrue(args.pruneExited)) {
|
|
339
|
+
const pruned = await pruneExitedSessions(client, listed, args);
|
|
340
|
+
return {
|
|
341
|
+
ok: true,
|
|
342
|
+
data: { ...pruned, zellij: client.zellijPath, transport, filter: "exited" },
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
const onlyExited = isTrue(args.exited);
|
|
278
346
|
const includeExited = isTrue(args.all);
|
|
279
|
-
const sessions =
|
|
280
|
-
? listed
|
|
281
|
-
:
|
|
347
|
+
const sessions = onlyExited
|
|
348
|
+
? listed.filter((s) => s.exited)
|
|
349
|
+
: includeExited
|
|
350
|
+
? listed
|
|
351
|
+
: listed.filter((s) => !s.exited);
|
|
282
352
|
const data = {
|
|
283
353
|
sessions,
|
|
284
354
|
zellij: client.zellijPath,
|
|
285
355
|
transport,
|
|
286
|
-
filter: includeExited ? "all" : "live",
|
|
356
|
+
filter: onlyExited ? "exited" : includeExited ? "all" : "live",
|
|
287
357
|
};
|
|
288
358
|
return { ok: true, data };
|
|
289
359
|
}
|
|
@@ -313,9 +383,26 @@ async function dispatchOperation(args, injected, deps, context, env) {
|
|
|
313
383
|
const body = String(args.body ?? args.text ?? "");
|
|
314
384
|
if (!body.trim())
|
|
315
385
|
throw new ZellijError("missing_body", "body required");
|
|
316
|
-
const { session, pane, panes } = await resolveTarget(client, args, state(), clock, env, policy, op, signal);
|
|
386
|
+
const { session, pane, panes, firstCallMs, timeoutMs } = await resolveTarget(client, args, state(), clock, env, policy, op, signal);
|
|
317
387
|
assertWritable(client, pane, args, "send");
|
|
318
388
|
await assertExpectedScreen(client, session, pane.id, args, signal);
|
|
389
|
+
if (isTrue(args.ifIdle)) {
|
|
390
|
+
// Before delivery starts: a live pane lease (a relay working it) is a
|
|
391
|
+
// hard refusal; a harness that is visibly working is the softer one.
|
|
392
|
+
const to = String(args.to ?? "").trim();
|
|
393
|
+
const held = findLease(state(), {
|
|
394
|
+
serve: typeof args.serve === "string" ? args.serve : env.ZSWARM_SERVE,
|
|
395
|
+
session,
|
|
396
|
+
pane: to,
|
|
397
|
+
});
|
|
398
|
+
if (held) {
|
|
399
|
+
throw new ZellijError("pane_leased", `pane ${to} is leased by relay ${held.relayId} (from ${held.from}, since ${new Date(held.at).toISOString()})`, { relayId: held.relayId, from: held.from, at: held.at, dir: held.dir });
|
|
400
|
+
}
|
|
401
|
+
const screen = await client.dumpPane({ session, paneId: pane.id }).then((res) => res.text, () => null);
|
|
402
|
+
if (screen !== null && busyPrompt(screen, resolveHarness(pane))) {
|
|
403
|
+
throw new ZellijError("pane_busy", `pane ${to} is busy with a task; retry when it is idle`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
319
406
|
const labeled = withSenderLabel(args, {
|
|
320
407
|
env,
|
|
321
408
|
selfTitle: selfPaneTitle(client, panes),
|
|
@@ -327,6 +414,8 @@ async function dispatchOperation(args, injected, deps, context, env) {
|
|
|
327
414
|
op: "send",
|
|
328
415
|
at: clock.now(),
|
|
329
416
|
clock,
|
|
417
|
+
firstCallMs,
|
|
418
|
+
timeoutMs,
|
|
330
419
|
});
|
|
331
420
|
if (!result.ok && result.error) {
|
|
332
421
|
throw new ZellijError(result.error.code, result.error.message);
|
|
@@ -401,15 +490,17 @@ async function dispatchOperation(args, injected, deps, context, env) {
|
|
|
401
490
|
};
|
|
402
491
|
}
|
|
403
492
|
case "dump": {
|
|
404
|
-
const { session, pane } = await resolveTarget(client, args, state(), clock, env, undefined, undefined, signal);
|
|
493
|
+
const { session, pane, fromBus } = await resolveTarget(client, args, state(), clock, env, undefined, undefined, signal);
|
|
494
|
+
const full = isTrue(args.full);
|
|
405
495
|
const dumped = await client.dumpPane({
|
|
406
496
|
session,
|
|
407
497
|
paneId: pane.id,
|
|
408
|
-
full
|
|
498
|
+
full,
|
|
409
499
|
});
|
|
500
|
+
const text = await unglueScreen(client, session, pane, dumped.text, full, fromBus);
|
|
410
501
|
const max = dumpMaxChars(args);
|
|
411
502
|
const keep = isTrue(args.head) ? "head" : "tail";
|
|
412
|
-
const clipped = truncateDumpText(
|
|
503
|
+
const clipped = truncateDumpText(text, max, keep);
|
|
413
504
|
return {
|
|
414
505
|
ok: true,
|
|
415
506
|
data: {
|
|
@@ -591,6 +682,52 @@ async function dispatchOperation(args, injected, deps, context, env) {
|
|
|
591
682
|
return fail(err);
|
|
592
683
|
}
|
|
593
684
|
}
|
|
685
|
+
/** Delete EXITED sessions only: `zellij delete-session` has no --force. */
|
|
686
|
+
async function pruneExitedSessions(client, listed, args) {
|
|
687
|
+
const olderThanRaw = optionalString(args.olderThan);
|
|
688
|
+
let minAge = null;
|
|
689
|
+
if (olderThanRaw) {
|
|
690
|
+
minAge = parseCreatedAge(olderThanRaw);
|
|
691
|
+
if (minAge === null) {
|
|
692
|
+
throw new ZellijError("bad_arg", `--older-than needs a duration like 7d, 12h, 30m (got ${olderThanRaw})`);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
const dryRun = isTrue(args.dryRun);
|
|
696
|
+
const deleted = [];
|
|
697
|
+
const skipped = [];
|
|
698
|
+
for (const session of listed.filter((s) => s.exited)) {
|
|
699
|
+
const row = { name: session.name, age: session.age, ageSeconds: session.ageSeconds };
|
|
700
|
+
if (minAge !== null && (session.ageSeconds === null || session.ageSeconds < minAge)) {
|
|
701
|
+
skipped.push({
|
|
702
|
+
...row,
|
|
703
|
+
reason: session.ageSeconds === null
|
|
704
|
+
? "age unknown"
|
|
705
|
+
: `created less than ${olderThanRaw} ago`,
|
|
706
|
+
});
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
if (dryRun) {
|
|
710
|
+
deleted.push(row);
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
713
|
+
try {
|
|
714
|
+
await client.deleteSession(session.name);
|
|
715
|
+
deleted.push(row);
|
|
716
|
+
}
|
|
717
|
+
catch (err) {
|
|
718
|
+
skipped.push({ ...row, reason: err instanceof Error ? err.message : String(err) });
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return {
|
|
722
|
+
pruneExited: true,
|
|
723
|
+
dryRun,
|
|
724
|
+
olderThanSeconds: minAge,
|
|
725
|
+
deleted,
|
|
726
|
+
deletedCount: deleted.length,
|
|
727
|
+
skipped,
|
|
728
|
+
skippedCount: skipped.length,
|
|
729
|
+
};
|
|
730
|
+
}
|
|
594
731
|
/** Shared MCP/CLI dispatch, with invocation routing on successes and failures. */
|
|
595
732
|
export async function dispatchZswarm(args, injected, deps = {}) {
|
|
596
733
|
try {
|
package/dist/ops/util.d.ts
CHANGED
|
@@ -44,6 +44,15 @@ export declare function truncateDumpText(text: string, maxChars: number, keep?:
|
|
|
44
44
|
};
|
|
45
45
|
/** Screen text with trailing blanks removed, so idle detection ignores padding. */
|
|
46
46
|
export declare function normalizeScreen(text: string): string;
|
|
47
|
+
/**
|
|
48
|
+
* Zellij's plain `dump-screen` puts a newline only before rows it marks
|
|
49
|
+
* canonical; a full-width repaint (a TUI) leaves every row a wrap
|
|
50
|
+
* continuation, so the whole viewport arrives as one line. The `--ansi`
|
|
51
|
+
* dump keeps those rows full width, so the text is a whole number of
|
|
52
|
+
* `columns`-wide rows once the SGR sequences are dropped. Returns null when
|
|
53
|
+
* that shape does not hold and the caller must keep the original text.
|
|
54
|
+
*/
|
|
55
|
+
export declare function unfoldScreen(text: string, columns: number): string | null;
|
|
47
56
|
export declare function numberArg(args: Record<string, unknown>, key: string, fallback: number, limits: {
|
|
48
57
|
min: number;
|
|
49
58
|
max: number;
|
package/dist/ops/util.js
CHANGED
|
@@ -71,6 +71,30 @@ export function normalizeScreen(text) {
|
|
|
71
71
|
.join("\n")
|
|
72
72
|
.replace(/\n+$/, "");
|
|
73
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Zellij's plain `dump-screen` puts a newline only before rows it marks
|
|
76
|
+
* canonical; a full-width repaint (a TUI) leaves every row a wrap
|
|
77
|
+
* continuation, so the whole viewport arrives as one line. The `--ansi`
|
|
78
|
+
* dump keeps those rows full width, so the text is a whole number of
|
|
79
|
+
* `columns`-wide rows once the SGR sequences are dropped. Returns null when
|
|
80
|
+
* that shape does not hold and the caller must keep the original text.
|
|
81
|
+
*/
|
|
82
|
+
export function unfoldScreen(text, columns) {
|
|
83
|
+
if (columns < 2)
|
|
84
|
+
return null;
|
|
85
|
+
const body = text.replace(/\x1b\[[0-9;]*m/g, "").replace(/\n+$/, "");
|
|
86
|
+
if (body.length === 0)
|
|
87
|
+
return null;
|
|
88
|
+
if (body.length % columns !== 0)
|
|
89
|
+
return null;
|
|
90
|
+
if (body.length / columns < 2)
|
|
91
|
+
return null;
|
|
92
|
+
const rows = [];
|
|
93
|
+
for (let i = 0; i < body.length; i += columns) {
|
|
94
|
+
rows.push(body.slice(i, i + columns).replace(/\s+$/, ""));
|
|
95
|
+
}
|
|
96
|
+
return rows.join("\n");
|
|
97
|
+
}
|
|
74
98
|
export function numberArg(args, key, fallback, limits) {
|
|
75
99
|
const raw = args[key];
|
|
76
100
|
if (raw === undefined || raw === null || raw === "")
|
package/dist/schema.js
CHANGED
|
@@ -72,6 +72,41 @@ export const PARAMS = [
|
|
|
72
72
|
flags: ["--live", "--active"],
|
|
73
73
|
description: "sessions: only live (non-EXITED) sessions — this is the default; use --all to include EXITED",
|
|
74
74
|
},
|
|
75
|
+
{
|
|
76
|
+
name: "exited",
|
|
77
|
+
type: "boolean",
|
|
78
|
+
flags: ["--exited"],
|
|
79
|
+
cliOnly: true,
|
|
80
|
+
description: "sessions: list only EXITED (resurrectable) sessions; rows carry name, exited, age and ageSeconds",
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: "pruneExited",
|
|
84
|
+
type: "boolean",
|
|
85
|
+
flags: ["--prune-exited"],
|
|
86
|
+
cliOnly: true,
|
|
87
|
+
description: "sessions: delete EXITED sessions (all, or those matching --older-than) with zellij delete-session; a live session is never removed",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: "olderThan",
|
|
91
|
+
type: "string",
|
|
92
|
+
flags: ["--older-than"],
|
|
93
|
+
cliOnly: true,
|
|
94
|
+
description: "sessions --prune-exited: only delete sessions created at least this long ago (e.g. 7d, 12h, 30m; default all exited)",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: "dryRun",
|
|
98
|
+
type: "boolean",
|
|
99
|
+
flags: ["--dry-run"],
|
|
100
|
+
cliOnly: true,
|
|
101
|
+
description: "sessions --prune-exited: report what would be deleted without deleting",
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: "json",
|
|
105
|
+
type: "boolean",
|
|
106
|
+
flags: ["--json"],
|
|
107
|
+
cliOnly: true,
|
|
108
|
+
description: "sessions: rows are name, exited, age and ageSeconds; the CLI prints JSON either way",
|
|
109
|
+
},
|
|
75
110
|
{
|
|
76
111
|
name: "local",
|
|
77
112
|
type: "boolean",
|
|
@@ -344,19 +379,31 @@ export const PARAMS = [
|
|
|
344
379
|
type: "string",
|
|
345
380
|
flags: ["--submit"],
|
|
346
381
|
values: ["auto", "double-enter", "none"],
|
|
347
|
-
description:
|
|
382
|
+
description: 'send/broadcast: auto verifies the paste actually submitted and presses Enter again if not (default). The result reports submitted: true, "queued" (held behind a running turn), false (still in the composer), "unverified" (the screen changed but the body is not visible), or "not-delivered" (the pane was unchanged apart from the composer, so the op fails not_delivered and a resend is safe)',
|
|
348
383
|
},
|
|
349
384
|
{
|
|
350
385
|
name: "observeMs",
|
|
351
386
|
type: "number",
|
|
352
387
|
flags: ["--observe-ms"],
|
|
353
|
-
description: "spawn: observe creation/alias for up to 3000ms; pane lookup: retry absence for 1000ms; 0 disables retries",
|
|
388
|
+
description: "spawn: observe creation/alias for up to 3000ms; pane lookup: retry absence for 1000ms (scaled up to 5x when the first Zellij call is slow); 0 disables retries",
|
|
354
389
|
},
|
|
355
390
|
{
|
|
356
391
|
name: "settleMs",
|
|
357
392
|
type: "number",
|
|
358
393
|
flags: ["--settle-ms"],
|
|
359
|
-
description: "send/broadcast: pause before checking the paste landed (default 300)",
|
|
394
|
+
description: "send/broadcast: pause before checking the paste landed (default 300, scaled up to 5x when the first Zellij call is slow; explicit value wins)",
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
name: "confirm",
|
|
398
|
+
type: "boolean",
|
|
399
|
+
flags: ["--confirm"],
|
|
400
|
+
description: 'send/broadcast: after the send, dump once more and report what it sees; paste again only on "not-delivered" (positive evidence nothing landed), press Enter when the body is still in the composer, and never paste the body twice',
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
name: "ifIdle",
|
|
404
|
+
type: "boolean",
|
|
405
|
+
flags: ["--if-idle"],
|
|
406
|
+
description: "send: refuse a pane that a relay has leased (pane_leased) or whose harness is visibly working (pane_busy) instead of pasting into it",
|
|
360
407
|
},
|
|
361
408
|
{
|
|
362
409
|
name: "expect",
|
package/dist/state.d.ts
CHANGED
|
@@ -29,6 +29,24 @@ export type BusMarkerRecord = {
|
|
|
29
29
|
configKey: string;
|
|
30
30
|
installedAt: number;
|
|
31
31
|
};
|
|
32
|
+
/**
|
|
33
|
+
* One held pane lease: a relay is working this target until it ends. `key`
|
|
34
|
+
* names the target (serve endpoint or local, session, pane) so two processes
|
|
35
|
+
* racing to take it are serialized by the lease lock.
|
|
36
|
+
*/
|
|
37
|
+
export type LeaseRecord = {
|
|
38
|
+
key: string;
|
|
39
|
+
relayId: string;
|
|
40
|
+
from: string;
|
|
41
|
+
at: number;
|
|
42
|
+
/** The relay run directory; a missing/finished one makes the lease stale. */
|
|
43
|
+
dir: string;
|
|
44
|
+
serve?: string;
|
|
45
|
+
session?: string;
|
|
46
|
+
pane: string;
|
|
47
|
+
/** Relays that forced through while this lease was held. */
|
|
48
|
+
sharedBy?: string[];
|
|
49
|
+
};
|
|
32
50
|
/** How much of the end of the log `readLog` reads; older entries are not shown. */
|
|
33
51
|
export declare const LOG_TAIL_BYTES: number;
|
|
34
52
|
/**
|
|
@@ -55,6 +73,8 @@ export declare function createStateStore(options?: StateStoreOptions): {
|
|
|
55
73
|
readBus: (session: string) => BusMarkerRecord | null;
|
|
56
74
|
writeBus: (session: string, marker: BusMarkerRecord) => void;
|
|
57
75
|
clearBus: (session?: string) => void;
|
|
76
|
+
readLeases: () => Record<string, LeaseRecord>;
|
|
77
|
+
withLeases: <T>(fn: (leases: Record<string, LeaseRecord>) => T) => T;
|
|
58
78
|
reset: () => void;
|
|
59
79
|
};
|
|
60
80
|
export type StateStore = ReturnType<typeof createStateStore>;
|
package/dist/state.js
CHANGED
|
@@ -7,6 +7,8 @@ const SIGNALS_LOCK = "signals.lock";
|
|
|
7
7
|
const CURSORS_FILE = "cursors.json";
|
|
8
8
|
const CURSORS_LOCK = "cursors.lock";
|
|
9
9
|
const BUS_FILE = "bus.json";
|
|
10
|
+
const LEASES_FILE = "leases.json";
|
|
11
|
+
const LEASES_LOCK = "leases.lock";
|
|
10
12
|
/** How much of the end of the log `readLog` reads; older entries are not shown. */
|
|
11
13
|
export const LOG_TAIL_BYTES = 512 * 1024;
|
|
12
14
|
/**
|
|
@@ -462,6 +464,22 @@ export function createStateStore(options = {}) {
|
|
|
462
464
|
}
|
|
463
465
|
writeJson(BUS_FILE, { sessions });
|
|
464
466
|
}
|
|
467
|
+
function readLeases() {
|
|
468
|
+
return readJson(LEASES_FILE, {});
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Atomic lease check-then-take/update. The callback mutates the map and its
|
|
472
|
+
* return value goes to the caller; the map is persisted while the state lock
|
|
473
|
+
* is held, so two processes racing to take one pane cannot both win.
|
|
474
|
+
*/
|
|
475
|
+
function withLeases(fn) {
|
|
476
|
+
return withFileLock(LEASES_LOCK, () => {
|
|
477
|
+
const leases = readJson(LEASES_FILE, {});
|
|
478
|
+
const result = fn(leases);
|
|
479
|
+
writeJson(LEASES_FILE, leases);
|
|
480
|
+
return result;
|
|
481
|
+
});
|
|
482
|
+
}
|
|
465
483
|
/** Test helper: drop everything this store wrote. */
|
|
466
484
|
function reset() {
|
|
467
485
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -480,6 +498,8 @@ export function createStateStore(options = {}) {
|
|
|
480
498
|
readBus,
|
|
481
499
|
writeBus,
|
|
482
500
|
clearBus,
|
|
501
|
+
readLeases,
|
|
502
|
+
withLeases,
|
|
483
503
|
reset,
|
|
484
504
|
};
|
|
485
505
|
}
|
package/dist/zellij/args.d.ts
CHANGED
|
@@ -83,7 +83,7 @@ export declare function buildLaunchPluginArgs(input: LaunchPluginInput): string[
|
|
|
83
83
|
export declare function buildListPanesArgs(session: string): string[];
|
|
84
84
|
export declare function buildPasteArgs(session: string, paneId: string, text: string): string[];
|
|
85
85
|
export declare function buildSendEnterArgs(session: string, paneId: string): string[];
|
|
86
|
-
export declare function buildDumpArgs(session: string, paneId: string, full?: boolean): string[];
|
|
86
|
+
export declare function buildDumpArgs(session: string, paneId: string, full?: boolean, ansi?: boolean): string[];
|
|
87
87
|
export declare function buildSendKeysArgs(session: string, paneId: string, keys: string[]): string[];
|
|
88
88
|
export declare function buildWriteCharsArgs(session: string, paneId: string, chars: string): string[];
|
|
89
89
|
export declare function buildRenamePaneArgs(session: string, paneId: string, name: string): string[];
|
package/dist/zellij/args.js
CHANGED
|
@@ -122,7 +122,7 @@ export function buildSendEnterArgs(session, paneId) {
|
|
|
122
122
|
"Enter",
|
|
123
123
|
];
|
|
124
124
|
}
|
|
125
|
-
export function buildDumpArgs(session, paneId, full = false) {
|
|
125
|
+
export function buildDumpArgs(session, paneId, full = false, ansi = false) {
|
|
126
126
|
const args = [
|
|
127
127
|
...sessionPrefix(session),
|
|
128
128
|
"action",
|
|
@@ -132,6 +132,8 @@ export function buildDumpArgs(session, paneId, full = false) {
|
|
|
132
132
|
];
|
|
133
133
|
if (full)
|
|
134
134
|
args.push("--full");
|
|
135
|
+
if (ansi)
|
|
136
|
+
args.push("--ansi");
|
|
135
137
|
return args;
|
|
136
138
|
}
|
|
137
139
|
export function buildSendKeysArgs(session, paneId, keys) {
|
package/dist/zellij/bus.js
CHANGED
package/dist/zellij/client.d.ts
CHANGED
|
@@ -41,6 +41,7 @@ export declare function createZellijClient(options?: ZellijClientOptions): {
|
|
|
41
41
|
listSessions: (callTimeoutMs?: number, opts?: {
|
|
42
42
|
fresh?: boolean;
|
|
43
43
|
}) => Promise<ZellijSession[]>;
|
|
44
|
+
deleteSession: (name: string, callTimeoutMs?: number) => Promise<void>;
|
|
44
45
|
resolveSession: (explicit?: string | null, callTimeoutMs?: number) => Promise<ZellijSessionResolve>;
|
|
45
46
|
listPanes: (session: string, callTimeoutMs?: number, opts?: {
|
|
46
47
|
fresh?: boolean;
|
|
@@ -58,6 +59,7 @@ export declare function createZellijClient(options?: ZellijClientOptions): {
|
|
|
58
59
|
session: string;
|
|
59
60
|
paneId: string;
|
|
60
61
|
full?: boolean;
|
|
62
|
+
ansi?: boolean;
|
|
61
63
|
timeoutMs?: number;
|
|
62
64
|
}) => Promise<{
|
|
63
65
|
paneId: string;
|
package/dist/zellij/client.js
CHANGED
|
@@ -173,6 +173,21 @@ export function createZellijClient(options = {}) {
|
|
|
173
173
|
return parseSessionList(result.stdout);
|
|
174
174
|
});
|
|
175
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* Delete one session by name. No `--force`: Zellij refuses to delete a live
|
|
178
|
+
* session, so this can only remove an EXITED (resurrectable) one.
|
|
179
|
+
*/
|
|
180
|
+
async function deleteSession(name, callTimeoutMs = timeoutMs) {
|
|
181
|
+
const result = await exec(["delete-session", name], { timeoutMs: callTimeoutMs });
|
|
182
|
+
if (result.code === NOT_FOUND_EXIT) {
|
|
183
|
+
throw zellijMissingError(zellijPath, result);
|
|
184
|
+
}
|
|
185
|
+
if (result.code !== 0) {
|
|
186
|
+
const detail = result.stderr.trim() || result.stdout.trim() || "no output";
|
|
187
|
+
throw new ZellijError("zellij_failed", `zellij delete-session ${name} failed (exit ${result.code}): ${detail}`, zellijExecDetails(result));
|
|
188
|
+
}
|
|
189
|
+
invalidateListings(name);
|
|
190
|
+
}
|
|
176
191
|
async function resolveSession(explicit, callTimeoutMs = timeoutMs) {
|
|
177
192
|
return (sessionFromEnv(env, explicit) ??
|
|
178
193
|
sessionFromList(await listSessions(callTimeoutMs)));
|
|
@@ -194,7 +209,7 @@ export function createZellijClient(options = {}) {
|
|
|
194
209
|
}
|
|
195
210
|
async function dumpPane(input) {
|
|
196
211
|
const paneId = normalizePaneId(input.paneId);
|
|
197
|
-
const result = await run(buildDumpArgs(input.session, paneId, input.full), "zellij action dump-screen", input.timeoutMs ?? timeoutMs);
|
|
212
|
+
const result = await run(buildDumpArgs(input.session, paneId, input.full, input.ansi), "zellij action dump-screen", input.timeoutMs ?? timeoutMs);
|
|
198
213
|
return { paneId, session: input.session, text: result.stdout };
|
|
199
214
|
}
|
|
200
215
|
async function sendKeys(input) {
|
|
@@ -362,6 +377,7 @@ export function createZellijClient(options = {}) {
|
|
|
362
377
|
/** A `file:` plugin url only names a path on the machine running Zellij. */
|
|
363
378
|
remote: ssh !== null,
|
|
364
379
|
listSessions,
|
|
380
|
+
deleteSession,
|
|
365
381
|
resolveSession,
|
|
366
382
|
listPanes,
|
|
367
383
|
resolvePane,
|
package/dist/zellij/panes.d.ts
CHANGED
package/dist/zellij/panes.js
CHANGED
|
@@ -32,6 +32,9 @@ function parsePaneRow(row) {
|
|
|
32
32
|
focused: row.is_focused === true,
|
|
33
33
|
exited: row.exited === true,
|
|
34
34
|
floating: row.is_floating === true,
|
|
35
|
+
contentColumns: typeof row.pane_content_columns === "number"
|
|
36
|
+
? row.pane_content_columns
|
|
37
|
+
: null,
|
|
35
38
|
};
|
|
36
39
|
}
|
|
37
40
|
/** Parse `list-panes --json` output. */
|
package/dist/zellij/session.d.ts
CHANGED
|
@@ -9,12 +9,23 @@ export type ZellijSession = {
|
|
|
9
9
|
exited: boolean;
|
|
10
10
|
/** True when the listing marks this session as the caller's current one. */
|
|
11
11
|
current: boolean;
|
|
12
|
+
/** The printed created-duration text without the trailing "ago" (`"1month 16days 2h"`), or null. */
|
|
13
|
+
age: string | null;
|
|
14
|
+
/** That duration in seconds, or null when it cannot be read. */
|
|
15
|
+
ageSeconds: number | null;
|
|
12
16
|
};
|
|
13
17
|
/**
|
|
14
18
|
* Pane hosting the caller, so writes can refuse to loop back into it.
|
|
15
19
|
* `ZSWARM_SELF_PANE` wins; Zellij exports `ZELLIJ_PANE_ID` inside a pane.
|
|
16
20
|
*/
|
|
17
21
|
export declare function resolveSelfPaneId(env?: NodeJS.ProcessEnv): string | null;
|
|
22
|
+
/**
|
|
23
|
+
* Zellij's human created age (`"1month 16days 2h 30m 5s ago"`) as seconds.
|
|
24
|
+
* Handles the units Zellij prints (s, m, h, days, months, years, singular and
|
|
25
|
+
* plural) with or without spaces between the pairs. Returns null when no
|
|
26
|
+
* number+unit pair is present (e.g. a bare `"demo"` listing).
|
|
27
|
+
*/
|
|
28
|
+
export declare function parseCreatedAge(text: string): number | null;
|
|
18
29
|
/**
|
|
19
30
|
* Parse `list-sessions --no-formatting` output.
|
|
20
31
|
*
|