@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/zellij/client.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { ZellijError } from "../errors.js";
|
|
2
|
+
import { ListingCache, executorIdentity, listingTtl, routingEnvironment, sharedListings } from "./cache.js";
|
|
2
3
|
import { buildClosePaneArgs, buildDumpArgs, buildDumpLayoutArgs, buildFocusPaneArgs, buildLaunchPluginArgs, buildListPanesArgs, buildListTabsArgs, buildNewPaneArgs, buildNewTabArgs, buildPasteArgs, buildPipeArgs, buildRenamePaneArgs, buildRenameTabArgs, buildSendEnterArgs, buildSendKeysArgs, buildStackPanesArgs, buildWriteCharsArgs, changedPayload, scrollbackPayload, waitPayload, } from "./args.js";
|
|
3
4
|
import { DEFAULT_BUS_TIMEOUT_MS, parseBusReply, parseChangedReply, parseScrollbackReply, parseWaitReply, } from "./bus.js";
|
|
4
5
|
import { parseTabList, resolveTab } from "./tabs.js";
|
|
5
6
|
import { createSshExec } from "../exec.js";
|
|
6
|
-
import { DEFAULT_TIMEOUT_MS, NOT_FOUND_EXIT, defaultExec,
|
|
7
|
+
import { DEFAULT_TIMEOUT_MS, NOT_FOUND_EXIT, defaultExec, ensureZellijProbes, identityCacheKey, resolveSshTarget, resolveZellijBinary, sanitizeZellijEnv, } from "./binary.js";
|
|
7
8
|
import { normalizePaneId, parsePaneList, resolvePane, } from "./panes.js";
|
|
8
9
|
import { isZellijNoSessionsOutput, parseSessionList, resolveSelfPaneId, sessionFromEnv, sessionFromList, } from "./session.js";
|
|
9
|
-
/**
|
|
10
|
+
/** Zellij wrapper with bounded caches of completed discovery observations. */
|
|
10
11
|
export function createZellijClient(options = {}) {
|
|
11
12
|
const env = options.env ?? process.env;
|
|
12
13
|
// A remote crew never resolves a local binary. An injected exec is a unit
|
|
@@ -21,13 +22,56 @@ export function createZellijClient(options = {}) {
|
|
|
21
22
|
: resolveZellijBinary(env));
|
|
22
23
|
const sshExec = options.exec || !ssh
|
|
23
24
|
? null
|
|
24
|
-
: createSshExec(ssh, sanitizeZellijEnv(env));
|
|
25
|
+
: createSshExec(ssh, sanitizeZellijEnv(env), { pinIpc: true });
|
|
25
26
|
const rawExec = options.exec ??
|
|
26
27
|
(sshExec ? sshExec : defaultExec(zellijPath, env));
|
|
27
28
|
const exec = (args, opts) => rawExec(args, { ...opts, signal: opts.signal ?? options.signal });
|
|
28
29
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
29
30
|
const selfPaneId = resolveSelfPaneId(env);
|
|
30
|
-
const probeKey = identityCacheKey(zellijPath, ssh);
|
|
31
|
+
const probeKey = JSON.stringify([identityCacheKey(zellijPath, ssh), routingEnvironment(env)]);
|
|
32
|
+
const now = options.now ?? Date.now;
|
|
33
|
+
const cache = options.cache instanceof ListingCache ? options.cache : sharedListings;
|
|
34
|
+
const ttl = options.cache === false || (options.exec && !options.cache) ? 0 : listingTtl(env);
|
|
35
|
+
const baseContext = JSON.stringify([zellijPath, ssh, executorIdentity(options.exec), routingEnvironment(env)]);
|
|
36
|
+
const scopeFor = (session) => JSON.stringify([baseContext, session ?? null]);
|
|
37
|
+
const contextKey = () => JSON.stringify([baseContext, readTransport().ipc ?? null]);
|
|
38
|
+
const cacheUsable = () => {
|
|
39
|
+
const ipc = readTransport().ipc;
|
|
40
|
+
return ttl > 0 && (ipc?.requested?.toLowerCase() !== "auto" || ipc.status === "resolved");
|
|
41
|
+
};
|
|
42
|
+
function invalidateListings(session) {
|
|
43
|
+
cache.invalidate(scopeFor(session));
|
|
44
|
+
cache.invalidate(scopeFor());
|
|
45
|
+
}
|
|
46
|
+
function observeManifest(session, revision) {
|
|
47
|
+
if (cache.observe(scopeFor(session), revision))
|
|
48
|
+
cache.invalidate(scopeFor());
|
|
49
|
+
}
|
|
50
|
+
async function listing(kind, session, budget, fresh, read) {
|
|
51
|
+
const remaining = operationBudget(budget, `zellij ${kind}`);
|
|
52
|
+
remaining(); // An aborted/expired caller cannot consume a cached value.
|
|
53
|
+
// Resolve auto IPC before selecting the namespace, including cold reads.
|
|
54
|
+
// A sibling can refresh discovery while this read is in flight, so keep
|
|
55
|
+
// the original namespace and refuse to cache under a different one later.
|
|
56
|
+
if (ssh?.tmp?.trim().toLowerCase() === "auto") {
|
|
57
|
+
await sshExec.prepareIpc(remaining(), options.signal);
|
|
58
|
+
remaining();
|
|
59
|
+
}
|
|
60
|
+
const key = () => JSON.stringify([contextKey(), session ?? null, kind]);
|
|
61
|
+
const startedKey = key();
|
|
62
|
+
if (cacheUsable() && !fresh) {
|
|
63
|
+
const hit = cache.get(startedKey, now());
|
|
64
|
+
if (hit !== undefined)
|
|
65
|
+
return hit;
|
|
66
|
+
}
|
|
67
|
+
const scope = scopeFor(session);
|
|
68
|
+
const ticket = cache.begin(scope);
|
|
69
|
+
const value = await read(remaining);
|
|
70
|
+
remaining();
|
|
71
|
+
if (cacheUsable() && key() === startedKey)
|
|
72
|
+
cache.put(startedKey, scope, ticket, value, ttl, now());
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
31
75
|
function readTransport() {
|
|
32
76
|
if (!ssh)
|
|
33
77
|
return { kind: "local", mode: "local" };
|
|
@@ -66,10 +110,11 @@ export function createZellijClient(options = {}) {
|
|
|
66
110
|
return;
|
|
67
111
|
// Only the helpers' positively verified results are cached. In-flight or
|
|
68
112
|
// unresolved probes must not tie this call to another operation's budget.
|
|
69
|
-
|
|
70
|
-
|
|
113
|
+
if (sshExec?.prepareIpc)
|
|
114
|
+
await sshExec.prepareIpc(remaining(), options.signal);
|
|
115
|
+
await ensureZellijProbes(exec, zellijPath, remaining(), probeKey, options.signal);
|
|
71
116
|
}
|
|
72
|
-
async function
|
|
117
|
+
async function runCommand(args, label, callTimeoutMs = timeoutMs) {
|
|
73
118
|
const remaining = operationBudget(callTimeoutMs, label);
|
|
74
119
|
await ensureIdentity(remaining);
|
|
75
120
|
const result = await exec(args, { timeoutMs: remaining() });
|
|
@@ -85,34 +130,58 @@ export function createZellijClient(options = {}) {
|
|
|
85
130
|
}
|
|
86
131
|
return result;
|
|
87
132
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
133
|
+
const mutations = new Set([
|
|
134
|
+
"new-pane", "new-tab", "close-pane", "rename-pane", "rename-tab-by-id",
|
|
135
|
+
"focus-pane-id", "stack-panes", "launch-or-focus-plugin", "paste", "send-keys", "write-chars",
|
|
136
|
+
]);
|
|
137
|
+
async function run(args, label, callTimeoutMs = timeoutMs) {
|
|
138
|
+
const action = args[args.indexOf("action") + 1];
|
|
139
|
+
const index = args.indexOf("--session");
|
|
140
|
+
const session = index >= 0 ? args[index + 1] : undefined;
|
|
141
|
+
const mutation = session && mutations.has(action ?? "");
|
|
142
|
+
if (mutation)
|
|
143
|
+
invalidateListings(session);
|
|
144
|
+
try {
|
|
145
|
+
return await runCommand(args, label, callTimeoutMs);
|
|
97
146
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (isZellijNoSessionsOutput(result.stdout, result.stderr))
|
|
104
|
-
return [];
|
|
105
|
-
throw new ZellijError("zellij_failed", `zellij list-sessions failed (exit ${result.code}): ${detail}`);
|
|
147
|
+
finally {
|
|
148
|
+
// Failed/uncertain writes can still have reached Zellij. Drop both reads
|
|
149
|
+
// from before the write and reads that raced while it was in flight.
|
|
150
|
+
if (mutation)
|
|
151
|
+
invalidateListings(session);
|
|
106
152
|
}
|
|
107
|
-
|
|
153
|
+
}
|
|
154
|
+
async function listSessions(callTimeoutMs = timeoutMs, opts = {}) {
|
|
155
|
+
return listing("list-sessions", undefined, callTimeoutMs, opts.fresh === true, async (remaining) => {
|
|
156
|
+
await ensureIdentity(remaining);
|
|
157
|
+
// Keep annotations (EXITED / current); --short drops them.
|
|
158
|
+
const result = await exec(["list-sessions", "--no-formatting"], {
|
|
159
|
+
timeoutMs: remaining(),
|
|
160
|
+
});
|
|
161
|
+
if (result.code === NOT_FOUND_EXIT) {
|
|
162
|
+
throw new ZellijError("zellij_missing", `zellij binary not found (${zellijPath}); install Zellij ≥ 0.42, add it to PATH, or set ZSWARM_BIN / ZSWARM_PATH`);
|
|
163
|
+
}
|
|
164
|
+
if (result.code !== 0) {
|
|
165
|
+
const detail = result.stderr.trim() || result.stdout.trim() || "no output";
|
|
166
|
+
if (/usage:\s*zswarm/i.test(detail) || /unknown arg:/i.test(detail)) {
|
|
167
|
+
throw new ZellijError("zellij_wrong_bin", `resolved binary looks like zswarm, not Zellij (${zellijPath}): ${detail}`);
|
|
168
|
+
}
|
|
169
|
+
if (isZellijNoSessionsOutput(result.stdout, result.stderr))
|
|
170
|
+
return [];
|
|
171
|
+
throw new ZellijError("zellij_failed", `zellij list-sessions failed (exit ${result.code}): ${detail}`);
|
|
172
|
+
}
|
|
173
|
+
return parseSessionList(result.stdout);
|
|
174
|
+
});
|
|
108
175
|
}
|
|
109
176
|
async function resolveSession(explicit, callTimeoutMs = timeoutMs) {
|
|
110
177
|
return (sessionFromEnv(env, explicit) ??
|
|
111
178
|
sessionFromList(await listSessions(callTimeoutMs)));
|
|
112
179
|
}
|
|
113
|
-
async function listPanes(session, callTimeoutMs = timeoutMs) {
|
|
114
|
-
|
|
115
|
-
|
|
180
|
+
async function listPanes(session, callTimeoutMs = timeoutMs, opts = {}) {
|
|
181
|
+
return listing("list-panes", session, callTimeoutMs, opts.fresh === true, async (remaining) => {
|
|
182
|
+
const result = await run(buildListPanesArgs(session), "zellij action list-panes", remaining());
|
|
183
|
+
return parsePaneList(result.stdout);
|
|
184
|
+
});
|
|
116
185
|
}
|
|
117
186
|
async function injectPane(input) {
|
|
118
187
|
const paneId = normalizePaneId(input.paneId);
|
|
@@ -164,9 +233,11 @@ export function createZellijClient(options = {}) {
|
|
|
164
233
|
await run(buildFocusPaneArgs(input.session, paneId), "zellij action focus-pane-id");
|
|
165
234
|
return { paneId, session: input.session };
|
|
166
235
|
}
|
|
167
|
-
async function listTabs(session, callTimeoutMs = timeoutMs) {
|
|
168
|
-
|
|
169
|
-
|
|
236
|
+
async function listTabs(session, callTimeoutMs = timeoutMs, opts = {}) {
|
|
237
|
+
return listing("list-tabs", session, callTimeoutMs, opts.fresh === true, async (remaining) => {
|
|
238
|
+
const result = await run(buildListTabsArgs(session), "zellij action list-tabs", remaining());
|
|
239
|
+
return parseTabList(result.stdout);
|
|
240
|
+
});
|
|
170
241
|
}
|
|
171
242
|
async function dumpLayout(session) {
|
|
172
243
|
const result = await run(buildDumpLayoutArgs(session), "zellij action dump-layout");
|
|
@@ -282,6 +353,9 @@ export function createZellijClient(options = {}) {
|
|
|
282
353
|
return {
|
|
283
354
|
zellijPath,
|
|
284
355
|
selfPaneId,
|
|
356
|
+
contextKey,
|
|
357
|
+
invalidateListings,
|
|
358
|
+
observeManifest,
|
|
285
359
|
get transport() {
|
|
286
360
|
return readTransport();
|
|
287
361
|
},
|