@zswarm/core 0.1.6 → 0.1.8
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 +8 -0
- package/dist/errors.d.ts +2 -1
- package/dist/errors.js +4 -1
- package/dist/exec.d.ts +44 -2
- package/dist/exec.js +198 -46
- package/dist/index.d.ts +7 -2
- package/dist/index.js +6 -1
- package/dist/ops/broadcast.js +1 -1
- package/dist/ops/bus.js +38 -14
- package/dist/ops/dispatch.js +94 -23
- package/dist/ops/doctor.d.ts +80 -0
- package/dist/ops/doctor.js +1377 -0
- package/dist/ops/panes.js +4 -4
- package/dist/ops/routing.js +3 -2
- package/dist/ops/serve-bind.d.ts +86 -0
- package/dist/ops/serve-bind.js +354 -0
- package/dist/ops/serve-install.d.ts +88 -0
- package/dist/ops/serve-install.js +1171 -0
- package/dist/ops/serve-tunnel.d.ts +109 -0
- package/dist/ops/serve-tunnel.js +1179 -0
- package/dist/ops/serve.d.ts +89 -21
- package/dist/ops/serve.js +622 -156
- package/dist/ops/spawn.js +4 -4
- package/dist/ops/status.js +18 -9
- package/dist/ops/types.d.ts +49 -0
- package/dist/ops/util.js +8 -1
- package/dist/ops/worktree.js +2 -2
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +21 -8
- package/dist/state.js +158 -38
- package/dist/zellij/binary.d.ts +22 -2
- package/dist/zellij/binary.js +58 -13
- package/dist/zellij/bus.js +1 -1
- package/dist/zellij/cache.d.ts +24 -0
- package/dist/zellij/cache.js +90 -0
- package/dist/zellij/client.d.ts +18 -4
- package/dist/zellij/client.js +108 -34
- package/package.json +1 -1
package/dist/ops/dispatch.js
CHANGED
|
@@ -6,7 +6,7 @@ import { assertOpAllowed, assertPaneAllowed, loadPolicy, } from "../policy.js";
|
|
|
6
6
|
import { createStateStore } from "../state.js";
|
|
7
7
|
import { busToPanes } from "../zellij/bus.js";
|
|
8
8
|
import { broadcast } from "./broadcast.js";
|
|
9
|
-
import { busChanged, busOp, busScreens, busSnapshot, busWait } from "./bus.js";
|
|
9
|
+
import { busChanged, busOp, busScreens, busSnapshot, busWait, planBus } 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";
|
|
@@ -22,7 +22,10 @@ import { OP_NAMES } from "../schema.js";
|
|
|
22
22
|
import { dumpMaxChars, fail, isTrue, isVerbose, numberArg, optionalString, paneViewBus, paneViewFull, paneViewSlim, throwIfAborted, truncateDumpText, } from "./util.js";
|
|
23
23
|
import { waitForPane } from "./wait.js";
|
|
24
24
|
import { listPeerWorktrees, removePeerWorktree } from "./worktree.js";
|
|
25
|
-
import {
|
|
25
|
+
import { DEFAULT_SERVE_INSTALL_TIMEOUT_MS, installServeLogon, uninstallServeLogon, } from "./serve-install.js";
|
|
26
|
+
import { serveCallTimeout, } from "./serve.js";
|
|
27
|
+
import { doctorOp } from "./doctor.js";
|
|
28
|
+
import { forwardServe } from "./serve-tunnel.js";
|
|
26
29
|
/**
|
|
27
30
|
* Per-invocation routing from `--local` / `--ssh`. `--local` clears both SSH
|
|
28
31
|
* and serve (and remote IPC). `--ssh` sets the destination for this call and
|
|
@@ -31,12 +34,18 @@ import { callServe, installServeLogon, serveCallTimeout, uninstallServeLogon, }
|
|
|
31
34
|
export function resolveInvocationEnv(args, base) {
|
|
32
35
|
const local = isTrue(args.local);
|
|
33
36
|
const sshDest = typeof args.ssh === "string" && args.ssh.trim() ? args.ssh.trim() : null;
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
const serveAddress = optionalString(args.serveAddress);
|
|
38
|
+
if (Number(local) + Number(Boolean(sshDest)) + Number(Boolean(serveAddress)) > 1) {
|
|
39
|
+
throw new ZellijError("usage", "choose only one of --local, --ssh, or --serve");
|
|
36
40
|
}
|
|
37
|
-
if (!local && !sshDest)
|
|
41
|
+
if (!local && !sshDest && !serveAddress)
|
|
38
42
|
return base;
|
|
39
43
|
const env = { ...base };
|
|
44
|
+
if (serveAddress) {
|
|
45
|
+
env.ZSWARM_SERVE = serveAddress;
|
|
46
|
+
delete env.ZSWARM_SSH;
|
|
47
|
+
return env;
|
|
48
|
+
}
|
|
40
49
|
if (local) {
|
|
41
50
|
delete env.ZSWARM_SSH;
|
|
42
51
|
delete env.ZSWARM_SSH_OPTS;
|
|
@@ -107,9 +116,10 @@ async function resolveTarget(client, args, state, clock, env, policy, op, signal
|
|
|
107
116
|
}
|
|
108
117
|
const until = Math.min(budget.deadline, clock.now() + observeMs);
|
|
109
118
|
let pane;
|
|
119
|
+
let fresh = isTrue(args.fresh) || Boolean(op);
|
|
110
120
|
for (;;) {
|
|
111
121
|
const left = observeMs === 0 ? budget.require() : Math.min(budget.require(), Math.max(1, until - clock.now()));
|
|
112
|
-
panes ??= await client.listPanes(session, left);
|
|
122
|
+
panes ??= await client.listPanes(session, left, { fresh });
|
|
113
123
|
throwIfAborted(signal);
|
|
114
124
|
try {
|
|
115
125
|
pane = client.resolvePane(panes, to);
|
|
@@ -122,6 +132,7 @@ async function resolveTarget(client, args, state, clock, env, policy, op, signal
|
|
|
122
132
|
if (clock.now() >= until)
|
|
123
133
|
throw err;
|
|
124
134
|
panes = null;
|
|
135
|
+
fresh = true;
|
|
125
136
|
}
|
|
126
137
|
}
|
|
127
138
|
if (policy && op)
|
|
@@ -163,27 +174,60 @@ async function dispatchOperation(args, injected, deps, context, env) {
|
|
|
163
174
|
// Policy gates the op before anything touches the session.
|
|
164
175
|
assertOpAllowed(policy, op);
|
|
165
176
|
assertSshGitAllowed(env, op, args);
|
|
177
|
+
if (op === "doctor") {
|
|
178
|
+
// Before serve forwarding and session resolution so a dead tunnel still
|
|
179
|
+
// yields controller findings. Policy already ran.
|
|
180
|
+
return await doctorOp(args, injected, deps, context, env, clock);
|
|
181
|
+
}
|
|
166
182
|
if (op === "serve") {
|
|
183
|
+
const timeoutMs = numberArg(args, "timeoutMs", DEFAULT_SERVE_INSTALL_TIMEOUT_MS, {
|
|
184
|
+
min: 1,
|
|
185
|
+
max: 900_000,
|
|
186
|
+
});
|
|
167
187
|
if (isTrue(args.clear)) {
|
|
168
188
|
const cleared = await uninstallServeLogon({
|
|
169
|
-
platform: process.platform,
|
|
189
|
+
platform: deps.serveInstall?.platform ?? process.platform,
|
|
190
|
+
timeoutMs,
|
|
191
|
+
signal,
|
|
192
|
+
env,
|
|
193
|
+
token: env.ZSWARM_SERVE_TOKEN,
|
|
194
|
+
...deps.serveInstall,
|
|
170
195
|
});
|
|
171
196
|
return { ok: true, data: cleared };
|
|
172
197
|
}
|
|
173
198
|
if (isTrue(args.install)) {
|
|
174
199
|
const installed = await installServeLogon({
|
|
175
200
|
listen: typeof args.listen === "string" ? args.listen : undefined,
|
|
201
|
+
session: optionalString(args.session),
|
|
202
|
+
timeoutMs,
|
|
203
|
+
signal,
|
|
204
|
+
env,
|
|
205
|
+
token: env.ZSWARM_SERVE_TOKEN,
|
|
206
|
+
now: clock.now,
|
|
207
|
+
sleep: clock.sleep,
|
|
208
|
+
tailscaleStatus: deps.tailscaleStatus,
|
|
209
|
+
networkInterfaces: deps.networkInterfaces,
|
|
210
|
+
...deps.serveInstall,
|
|
176
211
|
});
|
|
177
|
-
return { ok: true, data:
|
|
212
|
+
return { ok: true, data: installed };
|
|
178
213
|
}
|
|
179
214
|
throw new ZellijError("usage", "serve --listen is CLI-only; MCP/dispatch would hang the tool call. Run `zswarm serve --listen` in the session that owns Zellij");
|
|
180
215
|
}
|
|
181
216
|
// An injected client is a unit-test (or in-process) Zellij; do not skip it
|
|
182
217
|
// just because the host env has ZSWARM_SERVE set.
|
|
183
218
|
if (!injected && env.ZSWARM_SERVE?.trim()) {
|
|
184
|
-
|
|
219
|
+
const request = { ...attachKnownSender(args, env) };
|
|
220
|
+
return await forwardServe({
|
|
221
|
+
target: env.ZSWARM_SERVE.trim(),
|
|
222
|
+
args: request,
|
|
223
|
+
timeoutMs: serveCallTimeout(args),
|
|
224
|
+
token: env.ZSWARM_SERVE_TOKEN,
|
|
225
|
+
signal,
|
|
226
|
+
env,
|
|
227
|
+
manager: deps.serveTunnels,
|
|
228
|
+
});
|
|
185
229
|
}
|
|
186
|
-
const baseClient = injected ?? createZellijClient({ env, signal });
|
|
230
|
+
const baseClient = injected ?? createZellijClient({ env, signal, cache: isTrue(args.fresh) ? false : undefined });
|
|
187
231
|
const client = {
|
|
188
232
|
...baseClient,
|
|
189
233
|
get transport() { return baseClient.transport; },
|
|
@@ -224,9 +268,10 @@ async function dispatchOperation(args, injected, deps, context, env) {
|
|
|
224
268
|
const { session } = await client.resolveSession(typeof args.session === "string" ? args.session : undefined);
|
|
225
269
|
// The plugin's manifest carries no cwd or floating flag, so anything
|
|
226
270
|
// verbose has to go the polling route.
|
|
227
|
-
const
|
|
271
|
+
const observedBus = verbose
|
|
228
272
|
? null
|
|
229
273
|
: await busSnapshot(client, state(), session, clock, env);
|
|
274
|
+
const bus = observedBus?.snapshot.ready ? observedBus : null;
|
|
230
275
|
const view = bus ? paneViewBus : verbose ? paneViewFull : paneViewSlim;
|
|
231
276
|
const panes = (bus ? busToPanes(bus.snapshot) : await client.listPanes(session))
|
|
232
277
|
.filter((p) => !p.isPlugin)
|
|
@@ -391,24 +436,39 @@ async function dispatchOperation(args, injected, deps, context, env) {
|
|
|
391
436
|
throwIfAborted(signal);
|
|
392
437
|
const { session } = await client.resolveSession(typeof args.session === "string" ? args.session : undefined, setupBudget());
|
|
393
438
|
throwIfAborted(signal);
|
|
394
|
-
//
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
:
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
439
|
+
// Once the session is known, verbose metadata and the bus manifest
|
|
440
|
+
// are independent reads. Both consume this operation's remaining time.
|
|
441
|
+
const readBus = () => remaining() <= 0 ? Promise.resolve(null)
|
|
442
|
+
: busSnapshot(client, state(), session, clock, env, "status", { deadlineAt, signal });
|
|
443
|
+
let observedBus;
|
|
444
|
+
let detailedPanes;
|
|
445
|
+
if (verbose) {
|
|
446
|
+
const reads = await Promise.allSettled([
|
|
447
|
+
readBus(), client.listPanes(session, setupBudget(), { fresh: true }),
|
|
448
|
+
]);
|
|
449
|
+
throwIfAborted(signal);
|
|
450
|
+
if (reads[1].status === "rejected")
|
|
451
|
+
throw reads[1].reason;
|
|
452
|
+
if (reads[0].status === "rejected")
|
|
453
|
+
throw reads[0].reason;
|
|
454
|
+
observedBus = reads[0].value;
|
|
455
|
+
detailedPanes = reads[1].value;
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
observedBus = await readBus();
|
|
459
|
+
}
|
|
460
|
+
const bus = observedBus?.snapshot.ready ? observedBus : null;
|
|
401
461
|
throwIfAborted(signal);
|
|
402
462
|
// Always hand peerStatus the resolved session (and panes) so it does
|
|
403
463
|
// not spend the overall budget resolving again.
|
|
404
|
-
const panes = bus
|
|
464
|
+
const panes = detailedPanes ?? (bus
|
|
405
465
|
? busToPanes(bus.snapshot)
|
|
406
|
-
: await client.listPanes(session, setupBudget());
|
|
466
|
+
: await client.listPanes(session, setupBudget()));
|
|
407
467
|
throwIfAborted(signal);
|
|
408
468
|
let supplied = {
|
|
409
469
|
session,
|
|
410
470
|
panes,
|
|
411
|
-
source: bus ? "plugin" : "zellij",
|
|
471
|
+
source: bus && !verbose ? "plugin" : "zellij",
|
|
412
472
|
};
|
|
413
473
|
if (bus) {
|
|
414
474
|
supplied = {
|
|
@@ -433,13 +493,24 @@ async function dispatchOperation(args, injected, deps, context, env) {
|
|
|
433
493
|
catch {
|
|
434
494
|
// Command-shaped `to` needs the polled list; re-fetch without bus.
|
|
435
495
|
const polled = await client.listPanes(session, setupBudget());
|
|
436
|
-
supplied = { session, panes: polled, source: "zellij" };
|
|
496
|
+
supplied = { ...supplied, session, panes: polled, source: "zellij" };
|
|
437
497
|
}
|
|
438
498
|
}
|
|
439
|
-
|
|
499
|
+
const result = await peerStatus(client, args, clock, supplied, {
|
|
440
500
|
deadlineAt,
|
|
441
501
|
signal,
|
|
442
502
|
});
|
|
503
|
+
if (result.ok && result.data && typeof result.data === "object") {
|
|
504
|
+
const data = result.data;
|
|
505
|
+
data.polling = {
|
|
506
|
+
busAvailable: Boolean(bus),
|
|
507
|
+
...(bus ? {} : { reason: planBus(client, state(), env, session).reason }),
|
|
508
|
+
...(client.remote ? {
|
|
509
|
+
recommendation: "For frequent status calls, run zswarm serve beside Zellij and use --serve 127.0.0.1:9419 through an SSH tunnel; direct SSH cannot use the local event bus.",
|
|
510
|
+
} : {}),
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
return result;
|
|
443
514
|
}
|
|
444
515
|
case "bus":
|
|
445
516
|
return await busOp(client, state(), args, clock, env, policy);
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { type ZellijClient } from "../zellij/client.js";
|
|
2
|
+
import { type StateStore } from "../state.js";
|
|
3
|
+
import type { RoutingContext } from "./routing.js";
|
|
4
|
+
import { type ServeHelloData } from "./serve.js";
|
|
5
|
+
import type { Clock, DispatchDeps, OpsResult } from "./types.js";
|
|
6
|
+
/** Overall doctor deadline when `timeoutMs` is omitted. */
|
|
7
|
+
export declare const DEFAULT_DOCTOR_TIMEOUT_MS = 10000;
|
|
8
|
+
/** Internal serve-worker flag. Never a public CLI flag or MCP param. */
|
|
9
|
+
export declare const DOCTOR_SCOPE_FIELD = "doctorScope";
|
|
10
|
+
export declare const DOCTOR_SCOPE_HOST = "host";
|
|
11
|
+
export declare const DOCTOR_FAILED_CODE = "doctor_failed";
|
|
12
|
+
/** Host doctor reply after hello was empty, malformed, or not a report. */
|
|
13
|
+
export declare const HOST_REPORT_INVALID_CODE = "host_report_invalid";
|
|
14
|
+
/** Host doctor reply omitted required host/session coverage. */
|
|
15
|
+
export declare const HOST_REPORT_INCOMPLETE_CODE = "host_report_incomplete";
|
|
16
|
+
/** Cap for optional Tailscale so required SSH/hello/session keep budget. */
|
|
17
|
+
export declare const DOCTOR_TAILSCALE_MAX_MS = 1500;
|
|
18
|
+
export type DoctorCheckState = "ok" | "warn" | "fail" | "skipped";
|
|
19
|
+
export type DoctorCheckScope = "controller" | "host";
|
|
20
|
+
export type DoctorCheck = {
|
|
21
|
+
id: string;
|
|
22
|
+
scope: DoctorCheckScope;
|
|
23
|
+
state: DoctorCheckState;
|
|
24
|
+
code: string;
|
|
25
|
+
elapsedMs: number;
|
|
26
|
+
detail: Record<string, unknown>;
|
|
27
|
+
remedy: string | null;
|
|
28
|
+
};
|
|
29
|
+
export type DoctorRoute = {
|
|
30
|
+
transport: "local" | "ssh" | "serve";
|
|
31
|
+
endpoint: string;
|
|
32
|
+
session: string | null;
|
|
33
|
+
sessionOrigin: string;
|
|
34
|
+
};
|
|
35
|
+
export type DoctorReport = {
|
|
36
|
+
route: DoctorRoute;
|
|
37
|
+
server?: ServeHelloData;
|
|
38
|
+
checks: DoctorCheck[];
|
|
39
|
+
};
|
|
40
|
+
export type HostInspectInput = {
|
|
41
|
+
client: ZellijClient;
|
|
42
|
+
state: StateStore;
|
|
43
|
+
env: NodeJS.ProcessEnv;
|
|
44
|
+
sessionArg?: string | null;
|
|
45
|
+
clock: Clock;
|
|
46
|
+
timeoutMs: number;
|
|
47
|
+
signal?: AbortSignal;
|
|
48
|
+
/** Direct SSH cannot treat controller-local bus files as host facts. */
|
|
49
|
+
busUnsupported?: boolean;
|
|
50
|
+
busRemedy?: string;
|
|
51
|
+
};
|
|
52
|
+
export type HostInspectResult = {
|
|
53
|
+
checks: DoctorCheck[];
|
|
54
|
+
session: string | null;
|
|
55
|
+
sessionOrigin: string;
|
|
56
|
+
cancelled?: boolean;
|
|
57
|
+
timedOut?: boolean;
|
|
58
|
+
};
|
|
59
|
+
/** Required-layer failure using accepted doctor semantics. Host install must reuse this. */
|
|
60
|
+
export declare function isRequiredFailure(report: DoctorReport, item: DoctorCheck): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Passive host inspection. Safe reads only: binary/version via the client,
|
|
63
|
+
* session listing, pane listing for an existing plugin pane, and StateStore
|
|
64
|
+
* reads. Never pipes, launches, nudges, or writes markers.
|
|
65
|
+
*/
|
|
66
|
+
export declare function inspectDoctorHost(input: HostInspectInput): Promise<HostInspectResult>;
|
|
67
|
+
export declare function hostDoctorRequest(remainingMs: number, explicitSession: string | null): Record<string, unknown>;
|
|
68
|
+
export declare function coverHostReport(checks: DoctorCheck[], explicit: string | null): DoctorCheck[];
|
|
69
|
+
export declare function mergeHostReply(result: OpsResult): {
|
|
70
|
+
checks: DoctorCheck[];
|
|
71
|
+
session?: string | null;
|
|
72
|
+
sessionOrigin?: string;
|
|
73
|
+
unsupported?: boolean;
|
|
74
|
+
invalid?: boolean;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Inspect-only layered diagnostics. Handle this before ordinary serve
|
|
78
|
+
* forwarding so a dead tunnel still reports controller findings.
|
|
79
|
+
*/
|
|
80
|
+
export declare function doctorOp(args: Record<string, unknown>, injected: ZellijClient | undefined, deps: DispatchDeps, context: RoutingContext, env: NodeJS.ProcessEnv, clock: Clock): Promise<OpsResult>;
|