@zswarm/core 0.1.3 → 0.1.5
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/exec.d.ts +2 -0
- package/dist/exec.js +18 -2
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/ops/broadcast.d.ts +1 -1
- package/dist/ops/broadcast.js +9 -5
- package/dist/ops/bus.d.ts +10 -7
- package/dist/ops/bus.js +119 -86
- package/dist/ops/delivery.d.ts +31 -1
- package/dist/ops/delivery.js +53 -2
- package/dist/ops/dispatch.js +40 -18
- package/dist/ops/panes.d.ts +4 -3
- package/dist/ops/panes.js +14 -4
- package/dist/ops/serve.d.ts +10 -1
- package/dist/ops/serve.js +99 -26
- package/dist/ops/types.d.ts +2 -0
- package/dist/ops/util.d.ts +1 -0
- package/dist/ops/util.js +5 -0
- package/dist/ops/wait.d.ts +8 -1
- package/dist/ops/wait.js +7 -3
- package/dist/schema.js +6 -3
- package/dist/state.d.ts +7 -6
- package/dist/state.js +160 -23
- package/dist/zellij/args.d.ts +2 -0
- package/dist/zellij/args.js +1 -0
- package/dist/zellij/binary.d.ts +3 -1
- package/dist/zellij/binary.js +34 -2
- package/dist/zellij/bus.d.ts +9 -0
- package/dist/zellij/bus.js +20 -0
- package/dist/zellij/client.d.ts +1 -0
- package/dist/zellij/client.js +17 -3
- package/dist/zellij/session.d.ts +2 -0
- package/dist/zellij/session.js +4 -0
- package/package.json +2 -2
package/dist/state.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
import { appendFileSync, closeSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { appendFileSync, closeSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
const LOG_FILE = "log.jsonl";
|
|
5
5
|
const SIGNALS_FILE = "signals.json";
|
|
6
6
|
const SIGNALS_LOCK = "signals.lock";
|
|
7
7
|
const CURSORS_FILE = "cursors.json";
|
|
8
|
+
const CURSORS_LOCK = "cursors.lock";
|
|
8
9
|
const BUS_FILE = "bus.json";
|
|
9
10
|
/** Keeps the log bounded without needing a rotation daemon. */
|
|
10
11
|
const LOG_TAIL_BYTES = 512 * 1024;
|
|
11
12
|
const LOCK_WAIT_MS = 5_000;
|
|
13
|
+
/** A live pid older than this is treated as a recycle of a crashed holder. */
|
|
14
|
+
const LOCK_STALE_MS = 30_000;
|
|
12
15
|
function sleepSync(ms) {
|
|
13
16
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
14
17
|
}
|
|
@@ -77,32 +80,112 @@ export function createStateStore(options = {}) {
|
|
|
77
80
|
}
|
|
78
81
|
return out;
|
|
79
82
|
}
|
|
80
|
-
function
|
|
83
|
+
function pidAlive(pid) {
|
|
84
|
+
try {
|
|
85
|
+
process.kill(pid, 0);
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
// ESRCH: gone. EPERM: exists, just unsignalable — still a live holder.
|
|
90
|
+
return err.code === "EPERM";
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** Owner recorded in the lock file so a crash can be distinguished from a live holder. */
|
|
94
|
+
function readLockOwner(lockPath) {
|
|
95
|
+
try {
|
|
96
|
+
const rec = JSON.parse(readFileSync(lockPath, "utf8"));
|
|
97
|
+
if (typeof rec.pid !== "number" || !Number.isInteger(rec.pid) || rec.pid <= 0)
|
|
98
|
+
return null;
|
|
99
|
+
const at = typeof rec.at === "number" ? rec.at : 0;
|
|
100
|
+
return { pid: rec.pid, at };
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Dead pid → steal now. This process's own leftover (unlink failed, or a
|
|
108
|
+
* non-reentrant re-entry) → steal now. A live pid whose `at` is older than
|
|
109
|
+
* LOCK_STALE_MS is a recycled pid, not a holder still inside fn().
|
|
110
|
+
* Empty leftover from older writers (wx with no owner bytes) → steal once
|
|
111
|
+
* mtime is older than the wait, so an in-flight create is not yanked out
|
|
112
|
+
* from under the holder.
|
|
113
|
+
* Owner is read once: a second read can see a pid that appeared after an
|
|
114
|
+
* empty snapshot and would steal a live lock.
|
|
115
|
+
*/
|
|
116
|
+
function lockIsStale(lockPath) {
|
|
117
|
+
const owner = readLockOwner(lockPath);
|
|
118
|
+
if (owner) {
|
|
119
|
+
if (owner.pid === process.pid)
|
|
120
|
+
return true;
|
|
121
|
+
if (!pidAlive(owner.pid))
|
|
122
|
+
return true;
|
|
123
|
+
return Date.now() - owner.at >= LOCK_STALE_MS;
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
return Date.now() - statSync(lockPath).mtimeMs >= LOCK_WAIT_MS;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function lockBusy(code) {
|
|
133
|
+
// Unix: O_EXCL on an existing file is EEXIST. Windows: a holder that still
|
|
134
|
+
// has the handle open (or a delete-pending name) is EPERM / EACCES / EBUSY.
|
|
135
|
+
return (code === "EEXIST" ||
|
|
136
|
+
code === "EPERM" ||
|
|
137
|
+
code === "EACCES" ||
|
|
138
|
+
code === "EBUSY");
|
|
139
|
+
}
|
|
140
|
+
function unlinkLock(lockPath) {
|
|
141
|
+
const until = Date.now() + 500;
|
|
142
|
+
while (true) {
|
|
143
|
+
try {
|
|
144
|
+
rmSync(lockPath, { force: true });
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
if (!lockBusy(err.code) || Date.now() >= until) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
sleepSync(10);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function withFileLock(lockName, fn) {
|
|
81
156
|
ensureDir();
|
|
82
|
-
const lockPath = join(dir,
|
|
157
|
+
const lockPath = join(dir, lockName);
|
|
83
158
|
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
84
159
|
while (true) {
|
|
85
160
|
try {
|
|
86
161
|
const fd = openSync(lockPath, "wx");
|
|
87
162
|
try {
|
|
163
|
+
writeFileSync(fd, JSON.stringify({ pid: process.pid, at: Date.now() }));
|
|
88
164
|
return fn();
|
|
89
165
|
}
|
|
90
166
|
finally {
|
|
91
167
|
closeSync(fd);
|
|
92
|
-
|
|
168
|
+
unlinkLock(lockPath);
|
|
93
169
|
}
|
|
94
170
|
}
|
|
95
171
|
catch (err) {
|
|
96
172
|
const code = err.code;
|
|
97
|
-
if (code
|
|
173
|
+
if (!lockBusy(code))
|
|
98
174
|
throw err;
|
|
175
|
+
if (lockIsStale(lockPath)) {
|
|
176
|
+
unlinkLock(lockPath);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
99
179
|
if (Date.now() >= deadline) {
|
|
100
|
-
throw new Error(`timed out waiting for ${
|
|
180
|
+
throw new Error(`timed out waiting for ${lockName}`);
|
|
101
181
|
}
|
|
102
182
|
sleepSync(10);
|
|
103
183
|
}
|
|
104
184
|
}
|
|
105
185
|
}
|
|
186
|
+
function withSignalsLock(fn) {
|
|
187
|
+
return withFileLock(SIGNALS_LOCK, fn);
|
|
188
|
+
}
|
|
106
189
|
function readSignals() {
|
|
107
190
|
return readJson(SIGNALS_FILE, {});
|
|
108
191
|
}
|
|
@@ -136,31 +219,85 @@ export function createStateStore(options = {}) {
|
|
|
136
219
|
return all[key] ?? null;
|
|
137
220
|
}
|
|
138
221
|
function writeCursor(key, text) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
222
|
+
withFileLock(CURSORS_LOCK, () => {
|
|
223
|
+
const all = readJson(CURSORS_FILE, {});
|
|
224
|
+
all[key] = text;
|
|
225
|
+
writeJson(CURSORS_FILE, all);
|
|
226
|
+
});
|
|
142
227
|
}
|
|
143
228
|
function clearCursor(key) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
229
|
+
withFileLock(CURSORS_LOCK, () => {
|
|
230
|
+
const all = readJson(CURSORS_FILE, {});
|
|
231
|
+
delete all[key];
|
|
232
|
+
writeJson(CURSORS_FILE, all);
|
|
233
|
+
});
|
|
147
234
|
}
|
|
148
|
-
function
|
|
149
|
-
|
|
150
|
-
|
|
235
|
+
function asMarker(value) {
|
|
236
|
+
if (!value || typeof value !== "object")
|
|
237
|
+
return null;
|
|
238
|
+
const rec = value;
|
|
239
|
+
if (typeof rec.plugin !== "string" || !rec.plugin)
|
|
151
240
|
return null;
|
|
152
|
-
|
|
241
|
+
if (typeof rec.configKey !== "string" || !rec.configKey)
|
|
242
|
+
return null;
|
|
243
|
+
return {
|
|
244
|
+
plugin: rec.plugin,
|
|
245
|
+
configKey: rec.configKey,
|
|
246
|
+
installedAt: typeof rec.installedAt === "number" ? rec.installedAt : 0,
|
|
247
|
+
};
|
|
153
248
|
}
|
|
154
|
-
|
|
155
|
-
|
|
249
|
+
/**
|
|
250
|
+
* Current `{ sessions: { <name>: marker } }` plus the pre-0.1.6 flat file
|
|
251
|
+
* `{ plugin, configKey, installedAt }`, which any session may inherit until
|
|
252
|
+
* the next write namespaces it.
|
|
253
|
+
*/
|
|
254
|
+
function readBusFile() {
|
|
255
|
+
const raw = readJson(BUS_FILE, null);
|
|
256
|
+
if (!raw || typeof raw !== "object")
|
|
257
|
+
return { sessions: {}, legacy: null };
|
|
258
|
+
const sessions = {};
|
|
259
|
+
if (raw.sessions && typeof raw.sessions === "object") {
|
|
260
|
+
for (const [name, marker] of Object.entries(raw.sessions)) {
|
|
261
|
+
const parsed = asMarker(marker);
|
|
262
|
+
if (parsed)
|
|
263
|
+
sessions[name] = parsed;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return { sessions, legacy: asMarker(raw) };
|
|
156
267
|
}
|
|
157
|
-
function
|
|
158
|
-
|
|
159
|
-
|
|
268
|
+
function readBus(session) {
|
|
269
|
+
if (!session)
|
|
270
|
+
return null;
|
|
271
|
+
const { sessions, legacy } = readBusFile();
|
|
272
|
+
return sessions[session] ?? legacy;
|
|
273
|
+
}
|
|
274
|
+
function writeBus(session, marker) {
|
|
275
|
+
const { sessions } = readBusFile();
|
|
276
|
+
sessions[session] = marker;
|
|
277
|
+
writeJson(BUS_FILE, { sessions });
|
|
278
|
+
}
|
|
279
|
+
function clearBus(session) {
|
|
280
|
+
if (!session) {
|
|
281
|
+
try {
|
|
282
|
+
rmSync(join(dir, BUS_FILE), { force: true });
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
// Nothing to forget.
|
|
286
|
+
}
|
|
287
|
+
return;
|
|
160
288
|
}
|
|
161
|
-
|
|
162
|
-
|
|
289
|
+
const { sessions } = readBusFile();
|
|
290
|
+
delete sessions[session];
|
|
291
|
+
if (Object.keys(sessions).length === 0) {
|
|
292
|
+
try {
|
|
293
|
+
rmSync(join(dir, BUS_FILE), { force: true });
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
// Nothing to forget.
|
|
297
|
+
}
|
|
298
|
+
return;
|
|
163
299
|
}
|
|
300
|
+
writeJson(BUS_FILE, { sessions });
|
|
164
301
|
}
|
|
165
302
|
/** Test helper: drop everything this store wrote. */
|
|
166
303
|
function reset() {
|
package/dist/zellij/args.d.ts
CHANGED
|
@@ -47,6 +47,8 @@ export type WaitRequest = {
|
|
|
47
47
|
idleMs?: number;
|
|
48
48
|
timeoutMs?: number;
|
|
49
49
|
pollMs?: number;
|
|
50
|
+
/** Read scrollback above the viewport, matching dump --full. */
|
|
51
|
+
full?: boolean;
|
|
50
52
|
};
|
|
51
53
|
/**
|
|
52
54
|
* `regex` is deliberately absent: the plugin has no regex engine, refuses such
|
package/dist/zellij/args.js
CHANGED
package/dist/zellij/binary.d.ts
CHANGED
|
@@ -10,10 +10,12 @@ export declare function resolveZellijBinary(env?: NodeJS.ProcessEnv): string;
|
|
|
10
10
|
export declare function sanitizeZellijEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
11
11
|
/**
|
|
12
12
|
* Remote crew: `ZSWARM_SSH=user@host` routes every zellij call over ssh.
|
|
13
|
-
* `ZSWARM_SSH_OPTS` is split
|
|
13
|
+
* `ZSWARM_SSH_OPTS` is split like a shell so quoted paths stay one arg;
|
|
14
|
+
* `BatchMode` keeps it non-interactive.
|
|
14
15
|
* `ZSWARM_TMP` points at the interactive session's temp (or `auto` to discover).
|
|
15
16
|
* `ZSWARM_SSH_MODE=interactive` runs each call in the Windows desktop session.
|
|
16
17
|
*/
|
|
18
|
+
export declare function parseSshOpts(raw: string): string[];
|
|
17
19
|
export declare function resolveSshTarget(env?: NodeJS.ProcessEnv): SshTarget | null;
|
|
18
20
|
/** Runner bound to the resolved zellij binary and a cleaned environment. */
|
|
19
21
|
export declare function defaultExec(zellijPath: string, env: NodeJS.ProcessEnv): ZellijExecFn;
|
package/dist/zellij/binary.js
CHANGED
|
@@ -75,15 +75,47 @@ export function sanitizeZellijEnv(env) {
|
|
|
75
75
|
}
|
|
76
76
|
/**
|
|
77
77
|
* Remote crew: `ZSWARM_SSH=user@host` routes every zellij call over ssh.
|
|
78
|
-
* `ZSWARM_SSH_OPTS` is split
|
|
78
|
+
* `ZSWARM_SSH_OPTS` is split like a shell so quoted paths stay one arg;
|
|
79
|
+
* `BatchMode` keeps it non-interactive.
|
|
79
80
|
* `ZSWARM_TMP` points at the interactive session's temp (or `auto` to discover).
|
|
80
81
|
* `ZSWARM_SSH_MODE=interactive` runs each call in the Windows desktop session.
|
|
81
82
|
*/
|
|
83
|
+
export function parseSshOpts(raw) {
|
|
84
|
+
const out = [];
|
|
85
|
+
let cur = "";
|
|
86
|
+
let quote = null;
|
|
87
|
+
for (let i = 0; i < raw.length; i++) {
|
|
88
|
+
const ch = raw[i];
|
|
89
|
+
if (quote) {
|
|
90
|
+
if (ch === quote) {
|
|
91
|
+
quote = null;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
cur += ch;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
else if (ch === '"' || ch === "'") {
|
|
98
|
+
quote = ch;
|
|
99
|
+
}
|
|
100
|
+
else if (/\s/.test(ch)) {
|
|
101
|
+
if (cur.length > 0) {
|
|
102
|
+
out.push(cur);
|
|
103
|
+
cur = "";
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
cur += ch;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (cur.length > 0)
|
|
111
|
+
out.push(cur);
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
82
114
|
export function resolveSshTarget(env = process.env) {
|
|
83
115
|
const host = env.ZSWARM_SSH?.trim();
|
|
84
116
|
if (!host)
|
|
85
117
|
return null;
|
|
86
|
-
const options = (env.ZSWARM_SSH_OPTS ?? "")
|
|
118
|
+
const options = parseSshOpts(env.ZSWARM_SSH_OPTS ?? "");
|
|
87
119
|
if (!options.some((o) => o.startsWith("BatchMode"))) {
|
|
88
120
|
options.unshift("-o", "BatchMode=yes");
|
|
89
121
|
}
|
package/dist/zellij/bus.d.ts
CHANGED
|
@@ -67,6 +67,15 @@ export declare function resolveBusPlugin(env?: NodeJS.ProcessEnv): string | null
|
|
|
67
67
|
export declare function busPluginUrl(pluginPath: string): string;
|
|
68
68
|
/** zswarm-bus → zswarm-bus-2 → zswarm-bus-3: a fresh pipe destination each time. */
|
|
69
69
|
export declare function nextConfigKey(key: string): string;
|
|
70
|
+
/**
|
|
71
|
+
* Plugin panes belonging to this bus — never tab-bar / status-bar / room.
|
|
72
|
+
* Titles are the `file:` URL (or the wasm basename) Zellij shows for the pane.
|
|
73
|
+
*/
|
|
74
|
+
export declare function isBusPluginPane(pane: {
|
|
75
|
+
isPlugin?: boolean;
|
|
76
|
+
title?: string;
|
|
77
|
+
command?: string | null;
|
|
78
|
+
}, pluginPath?: string | null): boolean;
|
|
70
79
|
/**
|
|
71
80
|
* Pull the plugin's answer out of the pipe. Zellij interleaves its own notices
|
|
72
81
|
* ("Action CliPipe did not complete within 1s timeout" shows up on successful
|
package/dist/zellij/bus.js
CHANGED
|
@@ -140,6 +140,26 @@ export function nextConfigKey(key) {
|
|
|
140
140
|
return `${key}-2`;
|
|
141
141
|
return `${match[1]}-${Number(match[2]) + 1}`;
|
|
142
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Plugin panes belonging to this bus — never tab-bar / status-bar / room.
|
|
145
|
+
* Titles are the `file:` URL (or the wasm basename) Zellij shows for the pane.
|
|
146
|
+
*/
|
|
147
|
+
export function isBusPluginPane(pane, pluginPath) {
|
|
148
|
+
if (!pane.isPlugin)
|
|
149
|
+
return false;
|
|
150
|
+
const hay = `${pane.title ?? ""}\n${pane.command ?? ""}`
|
|
151
|
+
.replace(/\\/g, "/")
|
|
152
|
+
.toLowerCase();
|
|
153
|
+
if (hay.includes("zswarm-bus") || hay.includes("zswarm-events"))
|
|
154
|
+
return true;
|
|
155
|
+
if (!pluginPath)
|
|
156
|
+
return false;
|
|
157
|
+
const normalized = pluginPath.replace(/\\/g, "/").toLowerCase();
|
|
158
|
+
if (normalized && hay.includes(normalized))
|
|
159
|
+
return true;
|
|
160
|
+
const base = normalized.split("/").pop();
|
|
161
|
+
return Boolean(base && hay.includes(base));
|
|
162
|
+
}
|
|
143
163
|
function toSnapshot(value) {
|
|
144
164
|
if (value.ok !== true || !Array.isArray(value.panes))
|
|
145
165
|
return null;
|
package/dist/zellij/client.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export type ZellijClientOptions = {
|
|
|
8
8
|
zellijPath?: string;
|
|
9
9
|
timeoutMs?: number;
|
|
10
10
|
env?: NodeJS.ProcessEnv;
|
|
11
|
+
signal?: AbortSignal;
|
|
11
12
|
};
|
|
12
13
|
/** Thin, stateless wrapper over the `zellij` binary. */
|
|
13
14
|
export declare function createZellijClient(options?: ZellijClientOptions): {
|
package/dist/zellij/client.js
CHANGED
|
@@ -5,7 +5,7 @@ import { parseTabList, resolveTab } from "./tabs.js";
|
|
|
5
5
|
import { createSshExec } from "../exec.js";
|
|
6
6
|
import { DEFAULT_TIMEOUT_MS, NOT_FOUND_EXIT, defaultExec, resolveSshTarget, resolveZellijBinary, sanitizeZellijEnv, } from "./binary.js";
|
|
7
7
|
import { normalizePaneId, parsePaneList, resolvePane, } from "./panes.js";
|
|
8
|
-
import { parseSessionList, resolveSelfPaneId, sessionFromEnv, sessionFromList, } from "./session.js";
|
|
8
|
+
import { isZellijNoSessionsOutput, parseSessionList, resolveSelfPaneId, sessionFromEnv, sessionFromList, } from "./session.js";
|
|
9
9
|
/** Thin, stateless wrapper over the `zellij` binary. */
|
|
10
10
|
export function createZellijClient(options = {}) {
|
|
11
11
|
const env = options.env ?? process.env;
|
|
@@ -13,10 +13,11 @@ export function createZellijClient(options = {}) {
|
|
|
13
13
|
const ssh = options.exec ? null : resolveSshTarget(env);
|
|
14
14
|
const zellijPath = options.zellijPath ??
|
|
15
15
|
(ssh ? `ssh://${ssh.host}/${ssh.remoteBin}` : resolveZellijBinary(env));
|
|
16
|
-
const
|
|
16
|
+
const rawExec = options.exec ??
|
|
17
17
|
(ssh
|
|
18
18
|
? createSshExec(ssh, sanitizeZellijEnv(env))
|
|
19
19
|
: defaultExec(zellijPath, env));
|
|
20
|
+
const exec = (args, opts) => rawExec(args, { ...opts, signal: opts.signal ?? options.signal });
|
|
20
21
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
21
22
|
const selfPaneId = resolveSelfPaneId(env);
|
|
22
23
|
async function run(args, label) {
|
|
@@ -30,7 +31,20 @@ export function createZellijClient(options = {}) {
|
|
|
30
31
|
return result;
|
|
31
32
|
}
|
|
32
33
|
async function listSessions() {
|
|
33
|
-
const result = await
|
|
34
|
+
const result = await exec(["list-sessions", "--short", "--no-formatting"], {
|
|
35
|
+
timeoutMs,
|
|
36
|
+
});
|
|
37
|
+
if (result.code === NOT_FOUND_EXIT) {
|
|
38
|
+
throw new ZellijError("zellij_missing", `zellij binary not found (${zellijPath}); install Zellij ≥ 0.42, add it to PATH, or set ZSWARM_BIN / ZSWARM_PATH`);
|
|
39
|
+
}
|
|
40
|
+
if (result.code !== 0) {
|
|
41
|
+
// Nonzero here is Zellij's normal "none running", not a crash. Empty
|
|
42
|
+
// lets resolveSession throw zellij_no_session so unworktree can treat
|
|
43
|
+
// it as no occupants instead of zellij_failed.
|
|
44
|
+
if (isZellijNoSessionsOutput(result.stdout, result.stderr))
|
|
45
|
+
return [];
|
|
46
|
+
throw new ZellijError("zellij_failed", `zellij list-sessions failed (exit ${result.code}): ${result.stderr.trim() || result.stdout.trim() || "no output"}`);
|
|
47
|
+
}
|
|
34
48
|
return parseSessionList(result.stdout);
|
|
35
49
|
}
|
|
36
50
|
async function resolveSession(explicit) {
|
package/dist/zellij/session.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export type ZellijSessionResolve = {
|
|
|
8
8
|
*/
|
|
9
9
|
export declare function resolveSelfPaneId(env?: NodeJS.ProcessEnv): string | null;
|
|
10
10
|
export declare function parseSessionList(stdout: string): string[];
|
|
11
|
+
/** Zellij exits 1 and prints this when the machine simply has no sessions. */
|
|
12
|
+
export declare function isZellijNoSessionsOutput(stdout: string, stderr: string): boolean;
|
|
11
13
|
/** Session from an explicit argument or the environment, before asking Zellij. */
|
|
12
14
|
export declare function sessionFromEnv(env: NodeJS.ProcessEnv, explicit?: string | null): ZellijSessionResolve | null;
|
|
13
15
|
/** Pick a session when the environment gave no answer. */
|
package/dist/zellij/session.js
CHANGED
|
@@ -19,6 +19,10 @@ export function parseSessionList(stdout) {
|
|
|
19
19
|
.map((l) => l.trim())
|
|
20
20
|
.filter(Boolean);
|
|
21
21
|
}
|
|
22
|
+
/** Zellij exits 1 and prints this when the machine simply has no sessions. */
|
|
23
|
+
export function isZellijNoSessionsOutput(stdout, stderr) {
|
|
24
|
+
return /no active zellij sessions found/i.test(`${stderr}\n${stdout}`);
|
|
25
|
+
}
|
|
22
26
|
/** Session from an explicit argument or the environment, before asking Zellij. */
|
|
23
27
|
export function sessionFromEnv(env, explicit) {
|
|
24
28
|
const arg = explicit?.trim();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zswarm/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "zSwarm Zellij client and shared ops dispatch",
|
|
6
6
|
"exports": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"access": "public"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@zswarm/wasm": "0.1.
|
|
28
|
+
"@zswarm/wasm": "0.1.2"
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|
|
31
31
|
"build": "tsc -p tsconfig.json",
|