@zswarm/core 0.4.1 → 0.5.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/dist/host/crew.js CHANGED
@@ -116,6 +116,10 @@ function foreignLayoutHint(session) {
116
116
  `(on Windows often a renamed session whose server kept its old name). Stop the server that holds it ` +
117
117
  `(a zellij.exe --server process), run \`zellij delete-session ${session}\`, then \`zswarm crew start ${session}\``);
118
118
  }
119
+ function liveSessionHint(session) {
120
+ return (`If that session should go, stop it yourself (\`zellij kill-session ${session}\`; on Windows a renamed ` +
121
+ `session's server may have to be stopped by its PID), then run \`zswarm crew start ${session}\` again.`);
122
+ }
119
123
  function crewRecorder(deps, env) {
120
124
  const record = deps.record ?? ((entry) => createStateStore({ env }).appendLog(entry));
121
125
  return (session, ok, detail) => {
@@ -205,25 +209,39 @@ export async function crewStart(session, deps = {}) {
205
209
  const record = crewRecorder(deps, env0);
206
210
  const names = layoutPaneNames(readFileSync(crewFile(dir, files.layout), "utf8"));
207
211
  const config = files.config ? ["--config", crewFile(dir, files.config)] : [];
212
+ // One shared pre-check for both platforms: a live session that already has
213
+ // the crew's layout is the crew — keep it, never rebuild or kill it. A live
214
+ // session with another layout is not ours to stop; refuse and stop nothing.
215
+ const client = account.platform === "win32" ? windowsClient(deps, env) : null;
216
+ if ((await sessions(env)).includes(session)) {
217
+ const live = checkLayout(await panes(session, env), files.first, names);
218
+ if (live.state === "ok") {
219
+ if (client) {
220
+ const opened = await client.ensure(session);
221
+ log(`session ${session} already up; client window ${opened.opened ? "opened" : "already open"}`);
222
+ }
223
+ else {
224
+ log(`session ${session} already up with its layout`);
225
+ }
226
+ return 0;
227
+ }
228
+ // Live but not the crew's layout (state "foreign" or "missing"): refuse.
229
+ const why = `refused: a live session named ${session} is already running (${live.seen}) without this crew's layout; nothing was stopped. ${liveSessionHint(session)}`;
230
+ log(`session ${session} ${why}`);
231
+ record(session, false, why);
232
+ return 1;
233
+ }
208
234
  if (account.platform === "win32") {
209
235
  // Windows: a session created with `attach --create-background` renders
210
236
  // empty panes and drops pasted input, so the session is created BY an
211
237
  // attached client instead — a minimized console window running
212
238
  // `zellij --layout ... attach --create`, which `crew client` starts. That
213
239
  // window is what `crew start` reopens when it is gone.
214
- const client = windowsClient(deps, env);
215
- // A live session that already has its layout is the crew: keep it and only
216
- // make sure its window is there. Rebuilding would restart every pane.
217
- if ((await sessions(env)).includes(session)
218
- && checkLayout(await panes(session, env), files.first, names).state === "ok") {
219
- const opened = await client.ensure(session);
220
- log(`session ${session} already up; client window ${opened.opened ? "opened" : "already open"}`);
221
- return 0;
222
- }
240
+ const winClient = client ?? windowsClient(deps, env);
223
241
  for (let attempt = 1; attempt <= 3; attempt++) {
224
242
  zellij(["delete-session", session], env); // clears only a dead session of this name
225
- await client.close(session); // a window left over from the previous attempt
226
- const opened = await client.ensure(session);
243
+ await winClient.close(session); // a window left over from the previous attempt
244
+ const opened = await winClient.ensure(session);
227
245
  log(`client window ${opened.opened ? "opened" : "already open"} for ${session}`);
228
246
  for (let i = 0; i < 30 && !(await sessions(env)).includes(session); i++)
229
247
  await sleep(1_000);
@@ -234,7 +252,7 @@ export async function crewStart(session, deps = {}) {
234
252
  return 0;
235
253
  }
236
254
  zellij(["kill-session", session], env);
237
- await client.close(session);
255
+ await winClient.close(session);
238
256
  if (check.state === "foreign") {
239
257
  const why = `stopped: came up with ${check.seen}, none from its layout. ${foreignLayoutHint(session)}`;
240
258
  log(`session ${session} ${why}`);
@@ -646,7 +646,7 @@ async function dispatchOperation(args, injected, deps, context, env) {
646
646
  case "signal":
647
647
  return postSignal(state(), args, clock);
648
648
  case "signals":
649
- return listSignals(state());
649
+ return listSignals(state(), args);
650
650
  case "await":
651
651
  return await awaitSignal(state(), args, clock);
652
652
  case "log":
@@ -174,6 +174,10 @@ async function waitForExit(client, session, pane, kind, profile, budgetMs, clock
174
174
  const live = panes.find((p) => p.id === pane.id);
175
175
  if (live?.exited)
176
176
  return { exited: true, reason: "pane_exited" };
177
+ // A held command pane is Zellij's "exited, press Enter to re-run" state;
178
+ // the relaunch step already presses Enter for command panes.
179
+ if (kind === "command" && live?.held)
180
+ return { exited: true, reason: "pane_held" };
177
181
  }
178
182
  catch {
179
183
  // A transient listing failure is not evidence the pane is gone.
@@ -5,6 +5,6 @@ import type { Clock, OpsResult } from "./types.js";
5
5
  * crew can rendezvous without any of them staying resident.
6
6
  */
7
7
  export declare function postSignal(state: StateStore, args: Record<string, unknown>, clock: Clock): OpsResult;
8
- export declare function listSignals(state: StateStore): OpsResult;
8
+ export declare function listSignals(state: StateStore, args?: Record<string, unknown>): OpsResult;
9
9
  /** Block until a channel reaches `count` posts, or the deadline passes. */
10
10
  export declare function awaitSignal(state: StateStore, args: Record<string, unknown>, clock: Clock): Promise<OpsResult>;
@@ -28,20 +28,33 @@ export function postSignal(state, args, clock) {
28
28
  data: { channel, count: posted.count, at: posted.at, payload: posted.last },
29
29
  };
30
30
  }
31
- export function listSignals(state) {
31
+ export function listSignals(state, args = {}) {
32
32
  const all = state.readSignals();
33
+ const total = Object.keys(all).length;
34
+ const channel = optionalString(args.channel);
35
+ const since = optionalString(args.since);
36
+ const sinceAt = since ? Number(since) : null;
37
+ const hasLimit = args.limit !== undefined && args.limit !== null && args.limit !== "";
38
+ let matched = Object.entries(all).map(([name, v]) => ({
39
+ channel: name,
40
+ count: v.count,
41
+ at: v.at,
42
+ last: v.last ?? null,
43
+ }));
44
+ if (channel)
45
+ matched = matched.filter((c) => c.channel.startsWith(channel));
46
+ if (sinceAt !== null && Number.isFinite(sinceAt)) {
47
+ matched = matched.filter((c) => c.at >= sinceAt);
48
+ }
49
+ const matchedCount = matched.length;
50
+ const channels = hasLimit
51
+ ? matched
52
+ .sort((a, b) => b.at - a.at)
53
+ .slice(0, numberArg(args, "limit", 1, { min: 1, max: 1_000 }))
54
+ : matched.sort((a, b) => a.channel.localeCompare(b.channel));
33
55
  return {
34
56
  ok: true,
35
- data: {
36
- channels: Object.entries(all)
37
- .map(([channel, v]) => ({
38
- channel,
39
- count: v.count,
40
- at: v.at,
41
- last: v.last ?? null,
42
- }))
43
- .sort((a, b) => a.channel.localeCompare(b.channel)),
44
- },
57
+ data: { total, matched: matchedCount, channels },
45
58
  };
46
59
  }
47
60
  /** Block until a channel reaches `count` posts, or the deadline passes. */
package/dist/schema.js CHANGED
@@ -141,8 +141,8 @@ export const PARAMS = [
141
141
  name: "channel",
142
142
  type: "string",
143
143
  flags: ["--channel"],
144
- ops: ["signal", "await"],
145
- description: "signal/await: channel name",
144
+ ops: ["signal", "await", "signals"],
145
+ description: "signal/await: channel name; signals: only channels whose name starts with this",
146
146
  },
147
147
  {
148
148
  name: "payload",
@@ -216,15 +216,15 @@ export const PARAMS = [
216
216
  name: "limit",
217
217
  type: "number",
218
218
  flags: ["--limit"],
219
- ops: ["log"],
220
- description: "log: how many entries to return (default 20)",
219
+ ops: ["log", "signals"],
220
+ description: "log: how many entries to return (default 20); signals: the N most recently updated channels",
221
221
  },
222
222
  {
223
223
  name: "since",
224
224
  type: "string",
225
225
  flags: ["--since"],
226
- ops: ["log"],
227
- description: "log: only entries at or after this epoch millisecond",
226
+ ops: ["log", "signals"],
227
+ description: "log: only entries at or after this epoch millisecond; signals: only channels updated at or after this epoch millisecond",
228
228
  },
229
229
  {
230
230
  name: "failed",
@@ -294,6 +294,7 @@ export function busToPanes(snapshot) {
294
294
  tabId: snapshot.tabIds?.[pane.tab] ?? null,
295
295
  focused: pane.focused,
296
296
  exited: pane.exited,
297
+ held: false,
297
298
  floating: false,
298
299
  contentColumns: null,
299
300
  };
@@ -9,6 +9,8 @@ export type ZellijPane = {
9
9
  tabId?: number | null;
10
10
  focused: boolean;
11
11
  exited: boolean;
12
+ /** Zellij's "exited, press Enter to re-run" command-pane state. */
13
+ held: boolean;
12
14
  floating: boolean;
13
15
  contentColumns: number | null;
14
16
  };
@@ -31,6 +31,7 @@ function parsePaneRow(row) {
31
31
  tabId: typeof row.tab_id === "number" ? row.tab_id : null,
32
32
  focused: row.is_focused === true,
33
33
  exited: row.exited === true,
34
+ held: row.is_held === true,
34
35
  floating: row.is_floating === true,
35
36
  contentColumns: typeof row.pane_content_columns === "number"
36
37
  ? row.pane_content_columns
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zswarm/core",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "zSwarm Zellij client and shared ops dispatch",
6
6
  "exports": {