@zswarm/core 0.5.0 → 0.7.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/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { ZellijError } from "../errors.js";
3
- import { createStateStore } from "../state.js";
3
+ import { createStateStore, defaultStateDir } from "../state.js";
4
4
  import { crewCheck, crewClient, crewDown, crewStart, crewStatus, crewUp } from "./crew.js";
5
5
  import { hostDoctor, hostInstall, hostInstallArgs, hostScriptPath } from "./host.js";
6
6
  import { listLeases } from "./lease.js";
@@ -151,6 +151,10 @@ async function relay(argv) {
151
151
  if (flags["--json"] === true)
152
152
  return json(() => ({ leases }));
153
153
  process.stdout.write(`${leaseTable(leases)}\n`);
154
+ if (leases.length === 0) {
155
+ // A tool that runs zswarm with its own ZSWARM_STATE_DIR keeps its leases there.
156
+ process.stderr.write(`no live leases in ${defaultStateDir()}; set ZSWARM_STATE_DIR to read another state directory\n`);
157
+ }
154
158
  return 0;
155
159
  }
156
160
  const cancelId = str(flags["--cancel"]);
@@ -46,6 +46,9 @@ export type RelayConfig = {
46
46
  to: string;
47
47
  done: string;
48
48
  replyTo: string;
49
+ /** The reply pane's unique title when `replyTo` was a pane id: delivery tries
50
+ * the name first, because ids shift when panes are restarted or renumbered. */
51
+ replyToName?: string;
49
52
  replySession?: string;
50
53
  from: string;
51
54
  /** The target harness (`--harness`); sent with the body and the reply. */
@@ -112,6 +115,10 @@ export declare function fillMessage(template: string, fields: Record<string, str
112
115
  * the verdict and keeps waiting — the done line is the real proof of delivery.
113
116
  */
114
117
  export declare function softSendVerdict(res: OpsResult): boolean;
118
+ /** The unique title of the reply pane when `replyTo` is a pane id (`terminal_14`, `14`),
119
+ * else undefined: an untitled pane (default shell title) or a title shared by two panes
120
+ * keeps delivery by id. */
121
+ export declare function replyPaneName(cfg: Pick<RelayConfig, "replyTo" | "replySession">, env: NodeJS.ProcessEnv, call: NonNullable<RelayDeps["call"]>): Promise<string | undefined>;
115
122
  /** Paste `body` into the reply pane, retrying with backoff; false leaves it in undelivered/. */
116
123
  export declare function deliverMessage(cfg: RelayConfig, body: string, dir: string, deps?: RelayDeps): Promise<boolean>;
117
124
  /** The waiting loop; returns the final status and the message it delivered or left behind. */
@@ -111,6 +111,28 @@ export function softSendVerdict(res) {
111
111
  const submitted = res.data?.submitted;
112
112
  return submitted !== true;
113
113
  }
114
+ /** The unique title of the reply pane when `replyTo` is a pane id (`terminal_14`, `14`),
115
+ * else undefined: an untitled pane (default shell title) or a title shared by two panes
116
+ * keeps delivery by id. */
117
+ export async function replyPaneName(cfg, env, call) {
118
+ const ref = cfg.replyTo.trim();
119
+ if (!/^(terminal_)?\d+$/i.test(ref))
120
+ return undefined;
121
+ const args = { op: "list" };
122
+ if (cfg.replySession)
123
+ args.session = cfg.replySession;
124
+ const res = await call(args, localEnv(env)).catch(() => null);
125
+ if (!res?.ok)
126
+ return undefined;
127
+ const panes = (res.data.panes ?? [])
128
+ .filter((p) => !p.isPlugin);
129
+ const id = `terminal_${ref.replace(/^terminal_/i, "")}`;
130
+ const title = panes.find((p) => p.id === id)?.title?.trim();
131
+ if (!title || /^(Pane #\d+|-?(zsh|bash|fish|sh|pwsh|powershell))$/i.test(title))
132
+ return undefined;
133
+ const same = panes.filter((p) => (p.title ?? "").trim().toLowerCase() === title.toLowerCase());
134
+ return same.length === 1 ? title : undefined;
135
+ }
114
136
  /** Paste `body` into the reply pane, retrying with backoff; false leaves it in undelivered/. */
115
137
  export async function deliverMessage(cfg, body, dir, deps = {}) {
116
138
  const env = deps.env ?? process.env;
@@ -125,13 +147,20 @@ export async function deliverMessage(cfg, body, dir, deps = {}) {
125
147
  await sleep(delay * 1000);
126
148
  // Generous Zellij budgets: this runs exactly when the machine is busiest.
127
149
  const args = {
128
- op: "send", to: cfg.replyTo, body, from: cfg.from, fresh: true, settleMs: 800, observeMs: 30_000, timeoutMs: 60_000,
150
+ op: "send", to: cfg.replyToName ?? cfg.replyTo, body, from: cfg.from, fresh: true, settleMs: 800, observeMs: 30_000,
151
+ timeoutMs: 60_000,
129
152
  };
130
153
  if (cfg.replySession)
131
154
  args.session = cfg.replySession;
132
155
  if (cfg.harness)
133
156
  args.harness = cfg.harness;
134
- const res = await call(args, localEnv(env)).catch((err) => ({ ok: false, error: { code: "failed", message: String(err) } }));
157
+ const send = (a) => call(a, localEnv(env)).catch((err) => ({ ok: false, error: { code: "failed", message: String(err) } }));
158
+ let res = await send(args);
159
+ if (!res.ok && cfg.replyToName && (res.error.code === "peer_not_found" || res.error.code === "peer_ambiguous")) {
160
+ // The named pane is gone or now ambiguous: the id it had at start is the last resort.
161
+ log(`message attempt ${i + 1}: pane "${cfg.replyToName}" ${res.error.code}; trying ${cfg.replyTo}`);
162
+ res = await send({ ...args, to: cfg.replyTo });
163
+ }
135
164
  if (res.ok) {
136
165
  const submitted = String(res.data?.submitted ?? "unknown");
137
166
  recordDelivery(dir, submitted, now());
@@ -430,6 +459,7 @@ export async function startRelay(input) {
430
459
  };
431
460
  // Read the body before the relay directory (and its token copy) exists.
432
461
  const body = input.bodyFile ? readFileSync(input.bodyFile, "utf8") : undefined;
462
+ cfg.replyToName = await replyPaneName(cfg, env, input.deps?.call ?? ((args, e) => dispatchZswarm(args, undefined, { env: e })));
433
463
  const dir = prepareRelay(cfg, { check: input.check, message: input.message }, env);
434
464
  const store = input.store ?? createStateStore({ env });
435
465
  const leaseTarget = { serve, session: cfg.session, pane: cfg.to };
package/dist/index.d.ts CHANGED
@@ -5,11 +5,12 @@ export { createGitClient, defaultWorktreeRoot, normalizeRepoPath, parseWorktreeL
5
5
  export { expandHomePath, looksLikeZswarmBinary, isZellijVersionOutput, resolveZellijBinary, resetZellijIdentityCache, sanitizeZellijEnv, validateSshDestination, validateSshMode, type ZellijExecFn, type ZellijExecResult, } from "./zellij/binary.js";
6
6
  export { normalizePaneId, parsePaneList, resolvePane, type ZellijPane, } from "./zellij/panes.js";
7
7
  export { liveSessionNames, parseCreatedAge, parseSessionList, resolveSelfPaneId, resolveSelfSession, type ZellijSession, type ZellijSessionResolve, } from "./zellij/session.js";
8
- export { buildClosePaneArgs, buildDumpArgs, buildLaunchPluginArgs, buildListPanesArgs, buildNewPaneArgs, buildNewTabArgs, buildPasteArgs, buildPipeArgs, buildSendEnterArgs, buildSendKeysArgs, buildWriteCharsArgs, scrollbackPayload, waitPayload, type LaunchPluginInput, type NewPaneInput, type NewTabInput, type PaneDirection, type PipeInput, type ScrollbackRequest, type WaitRequest, } from "./zellij/args.js";
8
+ export { assignServersToSessions, findAncestorServer, liveServers, matchServerToSession, paneMatchesId, parseServerArgs, readProcessTable, type ProcInfo, type ProcessRunner, type ReadProcessTableOptions, type ServerInfo, type SessionPanes, } from "./zellij/server-identity.js";
9
+ export { buildClosePaneArgs, buildDumpArgs, buildLaunchPluginArgs, buildListPanesArgs, buildNewPaneArgs, buildNewTabArgs, buildPasteArgs, buildPipeArgs, buildRenameSessionArgs, buildSendEnterArgs, buildSendKeysArgs, buildWriteCharsArgs, scrollbackPayload, waitPayload, type LaunchPluginInput, type NewPaneInput, type NewTabInput, type PaneDirection, type PipeInput, type ScrollbackRequest, type WaitRequest, } from "./zellij/args.js";
9
10
  export { busPluginUrl, busToPanes, isBusPluginPane, nextConfigKey, parseBusReply, parseScrollbackReply, resolveBusPlugin, scrollbackToScreen, BUS_PIPE_NAME, DEFAULT_BUS_KEY, DEFAULT_BUS_TIMEOUT_MS, type BusMarker, type BusPane, type BusScrollback, type BusScrollbackPane, type BusSnapshot, } from "./zellij/bus.js";
10
11
  export { busSnapshot, closeBusPluginPanes, planBus, resetBusCache, type BusPlan, } from "./ops/bus.js";
11
- export { createZellijClient, type ZellijClient, type ZellijClientOptions, type ZellijTransport, } from "./zellij/client.js";
12
- export { createStateStore, defaultStateDir, type LeaseRecord, type LogEntry, type SignalChannel, type StateStore, type StateStoreOptions, } from "./state.js";
12
+ export { createZellijClient, resetSelfResolutionCache, type ZellijClient, type ZellijClientOptions, type ZellijTransport, } from "./zellij/client.js";
13
+ export { createStateStore, defaultStateDir, type LeaseRecord, type LogEntry, type SessionAliasEntry, type SessionAliasMap, type SignalChannel, type StateStore, type StateStoreOptions, } from "./state.js";
13
14
  export { findLease, leaseKey, leaseStale, listLeases, releaseLease, releaseLeaseKey, takeLease, type LeaseTakeInput, type LeaseTakeResult, type LeaseTarget, } from "./host/lease.js";
14
15
  export { dispatchZswarm, resolveInvocationEnv } from "./ops/dispatch.js";
15
16
  export { EXIT_WAIT_MS, HANDOFF_INLINE_MAX, HANDOFF_MARKER, HANDOFF_SELF_WAIT_MS, READY_SETTLE_MS, READY_STABLE_GAP_MS, READY_WAIT_MS, restartLaunchCommand, restartPane, restartPaneKind, type RestartPaneKind, type RestartStep, } from "./ops/restart.js";
@@ -38,6 +39,6 @@ export { findPython, runSlot, slotScriptPath } from "./host/slot.js";
38
39
  export { installServeService, serveServiceName, serveServiceSpec, serveTokenFromEnv, serveTokenPath, uninstallServeService, SERVE_AWAIT_SESSION_ENV, SERVE_TOKEN_FILE_ENV, type ServeServiceInput, } from "./host/serve-service.js";
39
40
  export { accountArgv, installServices, launchdLabel, launchdPlist, removeServices, serviceAccount, serviceStates, systemdUnitName, systemdUnits, writeAccountFile, type Runner, type ServiceAccount, type ServiceHost, type ServiceKind, type ServiceSpec, type ServiceState, } from "./host/services.js";
40
41
  export { crewCheck, crewClient, crewDir, crewDown, crewServiceSpecs, crewStart, crewStatus, crewUp, firstLayoutPane, layoutPaneNames, parseEnvFile, type CrewDeps, type CrewUpInput, } from "./host/crew.js";
41
- export { cancelAllTo, cancelRelay, deliverMessage, doneLines, fillMessage, freshDoneLine, launchRelay, listRelays, prepareRelay, pruneRelays, relayLoop, relaysDir, runRelayDir, scrubRelayToken, startRelay, waitForRelay, type RelayAdminDeps, type RelayCancelResult, type RelayConfig, type RelayDeps, type RelayJob, type RelayJobRunner, type RelayRow, type RelayStartInput, type RelayStatus, } from "./host/relay.js";
42
+ export { cancelAllTo, cancelRelay, deliverMessage, doneLines, fillMessage, freshDoneLine, launchRelay, listRelays, prepareRelay, pruneRelays, relayLoop, relaysDir, replyPaneName, runRelayDir, scrubRelayToken, startRelay, waitForRelay, type RelayAdminDeps, type RelayCancelResult, type RelayConfig, type RelayDeps, type RelayJob, type RelayJobRunner, type RelayRow, type RelayStartInput, type RelayStatus, } from "./host/relay.js";
42
43
  export { hostChecks, hostDoctor, hostInstall, hostInstallArgs, hostScriptPath, parseHostReport, type HostCheck, type HostDoctorInput, type HostInstallInput, type SshRunner, } from "./host/host.js";
43
44
  export { isSelfPane } from "./ops/guards.js";
package/dist/index.js CHANGED
@@ -5,10 +5,11 @@ export { createGitClient, defaultWorktreeRoot, normalizeRepoPath, parseWorktreeL
5
5
  export { expandHomePath, looksLikeZswarmBinary, isZellijVersionOutput, resolveZellijBinary, resetZellijIdentityCache, sanitizeZellijEnv, validateSshDestination, validateSshMode, } from "./zellij/binary.js";
6
6
  export { normalizePaneId, parsePaneList, resolvePane, } from "./zellij/panes.js";
7
7
  export { liveSessionNames, parseCreatedAge, parseSessionList, resolveSelfPaneId, resolveSelfSession, } from "./zellij/session.js";
8
- export { buildClosePaneArgs, buildDumpArgs, buildLaunchPluginArgs, buildListPanesArgs, buildNewPaneArgs, buildNewTabArgs, buildPasteArgs, buildPipeArgs, buildSendEnterArgs, buildSendKeysArgs, buildWriteCharsArgs, scrollbackPayload, waitPayload, } from "./zellij/args.js";
8
+ export { assignServersToSessions, findAncestorServer, liveServers, matchServerToSession, paneMatchesId, parseServerArgs, readProcessTable, } from "./zellij/server-identity.js";
9
+ export { buildClosePaneArgs, buildDumpArgs, buildLaunchPluginArgs, buildListPanesArgs, buildNewPaneArgs, buildNewTabArgs, buildPasteArgs, buildPipeArgs, buildRenameSessionArgs, buildSendEnterArgs, buildSendKeysArgs, buildWriteCharsArgs, scrollbackPayload, waitPayload, } from "./zellij/args.js";
9
10
  export { busPluginUrl, busToPanes, isBusPluginPane, nextConfigKey, parseBusReply, parseScrollbackReply, resolveBusPlugin, scrollbackToScreen, BUS_PIPE_NAME, DEFAULT_BUS_KEY, DEFAULT_BUS_TIMEOUT_MS, } from "./zellij/bus.js";
10
11
  export { busSnapshot, closeBusPluginPanes, planBus, resetBusCache, } from "./ops/bus.js";
11
- export { createZellijClient, } from "./zellij/client.js";
12
+ export { createZellijClient, resetSelfResolutionCache, } from "./zellij/client.js";
12
13
  export { createStateStore, defaultStateDir, } from "./state.js";
13
14
  export { findLease, leaseKey, leaseStale, listLeases, releaseLease, releaseLeaseKey, takeLease, } from "./host/lease.js";
14
15
  export { dispatchZswarm, resolveInvocationEnv } from "./ops/dispatch.js";
@@ -36,6 +37,6 @@ export { findPython, runSlot, slotScriptPath } from "./host/slot.js";
36
37
  export { installServeService, serveServiceName, serveServiceSpec, serveTokenFromEnv, serveTokenPath, uninstallServeService, SERVE_AWAIT_SESSION_ENV, SERVE_TOKEN_FILE_ENV, } from "./host/serve-service.js";
37
38
  export { accountArgv, installServices, launchdLabel, launchdPlist, removeServices, serviceAccount, serviceStates, systemdUnitName, systemdUnits, writeAccountFile, } from "./host/services.js";
38
39
  export { crewCheck, crewClient, crewDir, crewDown, crewServiceSpecs, crewStart, crewStatus, crewUp, firstLayoutPane, layoutPaneNames, parseEnvFile, } from "./host/crew.js";
39
- export { cancelAllTo, cancelRelay, deliverMessage, doneLines, fillMessage, freshDoneLine, launchRelay, listRelays, prepareRelay, pruneRelays, relayLoop, relaysDir, runRelayDir, scrubRelayToken, startRelay, waitForRelay, } from "./host/relay.js";
40
+ export { cancelAllTo, cancelRelay, deliverMessage, doneLines, fillMessage, freshDoneLine, launchRelay, listRelays, prepareRelay, pruneRelays, relayLoop, relaysDir, replyPaneName, runRelayDir, scrubRelayToken, startRelay, waitForRelay, } from "./host/relay.js";
40
41
  export { hostChecks, hostDoctor, hostInstall, hostInstallArgs, hostScriptPath, parseHostReport, } from "./host/host.js";
41
42
  export { isSelfPane } from "./ops/guards.js";
@@ -26,8 +26,12 @@ export type SenderOrigin = {
26
26
  * splittable), and the result is capped at 64 chars.
27
27
  */
28
28
  export declare function sanitizeOriginPart(raw: unknown): string | null;
29
- /** The origin of the caller's own process, read from its environment. */
30
- export declare function resolveSenderOrigin(env: NodeJS.ProcessEnv): SenderOrigin;
29
+ /**
30
+ * The origin of the caller's own process, read from its environment.
31
+ * `session` overrides the env name when the caller has resolved the live
32
+ * session through the process table (a renamed session leaves the env stale).
33
+ */
34
+ export declare function resolveSenderOrigin(env: NodeJS.ProcessEnv, session?: string | null): SenderOrigin;
31
35
  /**
32
36
  * The origin carried by a serve request. Only the serve worker trusts it: a
33
37
  * local caller cannot claim another pane. Missing/garbage input (an older
@@ -20,8 +20,12 @@ export function sanitizeOriginPart(raw) {
20
20
  ? cleaned
21
21
  : cleaned.slice(0, ORIGIN_PART_MAX).replace(/_+$/, "");
22
22
  }
23
- /** The origin of the caller's own process, read from its environment. */
24
- export function resolveSenderOrigin(env) {
23
+ /**
24
+ * The origin of the caller's own process, read from its environment.
25
+ * `session` overrides the env name when the caller has resolved the live
26
+ * session through the process table (a renamed session leaves the env stale).
27
+ */
28
+ export function resolveSenderOrigin(env, session) {
25
29
  const configured = env.ZSWARM_HOST;
26
30
  const host = configured === undefined
27
31
  ? sanitizeOriginPart(hostname().split(".")[0])
@@ -30,7 +34,7 @@ export function resolveSenderOrigin(env) {
30
34
  : sanitizeOriginPart(configured);
31
35
  return {
32
36
  host,
33
- session: sanitizeOriginPart(resolveSelfSession(env)),
37
+ session: sanitizeOriginPart(session ?? resolveSelfSession(env)),
34
38
  pane: sanitizeOriginPart(resolveSelfPaneId(env)),
35
39
  via: null,
36
40
  };
@@ -216,6 +216,18 @@ async function unglueScreen(client, session, pane, text, full, fromBus) {
216
216
  }
217
217
  return unfoldScreen(ansi, columns) ?? text;
218
218
  }
219
+ /**
220
+ * Ops whose sender header or self-pane guard needs the caller's live session.
221
+ * Read-only ops never pay for that resolution.
222
+ */
223
+ const SELF_SESSION_OPS = new Set([
224
+ "send",
225
+ "broadcast",
226
+ "keys",
227
+ "interrupt",
228
+ "close",
229
+ "restart",
230
+ ]);
219
231
  async function dispatchOperation(args, injected, deps, context, env) {
220
232
  const op = String(args.op ?? "");
221
233
  const verbose = isVerbose(args);
@@ -322,10 +334,20 @@ async function dispatchOperation(args, injected, deps, context, env) {
322
334
  manager: deps.serveTunnels,
323
335
  });
324
336
  }
325
- const baseClient = injected ?? createZellijClient({ env, signal, cache: isTrue(args.fresh) ? false : undefined });
337
+ const baseClient = injected ??
338
+ createZellijClient({
339
+ env,
340
+ signal,
341
+ cache: isTrue(args.fresh) ? false : undefined,
342
+ // Rename memory so stale env names keep working across runs.
343
+ aliases: state(),
344
+ });
326
345
  const client = {
327
346
  ...baseClient,
328
347
  get transport() { return baseClient.transport; },
348
+ // Pass the getter through: a spread would freeze the env value and the
349
+ // self-pane guard would compare a stale name.
350
+ get selfSession() { return baseClient.selfSession; },
329
351
  async resolveSession(...input) {
330
352
  const resolved = await baseClient.resolveSession(...input);
331
353
  context.session = resolved.session;
@@ -333,6 +355,15 @@ async function dispatchOperation(args, injected, deps, context, env) {
333
355
  return resolved;
334
356
  },
335
357
  };
358
+ // Only ops that build a sender header or compare against the caller's own
359
+ // pane need the resolved self session; read-only ops must not pay a
360
+ // Zellij round-trip for it.
361
+ if (deps.viaServe !== true && SELF_SESSION_OPS.has(op) && typeof client.resolveSelf === "function") {
362
+ // The pane's ZELLIJ_SESSION_NAME can be stale after a rename; the header
363
+ // must name the live session peers can reply to.
364
+ const self = await client.resolveSelf();
365
+ args = { ...args, origin: resolveSenderOrigin(env, self) };
366
+ }
336
367
  switch (op) {
337
368
  case "sessions": {
338
369
  // Resolve SSH/IPC first so a Windows interactive crew is reachable
@@ -3,7 +3,8 @@ import type { ZellijClient } from "../zellij/client.js";
3
3
  import type { OpsResult } from "./types.js";
4
4
  /**
5
5
  * Give a pane (or its tab) a stable name, so peers can be addressed as
6
- * `reviewer` instead of whatever id Zellij handed out.
6
+ * `reviewer` instead of whatever id Zellij handed out. With `sessionName`
7
+ * (and no `to`/`tab`) it renames the session itself instead.
7
8
  */
8
9
  export declare function renameTarget(client: ZellijClient, args: Record<string, unknown>, policy?: Policy): Promise<OpsResult>;
9
10
  export declare function focusTarget(client: ZellijClient, args: Record<string, unknown>, policy?: Policy): Promise<OpsResult>;
package/dist/ops/panes.js CHANGED
@@ -7,14 +7,27 @@ async function session(client, args) {
7
7
  }
8
8
  /**
9
9
  * Give a pane (or its tab) a stable name, so peers can be addressed as
10
- * `reviewer` instead of whatever id Zellij handed out.
10
+ * `reviewer` instead of whatever id Zellij handed out. With `sessionName`
11
+ * (and no `to`/`tab`) it renames the session itself instead.
11
12
  */
12
13
  export async function renameTarget(client, args, policy) {
14
+ const sessionName = optionalString(args.sessionName);
15
+ const tabKey = optionalString(args.tab);
16
+ const to = optionalString(args.to);
17
+ if (sessionName && !tabKey && !to) {
18
+ // Renaming the session itself: resolve the old name first so a stale
19
+ // ZELLIJ_SESSION_NAME or alias still names the live session.
20
+ const resolved = await client.resolveSession(typeof args.session === "string" ? args.session : undefined);
21
+ const renamed = await client.renameSession({
22
+ session: resolved.session,
23
+ name: sessionName,
24
+ });
25
+ return { ok: true, data: { session: renamed.session, was: renamed.was } };
26
+ }
13
27
  const name = optionalString(args.name);
14
28
  if (!name)
15
29
  throw new ZellijError("missing_name", "name required");
16
30
  const sess = await session(client, args);
17
- const tabKey = optionalString(args.tab);
18
31
  if (tabKey) {
19
32
  const tab = client.resolveTab(await client.listTabs(sess, undefined, { fresh: true }), tabKey);
20
33
  const renamed = await client.renameTab({
@@ -27,7 +40,6 @@ export async function renameTarget(client, args, policy) {
27
40
  data: { session: sess, tab: renamed.tabId, was: tab.name, name },
28
41
  };
29
42
  }
30
- const to = optionalString(args.to);
31
43
  if (!to)
32
44
  throw new ZellijError("missing_peer", "to (or tab) required");
33
45
  const pane = client.resolvePane(await client.listPanes(sess, undefined, { fresh: true }), to);
package/dist/schema.js CHANGED
@@ -446,6 +446,13 @@ export const PARAMS = [
446
446
  ops: ["spawn", "rename"],
447
447
  description: "spawn: pane (or tab) name; rename: the new name",
448
448
  },
449
+ {
450
+ name: "sessionName",
451
+ type: "string",
452
+ flags: ["--session-name"],
453
+ ops: ["rename"],
454
+ description: "rename: rename the session itself to this name; the old name stays an alias so panes that still carry it keep working",
455
+ },
449
456
  {
450
457
  name: "submit",
451
458
  type: "string",
package/dist/state.d.ts CHANGED
@@ -55,6 +55,20 @@ export type LeaseRecord = {
55
55
  /** Relays that forced through while this lease was held. */
56
56
  sharedBy?: string[];
57
57
  };
58
+ /**
59
+ * What an old session name now resolves to. Zellij never updates the
60
+ * ZELLIJ_SESSION_NAME a pane started with, so after a rename every older pane
61
+ * keeps the old name; this is how zswarm maps it to the live session. The
62
+ * server pid pins the entry to the process that owned the old name, so a
63
+ * later session reusing that name does not inherit the alias.
64
+ */
65
+ export type SessionAliasEntry = {
66
+ current: string;
67
+ serverPid: number | null;
68
+ creation: string | null;
69
+ at: number;
70
+ };
71
+ export type SessionAliasMap = Record<string, SessionAliasEntry>;
58
72
  /** How much of the end of the log `readLog` reads; older entries are not shown. */
59
73
  export declare const LOG_TAIL_BYTES: number;
60
74
  /**
@@ -78,6 +92,8 @@ export declare function createStateStore(options?: StateStoreOptions): {
78
92
  readCursor: (key: string) => string | null;
79
93
  writeCursor: (key: string, text: string) => void;
80
94
  clearCursor: (key: string) => void;
95
+ readSessionAliases: () => SessionAliasMap;
96
+ recordSessionAlias: (oldName: string, entry: SessionAliasEntry) => void;
81
97
  readBus: (session: string) => BusMarkerRecord | null;
82
98
  writeBus: (session: string, marker: BusMarkerRecord) => void;
83
99
  clearBus: (session?: string) => void;
package/dist/state.js CHANGED
@@ -9,6 +9,8 @@ const CURSORS_LOCK = "cursors.lock";
9
9
  const BUS_FILE = "bus.json";
10
10
  const LEASES_FILE = "leases.json";
11
11
  const LEASES_LOCK = "leases.lock";
12
+ const SESSION_ALIASES_FILE = "session-aliases.json";
13
+ const SESSION_ALIASES_LOCK = "session-aliases.lock";
12
14
  /** How much of the end of the log `readLog` reads; older entries are not shown. */
13
15
  export const LOG_TAIL_BYTES = 512 * 1024;
14
16
  /**
@@ -397,6 +399,52 @@ export function createStateStore(options = {}) {
397
399
  writeJson(CURSORS_FILE, all);
398
400
  });
399
401
  }
402
+ function asAliasEntry(value) {
403
+ if (!value || typeof value !== "object")
404
+ return null;
405
+ const rec = value;
406
+ if (typeof rec.current !== "string" || !rec.current)
407
+ return null;
408
+ return {
409
+ current: rec.current,
410
+ serverPid: typeof rec.serverPid === "number" &&
411
+ Number.isInteger(rec.serverPid) &&
412
+ rec.serverPid > 0
413
+ ? rec.serverPid
414
+ : null,
415
+ creation: typeof rec.creation === "string" && rec.creation ? rec.creation : null,
416
+ at: typeof rec.at === "number" ? rec.at : 0,
417
+ };
418
+ }
419
+ function readSessionAliases() {
420
+ const raw = readJson(SESSION_ALIASES_FILE, {});
421
+ const out = {};
422
+ for (const [name, entry] of Object.entries(raw)) {
423
+ const parsed = asAliasEntry(entry);
424
+ if (parsed)
425
+ out[name] = parsed;
426
+ }
427
+ return out;
428
+ }
429
+ /**
430
+ * Remember `oldName -> entry.current` and repoint every existing entry whose
431
+ * current name was the old one, so rename chains A -> B -> C leave both A
432
+ * and B resolving to C.
433
+ */
434
+ function recordSessionAlias(oldName, entry) {
435
+ const key = oldName.trim();
436
+ if (!key)
437
+ return;
438
+ withFileLock(SESSION_ALIASES_LOCK, () => {
439
+ const all = readSessionAliases();
440
+ for (const [name, previous] of Object.entries(all)) {
441
+ if (previous.current === key)
442
+ all[name] = { ...previous, current: entry.current };
443
+ }
444
+ all[key] = entry;
445
+ writeJson(SESSION_ALIASES_FILE, all);
446
+ });
447
+ }
400
448
  function asMarker(value) {
401
449
  if (!value || typeof value !== "object")
402
450
  return null;
@@ -495,6 +543,8 @@ export function createStateStore(options = {}) {
495
543
  readCursor,
496
544
  writeCursor,
497
545
  clearCursor,
546
+ readSessionAliases,
547
+ recordSessionAlias,
498
548
  readBus,
499
549
  writeBus,
500
550
  clearBus,
@@ -88,6 +88,8 @@ export declare function buildSendKeysArgs(session: string, paneId: string, keys:
88
88
  export declare function buildWriteCharsArgs(session: string, paneId: string, chars: string): string[];
89
89
  export declare function buildRenamePaneArgs(session: string, paneId: string, name: string): string[];
90
90
  export declare function buildRenameTabArgs(session: string, tabId: number, name: string): string[];
91
+ /** Rename the session itself: `zellij --session <old> action rename-session <new>`. */
92
+ export declare function buildRenameSessionArgs(session: string, name: string): string[];
91
93
  export declare function buildFocusPaneArgs(session: string, paneId: string): string[];
92
94
  export declare function buildListTabsArgs(session: string): string[];
93
95
  export declare function buildDumpLayoutArgs(session: string): string[];
@@ -183,6 +183,10 @@ export function buildRenameTabArgs(session, tabId, name) {
183
183
  name,
184
184
  ];
185
185
  }
186
+ /** Rename the session itself: `zellij --session <old> action rename-session <new>`. */
187
+ export function buildRenameSessionArgs(session, name) {
188
+ return [...sessionPrefix(session), "action", "rename-session", name];
189
+ }
186
190
  export function buildFocusPaneArgs(session, paneId) {
187
191
  return [
188
192
  ...sessionPrefix(session),
@@ -1,11 +1,13 @@
1
1
  import { ListingCache } from "./cache.js";
2
- import { buildClosePaneArgs, buildDumpArgs, buildListPanesArgs, buildNewPaneArgs, buildNewTabArgs, buildPasteArgs, buildSendEnterArgs, buildSendKeysArgs, buildWriteCharsArgs, type WaitRequest, type LaunchPluginInput, type NewPaneInput, type NewTabInput, type PipeInput } from "./args.js";
2
+ import { buildClosePaneArgs, buildDumpArgs, buildListPanesArgs, buildNewPaneArgs, buildNewTabArgs, buildPasteArgs, buildRenameSessionArgs, buildSendEnterArgs, buildSendKeysArgs, buildWriteCharsArgs, type WaitRequest, type LaunchPluginInput, type NewPaneInput, type NewTabInput, type PipeInput } from "./args.js";
3
3
  import { resolveTab, type ZellijTab } from "./tabs.js";
4
4
  import { type IpcDiscoveryState } from "../exec.js";
5
5
  import { type SenderOrigin } from "../ops/delivery.js";
6
6
  import { type ZellijExecFn } from "./binary.js";
7
7
  import { normalizePaneId, resolvePane, type ZellijPane } from "./panes.js";
8
+ import { type ProcInfo } from "./server-identity.js";
8
9
  import { type ZellijSession, type ZellijSessionResolve } from "./session.js";
10
+ import type { SessionAliasEntry, SessionAliasMap } from "../state.js";
9
11
  export type ZellijClientOptions = {
10
12
  exec?: ZellijExecFn;
11
13
  zellijPath?: string;
@@ -18,7 +20,18 @@ export type ZellijClientOptions = {
18
20
  cache?: ListingCache | false;
19
21
  /** Clock for cache TTLs; process deadlines continue using wall time. */
20
22
  now?: () => number;
23
+ /** Rename memory (StateStore); injected by dispatch, or by tests. */
24
+ aliases?: {
25
+ readSessionAliases(): SessionAliasMap;
26
+ recordSessionAlias(oldName: string, entry: SessionAliasEntry): void;
27
+ };
28
+ /** Process-table snapshot override (tests); defaults to a real read. */
29
+ processTable?: () => ProcInfo[] | Promise<ProcInfo[]>;
30
+ /** Platform override (tests); defaults to process.platform. */
31
+ platform?: NodeJS.Platform;
21
32
  };
33
+ /** Test helper: forget every cached self resolution (and its process table). */
34
+ export declare function resetSelfResolutionCache(): void;
22
35
  export type ZellijTransport = {
23
36
  kind: "local" | "ssh";
24
37
  mode: "local" | "ssh" | "interactive";
@@ -33,7 +46,12 @@ export type ZellijTransport = {
33
46
  export declare function createZellijClient(options?: ZellijClientOptions): {
34
47
  zellijPath: string;
35
48
  selfPaneId: string | null;
36
- selfSession: string | null;
49
+ /**
50
+ * The caller's session: the resolved live name once resolveSelf (or a
51
+ * self resolveSession) has run, else the raw ZELLIJ_SESSION_NAME. A getter
52
+ * so a rename resolution is visible to the self-pane guard.
53
+ */
54
+ readonly selfSession: string | null;
37
55
  contextKey: () => string;
38
56
  invalidateListings: (session: string) => void;
39
57
  observeManifest: (session: string, revision: string) => void;
@@ -45,6 +63,7 @@ export declare function createZellijClient(options?: ZellijClientOptions): {
45
63
  }) => Promise<ZellijSession[]>;
46
64
  deleteSession: (name: string, callTimeoutMs?: number) => Promise<void>;
47
65
  resolveSession: (explicit?: string | null, callTimeoutMs?: number) => Promise<ZellijSessionResolve>;
66
+ resolveSelf: () => Promise<string | null>;
48
67
  listPanes: (session: string, callTimeoutMs?: number, opts?: {
49
68
  fresh?: boolean;
50
69
  }) => Promise<ZellijPane[]>;
@@ -121,6 +140,14 @@ export declare function createZellijClient(options?: ZellijClientOptions): {
121
140
  session: string;
122
141
  name: string;
123
142
  }>;
143
+ renameSession: (input: {
144
+ session: string;
145
+ name: string;
146
+ callTimeoutMs?: number;
147
+ }) => Promise<{
148
+ session: string;
149
+ was: string;
150
+ }>;
124
151
  focusPane: (input: {
125
152
  session: string;
126
153
  paneId: string;
@@ -194,5 +221,6 @@ export declare function createZellijClient(options?: ZellijClientOptions): {
194
221
  buildClosePaneArgs: typeof buildClosePaneArgs;
195
222
  buildNewPaneArgs: typeof buildNewPaneArgs;
196
223
  buildNewTabArgs: typeof buildNewTabArgs;
224
+ buildRenameSessionArgs: typeof buildRenameSessionArgs;
197
225
  };
198
226
  export type ZellijClient = ReturnType<typeof createZellijClient>;