@zswarm/core 0.8.0 → 0.8.1

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/gate.js CHANGED
@@ -1,4 +1,4 @@
1
- import { appendFileSync, existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
1
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { ZellijError } from "../errors.js";
4
4
  import { dispatchZswarm } from "../ops/dispatch.js";
@@ -61,15 +61,6 @@ async function readStdin(limit) {
61
61
  }
62
62
  return Buffer.concat(chunks).toString("utf8");
63
63
  }
64
- function cliPath() {
65
- const script = process.argv[1] ?? "";
66
- try {
67
- return realpathSync(script);
68
- }
69
- catch {
70
- return script;
71
- }
72
- }
73
64
  const s = (v) => (typeof v === "string" ? v : undefined);
74
65
  export function gateAllow(id, flags, env = process.env) {
75
66
  id = validateGateId(id);
@@ -83,7 +74,7 @@ export function gateAllow(id, flags, env = process.env) {
83
74
  const key = s(flags["--key"]) ?? (keyFile ? readFileSync(keyFile, "utf8") : undefined);
84
75
  if (!key)
85
76
  throw new ZellijError("usage", "gate allow needs --key 'ssh-ed25519 AAAA…' or --key-file PATH (the sender's public key)");
86
- const command = gateForcedCommand({ id, node: process.execPath, cli: cliPath(), env });
77
+ const command = gateForcedCommand({ id, bin: s(flags["--bin"]), env });
87
78
  const line = gateKeyLine({ id, key, command, from: s(flags["--from"]) });
88
79
  const keysPath = s(flags["--authorized-keys"]) ?? defaultAuthorizedKeysPath(env);
89
80
  mkdirSync(dirname(keysPath), { recursive: true, mode: 0o700 });
@@ -119,10 +110,11 @@ export function gateList(flags, env = process.env) {
119
110
  };
120
111
  }
121
112
  export const GATE_USAGE = [
122
- "zswarm gate allow ID (--key 'ssh-ed25519 AAAA…' | --key-file PUB) --session S --pane P [--from ADDRS] [--authorized-keys PATH]",
113
+ "zswarm gate allow ID (--key 'ssh-ed25519 AAAA…' | --key-file PUB) --session S --pane P [--from ADDRS] [--bin PATH] [--authorized-keys PATH]",
123
114
  " on the receiving machine: let the key send to pane P in session S as ID. Writes a forced-command line",
124
115
  " (send only: no shell, no pty, no forwarding) to authorized_keys and the target to gate.json (ZSWARM_GATE_CONFIG,",
125
- " default ~/.zswarm/gate.json); --from limits the source addresses (sshd from= patterns)",
116
+ " default ~/.zswarm/gate.json); --from limits the source addresses (sshd from= patterns); the command runs",
117
+ " `zswarm` from this shell's PATH (kept across upgrades), or --bin PATH",
126
118
  "zswarm gate revoke ID [--session S --pane P] drop ID from one target or all; the key line goes when nothing is left",
127
119
  "zswarm gate list targets, allowed ids and gate keys",
128
120
  "zswarm gate ID the forced command itself: reads one request on stdin and sends locally (sshd runs this)",
@@ -67,12 +67,14 @@ export declare function authorizeGateRequest(config: GateConfig, id: string, raw
67
67
  /**
68
68
  * The forced command for one id. sshd gives it a bare environment, so the PATH
69
69
  * (and on macOS the per-user TMPDIR that Zellij keeps its sockets under) of the
70
- * process running `gate allow` is written into the command.
70
+ * process running `gate allow` is written into the command. The launcher is
71
+ * `zswarm` looked up on that PATH, not the resolved script: package managers
72
+ * install each version under its own directory, so a resolved path breaks on
73
+ * the next upgrade. `bin` pins an explicit launcher instead.
71
74
  */
72
75
  export declare function gateForcedCommand(input: {
73
76
  id: string;
74
- node: string;
75
- cli: string;
77
+ bin?: string;
76
78
  platform?: NodeJS.Platform;
77
79
  env?: NodeJS.ProcessEnv;
78
80
  }): string;
package/dist/ops/gate.js CHANGED
@@ -223,19 +223,20 @@ function posixQuote(arg) {
223
223
  /**
224
224
  * The forced command for one id. sshd gives it a bare environment, so the PATH
225
225
  * (and on macOS the per-user TMPDIR that Zellij keeps its sockets under) of the
226
- * process running `gate allow` is written into the command.
226
+ * process running `gate allow` is written into the command. The launcher is
227
+ * `zswarm` looked up on that PATH, not the resolved script: package managers
228
+ * install each version under its own directory, so a resolved path breaks on
229
+ * the next upgrade. `bin` pins an explicit launcher instead.
227
230
  */
228
231
  export function gateForcedCommand(input) {
229
232
  const platform = input.platform ?? process.platform;
230
233
  const env = input.env ?? process.env;
234
+ const bin = input.bin?.trim() || "zswarm";
231
235
  if (platform === "win32") {
232
236
  // Unquoted paths run the same in cmd and PowerShell (OpenSSH's DefaultShell
233
237
  // may be either); a path with spaces needs quoting, which only cmd accepts.
234
- const q = (s) => {
235
- const p = s.replace(/\\/g, "/");
236
- return /\s/.test(p) ? `"${p}"` : p;
237
- };
238
- return `${q(input.node)} ${q(input.cli)} gate ${input.id}`;
238
+ const p = bin.replace(/\\/g, "/");
239
+ return `${/\s/.test(p) ? `"${p}"` : p} gate ${input.id}`;
239
240
  }
240
241
  const vars = [];
241
242
  if (env.PATH)
@@ -246,7 +247,7 @@ export function gateForcedCommand(input) {
246
247
  vars.push(`ZSWARM_STATE_DIR=${posixQuote(env.ZSWARM_STATE_DIR)}`);
247
248
  if (env.ZSWARM_GATE_CONFIG)
248
249
  vars.push(`ZSWARM_GATE_CONFIG=${posixQuote(env.ZSWARM_GATE_CONFIG)}`);
249
- return ["env", ...vars, posixQuote(input.node), posixQuote(input.cli), "gate", input.id].join(" ");
250
+ return ["env", ...vars, posixQuote(bin), "gate", input.id].join(" ");
250
251
  }
251
252
  const KEY_TYPES = /^(ssh-ed25519|ssh-rsa|ecdsa-sha2-nistp(256|384|521)|sk-ssh-ed25519@openssh\.com|sk-ecdsa-sha2-nistp256@openssh\.com)$/;
252
253
  /** `type base64 [comment]` → `type base64`; anything else is refused. */
@@ -323,8 +323,15 @@ export function createZellijClient(options = {}) {
323
323
  creation: server.creationName,
324
324
  at: now(),
325
325
  };
326
- aliases.recordSessionAlias(candidate, entry);
327
- if (server.creationName && server.creationName !== candidate) {
326
+ // Remember only names that no live session carries. A live name that
327
+ // resolved elsewhere was reused, or came from a process whose env is
328
+ // stale (a daemon started from another session's pane); recording it
329
+ // would send every explicit --session NAME to the wrong session
330
+ // (bacloud, 2026-10-06: cratchet -> astro-dogster).
331
+ const liveName = (name) => live.some((session) => session.name === name);
332
+ if (!liveName(candidate))
333
+ aliases.recordSessionAlias(candidate, entry);
334
+ if (server.creationName && server.creationName !== candidate && !liveName(server.creationName)) {
328
335
  aliases.recordSessionAlias(server.creationName, entry);
329
336
  }
330
337
  }
@@ -345,9 +352,21 @@ export function createZellijClient(options = {}) {
345
352
  if (!isSelf) {
346
353
  if (aliases) {
347
354
  const entry = aliases.readSessionAliases()[candidate];
348
- if (entry?.serverPid != null) {
355
+ if (entry?.serverPid != null && entry.current !== candidate) {
349
356
  const table = await processTable();
350
357
  if (liveServers(table).some((server) => server.pid === entry.serverPid)) {
358
+ // A live session with exactly the requested name wins over a
359
+ // remembered rename; the listing is only paid when an alias would
360
+ // redirect. If the listing fails, the alias still applies.
361
+ try {
362
+ const liveNow = await listSessions(callTimeoutMs);
363
+ if (liveNow.some((session) => !session.exited && session.name === candidate)) {
364
+ return fromEnv;
365
+ }
366
+ }
367
+ catch {
368
+ // fall through to the alias
369
+ }
351
370
  return { session: entry.current, source: "alias", requested: candidate };
352
371
  }
353
372
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zswarm/core",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "type": "module",
5
5
  "description": "zSwarm Zellij client and shared ops dispatch",
6
6
  "exports": {