@zswarm/core 0.1.5 → 0.1.7
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 +47 -2
- package/dist/exec.js +249 -20
- package/dist/harness.js +12 -7
- package/dist/index.d.ts +8 -6
- package/dist/index.js +5 -4
- package/dist/ops/broadcast.js +1 -1
- package/dist/ops/bus.d.ts +6 -3
- package/dist/ops/bus.js +85 -35
- package/dist/ops/dispatch.d.ts +7 -1
- package/dist/ops/dispatch.js +235 -70
- 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/panes.js +4 -4
- 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 +245 -96
- 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/ops/worktree.js +2 -2
- package/dist/schema.d.ts +2 -0
- package/dist/schema.js +60 -9
- package/dist/zellij/args.d.ts +2 -0
- package/dist/zellij/binary.d.ts +47 -0
- package/dist/zellij/binary.js +201 -8
- package/dist/zellij/bus.d.ts +2 -0
- package/dist/zellij/bus.js +3 -2
- package/dist/zellij/cache.d.ts +24 -0
- package/dist/zellij/cache.js +90 -0
- package/dist/zellij/client.d.ts +37 -6
- package/dist/zellij/client.js +181 -42
- 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/zellij/args.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export type PaneDirection = "right" | "left" | "up" | "down";
|
|
2
2
|
export type NewPaneInput = {
|
|
3
3
|
session: string;
|
|
4
|
+
timeoutMs?: number;
|
|
4
5
|
command?: string[];
|
|
5
6
|
cwd?: string | null;
|
|
6
7
|
name?: string | null;
|
|
@@ -13,6 +14,7 @@ export type NewPaneInput = {
|
|
|
13
14
|
};
|
|
14
15
|
export type NewTabInput = {
|
|
15
16
|
session: string;
|
|
17
|
+
timeoutMs?: number;
|
|
16
18
|
command?: string[];
|
|
17
19
|
cwd?: string | null;
|
|
18
20
|
name?: string | null;
|
package/dist/zellij/binary.d.ts
CHANGED
|
@@ -6,6 +6,53 @@ export declare const DEFAULT_TIMEOUT_MS = 15000;
|
|
|
6
6
|
export { NOT_FOUND_EXIT };
|
|
7
7
|
/** Expand a leading `~/` or `~\` using USERPROFILE/HOME. */
|
|
8
8
|
export declare function expandHomePath(input: string, env?: NodeJS.ProcessEnv): string;
|
|
9
|
+
/**
|
|
10
|
+
* True when a path (or basename) is the zswarm CLI rather than Zellij.
|
|
11
|
+
* Setting ZSWARM_BIN to zswarm makes `list-sessions --short` fail with
|
|
12
|
+
* "unknown arg: --short" because zswarm re-parses argv as its own CLI.
|
|
13
|
+
*/
|
|
14
|
+
export declare function looksLikeZswarmBinary(path: string): boolean;
|
|
15
|
+
export declare function assertZellijBinaryPath(path: string): void;
|
|
16
|
+
/**
|
|
17
|
+
* ZSWARM_SSH is a destination, not a full ssh argv.
|
|
18
|
+
* Accept `user@host` or an SSH config alias; put flags in ZSWARM_SSH_OPTS.
|
|
19
|
+
*/
|
|
20
|
+
export declare function validateSshDestination(raw: string): string;
|
|
21
|
+
export declare function validateSshMode(raw: string): "ssh" | "interactive";
|
|
22
|
+
/** True when `--version` output is positively Zellij. */
|
|
23
|
+
export declare function isZellijVersionOutput(stdout: string, stderr?: string): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Cache key covering the resolved binary and SSH routing that can change the
|
|
26
|
+
* actual remote executable (opts, mode, remote bin).
|
|
27
|
+
*/
|
|
28
|
+
export declare function identityCacheKey(zellijPath: string, ssh?: {
|
|
29
|
+
host: string;
|
|
30
|
+
options: string[];
|
|
31
|
+
mode?: string;
|
|
32
|
+
remoteBin?: string;
|
|
33
|
+
/** SSH client binary (`SshTarget.ssh`). */
|
|
34
|
+
ssh?: string;
|
|
35
|
+
sshBin?: string;
|
|
36
|
+
remoteShell?: string;
|
|
37
|
+
} | null): string;
|
|
38
|
+
/**
|
|
39
|
+
* Confirm the resolved binary is Zellij. Only verified identities are cached.
|
|
40
|
+
* Transport timeouts leave the cache empty so a later call can retry.
|
|
41
|
+
* Returns false when verification is unresolved; only true is cached.
|
|
42
|
+
* In-flight probes are never shared — each caller supplies its own timeout.
|
|
43
|
+
*/
|
|
44
|
+
export declare function ensureZellijIdentity(exec: ExecFn, zellijPath: string, timeoutMs?: number, cacheKey?: string, signal?: AbortSignal): Promise<boolean>;
|
|
45
|
+
export declare function ensureZellijCapabilities(exec: ExecFn, zellijPath: string, timeoutMs?: number, cacheKey?: string, signal?: AbortSignal): Promise<boolean>;
|
|
46
|
+
/**
|
|
47
|
+
* Identity and capability are independent reads. Run them together under one
|
|
48
|
+
* remaining budget; do not share in-flight work with another caller.
|
|
49
|
+
*/
|
|
50
|
+
export declare function ensureZellijProbes(exec: ExecFn, zellijPath: string, timeoutMs?: number, cacheKey?: string, signal?: AbortSignal): Promise<{
|
|
51
|
+
identity: boolean;
|
|
52
|
+
capabilities: boolean;
|
|
53
|
+
}>;
|
|
54
|
+
/** Test helper: drop cached identity/capability probes. */
|
|
55
|
+
export declare function resetZellijIdentityCache(): void;
|
|
9
56
|
export declare function resolveZellijBinary(env?: NodeJS.ProcessEnv): string;
|
|
10
57
|
export declare function sanitizeZellijEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
11
58
|
/**
|
package/dist/zellij/binary.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { delimiter, join } from "node:path";
|
|
3
|
+
import { basename, delimiter, join } from "node:path";
|
|
4
|
+
import { ZellijError } from "../errors.js";
|
|
4
5
|
import { createExec, createSshExec, NOT_FOUND_EXIT, } from "../exec.js";
|
|
5
6
|
import { applyIpcTmpEnv } from "./ipc.js";
|
|
6
7
|
export { createSshExec };
|
|
@@ -16,15 +17,206 @@ export function expandHomePath(input, env = process.env) {
|
|
|
16
17
|
return trimmed;
|
|
17
18
|
return join(home, trimmed.slice(2));
|
|
18
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* True when a path (or basename) is the zswarm CLI rather than Zellij.
|
|
22
|
+
* Setting ZSWARM_BIN to zswarm makes `list-sessions --short` fail with
|
|
23
|
+
* "unknown arg: --short" because zswarm re-parses argv as its own CLI.
|
|
24
|
+
*/
|
|
25
|
+
export function looksLikeZswarmBinary(path) {
|
|
26
|
+
const trimmed = path.trim();
|
|
27
|
+
const base = basename(trimmed).toLowerCase();
|
|
28
|
+
// Windows paths on a Linux host still use backslashes in env values.
|
|
29
|
+
const winBase = trimmed.replace(/\\/g, "/").split("/").pop()?.toLowerCase() ?? base;
|
|
30
|
+
const name = winBase || base;
|
|
31
|
+
if (/^zswarm(\.(exe|cmd|js|mjs|cjs))?$/.test(name))
|
|
32
|
+
return true;
|
|
33
|
+
// Native binaries are never the Node CLI wrapper.
|
|
34
|
+
if (/\.(exe|dll|so|dylib)$/i.test(name))
|
|
35
|
+
return false;
|
|
36
|
+
if (name === "zellij")
|
|
37
|
+
return false;
|
|
38
|
+
// Shebang wrappers may keep another basename; sniff a short readable prefix.
|
|
39
|
+
try {
|
|
40
|
+
if (!existsSync(trimmed))
|
|
41
|
+
return false;
|
|
42
|
+
const head = readFileSync(trimmed, { encoding: "utf8" }).slice(0, 400);
|
|
43
|
+
if (head.includes("\0"))
|
|
44
|
+
return false;
|
|
45
|
+
return /@zswarm\/cli/.test(head) || /usage:\s*zswarm/i.test(head);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export function assertZellijBinaryPath(path) {
|
|
52
|
+
if (!looksLikeZswarmBinary(path))
|
|
53
|
+
return;
|
|
54
|
+
throw new ZellijError("zellij_wrong_bin", `ZSWARM_BIN/ZSWARM_PATH points at zswarm (${path}), not Zellij. Set it to the zellij binary (e.g. ~/.local/bin/zellij)`);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* ZSWARM_SSH is a destination, not a full ssh argv.
|
|
58
|
+
* Accept `user@host` or an SSH config alias; put flags in ZSWARM_SSH_OPTS.
|
|
59
|
+
*/
|
|
60
|
+
export function validateSshDestination(raw) {
|
|
61
|
+
const host = raw.trim();
|
|
62
|
+
if (!host) {
|
|
63
|
+
throw new ZellijError("bad_ssh", "ZSWARM_SSH is empty");
|
|
64
|
+
}
|
|
65
|
+
if (/^ssh(\s|$)/i.test(host)) {
|
|
66
|
+
throw new ZellijError("bad_ssh", `ZSWARM_SSH should be user@host or an SSH alias, not a full ssh command. Put flags in ZSWARM_SSH_OPTS. Got: ${JSON.stringify(host)}`);
|
|
67
|
+
}
|
|
68
|
+
// Leading dashes are SSH options (`-V`, `-F…`, `-o…`), not destinations.
|
|
69
|
+
if (host.startsWith("-")) {
|
|
70
|
+
throw new ZellijError("bad_ssh", `ZSWARM_SSH looks like an SSH option (${JSON.stringify(host)}); put flags in ZSWARM_SSH_OPTS and set ZSWARM_SSH to user@host or an alias`);
|
|
71
|
+
}
|
|
72
|
+
if (/\s/.test(host)) {
|
|
73
|
+
throw new ZellijError("bad_ssh", `ZSWARM_SSH must be a single destination (user@host or alias); put options in ZSWARM_SSH_OPTS. Got: ${JSON.stringify(host)}`);
|
|
74
|
+
}
|
|
75
|
+
if (/[;|&$<>()]/.test(host)) {
|
|
76
|
+
throw new ZellijError("bad_ssh", `ZSWARM_SSH contains shell metacharacters; expected user@host or an SSH alias. Got: ${JSON.stringify(host)}`);
|
|
77
|
+
}
|
|
78
|
+
return host;
|
|
79
|
+
}
|
|
80
|
+
export function validateSshMode(raw) {
|
|
81
|
+
const mode = raw.trim().toLowerCase();
|
|
82
|
+
if (!mode || mode === "ssh")
|
|
83
|
+
return "ssh";
|
|
84
|
+
if (mode === "interactive")
|
|
85
|
+
return "interactive";
|
|
86
|
+
throw new ZellijError("bad_ssh_mode", `ZSWARM_SSH_MODE must be "interactive" or "ssh" (or unset); got ${JSON.stringify(raw.trim())}`);
|
|
87
|
+
}
|
|
88
|
+
/** Cache of verified `zellij --version` probes keyed by target identity. */
|
|
89
|
+
const identityCache = new Set();
|
|
90
|
+
/** True when `--version` output is positively Zellij. */
|
|
91
|
+
export function isZellijVersionOutput(stdout, stderr = "") {
|
|
92
|
+
const text = `${stdout}\n${stderr}`;
|
|
93
|
+
return /\bzellij\s+\d+\.\d+/i.test(text) || /^\s*zellij\b/im.test(stdout);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Cache key covering the resolved binary and SSH routing that can change the
|
|
97
|
+
* actual remote executable (opts, mode, remote bin).
|
|
98
|
+
*/
|
|
99
|
+
export function identityCacheKey(zellijPath, ssh) {
|
|
100
|
+
if (!ssh)
|
|
101
|
+
return JSON.stringify([zellijPath]);
|
|
102
|
+
return JSON.stringify([
|
|
103
|
+
zellijPath,
|
|
104
|
+
ssh.host,
|
|
105
|
+
ssh.remoteBin ?? "",
|
|
106
|
+
ssh.mode ?? "ssh",
|
|
107
|
+
ssh.options,
|
|
108
|
+
ssh.sshBin ?? ssh.ssh ?? "",
|
|
109
|
+
ssh.remoteShell ?? "",
|
|
110
|
+
]);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Confirm the resolved binary is Zellij. Only verified identities are cached.
|
|
114
|
+
* Transport timeouts leave the cache empty so a later call can retry.
|
|
115
|
+
* Returns false when verification is unresolved; only true is cached.
|
|
116
|
+
* In-flight probes are never shared — each caller supplies its own timeout.
|
|
117
|
+
*/
|
|
118
|
+
export async function ensureZellijIdentity(exec, zellijPath, timeoutMs = 3_000, cacheKey = zellijPath, signal) {
|
|
119
|
+
if (identityCache.has(cacheKey))
|
|
120
|
+
return true;
|
|
121
|
+
if (signal?.aborted)
|
|
122
|
+
return false;
|
|
123
|
+
assertZellijBinaryPath(zellijPath);
|
|
124
|
+
const result = await exec(["--version"], {
|
|
125
|
+
timeoutMs: Math.min(timeoutMs, 5_000),
|
|
126
|
+
signal,
|
|
127
|
+
});
|
|
128
|
+
const text = `${result.stdout}\n${result.stderr}`;
|
|
129
|
+
if (/usage:\s*zswarm/i.test(text) || /unknown arg:/i.test(text)) {
|
|
130
|
+
throw new ZellijError("zellij_wrong_bin", `resolved binary is zswarm, not Zellij (${zellijPath}). Set ZSWARM_BIN to the zellij executable`);
|
|
131
|
+
}
|
|
132
|
+
if (result.code === NOT_FOUND_EXIT) {
|
|
133
|
+
throw new ZellijError("zellij_missing", `zellij binary not found (${zellijPath}); install Zellij ≥ 0.42, add it to PATH, or set ZSWARM_BIN / ZSWARM_PATH`);
|
|
134
|
+
}
|
|
135
|
+
if (result.code === 0) {
|
|
136
|
+
if (signal?.aborted)
|
|
137
|
+
return false;
|
|
138
|
+
if (isZellijVersionOutput(result.stdout, result.stderr)) {
|
|
139
|
+
identityCache.add(cacheKey);
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
throw new ZellijError("zellij_wrong_bin", `resolved binary is not Zellij (${zellijPath}); --version returned: ${result.stdout.trim() || result.stderr.trim() || "empty"}`);
|
|
143
|
+
}
|
|
144
|
+
// Timeout / transport failure: leave unresolved so a later call retries.
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Probe that the target understands the session-list flags we rely on.
|
|
149
|
+
* Cached with the same key as identity once verified.
|
|
150
|
+
* Returns false when the transport cannot verify support.
|
|
151
|
+
*/
|
|
152
|
+
const capabilityCache = new Set();
|
|
153
|
+
export async function ensureZellijCapabilities(exec, zellijPath, timeoutMs = 3_000, cacheKey = zellijPath, signal) {
|
|
154
|
+
if (capabilityCache.has(cacheKey))
|
|
155
|
+
return true;
|
|
156
|
+
if (signal?.aborted)
|
|
157
|
+
return false;
|
|
158
|
+
const result = await exec(["list-sessions", "--help"], {
|
|
159
|
+
timeoutMs: Math.min(timeoutMs, 5_000),
|
|
160
|
+
signal,
|
|
161
|
+
});
|
|
162
|
+
const text = `${result.stdout}\n${result.stderr}`;
|
|
163
|
+
if (/usage:\s*zswarm/i.test(text)) {
|
|
164
|
+
throw new ZellijError("zellij_wrong_bin", `resolved binary is zswarm, not Zellij (${zellijPath})`);
|
|
165
|
+
}
|
|
166
|
+
if (result.code === NOT_FOUND_EXIT) {
|
|
167
|
+
throw new ZellijError("zellij_missing", `zellij binary not found (${zellijPath})`);
|
|
168
|
+
}
|
|
169
|
+
// Require the flags this client always passes.
|
|
170
|
+
if (result.code === 0 &&
|
|
171
|
+
/--no-formatting/i.test(text) &&
|
|
172
|
+
/list-sessions/i.test(text)) {
|
|
173
|
+
if (signal?.aborted)
|
|
174
|
+
return false;
|
|
175
|
+
capabilityCache.add(cacheKey);
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
if (result.code === 0) {
|
|
179
|
+
throw new ZellijError("zellij_incompatible", `Zellij at ${zellijPath} does not advertise list-sessions --no-formatting; upgrade Zellij (≥ 0.42) or zswarm`);
|
|
180
|
+
}
|
|
181
|
+
// Soft: leave unresolved on transport failure.
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Identity and capability are independent reads. Run them together under one
|
|
186
|
+
* remaining budget; do not share in-flight work with another caller.
|
|
187
|
+
*/
|
|
188
|
+
export async function ensureZellijProbes(exec, zellijPath, timeoutMs = 3_000, cacheKey = zellijPath, signal) {
|
|
189
|
+
if (signal?.aborted)
|
|
190
|
+
return { identity: false, capabilities: false };
|
|
191
|
+
const probeTimeout = Math.min(timeoutMs, 5_000);
|
|
192
|
+
// Settle both so a wrong-bin throw does not leave the other SSH probe running.
|
|
193
|
+
const settled = await Promise.allSettled([
|
|
194
|
+
ensureZellijIdentity(exec, zellijPath, probeTimeout, cacheKey, signal),
|
|
195
|
+
ensureZellijCapabilities(exec, zellijPath, probeTimeout, cacheKey, signal),
|
|
196
|
+
]);
|
|
197
|
+
if (settled[0].status === "rejected")
|
|
198
|
+
throw settled[0].reason;
|
|
199
|
+
if (settled[1].status === "rejected")
|
|
200
|
+
throw settled[1].reason;
|
|
201
|
+
return { identity: settled[0].value, capabilities: settled[1].value };
|
|
202
|
+
}
|
|
203
|
+
/** Test helper: drop cached identity/capability probes. */
|
|
204
|
+
export function resetZellijIdentityCache() {
|
|
205
|
+
identityCache.clear();
|
|
206
|
+
capabilityCache.clear();
|
|
207
|
+
}
|
|
19
208
|
export function resolveZellijBinary(env = process.env) {
|
|
20
209
|
const fromEnv = expandHomePath((env.ZSWARM_BIN ?? env.ZSWARM_PATH ?? env.ZELLIJ_BIN ?? "")
|
|
21
210
|
.trim()
|
|
22
211
|
.replace(/^['"]|['"]$/g, ""), env);
|
|
23
212
|
if (fromEnv && existsSync(fromEnv)) {
|
|
213
|
+
assertZellijBinaryPath(fromEnv);
|
|
24
214
|
if (/\.cmd$/i.test(fromEnv)) {
|
|
25
215
|
const exe = fromEnv.replace(/\.cmd$/i, ".exe");
|
|
26
|
-
if (existsSync(exe))
|
|
216
|
+
if (existsSync(exe)) {
|
|
217
|
+
assertZellijBinaryPath(exe);
|
|
27
218
|
return exe;
|
|
219
|
+
}
|
|
28
220
|
const wingetExe = join(env.LOCALAPPDATA ||
|
|
29
221
|
join(env.USERPROFILE || env.HOME || homedir(), "AppData", "Local"), "Zellij", "zellij.exe");
|
|
30
222
|
if (existsSync(wingetExe))
|
|
@@ -112,17 +304,18 @@ export function parseSshOpts(raw) {
|
|
|
112
304
|
return out;
|
|
113
305
|
}
|
|
114
306
|
export function resolveSshTarget(env = process.env) {
|
|
115
|
-
const
|
|
116
|
-
if (!
|
|
307
|
+
const raw = env.ZSWARM_SSH?.trim();
|
|
308
|
+
if (!raw)
|
|
117
309
|
return null;
|
|
310
|
+
const host = validateSshDestination(raw);
|
|
118
311
|
const options = parseSshOpts(env.ZSWARM_SSH_OPTS ?? "");
|
|
119
312
|
if (!options.some((o) => o.startsWith("BatchMode"))) {
|
|
120
313
|
options.unshift("-o", "BatchMode=yes");
|
|
121
314
|
}
|
|
122
315
|
const shellRaw = (env.ZSWARM_REMOTE_SHELL ?? "").trim().toLowerCase();
|
|
123
|
-
const
|
|
316
|
+
const mode = validateSshMode(env.ZSWARM_SSH_MODE ?? "");
|
|
124
317
|
const tmpRaw = env.ZSWARM_TMP?.trim();
|
|
125
|
-
const interactive =
|
|
318
|
+
const interactive = mode === "interactive";
|
|
126
319
|
return {
|
|
127
320
|
ssh: env.ZSWARM_SSH_BIN?.trim() || "ssh",
|
|
128
321
|
host,
|
|
@@ -130,7 +323,7 @@ export function resolveSshTarget(env = process.env) {
|
|
|
130
323
|
options,
|
|
131
324
|
// Interactive tasks do not inherit the desktop TEMP; discover it unless set.
|
|
132
325
|
tmp: tmpRaw || (interactive ? "auto" : undefined),
|
|
133
|
-
mode
|
|
326
|
+
mode,
|
|
134
327
|
remoteShell: shellRaw === "cmd" || shellRaw === "sh" ? shellRaw : undefined,
|
|
135
328
|
};
|
|
136
329
|
}
|
package/dist/zellij/bus.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ export type BusSnapshot = {
|
|
|
23
23
|
paneUpdates: number;
|
|
24
24
|
tabUpdates: number;
|
|
25
25
|
tabs: string[];
|
|
26
|
+
/** Stable IDs by tab position; absent on older running plugin instances. */
|
|
27
|
+
tabIds?: (number | null)[];
|
|
26
28
|
panes: BusPane[];
|
|
27
29
|
};
|
|
28
30
|
export type BusMarker = {
|
package/dist/zellij/bus.js
CHANGED
|
@@ -74,7 +74,7 @@ export function parseChangedReply(stdout) {
|
|
|
74
74
|
if (!row || typeof row !== "object")
|
|
75
75
|
continue;
|
|
76
76
|
const p = row;
|
|
77
|
-
if (typeof p.id !== "string")
|
|
77
|
+
if (typeof p.id !== "string" || typeof p.changed !== "boolean" || typeof p.first !== "boolean" || !Array.isArray(p.viewport))
|
|
78
78
|
continue;
|
|
79
79
|
panes.push({
|
|
80
80
|
id: p.id,
|
|
@@ -186,6 +186,7 @@ function toSnapshot(value) {
|
|
|
186
186
|
tabs: Array.isArray(value.tabs)
|
|
187
187
|
? value.tabs.filter((t) => typeof t === "string")
|
|
188
188
|
: [],
|
|
189
|
+
...(Array.isArray(value.tabIds) ? { tabIds: value.tabIds.map((id) => typeof id === "number" && Number.isInteger(id) && id >= 0 ? id : null) } : {}),
|
|
189
190
|
panes,
|
|
190
191
|
};
|
|
191
192
|
}
|
|
@@ -290,7 +291,7 @@ export function busToPanes(snapshot) {
|
|
|
290
291
|
command: pane.command,
|
|
291
292
|
cwd: null,
|
|
292
293
|
tabName: snapshot.tabs[pane.tab] ?? null,
|
|
293
|
-
tabId: pane.tab,
|
|
294
|
+
tabId: snapshot.tabIds?.[pane.tab] ?? null,
|
|
294
295
|
focused: pane.focused,
|
|
295
296
|
exited: pane.exited,
|
|
296
297
|
floating: false,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Short-lived, completed observations only. Never shares a caller's promise or budget. */
|
|
2
|
+
export declare class ListingCache {
|
|
3
|
+
private readonly limit;
|
|
4
|
+
private entries;
|
|
5
|
+
private scopes;
|
|
6
|
+
constructor(limit?: number);
|
|
7
|
+
begin(scope: string): {
|
|
8
|
+
state: {
|
|
9
|
+
version: number;
|
|
10
|
+
revision?: string;
|
|
11
|
+
};
|
|
12
|
+
version: number;
|
|
13
|
+
};
|
|
14
|
+
get<T>(key: string, now: number): T | undefined;
|
|
15
|
+
put<T>(key: string, scope: string, ticket: ReturnType<ListingCache["begin"]>, value: T, ttl: number, now: number): void;
|
|
16
|
+
invalidate(scope: string): void;
|
|
17
|
+
observe(scope: string, revision: string): boolean;
|
|
18
|
+
clear(): void;
|
|
19
|
+
}
|
|
20
|
+
export declare const sharedListings: ListingCache;
|
|
21
|
+
export declare function executorIdentity(exec?: object): number | null;
|
|
22
|
+
/** Environment fields that select local sockets or an SSH identity/config. */
|
|
23
|
+
export declare function routingEnvironment(env: NodeJS.ProcessEnv): Record<string, string | null>;
|
|
24
|
+
export declare function listingTtl(env: NodeJS.ProcessEnv): number;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/** Short-lived, completed observations only. Never shares a caller's promise or budget. */
|
|
2
|
+
export class ListingCache {
|
|
3
|
+
limit;
|
|
4
|
+
entries = new Map();
|
|
5
|
+
scopes = new Map();
|
|
6
|
+
constructor(limit = 512) {
|
|
7
|
+
this.limit = limit;
|
|
8
|
+
}
|
|
9
|
+
begin(scope) {
|
|
10
|
+
let state = this.scopes.get(scope);
|
|
11
|
+
if (!state) {
|
|
12
|
+
state = { version: 0 };
|
|
13
|
+
this.scopes.set(scope, state);
|
|
14
|
+
if (this.scopes.size > this.limit) {
|
|
15
|
+
const oldest = this.scopes.keys().next().value;
|
|
16
|
+
this.scopes.delete(oldest);
|
|
17
|
+
for (const [key, entry] of this.entries)
|
|
18
|
+
if (entry.scope === oldest)
|
|
19
|
+
this.entries.delete(key);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return { state, version: state.version };
|
|
23
|
+
}
|
|
24
|
+
get(key, now) {
|
|
25
|
+
const entry = this.entries.get(key);
|
|
26
|
+
if (!entry)
|
|
27
|
+
return undefined;
|
|
28
|
+
if (now >= entry.expiresAt) {
|
|
29
|
+
this.entries.delete(key);
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
return structuredClone(entry.value);
|
|
33
|
+
}
|
|
34
|
+
put(key, scope, ticket, value, ttl, now) {
|
|
35
|
+
// A mutation/event during this read must not resurrect an older manifest.
|
|
36
|
+
if (ttl <= 0 || this.scopes.get(scope) !== ticket.state || ticket.state.version !== ticket.version)
|
|
37
|
+
return;
|
|
38
|
+
this.entries.delete(key);
|
|
39
|
+
this.entries.set(key, { scope, value: structuredClone(value), expiresAt: now + ttl });
|
|
40
|
+
while (this.entries.size > this.limit)
|
|
41
|
+
this.entries.delete(this.entries.keys().next().value);
|
|
42
|
+
}
|
|
43
|
+
invalidate(scope) {
|
|
44
|
+
this.begin(scope).state.version++;
|
|
45
|
+
for (const [key, entry] of this.entries)
|
|
46
|
+
if (entry.scope === scope)
|
|
47
|
+
this.entries.delete(key);
|
|
48
|
+
}
|
|
49
|
+
observe(scope, revision) {
|
|
50
|
+
const { state } = this.begin(scope);
|
|
51
|
+
if (state.revision === revision)
|
|
52
|
+
return false;
|
|
53
|
+
this.invalidate(scope);
|
|
54
|
+
state.revision = revision;
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
clear() {
|
|
58
|
+
this.entries.clear();
|
|
59
|
+
this.scopes.clear();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export const sharedListings = new ListingCache();
|
|
63
|
+
const executors = new WeakMap();
|
|
64
|
+
let nextExecutor = 0;
|
|
65
|
+
export function executorIdentity(exec) {
|
|
66
|
+
if (!exec)
|
|
67
|
+
return null;
|
|
68
|
+
let id = executors.get(exec);
|
|
69
|
+
if (id === undefined) {
|
|
70
|
+
id = ++nextExecutor;
|
|
71
|
+
executors.set(exec, id);
|
|
72
|
+
}
|
|
73
|
+
return id;
|
|
74
|
+
}
|
|
75
|
+
/** Environment fields that select local sockets or an SSH identity/config. */
|
|
76
|
+
export function routingEnvironment(env) {
|
|
77
|
+
return Object.fromEntries([
|
|
78
|
+
"ZSWARM_TMP", "ZELLIJ_SOCKET_DIR", "TMPDIR", "TEMP", "TMP", "XDG_RUNTIME_DIR",
|
|
79
|
+
"HOME", "USERPROFILE", "USER", "USERNAME", "LOGNAME", "USERDOMAIN", "SSH_AUTH_SOCK",
|
|
80
|
+
"PATH", "Path", "PATHEXT", "XDG_CONFIG_HOME", "ZELLIJ_CONFIG_DIR",
|
|
81
|
+
"ZELLIJ_SESSION_NAME", "ZSWARM_SESSION",
|
|
82
|
+
].map((key) => [key, env[key] ?? null]));
|
|
83
|
+
}
|
|
84
|
+
export function listingTtl(env) {
|
|
85
|
+
const value = env.ZSWARM_CACHE_TTL_MS;
|
|
86
|
+
if (value === undefined || value.trim() === "")
|
|
87
|
+
return 500;
|
|
88
|
+
const ttl = Number(value);
|
|
89
|
+
return Number.isFinite(ttl) ? Math.max(0, Math.min(5000, ttl)) : 500;
|
|
90
|
+
}
|
package/dist/zellij/client.d.ts
CHANGED
|
@@ -1,24 +1,50 @@
|
|
|
1
|
+
import { ListingCache } from "./cache.js";
|
|
1
2
|
import { buildClosePaneArgs, buildDumpArgs, buildListPanesArgs, buildNewPaneArgs, buildNewTabArgs, buildPasteArgs, buildSendEnterArgs, buildSendKeysArgs, buildWriteCharsArgs, type WaitRequest, type LaunchPluginInput, type NewPaneInput, type NewTabInput, type PipeInput } from "./args.js";
|
|
2
3
|
import { resolveTab, type ZellijTab } from "./tabs.js";
|
|
4
|
+
import { type IpcDiscoveryState } from "../exec.js";
|
|
3
5
|
import { type ZellijExecFn } from "./binary.js";
|
|
4
6
|
import { normalizePaneId, resolvePane, type ZellijPane } from "./panes.js";
|
|
5
|
-
import { type ZellijSessionResolve } from "./session.js";
|
|
7
|
+
import { type ZellijSession, type ZellijSessionResolve } from "./session.js";
|
|
6
8
|
export type ZellijClientOptions = {
|
|
7
9
|
exec?: ZellijExecFn;
|
|
8
10
|
zellijPath?: string;
|
|
9
11
|
timeoutMs?: number;
|
|
10
12
|
env?: NodeJS.ProcessEnv;
|
|
11
13
|
signal?: AbortSignal;
|
|
14
|
+
/** Skip the one-time `zellij --version` identity probe (tests). */
|
|
15
|
+
skipIdentityProbe?: boolean;
|
|
16
|
+
/** Completed listing cache shared by long-lived MCP/serve calls; false bypasses reads. */
|
|
17
|
+
cache?: ListingCache | false;
|
|
18
|
+
/** Clock for cache TTLs; process deadlines continue using wall time. */
|
|
19
|
+
now?: () => number;
|
|
12
20
|
};
|
|
13
|
-
|
|
21
|
+
export type ZellijTransport = {
|
|
22
|
+
kind: "local" | "ssh";
|
|
23
|
+
mode: "local" | "ssh" | "interactive";
|
|
24
|
+
host?: string;
|
|
25
|
+
remoteBin?: string;
|
|
26
|
+
/** Configured or resolved IPC temp (`auto` until discovery succeeds). */
|
|
27
|
+
tmp?: string;
|
|
28
|
+
/** Outcome of `ZSWARM_TMP=auto` discovery when SSH routing is active. */
|
|
29
|
+
ipc?: IpcDiscoveryState;
|
|
30
|
+
};
|
|
31
|
+
/** Zellij wrapper with bounded caches of completed discovery observations. */
|
|
14
32
|
export declare function createZellijClient(options?: ZellijClientOptions): {
|
|
15
33
|
zellijPath: string;
|
|
16
34
|
selfPaneId: string | null;
|
|
35
|
+
contextKey: () => string;
|
|
36
|
+
invalidateListings: (session: string) => void;
|
|
37
|
+
observeManifest: (session: string, revision: string) => void;
|
|
38
|
+
readonly transport: ZellijTransport;
|
|
17
39
|
/** A `file:` plugin url only names a path on the machine running Zellij. */
|
|
18
40
|
remote: boolean;
|
|
19
|
-
listSessions: (
|
|
20
|
-
|
|
21
|
-
|
|
41
|
+
listSessions: (callTimeoutMs?: number, opts?: {
|
|
42
|
+
fresh?: boolean;
|
|
43
|
+
}) => Promise<ZellijSession[]>;
|
|
44
|
+
resolveSession: (explicit?: string | null, callTimeoutMs?: number) => Promise<ZellijSessionResolve>;
|
|
45
|
+
listPanes: (session: string, callTimeoutMs?: number, opts?: {
|
|
46
|
+
fresh?: boolean;
|
|
47
|
+
}) => Promise<ZellijPane[]>;
|
|
22
48
|
resolvePane: typeof resolvePane;
|
|
23
49
|
injectPane: (input: {
|
|
24
50
|
session: string;
|
|
@@ -32,6 +58,7 @@ export declare function createZellijClient(options?: ZellijClientOptions): {
|
|
|
32
58
|
session: string;
|
|
33
59
|
paneId: string;
|
|
34
60
|
full?: boolean;
|
|
61
|
+
timeoutMs?: number;
|
|
35
62
|
}) => Promise<{
|
|
36
63
|
paneId: string;
|
|
37
64
|
session: string;
|
|
@@ -75,6 +102,7 @@ export declare function createZellijClient(options?: ZellijClientOptions): {
|
|
|
75
102
|
session: string;
|
|
76
103
|
paneId: string;
|
|
77
104
|
name: string;
|
|
105
|
+
timeoutMs?: number;
|
|
78
106
|
}) => Promise<{
|
|
79
107
|
paneId: string;
|
|
80
108
|
session: string;
|
|
@@ -96,7 +124,9 @@ export declare function createZellijClient(options?: ZellijClientOptions): {
|
|
|
96
124
|
paneId: string;
|
|
97
125
|
session: string;
|
|
98
126
|
}>;
|
|
99
|
-
listTabs: (session: string
|
|
127
|
+
listTabs: (session: string, callTimeoutMs?: number, opts?: {
|
|
128
|
+
fresh?: boolean;
|
|
129
|
+
}) => Promise<ZellijTab[]>;
|
|
100
130
|
dumpLayout: (session: string) => Promise<string>;
|
|
101
131
|
stackPanes: (input: {
|
|
102
132
|
session: string;
|
|
@@ -138,6 +168,7 @@ export declare function createZellijClient(options?: ZellijClientOptions): {
|
|
|
138
168
|
url: string;
|
|
139
169
|
configKey: string;
|
|
140
170
|
panes: string[];
|
|
171
|
+
timeoutMs?: number;
|
|
141
172
|
}) => Promise<{
|
|
142
173
|
code: number;
|
|
143
174
|
stdout: string;
|