@zswarm/core 0.1.6 → 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/dist/exec.d.ts +36 -2
- package/dist/exec.js +186 -34
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/ops/broadcast.js +1 -1
- package/dist/ops/bus.js +31 -14
- package/dist/ops/dispatch.js +57 -20
- package/dist/ops/panes.js +4 -4
- package/dist/ops/routing.js +1 -1
- package/dist/ops/spawn.js +4 -4
- package/dist/ops/status.js +18 -9
- package/dist/ops/worktree.js +2 -2
- package/dist/schema.js +15 -3
- package/dist/zellij/binary.d.ts +15 -2
- package/dist/zellij/binary.js +38 -6
- 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 +105 -31
- package/package.json +1 -1
package/dist/ops/panes.js
CHANGED
|
@@ -16,7 +16,7 @@ export async function renameTarget(client, args, policy) {
|
|
|
16
16
|
const sess = await session(client, args);
|
|
17
17
|
const tabKey = optionalString(args.tab);
|
|
18
18
|
if (tabKey) {
|
|
19
|
-
const tab = client.resolveTab(await client.listTabs(sess), tabKey);
|
|
19
|
+
const tab = client.resolveTab(await client.listTabs(sess, undefined, { fresh: true }), tabKey);
|
|
20
20
|
const renamed = await client.renameTab({
|
|
21
21
|
session: sess,
|
|
22
22
|
tabId: tab.id,
|
|
@@ -30,7 +30,7 @@ export async function renameTarget(client, args, policy) {
|
|
|
30
30
|
const to = optionalString(args.to);
|
|
31
31
|
if (!to)
|
|
32
32
|
throw new ZellijError("missing_peer", "to (or tab) required");
|
|
33
|
-
const pane = client.resolvePane(await client.listPanes(sess), to);
|
|
33
|
+
const pane = client.resolvePane(await client.listPanes(sess, undefined, { fresh: true }), to);
|
|
34
34
|
if (policy)
|
|
35
35
|
assertPaneAllowed(policy, pane, "rename");
|
|
36
36
|
await client.renamePane({ session: sess, paneId: pane.id, name });
|
|
@@ -44,7 +44,7 @@ export async function focusTarget(client, args, policy) {
|
|
|
44
44
|
if (!to)
|
|
45
45
|
throw new ZellijError("missing_peer", "to required");
|
|
46
46
|
const sess = await session(client, args);
|
|
47
|
-
const pane = client.resolvePane(await client.listPanes(sess), to);
|
|
47
|
+
const pane = client.resolvePane(await client.listPanes(sess, undefined, { fresh: true }), to);
|
|
48
48
|
if (policy)
|
|
49
49
|
assertPaneAllowed(policy, pane, "focus");
|
|
50
50
|
// Zellij exits non-zero when asked to focus the already-focused pane.
|
|
@@ -104,7 +104,7 @@ export async function stackTargets(client, args, policy) {
|
|
|
104
104
|
if (!list)
|
|
105
105
|
throw new ZellijError("missing_peer", "to (comma list) required");
|
|
106
106
|
const sess = await session(client, args);
|
|
107
|
-
const panes = await client.listPanes(sess);
|
|
107
|
+
const panes = await client.listPanes(sess, undefined, { fresh: true });
|
|
108
108
|
const ids = list
|
|
109
109
|
.split(",")
|
|
110
110
|
.map((k) => k.trim())
|
package/dist/ops/routing.js
CHANGED
|
@@ -13,7 +13,7 @@ export function invocationContext(args, env, injected) {
|
|
|
13
13
|
session: selected?.session ?? null,
|
|
14
14
|
origin: {
|
|
15
15
|
transport: injected ? "injected" : isTrue(args.local) ? "--local"
|
|
16
|
-
: optionalString(args.ssh) ? "--ssh" : serve ? "ZSWARM_SERVE"
|
|
16
|
+
: optionalString(args.ssh) ? "--ssh" : optionalString(args.serveAddress) ? "--serve" : serve ? "ZSWARM_SERVE"
|
|
17
17
|
: ssh ? "ZSWARM_SSH" : "default",
|
|
18
18
|
session: selected?.source ?? "unresolved",
|
|
19
19
|
},
|
package/dist/ops/spawn.js
CHANGED
|
@@ -27,7 +27,7 @@ export async function spawnPane(client, args, git, clock = { now: Date.now, slee
|
|
|
27
27
|
: null;
|
|
28
28
|
const cwd = worktree ? worktree.path : optionalString(args.cwd);
|
|
29
29
|
const name = optionalString(args.name) ?? worktree?.branch ?? null;
|
|
30
|
-
const beforePanes = await client.listPanes(session, budget.require());
|
|
30
|
+
const beforePanes = await client.listPanes(session, budget.require(), { fresh: true });
|
|
31
31
|
const before = new Set(beforePanes.map((p) => p.id));
|
|
32
32
|
let paneId = null;
|
|
33
33
|
let tabId = null;
|
|
@@ -49,7 +49,7 @@ export async function spawnPane(client, args, git, clock = { now: Date.now, slee
|
|
|
49
49
|
tabId = args.tabId;
|
|
50
50
|
}
|
|
51
51
|
else if (tabName) {
|
|
52
|
-
tabId = client.resolveTab(await client.listTabs(session, budget.require()), tabName).id;
|
|
52
|
+
tabId = client.resolveTab(await client.listTabs(session, budget.require(), { fresh: true }), tabName).id;
|
|
53
53
|
}
|
|
54
54
|
else {
|
|
55
55
|
// Address a tab explicitly even when no viewer is attached.
|
|
@@ -58,7 +58,7 @@ export async function spawnPane(client, args, git, clock = { now: Date.now, slee
|
|
|
58
58
|
tabId = focused?.tabId ?? first?.tabId ?? null;
|
|
59
59
|
tabSource = focused ? "focused-pane" : "first-tab";
|
|
60
60
|
if (tabId === null) {
|
|
61
|
-
const tabs = await client.listTabs(session, budget.require());
|
|
61
|
+
const tabs = await client.listTabs(session, budget.require(), { fresh: true });
|
|
62
62
|
tabId = [...tabs].sort((a, b) => a.position - b.position)[0]?.id ?? null;
|
|
63
63
|
}
|
|
64
64
|
if (tabId === null)
|
|
@@ -83,7 +83,7 @@ export async function spawnPane(client, args, git, clock = { now: Date.now, slee
|
|
|
83
83
|
firstRead = false;
|
|
84
84
|
try {
|
|
85
85
|
const left = observeMs === 0 ? budget.require() : Math.min(budget.require(), Math.max(1, observedUntil - clock.now()));
|
|
86
|
-
const panes = await client.listPanes(session, left);
|
|
86
|
+
const panes = await client.listPanes(session, left, { fresh: true });
|
|
87
87
|
throwIfAborted(signal);
|
|
88
88
|
if (paneId) {
|
|
89
89
|
pane = panes.find((p) => p.id === paneId && !p.isPlugin) ?? null;
|
package/dist/ops/status.js
CHANGED
|
@@ -163,9 +163,20 @@ export async function peerStatus(client, args, clock, supplied, opts = {}) {
|
|
|
163
163
|
}
|
|
164
164
|
const sampleMs = Math.max(50, requested);
|
|
165
165
|
const live = targets.filter((pane) => !pane.exited);
|
|
166
|
-
|
|
166
|
+
// Explicit sampling retains its interval semantics. Ordinary status prefers
|
|
167
|
+
// one bus observation, including a single-pane crew, with no sample sleep.
|
|
168
|
+
const preferChanges = isTrue(args.sinceLast) || (args.sinceLast === undefined && args.sampleMs === undefined);
|
|
169
|
+
if (preferChanges && supplied?.readChanged) {
|
|
167
170
|
const left = remaining();
|
|
168
|
-
|
|
171
|
+
let changed = null;
|
|
172
|
+
try {
|
|
173
|
+
changed = live.length === 0 ? new Map() : left > 0 ? await supplied.readChanged(live.map((p) => p.id), left) : null;
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
if (isCancelledError(err) || signal?.aborted)
|
|
177
|
+
throw new ZellijError("cancelled", "operation cancelled");
|
|
178
|
+
// A missing/older bus falls back to bounded screen samples below.
|
|
179
|
+
}
|
|
169
180
|
throwIfAborted(signal);
|
|
170
181
|
if (changed) {
|
|
171
182
|
const peers = targets
|
|
@@ -174,13 +185,9 @@ export async function peerStatus(client, args, clock, supplied, opts = {}) {
|
|
|
174
185
|
return peerEntry(pane, "exited", "", verbose);
|
|
175
186
|
}
|
|
176
187
|
const row = changed.get(pane.id);
|
|
177
|
-
const state = !row
|
|
178
|
-
? "
|
|
179
|
-
|
|
180
|
-
? "busy"
|
|
181
|
-
: promptHolds(row?.screen ?? "", resolveHarness(pane))
|
|
182
|
-
? "waiting"
|
|
183
|
-
: "idle";
|
|
188
|
+
const state = !row ? "unknown"
|
|
189
|
+
: promptHolds(row.screen, resolveHarness(pane)) ? "waiting"
|
|
190
|
+
: row.first ? "unknown" : row.changed ? "busy" : "idle";
|
|
184
191
|
return peerEntry(pane, state, row?.screen ?? "", verbose, {
|
|
185
192
|
...(row?.first ? { first: true } : {}),
|
|
186
193
|
});
|
|
@@ -193,6 +200,7 @@ export async function peerStatus(client, args, clock, supplied, opts = {}) {
|
|
|
193
200
|
source,
|
|
194
201
|
sampled: false,
|
|
195
202
|
sinceLast: true,
|
|
203
|
+
observation: "bus-changes",
|
|
196
204
|
peers,
|
|
197
205
|
tabs: statusTabs(peers),
|
|
198
206
|
free: peers.filter((p) => p.state === "idle").map((p) => p.id),
|
|
@@ -321,6 +329,7 @@ export async function peerStatus(client, args, clock, supplied, opts = {}) {
|
|
|
321
329
|
session,
|
|
322
330
|
source,
|
|
323
331
|
sampled: true,
|
|
332
|
+
observation: "samples",
|
|
324
333
|
sampleMs,
|
|
325
334
|
peers,
|
|
326
335
|
tabs: statusTabs(peers),
|
package/dist/ops/worktree.js
CHANGED
|
@@ -62,7 +62,7 @@ export async function listPeerWorktrees(git, client, args) {
|
|
|
62
62
|
let panes = [];
|
|
63
63
|
try {
|
|
64
64
|
const { session } = await client.resolveSession(typeof args.session === "string" ? args.session : undefined);
|
|
65
|
-
panes = await client.listPanes(session);
|
|
65
|
+
panes = await client.listPanes(session, undefined, { fresh: true });
|
|
66
66
|
}
|
|
67
67
|
catch {
|
|
68
68
|
// Worktrees are useful to list even with no live Zellij session.
|
|
@@ -118,7 +118,7 @@ export async function removePeerWorktree(git, client, args, env = process.env) {
|
|
|
118
118
|
let occupants = [];
|
|
119
119
|
try {
|
|
120
120
|
const { session } = await client.resolveSession(typeof args.session === "string" ? args.session : undefined);
|
|
121
|
-
occupants = panesIn(await client.listPanes(session), target.path);
|
|
121
|
+
occupants = panesIn(await client.listPanes(session, undefined, { fresh: true }), target.path);
|
|
122
122
|
}
|
|
123
123
|
catch (err) {
|
|
124
124
|
// No live session means nobody is working in it. Any other failure must
|
package/dist/schema.js
CHANGED
|
@@ -129,13 +129,25 @@ export const PARAMS = [
|
|
|
129
129
|
name: "sampleMs",
|
|
130
130
|
type: "number",
|
|
131
131
|
flags: ["--sample-ms"],
|
|
132
|
-
description: "status:
|
|
132
|
+
description: "status: explicitly sample twice with this gap (fallback default 400); 0 reports running/exited only; default prefers bus changes",
|
|
133
133
|
},
|
|
134
134
|
{
|
|
135
135
|
name: "sinceLast",
|
|
136
136
|
type: "boolean",
|
|
137
137
|
flags: ["--since-last"],
|
|
138
|
-
description: "status: classify
|
|
138
|
+
description: "status: classify changes since the previous bus observation (default when bus available); first observation is unknown unless a prompt is recognized",
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: "fresh",
|
|
142
|
+
type: "boolean",
|
|
143
|
+
flags: ["--fresh"],
|
|
144
|
+
description: "bypass the short-lived session/pane/tab listing cache for this call",
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: "serveAddress",
|
|
148
|
+
type: "string",
|
|
149
|
+
flags: ["--serve"],
|
|
150
|
+
description: "use an existing serve endpoint (e.g. 127.0.0.1:9419 through an SSH tunnel); preferred for frequent status calls; uses ZSWARM_SERVE_TOKEN",
|
|
139
151
|
},
|
|
140
152
|
{
|
|
141
153
|
name: "limit",
|
|
@@ -483,7 +495,7 @@ export function cliUsage() {
|
|
|
483
495
|
const value = param.type === "boolean" ? "" : param.type === "number" ? " N" : " VALUE";
|
|
484
496
|
lines.push(` ${param.flags.join(", ").padEnd(28)}${value.trim().padEnd(6)}${param.description}`);
|
|
485
497
|
}
|
|
486
|
-
lines.push("", "Guards: writes refuse zswarm's own pane (--allow-self) and exited panes (--force). --expect requires the screen to contain a substring first.", "Bus: `zswarm bus --install` once per Zellij session. `--force` closes orphan bus panes and reloads; do not use it as a retry.", "Remote: ZSWARM_SSH (+ ZSWARM_TMP=auto or ZSWARM_SSH_MODE=interactive on Windows). Or run `zswarm serve --listen` next to Zellij and set ZSWARM_SERVE (+ ZSWARM_SERVE_TOKEN). Serve binds loopback only and always requires a token.", "Env: ZSWARM_BIN, ZSWARM_PATH, ZSWARM_SESSION, ZSWARM_SELF_PANE, ZSWARM_FROM, ZELLIJ_PANE_ID, ZELLIJ_SESSION_NAME, ZSWARM_BUS, ZSWARM_BUS_PLUGIN, ZSWARM_SSH, ZSWARM_TMP, ZSWARM_SSH_MODE, ZSWARM_SERVE, ZSWARM_SERVE_TOKEN", "");
|
|
498
|
+
lines.push("", "Guards: writes refuse zswarm's own pane (--allow-self) and exited panes (--force). --expect requires the screen to contain a substring first.", "Bus: `zswarm bus --install` once per Zellij session. `--force` closes orphan bus panes and reloads; do not use it as a retry.", "Remote: ZSWARM_SSH (+ ZSWARM_TMP=auto or ZSWARM_SSH_MODE=interactive on Windows). Or run `zswarm serve --listen` next to Zellij and set ZSWARM_SERVE (+ ZSWARM_SERVE_TOKEN). Serve binds loopback only and always requires a token.", "Env: ZSWARM_BIN, ZSWARM_PATH, ZSWARM_SESSION, ZSWARM_SELF_PANE, ZSWARM_FROM, ZELLIJ_PANE_ID, ZELLIJ_SESSION_NAME, ZSWARM_BUS, ZSWARM_BUS_PLUGIN, ZSWARM_SSH, ZSWARM_TMP, ZSWARM_SSH_MODE, ZSWARM_SERVE, ZSWARM_SERVE_TOKEN, ZSWARM_CACHE_TTL_MS", "");
|
|
487
499
|
return lines.join("\n");
|
|
488
500
|
}
|
|
489
501
|
/** Turn argv (without the op) into dispatch args, driven by PARAMS. */
|
package/dist/zellij/binary.d.ts
CHANGED
|
@@ -30,14 +30,27 @@ export declare function identityCacheKey(zellijPath: string, ssh?: {
|
|
|
30
30
|
options: string[];
|
|
31
31
|
mode?: string;
|
|
32
32
|
remoteBin?: string;
|
|
33
|
+
/** SSH client binary (`SshTarget.ssh`). */
|
|
34
|
+
ssh?: string;
|
|
35
|
+
sshBin?: string;
|
|
36
|
+
remoteShell?: string;
|
|
33
37
|
} | null): string;
|
|
34
38
|
/**
|
|
35
39
|
* Confirm the resolved binary is Zellij. Only verified identities are cached.
|
|
36
40
|
* Transport timeouts leave the cache empty so a later call can retry.
|
|
37
41
|
* Returns false when verification is unresolved; only true is cached.
|
|
42
|
+
* In-flight probes are never shared — each caller supplies its own timeout.
|
|
38
43
|
*/
|
|
39
|
-
export declare function ensureZellijIdentity(exec: ExecFn, zellijPath: string, timeoutMs?: number, cacheKey?: string): Promise<boolean>;
|
|
40
|
-
export declare function ensureZellijCapabilities(exec: ExecFn, zellijPath: string, timeoutMs?: number, cacheKey?: string): Promise<boolean>;
|
|
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
|
+
}>;
|
|
41
54
|
/** Test helper: drop cached identity/capability probes. */
|
|
42
55
|
export declare function resetZellijIdentityCache(): void;
|
|
43
56
|
export declare function resolveZellijBinary(env?: NodeJS.ProcessEnv): string;
|
package/dist/zellij/binary.js
CHANGED
|
@@ -98,26 +98,32 @@ export function isZellijVersionOutput(stdout, stderr = "") {
|
|
|
98
98
|
*/
|
|
99
99
|
export function identityCacheKey(zellijPath, ssh) {
|
|
100
100
|
if (!ssh)
|
|
101
|
-
return zellijPath;
|
|
102
|
-
return [
|
|
101
|
+
return JSON.stringify([zellijPath]);
|
|
102
|
+
return JSON.stringify([
|
|
103
103
|
zellijPath,
|
|
104
104
|
ssh.host,
|
|
105
105
|
ssh.remoteBin ?? "",
|
|
106
106
|
ssh.mode ?? "ssh",
|
|
107
|
-
ssh.options
|
|
108
|
-
|
|
107
|
+
ssh.options,
|
|
108
|
+
ssh.sshBin ?? ssh.ssh ?? "",
|
|
109
|
+
ssh.remoteShell ?? "",
|
|
110
|
+
]);
|
|
109
111
|
}
|
|
110
112
|
/**
|
|
111
113
|
* Confirm the resolved binary is Zellij. Only verified identities are cached.
|
|
112
114
|
* Transport timeouts leave the cache empty so a later call can retry.
|
|
113
115
|
* Returns false when verification is unresolved; only true is cached.
|
|
116
|
+
* In-flight probes are never shared — each caller supplies its own timeout.
|
|
114
117
|
*/
|
|
115
|
-
export async function ensureZellijIdentity(exec, zellijPath, timeoutMs = 3_000, cacheKey = zellijPath) {
|
|
118
|
+
export async function ensureZellijIdentity(exec, zellijPath, timeoutMs = 3_000, cacheKey = zellijPath, signal) {
|
|
116
119
|
if (identityCache.has(cacheKey))
|
|
117
120
|
return true;
|
|
121
|
+
if (signal?.aborted)
|
|
122
|
+
return false;
|
|
118
123
|
assertZellijBinaryPath(zellijPath);
|
|
119
124
|
const result = await exec(["--version"], {
|
|
120
125
|
timeoutMs: Math.min(timeoutMs, 5_000),
|
|
126
|
+
signal,
|
|
121
127
|
});
|
|
122
128
|
const text = `${result.stdout}\n${result.stderr}`;
|
|
123
129
|
if (/usage:\s*zswarm/i.test(text) || /unknown arg:/i.test(text)) {
|
|
@@ -127,6 +133,8 @@ export async function ensureZellijIdentity(exec, zellijPath, timeoutMs = 3_000,
|
|
|
127
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`);
|
|
128
134
|
}
|
|
129
135
|
if (result.code === 0) {
|
|
136
|
+
if (signal?.aborted)
|
|
137
|
+
return false;
|
|
130
138
|
if (isZellijVersionOutput(result.stdout, result.stderr)) {
|
|
131
139
|
identityCache.add(cacheKey);
|
|
132
140
|
return true;
|
|
@@ -142,11 +150,14 @@ export async function ensureZellijIdentity(exec, zellijPath, timeoutMs = 3_000,
|
|
|
142
150
|
* Returns false when the transport cannot verify support.
|
|
143
151
|
*/
|
|
144
152
|
const capabilityCache = new Set();
|
|
145
|
-
export async function ensureZellijCapabilities(exec, zellijPath, timeoutMs = 3_000, cacheKey = zellijPath) {
|
|
153
|
+
export async function ensureZellijCapabilities(exec, zellijPath, timeoutMs = 3_000, cacheKey = zellijPath, signal) {
|
|
146
154
|
if (capabilityCache.has(cacheKey))
|
|
147
155
|
return true;
|
|
156
|
+
if (signal?.aborted)
|
|
157
|
+
return false;
|
|
148
158
|
const result = await exec(["list-sessions", "--help"], {
|
|
149
159
|
timeoutMs: Math.min(timeoutMs, 5_000),
|
|
160
|
+
signal,
|
|
150
161
|
});
|
|
151
162
|
const text = `${result.stdout}\n${result.stderr}`;
|
|
152
163
|
if (/usage:\s*zswarm/i.test(text)) {
|
|
@@ -159,6 +170,8 @@ export async function ensureZellijCapabilities(exec, zellijPath, timeoutMs = 3_0
|
|
|
159
170
|
if (result.code === 0 &&
|
|
160
171
|
/--no-formatting/i.test(text) &&
|
|
161
172
|
/list-sessions/i.test(text)) {
|
|
173
|
+
if (signal?.aborted)
|
|
174
|
+
return false;
|
|
162
175
|
capabilityCache.add(cacheKey);
|
|
163
176
|
return true;
|
|
164
177
|
}
|
|
@@ -168,6 +181,25 @@ export async function ensureZellijCapabilities(exec, zellijPath, timeoutMs = 3_0
|
|
|
168
181
|
// Soft: leave unresolved on transport failure.
|
|
169
182
|
return false;
|
|
170
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
|
+
}
|
|
171
203
|
/** Test helper: drop cached identity/capability probes. */
|
|
172
204
|
export function resetZellijIdentityCache() {
|
|
173
205
|
identityCache.clear();
|
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,
|
|
@@ -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,3 +1,4 @@
|
|
|
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";
|
|
3
4
|
import { type IpcDiscoveryState } from "../exec.js";
|
|
@@ -12,6 +13,10 @@ export type ZellijClientOptions = {
|
|
|
12
13
|
signal?: AbortSignal;
|
|
13
14
|
/** Skip the one-time `zellij --version` identity probe (tests). */
|
|
14
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;
|
|
15
20
|
};
|
|
16
21
|
export type ZellijTransport = {
|
|
17
22
|
kind: "local" | "ssh";
|
|
@@ -23,16 +28,23 @@ export type ZellijTransport = {
|
|
|
23
28
|
/** Outcome of `ZSWARM_TMP=auto` discovery when SSH routing is active. */
|
|
24
29
|
ipc?: IpcDiscoveryState;
|
|
25
30
|
};
|
|
26
|
-
/**
|
|
31
|
+
/** Zellij wrapper with bounded caches of completed discovery observations. */
|
|
27
32
|
export declare function createZellijClient(options?: ZellijClientOptions): {
|
|
28
33
|
zellijPath: string;
|
|
29
34
|
selfPaneId: string | null;
|
|
35
|
+
contextKey: () => string;
|
|
36
|
+
invalidateListings: (session: string) => void;
|
|
37
|
+
observeManifest: (session: string, revision: string) => void;
|
|
30
38
|
readonly transport: ZellijTransport;
|
|
31
39
|
/** A `file:` plugin url only names a path on the machine running Zellij. */
|
|
32
40
|
remote: boolean;
|
|
33
|
-
listSessions: (callTimeoutMs?: number
|
|
41
|
+
listSessions: (callTimeoutMs?: number, opts?: {
|
|
42
|
+
fresh?: boolean;
|
|
43
|
+
}) => Promise<ZellijSession[]>;
|
|
34
44
|
resolveSession: (explicit?: string | null, callTimeoutMs?: number) => Promise<ZellijSessionResolve>;
|
|
35
|
-
listPanes: (session: string, callTimeoutMs?: number
|
|
45
|
+
listPanes: (session: string, callTimeoutMs?: number, opts?: {
|
|
46
|
+
fresh?: boolean;
|
|
47
|
+
}) => Promise<ZellijPane[]>;
|
|
36
48
|
resolvePane: typeof resolvePane;
|
|
37
49
|
injectPane: (input: {
|
|
38
50
|
session: string;
|
|
@@ -112,7 +124,9 @@ export declare function createZellijClient(options?: ZellijClientOptions): {
|
|
|
112
124
|
paneId: string;
|
|
113
125
|
session: string;
|
|
114
126
|
}>;
|
|
115
|
-
listTabs: (session: string, callTimeoutMs?: number
|
|
127
|
+
listTabs: (session: string, callTimeoutMs?: number, opts?: {
|
|
128
|
+
fresh?: boolean;
|
|
129
|
+
}) => Promise<ZellijTab[]>;
|
|
116
130
|
dumpLayout: (session: string) => Promise<string>;
|
|
117
131
|
stackPanes: (input: {
|
|
118
132
|
session: string;
|