@zswarm/core 0.1.7 → 0.1.9
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 +10 -1
- package/dist/exec.js +28 -15
- package/dist/index.d.ts +6 -2
- package/dist/index.js +5 -1
- package/dist/ops/broadcast.d.ts +6 -0
- package/dist/ops/broadcast.js +15 -11
- package/dist/ops/bus.js +7 -0
- package/dist/ops/dispatch.js +39 -5
- package/dist/ops/doctor.d.ts +80 -0
- package/dist/ops/doctor.js +1377 -0
- package/dist/ops/routing.js +2 -1
- 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/status.d.ts +4 -2
- package/dist/ops/status.js +15 -12
- package/dist/ops/types.d.ts +49 -0
- package/dist/ops/util.js +8 -1
- package/dist/ops/wait.js +29 -7
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +8 -7
- package/dist/state.d.ts +7 -0
- package/dist/state.js +209 -46
- package/dist/zellij/binary.d.ts +7 -0
- package/dist/zellij/binary.js +20 -7
- package/dist/zellij/client.js +7 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,6 +30,14 @@ const result = await dispatchZswarm({ op: "list" });
|
|
|
30
30
|
`{ ok, data }` or `{ ok: false, error }`, with routing `context` once the
|
|
31
31
|
invocation's destination is known.
|
|
32
32
|
|
|
33
|
+
`ZSWARM_SERVE` / `--serve` may be `host:port`, `tcp://host:port`, or
|
|
34
|
+
`ssh://user@host[:sshPort]?servePort=9419`. The ssh URI opens a process-owned
|
|
35
|
+
SSH LocalForward and calls `probeServe` before ops (including before reuse);
|
|
36
|
+
pass a `serveTunnels` manager if the caller (MCP) should reuse it. Omitting
|
|
37
|
+
the SSH port leaves `Port` to OpenSSH instead of forcing 22. `tcp://` also
|
|
38
|
+
names a private Tailscale Serve frontend over a loopback backend; see
|
|
39
|
+
[Tailscale crew](../../docs/tailscale.md).
|
|
40
|
+
|
|
33
41
|
Spawn lifecycle, body files, guarded keys, status tabs and routing context: [crew operations](../../docs/crew-operations.md).
|
|
34
42
|
|
|
35
43
|
Ops, env, and the event bus: the [zswarm README](../../README.md).
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Every zSwarm failure carries a stable machine-readable code. */
|
|
2
2
|
export declare class ZellijError extends Error {
|
|
3
3
|
readonly code: string;
|
|
4
|
-
|
|
4
|
+
readonly details?: Record<string, unknown>;
|
|
5
|
+
constructor(code: string, message: string, details?: Record<string, unknown>);
|
|
5
6
|
}
|
package/dist/errors.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/** Every zSwarm failure carries a stable machine-readable code. */
|
|
2
2
|
export class ZellijError extends Error {
|
|
3
3
|
code;
|
|
4
|
-
|
|
4
|
+
details;
|
|
5
|
+
constructor(code, message, details) {
|
|
5
6
|
super(message);
|
|
6
7
|
this.name = "ZellijError";
|
|
7
8
|
this.code = code;
|
|
9
|
+
if (details && Object.keys(details).length > 0)
|
|
10
|
+
this.details = details;
|
|
8
11
|
}
|
|
9
12
|
}
|
package/dist/exec.d.ts
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import { type IpcDirs, type RemoteShell } from "./zellij/ipc.js";
|
|
2
|
+
export type ExecSpawnFailure = {
|
|
3
|
+
/** Node could not start this process. Distinct from a remote 127. */
|
|
4
|
+
origin: "local";
|
|
5
|
+
errno: string;
|
|
6
|
+
bin: string;
|
|
7
|
+
};
|
|
2
8
|
export type ExecResult = {
|
|
3
9
|
code: number;
|
|
4
10
|
stdout: string;
|
|
5
11
|
stderr: string;
|
|
12
|
+
/** Present only when this host failed to spawn the binary. */
|
|
13
|
+
spawn?: ExecSpawnFailure;
|
|
6
14
|
};
|
|
7
15
|
export type ExecOptions = {
|
|
8
16
|
timeoutMs: number;
|
|
@@ -13,7 +21,8 @@ export type ExecOptions = {
|
|
|
13
21
|
* Stop as soon as the accumulated stdout satisfies this, instead of waiting
|
|
14
22
|
* for the process to exit. `zellij pipe` answers in milliseconds but stays
|
|
15
23
|
* resident when it has no terminal, so waiting for exit means waiting for the
|
|
16
|
-
* timeout.
|
|
24
|
+
* timeout. Replies are JSON lines, so this is only consulted when a chunk
|
|
25
|
+
* could have completed one (see `mayCompleteLine`).
|
|
17
26
|
*/
|
|
18
27
|
until?: (stdout: string) => boolean;
|
|
19
28
|
/** Kill the child when aborted (MCP cancellation). */
|
package/dist/exec.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { execFile, spawn } from "node:child_process";
|
|
2
2
|
import { cmdQuote, inferRemoteShell, parseZellijServerPaths, pickIpcDirs, unixDiscoverRemote, windowsDiscoverRemote, windowsInteractiveRemote, wrapWithTmpEnv, } from "./zellij/ipc.js";
|
|
3
3
|
export const NOT_FOUND_EXIT = 127;
|
|
4
|
+
function localSpawnResult(err, binPath) {
|
|
5
|
+
const errno = typeof err.code === "string" && err.code ? err.code : "error";
|
|
6
|
+
const missing = errno === "ENOENT" || errno === "ENOTDIR";
|
|
7
|
+
return {
|
|
8
|
+
code: missing ? NOT_FOUND_EXIT : 1,
|
|
9
|
+
stdout: "",
|
|
10
|
+
stderr: `${errno}: ${err.message} (bin=${binPath})`,
|
|
11
|
+
spawn: { origin: "local", errno, bin: binPath },
|
|
12
|
+
};
|
|
13
|
+
}
|
|
4
14
|
/** POSIX single-quoting, for building a command line the remote shell parses. */
|
|
5
15
|
export function shellQuote(arg) {
|
|
6
16
|
if (arg === "")
|
|
@@ -312,6 +322,14 @@ export function createSshExec(target, env, options = {}) {
|
|
|
312
322
|
};
|
|
313
323
|
return exec;
|
|
314
324
|
}
|
|
325
|
+
/**
|
|
326
|
+
* A JSON line is complete once its newline arrives, or once the stream ends on
|
|
327
|
+
* `}` (a last line written without one). Asking `until` about every chunk
|
|
328
|
+
* re-parses a multi-megabyte single-line scrollback reply once per 64KB read.
|
|
329
|
+
*/
|
|
330
|
+
function mayCompleteLine(chunk) {
|
|
331
|
+
return chunk.includes("\n") || chunk.trimEnd().endsWith("}");
|
|
332
|
+
}
|
|
315
333
|
/** Read stdout until the answer is in hand, then stop caring about the child. */
|
|
316
334
|
function runUntil(binPath, args, env, options) {
|
|
317
335
|
return new Promise((resolve) => {
|
|
@@ -349,20 +367,20 @@ function runUntil(binPath, args, env, options) {
|
|
|
349
367
|
}
|
|
350
368
|
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
351
369
|
child.on("error", (err) => {
|
|
352
|
-
|
|
353
|
-
finish({
|
|
354
|
-
code: missing ? NOT_FOUND_EXIT : 1,
|
|
355
|
-
stdout: "",
|
|
356
|
-
stderr: `${err.code ?? "error"}: ${err.message} (bin=${binPath})`,
|
|
357
|
-
});
|
|
370
|
+
finish(localSpawnResult(err, binPath));
|
|
358
371
|
});
|
|
372
|
+
// Decode across reads: a multi-byte character split between two chunks
|
|
373
|
+
// must not come out as U+FFFD in the screen text.
|
|
374
|
+
child.stdout.setEncoding("utf8");
|
|
375
|
+
child.stderr.setEncoding("utf8");
|
|
359
376
|
child.stdout.on("data", (chunk) => {
|
|
360
|
-
stdout += chunk
|
|
361
|
-
if (options.until(stdout))
|
|
377
|
+
stdout += chunk;
|
|
378
|
+
if (mayCompleteLine(chunk) && options.until(stdout)) {
|
|
362
379
|
finish({ code: 0, stdout, stderr });
|
|
380
|
+
}
|
|
363
381
|
});
|
|
364
382
|
child.stderr.on("data", (chunk) => {
|
|
365
|
-
stderr += chunk
|
|
383
|
+
stderr += chunk;
|
|
366
384
|
});
|
|
367
385
|
child.on("close", (code) => finish({ code: code ?? 0, stdout, stderr }));
|
|
368
386
|
});
|
|
@@ -383,12 +401,7 @@ function runToExit(binPath, args, env, options) {
|
|
|
383
401
|
}, (error, stdout, stderr) => {
|
|
384
402
|
const failure = error;
|
|
385
403
|
if (failure && typeof failure.code === "string") {
|
|
386
|
-
|
|
387
|
-
resolve({
|
|
388
|
-
code: missing ? NOT_FOUND_EXIT : 1,
|
|
389
|
-
stdout: "",
|
|
390
|
-
stderr: `${failure.code}: ${failure.message} (bin=${binPath})`,
|
|
391
|
-
});
|
|
404
|
+
resolve(localSpawnResult(failure, binPath));
|
|
392
405
|
return;
|
|
393
406
|
}
|
|
394
407
|
if (failure &&
|
package/dist/index.d.ts
CHANGED
|
@@ -15,13 +15,17 @@ export { assertOpAllowed, assertPaneAllowed, isWriteOp, loadPolicy, type Policy,
|
|
|
15
15
|
export { buildSshRemoteCommand, createSshExec, quoteRemoteArg, shellQuote, type IpcDiscoveryState, type SshExecFn, type SshTarget, } from "./exec.js";
|
|
16
16
|
export { resolveSshTarget } from "./zellij/binary.js";
|
|
17
17
|
export { applyIpcTmpEnv, cmdQuote, concreteTmp, encodePowerShellCommand, inferRemoteShell, parseZellijServerPaths, pickIpcDirs, pickIpcTmp, socketDirFromServerPath, tmpFromServerPath, unixDiscoverRemote, windowsDiscoverRemote, windowsInteractiveRemote, wrapWithTmpEnv, } from "./zellij/ipc.js";
|
|
18
|
-
export { callServe, DEFAULT_SERVE_LISTEN,
|
|
18
|
+
export { callServe, DEFAULT_SERVE_LISTEN, isLoopbackHost, parseListenAddress, probeServe, redactServeSecret, SERVE_CALL_TIMEOUT_CAP_MS, SERVE_CAPABILITY_HELLO, SERVE_CONNECT_TIMEOUT_MS, SERVE_CONTROL_FIELD, SERVE_HELLO_CONTROL, SERVE_HELLO_TIMEOUT_MS, SERVE_CORE_VERSION, SERVE_LAUNCH_ID_ENV, SERVE_MAX_HELLO_BYTES, SERVE_MAX_REPLY_BYTES, SERVE_MAX_REQUEST_BYTES, SERVE_CAPTURE_BUDGET_BYTES, SERVE_PROTOCOL, SERVE_REMEDY, SERVE_TASK_NAME, serveCallTimeout, serveChildEnv, serveLogonCommand, serveMaxReplyBytes, startServe, type CallServeOptions, type ProbeServeOptions, type ServeCapability, type ServeDelivery, type ServeErrorDetails, type ServeHelloData, type ServePhase, type StartServeOptions, } from "./ops/serve.js";
|
|
19
|
+
export { authorizeServeListen, canonicalizeListenIp, expandIPv6, sameCanonicalIp, SERVE_BIND_REMEDY, SERVE_BIND_STATUS_MAX_BYTES, SERVE_BIND_VERIFY_TIMEOUT_MS, SERVE_TAILSCALE_BIN_ENV, type ServeBindAuthorization, type ServeBindDeps, } from "./ops/serve-bind.js";
|
|
20
|
+
export { buildServeTaskScript, DEFAULT_SERVE_INSTALL_TIMEOUT_MS, installServeLogon, looksLikeCliEntrypoint, looksLikeMcpEntrypoint, resolveServeCliLaunch, sameWindowsAccount, sameWindowsSid, SERVE_NOT_READY_CODE, SERVE_TASK_OWNED_CODE, serveCommandFingerprint, serveTaskChildEnv, serveTaskLaunchEnv, uninstallServeLogon, type ServeCliLaunch, type ServeInstallInput, type ServeInstallSuccess, type ServeTaskAction, } from "./ops/serve-install.js";
|
|
21
|
+
export { allocateLoopbackPort, assertSafeSshTunnelOpts, buildSshTunnelArgv, createServeTunnelManager, DEFAULT_SSH_PORT, DEFAULT_SSH_SERVE_PORT, describeServeTarget, formatSshServeTarget, forwardServe, isSshServeTarget, parseSshServeTarget, redactSshUserinfo, serveTunnelCacheKey, SSH_SERVE_REMOTE_HOST, SSH_TUNNEL_KEEPALIVE_COUNT, SSH_TUNNEL_KEEPALIVE_INTERVAL_S, stripControllerRouting, type AcquireServeTunnelOptions, type ParsedSshServeTarget, type ServeTunnelHandle, type ServeTunnelManager, type ServeTunnelManagerOptions, } from "./ops/serve-tunnel.js";
|
|
19
22
|
export { parseTabList, resolveTab, type ZellijTab } from "./zellij/tabs.js";
|
|
20
23
|
export { selectTargets } from "./ops/broadcast.js";
|
|
21
24
|
export { diffScreens, cursorKey } from "./ops/tail.js";
|
|
22
25
|
export { classify, lastLine, mapPool, peerStatus, DEFAULT_STATUS_TIMEOUT_MS, STATUS_DUMP_CONCURRENCY, type PeerState, } from "./ops/status.js";
|
|
23
26
|
export { normalizeScreen, truncateDumpText, DEFAULT_DUMP_MAX_CHARS, DEFAULT_WAIT_MAX_CHARS, } from "./ops/util.js";
|
|
24
27
|
export type { RoutingContext } from "./ops/routing.js";
|
|
25
|
-
export type { DispatchDeps, OpsResult } from "./ops/types.js";
|
|
28
|
+
export type { DispatchDeps, OpsResult, ServeInstallDeps } from "./ops/types.js";
|
|
29
|
+
export { DEFAULT_DOCTOR_TIMEOUT_MS, DOCTOR_FAILED_CODE, DOCTOR_SCOPE_FIELD, DOCTOR_SCOPE_HOST, DOCTOR_TAILSCALE_MAX_MS, HOST_REPORT_INCOMPLETE_CODE, HOST_REPORT_INVALID_CODE, doctorOp, inspectDoctorHost, coverHostReport, hostDoctorRequest, isRequiredFailure, mergeHostReply, type DoctorCheck, type DoctorCheckScope, type DoctorCheckState, type DoctorReport, type DoctorRoute, type HostInspectInput, } from "./ops/doctor.js";
|
|
26
30
|
export { normalizeKey, normalizeKeys, tokenizeCommand } from "./keys.js";
|
|
27
31
|
export { cliUsage, mcpInputSchema, parseCliArgv, MCP_TOOL_DESCRIPTION, OP_NAMES, PARAMS, TARGET_OPS, type OpName, type ParamSpec, type ParamType, } from "./schema.js";
|
package/dist/index.js
CHANGED
|
@@ -15,11 +15,15 @@ export { assertOpAllowed, assertPaneAllowed, isWriteOp, loadPolicy, } from "./po
|
|
|
15
15
|
export { buildSshRemoteCommand, createSshExec, quoteRemoteArg, shellQuote, } from "./exec.js";
|
|
16
16
|
export { resolveSshTarget } from "./zellij/binary.js";
|
|
17
17
|
export { applyIpcTmpEnv, cmdQuote, concreteTmp, encodePowerShellCommand, inferRemoteShell, parseZellijServerPaths, pickIpcDirs, pickIpcTmp, socketDirFromServerPath, tmpFromServerPath, unixDiscoverRemote, windowsDiscoverRemote, windowsInteractiveRemote, wrapWithTmpEnv, } from "./zellij/ipc.js";
|
|
18
|
-
export { callServe, DEFAULT_SERVE_LISTEN,
|
|
18
|
+
export { callServe, DEFAULT_SERVE_LISTEN, isLoopbackHost, parseListenAddress, probeServe, redactServeSecret, SERVE_CALL_TIMEOUT_CAP_MS, SERVE_CAPABILITY_HELLO, SERVE_CONNECT_TIMEOUT_MS, SERVE_CONTROL_FIELD, SERVE_HELLO_CONTROL, SERVE_HELLO_TIMEOUT_MS, SERVE_CORE_VERSION, SERVE_LAUNCH_ID_ENV, SERVE_MAX_HELLO_BYTES, SERVE_MAX_REPLY_BYTES, SERVE_MAX_REQUEST_BYTES, SERVE_CAPTURE_BUDGET_BYTES, SERVE_PROTOCOL, SERVE_REMEDY, SERVE_TASK_NAME, serveCallTimeout, serveChildEnv, serveLogonCommand, serveMaxReplyBytes, startServe, } from "./ops/serve.js";
|
|
19
|
+
export { authorizeServeListen, canonicalizeListenIp, expandIPv6, sameCanonicalIp, SERVE_BIND_REMEDY, SERVE_BIND_STATUS_MAX_BYTES, SERVE_BIND_VERIFY_TIMEOUT_MS, SERVE_TAILSCALE_BIN_ENV, } from "./ops/serve-bind.js";
|
|
20
|
+
export { buildServeTaskScript, DEFAULT_SERVE_INSTALL_TIMEOUT_MS, installServeLogon, looksLikeCliEntrypoint, looksLikeMcpEntrypoint, resolveServeCliLaunch, sameWindowsAccount, sameWindowsSid, SERVE_NOT_READY_CODE, SERVE_TASK_OWNED_CODE, serveCommandFingerprint, serveTaskChildEnv, serveTaskLaunchEnv, uninstallServeLogon, } from "./ops/serve-install.js";
|
|
21
|
+
export { allocateLoopbackPort, assertSafeSshTunnelOpts, buildSshTunnelArgv, createServeTunnelManager, DEFAULT_SSH_PORT, DEFAULT_SSH_SERVE_PORT, describeServeTarget, formatSshServeTarget, forwardServe, isSshServeTarget, parseSshServeTarget, redactSshUserinfo, serveTunnelCacheKey, SSH_SERVE_REMOTE_HOST, SSH_TUNNEL_KEEPALIVE_COUNT, SSH_TUNNEL_KEEPALIVE_INTERVAL_S, stripControllerRouting, } from "./ops/serve-tunnel.js";
|
|
19
22
|
export { parseTabList, resolveTab } from "./zellij/tabs.js";
|
|
20
23
|
export { selectTargets } from "./ops/broadcast.js";
|
|
21
24
|
export { diffScreens, cursorKey } from "./ops/tail.js";
|
|
22
25
|
export { classify, lastLine, mapPool, peerStatus, DEFAULT_STATUS_TIMEOUT_MS, STATUS_DUMP_CONCURRENCY, } from "./ops/status.js";
|
|
23
26
|
export { normalizeScreen, truncateDumpText, DEFAULT_DUMP_MAX_CHARS, DEFAULT_WAIT_MAX_CHARS, } from "./ops/util.js";
|
|
27
|
+
export { DEFAULT_DOCTOR_TIMEOUT_MS, DOCTOR_FAILED_CODE, DOCTOR_SCOPE_FIELD, DOCTOR_SCOPE_HOST, DOCTOR_TAILSCALE_MAX_MS, HOST_REPORT_INCOMPLETE_CODE, HOST_REPORT_INVALID_CODE, doctorOp, inspectDoctorHost, coverHostReport, hostDoctorRequest, isRequiredFailure, mergeHostReply, } from "./ops/doctor.js";
|
|
24
28
|
export { normalizeKey, normalizeKeys, tokenizeCommand } from "./keys.js";
|
|
25
29
|
export { cliUsage, mcpInputSchema, parseCliArgv, MCP_TOOL_DESCRIPTION, OP_NAMES, PARAMS, TARGET_OPS, } from "./schema.js";
|
package/dist/ops/broadcast.d.ts
CHANGED
|
@@ -3,6 +3,12 @@ import type { StateStore } from "../state.js";
|
|
|
3
3
|
import type { ZellijClient } from "../zellij/client.js";
|
|
4
4
|
import type { ZellijPane } from "../zellij/panes.js";
|
|
5
5
|
import type { Clock, OpsResult } from "./types.js";
|
|
6
|
+
/**
|
|
7
|
+
* Deliveries in flight at once. Each one is a dump, paste, Enter, settle sleep
|
|
8
|
+
* and re-dump against its own `--pane-id`, so panes do not wait on each other;
|
|
9
|
+
* the cap keeps a large crew from launching a burst of zellij (or ssh) clients.
|
|
10
|
+
*/
|
|
11
|
+
export declare const BROADCAST_CONCURRENCY = 3;
|
|
6
12
|
export type Skipped = {
|
|
7
13
|
id: string;
|
|
8
14
|
reason: string;
|
package/dist/ops/broadcast.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { ZellijError } from "../errors.js";
|
|
2
2
|
import { assertPaneAllowed } from "../policy.js";
|
|
3
3
|
import { deliverTo, selfPaneTitle, withSenderLabel } from "./delivery.js";
|
|
4
|
+
import { mapPool } from "./status.js";
|
|
4
5
|
import { isTrue, optionalString } from "./util.js";
|
|
6
|
+
/**
|
|
7
|
+
* Deliveries in flight at once. Each one is a dump, paste, Enter, settle sleep
|
|
8
|
+
* and re-dump against its own `--pane-id`, so panes do not wait on each other;
|
|
9
|
+
* the cap keeps a large crew from launching a burst of zellij (or ssh) clients.
|
|
10
|
+
*/
|
|
11
|
+
export const BROADCAST_CONCURRENCY = 3;
|
|
5
12
|
/**
|
|
6
13
|
* Pick the panes a broadcast should reach: an explicit comma list, a tab, or
|
|
7
14
|
* every terminal pane — then drop the ones it must not write to.
|
|
@@ -92,17 +99,14 @@ export async function broadcast(client, state, args, clock, policy, env = proces
|
|
|
92
99
|
if (targets.length === 0) {
|
|
93
100
|
throw new ZellijError("no_targets", `nothing to broadcast to${skipped.length > 0 ? ` (skipped ${skipped.map((s) => `${s.id}:${s.reason}`).join(", ")})` : ""}`);
|
|
94
101
|
}
|
|
95
|
-
const results =
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
clock,
|
|
104
|
-
}));
|
|
105
|
-
}
|
|
102
|
+
const results = await mapPool(targets, BROADCAST_CONCURRENCY, (pane) => deliverTo(client, state, labeled, {
|
|
103
|
+
session,
|
|
104
|
+
pane,
|
|
105
|
+
body,
|
|
106
|
+
op: "broadcast",
|
|
107
|
+
at: clock.now(),
|
|
108
|
+
clock,
|
|
109
|
+
}));
|
|
106
110
|
const delivered = results.filter((r) => r.ok);
|
|
107
111
|
return {
|
|
108
112
|
ok: true,
|
package/dist/ops/bus.js
CHANGED
|
@@ -296,8 +296,15 @@ export async function closeBusPluginPanes(client, session, pluginPath) {
|
|
|
296
296
|
}
|
|
297
297
|
return closed;
|
|
298
298
|
}
|
|
299
|
+
/** Effective SSH transport — not controller OS, not leftover ZSWARM_SSH in env. */
|
|
300
|
+
function isDirectSshClient(client) {
|
|
301
|
+
return client.remote || client.transport.kind === "ssh";
|
|
302
|
+
}
|
|
299
303
|
/** Report on the bus, install it, or forget it. */
|
|
300
304
|
export async function busOp(client, state, args, clock, env = process.env, policy) {
|
|
305
|
+
if ((isTrue(args.install) || isTrue(args.clear)) && isDirectSshClient(client)) {
|
|
306
|
+
throw new ZellijError("bus_remote_unsupported", "run on Zellij host or use --serve through a tunnel; direct SSH cannot install/clear the bus");
|
|
307
|
+
}
|
|
301
308
|
const sessionArg = typeof args.session === "string" ? args.session : undefined;
|
|
302
309
|
if (isTrue(args.clear)) {
|
|
303
310
|
let session = null;
|
package/dist/ops/dispatch.js
CHANGED
|
@@ -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
|
|
@@ -171,18 +174,42 @@ async function dispatchOperation(args, injected, deps, context, env) {
|
|
|
171
174
|
// Policy gates the op before anything touches the session.
|
|
172
175
|
assertOpAllowed(policy, op);
|
|
173
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
|
+
}
|
|
174
182
|
if (op === "serve") {
|
|
183
|
+
const timeoutMs = numberArg(args, "timeoutMs", DEFAULT_SERVE_INSTALL_TIMEOUT_MS, {
|
|
184
|
+
min: 1,
|
|
185
|
+
max: 900_000,
|
|
186
|
+
});
|
|
175
187
|
if (isTrue(args.clear)) {
|
|
176
188
|
const cleared = await uninstallServeLogon({
|
|
177
|
-
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,
|
|
178
195
|
});
|
|
179
196
|
return { ok: true, data: cleared };
|
|
180
197
|
}
|
|
181
198
|
if (isTrue(args.install)) {
|
|
182
199
|
const installed = await installServeLogon({
|
|
183
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,
|
|
184
211
|
});
|
|
185
|
-
return { ok: true, data:
|
|
212
|
+
return { ok: true, data: installed };
|
|
186
213
|
}
|
|
187
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");
|
|
188
215
|
}
|
|
@@ -190,8 +217,15 @@ async function dispatchOperation(args, injected, deps, context, env) {
|
|
|
190
217
|
// just because the host env has ZSWARM_SERVE set.
|
|
191
218
|
if (!injected && env.ZSWARM_SERVE?.trim()) {
|
|
192
219
|
const request = { ...attachKnownSender(args, env) };
|
|
193
|
-
|
|
194
|
-
|
|
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
|
+
});
|
|
195
229
|
}
|
|
196
230
|
const baseClient = injected ?? createZellijClient({ env, signal, cache: isTrue(args.fresh) ? false : undefined });
|
|
197
231
|
const client = {
|
|
@@ -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>;
|