@zswarm/core 0.1.4 → 0.1.5
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 +2 -0
- package/dist/exec.js +18 -2
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/ops/bus.d.ts +10 -7
- package/dist/ops/bus.js +119 -86
- package/dist/ops/delivery.d.ts +3 -3
- package/dist/ops/delivery.js +4 -7
- package/dist/ops/dispatch.js +19 -7
- package/dist/ops/serve.d.ts +1 -0
- package/dist/ops/serve.js +7 -1
- package/dist/ops/types.d.ts +2 -0
- package/dist/ops/util.d.ts +1 -0
- package/dist/ops/util.js +5 -0
- package/dist/ops/wait.d.ts +8 -1
- package/dist/ops/wait.js +7 -3
- package/dist/schema.js +5 -2
- package/dist/state.d.ts +7 -6
- package/dist/state.js +77 -17
- package/dist/zellij/args.d.ts +2 -0
- package/dist/zellij/args.js +1 -0
- package/dist/zellij/bus.d.ts +9 -0
- package/dist/zellij/bus.js +20 -0
- package/dist/zellij/client.d.ts +1 -0
- package/dist/zellij/client.js +2 -1
- package/package.json +2 -2
package/dist/exec.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export type ExecOptions = {
|
|
|
16
16
|
* timeout.
|
|
17
17
|
*/
|
|
18
18
|
until?: (stdout: string) => boolean;
|
|
19
|
+
/** Kill the child when aborted (MCP cancellation). */
|
|
20
|
+
signal?: AbortSignal;
|
|
19
21
|
};
|
|
20
22
|
export type ExecFn = (args: string[], options: ExecOptions) => Promise<ExecResult>;
|
|
21
23
|
export declare const NOT_FOUND_EXIT = 127;
|
package/dist/exec.js
CHANGED
|
@@ -100,14 +100,25 @@ function runUntil(binPath, args, env, options) {
|
|
|
100
100
|
return;
|
|
101
101
|
settled = true;
|
|
102
102
|
clearTimeout(timer);
|
|
103
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
103
104
|
child.kill();
|
|
104
105
|
resolve(result);
|
|
105
106
|
};
|
|
107
|
+
const onAbort = () => finish({
|
|
108
|
+
code: -1,
|
|
109
|
+
stdout,
|
|
110
|
+
stderr: `${binPath} cancelled`,
|
|
111
|
+
});
|
|
106
112
|
const timer = setTimeout(() => finish({
|
|
107
113
|
code: -1,
|
|
108
114
|
stdout,
|
|
109
115
|
stderr: `${binPath} timed out after ${options.timeoutMs}ms`,
|
|
110
116
|
}), options.timeoutMs);
|
|
117
|
+
if (options.signal?.aborted) {
|
|
118
|
+
onAbort();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
111
122
|
child.on("error", (err) => {
|
|
112
123
|
const missing = err.code === "ENOENT" || err.code === "ENOTDIR";
|
|
113
124
|
finish({
|
|
@@ -138,6 +149,7 @@ function runToExit(binPath, args, env, options) {
|
|
|
138
149
|
cwd: options.cwd,
|
|
139
150
|
maxBuffer: 8 * 1024 * 1024,
|
|
140
151
|
windowsHide: true,
|
|
152
|
+
signal: options.signal,
|
|
141
153
|
env: options.env ? { ...env, ...options.env } : env,
|
|
142
154
|
}, (error, stdout, stderr) => {
|
|
143
155
|
const failure = error;
|
|
@@ -151,11 +163,15 @@ function runToExit(binPath, args, env, options) {
|
|
|
151
163
|
return;
|
|
152
164
|
}
|
|
153
165
|
if (failure &&
|
|
154
|
-
(failure.killed === true ||
|
|
166
|
+
(failure.killed === true ||
|
|
167
|
+
typeof failure.signal === "string" ||
|
|
168
|
+
failure.name === "AbortError")) {
|
|
155
169
|
resolve({
|
|
156
170
|
code: -1,
|
|
157
171
|
stdout: String(stdout ?? ""),
|
|
158
|
-
stderr:
|
|
172
|
+
stderr: failure.name === "AbortError"
|
|
173
|
+
? `${binPath} cancelled`
|
|
174
|
+
: `${binPath} timed out after ${options.timeoutMs}ms`,
|
|
159
175
|
});
|
|
160
176
|
return;
|
|
161
177
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,9 +4,9 @@ export { createGitClient, defaultWorktreeRoot, normalizeRepoPath, parseWorktreeL
|
|
|
4
4
|
export { expandHomePath, resolveZellijBinary, sanitizeZellijEnv, type ZellijExecFn, type ZellijExecResult, } from "./zellij/binary.js";
|
|
5
5
|
export { normalizePaneId, parsePaneList, resolvePane, type ZellijPane, } from "./zellij/panes.js";
|
|
6
6
|
export { parseSessionList, resolveSelfPaneId, type ZellijSessionResolve, } from "./zellij/session.js";
|
|
7
|
-
export { buildClosePaneArgs, buildDumpArgs, buildLaunchPluginArgs, buildListPanesArgs, buildNewPaneArgs, buildNewTabArgs, buildPasteArgs, buildPipeArgs, buildSendEnterArgs, buildSendKeysArgs, buildWriteCharsArgs, scrollbackPayload, type LaunchPluginInput, type NewPaneInput, type NewTabInput, type PaneDirection, type PipeInput, type ScrollbackRequest, } from "./zellij/args.js";
|
|
8
|
-
export { busPluginUrl, busToPanes, nextConfigKey, parseBusReply, parseScrollbackReply, resolveBusPlugin, scrollbackToScreen, BUS_PIPE_NAME, DEFAULT_BUS_KEY, DEFAULT_BUS_TIMEOUT_MS, type BusMarker, type BusPane, type BusScrollback, type BusScrollbackPane, type BusSnapshot, } from "./zellij/bus.js";
|
|
9
|
-
export { busSnapshot, planBus, resetBusCache, type BusPlan, } from "./ops/bus.js";
|
|
7
|
+
export { buildClosePaneArgs, buildDumpArgs, buildLaunchPluginArgs, buildListPanesArgs, buildNewPaneArgs, buildNewTabArgs, buildPasteArgs, buildPipeArgs, buildSendEnterArgs, buildSendKeysArgs, buildWriteCharsArgs, scrollbackPayload, waitPayload, type LaunchPluginInput, type NewPaneInput, type NewTabInput, type PaneDirection, type PipeInput, type ScrollbackRequest, type WaitRequest, } from "./zellij/args.js";
|
|
8
|
+
export { busPluginUrl, busToPanes, isBusPluginPane, nextConfigKey, parseBusReply, parseScrollbackReply, resolveBusPlugin, scrollbackToScreen, BUS_PIPE_NAME, DEFAULT_BUS_KEY, DEFAULT_BUS_TIMEOUT_MS, type BusMarker, type BusPane, type BusScrollback, type BusScrollbackPane, type BusSnapshot, } from "./zellij/bus.js";
|
|
9
|
+
export { busSnapshot, closeBusPluginPanes, planBus, resetBusCache, type BusPlan, } from "./ops/bus.js";
|
|
10
10
|
export { createZellijClient, type ZellijClient, type ZellijClientOptions, } from "./zellij/client.js";
|
|
11
11
|
export { createStateStore, defaultStateDir, type LogEntry, type SignalChannel, type StateStore, type StateStoreOptions, } from "./state.js";
|
|
12
12
|
export { dispatchZswarm } from "./ops/dispatch.js";
|
|
@@ -14,7 +14,7 @@ export { assertOpAllowed, assertPaneAllowed, isWriteOp, loadPolicy, type Policy,
|
|
|
14
14
|
export { buildSshRemoteCommand, createSshExec, quoteRemoteArg, shellQuote, type SshTarget, } from "./exec.js";
|
|
15
15
|
export { resolveSshTarget } from "./zellij/binary.js";
|
|
16
16
|
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, installServeLogon, isLoopbackHost, parseListenAddress, SERVE_CALL_TIMEOUT_CAP_MS, SERVE_MAX_REQUEST_BYTES, SERVE_TASK_NAME, serveCallTimeout, serveChildEnv, serveLogonCommand, startServe, uninstallServeLogon, } from "./ops/serve.js";
|
|
17
|
+
export { callServe, DEFAULT_SERVE_LISTEN, installServeLogon, isLoopbackHost, parseListenAddress, redactServeSecret, SERVE_CALL_TIMEOUT_CAP_MS, SERVE_MAX_REQUEST_BYTES, SERVE_TASK_NAME, serveCallTimeout, serveChildEnv, serveLogonCommand, startServe, uninstallServeLogon, } from "./ops/serve.js";
|
|
18
18
|
export { parseTabList, resolveTab, type ZellijTab } from "./zellij/tabs.js";
|
|
19
19
|
export { selectTargets } from "./ops/broadcast.js";
|
|
20
20
|
export { diffScreens, cursorKey } from "./ops/tail.js";
|
package/dist/index.js
CHANGED
|
@@ -4,9 +4,9 @@ export { createGitClient, defaultWorktreeRoot, normalizeRepoPath, parseWorktreeL
|
|
|
4
4
|
export { expandHomePath, resolveZellijBinary, sanitizeZellijEnv, } from "./zellij/binary.js";
|
|
5
5
|
export { normalizePaneId, parsePaneList, resolvePane, } from "./zellij/panes.js";
|
|
6
6
|
export { parseSessionList, resolveSelfPaneId, } from "./zellij/session.js";
|
|
7
|
-
export { buildClosePaneArgs, buildDumpArgs, buildLaunchPluginArgs, buildListPanesArgs, buildNewPaneArgs, buildNewTabArgs, buildPasteArgs, buildPipeArgs, buildSendEnterArgs, buildSendKeysArgs, buildWriteCharsArgs, scrollbackPayload, } from "./zellij/args.js";
|
|
8
|
-
export { busPluginUrl, busToPanes, nextConfigKey, parseBusReply, parseScrollbackReply, resolveBusPlugin, scrollbackToScreen, BUS_PIPE_NAME, DEFAULT_BUS_KEY, DEFAULT_BUS_TIMEOUT_MS, } from "./zellij/bus.js";
|
|
9
|
-
export { busSnapshot, planBus, resetBusCache, } from "./ops/bus.js";
|
|
7
|
+
export { buildClosePaneArgs, buildDumpArgs, buildLaunchPluginArgs, buildListPanesArgs, buildNewPaneArgs, buildNewTabArgs, buildPasteArgs, buildPipeArgs, buildSendEnterArgs, buildSendKeysArgs, buildWriteCharsArgs, scrollbackPayload, waitPayload, } from "./zellij/args.js";
|
|
8
|
+
export { busPluginUrl, busToPanes, isBusPluginPane, nextConfigKey, parseBusReply, parseScrollbackReply, resolveBusPlugin, scrollbackToScreen, BUS_PIPE_NAME, DEFAULT_BUS_KEY, DEFAULT_BUS_TIMEOUT_MS, } from "./zellij/bus.js";
|
|
9
|
+
export { busSnapshot, closeBusPluginPanes, planBus, resetBusCache, } from "./ops/bus.js";
|
|
10
10
|
export { createZellijClient, } from "./zellij/client.js";
|
|
11
11
|
export { createStateStore, defaultStateDir, } from "./state.js";
|
|
12
12
|
export { dispatchZswarm } from "./ops/dispatch.js";
|
|
@@ -14,7 +14,7 @@ export { assertOpAllowed, assertPaneAllowed, isWriteOp, loadPolicy, } from "./po
|
|
|
14
14
|
export { buildSshRemoteCommand, createSshExec, quoteRemoteArg, shellQuote, } from "./exec.js";
|
|
15
15
|
export { resolveSshTarget } from "./zellij/binary.js";
|
|
16
16
|
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, installServeLogon, isLoopbackHost, parseListenAddress, SERVE_CALL_TIMEOUT_CAP_MS, SERVE_MAX_REQUEST_BYTES, SERVE_TASK_NAME, serveCallTimeout, serveChildEnv, serveLogonCommand, startServe, uninstallServeLogon, } from "./ops/serve.js";
|
|
17
|
+
export { callServe, DEFAULT_SERVE_LISTEN, installServeLogon, isLoopbackHost, parseListenAddress, redactServeSecret, SERVE_CALL_TIMEOUT_CAP_MS, SERVE_MAX_REQUEST_BYTES, SERVE_TASK_NAME, serveCallTimeout, serveChildEnv, serveLogonCommand, startServe, uninstallServeLogon, } from "./ops/serve.js";
|
|
18
18
|
export { parseTabList, resolveTab } from "./zellij/tabs.js";
|
|
19
19
|
export { selectTargets } from "./ops/broadcast.js";
|
|
20
20
|
export { diffScreens, cursorKey } from "./ops/tail.js";
|
package/dist/ops/bus.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Policy } from "../policy.js";
|
|
2
2
|
import type { StateStore } from "../state.js";
|
|
3
3
|
import { type BusChanged, type BusSnapshot, type BusWait } from "../zellij/bus.js";
|
|
4
4
|
import type { WaitRequest } from "../zellij/args.js";
|
|
@@ -15,15 +15,16 @@ export type BusPlan = {
|
|
|
15
15
|
};
|
|
16
16
|
/** Tests share a module instance; this drops what a previous case cached. */
|
|
17
17
|
export declare function resetBusCache(): void;
|
|
18
|
-
export declare function planBus(client: ZellijClient, state: StateStore, env?: NodeJS.ProcessEnv): BusPlan;
|
|
18
|
+
export declare function planBus(client: ZellijClient, state: StateStore, env?: NodeJS.ProcessEnv, session?: string): BusPlan;
|
|
19
19
|
/**
|
|
20
20
|
* One pipe, one answer — or null, and the caller polls instead.
|
|
21
21
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
22
|
+
* A freshly launched instance replies before Zellij has pushed it a manifest,
|
|
23
|
+
* so a not-ready answer is retried once rather than reported as an empty
|
|
24
|
+
* session. If it is still cold after that, one same-title rename nudges Zellij
|
|
25
|
+
* to push. A silent pipe used to rotate `instance=` keys, and `zellij pipe
|
|
26
|
+
* --plugin` loads a new copy for each key — that is how a dead bus became
|
|
27
|
+
* dozens of WASM panes. One remembered key, then polling.
|
|
27
28
|
*/
|
|
28
29
|
export declare function busSnapshot(client: ZellijClient, state: StateStore, session: string, clock: Clock, env?: NodeJS.ProcessEnv, payload?: string): Promise<{
|
|
29
30
|
snapshot: BusSnapshot;
|
|
@@ -56,5 +57,7 @@ export declare function busWait(client: ZellijClient, state: StateStore, session
|
|
|
56
57
|
* relative to the previous call rather than a fixed window.
|
|
57
58
|
*/
|
|
58
59
|
export declare function busChanged(client: ZellijClient, state: StateStore, session: string, paneIds: string[], env?: NodeJS.ProcessEnv): Promise<BusChanged | null>;
|
|
60
|
+
/** Close every zswarm-bus plugin pane in this session. Best-effort per pane. */
|
|
61
|
+
export declare function closeBusPluginPanes(client: ZellijClient, session: string, pluginPath: string | null): Promise<string[]>;
|
|
59
62
|
/** Report on the bus, install it, or forget it. */
|
|
60
63
|
export declare function busOp(client: ZellijClient, state: StateStore, args: Record<string, unknown>, clock: Clock, env?: NodeJS.ProcessEnv, policy?: Policy): Promise<OpsResult>;
|
package/dist/ops/bus.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { ZellijError } from "../errors.js";
|
|
3
|
-
import {
|
|
3
|
+
import { loadPolicy } from "../policy.js";
|
|
4
|
+
import { DEFAULT_BUS_KEY, busPluginUrl, isBusPluginPane, parseBusReply, parseChangedReply, parseScrollbackReply, parseWaitReply, resolveBusPlugin, scrollbackToScreen, } from "../zellij/bus.js";
|
|
4
5
|
import { isTrue } from "./util.js";
|
|
5
6
|
/**
|
|
6
7
|
* Whether to ask the event-bus plugin, and what to do when it does not answer.
|
|
@@ -9,10 +10,14 @@ import { isTrue } from "./util.js";
|
|
|
9
10
|
* prompt, because a pipe to a plugin that was never approved costs a process
|
|
10
11
|
* and a timeout on every call and then falls back anyway.
|
|
11
12
|
*/
|
|
12
|
-
/** Config keys to try before giving up; each one is a fresh pipe destination. */
|
|
13
|
-
const MAX_KEY_ATTEMPTS = 3;
|
|
14
13
|
/** A just-launched instance answers before Zellij has pushed it anything. */
|
|
15
14
|
const COLD_RETRY_MS = 200;
|
|
15
|
+
/**
|
|
16
|
+
* A marker written this recently with no live pane is an in-flight install,
|
|
17
|
+
* not a closed pane. Relaunching it is how concurrent agents used to stack
|
|
18
|
+
* copies.
|
|
19
|
+
*/
|
|
20
|
+
const INSTALL_IN_FLIGHT_MS = 10_000;
|
|
16
21
|
/** Set once a pipe has failed, so one dead plugin costs one timeout, not many. */
|
|
17
22
|
let processDisabled = null;
|
|
18
23
|
/** The key that actually answered, per session, for the life of this process. */
|
|
@@ -30,8 +35,8 @@ function negative(raw) {
|
|
|
30
35
|
const v = (raw ?? "").trim().toLowerCase();
|
|
31
36
|
return v === "0" || v === "false" || v === "no" || v === "off";
|
|
32
37
|
}
|
|
33
|
-
export function planBus(client, state, env = process.env) {
|
|
34
|
-
const marker = state.readBus();
|
|
38
|
+
export function planBus(client, state, env = process.env, session = "") {
|
|
39
|
+
const marker = session ? state.readBus(session) : null;
|
|
35
40
|
// A marker can outlive the wasm it names — an upgrade renames the artifact,
|
|
36
41
|
// so a remembered path that no longer exists must not shadow the current one.
|
|
37
42
|
const remembered = marker && existsSync(marker.plugin) ? marker : null;
|
|
@@ -75,7 +80,9 @@ async function askOnce(client, session, url, configKey, payload) {
|
|
|
75
80
|
* blind (ready:false, paneUpdates:0) until something moves. Renaming a pane
|
|
76
81
|
* to the title it already has is an invisible change that forces the push.
|
|
77
82
|
*/
|
|
78
|
-
async function nudgeManifest(client, session) {
|
|
83
|
+
async function nudgeManifest(client, session, env) {
|
|
84
|
+
if (loadPolicy(env).readOnly)
|
|
85
|
+
return;
|
|
79
86
|
const panes = await client.listPanes(session);
|
|
80
87
|
const pane = panes.find((p) => !p.isPlugin && !p.exited && p.title.trim());
|
|
81
88
|
if (!pane)
|
|
@@ -85,52 +92,40 @@ async function nudgeManifest(client, session) {
|
|
|
85
92
|
/**
|
|
86
93
|
* One pipe, one answer — or null, and the caller polls instead.
|
|
87
94
|
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
95
|
+
* A freshly launched instance replies before Zellij has pushed it a manifest,
|
|
96
|
+
* so a not-ready answer is retried once rather than reported as an empty
|
|
97
|
+
* session. If it is still cold after that, one same-title rename nudges Zellij
|
|
98
|
+
* to push. A silent pipe used to rotate `instance=` keys, and `zellij pipe
|
|
99
|
+
* --plugin` loads a new copy for each key — that is how a dead bus became
|
|
100
|
+
* dozens of WASM panes. One remembered key, then polling.
|
|
93
101
|
*/
|
|
94
102
|
export async function busSnapshot(client, state, session, clock, env = process.env, payload = "status") {
|
|
95
|
-
const plan = planBus(client, state, env);
|
|
103
|
+
const plan = planBus(client, state, env, session);
|
|
96
104
|
const pluginPath = plan.plugin;
|
|
97
105
|
if (!plan.enabled || !plan.url || !pluginPath)
|
|
98
106
|
return null;
|
|
99
|
-
|
|
100
|
-
let
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
await
|
|
107
|
+
const configKey = answeredWith.get(session) ?? plan.configKey;
|
|
108
|
+
let snapshot = await askOnce(client, session, plan.url, configKey, payload);
|
|
109
|
+
if (snapshot && !snapshot.ready) {
|
|
110
|
+
await clock.sleep(COLD_RETRY_MS);
|
|
111
|
+
snapshot =
|
|
112
|
+
(await askOnce(client, session, plan.url, configKey, payload)) ??
|
|
113
|
+
snapshot;
|
|
114
|
+
}
|
|
115
|
+
if (snapshot && !snapshot.ready) {
|
|
116
|
+
try {
|
|
117
|
+
await nudgeManifest(client, session, env);
|
|
105
118
|
snapshot =
|
|
106
119
|
(await askOnce(client, session, plan.url, configKey, payload)) ??
|
|
107
120
|
snapshot;
|
|
108
121
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
try {
|
|
112
|
-
await nudgeManifest(client, session);
|
|
113
|
-
snapshot =
|
|
114
|
-
(await askOnce(client, session, plan.url, configKey, payload)) ??
|
|
115
|
-
snapshot;
|
|
116
|
-
}
|
|
117
|
-
catch {
|
|
118
|
-
// A failed nudge must not fail the op — the caller polls instead.
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
if (snapshot) {
|
|
122
|
-
answeredWith.set(session, configKey);
|
|
123
|
-
if (plan.installed && configKey !== plan.configKey) {
|
|
124
|
-
// Remember the rotation, so the next process starts on a live key.
|
|
125
|
-
state.writeBus({
|
|
126
|
-
plugin: pluginPath,
|
|
127
|
-
configKey,
|
|
128
|
-
installedAt: clock.now(),
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
return { snapshot, configKey };
|
|
122
|
+
catch {
|
|
123
|
+
// A failed nudge must not fail the op — the caller polls instead.
|
|
132
124
|
}
|
|
133
|
-
|
|
125
|
+
}
|
|
126
|
+
if (snapshot) {
|
|
127
|
+
answeredWith.set(session, configKey);
|
|
128
|
+
return { snapshot, configKey };
|
|
134
129
|
}
|
|
135
130
|
processDisabled = "plugin did not answer; using zellij polling for this run";
|
|
136
131
|
return null;
|
|
@@ -149,7 +144,7 @@ export async function busSnapshot(client, state, session, clock, env = process.e
|
|
|
149
144
|
export async function busScreens(client, state, session, paneIds, clock, env = process.env) {
|
|
150
145
|
if (paneIds.length < 2)
|
|
151
146
|
return null;
|
|
152
|
-
const plan = planBus(client, state, env);
|
|
147
|
+
const plan = planBus(client, state, env, session);
|
|
153
148
|
if (!plan.enabled || !plan.url)
|
|
154
149
|
return null;
|
|
155
150
|
const configKey = answeredWith.get(session) ?? plan.configKey;
|
|
@@ -179,7 +174,7 @@ export async function busScreens(client, state, session, paneIds, clock, env = p
|
|
|
179
174
|
* instance, no reply — and the caller runs its own loop.
|
|
180
175
|
*/
|
|
181
176
|
export async function busWait(client, state, session, request, env = process.env) {
|
|
182
|
-
const plan = planBus(client, state, env);
|
|
177
|
+
const plan = planBus(client, state, env, session);
|
|
183
178
|
if (!plan.enabled || !plan.url)
|
|
184
179
|
return null;
|
|
185
180
|
const configKey = answeredWith.get(session) ?? plan.configKey;
|
|
@@ -202,7 +197,7 @@ export async function busWait(client, state, session, request, env = process.env
|
|
|
202
197
|
export async function busChanged(client, state, session, paneIds, env = process.env) {
|
|
203
198
|
if (paneIds.length === 0)
|
|
204
199
|
return null;
|
|
205
|
-
const plan = planBus(client, state, env);
|
|
200
|
+
const plan = planBus(client, state, env, session);
|
|
206
201
|
if (!plan.enabled || !plan.url)
|
|
207
202
|
return null;
|
|
208
203
|
const configKey = answeredWith.get(session) ?? plan.configKey;
|
|
@@ -217,14 +212,63 @@ export async function busChanged(client, state, session, paneIds, env = process.
|
|
|
217
212
|
return null;
|
|
218
213
|
return parsed.panes.length === paneIds.length ? parsed : null;
|
|
219
214
|
}
|
|
215
|
+
function installedReply(session, plugin, configKey, ready, panes, extra = {}) {
|
|
216
|
+
return {
|
|
217
|
+
ok: true,
|
|
218
|
+
data: {
|
|
219
|
+
session,
|
|
220
|
+
installed: true,
|
|
221
|
+
plugin,
|
|
222
|
+
url: busPluginUrl(plugin),
|
|
223
|
+
configKey,
|
|
224
|
+
ready,
|
|
225
|
+
panes,
|
|
226
|
+
...extra,
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
async function listBusPluginPanes(client, session, pluginPath) {
|
|
231
|
+
const panes = await client.listPanes(session);
|
|
232
|
+
return panes.filter((pane) => isBusPluginPane(pane, pluginPath));
|
|
233
|
+
}
|
|
234
|
+
/** Close every zswarm-bus plugin pane in this session. Best-effort per pane. */
|
|
235
|
+
export async function closeBusPluginPanes(client, session, pluginPath) {
|
|
236
|
+
const closed = [];
|
|
237
|
+
const panes = await listBusPluginPanes(client, session, pluginPath);
|
|
238
|
+
for (const pane of panes) {
|
|
239
|
+
try {
|
|
240
|
+
await client.closePane({ session, paneId: pane.id });
|
|
241
|
+
closed.push(pane.id);
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
// A pane that vanished between list and close is the desired end state.
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return closed;
|
|
248
|
+
}
|
|
220
249
|
/** Report on the bus, install it, or forget it. */
|
|
221
250
|
export async function busOp(client, state, args, clock, env = process.env, policy) {
|
|
251
|
+
const sessionArg = typeof args.session === "string" ? args.session : undefined;
|
|
222
252
|
if (isTrue(args.clear)) {
|
|
223
|
-
|
|
253
|
+
let session = null;
|
|
254
|
+
let closed = [];
|
|
255
|
+
try {
|
|
256
|
+
session = (await client.resolveSession(sessionArg)).session;
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
session = sessionArg?.trim() || null;
|
|
260
|
+
}
|
|
261
|
+
if (session) {
|
|
262
|
+
closed = await closeBusPluginPanes(client, session, resolveBusPlugin(env) ?? state.readBus(session)?.plugin ?? null);
|
|
263
|
+
state.clearBus(session);
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
state.clearBus();
|
|
267
|
+
}
|
|
224
268
|
resetBusCache();
|
|
225
|
-
return { ok: true, data: { installed: false, cleared: true } };
|
|
269
|
+
return { ok: true, data: { installed: false, cleared: true, closed } };
|
|
226
270
|
}
|
|
227
|
-
const { session } = await client.resolveSession(
|
|
271
|
+
const { session } = await client.resolveSession(sessionArg);
|
|
228
272
|
if (isTrue(args.install)) {
|
|
229
273
|
// Installing opens a pane, which a read-only crew is not allowed to do.
|
|
230
274
|
if (policy?.readOnly) {
|
|
@@ -234,32 +278,31 @@ export async function busOp(client, state, args, clock, env = process.env, polic
|
|
|
234
278
|
if (!plugin) {
|
|
235
279
|
throw new ZellijError("bus_missing", "no plugin wasm found; run pnpm build:plugin or set ZSWARM_BUS_PLUGIN");
|
|
236
280
|
}
|
|
237
|
-
const previous = state.readBus();
|
|
281
|
+
const previous = state.readBus(session);
|
|
238
282
|
const force = isTrue(args.force);
|
|
239
|
-
// Launching again would add a second instance, so an install that is
|
|
240
|
-
// already answering is a no-op unless the caller asked for a fresh one.
|
|
241
283
|
if (previous && !force) {
|
|
242
284
|
const live = await busSnapshot(client, state, session, clock, env);
|
|
243
285
|
if (live?.snapshot.ready) {
|
|
244
|
-
return {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
};
|
|
286
|
+
return installedReply(session, previous.plugin, live.configKey, true, live.snapshot.panes.length, { note: "already installed" });
|
|
287
|
+
}
|
|
288
|
+
const existing = await listBusPluginPanes(client, session, plugin);
|
|
289
|
+
if (existing.length > 0) {
|
|
290
|
+
return installedReply(session, previous.plugin, previous.configKey, false, null, {
|
|
291
|
+
pane: existing[0]?.id ?? null,
|
|
292
|
+
note: "already installed but not answering; approve the permission prompt in the existing pane — do not --force unless you intend to replace it",
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
if (clock.now() - previous.installedAt < INSTALL_IN_FLIGHT_MS) {
|
|
296
|
+
return installedReply(session, previous.plugin, previous.configKey, false, null, {
|
|
297
|
+
note: "install already in flight; wait for the permission prompt rather than launching another pane",
|
|
298
|
+
});
|
|
257
299
|
}
|
|
258
300
|
}
|
|
259
|
-
//
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
301
|
+
// Marker first so a concurrent install sees an in-flight write and
|
|
302
|
+
// stops. Then reap orphans so a reload cannot stack on the old copies.
|
|
303
|
+
const configKey = previous?.configKey ?? DEFAULT_BUS_KEY;
|
|
304
|
+
state.writeBus(session, { plugin, configKey, installedAt: clock.now() });
|
|
305
|
+
const closed = await closeBusPluginPanes(client, session, plugin);
|
|
263
306
|
const url = busPluginUrl(plugin);
|
|
264
307
|
const launched = await client.launchPlugin({
|
|
265
308
|
session,
|
|
@@ -268,27 +311,17 @@ export async function busOp(client, state, args, clock, env = process.env, polic
|
|
|
268
311
|
floating: true,
|
|
269
312
|
skipCache: force,
|
|
270
313
|
});
|
|
271
|
-
state.writeBus({ plugin, configKey, installedAt: clock.now() });
|
|
272
314
|
resetBusCache();
|
|
273
315
|
const probe = await busSnapshot(client, state, session, clock, env);
|
|
274
|
-
return {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
plugin,
|
|
280
|
-
|
|
281
|
-
configKey,
|
|
282
|
-
pane: launched.paneId,
|
|
283
|
-
ready: probe?.snapshot.ready ?? false,
|
|
284
|
-
panes: probe?.snapshot.panes.length ?? null,
|
|
285
|
-
note: probe
|
|
286
|
-
? "bus answering; list and status use it now, and this pane can be closed"
|
|
287
|
-
: "approve the plugin's permission prompt in the new pane, then re-run",
|
|
288
|
-
},
|
|
289
|
-
};
|
|
316
|
+
return installedReply(session, plugin, configKey, probe?.snapshot.ready ?? false, probe?.snapshot.panes.length ?? null, {
|
|
317
|
+
pane: launched.paneId,
|
|
318
|
+
closed,
|
|
319
|
+
note: probe
|
|
320
|
+
? "bus answering; keep this floating pane open — closing it unloads the bus"
|
|
321
|
+
: "approve the plugin's permission prompt in the new pane, then re-run without --install",
|
|
322
|
+
});
|
|
290
323
|
}
|
|
291
|
-
const plan = planBus(client, state, env);
|
|
324
|
+
const plan = planBus(client, state, env, session);
|
|
292
325
|
const probe = plan.enabled
|
|
293
326
|
? await busSnapshot(client, state, session, clock, env)
|
|
294
327
|
: null;
|
package/dist/ops/delivery.d.ts
CHANGED
|
@@ -40,9 +40,9 @@ export declare function withSenderLabel(args: Record<string, unknown>, opts?: {
|
|
|
40
40
|
selfTitle?: string | null;
|
|
41
41
|
}): Record<string, unknown>;
|
|
42
42
|
/**
|
|
43
|
-
* Before forwarding over `ZSWARM_SERVE`,
|
|
44
|
-
*
|
|
45
|
-
*
|
|
43
|
+
* Before forwarding over `ZSWARM_SERVE`, stamp a caller-chosen label. Leaving
|
|
44
|
+
* `from` unset used to let the Zellij-side process fill in its pane title —
|
|
45
|
+
* which is the serve daemon when it runs in a pane, not the remote sender.
|
|
46
46
|
*/
|
|
47
47
|
export declare function attachKnownSender(args: Record<string, unknown>, env: NodeJS.ProcessEnv): Record<string, unknown>;
|
|
48
48
|
export declare function bodyText(client: ZellijClient, args: Record<string, unknown>, body: string): string;
|
package/dist/ops/delivery.js
CHANGED
|
@@ -45,19 +45,16 @@ export function withSenderLabel(args, opts = {}) {
|
|
|
45
45
|
return { ...args, from: senderLabel(args, opts) };
|
|
46
46
|
}
|
|
47
47
|
/**
|
|
48
|
-
* Before forwarding over `ZSWARM_SERVE`,
|
|
49
|
-
*
|
|
50
|
-
*
|
|
48
|
+
* Before forwarding over `ZSWARM_SERVE`, stamp a caller-chosen label. Leaving
|
|
49
|
+
* `from` unset used to let the Zellij-side process fill in its pane title —
|
|
50
|
+
* which is the serve daemon when it runs in a pane, not the remote sender.
|
|
51
51
|
*/
|
|
52
52
|
export function attachKnownSender(args, env) {
|
|
53
53
|
const op = String(args.op ?? "");
|
|
54
54
|
if (op !== "send" && op !== "broadcast")
|
|
55
55
|
return args;
|
|
56
56
|
const sender = resolveSender(args, { env });
|
|
57
|
-
|
|
58
|
-
return { ...args, from: sender.label };
|
|
59
|
-
}
|
|
60
|
-
return args;
|
|
57
|
+
return { ...args, from: sender.label };
|
|
61
58
|
}
|
|
62
59
|
export function bodyText(client, args, body) {
|
|
63
60
|
return isTrue(args.raw) ? body : client.formatPeerMessage(senderLabel(args), body);
|
package/dist/ops/dispatch.js
CHANGED
|
@@ -17,7 +17,7 @@ import { spawnPane } from "./spawn.js";
|
|
|
17
17
|
import { peerStatus } from "./status.js";
|
|
18
18
|
import { tailPane } from "./tail.js";
|
|
19
19
|
import { OP_NAMES } from "../schema.js";
|
|
20
|
-
import { dumpMaxChars, fail, isTrue, isVerbose, optionalString, paneViewBus, paneViewFull, paneViewSlim, truncateDumpText, } from "./util.js";
|
|
20
|
+
import { dumpMaxChars, fail, isTrue, isVerbose, optionalString, paneViewBus, paneViewFull, paneViewSlim, throwIfAborted, truncateDumpText, } from "./util.js";
|
|
21
21
|
import { waitForPane } from "./wait.js";
|
|
22
22
|
import { listPeerWorktrees, removePeerWorktree } from "./worktree.js";
|
|
23
23
|
import { callServe, installServeLogon, serveCallTimeout, uninstallServeLogon, } from "./serve.js";
|
|
@@ -85,10 +85,23 @@ async function resolveTarget(client, args, state, clock, env, policy, op) {
|
|
|
85
85
|
export async function dispatchZswarm(args, injected, deps = {}) {
|
|
86
86
|
const op = String(args.op ?? "");
|
|
87
87
|
const verbose = isVerbose(args);
|
|
88
|
+
const signal = deps.signal;
|
|
89
|
+
throwIfAborted(signal);
|
|
88
90
|
const clock = {
|
|
89
91
|
now: deps.now ?? (() => Date.now()),
|
|
90
92
|
sleep: deps.sleep ??
|
|
91
|
-
((ms) => new Promise((
|
|
93
|
+
((ms) => new Promise((resolve, reject) => {
|
|
94
|
+
if (signal?.aborted) {
|
|
95
|
+
reject(new ZellijError("cancelled", "operation cancelled"));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const timer = setTimeout(resolve, ms);
|
|
99
|
+
const onAbort = () => {
|
|
100
|
+
clearTimeout(timer);
|
|
101
|
+
reject(new ZellijError("cancelled", "operation cancelled"));
|
|
102
|
+
};
|
|
103
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
104
|
+
})),
|
|
92
105
|
};
|
|
93
106
|
// Only the worktree ops need git, so the client is built on demand.
|
|
94
107
|
let gitClient = deps.git ?? null;
|
|
@@ -121,7 +134,7 @@ export async function dispatchZswarm(args, injected, deps = {}) {
|
|
|
121
134
|
if (!injected && env.ZSWARM_SERVE?.trim()) {
|
|
122
135
|
return await callServe(env.ZSWARM_SERVE.trim(), attachKnownSender(args, env), serveCallTimeout(args), env.ZSWARM_SERVE_TOKEN);
|
|
123
136
|
}
|
|
124
|
-
const client = injected ?? createZellijClient({ env: deps.env });
|
|
137
|
+
const client = injected ?? createZellijClient({ env: deps.env, signal });
|
|
125
138
|
switch (op) {
|
|
126
139
|
case "sessions": {
|
|
127
140
|
const sessions = await client.listSessions();
|
|
@@ -278,15 +291,14 @@ export async function dispatchZswarm(args, injected, deps = {}) {
|
|
|
278
291
|
// no engine — and then this is the loop that runs.
|
|
279
292
|
const viaBus = isTrue(args.regex)
|
|
280
293
|
? undefined
|
|
281
|
-
: () => busWait(client, state(), target.session, {
|
|
294
|
+
: (timing) => busWait(client, state(), target.session, {
|
|
282
295
|
pane: target.pane.id,
|
|
283
296
|
for: waitMode(args),
|
|
284
297
|
match: typeof args.match === "string" ? args.match : null,
|
|
285
298
|
ignoreCase: isTrue(args.ignoreCase),
|
|
286
|
-
|
|
287
|
-
timeoutMs: Number(args.timeoutMs ?? 60_000),
|
|
299
|
+
...timing,
|
|
288
300
|
}, deps.env);
|
|
289
|
-
return await waitForPane(client, target, args, clock, viaBus);
|
|
301
|
+
return await waitForPane(client, target, args, clock, viaBus, deps.signal);
|
|
290
302
|
}
|
|
291
303
|
case "status": {
|
|
292
304
|
const { session } = await client.resolveSession(typeof args.session === "string" ? args.session : undefined);
|
package/dist/ops/serve.d.ts
CHANGED
|
@@ -36,6 +36,7 @@ export declare function parseServeTarget(raw: string): {
|
|
|
36
36
|
port: number;
|
|
37
37
|
};
|
|
38
38
|
export declare function callServe(target: string, args: Record<string, unknown>, timeoutMs?: number, token?: string): Promise<OpsResult>;
|
|
39
|
+
export declare function redactServeSecret(command: string, token: string): string;
|
|
39
40
|
export declare function serveLogonCommand(execPath: string, scriptPath: string, listen: string, token?: string): string;
|
|
40
41
|
export declare function installServeLogon(input: {
|
|
41
42
|
listen?: string;
|
package/dist/ops/serve.js
CHANGED
|
@@ -252,6 +252,12 @@ export function callServe(target, args, timeoutMs = 15_000, token) {
|
|
|
252
252
|
});
|
|
253
253
|
});
|
|
254
254
|
}
|
|
255
|
+
export function redactServeSecret(command, token) {
|
|
256
|
+
const secret = token.trim();
|
|
257
|
+
if (!secret)
|
|
258
|
+
return command;
|
|
259
|
+
return command.split(secret).join("***");
|
|
260
|
+
}
|
|
255
261
|
export function serveLogonCommand(execPath, scriptPath, listen, token) {
|
|
256
262
|
const launch = `"${execPath}" "${scriptPath}" serve --listen ${listen}`;
|
|
257
263
|
const secret = token?.trim();
|
|
@@ -323,7 +329,7 @@ export async function installServeLogon(input) {
|
|
|
323
329
|
if (result.code !== 0) {
|
|
324
330
|
throw new ZellijError("zellij_failed", `serve --install failed: ${(result.stderr || result.stdout).trim() || "no output"}`);
|
|
325
331
|
}
|
|
326
|
-
return { task: SERVE_TASK_NAME, listen: label, command };
|
|
332
|
+
return { task: SERVE_TASK_NAME, listen: label, command: redactServeSecret(command, token) };
|
|
327
333
|
}
|
|
328
334
|
export async function uninstallServeLogon(input = {}) {
|
|
329
335
|
if ((input.platform ?? process.platform) !== "win32") {
|
package/dist/ops/types.d.ts
CHANGED
package/dist/ops/util.d.ts
CHANGED
|
@@ -45,4 +45,5 @@ export declare function numberArg(args: Record<string, unknown>, key: string, fa
|
|
|
45
45
|
min: number;
|
|
46
46
|
max: number;
|
|
47
47
|
}): number;
|
|
48
|
+
export declare function throwIfAborted(signal?: AbortSignal): void;
|
|
48
49
|
export declare function dumpMaxChars(args: Record<string, unknown>, fallback?: number): number;
|
package/dist/ops/util.js
CHANGED
|
@@ -73,6 +73,11 @@ export function numberArg(args, key, fallback, limits) {
|
|
|
73
73
|
}
|
|
74
74
|
return Math.min(limits.max, Math.max(limits.min, Math.floor(n)));
|
|
75
75
|
}
|
|
76
|
+
export function throwIfAborted(signal) {
|
|
77
|
+
if (signal?.aborted) {
|
|
78
|
+
throw new ZellijError("cancelled", "operation cancelled");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
76
81
|
export function dumpMaxChars(args, fallback = DEFAULT_DUMP_MAX_CHARS) {
|
|
77
82
|
if (args.max === undefined || args.max === null || args.max === "") {
|
|
78
83
|
return fallback;
|
package/dist/ops/wait.d.ts
CHANGED
|
@@ -5,6 +5,13 @@ import type { Clock, OpsResult } from "./types.js";
|
|
|
5
5
|
export declare function buildMatcher(args: Record<string, unknown>): ((text: string) => boolean) | null;
|
|
6
6
|
/** Held-pipe wait answer, shaped by `parseWaitReply` in `zellij/bus.ts`. */
|
|
7
7
|
export type { BusWait };
|
|
8
|
+
/** Clamped timing the bus path must use — never the raw request numbers. */
|
|
9
|
+
export type WaitBusTiming = {
|
|
10
|
+
idleMs: number;
|
|
11
|
+
timeoutMs: number;
|
|
12
|
+
pollMs: number;
|
|
13
|
+
full: boolean;
|
|
14
|
+
};
|
|
8
15
|
/**
|
|
9
16
|
* Poll a pane's screen until it goes quiet, prints a match, or the deadline
|
|
10
17
|
* passes — so callers stop re-dumping in a loop of their own.
|
|
@@ -16,4 +23,4 @@ export type { BusWait };
|
|
|
16
23
|
export declare function waitForPane(client: ZellijClient, target: {
|
|
17
24
|
session: string;
|
|
18
25
|
pane: ZellijPane;
|
|
19
|
-
}, args: Record<string, unknown>, clock: Clock, waitViaBus?: () => Promise<BusWait | null
|
|
26
|
+
}, args: Record<string, unknown>, clock: Clock, waitViaBus?: (timing: WaitBusTiming) => Promise<BusWait | null>, signal?: AbortSignal): Promise<OpsResult>;
|
package/dist/ops/wait.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ZellijError } from "../errors.js";
|
|
2
|
-
import { DEFAULT_WAIT_MAX_CHARS, dumpMaxChars, isTrue, normalizeScreen, numberArg, optionalString, truncateDumpText, } from "./util.js";
|
|
2
|
+
import { DEFAULT_WAIT_MAX_CHARS, dumpMaxChars, isTrue, normalizeScreen, numberArg, optionalString, throwIfAborted, truncateDumpText, } from "./util.js";
|
|
3
3
|
const WAIT_DEFAULTS = {
|
|
4
4
|
idleMs: 2_000,
|
|
5
5
|
pollMs: 150,
|
|
@@ -56,7 +56,7 @@ function waitResult(reason, ctx) {
|
|
|
56
56
|
* Null (plugin missing or silent) falls back to dump-screen polling. The
|
|
57
57
|
* pipe's own timeout belongs to the transport; this op does not cap it.
|
|
58
58
|
*/
|
|
59
|
-
export async function waitForPane(client, target, args, clock, waitViaBus) {
|
|
59
|
+
export async function waitForPane(client, target, args, clock, waitViaBus, signal) {
|
|
60
60
|
const { session, pane } = target;
|
|
61
61
|
const matcher = buildMatcher(args);
|
|
62
62
|
const requested = optionalString(args.for) ?? (matcher ? "match" : "idle");
|
|
@@ -72,8 +72,11 @@ export async function waitForPane(client, target, args, clock, waitViaBus) {
|
|
|
72
72
|
const pollMs = numberArg(args, "pollMs", WAIT_DEFAULTS.pollMs, WAIT_LIMITS.pollMs);
|
|
73
73
|
const timeoutMs = numberArg(args, "timeoutMs", WAIT_DEFAULTS.timeoutMs, WAIT_LIMITS.timeoutMs);
|
|
74
74
|
const started = clock.now();
|
|
75
|
+
const full = isTrue(args.full);
|
|
75
76
|
if (waitViaBus) {
|
|
76
|
-
|
|
77
|
+
throwIfAborted(signal);
|
|
78
|
+
const reply = await waitViaBus({ idleMs, timeoutMs, pollMs, full });
|
|
79
|
+
throwIfAborted(signal);
|
|
77
80
|
// `gone` is filtered upstream: a pane that vanished mid-wait is reported by
|
|
78
81
|
// the polling path, which has the error message for it.
|
|
79
82
|
if (reply && reply.reason !== "gone") {
|
|
@@ -96,6 +99,7 @@ export async function waitForPane(client, target, args, clock, waitViaBus) {
|
|
|
96
99
|
let polls = 0;
|
|
97
100
|
let changes = 0;
|
|
98
101
|
for (;;) {
|
|
102
|
+
throwIfAborted(signal);
|
|
99
103
|
const dumped = await client.dumpPane({
|
|
100
104
|
session,
|
|
101
105
|
paneId: pane.id,
|
package/dist/schema.js
CHANGED
|
@@ -42,6 +42,9 @@ export const TARGET_OPS = [
|
|
|
42
42
|
"keys",
|
|
43
43
|
"interrupt",
|
|
44
44
|
"close",
|
|
45
|
+
"rename",
|
|
46
|
+
"focus",
|
|
47
|
+
"stack",
|
|
45
48
|
];
|
|
46
49
|
export const PARAMS = [
|
|
47
50
|
{
|
|
@@ -382,7 +385,7 @@ export const PARAMS = [
|
|
|
382
385
|
name: "force",
|
|
383
386
|
type: "boolean",
|
|
384
387
|
flags: ["--force"],
|
|
385
|
-
description: "send/keys: write to a pane whose command has exited; unworktree: remove a busy or dirty worktree; bus:
|
|
388
|
+
description: "send/keys: write to a pane whose command has exited; unworktree: remove a busy or dirty worktree; bus: close orphan bus panes and reload this session's plugin",
|
|
386
389
|
},
|
|
387
390
|
{
|
|
388
391
|
name: "listen",
|
|
@@ -447,7 +450,7 @@ export function cliUsage() {
|
|
|
447
450
|
const value = param.type === "boolean" ? "" : param.type === "number" ? " N" : " VALUE";
|
|
448
451
|
lines.push(` ${param.flags.join(", ").padEnd(28)}${value.trim().padEnd(6)}${param.description}`);
|
|
449
452
|
}
|
|
450
|
-
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
|
|
453
|
+
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", "");
|
|
451
454
|
return lines.join("\n");
|
|
452
455
|
}
|
|
453
456
|
/** Turn argv (without the op) into dispatch args, driven by PARAMS. */
|
package/dist/state.d.ts
CHANGED
|
@@ -19,9 +19,10 @@ export type SignalChannel = {
|
|
|
19
19
|
last: string | null;
|
|
20
20
|
};
|
|
21
21
|
/**
|
|
22
|
-
* Written
|
|
23
|
-
* been answered. Its presence is what lets later runs try the fast
|
|
24
|
-
* every cold `zswarm status` paying for a pipe that was never
|
|
22
|
+
* Written by `bus --install` per Zellij session, after the plugin's permission
|
|
23
|
+
* prompt has been answered. Its presence is what lets later runs try the fast
|
|
24
|
+
* path without every cold `zswarm status` paying for a pipe that was never
|
|
25
|
+
* going to answer.
|
|
25
26
|
*/
|
|
26
27
|
export type BusMarkerRecord = {
|
|
27
28
|
plugin: string;
|
|
@@ -44,9 +45,9 @@ export declare function createStateStore(options?: StateStoreOptions): {
|
|
|
44
45
|
readCursor: (key: string) => string | null;
|
|
45
46
|
writeCursor: (key: string, text: string) => void;
|
|
46
47
|
clearCursor: (key: string) => void;
|
|
47
|
-
readBus: () => BusMarkerRecord | null;
|
|
48
|
-
writeBus: (marker: BusMarkerRecord) => void;
|
|
49
|
-
clearBus: () => void;
|
|
48
|
+
readBus: (session: string) => BusMarkerRecord | null;
|
|
49
|
+
writeBus: (session: string, marker: BusMarkerRecord) => void;
|
|
50
|
+
clearBus: (session?: string) => void;
|
|
50
51
|
reset: () => void;
|
|
51
52
|
};
|
|
52
53
|
export type StateStore = ReturnType<typeof createStateStore>;
|
package/dist/state.js
CHANGED
|
@@ -10,6 +10,8 @@ const BUS_FILE = "bus.json";
|
|
|
10
10
|
/** Keeps the log bounded without needing a rotation daemon. */
|
|
11
11
|
const LOG_TAIL_BYTES = 512 * 1024;
|
|
12
12
|
const LOCK_WAIT_MS = 5_000;
|
|
13
|
+
/** A live pid older than this is treated as a recycle of a crashed holder. */
|
|
14
|
+
const LOCK_STALE_MS = 30_000;
|
|
13
15
|
function sleepSync(ms) {
|
|
14
16
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
15
17
|
}
|
|
@@ -102,16 +104,24 @@ export function createStateStore(options = {}) {
|
|
|
102
104
|
}
|
|
103
105
|
}
|
|
104
106
|
/**
|
|
105
|
-
* Dead pid → steal now.
|
|
106
|
-
*
|
|
107
|
-
*
|
|
107
|
+
* Dead pid → steal now. This process's own leftover (unlink failed, or a
|
|
108
|
+
* non-reentrant re-entry) → steal now. A live pid whose `at` is older than
|
|
109
|
+
* LOCK_STALE_MS is a recycled pid, not a holder still inside fn().
|
|
110
|
+
* Empty leftover from older writers (wx with no owner bytes) → steal once
|
|
111
|
+
* mtime is older than the wait, so an in-flight create is not yanked out
|
|
112
|
+
* from under the holder.
|
|
108
113
|
* Owner is read once: a second read can see a pid that appeared after an
|
|
109
114
|
* empty snapshot and would steal a live lock.
|
|
110
115
|
*/
|
|
111
116
|
function lockIsStale(lockPath) {
|
|
112
117
|
const owner = readLockOwner(lockPath);
|
|
113
|
-
if (owner)
|
|
114
|
-
|
|
118
|
+
if (owner) {
|
|
119
|
+
if (owner.pid === process.pid)
|
|
120
|
+
return true;
|
|
121
|
+
if (!pidAlive(owner.pid))
|
|
122
|
+
return true;
|
|
123
|
+
return Date.now() - owner.at >= LOCK_STALE_MS;
|
|
124
|
+
}
|
|
115
125
|
try {
|
|
116
126
|
return Date.now() - statSync(lockPath).mtimeMs >= LOCK_WAIT_MS;
|
|
117
127
|
}
|
|
@@ -162,7 +172,7 @@ export function createStateStore(options = {}) {
|
|
|
162
172
|
const code = err.code;
|
|
163
173
|
if (!lockBusy(code))
|
|
164
174
|
throw err;
|
|
165
|
-
if (
|
|
175
|
+
if (lockIsStale(lockPath)) {
|
|
166
176
|
unlinkLock(lockPath);
|
|
167
177
|
continue;
|
|
168
178
|
}
|
|
@@ -222,22 +232,72 @@ export function createStateStore(options = {}) {
|
|
|
222
232
|
writeJson(CURSORS_FILE, all);
|
|
223
233
|
});
|
|
224
234
|
}
|
|
225
|
-
function
|
|
226
|
-
|
|
227
|
-
|
|
235
|
+
function asMarker(value) {
|
|
236
|
+
if (!value || typeof value !== "object")
|
|
237
|
+
return null;
|
|
238
|
+
const rec = value;
|
|
239
|
+
if (typeof rec.plugin !== "string" || !rec.plugin)
|
|
240
|
+
return null;
|
|
241
|
+
if (typeof rec.configKey !== "string" || !rec.configKey)
|
|
228
242
|
return null;
|
|
229
|
-
return
|
|
243
|
+
return {
|
|
244
|
+
plugin: rec.plugin,
|
|
245
|
+
configKey: rec.configKey,
|
|
246
|
+
installedAt: typeof rec.installedAt === "number" ? rec.installedAt : 0,
|
|
247
|
+
};
|
|
230
248
|
}
|
|
231
|
-
|
|
232
|
-
|
|
249
|
+
/**
|
|
250
|
+
* Current `{ sessions: { <name>: marker } }` plus the pre-0.1.6 flat file
|
|
251
|
+
* `{ plugin, configKey, installedAt }`, which any session may inherit until
|
|
252
|
+
* the next write namespaces it.
|
|
253
|
+
*/
|
|
254
|
+
function readBusFile() {
|
|
255
|
+
const raw = readJson(BUS_FILE, null);
|
|
256
|
+
if (!raw || typeof raw !== "object")
|
|
257
|
+
return { sessions: {}, legacy: null };
|
|
258
|
+
const sessions = {};
|
|
259
|
+
if (raw.sessions && typeof raw.sessions === "object") {
|
|
260
|
+
for (const [name, marker] of Object.entries(raw.sessions)) {
|
|
261
|
+
const parsed = asMarker(marker);
|
|
262
|
+
if (parsed)
|
|
263
|
+
sessions[name] = parsed;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return { sessions, legacy: asMarker(raw) };
|
|
233
267
|
}
|
|
234
|
-
function
|
|
235
|
-
|
|
236
|
-
|
|
268
|
+
function readBus(session) {
|
|
269
|
+
if (!session)
|
|
270
|
+
return null;
|
|
271
|
+
const { sessions, legacy } = readBusFile();
|
|
272
|
+
return sessions[session] ?? legacy;
|
|
273
|
+
}
|
|
274
|
+
function writeBus(session, marker) {
|
|
275
|
+
const { sessions } = readBusFile();
|
|
276
|
+
sessions[session] = marker;
|
|
277
|
+
writeJson(BUS_FILE, { sessions });
|
|
278
|
+
}
|
|
279
|
+
function clearBus(session) {
|
|
280
|
+
if (!session) {
|
|
281
|
+
try {
|
|
282
|
+
rmSync(join(dir, BUS_FILE), { force: true });
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
// Nothing to forget.
|
|
286
|
+
}
|
|
287
|
+
return;
|
|
237
288
|
}
|
|
238
|
-
|
|
239
|
-
|
|
289
|
+
const { sessions } = readBusFile();
|
|
290
|
+
delete sessions[session];
|
|
291
|
+
if (Object.keys(sessions).length === 0) {
|
|
292
|
+
try {
|
|
293
|
+
rmSync(join(dir, BUS_FILE), { force: true });
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
// Nothing to forget.
|
|
297
|
+
}
|
|
298
|
+
return;
|
|
240
299
|
}
|
|
300
|
+
writeJson(BUS_FILE, { sessions });
|
|
241
301
|
}
|
|
242
302
|
/** Test helper: drop everything this store wrote. */
|
|
243
303
|
function reset() {
|
package/dist/zellij/args.d.ts
CHANGED
|
@@ -47,6 +47,8 @@ export type WaitRequest = {
|
|
|
47
47
|
idleMs?: number;
|
|
48
48
|
timeoutMs?: number;
|
|
49
49
|
pollMs?: number;
|
|
50
|
+
/** Read scrollback above the viewport, matching dump --full. */
|
|
51
|
+
full?: boolean;
|
|
50
52
|
};
|
|
51
53
|
/**
|
|
52
54
|
* `regex` is deliberately absent: the plugin has no regex engine, refuses such
|
package/dist/zellij/args.js
CHANGED
package/dist/zellij/bus.d.ts
CHANGED
|
@@ -67,6 +67,15 @@ export declare function resolveBusPlugin(env?: NodeJS.ProcessEnv): string | null
|
|
|
67
67
|
export declare function busPluginUrl(pluginPath: string): string;
|
|
68
68
|
/** zswarm-bus → zswarm-bus-2 → zswarm-bus-3: a fresh pipe destination each time. */
|
|
69
69
|
export declare function nextConfigKey(key: string): string;
|
|
70
|
+
/**
|
|
71
|
+
* Plugin panes belonging to this bus — never tab-bar / status-bar / room.
|
|
72
|
+
* Titles are the `file:` URL (or the wasm basename) Zellij shows for the pane.
|
|
73
|
+
*/
|
|
74
|
+
export declare function isBusPluginPane(pane: {
|
|
75
|
+
isPlugin?: boolean;
|
|
76
|
+
title?: string;
|
|
77
|
+
command?: string | null;
|
|
78
|
+
}, pluginPath?: string | null): boolean;
|
|
70
79
|
/**
|
|
71
80
|
* Pull the plugin's answer out of the pipe. Zellij interleaves its own notices
|
|
72
81
|
* ("Action CliPipe did not complete within 1s timeout" shows up on successful
|
package/dist/zellij/bus.js
CHANGED
|
@@ -140,6 +140,26 @@ export function nextConfigKey(key) {
|
|
|
140
140
|
return `${key}-2`;
|
|
141
141
|
return `${match[1]}-${Number(match[2]) + 1}`;
|
|
142
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Plugin panes belonging to this bus — never tab-bar / status-bar / room.
|
|
145
|
+
* Titles are the `file:` URL (or the wasm basename) Zellij shows for the pane.
|
|
146
|
+
*/
|
|
147
|
+
export function isBusPluginPane(pane, pluginPath) {
|
|
148
|
+
if (!pane.isPlugin)
|
|
149
|
+
return false;
|
|
150
|
+
const hay = `${pane.title ?? ""}\n${pane.command ?? ""}`
|
|
151
|
+
.replace(/\\/g, "/")
|
|
152
|
+
.toLowerCase();
|
|
153
|
+
if (hay.includes("zswarm-bus") || hay.includes("zswarm-events"))
|
|
154
|
+
return true;
|
|
155
|
+
if (!pluginPath)
|
|
156
|
+
return false;
|
|
157
|
+
const normalized = pluginPath.replace(/\\/g, "/").toLowerCase();
|
|
158
|
+
if (normalized && hay.includes(normalized))
|
|
159
|
+
return true;
|
|
160
|
+
const base = normalized.split("/").pop();
|
|
161
|
+
return Boolean(base && hay.includes(base));
|
|
162
|
+
}
|
|
143
163
|
function toSnapshot(value) {
|
|
144
164
|
if (value.ok !== true || !Array.isArray(value.panes))
|
|
145
165
|
return null;
|
package/dist/zellij/client.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export type ZellijClientOptions = {
|
|
|
8
8
|
zellijPath?: string;
|
|
9
9
|
timeoutMs?: number;
|
|
10
10
|
env?: NodeJS.ProcessEnv;
|
|
11
|
+
signal?: AbortSignal;
|
|
11
12
|
};
|
|
12
13
|
/** Thin, stateless wrapper over the `zellij` binary. */
|
|
13
14
|
export declare function createZellijClient(options?: ZellijClientOptions): {
|
package/dist/zellij/client.js
CHANGED
|
@@ -13,10 +13,11 @@ export function createZellijClient(options = {}) {
|
|
|
13
13
|
const ssh = options.exec ? null : resolveSshTarget(env);
|
|
14
14
|
const zellijPath = options.zellijPath ??
|
|
15
15
|
(ssh ? `ssh://${ssh.host}/${ssh.remoteBin}` : resolveZellijBinary(env));
|
|
16
|
-
const
|
|
16
|
+
const rawExec = options.exec ??
|
|
17
17
|
(ssh
|
|
18
18
|
? createSshExec(ssh, sanitizeZellijEnv(env))
|
|
19
19
|
: defaultExec(zellijPath, env));
|
|
20
|
+
const exec = (args, opts) => rawExec(args, { ...opts, signal: opts.signal ?? options.signal });
|
|
20
21
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
21
22
|
const selfPaneId = resolveSelfPaneId(env);
|
|
22
23
|
async function run(args, label) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zswarm/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "zSwarm Zellij client and shared ops dispatch",
|
|
6
6
|
"exports": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"access": "public"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@zswarm/wasm": "0.1.
|
|
28
|
+
"@zswarm/wasm": "0.1.2"
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|
|
31
31
|
"build": "tsc -p tsconfig.json",
|