@zswarm/core 0.1.5 → 0.1.6
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 -1
- package/dist/exec.d.ts +12 -1
- package/dist/exec.js +89 -12
- package/dist/harness.js +12 -7
- package/dist/index.d.ts +7 -6
- package/dist/index.js +4 -4
- package/dist/ops/bus.d.ts +6 -3
- package/dist/ops/bus.js +55 -22
- package/dist/ops/dispatch.d.ts +7 -1
- package/dist/ops/dispatch.js +193 -65
- package/dist/ops/guards.d.ts +2 -0
- package/dist/ops/guards.js +11 -1
- package/dist/ops/observation.d.ts +8 -0
- package/dist/ops/observation.js +26 -0
- package/dist/ops/routing.d.ts +13 -0
- package/dist/ops/routing.js +21 -0
- package/dist/ops/spawn.d.ts +3 -3
- package/dist/ops/spawn.js +130 -64
- package/dist/ops/status.d.ts +30 -5
- package/dist/ops/status.js +231 -91
- package/dist/ops/types.d.ts +5 -1
- package/dist/ops/util.d.ts +3 -0
- package/dist/ops/util.js +2 -1
- package/dist/schema.d.ts +2 -0
- package/dist/schema.js +45 -6
- package/dist/zellij/args.d.ts +2 -0
- package/dist/zellij/binary.d.ts +34 -0
- package/dist/zellij/binary.js +169 -8
- package/dist/zellij/bus.d.ts +2 -0
- package/dist/zellij/bus.js +2 -1
- package/dist/zellij/client.d.ts +22 -5
- package/dist/zellij/client.js +97 -32
- package/dist/zellij/panes.js +3 -0
- package/dist/zellij/session.d.ts +19 -3
- package/dist/zellij/session.js +39 -9
- package/package.json +3 -3
package/dist/ops/dispatch.js
CHANGED
|
@@ -10,17 +10,50 @@ import { busChanged, busOp, busScreens, busSnapshot, busWait } from "./bus.js";
|
|
|
10
10
|
import { dumpLayoutOp, focusTarget, listTabsOp, renameTarget, stackTargets, } from "./panes.js";
|
|
11
11
|
import { peerCheckpoint, peerDiff } from "./review.js";
|
|
12
12
|
import { attachKnownSender, deliverTo, withSenderLabel, selfPaneTitle } from "./delivery.js";
|
|
13
|
-
import { assertNotPlugin, assertNotSelf,
|
|
13
|
+
import { assertNotPlugin, assertNotSelf, assertExpectedScreen, assertWritable, } from "./guards.js";
|
|
14
14
|
import { readDeliveryLog } from "./log.js";
|
|
15
15
|
import { awaitSignal, listSignals, postSignal } from "./signals.js";
|
|
16
16
|
import { spawnPane } from "./spawn.js";
|
|
17
|
-
import {
|
|
17
|
+
import { observationBudget } from "./observation.js";
|
|
18
|
+
import { invocationContext } from "./routing.js";
|
|
19
|
+
import { DEFAULT_STATUS_TIMEOUT_MS, peerStatus, } from "./status.js";
|
|
18
20
|
import { tailPane } from "./tail.js";
|
|
19
21
|
import { OP_NAMES } from "../schema.js";
|
|
20
|
-
import { dumpMaxChars, fail, isTrue, isVerbose, optionalString, paneViewBus, paneViewFull, paneViewSlim, throwIfAborted, truncateDumpText, } from "./util.js";
|
|
22
|
+
import { dumpMaxChars, fail, isTrue, isVerbose, numberArg, optionalString, paneViewBus, paneViewFull, paneViewSlim, throwIfAborted, truncateDumpText, } from "./util.js";
|
|
21
23
|
import { waitForPane } from "./wait.js";
|
|
22
24
|
import { listPeerWorktrees, removePeerWorktree } from "./worktree.js";
|
|
23
25
|
import { callServe, installServeLogon, serveCallTimeout, uninstallServeLogon, } from "./serve.js";
|
|
26
|
+
/**
|
|
27
|
+
* Per-invocation routing from `--local` / `--ssh`. `--local` clears both SSH
|
|
28
|
+
* and serve (and remote IPC). `--ssh` sets the destination for this call and
|
|
29
|
+
* clears serve so the flag wins over a sticky ZSWARM_SERVE.
|
|
30
|
+
*/
|
|
31
|
+
export function resolveInvocationEnv(args, base) {
|
|
32
|
+
const local = isTrue(args.local);
|
|
33
|
+
const sshDest = typeof args.ssh === "string" && args.ssh.trim() ? args.ssh.trim() : null;
|
|
34
|
+
if (local && sshDest) {
|
|
35
|
+
throw new ZellijError("usage", "pass --local or --ssh, not both");
|
|
36
|
+
}
|
|
37
|
+
if (!local && !sshDest)
|
|
38
|
+
return base;
|
|
39
|
+
const env = { ...base };
|
|
40
|
+
if (local) {
|
|
41
|
+
delete env.ZSWARM_SSH;
|
|
42
|
+
delete env.ZSWARM_SSH_OPTS;
|
|
43
|
+
delete env.ZSWARM_SSH_MODE;
|
|
44
|
+
delete env.ZSWARM_SSH_BIN;
|
|
45
|
+
delete env.ZSWARM_TMP;
|
|
46
|
+
delete env.ZSWARM_REMOTE_BIN;
|
|
47
|
+
delete env.ZSWARM_REMOTE_SHELL;
|
|
48
|
+
delete env.ZSWARM_SERVE;
|
|
49
|
+
delete env.ZSWARM_SERVE_TOKEN;
|
|
50
|
+
return env;
|
|
51
|
+
}
|
|
52
|
+
env.ZSWARM_SSH = sshDest;
|
|
53
|
+
delete env.ZSWARM_SERVE;
|
|
54
|
+
delete env.ZSWARM_SERVE_TOKEN;
|
|
55
|
+
return env;
|
|
56
|
+
}
|
|
24
57
|
/** Same default `wait` applies: match when a needle is given, else idle. */
|
|
25
58
|
function waitMode(args) {
|
|
26
59
|
const requested = typeof args.for === "string" ? args.for.trim() : "";
|
|
@@ -53,40 +86,52 @@ function assertSshGitAllowed(env, op, args) {
|
|
|
53
86
|
: "uses local git";
|
|
54
87
|
throw new ZellijError("ssh_git_unsupported", `${op} ${why}; ZSWARM_SSH only forwards Zellij. Run zswarm serve on the host that owns the session`);
|
|
55
88
|
}
|
|
56
|
-
async function resolveTarget(client, args, state, clock, env, policy, op) {
|
|
89
|
+
async function resolveTarget(client, args, state, clock, env, policy, op, signal) {
|
|
57
90
|
const to = String(args.to ?? "").trim();
|
|
58
91
|
if (!to)
|
|
59
92
|
throw new ZellijError("missing_peer", "to required");
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const bus = isVerbose(args)
|
|
64
|
-
|
|
65
|
-
|
|
93
|
+
const budget = observationBudget(clock, numberArg(args, "timeoutMs", 30_000, { min: 1, max: 900_000 }), signal);
|
|
94
|
+
const observeMs = numberArg(args, "observeMs", 1_000, { min: 0, max: 30_000 });
|
|
95
|
+
const { session } = await client.resolveSession(optionalString(args.session), budget.require());
|
|
96
|
+
const bus = isVerbose(args) ? null : await busSnapshot(client, state, session, clock, env, "status", {
|
|
97
|
+
deadlineAt: budget.deadline, signal,
|
|
98
|
+
});
|
|
66
99
|
let panes = null;
|
|
67
100
|
if (bus) {
|
|
68
|
-
const
|
|
101
|
+
const snapshot = busToPanes(bus.snapshot);
|
|
69
102
|
try {
|
|
70
|
-
client.resolvePane(
|
|
71
|
-
panes =
|
|
103
|
+
client.resolvePane(snapshot, to);
|
|
104
|
+
panes = snapshot;
|
|
72
105
|
}
|
|
73
|
-
catch {
|
|
74
|
-
|
|
106
|
+
catch { /* An absent title/command/ID gets a fresh direct observation. */ }
|
|
107
|
+
}
|
|
108
|
+
const until = Math.min(budget.deadline, clock.now() + observeMs);
|
|
109
|
+
let pane;
|
|
110
|
+
for (;;) {
|
|
111
|
+
const left = observeMs === 0 ? budget.require() : Math.min(budget.require(), Math.max(1, until - clock.now()));
|
|
112
|
+
panes ??= await client.listPanes(session, left);
|
|
113
|
+
throwIfAborted(signal);
|
|
114
|
+
try {
|
|
115
|
+
pane = client.resolvePane(panes, to);
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
if (!(err instanceof ZellijError) || err.code !== "peer_not_found" || clock.now() >= until)
|
|
120
|
+
throw err;
|
|
121
|
+
await budget.pause(until);
|
|
122
|
+
if (clock.now() >= until)
|
|
123
|
+
throw err;
|
|
124
|
+
panes = null;
|
|
75
125
|
}
|
|
76
126
|
}
|
|
77
|
-
if (!panes)
|
|
78
|
-
panes = await client.listPanes(session);
|
|
79
|
-
const pane = client.resolvePane(panes, to);
|
|
80
127
|
if (policy && op)
|
|
81
128
|
assertPaneAllowed(policy, pane, op);
|
|
82
129
|
return { session, panes, pane };
|
|
83
130
|
}
|
|
84
|
-
|
|
85
|
-
export async function dispatchZswarm(args, injected, deps = {}) {
|
|
131
|
+
async function dispatchOperation(args, injected, deps, context, env) {
|
|
86
132
|
const op = String(args.op ?? "");
|
|
87
133
|
const verbose = isVerbose(args);
|
|
88
134
|
const signal = deps.signal;
|
|
89
|
-
throwIfAborted(signal);
|
|
90
135
|
const clock = {
|
|
91
136
|
now: deps.now ?? (() => Date.now()),
|
|
92
137
|
sleep: deps.sleep ??
|
|
@@ -95,9 +140,13 @@ export async function dispatchZswarm(args, injected, deps = {}) {
|
|
|
95
140
|
reject(new ZellijError("cancelled", "operation cancelled"));
|
|
96
141
|
return;
|
|
97
142
|
}
|
|
98
|
-
const timer = setTimeout(
|
|
143
|
+
const timer = setTimeout(() => {
|
|
144
|
+
signal?.removeEventListener("abort", onAbort);
|
|
145
|
+
resolve();
|
|
146
|
+
}, ms);
|
|
99
147
|
const onAbort = () => {
|
|
100
148
|
clearTimeout(timer);
|
|
149
|
+
signal?.removeEventListener("abort", onAbort);
|
|
101
150
|
reject(new ZellijError("cancelled", "operation cancelled"));
|
|
102
151
|
};
|
|
103
152
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
@@ -107,10 +156,10 @@ export async function dispatchZswarm(args, injected, deps = {}) {
|
|
|
107
156
|
let gitClient = deps.git ?? null;
|
|
108
157
|
const git = () => (gitClient ??= createGitClient());
|
|
109
158
|
let stateStore = deps.state ?? null;
|
|
110
|
-
const state = () => (stateStore ??= createStateStore());
|
|
111
|
-
const policy = deps.policy ?? loadPolicy(deps.env);
|
|
112
|
-
const env = deps.env ?? process.env;
|
|
159
|
+
const state = () => (stateStore ??= createStateStore({ env }));
|
|
113
160
|
try {
|
|
161
|
+
throwIfAborted(signal);
|
|
162
|
+
const policy = deps.policy ?? loadPolicy(env);
|
|
114
163
|
// Policy gates the op before anything touches the session.
|
|
115
164
|
assertOpAllowed(policy, op);
|
|
116
165
|
assertSshGitAllowed(env, op, args);
|
|
@@ -134,11 +183,42 @@ export async function dispatchZswarm(args, injected, deps = {}) {
|
|
|
134
183
|
if (!injected && env.ZSWARM_SERVE?.trim()) {
|
|
135
184
|
return await callServe(env.ZSWARM_SERVE.trim(), attachKnownSender(args, env), serveCallTimeout(args), env.ZSWARM_SERVE_TOKEN);
|
|
136
185
|
}
|
|
137
|
-
const
|
|
186
|
+
const baseClient = injected ?? createZellijClient({ env, signal });
|
|
187
|
+
const client = {
|
|
188
|
+
...baseClient,
|
|
189
|
+
get transport() { return baseClient.transport; },
|
|
190
|
+
async resolveSession(...input) {
|
|
191
|
+
const resolved = await baseClient.resolveSession(...input);
|
|
192
|
+
context.session = resolved.session;
|
|
193
|
+
context.origin.session = resolved.source;
|
|
194
|
+
return resolved;
|
|
195
|
+
},
|
|
196
|
+
};
|
|
138
197
|
switch (op) {
|
|
139
198
|
case "sessions": {
|
|
140
|
-
|
|
141
|
-
|
|
199
|
+
// Resolve SSH/IPC first so a Windows interactive crew is reachable
|
|
200
|
+
// before --live filters EXITED rows.
|
|
201
|
+
const listed = await client.listSessions();
|
|
202
|
+
const transport = client.transport;
|
|
203
|
+
const ipc = transport.ipc;
|
|
204
|
+
if (ipc &&
|
|
205
|
+
ipc.requested?.toLowerCase() === "auto" &&
|
|
206
|
+
(ipc.status === "failed" ||
|
|
207
|
+
ipc.status === "expired" ||
|
|
208
|
+
ipc.status === "cancelled")) {
|
|
209
|
+
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".`);
|
|
210
|
+
}
|
|
211
|
+
const includeExited = isTrue(args.all);
|
|
212
|
+
const sessions = includeExited
|
|
213
|
+
? listed
|
|
214
|
+
: listed.filter((s) => !s.exited);
|
|
215
|
+
const data = {
|
|
216
|
+
sessions,
|
|
217
|
+
zellij: client.zellijPath,
|
|
218
|
+
transport,
|
|
219
|
+
filter: includeExited ? "all" : "live",
|
|
220
|
+
};
|
|
221
|
+
return { ok: true, data };
|
|
142
222
|
}
|
|
143
223
|
case "list": {
|
|
144
224
|
const { session } = await client.resolveSession(typeof args.session === "string" ? args.session : undefined);
|
|
@@ -146,7 +226,7 @@ export async function dispatchZswarm(args, injected, deps = {}) {
|
|
|
146
226
|
// verbose has to go the polling route.
|
|
147
227
|
const bus = verbose
|
|
148
228
|
? null
|
|
149
|
-
: await busSnapshot(client, state(), session, clock,
|
|
229
|
+
: await busSnapshot(client, state(), session, clock, env);
|
|
150
230
|
const view = bus ? paneViewBus : verbose ? paneViewFull : paneViewSlim;
|
|
151
231
|
const panes = (bus ? busToPanes(bus.snapshot) : await client.listPanes(session))
|
|
152
232
|
.filter((p) => !p.isPlugin)
|
|
@@ -165,16 +245,9 @@ export async function dispatchZswarm(args, injected, deps = {}) {
|
|
|
165
245
|
const body = String(args.body ?? args.text ?? "");
|
|
166
246
|
if (!body.trim())
|
|
167
247
|
throw new ZellijError("missing_body", "body required");
|
|
168
|
-
const { session, pane, panes } = await resolveTarget(client, args, state(), clock,
|
|
248
|
+
const { session, pane, panes } = await resolveTarget(client, args, state(), clock, env, policy, op, signal);
|
|
169
249
|
assertWritable(client, pane, args, "send");
|
|
170
|
-
|
|
171
|
-
// no reliable way to tell an agent from a prompt, so the caller names
|
|
172
|
-
// something the screen must show first.
|
|
173
|
-
const expect = typeof args.expect === "string" ? args.expect.trim() : "";
|
|
174
|
-
if (expect) {
|
|
175
|
-
const screen = await client.dumpPane({ session, paneId: pane.id });
|
|
176
|
-
assertPaneExpects(screen.text, expect, pane.id);
|
|
177
|
-
}
|
|
250
|
+
await assertExpectedScreen(client, session, pane.id, args, signal);
|
|
178
251
|
const labeled = withSenderLabel(args, {
|
|
179
252
|
env,
|
|
180
253
|
selfTitle: selfPaneTitle(client, panes),
|
|
@@ -205,8 +278,9 @@ export async function dispatchZswarm(args, injected, deps = {}) {
|
|
|
205
278
|
return await broadcast(client, state(), args, clock, policy, env);
|
|
206
279
|
case "keys":
|
|
207
280
|
case "interrupt": {
|
|
208
|
-
const { session, pane } = await resolveTarget(client, args, state(), clock,
|
|
281
|
+
const { session, pane } = await resolveTarget(client, args, state(), clock, env, policy, op, signal);
|
|
209
282
|
assertWritable(client, pane, args, op);
|
|
283
|
+
await assertExpectedScreen(client, session, pane.id, args, signal);
|
|
210
284
|
const chars = typeof args.chars === "string" ? args.chars : "";
|
|
211
285
|
if (op === "keys" && chars) {
|
|
212
286
|
await client.writeChars({ session, paneId: pane.id, chars });
|
|
@@ -259,7 +333,7 @@ export async function dispatchZswarm(args, injected, deps = {}) {
|
|
|
259
333
|
};
|
|
260
334
|
}
|
|
261
335
|
case "dump": {
|
|
262
|
-
const { session, pane } = await resolveTarget(client, args, state(), clock,
|
|
336
|
+
const { session, pane } = await resolveTarget(client, args, state(), clock, env, undefined, undefined, signal);
|
|
263
337
|
const dumped = await client.dumpPane({
|
|
264
338
|
session,
|
|
265
339
|
paneId: pane.id,
|
|
@@ -281,11 +355,11 @@ export async function dispatchZswarm(args, injected, deps = {}) {
|
|
|
281
355
|
};
|
|
282
356
|
}
|
|
283
357
|
case "tail": {
|
|
284
|
-
const target = await resolveTarget(client, args, state(), clock,
|
|
358
|
+
const target = await resolveTarget(client, args, state(), clock, env, undefined, undefined, signal);
|
|
285
359
|
return await tailPane(client, state(), args, target);
|
|
286
360
|
}
|
|
287
361
|
case "wait": {
|
|
288
|
-
const target = await resolveTarget(client, args, state(), clock,
|
|
362
|
+
const target = await resolveTarget(client, args, state(), clock, env, undefined, undefined, signal);
|
|
289
363
|
// The plugin holds one pipe for the whole wait and polls far tighter
|
|
290
364
|
// than spawning a process allows. It declines a regex needle — it has
|
|
291
365
|
// no engine — and then this is the loop that runs.
|
|
@@ -297,24 +371,51 @@ export async function dispatchZswarm(args, injected, deps = {}) {
|
|
|
297
371
|
match: typeof args.match === "string" ? args.match : null,
|
|
298
372
|
ignoreCase: isTrue(args.ignoreCase),
|
|
299
373
|
...timing,
|
|
300
|
-
},
|
|
374
|
+
}, env);
|
|
301
375
|
return await waitForPane(client, target, args, clock, viaBus, deps.signal);
|
|
302
376
|
}
|
|
303
377
|
case "status": {
|
|
304
|
-
|
|
378
|
+
// One overall deadline covering session resolve, bus snapshot, IPC,
|
|
379
|
+
// and screen samples — not just the dump loop inside peerStatus.
|
|
380
|
+
const statusBudget = numberArg(args, "timeoutMs", DEFAULT_STATUS_TIMEOUT_MS, { min: 1_000, max: 900_000 });
|
|
381
|
+
const deadlineAt = clock.now() + statusBudget;
|
|
382
|
+
const remaining = () => Math.max(0, deadlineAt - clock.now());
|
|
383
|
+
const setupBudget = () => {
|
|
384
|
+
throwIfAborted(signal);
|
|
385
|
+
const left = remaining();
|
|
386
|
+
if (left <= 0) {
|
|
387
|
+
throw new ZellijError("zellij_failed", "status timed out during setup");
|
|
388
|
+
}
|
|
389
|
+
return left;
|
|
390
|
+
};
|
|
391
|
+
throwIfAborted(signal);
|
|
392
|
+
const { session } = await client.resolveSession(typeof args.session === "string" ? args.session : undefined, setupBudget());
|
|
393
|
+
throwIfAborted(signal);
|
|
305
394
|
// Verbose reports cwd and command, which the plugin manifest lacks.
|
|
306
|
-
const bus = verbose
|
|
395
|
+
const bus = verbose || remaining() <= 0
|
|
307
396
|
? null
|
|
308
|
-
: await busSnapshot(client, state(), session, clock,
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
397
|
+
: await busSnapshot(client, state(), session, clock, env, "status", {
|
|
398
|
+
deadlineAt,
|
|
399
|
+
signal,
|
|
400
|
+
});
|
|
401
|
+
throwIfAborted(signal);
|
|
402
|
+
// Always hand peerStatus the resolved session (and panes) so it does
|
|
403
|
+
// not spend the overall budget resolving again.
|
|
404
|
+
const panes = bus
|
|
405
|
+
? busToPanes(bus.snapshot)
|
|
406
|
+
: await client.listPanes(session, setupBudget());
|
|
407
|
+
throwIfAborted(signal);
|
|
408
|
+
let supplied = {
|
|
409
|
+
session,
|
|
410
|
+
panes,
|
|
411
|
+
source: bus ? "plugin" : "zellij",
|
|
412
|
+
};
|
|
413
|
+
if (bus) {
|
|
414
|
+
supplied = {
|
|
415
|
+
...supplied,
|
|
416
|
+
readScreens: (paneIds, timeoutMs) => busScreens(client, state(), session, paneIds, clock, env, timeoutMs),
|
|
417
|
+
readChanged: async (paneIds, timeoutMs) => {
|
|
418
|
+
const reply = await busChanged(client, state(), session, paneIds, env, timeoutMs);
|
|
318
419
|
if (!reply)
|
|
319
420
|
return null;
|
|
320
421
|
return new Map(reply.panes.map((p) => [
|
|
@@ -322,23 +423,26 @@ export async function dispatchZswarm(args, injected, deps = {}) {
|
|
|
322
423
|
{ changed: p.changed, first: p.first, screen: p.screen },
|
|
323
424
|
]));
|
|
324
425
|
},
|
|
325
|
-
}
|
|
326
|
-
|
|
426
|
+
};
|
|
427
|
+
}
|
|
327
428
|
const only = typeof args.to === "string" ? args.to.trim() : "";
|
|
328
|
-
if (
|
|
429
|
+
if (bus && only) {
|
|
329
430
|
try {
|
|
330
431
|
client.resolvePane(supplied.panes, only);
|
|
331
432
|
}
|
|
332
433
|
catch {
|
|
333
|
-
//
|
|
334
|
-
|
|
335
|
-
supplied =
|
|
434
|
+
// Command-shaped `to` needs the polled list; re-fetch without bus.
|
|
435
|
+
const polled = await client.listPanes(session, setupBudget());
|
|
436
|
+
supplied = { session, panes: polled, source: "zellij" };
|
|
336
437
|
}
|
|
337
438
|
}
|
|
338
|
-
return await peerStatus(client, args, clock, supplied
|
|
439
|
+
return await peerStatus(client, args, clock, supplied, {
|
|
440
|
+
deadlineAt,
|
|
441
|
+
signal,
|
|
442
|
+
});
|
|
339
443
|
}
|
|
340
444
|
case "bus":
|
|
341
|
-
return await busOp(client, state(), args, clock,
|
|
445
|
+
return await busOp(client, state(), args, clock, env, policy);
|
|
342
446
|
case "signal":
|
|
343
447
|
return postSignal(state(), args, clock);
|
|
344
448
|
case "signals":
|
|
@@ -362,13 +466,13 @@ export async function dispatchZswarm(args, injected, deps = {}) {
|
|
|
362
466
|
case "checkpoint":
|
|
363
467
|
return await peerCheckpoint(git(), args, clock);
|
|
364
468
|
case "spawn":
|
|
365
|
-
return await spawnPane(client, args, deps.git);
|
|
469
|
+
return await spawnPane(client, args, deps.git, clock, signal);
|
|
366
470
|
case "worktrees":
|
|
367
471
|
return await listPeerWorktrees(git(), client, args);
|
|
368
472
|
case "unworktree":
|
|
369
473
|
return await removePeerWorktree(git(), client, args);
|
|
370
474
|
case "close": {
|
|
371
|
-
const { session, pane } = await resolveTarget(client, args, state(), clock,
|
|
475
|
+
const { session, pane } = await resolveTarget(client, args, state(), clock, env, policy, op, signal);
|
|
372
476
|
// Not assertWritable: closing an exited pane is the point of close.
|
|
373
477
|
assertNotPlugin(pane, "close");
|
|
374
478
|
assertNotSelf(client, pane, args, "close");
|
|
@@ -393,3 +497,27 @@ export async function dispatchZswarm(args, injected, deps = {}) {
|
|
|
393
497
|
return fail(err);
|
|
394
498
|
}
|
|
395
499
|
}
|
|
500
|
+
/** Shared MCP/CLI dispatch, with invocation routing on successes and failures. */
|
|
501
|
+
export async function dispatchZswarm(args, injected, deps = {}) {
|
|
502
|
+
try {
|
|
503
|
+
const env = resolveInvocationEnv(args, deps.env ?? process.env);
|
|
504
|
+
const context = invocationContext(args, env, injected?.transport);
|
|
505
|
+
const result = await dispatchOperation(args, injected, deps, context, env);
|
|
506
|
+
if (context.transport === "serve" && result.context) {
|
|
507
|
+
context.server = result.context;
|
|
508
|
+
context.session = result.context.session;
|
|
509
|
+
context.origin.session = result.context.origin.session;
|
|
510
|
+
}
|
|
511
|
+
else if (context.transport === "serve" && result.ok && result.data && typeof result.data === "object") {
|
|
512
|
+
const session = result.data.session;
|
|
513
|
+
if (typeof session === "string") {
|
|
514
|
+
context.session = session;
|
|
515
|
+
context.origin.session = "server";
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return { ...result, context };
|
|
519
|
+
}
|
|
520
|
+
catch (err) {
|
|
521
|
+
return fail(err);
|
|
522
|
+
}
|
|
523
|
+
}
|
package/dist/ops/guards.d.ts
CHANGED
|
@@ -13,3 +13,5 @@ export declare function assertWritable(client: ZellijClient, pane: ZellijPane, a
|
|
|
13
13
|
* in a safety check is a foot-gun.
|
|
14
14
|
*/
|
|
15
15
|
export declare function assertPaneExpects(screen: string, expect: string, paneId: string): void;
|
|
16
|
+
/** Check immediately before input. A failed read or mismatch must not write. */
|
|
17
|
+
export declare function assertExpectedScreen(client: ZellijClient, session: string, paneId: string, args: Record<string, unknown>, signal?: AbortSignal): Promise<void>;
|
package/dist/ops/guards.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ZellijError } from "../errors.js";
|
|
2
|
-
import { isTrue } from "./util.js";
|
|
2
|
+
import { isTrue, optionalString, throwIfAborted } from "./util.js";
|
|
3
3
|
export function assertNotPlugin(pane, action) {
|
|
4
4
|
if (pane.isPlugin) {
|
|
5
5
|
throw new ZellijError("pane_is_plugin", `${pane.id} is a plugin pane; ${action} targets terminal panes`);
|
|
@@ -34,3 +34,13 @@ export function assertPaneExpects(screen, expect, paneId) {
|
|
|
34
34
|
return;
|
|
35
35
|
throw new ZellijError("expect_missing", `${paneId} screen does not contain ${JSON.stringify(expect)}`);
|
|
36
36
|
}
|
|
37
|
+
/** Check immediately before input. A failed read or mismatch must not write. */
|
|
38
|
+
export async function assertExpectedScreen(client, session, paneId, args, signal) {
|
|
39
|
+
throwIfAborted(signal);
|
|
40
|
+
const expect = optionalString(args.expect);
|
|
41
|
+
if (!expect)
|
|
42
|
+
return;
|
|
43
|
+
const screen = await client.dumpPane({ session, paneId });
|
|
44
|
+
throwIfAborted(signal);
|
|
45
|
+
assertPaneExpects(screen.text, expect, paneId);
|
|
46
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Clock } from "./types.js";
|
|
2
|
+
/** One deadline shared by setup, child processes, retries, and sleeps. */
|
|
3
|
+
export declare function observationBudget(clock: Clock, timeoutMs: number, signal?: AbortSignal): {
|
|
4
|
+
deadline: number;
|
|
5
|
+
remaining: () => number;
|
|
6
|
+
require(): number;
|
|
7
|
+
pause(until?: number): Promise<void>;
|
|
8
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ZellijError } from "../errors.js";
|
|
2
|
+
import { throwIfAborted } from "./util.js";
|
|
3
|
+
/** One deadline shared by setup, child processes, retries, and sleeps. */
|
|
4
|
+
export function observationBudget(clock, timeoutMs, signal) {
|
|
5
|
+
const deadline = clock.now() + timeoutMs;
|
|
6
|
+
const remaining = () => {
|
|
7
|
+
throwIfAborted(signal);
|
|
8
|
+
return Math.max(0, deadline - clock.now());
|
|
9
|
+
};
|
|
10
|
+
return {
|
|
11
|
+
deadline,
|
|
12
|
+
remaining,
|
|
13
|
+
require() {
|
|
14
|
+
const left = remaining();
|
|
15
|
+
if (left <= 0)
|
|
16
|
+
throw new ZellijError("observation_timeout", "observation deadline expired");
|
|
17
|
+
return left;
|
|
18
|
+
},
|
|
19
|
+
async pause(until = deadline) {
|
|
20
|
+
const ms = Math.min(100, remaining(), Math.max(0, until - clock.now()));
|
|
21
|
+
if (ms > 0)
|
|
22
|
+
await clock.sleep(ms);
|
|
23
|
+
throwIfAborted(signal);
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ZellijTransport } from "../zellij/client.js";
|
|
2
|
+
export type RoutingContext = {
|
|
3
|
+
transport: "local" | "ssh" | "serve";
|
|
4
|
+
host: string;
|
|
5
|
+
session: string | null;
|
|
6
|
+
origin: {
|
|
7
|
+
transport: string;
|
|
8
|
+
session: string;
|
|
9
|
+
};
|
|
10
|
+
/** The serve endpoint can be a local tunnel; this identifies its server. */
|
|
11
|
+
server?: RoutingContext;
|
|
12
|
+
};
|
|
13
|
+
export declare function invocationContext(args: Record<string, unknown>, env: NodeJS.ProcessEnv, injected?: ZellijTransport): RoutingContext;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { hostname } from "node:os";
|
|
2
|
+
import { sessionFromEnv } from "../zellij/session.js";
|
|
3
|
+
import { isTrue, optionalString } from "./util.js";
|
|
4
|
+
export function invocationContext(args, env, injected) {
|
|
5
|
+
const serve = optionalString(env.ZSWARM_SERVE);
|
|
6
|
+
const ssh = optionalString(env.ZSWARM_SSH);
|
|
7
|
+
// Serve forwards explicit arguments; inherited session defaults belong to
|
|
8
|
+
// the server, not the caller (which might only be a local tunnel).
|
|
9
|
+
const selected = sessionFromEnv(serve && !injected ? {} : env, optionalString(args.session));
|
|
10
|
+
return {
|
|
11
|
+
transport: injected?.kind ?? (serve ? "serve" : ssh ? "ssh" : "local"),
|
|
12
|
+
host: injected ? injected.host ?? hostname() : serve ?? ssh ?? hostname(),
|
|
13
|
+
session: selected?.session ?? null,
|
|
14
|
+
origin: {
|
|
15
|
+
transport: injected ? "injected" : isTrue(args.local) ? "--local"
|
|
16
|
+
: optionalString(args.ssh) ? "--ssh" : serve ? "ZSWARM_SERVE"
|
|
17
|
+
: ssh ? "ZSWARM_SSH" : "default",
|
|
18
|
+
session: selected?.source ?? "unresolved",
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
package/dist/ops/spawn.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type GitClient } from "../git.js";
|
|
2
2
|
import type { ZellijClient } from "../zellij/client.js";
|
|
3
|
-
import type { OpsResult } from "./types.js";
|
|
4
|
-
/**
|
|
5
|
-
export declare function spawnPane(client: ZellijClient, args: Record<string, unknown>, git?: GitClient): Promise<OpsResult>;
|
|
3
|
+
import type { Clock, OpsResult } from "./types.js";
|
|
4
|
+
/** Create exactly once. Only observations are retried, never the create action. */
|
|
5
|
+
export declare function spawnPane(client: ZellijClient, args: Record<string, unknown>, git?: GitClient, clock?: Clock, signal?: AbortSignal): Promise<OpsResult>;
|