@zswarm/core 0.1.0
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/LICENSE +21 -0
- package/README.md +36 -0
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +9 -0
- package/dist/exec.d.ts +32 -0
- package/dist/exec.js +118 -0
- package/dist/git.d.ts +51 -0
- package/dist/git.js +190 -0
- package/dist/harness.d.ts +21 -0
- package/dist/harness.js +98 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +22 -0
- package/dist/keys.d.ts +15 -0
- package/dist/keys.js +168 -0
- package/dist/ops/broadcast.d.ts +19 -0
- package/dist/ops/broadcast.js +113 -0
- package/dist/ops/bus.d.ts +59 -0
- package/dist/ops/bus.js +284 -0
- package/dist/ops/delivery.d.ts +43 -0
- package/dist/ops/delivery.js +191 -0
- package/dist/ops/dispatch.d.ts +4 -0
- package/dist/ops/dispatch.js +331 -0
- package/dist/ops/guards.d.ts +15 -0
- package/dist/ops/guards.js +36 -0
- package/dist/ops/log.d.ts +4 -0
- package/dist/ops/log.js +26 -0
- package/dist/ops/panes.d.ts +13 -0
- package/dist/ops/panes.js +111 -0
- package/dist/ops/review.d.ts +9 -0
- package/dist/ops/review.js +108 -0
- package/dist/ops/signals.d.ts +10 -0
- package/dist/ops/signals.js +86 -0
- package/dist/ops/spawn.d.ts +5 -0
- package/dist/ops/spawn.js +98 -0
- package/dist/ops/status.d.ts +36 -0
- package/dist/ops/status.js +192 -0
- package/dist/ops/tail.d.ts +19 -0
- package/dist/ops/tail.js +59 -0
- package/dist/ops/types.d.ts +26 -0
- package/dist/ops/types.js +1 -0
- package/dist/ops/util.d.ts +48 -0
- package/dist/ops/util.js +85 -0
- package/dist/ops/wait.d.ts +19 -0
- package/dist/ops/wait.js +138 -0
- package/dist/ops/worktree.d.ts +21 -0
- package/dist/ops/worktree.js +144 -0
- package/dist/policy.d.ts +15 -0
- package/dist/policy.js +75 -0
- package/dist/schema.d.ts +26 -0
- package/dist/schema.js +489 -0
- package/dist/state.d.ts +52 -0
- package/dist/state.js +150 -0
- package/dist/zellij/args.d.ts +93 -0
- package/dist/zellij/args.js +261 -0
- package/dist/zellij/binary.d.ts +17 -0
- package/dist/zellij/binary.js +96 -0
- package/dist/zellij/bus.d.ts +91 -0
- package/dist/zellij/bus.js +279 -0
- package/dist/zellij/client.d.ts +162 -0
- package/dist/zellij/client.js +246 -0
- package/dist/zellij/panes.d.ts +21 -0
- package/dist/zellij/panes.js +110 -0
- package/dist/zellij/session.d.ts +14 -0
- package/dist/zellij/session.js +44 -0
- package/dist/zellij/tabs.d.ts +14 -0
- package/dist/zellij/tabs.js +56 -0
- package/package.json +34 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export type PaneDirection = "right" | "left" | "up" | "down";
|
|
2
|
+
export type NewPaneInput = {
|
|
3
|
+
session: string;
|
|
4
|
+
command?: string[];
|
|
5
|
+
cwd?: string | null;
|
|
6
|
+
name?: string | null;
|
|
7
|
+
direction?: PaneDirection | null;
|
|
8
|
+
floating?: boolean;
|
|
9
|
+
closeOnExit?: boolean;
|
|
10
|
+
tabId?: number | null;
|
|
11
|
+
width?: string | null;
|
|
12
|
+
height?: string | null;
|
|
13
|
+
};
|
|
14
|
+
export type NewTabInput = {
|
|
15
|
+
session: string;
|
|
16
|
+
command?: string[];
|
|
17
|
+
cwd?: string | null;
|
|
18
|
+
name?: string | null;
|
|
19
|
+
layout?: string | null;
|
|
20
|
+
closeOnExit?: boolean;
|
|
21
|
+
};
|
|
22
|
+
export type PipeInput = {
|
|
23
|
+
session: string;
|
|
24
|
+
/** `file:` url of the plugin wasm. */
|
|
25
|
+
url: string;
|
|
26
|
+
/** Value of the `instance` configuration key — see buildPipeArgs. */
|
|
27
|
+
configKey: string;
|
|
28
|
+
name?: string;
|
|
29
|
+
payload: string;
|
|
30
|
+
};
|
|
31
|
+
/** JSON pipe payload for a multi-pane scrollback read. Keys match the frozen contract. */
|
|
32
|
+
export type ScrollbackRequest = {
|
|
33
|
+
panes: string[];
|
|
34
|
+
full?: boolean;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Emit `{op, panes, full}` in that order. Callers must not hand-roll this JSON —
|
|
38
|
+
* a payload that does not start with `{` is treated as a bare-word bus command.
|
|
39
|
+
*/
|
|
40
|
+
export declare function scrollbackPayload(req: ScrollbackRequest): string;
|
|
41
|
+
/** A wait the plugin serves by holding the pipe until it resolves. */
|
|
42
|
+
export type WaitRequest = {
|
|
43
|
+
pane: string;
|
|
44
|
+
for: "idle" | "match" | "either";
|
|
45
|
+
match?: string | null;
|
|
46
|
+
ignoreCase?: boolean;
|
|
47
|
+
idleMs?: number;
|
|
48
|
+
timeoutMs?: number;
|
|
49
|
+
pollMs?: number;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* `regex` is deliberately absent: the plugin has no regex engine, refuses such
|
|
53
|
+
* requests, and the caller falls back to polling where its own engine decides.
|
|
54
|
+
*/
|
|
55
|
+
export declare function waitPayload(req: WaitRequest): string;
|
|
56
|
+
/** Screens plus "did this move since you last asked", so status needs no gap. */
|
|
57
|
+
export declare function changedPayload(panes: string[]): string;
|
|
58
|
+
export type LaunchPluginInput = {
|
|
59
|
+
session: string;
|
|
60
|
+
url: string;
|
|
61
|
+
configKey: string;
|
|
62
|
+
floating?: boolean;
|
|
63
|
+
skipCache?: boolean;
|
|
64
|
+
};
|
|
65
|
+
export declare function sessionPrefix(session: string | undefined): string[];
|
|
66
|
+
/**
|
|
67
|
+
* Ask the event-bus plugin a question over a CLI pipe.
|
|
68
|
+
*
|
|
69
|
+
* The configuration is not decoration: Zellij treats the same plugin under a
|
|
70
|
+
* different configuration as a different pipe destination, so a distinct
|
|
71
|
+
* `instance` key is what stops a dead instance from swallowing the message.
|
|
72
|
+
*/
|
|
73
|
+
export declare function buildPipeArgs(input: PipeInput): string[];
|
|
74
|
+
/**
|
|
75
|
+
* Load the plugin in a visible pane. A pipe-launched plugin has no pane, so its
|
|
76
|
+
* permission prompt cannot be answered — the first load has to be this one.
|
|
77
|
+
*/
|
|
78
|
+
export declare function buildLaunchPluginArgs(input: LaunchPluginInput): string[];
|
|
79
|
+
export declare function buildListPanesArgs(session: string): string[];
|
|
80
|
+
export declare function buildPasteArgs(session: string, paneId: string, text: string): string[];
|
|
81
|
+
export declare function buildSendEnterArgs(session: string, paneId: string): string[];
|
|
82
|
+
export declare function buildDumpArgs(session: string, paneId: string, full?: boolean): string[];
|
|
83
|
+
export declare function buildSendKeysArgs(session: string, paneId: string, keys: string[]): string[];
|
|
84
|
+
export declare function buildWriteCharsArgs(session: string, paneId: string, chars: string): string[];
|
|
85
|
+
export declare function buildRenamePaneArgs(session: string, paneId: string, name: string): string[];
|
|
86
|
+
export declare function buildRenameTabArgs(session: string, tabId: number, name: string): string[];
|
|
87
|
+
export declare function buildFocusPaneArgs(session: string, paneId: string): string[];
|
|
88
|
+
export declare function buildListTabsArgs(session: string): string[];
|
|
89
|
+
export declare function buildDumpLayoutArgs(session: string): string[];
|
|
90
|
+
export declare function buildStackPanesArgs(session: string, paneIds: string[]): string[];
|
|
91
|
+
export declare function buildClosePaneArgs(session: string, paneId: string): string[];
|
|
92
|
+
export declare function buildNewPaneArgs(input: NewPaneInput): string[];
|
|
93
|
+
export declare function buildNewTabArgs(input: NewTabInput): string[];
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { ZellijError } from "../errors.js";
|
|
2
|
+
import { BUS_PIPE_NAME } from "./bus.js";
|
|
3
|
+
import { normalizePaneId } from "./panes.js";
|
|
4
|
+
/**
|
|
5
|
+
* Emit `{op, panes, full}` in that order. Callers must not hand-roll this JSON —
|
|
6
|
+
* a payload that does not start with `{` is treated as a bare-word bus command.
|
|
7
|
+
*/
|
|
8
|
+
export function scrollbackPayload(req) {
|
|
9
|
+
if (req.panes.length === 0) {
|
|
10
|
+
throw new ZellijError("bad_arg", "scrollback panes must be a non-empty list");
|
|
11
|
+
}
|
|
12
|
+
return JSON.stringify({
|
|
13
|
+
op: "scrollback",
|
|
14
|
+
panes: req.panes,
|
|
15
|
+
full: req.full === true,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* `regex` is deliberately absent: the plugin has no regex engine, refuses such
|
|
20
|
+
* requests, and the caller falls back to polling where its own engine decides.
|
|
21
|
+
*/
|
|
22
|
+
export function waitPayload(req) {
|
|
23
|
+
if (!req.pane.trim()) {
|
|
24
|
+
throw new ZellijError("bad_arg", "wait needs a pane");
|
|
25
|
+
}
|
|
26
|
+
if (req.for !== "idle" && !req.match) {
|
|
27
|
+
throw new ZellijError("bad_arg", "match waits need a needle");
|
|
28
|
+
}
|
|
29
|
+
return JSON.stringify({
|
|
30
|
+
op: "wait",
|
|
31
|
+
pane: req.pane,
|
|
32
|
+
for: req.for,
|
|
33
|
+
match: req.match ?? null,
|
|
34
|
+
ignoreCase: req.ignoreCase === true,
|
|
35
|
+
idleMs: req.idleMs ?? 2000,
|
|
36
|
+
timeoutMs: req.timeoutMs ?? 60000,
|
|
37
|
+
pollMs: req.pollMs ?? 50,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/** Screens plus "did this move since you last asked", so status needs no gap. */
|
|
41
|
+
export function changedPayload(panes) {
|
|
42
|
+
if (panes.length === 0) {
|
|
43
|
+
throw new ZellijError("bad_arg", "changed needs a non-empty pane list");
|
|
44
|
+
}
|
|
45
|
+
return JSON.stringify({ op: "changed", panes });
|
|
46
|
+
}
|
|
47
|
+
export function sessionPrefix(session) {
|
|
48
|
+
return session ? ["--session", session] : [];
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Ask the event-bus plugin a question over a CLI pipe.
|
|
52
|
+
*
|
|
53
|
+
* The configuration is not decoration: Zellij treats the same plugin under a
|
|
54
|
+
* different configuration as a different pipe destination, so a distinct
|
|
55
|
+
* `instance` key is what stops a dead instance from swallowing the message.
|
|
56
|
+
*/
|
|
57
|
+
export function buildPipeArgs(input) {
|
|
58
|
+
return [
|
|
59
|
+
...sessionPrefix(input.session),
|
|
60
|
+
"pipe",
|
|
61
|
+
"--plugin",
|
|
62
|
+
input.url,
|
|
63
|
+
"--plugin-configuration",
|
|
64
|
+
`instance=${input.configKey}`,
|
|
65
|
+
"--name",
|
|
66
|
+
input.name ?? BUS_PIPE_NAME,
|
|
67
|
+
"--",
|
|
68
|
+
input.payload,
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Load the plugin in a visible pane. A pipe-launched plugin has no pane, so its
|
|
73
|
+
* permission prompt cannot be answered — the first load has to be this one.
|
|
74
|
+
*/
|
|
75
|
+
export function buildLaunchPluginArgs(input) {
|
|
76
|
+
const args = [
|
|
77
|
+
...sessionPrefix(input.session),
|
|
78
|
+
"action",
|
|
79
|
+
"launch-or-focus-plugin",
|
|
80
|
+
"--configuration",
|
|
81
|
+
`instance=${input.configKey}`,
|
|
82
|
+
];
|
|
83
|
+
if (input.floating)
|
|
84
|
+
args.push("--floating");
|
|
85
|
+
if (input.skipCache)
|
|
86
|
+
args.push("--skip-plugin-cache");
|
|
87
|
+
args.push(input.url);
|
|
88
|
+
return args;
|
|
89
|
+
}
|
|
90
|
+
export function buildListPanesArgs(session) {
|
|
91
|
+
return [
|
|
92
|
+
...sessionPrefix(session),
|
|
93
|
+
"action",
|
|
94
|
+
"list-panes",
|
|
95
|
+
"--json",
|
|
96
|
+
"--command",
|
|
97
|
+
"--state",
|
|
98
|
+
"--tab",
|
|
99
|
+
];
|
|
100
|
+
}
|
|
101
|
+
export function buildPasteArgs(session, paneId, text) {
|
|
102
|
+
return [
|
|
103
|
+
...sessionPrefix(session),
|
|
104
|
+
"action",
|
|
105
|
+
"paste",
|
|
106
|
+
"--pane-id",
|
|
107
|
+
normalizePaneId(paneId),
|
|
108
|
+
// Without the end-of-options marker, a body starting with `-` is parsed
|
|
109
|
+
// as a zellij flag instead of being typed.
|
|
110
|
+
"--",
|
|
111
|
+
text,
|
|
112
|
+
];
|
|
113
|
+
}
|
|
114
|
+
export function buildSendEnterArgs(session, paneId) {
|
|
115
|
+
return [
|
|
116
|
+
...sessionPrefix(session),
|
|
117
|
+
"action",
|
|
118
|
+
"send-keys",
|
|
119
|
+
"--pane-id",
|
|
120
|
+
normalizePaneId(paneId),
|
|
121
|
+
"Enter",
|
|
122
|
+
];
|
|
123
|
+
}
|
|
124
|
+
export function buildDumpArgs(session, paneId, full = false) {
|
|
125
|
+
const args = [
|
|
126
|
+
...sessionPrefix(session),
|
|
127
|
+
"action",
|
|
128
|
+
"dump-screen",
|
|
129
|
+
"--pane-id",
|
|
130
|
+
normalizePaneId(paneId),
|
|
131
|
+
];
|
|
132
|
+
if (full)
|
|
133
|
+
args.push("--full");
|
|
134
|
+
return args;
|
|
135
|
+
}
|
|
136
|
+
export function buildSendKeysArgs(session, paneId, keys) {
|
|
137
|
+
for (const key of keys) {
|
|
138
|
+
if (key.length > 1 && key.startsWith("-")) {
|
|
139
|
+
throw new ZellijError("bad_key", `key "${key}" looks like a flag`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return [
|
|
143
|
+
...sessionPrefix(session),
|
|
144
|
+
"action",
|
|
145
|
+
"send-keys",
|
|
146
|
+
"--pane-id",
|
|
147
|
+
normalizePaneId(paneId),
|
|
148
|
+
...keys,
|
|
149
|
+
];
|
|
150
|
+
}
|
|
151
|
+
export function buildWriteCharsArgs(session, paneId, chars) {
|
|
152
|
+
return [
|
|
153
|
+
...sessionPrefix(session),
|
|
154
|
+
"action",
|
|
155
|
+
"write-chars",
|
|
156
|
+
"--pane-id",
|
|
157
|
+
normalizePaneId(paneId),
|
|
158
|
+
"--",
|
|
159
|
+
chars,
|
|
160
|
+
];
|
|
161
|
+
}
|
|
162
|
+
export function buildRenamePaneArgs(session, paneId, name) {
|
|
163
|
+
return [
|
|
164
|
+
...sessionPrefix(session),
|
|
165
|
+
"action",
|
|
166
|
+
"rename-pane",
|
|
167
|
+
"--pane-id",
|
|
168
|
+
normalizePaneId(paneId),
|
|
169
|
+
"--",
|
|
170
|
+
name,
|
|
171
|
+
];
|
|
172
|
+
}
|
|
173
|
+
export function buildRenameTabArgs(session, tabId, name) {
|
|
174
|
+
return [
|
|
175
|
+
...sessionPrefix(session),
|
|
176
|
+
"action",
|
|
177
|
+
"rename-tab-by-id",
|
|
178
|
+
String(tabId),
|
|
179
|
+
"--",
|
|
180
|
+
name,
|
|
181
|
+
];
|
|
182
|
+
}
|
|
183
|
+
export function buildFocusPaneArgs(session, paneId) {
|
|
184
|
+
return [
|
|
185
|
+
...sessionPrefix(session),
|
|
186
|
+
"action",
|
|
187
|
+
"focus-pane-id",
|
|
188
|
+
normalizePaneId(paneId),
|
|
189
|
+
];
|
|
190
|
+
}
|
|
191
|
+
export function buildListTabsArgs(session) {
|
|
192
|
+
return [
|
|
193
|
+
...sessionPrefix(session),
|
|
194
|
+
"action",
|
|
195
|
+
"list-tabs",
|
|
196
|
+
"--json",
|
|
197
|
+
"--panes",
|
|
198
|
+
"--state",
|
|
199
|
+
"--layout",
|
|
200
|
+
];
|
|
201
|
+
}
|
|
202
|
+
export function buildDumpLayoutArgs(session) {
|
|
203
|
+
return [...sessionPrefix(session), "action", "dump-layout"];
|
|
204
|
+
}
|
|
205
|
+
export function buildStackPanesArgs(session, paneIds) {
|
|
206
|
+
return [
|
|
207
|
+
...sessionPrefix(session),
|
|
208
|
+
"action",
|
|
209
|
+
"stack-panes",
|
|
210
|
+
"--",
|
|
211
|
+
...paneIds.map((id) => normalizePaneId(id)),
|
|
212
|
+
];
|
|
213
|
+
}
|
|
214
|
+
export function buildClosePaneArgs(session, paneId) {
|
|
215
|
+
return [
|
|
216
|
+
...sessionPrefix(session),
|
|
217
|
+
"action",
|
|
218
|
+
"close-pane",
|
|
219
|
+
"--pane-id",
|
|
220
|
+
normalizePaneId(paneId),
|
|
221
|
+
];
|
|
222
|
+
}
|
|
223
|
+
export function buildNewPaneArgs(input) {
|
|
224
|
+
const args = [...sessionPrefix(input.session), "action", "new-pane"];
|
|
225
|
+
if (input.cwd)
|
|
226
|
+
args.push("--cwd", input.cwd);
|
|
227
|
+
if (input.name)
|
|
228
|
+
args.push("--name", input.name);
|
|
229
|
+
if (input.direction)
|
|
230
|
+
args.push("--direction", input.direction);
|
|
231
|
+
if (input.floating)
|
|
232
|
+
args.push("--floating");
|
|
233
|
+
if (input.width)
|
|
234
|
+
args.push("--width", input.width);
|
|
235
|
+
if (input.height)
|
|
236
|
+
args.push("--height", input.height);
|
|
237
|
+
if (typeof input.tabId === "number") {
|
|
238
|
+
args.push("--tab-id", String(input.tabId));
|
|
239
|
+
}
|
|
240
|
+
if (input.closeOnExit)
|
|
241
|
+
args.push("--close-on-exit");
|
|
242
|
+
if (input.command && input.command.length > 0) {
|
|
243
|
+
args.push("--", ...input.command);
|
|
244
|
+
}
|
|
245
|
+
return args;
|
|
246
|
+
}
|
|
247
|
+
export function buildNewTabArgs(input) {
|
|
248
|
+
const args = [...sessionPrefix(input.session), "action", "new-tab"];
|
|
249
|
+
if (input.cwd)
|
|
250
|
+
args.push("--cwd", input.cwd);
|
|
251
|
+
if (input.name)
|
|
252
|
+
args.push("--name", input.name);
|
|
253
|
+
if (input.layout)
|
|
254
|
+
args.push("--layout", input.layout);
|
|
255
|
+
if (input.closeOnExit)
|
|
256
|
+
args.push("--close-on-exit");
|
|
257
|
+
if (input.command && input.command.length > 0) {
|
|
258
|
+
args.push("--", ...input.command);
|
|
259
|
+
}
|
|
260
|
+
return args;
|
|
261
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createSshExec, NOT_FOUND_EXIT, type ExecFn, type ExecResult, type SshTarget } from "../exec.js";
|
|
2
|
+
export { createSshExec, type SshTarget };
|
|
3
|
+
export type ZellijExecResult = ExecResult;
|
|
4
|
+
export type ZellijExecFn = ExecFn;
|
|
5
|
+
export declare const DEFAULT_TIMEOUT_MS = 15000;
|
|
6
|
+
export { NOT_FOUND_EXIT };
|
|
7
|
+
/** Expand a leading `~/` or `~\` using USERPROFILE/HOME. */
|
|
8
|
+
export declare function expandHomePath(input: string, env?: NodeJS.ProcessEnv): string;
|
|
9
|
+
export declare function resolveZellijBinary(env?: NodeJS.ProcessEnv): string;
|
|
10
|
+
export declare function sanitizeZellijEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
11
|
+
/**
|
|
12
|
+
* Remote crew: `ZSWARM_SSH=user@host` routes every zellij call over ssh.
|
|
13
|
+
* `ZSWARM_SSH_OPTS` is split on whitespace; `BatchMode` keeps it non-interactive.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveSshTarget(env?: NodeJS.ProcessEnv): SshTarget | null;
|
|
16
|
+
/** Runner bound to the resolved zellij binary and a cleaned environment. */
|
|
17
|
+
export declare function defaultExec(zellijPath: string, env: NodeJS.ProcessEnv): ZellijExecFn;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { delimiter, join } from "node:path";
|
|
4
|
+
import { createExec, createSshExec, NOT_FOUND_EXIT, } from "../exec.js";
|
|
5
|
+
export { createSshExec };
|
|
6
|
+
export const DEFAULT_TIMEOUT_MS = 15_000;
|
|
7
|
+
export { NOT_FOUND_EXIT };
|
|
8
|
+
/** Expand a leading `~/` or `~\` using USERPROFILE/HOME. */
|
|
9
|
+
export function expandHomePath(input, env = process.env) {
|
|
10
|
+
const trimmed = input.trim();
|
|
11
|
+
if (!trimmed.startsWith("~/") && !trimmed.startsWith("~\\"))
|
|
12
|
+
return trimmed;
|
|
13
|
+
const home = env.USERPROFILE || env.HOME || homedir();
|
|
14
|
+
if (!home)
|
|
15
|
+
return trimmed;
|
|
16
|
+
return join(home, trimmed.slice(2));
|
|
17
|
+
}
|
|
18
|
+
export function resolveZellijBinary(env = process.env) {
|
|
19
|
+
const fromEnv = expandHomePath((env.ZSWARM_BIN ?? env.ZSWARM_PATH ?? env.ZELLIJ_BIN ?? "")
|
|
20
|
+
.trim()
|
|
21
|
+
.replace(/^['"]|['"]$/g, ""), env);
|
|
22
|
+
if (fromEnv && existsSync(fromEnv)) {
|
|
23
|
+
if (/\.cmd$/i.test(fromEnv)) {
|
|
24
|
+
const exe = fromEnv.replace(/\.cmd$/i, ".exe");
|
|
25
|
+
if (existsSync(exe))
|
|
26
|
+
return exe;
|
|
27
|
+
const wingetExe = join(env.LOCALAPPDATA ||
|
|
28
|
+
join(env.USERPROFILE || env.HOME || homedir(), "AppData", "Local"), "Zellij", "zellij.exe");
|
|
29
|
+
if (existsSync(wingetExe))
|
|
30
|
+
return wingetExe;
|
|
31
|
+
}
|
|
32
|
+
return fromEnv;
|
|
33
|
+
}
|
|
34
|
+
const home = env.USERPROFILE || env.HOME || homedir();
|
|
35
|
+
const localAppData = env.LOCALAPPDATA || (home ? join(home, "AppData", "Local") : "");
|
|
36
|
+
const candidates = [
|
|
37
|
+
localAppData ? join(localAppData, "Zellij", "zellij.exe") : "",
|
|
38
|
+
home ? join(home, ".local", "bin", "zellij") : "",
|
|
39
|
+
home ? join(home, ".cargo", "bin", "zellij") : "",
|
|
40
|
+
home ? join(home, ".cargo", "bin", "zellij.exe") : "",
|
|
41
|
+
"/usr/local/bin/zellij",
|
|
42
|
+
"/opt/homebrew/bin/zellij",
|
|
43
|
+
...(env.PATH ?? env.Path ?? "")
|
|
44
|
+
.split(delimiter)
|
|
45
|
+
.filter(Boolean)
|
|
46
|
+
.flatMap((dir) => process.platform === "win32"
|
|
47
|
+
? [join(dir, "zellij.exe"), join(dir, "zellij")]
|
|
48
|
+
: [join(dir, "zellij")]),
|
|
49
|
+
].filter(Boolean);
|
|
50
|
+
for (const cand of candidates) {
|
|
51
|
+
if (existsSync(cand))
|
|
52
|
+
return cand;
|
|
53
|
+
}
|
|
54
|
+
return process.platform === "win32" ? "zellij.exe" : "zellij";
|
|
55
|
+
}
|
|
56
|
+
export function sanitizeZellijEnv(env) {
|
|
57
|
+
const out = {};
|
|
58
|
+
for (const key of Object.keys(env)) {
|
|
59
|
+
const value = env[key];
|
|
60
|
+
if (typeof value === "string")
|
|
61
|
+
out[key] = value;
|
|
62
|
+
}
|
|
63
|
+
if (process.platform === "win32") {
|
|
64
|
+
if (!out.SystemRoot && !out.SYSTEMROOT) {
|
|
65
|
+
out.SystemRoot = process.env.SystemRoot || "C:\\Windows";
|
|
66
|
+
}
|
|
67
|
+
if (out.PATH && !out.Path)
|
|
68
|
+
out.Path = out.PATH;
|
|
69
|
+
if (out.Path && !out.PATH)
|
|
70
|
+
out.PATH = out.Path;
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Remote crew: `ZSWARM_SSH=user@host` routes every zellij call over ssh.
|
|
76
|
+
* `ZSWARM_SSH_OPTS` is split on whitespace; `BatchMode` keeps it non-interactive.
|
|
77
|
+
*/
|
|
78
|
+
export function resolveSshTarget(env = process.env) {
|
|
79
|
+
const host = env.ZSWARM_SSH?.trim();
|
|
80
|
+
if (!host)
|
|
81
|
+
return null;
|
|
82
|
+
const options = (env.ZSWARM_SSH_OPTS ?? "").trim().split(/\s+/).filter(Boolean);
|
|
83
|
+
if (!options.some((o) => o.startsWith("BatchMode"))) {
|
|
84
|
+
options.unshift("-o", "BatchMode=yes");
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
ssh: env.ZSWARM_SSH_BIN?.trim() || "ssh",
|
|
88
|
+
host,
|
|
89
|
+
remoteBin: env.ZSWARM_REMOTE_BIN?.trim() || "zellij",
|
|
90
|
+
options,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/** Runner bound to the resolved zellij binary and a cleaned environment. */
|
|
94
|
+
export function defaultExec(zellijPath, env) {
|
|
95
|
+
return createExec(zellijPath, sanitizeZellijEnv(env));
|
|
96
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { ZellijPane } from "./panes.js";
|
|
2
|
+
/**
|
|
3
|
+
* The event-bus side of zswarm: Zellij pushes pane and tab state into a WASM
|
|
4
|
+
* plugin as it changes, and one CLI pipe reads it back out of memory. Parsing
|
|
5
|
+
* and path resolution live here; the process calls live in the client, and the
|
|
6
|
+
* fallback policy lives in `ops/bus.ts`.
|
|
7
|
+
*/
|
|
8
|
+
export declare const BUS_PIPE_NAME = "zswarm";
|
|
9
|
+
export declare const DEFAULT_BUS_KEY = "zswarm-bus";
|
|
10
|
+
/** A stalled instance answers nothing, so the pipe must not inherit the long timeout. */
|
|
11
|
+
export declare const DEFAULT_BUS_TIMEOUT_MS = 2500;
|
|
12
|
+
export type BusPane = {
|
|
13
|
+
id: string;
|
|
14
|
+
title: string;
|
|
15
|
+
exited: boolean;
|
|
16
|
+
focused: boolean;
|
|
17
|
+
command: string | null;
|
|
18
|
+
tab: number;
|
|
19
|
+
};
|
|
20
|
+
export type BusSnapshot = {
|
|
21
|
+
/** False until Zellij has pushed a manifest — a cold instance, not an empty session. */
|
|
22
|
+
ready: boolean;
|
|
23
|
+
paneUpdates: number;
|
|
24
|
+
tabUpdates: number;
|
|
25
|
+
tabs: string[];
|
|
26
|
+
panes: BusPane[];
|
|
27
|
+
};
|
|
28
|
+
export type BusMarker = {
|
|
29
|
+
plugin: string;
|
|
30
|
+
configKey: string;
|
|
31
|
+
installedAt: number;
|
|
32
|
+
};
|
|
33
|
+
export type BusScrollbackPane = {
|
|
34
|
+
id: string;
|
|
35
|
+
viewport: string[];
|
|
36
|
+
above: string[];
|
|
37
|
+
below: string[];
|
|
38
|
+
};
|
|
39
|
+
export type BusScrollback = {
|
|
40
|
+
ready: boolean;
|
|
41
|
+
panes: BusScrollbackPane[];
|
|
42
|
+
missing: string[];
|
|
43
|
+
};
|
|
44
|
+
export type BusWait = {
|
|
45
|
+
reason: "match" | "idle" | "timeout" | "gone";
|
|
46
|
+
pane: string;
|
|
47
|
+
screen: string;
|
|
48
|
+
};
|
|
49
|
+
export type BusChangedPane = {
|
|
50
|
+
id: string;
|
|
51
|
+
screen: string;
|
|
52
|
+
changed: boolean;
|
|
53
|
+
/** True the first time the plugin has been asked about this pane. */
|
|
54
|
+
first: boolean;
|
|
55
|
+
};
|
|
56
|
+
export type BusChanged = {
|
|
57
|
+
ready: boolean;
|
|
58
|
+
panes: BusChangedPane[];
|
|
59
|
+
missing: string[];
|
|
60
|
+
};
|
|
61
|
+
/** A held wait answers once, late. Same scan as every other bus reply. */
|
|
62
|
+
export declare function parseWaitReply(stdout: string): BusWait | null;
|
|
63
|
+
export declare function parseChangedReply(stdout: string): BusChanged | null;
|
|
64
|
+
/** Absolute path of the plugin wasm, or null when there is nothing to load. */
|
|
65
|
+
export declare function resolveBusPlugin(env?: NodeJS.ProcessEnv): string | null;
|
|
66
|
+
/** Zellij wants a `file:` url with forward slashes, including on Windows. */
|
|
67
|
+
export declare function busPluginUrl(pluginPath: string): string;
|
|
68
|
+
/** zswarm-bus → zswarm-bus-2 → zswarm-bus-3: a fresh pipe destination each time. */
|
|
69
|
+
export declare function nextConfigKey(key: string): string;
|
|
70
|
+
/**
|
|
71
|
+
* Pull the plugin's answer out of the pipe. Zellij interleaves its own notices
|
|
72
|
+
* ("Action CliPipe did not complete within 1s timeout" shows up on successful
|
|
73
|
+
* runs too), so scan for the first line that is our JSON rather than parsing
|
|
74
|
+
* the whole of stdout.
|
|
75
|
+
*/
|
|
76
|
+
export declare function parseBusReply(stdout: string): BusSnapshot | null;
|
|
77
|
+
/**
|
|
78
|
+
* Same scan as parseBusReply: Zellij prints "Action CliPipe did not complete
|
|
79
|
+
* within 1s timeout" on successful pipes, so the JSON is never the whole of
|
|
80
|
+
* stdout. First line with ok===true and a panes array wins; anything else is
|
|
81
|
+
* no reply and the caller falls back.
|
|
82
|
+
*/
|
|
83
|
+
export declare function parseScrollbackReply(stdout: string): BusScrollback | null;
|
|
84
|
+
/** Rebuild dump-screen-shaped text. Byte-identity with dump-screen is unproven. */
|
|
85
|
+
export declare function scrollbackToScreen(pane: BusScrollbackPane): string;
|
|
86
|
+
/**
|
|
87
|
+
* Shape the snapshot like `list-panes` output. `cwd` and `floating` are not in
|
|
88
|
+
* the manifest Zellij pushes, so they read as unknown here — callers that need
|
|
89
|
+
* them (anything verbose) must take the polling path instead.
|
|
90
|
+
*/
|
|
91
|
+
export declare function busToPanes(snapshot: BusSnapshot): ZellijPane[];
|