@zswarm/core 0.4.0 → 0.4.2

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}`);
@@ -21,8 +21,9 @@ export type SenderOrigin = {
21
21
  };
22
22
  /**
23
23
  * Clean one origin part for the `[zswarm from=…]` header: non-strings and
24
- * blanks become null, header-breaking characters become spaces, runs of
25
- * whitespace collapse, and the result is capped at 64 chars.
24
+ * blanks become null, header-breaking characters and whitespace runs become a
25
+ * single `_` (a value never holds a space, so `key=value` pairs stay
26
+ * splittable), and the result is capped at 64 chars.
26
27
  */
27
28
  export declare function sanitizeOriginPart(raw: unknown): string | null;
28
29
  /** The origin of the caller's own process, read from its environment. */
@@ -33,7 +34,10 @@ export declare function resolveSenderOrigin(env: NodeJS.ProcessEnv): SenderOrigi
33
34
  * client) keeps every part null but still records that serve carried it.
34
35
  */
35
36
  export declare function originFromServeRequest(raw: unknown): SenderOrigin;
36
- /** " @ session/pane on host via serve" (or the parts that are known). */
37
+ /**
38
+ * " src=session/pane host=host via=serve" (only the parts that are known).
39
+ * `from=` stays the bare label, so a reader can split the header on spaces.
40
+ */
37
41
  export declare function originSuffix(origin: SenderOrigin | null | undefined): string;
38
42
  export type DeliveryResult = {
39
43
  to: string;
@@ -6,18 +6,19 @@ import { isTrue, numberArg, optionalString } from "./util.js";
6
6
  const ORIGIN_PART_MAX = 64;
7
7
  /**
8
8
  * Clean one origin part for the `[zswarm from=…]` header: non-strings and
9
- * blanks become null, header-breaking characters become spaces, runs of
10
- * whitespace collapse, and the result is capped at 64 chars.
9
+ * blanks become null, header-breaking characters and whitespace runs become a
10
+ * single `_` (a value never holds a space, so `key=value` pairs stay
11
+ * splittable), and the result is capped at 64 chars.
11
12
  */
12
13
  export function sanitizeOriginPart(raw) {
13
14
  if (typeof raw !== "string")
14
15
  return null;
15
- const cleaned = raw.replace(/[\r\n[\]]+/g, " ").replace(/\s+/g, " ").trim();
16
+ const cleaned = raw.replace(/[\r\n[\]\s]+/g, " ").trim().replace(/ /g, "_");
16
17
  if (!cleaned)
17
18
  return null;
18
19
  return cleaned.length <= ORIGIN_PART_MAX
19
20
  ? cleaned
20
- : cleaned.slice(0, ORIGIN_PART_MAX).trimEnd();
21
+ : cleaned.slice(0, ORIGIN_PART_MAX).replace(/_+$/, "");
21
22
  }
22
23
  /** The origin of the caller's own process, read from its environment. */
23
24
  export function resolveSenderOrigin(env) {
@@ -50,15 +51,18 @@ export function originFromServeRequest(raw) {
50
51
  via: "serve",
51
52
  };
52
53
  }
53
- /** " @ session/pane on host via serve" (or the parts that are known). */
54
+ /**
55
+ * " src=session/pane host=host via=serve" (only the parts that are known).
56
+ * `from=` stays the bare label, so a reader can split the header on spaces.
57
+ */
54
58
  export function originSuffix(origin) {
55
59
  if (!origin)
56
60
  return "";
57
61
  const { session, pane, host, via } = origin;
58
- const loc = session && pane ? `${session}/${pane}` : (pane ?? session ?? null);
59
- return ((loc ? " @ " + loc : "") +
60
- (host ? " on " + host : "") +
61
- (via === "serve" ? " via serve" : ""));
62
+ const src = session && pane ? `${session}/${pane}` : (pane ?? session ?? null);
63
+ return ((src ? " src=" + src : "") +
64
+ (host ? " host=" + host : "") +
65
+ (via === "serve" ? " via=serve" : ""));
62
66
  }
63
67
  /**
64
68
  * An args-supplied origin, cleaned again (a direct caller may skip dispatch),
@@ -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.
package/dist/schema.js CHANGED
@@ -281,7 +281,7 @@ export const PARAMS = [
281
281
  type: "string",
282
282
  flags: ["--from", "-f"],
283
283
  ops: ["send", "broadcast"],
284
- description: "send: sender label in the [zswarm from=…] prefix (default: ZSWARM_FROM, else the sending pane's title, else swarm). zswarm appends the sender's session/pane, host (ZSWARM_HOST) and `via serve`",
284
+ description: "send: sender label in the [zswarm from=…] prefix (default: ZSWARM_FROM, else the sending pane's title, else swarm). zswarm appends src=<session>/<pane>, host=<host> (ZSWARM_HOST) and via=serve",
285
285
  },
286
286
  {
287
287
  name: "raw",
@@ -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.0",
3
+ "version": "0.4.2",
4
4
  "type": "module",
5
5
  "description": "zSwarm Zellij client and shared ops dispatch",
6
6
  "exports": {