@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
package/dist/schema.js
ADDED
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
import { ZellijError } from "./errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* One description of the zswarm surface. The MCP tool schema, the CLI flags,
|
|
4
|
+
* and the CLI help text are all generated from it, so they cannot drift apart.
|
|
5
|
+
*/
|
|
6
|
+
export const OP_NAMES = [
|
|
7
|
+
"list",
|
|
8
|
+
"sessions",
|
|
9
|
+
"send",
|
|
10
|
+
"broadcast",
|
|
11
|
+
"dump",
|
|
12
|
+
"tail",
|
|
13
|
+
"wait",
|
|
14
|
+
"status",
|
|
15
|
+
"keys",
|
|
16
|
+
"interrupt",
|
|
17
|
+
"spawn",
|
|
18
|
+
"close",
|
|
19
|
+
"worktrees",
|
|
20
|
+
"unworktree",
|
|
21
|
+
"signal",
|
|
22
|
+
"signals",
|
|
23
|
+
"await",
|
|
24
|
+
"log",
|
|
25
|
+
"rename",
|
|
26
|
+
"focus",
|
|
27
|
+
"tabs",
|
|
28
|
+
"layout",
|
|
29
|
+
"stack",
|
|
30
|
+
"diff",
|
|
31
|
+
"checkpoint",
|
|
32
|
+
"bus",
|
|
33
|
+
];
|
|
34
|
+
/** Ops that address an existing pane through `to`. */
|
|
35
|
+
export const TARGET_OPS = [
|
|
36
|
+
"send",
|
|
37
|
+
"dump",
|
|
38
|
+
"tail",
|
|
39
|
+
"wait",
|
|
40
|
+
"status",
|
|
41
|
+
"keys",
|
|
42
|
+
"interrupt",
|
|
43
|
+
"close",
|
|
44
|
+
];
|
|
45
|
+
export const PARAMS = [
|
|
46
|
+
{
|
|
47
|
+
name: "session",
|
|
48
|
+
type: "string",
|
|
49
|
+
flags: ["--session", "-s"],
|
|
50
|
+
description: "Zellij session name (optional if sole live session or ZSWARM_SESSION / ZELLIJ_SESSION_NAME)",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "to",
|
|
54
|
+
type: "string",
|
|
55
|
+
flags: ["--to", "-t"],
|
|
56
|
+
description: "target pane: id (3 / terminal_3) or unique title/command; broadcast takes a comma list; log filters by it",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "all",
|
|
60
|
+
type: "boolean",
|
|
61
|
+
flags: ["--all", "-a"],
|
|
62
|
+
description: "broadcast: every terminal pane in the session",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: "group",
|
|
66
|
+
type: "string",
|
|
67
|
+
flags: ["--group", "-g"],
|
|
68
|
+
description: "broadcast: narrow the selection to panes whose title or command contains this",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "channel",
|
|
72
|
+
type: "string",
|
|
73
|
+
flags: ["--channel"],
|
|
74
|
+
description: "signal/await: channel name",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: "payload",
|
|
78
|
+
type: "string",
|
|
79
|
+
flags: ["--payload"],
|
|
80
|
+
description: "signal: short note stored with the post",
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: "count",
|
|
84
|
+
type: "number",
|
|
85
|
+
flags: ["--count"],
|
|
86
|
+
description: "await: how many posts to wait for (default 1)",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: "clear",
|
|
90
|
+
type: "boolean",
|
|
91
|
+
flags: ["--clear"],
|
|
92
|
+
description: "signal: reset the channel (all channels when none is given); bus: forget the installed plugin",
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: "install",
|
|
96
|
+
type: "boolean",
|
|
97
|
+
flags: ["--install"],
|
|
98
|
+
description: "bus: load the event-bus plugin in a pane so its permission prompt can be answered, then remember it",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: "reset",
|
|
102
|
+
type: "boolean",
|
|
103
|
+
flags: ["--reset"],
|
|
104
|
+
description: "tail: forget the stored cursor and return the whole screen",
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: "sampleMs",
|
|
108
|
+
type: "number",
|
|
109
|
+
flags: ["--sample-ms"],
|
|
110
|
+
description: "status: gap between the two screen samples (default 400); 0 skips sampling and reports running/exited only",
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: "sinceLast",
|
|
114
|
+
type: "boolean",
|
|
115
|
+
flags: ["--since-last"],
|
|
116
|
+
description: "status: classify by what changed since the last status call instead of sampling twice; needs the event bus, no sample gap",
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: "limit",
|
|
120
|
+
type: "number",
|
|
121
|
+
flags: ["--limit"],
|
|
122
|
+
description: "log: how many entries to return (default 20)",
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: "since",
|
|
126
|
+
type: "string",
|
|
127
|
+
flags: ["--since"],
|
|
128
|
+
description: "log: only entries at or after this epoch millisecond",
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: "failed",
|
|
132
|
+
type: "boolean",
|
|
133
|
+
flags: ["--failed"],
|
|
134
|
+
description: "log: only deliveries that did not land",
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: "body",
|
|
138
|
+
type: "string",
|
|
139
|
+
flags: ["--body", "-b", "--text"],
|
|
140
|
+
description: "send: message body",
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: "text",
|
|
144
|
+
type: "string",
|
|
145
|
+
flags: [],
|
|
146
|
+
description: "send: alias for body",
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: "from",
|
|
150
|
+
type: "string",
|
|
151
|
+
flags: ["--from", "-f"],
|
|
152
|
+
description: "send: sender label in the [zswarm from=…] prefix",
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: "raw",
|
|
156
|
+
type: "boolean",
|
|
157
|
+
flags: ["--raw"],
|
|
158
|
+
description: "send: skip the peer prefix (default false)",
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: "full",
|
|
162
|
+
type: "boolean",
|
|
163
|
+
flags: ["--full"],
|
|
164
|
+
description: "dump/wait: include full scrollback (default false)",
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: "max",
|
|
168
|
+
type: "number",
|
|
169
|
+
flags: ["--max"],
|
|
170
|
+
description: "dump/wait: max text chars (dump 8000, wait 2000; keeps tail, 0 = unlimited)",
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
name: "head",
|
|
174
|
+
type: "boolean",
|
|
175
|
+
flags: ["--head"],
|
|
176
|
+
description: "dump: keep the start instead of the tail when truncating",
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: "for",
|
|
180
|
+
type: "string",
|
|
181
|
+
flags: ["--for"],
|
|
182
|
+
values: ["idle", "match", "either"],
|
|
183
|
+
description: "wait: stop on a quiet screen, on a match, or whichever lands first (default: match if match= given, else idle)",
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: "match",
|
|
187
|
+
type: "string",
|
|
188
|
+
flags: ["--match", "-m"],
|
|
189
|
+
description: "wait: text to look for in the pane screen",
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
name: "regex",
|
|
193
|
+
type: "boolean",
|
|
194
|
+
flags: ["--regex"],
|
|
195
|
+
description: "wait: treat match as a regex (default false)",
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
name: "ignoreCase",
|
|
199
|
+
type: "boolean",
|
|
200
|
+
flags: ["--ignore-case"],
|
|
201
|
+
description: "wait: case-insensitive match (default false)",
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
name: "idleMs",
|
|
205
|
+
type: "number",
|
|
206
|
+
flags: ["--idle-ms"],
|
|
207
|
+
description: "wait: screen must be unchanged this long to count as idle (default 2000)",
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: "pollMs",
|
|
211
|
+
type: "number",
|
|
212
|
+
flags: ["--poll-ms"],
|
|
213
|
+
description: "wait: fallback poll interval (default 150; bus path polls at 50ms)",
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
name: "timeoutMs",
|
|
217
|
+
type: "number",
|
|
218
|
+
flags: ["--timeout-ms"],
|
|
219
|
+
description: "wait: give up after this long (default 60000)",
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: "keys",
|
|
223
|
+
type: "stringOrArray",
|
|
224
|
+
flags: ["--key", "--keys", "-k"],
|
|
225
|
+
repeat: true,
|
|
226
|
+
description: 'keys: key specs, one per entry — "Ctrl c", "Esc", "F1", "Up". A bare string is one key; comma-separate for several.',
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
name: "chars",
|
|
230
|
+
type: "string",
|
|
231
|
+
flags: ["--chars"],
|
|
232
|
+
description: "keys: literal characters to type instead of key specs (no Enter unless enter=true)",
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
name: "enter",
|
|
236
|
+
type: "boolean",
|
|
237
|
+
flags: ["--enter"],
|
|
238
|
+
description: "keys: press Enter after the keys/chars",
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
name: "hard",
|
|
242
|
+
type: "boolean",
|
|
243
|
+
flags: ["--hard"],
|
|
244
|
+
description: "interrupt: send Ctrl c instead of the default Esc",
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
name: "command",
|
|
248
|
+
type: "stringOrArray",
|
|
249
|
+
flags: ["--command", "--cmd", "-c"],
|
|
250
|
+
description: "spawn: program to run in the new pane, argv-style (no shell). Empty starts a plain shell.",
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
name: "cwd",
|
|
254
|
+
type: "string",
|
|
255
|
+
flags: ["--cwd"],
|
|
256
|
+
description: "spawn: working directory for the new pane; worktrees/unworktree: any directory inside the repo",
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
name: "worktree",
|
|
260
|
+
type: "string",
|
|
261
|
+
flags: ["--worktree", "-w"],
|
|
262
|
+
description: "spawn: branch to give the peer its own git worktree (overrides cwd); unworktree: alias for branch",
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
name: "worktreeRoot",
|
|
266
|
+
type: "string",
|
|
267
|
+
flags: ["--worktree-root"],
|
|
268
|
+
description: "where worktrees live (default <repo>-worktrees beside the repo, or ZSWARM_WORKTREE_ROOT)",
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
name: "baseRef",
|
|
272
|
+
type: "string",
|
|
273
|
+
flags: ["--base-ref", "--base"],
|
|
274
|
+
description: "spawn: ref to branch from when the worktree branch is new",
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
name: "path",
|
|
278
|
+
type: "string",
|
|
279
|
+
flags: ["--path"],
|
|
280
|
+
description: "unworktree: worktree path to remove",
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
name: "branch",
|
|
284
|
+
type: "string",
|
|
285
|
+
flags: ["--branch"],
|
|
286
|
+
description: "unworktree: remove the worktree holding this branch",
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
name: "name",
|
|
290
|
+
type: "string",
|
|
291
|
+
flags: ["--name", "-n"],
|
|
292
|
+
description: "spawn: pane (or tab) name; rename: the new name",
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
name: "submit",
|
|
296
|
+
type: "string",
|
|
297
|
+
flags: ["--submit"],
|
|
298
|
+
values: ["auto", "double-enter", "none"],
|
|
299
|
+
description: "send/broadcast: auto verifies the paste actually submitted and presses Enter again if not (default)",
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
name: "settleMs",
|
|
303
|
+
type: "number",
|
|
304
|
+
flags: ["--settle-ms"],
|
|
305
|
+
description: "send/broadcast: pause before checking the paste landed (default 300)",
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
name: "expect",
|
|
309
|
+
type: "string",
|
|
310
|
+
flags: ["--expect"],
|
|
311
|
+
description: "text the target pane's screen must contain before zswarm will write to it",
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
name: "message",
|
|
315
|
+
type: "string",
|
|
316
|
+
flags: ["--message"],
|
|
317
|
+
description: "checkpoint: commit message",
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
name: "stat",
|
|
321
|
+
type: "boolean",
|
|
322
|
+
flags: ["--stat"],
|
|
323
|
+
description: "diff: stat only, no patch body",
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
name: "direction",
|
|
327
|
+
type: "string",
|
|
328
|
+
flags: ["--direction", "-d"],
|
|
329
|
+
values: ["right", "left", "up", "down"],
|
|
330
|
+
description: "spawn: split direction",
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
name: "floating",
|
|
334
|
+
type: "boolean",
|
|
335
|
+
flags: ["--floating"],
|
|
336
|
+
description: "spawn: open the pane floating",
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
name: "width",
|
|
340
|
+
type: "string",
|
|
341
|
+
flags: ["--width"],
|
|
342
|
+
description: "spawn: floating pane width (e.g. 80 or 50%)",
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
name: "height",
|
|
346
|
+
type: "string",
|
|
347
|
+
flags: ["--height"],
|
|
348
|
+
description: "spawn: floating pane height (e.g. 20 or 40%)",
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
name: "tab",
|
|
352
|
+
type: "string",
|
|
353
|
+
flags: ["--tab"],
|
|
354
|
+
description: "tab name: broadcast targets it, rename retitles it, spawn opens the new pane's tab by it",
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
name: "newTab",
|
|
358
|
+
type: "boolean",
|
|
359
|
+
flags: ["--new-tab"],
|
|
360
|
+
description: "spawn: open a new tab instead of splitting",
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
name: "layout",
|
|
364
|
+
type: "string",
|
|
365
|
+
flags: ["--layout", "-l"],
|
|
366
|
+
description: "spawn: layout name for the new tab (newTab=true)",
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
name: "closeOnExit",
|
|
370
|
+
type: "boolean",
|
|
371
|
+
flags: ["--close-on-exit"],
|
|
372
|
+
description: "spawn: close the pane when its command exits",
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
name: "allowSelf",
|
|
376
|
+
type: "boolean",
|
|
377
|
+
flags: ["--allow-self"],
|
|
378
|
+
description: "send/keys/close: allow targeting zswarm's own pane (default false)",
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
name: "force",
|
|
382
|
+
type: "boolean",
|
|
383
|
+
flags: ["--force"],
|
|
384
|
+
description: "send/keys: write to a pane whose command has exited; unworktree: remove a busy or dirty worktree; bus: reinstall under a fresh key",
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
name: "verbose",
|
|
388
|
+
type: "boolean",
|
|
389
|
+
flags: ["--verbose", "-v"],
|
|
390
|
+
description: "list/send/spawn: include cwd/focus/exited/floating (and the pane on send/spawn)",
|
|
391
|
+
},
|
|
392
|
+
];
|
|
393
|
+
function propertyFor(param) {
|
|
394
|
+
const base = { description: param.description };
|
|
395
|
+
if (param.type === "stringOrArray") {
|
|
396
|
+
base.anyOf = [
|
|
397
|
+
{ type: "string" },
|
|
398
|
+
{ type: "array", items: { type: "string" } },
|
|
399
|
+
];
|
|
400
|
+
return base;
|
|
401
|
+
}
|
|
402
|
+
base.type = param.type;
|
|
403
|
+
if (param.values)
|
|
404
|
+
base.enum = [...param.values];
|
|
405
|
+
return base;
|
|
406
|
+
}
|
|
407
|
+
/** JSON Schema for the single `zswarm` MCP tool. */
|
|
408
|
+
export function mcpInputSchema() {
|
|
409
|
+
const properties = {
|
|
410
|
+
op: {
|
|
411
|
+
type: "string",
|
|
412
|
+
enum: [...OP_NAMES],
|
|
413
|
+
description: OP_NAMES.join(" | "),
|
|
414
|
+
},
|
|
415
|
+
};
|
|
416
|
+
for (const param of PARAMS)
|
|
417
|
+
properties[param.name] = propertyFor(param);
|
|
418
|
+
return {
|
|
419
|
+
type: "object",
|
|
420
|
+
additionalProperties: false,
|
|
421
|
+
properties,
|
|
422
|
+
required: ["op"],
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
export const MCP_TOOL_DESCRIPTION = `zSwarm Zellij pane coordination (op=${OP_NAMES.join("|")}). ` +
|
|
426
|
+
"List panes, send text into a CLI pane (paste+Enter), block until a pane goes idle or prints a match, " +
|
|
427
|
+
"send raw keys, open or close panes, and give a peer its own git worktree. Local Zellij only.";
|
|
428
|
+
const FLAG_INDEX = new Map(PARAMS.flatMap((p) => p.flags.map((flag) => [flag, p])));
|
|
429
|
+
export function cliUsage() {
|
|
430
|
+
const lines = [
|
|
431
|
+
`usage: zswarm <${OP_NAMES.join("|")}> [options]`,
|
|
432
|
+
"",
|
|
433
|
+
" positional: first bare argument is --to, second is --body for send",
|
|
434
|
+
"",
|
|
435
|
+
];
|
|
436
|
+
for (const param of PARAMS) {
|
|
437
|
+
if (param.flags.length === 0)
|
|
438
|
+
continue;
|
|
439
|
+
const value = param.type === "boolean" ? "" : param.type === "number" ? " N" : " VALUE";
|
|
440
|
+
lines.push(` ${param.flags.join(", ").padEnd(28)}${value.trim().padEnd(6)}${param.description}`);
|
|
441
|
+
}
|
|
442
|
+
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, then list/status read pane state from the plugin instead of polling.", "Env: ZSWARM_BIN, ZSWARM_PATH, ZSWARM_SESSION, ZSWARM_SELF_PANE, ZELLIJ_SESSION_NAME, ZSWARM_BUS, ZSWARM_BUS_PLUGIN", "");
|
|
443
|
+
return lines.join("\n");
|
|
444
|
+
}
|
|
445
|
+
/** Turn argv (without the op) into dispatch args, driven by PARAMS. */
|
|
446
|
+
export function parseCliArgv(argv) {
|
|
447
|
+
const [op, ...rest] = argv;
|
|
448
|
+
if (!op)
|
|
449
|
+
throw new ZellijError("usage", "no op given");
|
|
450
|
+
const out = { op };
|
|
451
|
+
const repeated = new Map();
|
|
452
|
+
for (let i = 0; i < rest.length; i++) {
|
|
453
|
+
const token = rest[i];
|
|
454
|
+
const param = FLAG_INDEX.get(token);
|
|
455
|
+
if (param) {
|
|
456
|
+
if (param.type === "boolean") {
|
|
457
|
+
out[param.name] = true;
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
const value = rest[++i];
|
|
461
|
+
if (value === undefined) {
|
|
462
|
+
throw new ZellijError("usage", `${token} needs a value`);
|
|
463
|
+
}
|
|
464
|
+
if (param.repeat) {
|
|
465
|
+
const bucket = repeated.get(param.name) ?? [];
|
|
466
|
+
bucket.push(value);
|
|
467
|
+
repeated.set(param.name, bucket);
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
out[param.name] = param.type === "number" ? Number(value) : value;
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
if (token.startsWith("-")) {
|
|
474
|
+
throw new ZellijError("usage", `unknown arg: ${token}`);
|
|
475
|
+
}
|
|
476
|
+
if (!out.to && TARGET_OPS.includes(op)) {
|
|
477
|
+
out.to = token;
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
if (!out.body && op === "send") {
|
|
481
|
+
out.body = token;
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
throw new ZellijError("usage", `unexpected argument: ${token}`);
|
|
485
|
+
}
|
|
486
|
+
for (const [name, values] of repeated)
|
|
487
|
+
out[name] = values;
|
|
488
|
+
return out;
|
|
489
|
+
}
|
package/dist/state.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small on-disk store shared by every zswarm process on the machine: the
|
|
3
|
+
* delivery log, signal channels, and per-pane read cursors. Files are tiny and
|
|
4
|
+
* written whole, so a torn read is a lost update at worst — never corruption.
|
|
5
|
+
*/
|
|
6
|
+
export type LogEntry = {
|
|
7
|
+
at: number;
|
|
8
|
+
op: string;
|
|
9
|
+
session?: string | null;
|
|
10
|
+
to?: string | null;
|
|
11
|
+
from?: string | null;
|
|
12
|
+
bytes?: number;
|
|
13
|
+
ok: boolean;
|
|
14
|
+
detail?: string | null;
|
|
15
|
+
};
|
|
16
|
+
export type SignalChannel = {
|
|
17
|
+
count: number;
|
|
18
|
+
at: number;
|
|
19
|
+
last: string | null;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Written once by `bus --install`, after the plugin's permission prompt has
|
|
23
|
+
* been answered. Its presence is what lets later runs try the fast path without
|
|
24
|
+
* every cold `zswarm status` paying for a pipe that was never going to answer.
|
|
25
|
+
*/
|
|
26
|
+
export type BusMarkerRecord = {
|
|
27
|
+
plugin: string;
|
|
28
|
+
configKey: string;
|
|
29
|
+
installedAt: number;
|
|
30
|
+
};
|
|
31
|
+
export declare function defaultStateDir(env?: NodeJS.ProcessEnv): string;
|
|
32
|
+
export type StateStoreOptions = {
|
|
33
|
+
dir?: string;
|
|
34
|
+
env?: NodeJS.ProcessEnv;
|
|
35
|
+
};
|
|
36
|
+
export declare function createStateStore(options?: StateStoreOptions): {
|
|
37
|
+
dir: string;
|
|
38
|
+
logging: boolean;
|
|
39
|
+
appendLog: (entry: LogEntry) => void;
|
|
40
|
+
readLog: () => LogEntry[];
|
|
41
|
+
readSignals: () => Record<string, SignalChannel>;
|
|
42
|
+
postSignal: (channel: string, payload: string | null, at: number) => SignalChannel;
|
|
43
|
+
clearSignal: (channel: string | null) => void;
|
|
44
|
+
readCursor: (key: string) => string | null;
|
|
45
|
+
writeCursor: (key: string, text: string) => void;
|
|
46
|
+
clearCursor: (key: string) => void;
|
|
47
|
+
readBus: () => BusMarkerRecord | null;
|
|
48
|
+
writeBus: (marker: BusMarkerRecord) => void;
|
|
49
|
+
clearBus: () => void;
|
|
50
|
+
reset: () => void;
|
|
51
|
+
};
|
|
52
|
+
export type StateStore = ReturnType<typeof createStateStore>;
|
package/dist/state.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
const LOG_FILE = "log.jsonl";
|
|
5
|
+
const SIGNALS_FILE = "signals.json";
|
|
6
|
+
const CURSORS_FILE = "cursors.json";
|
|
7
|
+
const BUS_FILE = "bus.json";
|
|
8
|
+
/** Keeps the log bounded without needing a rotation daemon. */
|
|
9
|
+
const LOG_TAIL_BYTES = 512 * 1024;
|
|
10
|
+
export function defaultStateDir(env = process.env) {
|
|
11
|
+
const explicit = env.ZSWARM_STATE_DIR?.trim();
|
|
12
|
+
if (explicit)
|
|
13
|
+
return explicit;
|
|
14
|
+
return join(env.USERPROFILE || env.HOME || homedir(), ".zswarm");
|
|
15
|
+
}
|
|
16
|
+
export function createStateStore(options = {}) {
|
|
17
|
+
const env = options.env ?? process.env;
|
|
18
|
+
const dir = options.dir ?? defaultStateDir(env);
|
|
19
|
+
const logging = (env.ZSWARM_LOG ?? "").trim() !== "0";
|
|
20
|
+
function ensureDir() {
|
|
21
|
+
mkdirSync(dir, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
function readJson(file, fallback) {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(readFileSync(join(dir, file), "utf8"));
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return fallback;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Write through a temp file so readers never see a half-written object. */
|
|
32
|
+
function writeJson(file, value) {
|
|
33
|
+
ensureDir();
|
|
34
|
+
const target = join(dir, file);
|
|
35
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
36
|
+
writeFileSync(tmp, JSON.stringify(value), "utf8");
|
|
37
|
+
renameSync(tmp, target);
|
|
38
|
+
}
|
|
39
|
+
function appendLog(entry) {
|
|
40
|
+
if (!logging)
|
|
41
|
+
return;
|
|
42
|
+
try {
|
|
43
|
+
ensureDir();
|
|
44
|
+
appendFileSync(join(dir, LOG_FILE), `${JSON.stringify(entry)}\n`, "utf8");
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// The log is an aid, never a reason to fail an op.
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function readLog() {
|
|
51
|
+
let raw;
|
|
52
|
+
try {
|
|
53
|
+
raw = readFileSync(join(dir, LOG_FILE), "utf8");
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
if (raw.length > LOG_TAIL_BYTES) {
|
|
59
|
+
raw = raw.slice(raw.length - LOG_TAIL_BYTES);
|
|
60
|
+
raw = raw.slice(raw.indexOf("\n") + 1);
|
|
61
|
+
}
|
|
62
|
+
const out = [];
|
|
63
|
+
for (const line of raw.split("\n")) {
|
|
64
|
+
if (!line.trim())
|
|
65
|
+
continue;
|
|
66
|
+
try {
|
|
67
|
+
out.push(JSON.parse(line));
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Skip a partially written trailing line.
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
function readSignals() {
|
|
76
|
+
return readJson(SIGNALS_FILE, {});
|
|
77
|
+
}
|
|
78
|
+
function postSignal(channel, payload, at) {
|
|
79
|
+
const all = readSignals();
|
|
80
|
+
const prev = all[channel];
|
|
81
|
+
const next = {
|
|
82
|
+
count: (prev?.count ?? 0) + 1,
|
|
83
|
+
at,
|
|
84
|
+
last: payload,
|
|
85
|
+
};
|
|
86
|
+
all[channel] = next;
|
|
87
|
+
writeJson(SIGNALS_FILE, all);
|
|
88
|
+
return next;
|
|
89
|
+
}
|
|
90
|
+
function clearSignal(channel) {
|
|
91
|
+
if (channel === null) {
|
|
92
|
+
writeJson(SIGNALS_FILE, {});
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const all = readSignals();
|
|
96
|
+
delete all[channel];
|
|
97
|
+
writeJson(SIGNALS_FILE, all);
|
|
98
|
+
}
|
|
99
|
+
function readCursor(key) {
|
|
100
|
+
const all = readJson(CURSORS_FILE, {});
|
|
101
|
+
return all[key] ?? null;
|
|
102
|
+
}
|
|
103
|
+
function writeCursor(key, text) {
|
|
104
|
+
const all = readJson(CURSORS_FILE, {});
|
|
105
|
+
all[key] = text;
|
|
106
|
+
writeJson(CURSORS_FILE, all);
|
|
107
|
+
}
|
|
108
|
+
function clearCursor(key) {
|
|
109
|
+
const all = readJson(CURSORS_FILE, {});
|
|
110
|
+
delete all[key];
|
|
111
|
+
writeJson(CURSORS_FILE, all);
|
|
112
|
+
}
|
|
113
|
+
function readBus() {
|
|
114
|
+
const value = readJson(BUS_FILE, null);
|
|
115
|
+
if (!value || typeof value.plugin !== "string" || !value.plugin)
|
|
116
|
+
return null;
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
function writeBus(marker) {
|
|
120
|
+
writeJson(BUS_FILE, marker);
|
|
121
|
+
}
|
|
122
|
+
function clearBus() {
|
|
123
|
+
try {
|
|
124
|
+
rmSync(join(dir, BUS_FILE), { force: true });
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Nothing to forget.
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/** Test helper: drop everything this store wrote. */
|
|
131
|
+
function reset() {
|
|
132
|
+
rmSync(dir, { recursive: true, force: true });
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
dir,
|
|
136
|
+
logging,
|
|
137
|
+
appendLog,
|
|
138
|
+
readLog,
|
|
139
|
+
readSignals,
|
|
140
|
+
postSignal,
|
|
141
|
+
clearSignal,
|
|
142
|
+
readCursor,
|
|
143
|
+
writeCursor,
|
|
144
|
+
clearCursor,
|
|
145
|
+
readBus,
|
|
146
|
+
writeBus,
|
|
147
|
+
clearBus,
|
|
148
|
+
reset,
|
|
149
|
+
};
|
|
150
|
+
}
|