@zswarm/core 0.2.5 → 0.2.6

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/index.d.ts CHANGED
@@ -29,7 +29,7 @@ export type { RoutingContext } from "./ops/routing.js";
29
29
  export type { DispatchDeps, OpsResult, ServeInstallDeps } from "./ops/types.js";
30
30
  export { DEFAULT_DOCTOR_TIMEOUT_MS, DOCTOR_FAILED_CODE, DOCTOR_SCOPE_FIELD, DOCTOR_SCOPE_HOST, DOCTOR_TAILSCALE_MAX_MS, HOST_REPORT_INCOMPLETE_CODE, HOST_REPORT_INVALID_CODE, doctorOp, inspectDoctorHost, coverHostReport, hostDoctorRequest, isRequiredFailure, mergeHostReply, type DoctorCheck, type DoctorCheckScope, type DoctorCheckState, type DoctorReport, type DoctorRoute, type HostInspectInput, } from "./ops/doctor.js";
31
31
  export { normalizeKey, normalizeKeys, tokenizeCommand } from "./keys.js";
32
- export { cliUsage, mcpInputSchema, parseCliArgv, MCP_TOOL_DESCRIPTION, OP_NAMES, PARAMS, TARGET_OPS, type OpName, type ParamSpec, type ParamType, } from "./schema.js";
32
+ export { cliUsage, commandUsage, wantsHelp, extractOp, mcpInputSchema, parseCliArgv, MCP_TOOL_DESCRIPTION, OP_NAMES, PARAMS, TARGET_OPS, type OpName, type ParamSpec, type ParamType, } from "./schema.js";
33
33
  export { HOST_COMMANDS, hostUsage, parseHostArgs, runHostCommand } from "./host/cli.js";
34
34
  export { findPython, runSlot, slotScriptPath } from "./host/slot.js";
35
35
  export { installServeService, serveServiceName, serveServiceSpec, serveTokenFromEnv, serveTokenPath, uninstallServeService, SERVE_AWAIT_SESSION_ENV, SERVE_TOKEN_FILE_ENV, type ServeServiceInput, } from "./host/serve-service.js";
package/dist/index.js CHANGED
@@ -27,7 +27,7 @@ export { classify, lastLine, mapPool, peerStatus, DEFAULT_STATUS_TIMEOUT_MS, STA
27
27
  export { normalizeScreen, unfoldScreen, truncateDumpText, DEFAULT_DUMP_MAX_CHARS, DEFAULT_WAIT_MAX_CHARS, } from "./ops/util.js";
28
28
  export { DEFAULT_DOCTOR_TIMEOUT_MS, DOCTOR_FAILED_CODE, DOCTOR_SCOPE_FIELD, DOCTOR_SCOPE_HOST, DOCTOR_TAILSCALE_MAX_MS, HOST_REPORT_INCOMPLETE_CODE, HOST_REPORT_INVALID_CODE, doctorOp, inspectDoctorHost, coverHostReport, hostDoctorRequest, isRequiredFailure, mergeHostReply, } from "./ops/doctor.js";
29
29
  export { normalizeKey, normalizeKeys, tokenizeCommand } from "./keys.js";
30
- export { cliUsage, mcpInputSchema, parseCliArgv, MCP_TOOL_DESCRIPTION, OP_NAMES, PARAMS, TARGET_OPS, } from "./schema.js";
30
+ export { cliUsage, commandUsage, wantsHelp, extractOp, mcpInputSchema, parseCliArgv, MCP_TOOL_DESCRIPTION, OP_NAMES, PARAMS, TARGET_OPS, } from "./schema.js";
31
31
  export { HOST_COMMANDS, hostUsage, parseHostArgs, runHostCommand } from "./host/cli.js";
32
32
  export { findPython, runSlot, slotScriptPath } from "./host/slot.js";
33
33
  export { installServeService, serveServiceName, serveServiceSpec, serveTokenFromEnv, serveTokenPath, uninstallServeService, SERVE_AWAIT_SESSION_ENV, SERVE_TOKEN_FILE_ENV, } from "./host/serve-service.js";
package/dist/schema.d.ts CHANGED
@@ -12,6 +12,8 @@ export type ParamSpec = {
12
12
  type: ParamType;
13
13
  /** CLI flags; empty means the parameter is reachable through MCP only. */
14
14
  flags: string[];
15
+ /** Ops this flag belongs to; omitted means it applies to every op (routing/global flags). */
16
+ ops?: readonly OpName[];
15
17
  /** Repeatable flags collect into an array (`--key a --key b`). */
16
18
  repeat?: boolean;
17
19
  /** Local CLI preprocessing, excluded from the MCP protocol. */
@@ -24,4 +26,29 @@ export declare const PARAMS: readonly ParamSpec[];
24
26
  export declare function mcpInputSchema(): Record<string, unknown>;
25
27
  export declare const MCP_TOOL_DESCRIPTION: string;
26
28
  export declare function cliUsage(): string;
29
+ /**
30
+ * True when argv asks for help: `--help` or `-h` as a flag of its own, not the
31
+ * value of a flag that takes one (`--body -h` sends "-h") and not after `--`.
32
+ */
33
+ export declare function wantsHelp(argv: readonly string[]): boolean;
34
+ /**
35
+ * Per-command help for `zswarm <op> --help`: the usage line, the positional
36
+ * shorthand, and only the flags whose `ops` include this op. A flag without
37
+ * `ops` (session, --local, --ssh, --fresh, --serve) applies to every op.
38
+ * Render from the same PARAMS table that parses the flags, so they cannot drift.
39
+ */
40
+ export declare function commandUsage(op: OpName): string;
41
+ /** Turn argv (without the op) into dispatch args, driven by PARAMS. */
42
+ /**
43
+ * Pull the op out of argv, tolerating flags before it.
44
+ *
45
+ * The op normally comes first. When it does not — `zswarm --session crew list`
46
+ * — the first token is a flag, and taking argv[0] blindly reported the flag as
47
+ * an unknown op. Only positions that already errored are affected: if argv[0]
48
+ * is a real op it wins, so `send reviewer list` still sends the word "list".
49
+ */
50
+ export declare function extractOp(argv: string[]): {
51
+ op: string;
52
+ rest: string[];
53
+ };
27
54
  export declare function parseCliArgv(argv: string[]): Record<string, unknown>;
package/dist/schema.js CHANGED
@@ -57,25 +57,29 @@ export const PARAMS = [
57
57
  {
58
58
  name: "to",
59
59
  type: "string",
60
- flags: ["--to", "-t"],
60
+ flags: ["--to", "-t", "--pane"],
61
+ ops: [...TARGET_OPS, "broadcast", "log"],
61
62
  description: "target pane: id (3 / terminal_3) or unique title/command; broadcast takes a comma list; log filters by it",
62
63
  },
63
64
  {
64
65
  name: "all",
65
66
  type: "boolean",
66
67
  flags: ["--all", "-a"],
68
+ ops: ["broadcast", "sessions"],
67
69
  description: "broadcast: every terminal pane in the session; sessions: include EXITED resurrectable sessions",
68
70
  },
69
71
  {
70
72
  name: "live",
71
73
  type: "boolean",
72
74
  flags: ["--live", "--active"],
75
+ ops: ["sessions"],
73
76
  description: "sessions: only live (non-EXITED) sessions — this is the default; use --all to include EXITED",
74
77
  },
75
78
  {
76
79
  name: "exited",
77
80
  type: "boolean",
78
81
  flags: ["--exited"],
82
+ ops: ["sessions"],
79
83
  cliOnly: true,
80
84
  description: "sessions: list only EXITED (resurrectable) sessions; rows carry name, exited, age and ageSeconds",
81
85
  },
@@ -83,6 +87,7 @@ export const PARAMS = [
83
87
  name: "pruneExited",
84
88
  type: "boolean",
85
89
  flags: ["--prune-exited"],
90
+ ops: ["sessions"],
86
91
  cliOnly: true,
87
92
  description: "sessions: delete EXITED sessions (all, or those matching --older-than) with zellij delete-session; a live session is never removed",
88
93
  },
@@ -90,6 +95,7 @@ export const PARAMS = [
90
95
  name: "olderThan",
91
96
  type: "string",
92
97
  flags: ["--older-than"],
98
+ ops: ["sessions"],
93
99
  cliOnly: true,
94
100
  description: "sessions --prune-exited: only delete sessions created at least this long ago (e.g. 7d, 12h, 30m; default all exited)",
95
101
  },
@@ -97,6 +103,7 @@ export const PARAMS = [
97
103
  name: "dryRun",
98
104
  type: "boolean",
99
105
  flags: ["--dry-run"],
106
+ ops: ["sessions"],
100
107
  cliOnly: true,
101
108
  description: "sessions --prune-exited: report what would be deleted without deleting",
102
109
  },
@@ -104,6 +111,7 @@ export const PARAMS = [
104
111
  name: "json",
105
112
  type: "boolean",
106
113
  flags: ["--json"],
114
+ ops: ["sessions"],
107
115
  cliOnly: true,
108
116
  description: "sessions: rows are name, exited, age and ageSeconds; the CLI prints JSON either way",
109
117
  },
@@ -123,60 +131,70 @@ export const PARAMS = [
123
131
  name: "group",
124
132
  type: "string",
125
133
  flags: ["--group", "-g"],
134
+ ops: ["broadcast"],
126
135
  description: "broadcast: narrow the selection to panes whose title or command contains this",
127
136
  },
128
137
  {
129
138
  name: "channel",
130
139
  type: "string",
131
140
  flags: ["--channel"],
141
+ ops: ["signal", "await"],
132
142
  description: "signal/await: channel name",
133
143
  },
134
144
  {
135
145
  name: "payload",
136
146
  type: "string",
137
147
  flags: ["--payload"],
148
+ ops: ["signal"],
138
149
  description: "signal: short note stored with the post",
139
150
  },
140
151
  {
141
152
  name: "count",
142
153
  type: "number",
143
154
  flags: ["--count"],
155
+ ops: ["await"],
144
156
  description: "await: how many posts to wait for (default 1)",
145
157
  },
146
158
  {
147
159
  name: "clear",
148
160
  type: "boolean",
149
161
  flags: ["--clear"],
162
+ ops: ["signal", "bus", "serve"],
150
163
  description: "signal: reset the channel (all channels when none is given); bus: forget the installed plugin; serve: stop and unregister the owned Windows zswarm-serve logon task, or the Linux/macOS serve service, if present",
151
164
  },
152
165
  {
153
166
  name: "install",
154
167
  type: "boolean",
155
168
  flags: ["--install"],
169
+ ops: ["bus", "serve"],
156
170
  description: "bus: load the event-bus plugin in a pane so its permission prompt can be answered, then remember it; serve: register the current-user Windows Interactive logon task, or on Linux/macOS a systemd user unit (with linger) or launchd job whose token lives in ~/.zswarm/serve/, and wait for authenticated hello plus host session visibility",
157
171
  },
158
172
  {
159
173
  name: "runAs",
160
174
  type: "string",
161
175
  flags: ["--run-as"],
176
+ ops: ["serve"],
162
177
  description: "serve --install/--clear on Linux/macOS, run as root: install the service for this user (a LaunchDaemon with UserName on macOS, the user's systemd units on Linux), for accounts that never log in",
163
178
  },
164
179
  {
165
180
  name: "reset",
166
181
  type: "boolean",
167
182
  flags: ["--reset"],
183
+ ops: ["tail"],
168
184
  description: "tail: forget the stored cursor and return the whole screen",
169
185
  },
170
186
  {
171
187
  name: "sampleMs",
172
188
  type: "number",
173
189
  flags: ["--sample-ms"],
190
+ ops: ["status"],
174
191
  description: "status: explicitly sample twice with this gap (fallback default 400); 0 reports running/exited only; default prefers bus changes",
175
192
  },
176
193
  {
177
194
  name: "sinceLast",
178
195
  type: "boolean",
179
196
  flags: ["--since-last"],
197
+ ops: ["status"],
180
198
  description: "status: classify changes since the previous bus observation (default when bus available); first observation is unknown unless a prompt is recognized",
181
199
  },
182
200
  {
@@ -195,30 +213,35 @@ export const PARAMS = [
195
213
  name: "limit",
196
214
  type: "number",
197
215
  flags: ["--limit"],
216
+ ops: ["log"],
198
217
  description: "log: how many entries to return (default 20)",
199
218
  },
200
219
  {
201
220
  name: "since",
202
221
  type: "string",
203
222
  flags: ["--since"],
223
+ ops: ["log"],
204
224
  description: "log: only entries at or after this epoch millisecond",
205
225
  },
206
226
  {
207
227
  name: "failed",
208
228
  type: "boolean",
209
229
  flags: ["--failed"],
230
+ ops: ["log"],
210
231
  description: "log: only deliveries that did not land",
211
232
  },
212
233
  {
213
234
  name: "body",
214
235
  type: "string",
215
236
  flags: ["--body", "-b", "--text"],
237
+ ops: ["send", "broadcast"],
216
238
  description: "send: message body",
217
239
  },
218
240
  {
219
241
  name: "bodyFile",
220
242
  type: "string",
221
243
  flags: ["--body-file"],
244
+ ops: ["send"],
222
245
  cliOnly: true,
223
246
  description: "send: read UTF-8 on the caller from PATH, or - for stdin; cannot combine with --body/--text",
224
247
  },
@@ -232,36 +255,42 @@ export const PARAMS = [
232
255
  name: "from",
233
256
  type: "string",
234
257
  flags: ["--from", "-f"],
258
+ ops: ["send", "broadcast"],
235
259
  description: "send: sender label in the [zswarm from=…] prefix (default: ZSWARM_FROM, else the sending pane's title, else swarm)",
236
260
  },
237
261
  {
238
262
  name: "raw",
239
263
  type: "boolean",
240
264
  flags: ["--raw"],
265
+ ops: ["send", "broadcast"],
241
266
  description: "send: skip the peer prefix (default false)",
242
267
  },
243
268
  {
244
269
  name: "full",
245
270
  type: "boolean",
246
271
  flags: ["--full"],
272
+ ops: ["dump", "wait", "tail"],
247
273
  description: "dump/wait: include full scrollback (default false)",
248
274
  },
249
275
  {
250
276
  name: "max",
251
277
  type: "number",
252
278
  flags: ["--max"],
279
+ ops: ["dump", "wait", "tail", "layout", "diff"],
253
280
  description: "dump/wait: max text chars (dump 8000, wait 2000; keeps tail, 0 = unlimited)",
254
281
  },
255
282
  {
256
283
  name: "head",
257
284
  type: "boolean",
258
285
  flags: ["--head"],
286
+ ops: ["dump"],
259
287
  description: "dump: keep the start instead of the tail when truncating",
260
288
  },
261
289
  {
262
290
  name: "for",
263
291
  type: "string",
264
292
  flags: ["--for"],
293
+ ops: ["wait"],
265
294
  values: ["idle", "match", "either"],
266
295
  description: "wait: stop on a quiet screen, on a match, or whichever lands first (default: match if match= given, else idle)",
267
296
  },
@@ -269,42 +298,49 @@ export const PARAMS = [
269
298
  name: "match",
270
299
  type: "string",
271
300
  flags: ["--match", "-m"],
301
+ ops: ["wait"],
272
302
  description: "wait: text to look for in the pane screen",
273
303
  },
274
304
  {
275
305
  name: "regex",
276
306
  type: "boolean",
277
307
  flags: ["--regex"],
308
+ ops: ["wait"],
278
309
  description: "wait: treat match as a regex (default false)",
279
310
  },
280
311
  {
281
312
  name: "ignoreCase",
282
313
  type: "boolean",
283
314
  flags: ["--ignore-case"],
315
+ ops: ["wait"],
284
316
  description: "wait: case-insensitive match (default false)",
285
317
  },
286
318
  {
287
319
  name: "idleMs",
288
320
  type: "number",
289
321
  flags: ["--idle-ms"],
322
+ ops: ["wait"],
290
323
  description: "wait: screen must be unchanged this long to count as idle (default 2000)",
291
324
  },
292
325
  {
293
326
  name: "pollMs",
294
327
  type: "number",
295
328
  flags: ["--poll-ms"],
329
+ ops: ["wait"],
296
330
  description: "wait: fallback poll interval (default 150; bus path polls at 50ms)",
297
331
  },
298
332
  {
299
333
  name: "timeoutMs",
300
334
  type: "number",
301
335
  flags: ["--timeout-ms"],
336
+ ops: ["wait", "status", "spawn", "doctor", "serve", "send", "dump", "tail", "keys", "interrupt", "close", "await"],
302
337
  description: "wait: timeout (default 60000); status/spawn: overall deadline (default 30000); doctor: overall deadline (default 10000), including Tailscale/SSH/hello/host checks; serve --install: overall install/readiness deadline (default 30000)",
303
338
  },
304
339
  {
305
340
  name: "keys",
306
341
  type: "stringOrArray",
307
342
  flags: ["--key", "--keys", "-k"],
343
+ ops: ["keys", "interrupt"],
308
344
  repeat: true,
309
345
  description: 'keys: key specs, one per entry — "Ctrl c", "Esc", "F1", "Up". A bare string is one key; comma-separate for several.',
310
346
  },
@@ -312,72 +348,84 @@ export const PARAMS = [
312
348
  name: "chars",
313
349
  type: "string",
314
350
  flags: ["--chars"],
351
+ ops: ["keys"],
315
352
  description: "keys: literal characters to type instead of key specs (no Enter unless enter=true)",
316
353
  },
317
354
  {
318
355
  name: "enter",
319
356
  type: "boolean",
320
357
  flags: ["--enter"],
358
+ ops: ["keys"],
321
359
  description: "keys: press Enter after the keys/chars",
322
360
  },
323
361
  {
324
362
  name: "hard",
325
363
  type: "boolean",
326
364
  flags: ["--hard"],
365
+ ops: ["interrupt"],
327
366
  description: "interrupt: send Ctrl c instead of the default Esc",
328
367
  },
329
368
  {
330
369
  name: "command",
331
370
  type: "stringOrArray",
332
371
  flags: ["--command", "--cmd", "-c"],
372
+ ops: ["spawn"],
333
373
  description: "spawn: program to run in the new pane, argv-style (no shell). Empty starts a plain shell.",
334
374
  },
335
375
  {
336
376
  name: "cwd",
337
377
  type: "string",
338
378
  flags: ["--cwd"],
379
+ ops: ["spawn", "worktrees", "unworktree", "diff", "checkpoint"],
339
380
  description: "spawn: working directory for the new pane; worktrees/unworktree: any directory inside the repo",
340
381
  },
341
382
  {
342
383
  name: "worktree",
343
384
  type: "string",
344
385
  flags: ["--worktree", "-w"],
386
+ ops: ["spawn", "unworktree"],
345
387
  description: "spawn: branch to give the peer its own git worktree (overrides cwd); unworktree: alias for branch",
346
388
  },
347
389
  {
348
390
  name: "worktreeRoot",
349
391
  type: "string",
350
392
  flags: ["--worktree-root"],
393
+ ops: ["spawn", "unworktree"],
351
394
  description: "where worktrees live (default <repo>-worktrees beside the repo, or ZSWARM_WORKTREE_ROOT)",
352
395
  },
353
396
  {
354
397
  name: "baseRef",
355
398
  type: "string",
356
399
  flags: ["--base-ref", "--base"],
400
+ ops: ["spawn"],
357
401
  description: "spawn: ref to branch from when the worktree branch is new",
358
402
  },
359
403
  {
360
404
  name: "path",
361
405
  type: "string",
362
406
  flags: ["--path"],
407
+ ops: ["unworktree", "diff", "checkpoint"],
363
408
  description: "unworktree: worktree path to remove",
364
409
  },
365
410
  {
366
411
  name: "branch",
367
412
  type: "string",
368
413
  flags: ["--branch"],
414
+ ops: ["unworktree", "diff", "checkpoint"],
369
415
  description: "unworktree: remove the worktree holding this branch",
370
416
  },
371
417
  {
372
418
  name: "name",
373
419
  type: "string",
374
420
  flags: ["--name", "-n"],
421
+ ops: ["spawn", "rename"],
375
422
  description: "spawn: pane (or tab) name; rename: the new name",
376
423
  },
377
424
  {
378
425
  name: "submit",
379
426
  type: "string",
380
427
  flags: ["--submit"],
428
+ ops: ["send", "broadcast"],
381
429
  values: ["auto", "double-enter", "none"],
382
430
  description: 'send/broadcast: auto verifies the paste actually submitted and presses Enter again if not (default). The result reports submitted: true, "queued" (held behind a running turn), false (still in the composer), "unverified" (the screen changed but the body is not visible), or "not-delivered" (the pane was unchanged apart from the composer, so the op fails not_delivered and a resend is safe)',
383
431
  },
@@ -385,48 +433,56 @@ export const PARAMS = [
385
433
  name: "observeMs",
386
434
  type: "number",
387
435
  flags: ["--observe-ms"],
436
+ ops: ["send", "dump", "tail", "wait", "keys", "interrupt", "close", "spawn"],
388
437
  description: "spawn: observe creation/alias for up to 3000ms; pane lookup: retry absence for 1000ms (scaled up to 5x when the first Zellij call is slow); 0 disables retries",
389
438
  },
390
439
  {
391
440
  name: "settleMs",
392
441
  type: "number",
393
442
  flags: ["--settle-ms"],
443
+ ops: ["send", "broadcast"],
394
444
  description: "send/broadcast: pause before checking the paste landed (default 300, scaled up to 5x when the first Zellij call is slow; explicit value wins)",
395
445
  },
396
446
  {
397
447
  name: "confirm",
398
448
  type: "boolean",
399
449
  flags: ["--confirm"],
450
+ ops: ["send", "broadcast"],
400
451
  description: 'send/broadcast: after the send, dump once more and report what it sees; paste again only on "not-delivered" (positive evidence nothing landed), press Enter when the body is still in the composer, and never paste the body twice',
401
452
  },
402
453
  {
403
454
  name: "ifIdle",
404
455
  type: "boolean",
405
456
  flags: ["--if-idle"],
457
+ ops: ["send"],
406
458
  description: "send: refuse a pane that a relay has leased (pane_leased) or whose harness is visibly working (pane_busy) instead of pasting into it",
407
459
  },
408
460
  {
409
461
  name: "expect",
410
462
  type: "string",
411
463
  flags: ["--expect"],
464
+ ops: ["send", "keys", "interrupt"],
412
465
  description: "send/keys/interrupt: case-insensitive substring required on the current screen immediately before input",
413
466
  },
414
467
  {
415
468
  name: "message",
416
469
  type: "string",
417
470
  flags: ["--message"],
471
+ ops: ["checkpoint"],
418
472
  description: "checkpoint: commit message",
419
473
  },
420
474
  {
421
475
  name: "stat",
422
476
  type: "boolean",
423
477
  flags: ["--stat"],
478
+ ops: ["diff"],
424
479
  description: "diff: stat only, no patch body",
425
480
  },
426
481
  {
427
482
  name: "direction",
428
483
  type: "string",
429
484
  flags: ["--direction", "-d"],
485
+ ops: ["spawn"],
430
486
  values: ["right", "left", "up", "down"],
431
487
  description: "spawn: split direction",
432
488
  },
@@ -434,66 +490,77 @@ export const PARAMS = [
434
490
  name: "floating",
435
491
  type: "boolean",
436
492
  flags: ["--floating"],
493
+ ops: ["spawn"],
437
494
  description: "spawn: open the pane floating",
438
495
  },
439
496
  {
440
497
  name: "width",
441
498
  type: "string",
442
499
  flags: ["--width"],
500
+ ops: ["spawn"],
443
501
  description: "spawn: floating pane width (e.g. 80 or 50%)",
444
502
  },
445
503
  {
446
504
  name: "height",
447
505
  type: "string",
448
506
  flags: ["--height"],
507
+ ops: ["spawn"],
449
508
  description: "spawn: floating pane height (e.g. 20 or 40%)",
450
509
  },
451
510
  {
452
511
  name: "tab",
453
512
  type: "string",
454
513
  flags: ["--tab"],
514
+ ops: ["broadcast", "rename", "spawn"],
455
515
  description: "tab name: broadcast targets it, rename retitles it, spawn opens the new pane's tab by it",
456
516
  },
457
517
  {
458
518
  name: "newTab",
459
519
  type: "boolean",
460
520
  flags: ["--new-tab"],
521
+ ops: ["spawn"],
461
522
  description: "spawn: open a new tab instead of splitting",
462
523
  },
463
524
  {
464
525
  name: "layout",
465
526
  type: "string",
466
527
  flags: ["--layout", "-l"],
528
+ ops: ["spawn"],
467
529
  description: "spawn: layout name for the new tab (newTab=true)",
468
530
  },
469
531
  {
470
532
  name: "closeOnExit",
471
533
  type: "boolean",
472
534
  flags: ["--close-on-exit"],
535
+ ops: ["spawn"],
473
536
  description: "spawn: close the pane when its command exits",
474
537
  },
475
538
  {
476
539
  name: "allowSelf",
477
540
  type: "boolean",
478
541
  flags: ["--allow-self"],
542
+ ops: ["send", "keys", "close", "interrupt", "broadcast"],
479
543
  description: "send/keys/close: allow targeting zswarm's own pane (default false)",
480
544
  },
481
545
  {
482
546
  name: "force",
483
547
  type: "boolean",
484
548
  flags: ["--force"],
549
+ ops: ["send", "keys", "unworktree", "bus", "interrupt", "broadcast"],
485
550
  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",
486
551
  },
487
552
  {
488
553
  name: "listen",
489
554
  type: "string",
490
555
  flags: ["--listen"],
556
+ ops: ["serve"],
491
557
  description: "serve: bind address (default 127.0.0.1:9419). Loopback needs no Tailscale; a non-loopback literal must be a verified local Tailscale IP (see docs/tailscale.md). Reach via ZSWARM_SERVE / --serve (direct host:port, private Tailscale Serve tcp:// frontend, or ssh:// to remote loopback)",
492
558
  },
493
559
  {
494
560
  name: "verbose",
495
561
  type: "boolean",
496
562
  flags: ["--verbose", "-v"],
563
+ ops: ["list", "send", "spawn", "status"],
497
564
  description: "list/send/spawn: include cwd/focus/exited/floating (and the pane on send/spawn)",
498
565
  },
499
566
  ];
@@ -552,6 +619,49 @@ export function cliUsage() {
552
619
  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 / --serve (host:port, tcp://, or ssh://user@host?servePort=9419) plus ZSWARM_SERVE_TOKEN. Serve defaults to loopback and always requires a token; an explicit local Tailscale IP is allowed only after host verification. Private raw TCP Tailscale Serve keeps the backend on 127.0.0.1 behind `tailscale serve --tcp=…` (docs/tailscale.md). ssh:// does not start remote serve. Windows default recipe: `zswarm serve --install` (verified readiness).", "Hosts (CLI only, see docs/hosts.md): `zswarm slot`, `zswarm crew`, `zswarm relay`, `zswarm host`; `zswarm <command> --help` for each.", "Doctor: `zswarm doctor --session crew` inspects local, --ssh, and --serve routes without installs, pane changes, or plugin launch. See docs/doctor.md and docs/tailscale.md.", "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_SSH_BIN, ZSWARM_SSH_OPTS, ZSWARM_TMP, ZSWARM_SSH_MODE, ZSWARM_SERVE, ZSWARM_SERVE_TOKEN, ZSWARM_TAILSCALE_BIN, ZSWARM_CACHE_TTL_MS", "");
553
620
  return lines.join("\n");
554
621
  }
622
+ /**
623
+ * True when argv asks for help: `--help` or `-h` as a flag of its own, not the
624
+ * value of a flag that takes one (`--body -h` sends "-h") and not after `--`.
625
+ */
626
+ export function wantsHelp(argv) {
627
+ const valued = new Set(PARAMS.filter((p) => p.type !== "boolean").flatMap((p) => p.flags));
628
+ for (let i = 0; i < argv.length; i++) {
629
+ const arg = argv[i];
630
+ if (arg === "--")
631
+ return false;
632
+ if (arg === "--help" || arg === "-h")
633
+ return true;
634
+ if (valued.has(arg))
635
+ i++; // its value may be anything, "-h" included
636
+ }
637
+ return false;
638
+ }
639
+ /**
640
+ * Per-command help for `zswarm <op> --help`: the usage line, the positional
641
+ * shorthand, and only the flags whose `ops` include this op. A flag without
642
+ * `ops` (session, --local, --ssh, --fresh, --serve) applies to every op.
643
+ * Render from the same PARAMS table that parses the flags, so they cannot drift.
644
+ */
645
+ export function commandUsage(op) {
646
+ const lines = [`usage: zswarm ${op} [options]`, ""];
647
+ const positional = op === "send"
648
+ ? " positional: first bare argument is --to, second is --body"
649
+ : TARGET_OPS.includes(op)
650
+ ? " positional: first bare argument is --to"
651
+ : undefined;
652
+ if (positional)
653
+ lines.push(positional, "");
654
+ for (const param of PARAMS) {
655
+ if (param.flags.length === 0)
656
+ continue;
657
+ if (param.ops && !param.ops.includes(op))
658
+ continue;
659
+ const value = param.type === "boolean" ? "" : param.type === "number" ? " N" : " VALUE";
660
+ lines.push(` ${param.flags.join(", ").padEnd(28)}${value.trim().padEnd(6)}${param.description}`);
661
+ }
662
+ lines.push("");
663
+ return lines.join("\n");
664
+ }
555
665
  /** Turn argv (without the op) into dispatch args, driven by PARAMS. */
556
666
  /**
557
667
  * Pull the op out of argv, tolerating flags before it.
@@ -561,7 +671,7 @@ export function cliUsage() {
561
671
  * an unknown op. Only positions that already errored are affected: if argv[0]
562
672
  * is a real op it wins, so `send reviewer list` still sends the word "list".
563
673
  */
564
- function extractOp(argv) {
674
+ export function extractOp(argv) {
565
675
  const first = argv[0];
566
676
  if (first && !first.startsWith("-")) {
567
677
  return { op: first, rest: argv.slice(1) };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zswarm/core",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "type": "module",
5
5
  "description": "zSwarm Zellij client and shared ops dispatch",
6
6
  "exports": {