@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/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
|
-
import { type RemoteShell } from "./zellij/ipc.js";
|
|
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;
|
|
@@ -49,10 +57,44 @@ export type IpcDiscoveryState = {
|
|
|
49
57
|
};
|
|
50
58
|
export type SshExecFn = ExecFn & {
|
|
51
59
|
ipcState: IpcDiscoveryState;
|
|
60
|
+
/**
|
|
61
|
+
* Resolve `ZSWARM_TMP=auto` once under this caller's budget before parallel
|
|
62
|
+
* identity/capability probes. Independent of other callers' in-flight work.
|
|
63
|
+
*/
|
|
64
|
+
prepareIpc: (timeoutMs: number, signal?: AbortSignal) => Promise<IpcDirs | undefined>;
|
|
52
65
|
};
|
|
66
|
+
/** Positive `auto` IPC hits only; failures/cancels never enter this map. */
|
|
67
|
+
export declare const DEFAULT_IPC_DISCOVERY_TTL_MS = 15000;
|
|
68
|
+
export declare const DEFAULT_IPC_DISCOVERY_CACHE_LIMIT = 512;
|
|
69
|
+
/** Fully scoped key so hosts / SSH bins / remote shells / users never collide. */
|
|
70
|
+
export declare function ipcDiscoveryCacheKey(target: SshTarget, env?: NodeJS.ProcessEnv): string;
|
|
71
|
+
/**
|
|
72
|
+
* Probe list for `ZSWARM_TMP=auto`. Known Windows (cmd / interactive / .exe)
|
|
73
|
+
* skips the Unix `ps` fallback; explicit `sh` skips PowerShell. Unknown
|
|
74
|
+
* `zellij` still tries both — a Windows host with remoteBin=zellij looks Unix.
|
|
75
|
+
*/
|
|
76
|
+
export declare function ipcDiscoveryProbes(target: SshTarget): string[];
|
|
77
|
+
export declare function resetIpcDiscoveryCache(): void;
|
|
78
|
+
/** Test helper: shrink the positive-hit TTL without exposing the map. */
|
|
79
|
+
export declare function setIpcDiscoveryTtlMs(ttlMs: number): void;
|
|
80
|
+
export declare function peekIpcDiscoveryCache(key: string): IpcDirs | undefined;
|
|
81
|
+
/**
|
|
82
|
+
* Canonical SSH + resolved IPC namespace. Listing caches should hash this
|
|
83
|
+
* (or `ipcState.tmp` / `socketDir` after a refreshed read) so a warmed
|
|
84
|
+
* socket context matches across `createSshExec` instances.
|
|
85
|
+
*/
|
|
86
|
+
export declare function sshIpcContextKey(target: SshTarget, env?: NodeJS.ProcessEnv, ipc?: {
|
|
87
|
+
tmp?: string;
|
|
88
|
+
socketDir?: string;
|
|
89
|
+
} | null): string;
|
|
90
|
+
/** Test helper: shrink the positive-hit map so eviction is observable. */
|
|
91
|
+
export declare function setIpcDiscoveryCacheLimit(limit: number): void;
|
|
92
|
+
export declare function ipcDiscoveryCacheSize(): number;
|
|
53
93
|
/**
|
|
54
94
|
* Run the remote zellij over ssh. The whole invocation is quoted into a single
|
|
55
95
|
* command string, because the remote login shell re-parses it.
|
|
56
96
|
*/
|
|
57
|
-
export declare function createSshExec(target: SshTarget, env: NodeJS.ProcessEnv
|
|
97
|
+
export declare function createSshExec(target: SshTarget, env: NodeJS.ProcessEnv, options?: {
|
|
98
|
+
pinIpc?: boolean;
|
|
99
|
+
}): SshExecFn;
|
|
58
100
|
export declare function createExec(binPath: string, env: NodeJS.ProcessEnv): ExecFn;
|
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 === "")
|
|
@@ -35,17 +45,104 @@ export function buildSshRemoteCommand(target, args, tmp, timeoutMs, socketDir) {
|
|
|
35
45
|
}
|
|
36
46
|
return withTmp;
|
|
37
47
|
}
|
|
38
|
-
|
|
48
|
+
/** Positive `auto` IPC hits only; failures/cancels never enter this map. */
|
|
49
|
+
export const DEFAULT_IPC_DISCOVERY_TTL_MS = 15_000;
|
|
50
|
+
export const DEFAULT_IPC_DISCOVERY_CACHE_LIMIT = 512;
|
|
51
|
+
const ipcDiscoveryCache = new Map();
|
|
52
|
+
let ipcDiscoveryTtlMs = DEFAULT_IPC_DISCOVERY_TTL_MS;
|
|
53
|
+
let ipcDiscoveryCacheLimit = DEFAULT_IPC_DISCOVERY_CACHE_LIMIT;
|
|
54
|
+
const IPC_USER_ENV = [
|
|
55
|
+
"USER",
|
|
56
|
+
"USERNAME",
|
|
57
|
+
"LOGNAME",
|
|
58
|
+
"HOME",
|
|
59
|
+
"USERPROFILE",
|
|
60
|
+
"USERDOMAIN",
|
|
61
|
+
"SSH_AUTH_SOCK",
|
|
62
|
+
"PATH",
|
|
63
|
+
"Path",
|
|
64
|
+
"XDG_CONFIG_HOME",
|
|
65
|
+
];
|
|
66
|
+
/** Fully scoped key so hosts / SSH bins / remote shells / users never collide. */
|
|
67
|
+
export function ipcDiscoveryCacheKey(target, env = {}) {
|
|
68
|
+
const requested = target.tmp?.trim() ?? "";
|
|
69
|
+
const user = IPC_USER_ENV.map((name) => env[name] ?? "");
|
|
70
|
+
return JSON.stringify([
|
|
71
|
+
target.ssh,
|
|
72
|
+
target.host,
|
|
73
|
+
target.options,
|
|
74
|
+
target.remoteBin,
|
|
75
|
+
target.remoteShell ?? "",
|
|
76
|
+
target.mode ?? "ssh",
|
|
77
|
+
requested.toLowerCase() === "auto" ? "auto" : requested,
|
|
78
|
+
user,
|
|
79
|
+
]);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Probe list for `ZSWARM_TMP=auto`. Known Windows (cmd / interactive / .exe)
|
|
83
|
+
* skips the Unix `ps` fallback; explicit `sh` skips PowerShell. Unknown
|
|
84
|
+
* `zellij` still tries both — a Windows host with remoteBin=zellij looks Unix.
|
|
85
|
+
*/
|
|
86
|
+
export function ipcDiscoveryProbes(target) {
|
|
39
87
|
const shell = inferRemoteShell({
|
|
40
88
|
explicit: target.remoteShell,
|
|
41
89
|
remoteBin: target.remoteBin,
|
|
42
90
|
mode: target.mode,
|
|
43
91
|
});
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
92
|
+
const windows = windowsDiscoverRemote();
|
|
93
|
+
const unix = unixDiscoverRemote();
|
|
94
|
+
if (shell === "cmd")
|
|
95
|
+
return [windows];
|
|
96
|
+
if (target.remoteShell === "sh")
|
|
97
|
+
return [unix];
|
|
98
|
+
return [unix, windows];
|
|
99
|
+
}
|
|
100
|
+
export function resetIpcDiscoveryCache() {
|
|
101
|
+
ipcDiscoveryCache.clear();
|
|
102
|
+
ipcDiscoveryTtlMs = DEFAULT_IPC_DISCOVERY_TTL_MS;
|
|
103
|
+
ipcDiscoveryCacheLimit = DEFAULT_IPC_DISCOVERY_CACHE_LIMIT;
|
|
104
|
+
}
|
|
105
|
+
/** Test helper: shrink the positive-hit TTL without exposing the map. */
|
|
106
|
+
export function setIpcDiscoveryTtlMs(ttlMs) {
|
|
107
|
+
ipcDiscoveryTtlMs = Math.max(0, ttlMs);
|
|
108
|
+
}
|
|
109
|
+
export function peekIpcDiscoveryCache(key) {
|
|
110
|
+
const entry = ipcDiscoveryCache.get(key);
|
|
111
|
+
if (!entry)
|
|
112
|
+
return undefined;
|
|
113
|
+
if (entry.expiresAt <= Date.now()) {
|
|
114
|
+
ipcDiscoveryCache.delete(key);
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
return { tmp: entry.dirs.tmp, socketDir: entry.dirs.socketDir };
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Canonical SSH + resolved IPC namespace. Listing caches should hash this
|
|
121
|
+
* (or `ipcState.tmp` / `socketDir` after a refreshed read) so a warmed
|
|
122
|
+
* socket context matches across `createSshExec` instances.
|
|
123
|
+
*/
|
|
124
|
+
export function sshIpcContextKey(target, env = {}, ipc) {
|
|
125
|
+
return JSON.stringify([ipcDiscoveryCacheKey(target, env), ipc?.tmp ?? "", ipc?.socketDir ?? ""]);
|
|
126
|
+
}
|
|
127
|
+
/** Test helper: shrink the positive-hit map so eviction is observable. */
|
|
128
|
+
export function setIpcDiscoveryCacheLimit(limit) {
|
|
129
|
+
ipcDiscoveryCacheLimit = Math.max(1, limit);
|
|
130
|
+
}
|
|
131
|
+
export function ipcDiscoveryCacheSize() {
|
|
132
|
+
return ipcDiscoveryCache.size;
|
|
133
|
+
}
|
|
134
|
+
function storeIpcDiscovery(key, dirs) {
|
|
135
|
+
ipcDiscoveryCache.delete(key);
|
|
136
|
+
ipcDiscoveryCache.set(key, {
|
|
137
|
+
dirs: { ...dirs },
|
|
138
|
+
expiresAt: Date.now() + ipcDiscoveryTtlMs,
|
|
139
|
+
});
|
|
140
|
+
while (ipcDiscoveryCache.size > ipcDiscoveryCacheLimit) {
|
|
141
|
+
ipcDiscoveryCache.delete(ipcDiscoveryCache.keys().next().value);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async function discoverRemoteIpc(runner, target, remaining, signal) {
|
|
145
|
+
const probes = ipcDiscoveryProbes(target);
|
|
49
146
|
for (const probe of probes) {
|
|
50
147
|
if (signal?.aborted)
|
|
51
148
|
return undefined;
|
|
@@ -56,6 +153,10 @@ async function discoverRemoteIpc(runner, target, remaining, signal) {
|
|
|
56
153
|
timeoutMs: left,
|
|
57
154
|
signal,
|
|
58
155
|
});
|
|
156
|
+
if (result.code !== 0)
|
|
157
|
+
continue;
|
|
158
|
+
if (remaining() <= 0)
|
|
159
|
+
return undefined;
|
|
59
160
|
const dirs = pickIpcDirs(parseZellijServerPaths(result.stdout));
|
|
60
161
|
if (dirs)
|
|
61
162
|
return dirs;
|
|
@@ -66,22 +167,63 @@ async function discoverRemoteIpc(runner, target, remaining, signal) {
|
|
|
66
167
|
* Run the remote zellij over ssh. The whole invocation is quoted into a single
|
|
67
168
|
* command string, because the remote login shell re-parses it.
|
|
68
169
|
*/
|
|
69
|
-
export function createSshExec(target, env) {
|
|
170
|
+
export function createSshExec(target, env, options = {}) {
|
|
70
171
|
const runner = createExec(target.ssh, env);
|
|
71
|
-
|
|
72
|
-
let
|
|
73
|
-
|
|
172
|
+
const cacheKey = ipcDiscoveryCacheKey(target, env);
|
|
173
|
+
let autoFailed = false;
|
|
174
|
+
let pinnedIpc;
|
|
74
175
|
const ipcState = {
|
|
75
|
-
requested:
|
|
76
|
-
status:
|
|
77
|
-
? "none"
|
|
78
|
-
: requestedTmp.toLowerCase() === "auto"
|
|
79
|
-
? "none"
|
|
80
|
-
: "skipped",
|
|
81
|
-
...(requestedTmp && requestedTmp.toLowerCase() !== "auto"
|
|
82
|
-
? { tmp: requestedTmp, socketDir: "" }
|
|
83
|
-
: {}),
|
|
176
|
+
requested: null,
|
|
177
|
+
status: "none",
|
|
84
178
|
};
|
|
179
|
+
function adopt(dirs) {
|
|
180
|
+
if (options.pinIpc) {
|
|
181
|
+
pinnedIpc ??= { ...dirs };
|
|
182
|
+
dirs = pinnedIpc;
|
|
183
|
+
}
|
|
184
|
+
autoFailed = false;
|
|
185
|
+
ipcState.status = "resolved";
|
|
186
|
+
ipcState.tmp = dirs.tmp;
|
|
187
|
+
ipcState.socketDir = dirs.socketDir;
|
|
188
|
+
return { ...dirs };
|
|
189
|
+
}
|
|
190
|
+
/** Seed from a positive hit; unpinned execs drop resolved dirs after TTL. */
|
|
191
|
+
function refreshIpcView() {
|
|
192
|
+
const requested = target.tmp?.trim() || null;
|
|
193
|
+
ipcState.requested = requested;
|
|
194
|
+
if (!requested) {
|
|
195
|
+
ipcState.status = "none";
|
|
196
|
+
delete ipcState.tmp;
|
|
197
|
+
delete ipcState.socketDir;
|
|
198
|
+
return ipcState;
|
|
199
|
+
}
|
|
200
|
+
if (requested.toLowerCase() !== "auto") {
|
|
201
|
+
ipcState.status = "skipped";
|
|
202
|
+
ipcState.tmp = requested;
|
|
203
|
+
ipcState.socketDir = "";
|
|
204
|
+
return ipcState;
|
|
205
|
+
}
|
|
206
|
+
// A high-level client pins its namespace so pane lookup and the following
|
|
207
|
+
// write cannot switch sockets when another caller refreshes discovery.
|
|
208
|
+
if (pinnedIpc)
|
|
209
|
+
return ipcState;
|
|
210
|
+
// Diagnostics describe this exec's last attempt, not a sibling's later
|
|
211
|
+
// success. A subsequent actual invocation may adopt a positive cache hit.
|
|
212
|
+
if (autoFailed)
|
|
213
|
+
return ipcState;
|
|
214
|
+
const cached = peekIpcDiscoveryCache(cacheKey);
|
|
215
|
+
if (cached) {
|
|
216
|
+
adopt(cached);
|
|
217
|
+
return ipcState;
|
|
218
|
+
}
|
|
219
|
+
if (ipcState.status === "resolved") {
|
|
220
|
+
ipcState.status = "none";
|
|
221
|
+
delete ipcState.tmp;
|
|
222
|
+
delete ipcState.socketDir;
|
|
223
|
+
}
|
|
224
|
+
return ipcState;
|
|
225
|
+
}
|
|
226
|
+
refreshIpcView();
|
|
85
227
|
async function resolveIpc(remaining, signal) {
|
|
86
228
|
const requested = target.tmp?.trim();
|
|
87
229
|
if (!requested) {
|
|
@@ -96,40 +238,50 @@ export function createSshExec(target, env) {
|
|
|
96
238
|
ipcState.socketDir = "";
|
|
97
239
|
return { tmp: requested, socketDir: "" };
|
|
98
240
|
}
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return cached;
|
|
104
|
-
}
|
|
241
|
+
if (pinnedIpc)
|
|
242
|
+
return adopt(pinnedIpc);
|
|
243
|
+
const cached = peekIpcDiscoveryCache(cacheKey);
|
|
244
|
+
if (cached)
|
|
245
|
+
return adopt(cached);
|
|
105
246
|
// Do not re-probe after a failed/expired/cancelled attempt on this exec.
|
|
106
|
-
|
|
247
|
+
// A later createSshExec with the same key still probes — failures are not
|
|
248
|
+
// stored, and in-flight work is never shared across callers' budgets.
|
|
249
|
+
if (autoFailed)
|
|
107
250
|
return undefined;
|
|
108
251
|
if (signal?.aborted) {
|
|
252
|
+
autoFailed = true;
|
|
109
253
|
ipcState.status = "cancelled";
|
|
110
254
|
return undefined;
|
|
111
255
|
}
|
|
112
256
|
if (remaining() <= 0) {
|
|
257
|
+
autoFailed = true;
|
|
113
258
|
ipcState.status = "expired";
|
|
114
259
|
return undefined;
|
|
115
260
|
}
|
|
116
|
-
autoAttempted = true;
|
|
117
261
|
const dirs = await discoverRemoteIpc(runner, target, remaining, signal);
|
|
262
|
+
// Another probe on this client may have pinned a namespace while we were
|
|
263
|
+
// waiting. Its first positive result wins, even if this probe failed.
|
|
264
|
+
if (pinnedIpc)
|
|
265
|
+
return adopt(pinnedIpc);
|
|
118
266
|
if (signal?.aborted) {
|
|
267
|
+
autoFailed = true;
|
|
119
268
|
ipcState.status = "cancelled";
|
|
120
269
|
return undefined;
|
|
121
270
|
}
|
|
122
271
|
if (dirs) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
272
|
+
if (remaining() <= 0) {
|
|
273
|
+
autoFailed = true;
|
|
274
|
+
ipcState.status = "expired";
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
storeIpcDiscovery(cacheKey, dirs);
|
|
278
|
+
return adopt(dirs);
|
|
128
279
|
}
|
|
280
|
+
autoFailed = true;
|
|
129
281
|
ipcState.status = remaining() <= 0 ? "expired" : "failed";
|
|
130
282
|
return undefined;
|
|
131
283
|
}
|
|
132
|
-
const exec = async (args, options) => {
|
|
284
|
+
const exec = (async (args, options) => {
|
|
133
285
|
const deadline = Date.now() + options.timeoutMs;
|
|
134
286
|
const remaining = () => Math.max(0, deadline - Date.now());
|
|
135
287
|
const ipc = await resolveIpc(remaining, options.signal);
|
|
@@ -156,8 +308,18 @@ export function createSshExec(target, env) {
|
|
|
156
308
|
...options,
|
|
157
309
|
timeoutMs: left,
|
|
158
310
|
});
|
|
311
|
+
});
|
|
312
|
+
Object.defineProperty(exec, "ipcState", {
|
|
313
|
+
enumerable: true,
|
|
314
|
+
configurable: true,
|
|
315
|
+
get() {
|
|
316
|
+
return { ...refreshIpcView() };
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
exec.prepareIpc = (timeoutMs, signal) => {
|
|
320
|
+
const deadline = Date.now() + timeoutMs;
|
|
321
|
+
return resolveIpc(() => Math.max(0, deadline - Date.now()), signal);
|
|
159
322
|
};
|
|
160
|
-
exec.ipcState = ipcState;
|
|
161
323
|
return exec;
|
|
162
324
|
}
|
|
163
325
|
/** Read stdout until the answer is in hand, then stop caring about the child. */
|
|
@@ -197,12 +359,7 @@ function runUntil(binPath, args, env, options) {
|
|
|
197
359
|
}
|
|
198
360
|
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
199
361
|
child.on("error", (err) => {
|
|
200
|
-
|
|
201
|
-
finish({
|
|
202
|
-
code: missing ? NOT_FOUND_EXIT : 1,
|
|
203
|
-
stdout: "",
|
|
204
|
-
stderr: `${err.code ?? "error"}: ${err.message} (bin=${binPath})`,
|
|
205
|
-
});
|
|
362
|
+
finish(localSpawnResult(err, binPath));
|
|
206
363
|
});
|
|
207
364
|
child.stdout.on("data", (chunk) => {
|
|
208
365
|
stdout += chunk.toString();
|
|
@@ -231,12 +388,7 @@ function runToExit(binPath, args, env, options) {
|
|
|
231
388
|
}, (error, stdout, stderr) => {
|
|
232
389
|
const failure = error;
|
|
233
390
|
if (failure && typeof failure.code === "string") {
|
|
234
|
-
|
|
235
|
-
resolve({
|
|
236
|
-
code: missing ? NOT_FOUND_EXIT : 1,
|
|
237
|
-
stdout: "",
|
|
238
|
-
stderr: `${failure.code}: ${failure.message} (bin=${binPath})`,
|
|
239
|
-
});
|
|
391
|
+
resolve(localSpawnResult(failure, binPath));
|
|
240
392
|
return;
|
|
241
393
|
}
|
|
242
394
|
if (failure &&
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { ZellijError } from "./errors.js";
|
|
2
|
+
export { ListingCache } from "./zellij/cache.js";
|
|
2
3
|
export { createExec, NOT_FOUND_EXIT, type ExecFn, type ExecOptions, type ExecResult, } from "./exec.js";
|
|
3
4
|
export { createGitClient, defaultWorktreeRoot, normalizeRepoPath, parseWorktreeList, worktreeDirName, type GitClient, type GitClientOptions, type Worktree, } from "./git.js";
|
|
4
5
|
export { expandHomePath, looksLikeZswarmBinary, isZellijVersionOutput, resolveZellijBinary, resetZellijIdentityCache, sanitizeZellijEnv, validateSshDestination, validateSshMode, type ZellijExecFn, type ZellijExecResult, } from "./zellij/binary.js";
|
|
@@ -14,13 +15,17 @@ export { assertOpAllowed, assertPaneAllowed, isWriteOp, loadPolicy, type Policy,
|
|
|
14
15
|
export { buildSshRemoteCommand, createSshExec, quoteRemoteArg, shellQuote, type IpcDiscoveryState, type SshExecFn, type SshTarget, } from "./exec.js";
|
|
15
16
|
export { resolveSshTarget } from "./zellij/binary.js";
|
|
16
17
|
export { applyIpcTmpEnv, cmdQuote, concreteTmp, encodePowerShellCommand, inferRemoteShell, parseZellijServerPaths, pickIpcDirs, pickIpcTmp, socketDirFromServerPath, tmpFromServerPath, unixDiscoverRemote, windowsDiscoverRemote, windowsInteractiveRemote, wrapWithTmpEnv, } from "./zellij/ipc.js";
|
|
17
|
-
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";
|
|
18
22
|
export { parseTabList, resolveTab, type ZellijTab } from "./zellij/tabs.js";
|
|
19
23
|
export { selectTargets } from "./ops/broadcast.js";
|
|
20
24
|
export { diffScreens, cursorKey } from "./ops/tail.js";
|
|
21
25
|
export { classify, lastLine, mapPool, peerStatus, DEFAULT_STATUS_TIMEOUT_MS, STATUS_DUMP_CONCURRENCY, type PeerState, } from "./ops/status.js";
|
|
22
26
|
export { normalizeScreen, truncateDumpText, DEFAULT_DUMP_MAX_CHARS, DEFAULT_WAIT_MAX_CHARS, } from "./ops/util.js";
|
|
23
27
|
export type { RoutingContext } from "./ops/routing.js";
|
|
24
|
-
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";
|
|
25
30
|
export { normalizeKey, normalizeKeys, tokenizeCommand } from "./keys.js";
|
|
26
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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { ZellijError } from "./errors.js";
|
|
2
|
+
export { ListingCache } from "./zellij/cache.js";
|
|
2
3
|
export { createExec, NOT_FOUND_EXIT, } from "./exec.js";
|
|
3
4
|
export { createGitClient, defaultWorktreeRoot, normalizeRepoPath, parseWorktreeList, worktreeDirName, } from "./git.js";
|
|
4
5
|
export { expandHomePath, looksLikeZswarmBinary, isZellijVersionOutput, resolveZellijBinary, resetZellijIdentityCache, sanitizeZellijEnv, validateSshDestination, validateSshMode, } from "./zellij/binary.js";
|
|
@@ -14,11 +15,15 @@ export { assertOpAllowed, assertPaneAllowed, isWriteOp, loadPolicy, } from "./po
|
|
|
14
15
|
export { buildSshRemoteCommand, createSshExec, quoteRemoteArg, shellQuote, } from "./exec.js";
|
|
15
16
|
export { resolveSshTarget } from "./zellij/binary.js";
|
|
16
17
|
export { applyIpcTmpEnv, cmdQuote, concreteTmp, encodePowerShellCommand, inferRemoteShell, parseZellijServerPaths, pickIpcDirs, pickIpcTmp, socketDirFromServerPath, tmpFromServerPath, unixDiscoverRemote, windowsDiscoverRemote, windowsInteractiveRemote, wrapWithTmpEnv, } from "./zellij/ipc.js";
|
|
17
|
-
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";
|
|
18
22
|
export { parseTabList, resolveTab } from "./zellij/tabs.js";
|
|
19
23
|
export { selectTargets } from "./ops/broadcast.js";
|
|
20
24
|
export { diffScreens, cursorKey } from "./ops/tail.js";
|
|
21
25
|
export { classify, lastLine, mapPool, peerStatus, DEFAULT_STATUS_TIMEOUT_MS, STATUS_DUMP_CONCURRENCY, } from "./ops/status.js";
|
|
22
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";
|
|
23
28
|
export { normalizeKey, normalizeKeys, tokenizeCommand } from "./keys.js";
|
|
24
29
|
export { cliUsage, mcpInputSchema, parseCliArgv, MCP_TOOL_DESCRIPTION, OP_NAMES, PARAMS, TARGET_OPS, } from "./schema.js";
|
package/dist/ops/broadcast.js
CHANGED
|
@@ -70,7 +70,7 @@ export async function broadcast(client, state, args, clock, policy, env = proces
|
|
|
70
70
|
if (!body.trim())
|
|
71
71
|
throw new ZellijError("missing_body", "body required");
|
|
72
72
|
const { session } = await client.resolveSession(typeof args.session === "string" ? args.session : undefined);
|
|
73
|
-
const panes = await client.listPanes(session);
|
|
73
|
+
const panes = await client.listPanes(session, undefined, { fresh: true });
|
|
74
74
|
const labeled = withSenderLabel(args, {
|
|
75
75
|
env,
|
|
76
76
|
selfTitle: selfPaneTitle(client, panes),
|
package/dist/ops/bus.js
CHANGED
|
@@ -19,15 +19,18 @@ const COLD_RETRY_MS = 200;
|
|
|
19
19
|
* copies.
|
|
20
20
|
*/
|
|
21
21
|
const INSTALL_IN_FLIGHT_MS = 10_000;
|
|
22
|
-
/**
|
|
23
|
-
|
|
22
|
+
/** Back off failed pipes briefly, isolated to their transport/session/plugin. */
|
|
23
|
+
const busFailures = new Map();
|
|
24
24
|
/** The key that actually answered, per session, for the life of this process. */
|
|
25
25
|
const answeredWith = new Map();
|
|
26
26
|
/** Tests share a module instance; this drops what a previous case cached. */
|
|
27
27
|
export function resetBusCache() {
|
|
28
|
-
|
|
28
|
+
busFailures.clear();
|
|
29
29
|
answeredWith.clear();
|
|
30
30
|
}
|
|
31
|
+
function busScope(client, session, plan) {
|
|
32
|
+
return JSON.stringify([client.contextKey(), session, plan.url, plan.configKey]);
|
|
33
|
+
}
|
|
31
34
|
function affirmative(raw) {
|
|
32
35
|
const v = (raw ?? "").trim().toLowerCase();
|
|
33
36
|
return v === "1" || v === "true" || v === "yes" || v === "on";
|
|
@@ -62,8 +65,12 @@ export function planBus(client, state, env = process.env, session = "") {
|
|
|
62
65
|
reason: "no plugin wasm found; run pnpm build:plugin or set ZSWARM_BUS_PLUGIN",
|
|
63
66
|
};
|
|
64
67
|
}
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
const scope = busScope(client, session, base);
|
|
69
|
+
const failed = busFailures.get(scope);
|
|
70
|
+
if (failed && Date.now() < failed.until)
|
|
71
|
+
return { ...base, reason: failed.reason };
|
|
72
|
+
if (failed)
|
|
73
|
+
busFailures.delete(scope);
|
|
67
74
|
if (affirmative(env.ZSWARM_BUS)) {
|
|
68
75
|
return { ...base, enabled: true, reason: "ZSWARM_BUS=1" };
|
|
69
76
|
}
|
|
@@ -89,7 +96,7 @@ async function nudgeManifest(client, session, env, remaining) {
|
|
|
89
96
|
const listBudget = Math.min(DEFAULT_TIMEOUT_MS, remaining());
|
|
90
97
|
if (listBudget <= 0)
|
|
91
98
|
return;
|
|
92
|
-
const panes = await client.listPanes(session, listBudget);
|
|
99
|
+
const panes = await client.listPanes(session, listBudget, { fresh: true });
|
|
93
100
|
const pane = panes.find((p) => !p.isPlugin && !p.exited && p.title.trim());
|
|
94
101
|
if (!pane)
|
|
95
102
|
return;
|
|
@@ -127,7 +134,8 @@ export async function busSnapshot(client, state, session, clock, env = process.e
|
|
|
127
134
|
const url = plan.url;
|
|
128
135
|
if (!plan.enabled || !url || !pluginPath)
|
|
129
136
|
return null;
|
|
130
|
-
const
|
|
137
|
+
const scope = busScope(client, session, plan);
|
|
138
|
+
const configKey = answeredWith.get(scope) ?? plan.configKey;
|
|
131
139
|
const ask = async () => {
|
|
132
140
|
const budget = Math.min(DEFAULT_BUS_TIMEOUT_MS, remaining());
|
|
133
141
|
if (budget <= 0)
|
|
@@ -155,10 +163,18 @@ export async function busSnapshot(client, state, session, clock, env = process.e
|
|
|
155
163
|
if (remaining() <= 0 && !snapshot?.ready)
|
|
156
164
|
return null;
|
|
157
165
|
if (snapshot) {
|
|
158
|
-
answeredWith.set(
|
|
166
|
+
answeredWith.set(scope, configKey);
|
|
167
|
+
if (answeredWith.size > 256)
|
|
168
|
+
answeredWith.delete(answeredWith.keys().next().value);
|
|
169
|
+
if (snapshot.ready)
|
|
170
|
+
client.observeManifest(session, JSON.stringify([
|
|
171
|
+
scope, snapshot.paneUpdates, snapshot.tabUpdates, snapshot.panes, snapshot.tabs, snapshot.tabIds,
|
|
172
|
+
]));
|
|
159
173
|
return { snapshot, configKey };
|
|
160
174
|
}
|
|
161
|
-
|
|
175
|
+
busFailures.set(scope, { reason: "plugin did not answer; using zellij polling for 5s", until: Date.now() + 5000 });
|
|
176
|
+
if (busFailures.size > 256)
|
|
177
|
+
busFailures.delete(busFailures.keys().next().value);
|
|
162
178
|
return null;
|
|
163
179
|
}
|
|
164
180
|
/**
|
|
@@ -178,7 +194,7 @@ export async function busScreens(client, state, session, paneIds, clock, env = p
|
|
|
178
194
|
const plan = planBus(client, state, env, session);
|
|
179
195
|
if (!plan.enabled || !plan.url)
|
|
180
196
|
return null;
|
|
181
|
-
const configKey = answeredWith.get(session) ?? plan.configKey;
|
|
197
|
+
const configKey = answeredWith.get(busScope(client, session, plan)) ?? plan.configKey;
|
|
182
198
|
const reply = await client.scrollbackPlugin({
|
|
183
199
|
session,
|
|
184
200
|
url: plan.url,
|
|
@@ -209,7 +225,7 @@ export async function busWait(client, state, session, request, env = process.env
|
|
|
209
225
|
const plan = planBus(client, state, env, session);
|
|
210
226
|
if (!plan.enabled || !plan.url)
|
|
211
227
|
return null;
|
|
212
|
-
const configKey = answeredWith.get(session) ?? plan.configKey;
|
|
228
|
+
const configKey = answeredWith.get(busScope(client, session, plan)) ?? plan.configKey;
|
|
213
229
|
const reply = await client.waitPlugin({
|
|
214
230
|
session,
|
|
215
231
|
url: plan.url,
|
|
@@ -232,7 +248,7 @@ export async function busChanged(client, state, session, paneIds, env = process.
|
|
|
232
248
|
const plan = planBus(client, state, env, session);
|
|
233
249
|
if (!plan.enabled || !plan.url)
|
|
234
250
|
return null;
|
|
235
|
-
const configKey = answeredWith.get(session) ?? plan.configKey;
|
|
251
|
+
const configKey = answeredWith.get(busScope(client, session, plan)) ?? plan.configKey;
|
|
236
252
|
const reply = await client.changedPlugin({
|
|
237
253
|
session,
|
|
238
254
|
url: plan.url,
|
|
@@ -243,7 +259,8 @@ export async function busChanged(client, state, session, paneIds, env = process.
|
|
|
243
259
|
const parsed = parseChangedReply(reply.stdout);
|
|
244
260
|
if (!parsed || !parsed.ready)
|
|
245
261
|
return null;
|
|
246
|
-
|
|
262
|
+
const received = new Set([...parsed.panes.map((pane) => pane.id), ...parsed.missing]);
|
|
263
|
+
return paneIds.every((id) => received.has(id)) ? parsed : null;
|
|
247
264
|
}
|
|
248
265
|
function installedReply(session, plugin, configKey, ready, panes, extra = {}) {
|
|
249
266
|
return {
|
|
@@ -261,7 +278,7 @@ function installedReply(session, plugin, configKey, ready, panes, extra = {}) {
|
|
|
261
278
|
};
|
|
262
279
|
}
|
|
263
280
|
async function listBusPluginPanes(client, session, pluginPath) {
|
|
264
|
-
const panes = await client.listPanes(session);
|
|
281
|
+
const panes = await client.listPanes(session, undefined, { fresh: true });
|
|
265
282
|
return panes.filter((pane) => isBusPluginPane(pane, pluginPath));
|
|
266
283
|
}
|
|
267
284
|
/** Close every zswarm-bus plugin pane in this session. Best-effort per pane. */
|
|
@@ -279,8 +296,15 @@ export async function closeBusPluginPanes(client, session, pluginPath) {
|
|
|
279
296
|
}
|
|
280
297
|
return closed;
|
|
281
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
|
+
}
|
|
282
303
|
/** Report on the bus, install it, or forget it. */
|
|
283
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
|
+
}
|
|
284
308
|
const sessionArg = typeof args.session === "string" ? args.session : undefined;
|
|
285
309
|
if (isTrue(args.clear)) {
|
|
286
310
|
let session = null;
|