opencode-zellij 0.0.7 → 0.0.8
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/index.mjs +168 -207
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
2
|
import { execFile, spawn, spawnSync } from "node:child_process";
|
|
3
|
-
import { randomUUID } from "node:crypto";
|
|
4
3
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
-
import {
|
|
4
|
+
import { readFile, rename, rm } from "node:fs/promises";
|
|
6
5
|
import path, { basename, dirname, join } from "node:path";
|
|
7
6
|
import { fileURLToPath } from "node:url";
|
|
8
7
|
import { promisify } from "node:util";
|
|
9
8
|
import { homedir, tmpdir } from "node:os";
|
|
10
9
|
import { parseJSON, parseJSONC } from "confbox";
|
|
11
10
|
import { z } from "zod";
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
12
12
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
13
13
|
import { tool } from "@opencode-ai/plugin";
|
|
14
14
|
import { Buffer } from "node:buffer";
|
|
@@ -23,14 +23,10 @@ const PACKAGE_NAME = "opencode-zellij";
|
|
|
23
23
|
const NPM_REGISTRY_URL = "https://registry.npmjs.org/-/package/opencode-zellij/dist-tags";
|
|
24
24
|
const FETCH_TIMEOUT_MS = 5e3;
|
|
25
25
|
const INSTALL_TIMEOUT_MS = 6e4;
|
|
26
|
-
const LOCK_STALE_MS = INSTALL_TIMEOUT_MS * 2 + FETCH_TIMEOUT_MS + 3e4;
|
|
27
26
|
const defaultExecFile = promisify(execFile);
|
|
28
27
|
function packageDir(installRoot) {
|
|
29
28
|
return join(installRoot, "node_modules", PACKAGE_NAME);
|
|
30
29
|
}
|
|
31
|
-
function lockDir(installRoot) {
|
|
32
|
-
return join(installRoot, ".opencode-zellij-update.lock");
|
|
33
|
-
}
|
|
34
30
|
function backupDir(installRoot) {
|
|
35
31
|
return join(installRoot, "node_modules", `${PACKAGE_NAME}.update-backup`);
|
|
36
32
|
}
|
|
@@ -64,52 +60,6 @@ async function removeInstalledPackage(installRoot) {
|
|
|
64
60
|
recursive: true
|
|
65
61
|
});
|
|
66
62
|
}
|
|
67
|
-
async function installRootLockIsStale(installRoot) {
|
|
68
|
-
try {
|
|
69
|
-
const content = await readFile(join(lockDir(installRoot), "owner.json"), "utf8");
|
|
70
|
-
const owner = JSON.parse(content);
|
|
71
|
-
if (isRecord$1(owner) && typeof owner.createdAt === "number") return Date.now() - owner.createdAt > LOCK_STALE_MS;
|
|
72
|
-
} catch {
|
|
73
|
-
return true;
|
|
74
|
-
}
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
77
|
-
async function acquireInstallLock(installRoot) {
|
|
78
|
-
const dir = lockDir(installRoot);
|
|
79
|
-
const token = randomUUID();
|
|
80
|
-
try {
|
|
81
|
-
await mkdir(dir);
|
|
82
|
-
} catch {
|
|
83
|
-
if (!await installRootLockIsStale(installRoot)) return void 0;
|
|
84
|
-
await rm(dir, {
|
|
85
|
-
force: true,
|
|
86
|
-
recursive: true
|
|
87
|
-
});
|
|
88
|
-
try {
|
|
89
|
-
await mkdir(dir);
|
|
90
|
-
} catch {
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
await writeFile(join(dir, "owner.json"), JSON.stringify({
|
|
95
|
-
pid: process.pid,
|
|
96
|
-
token,
|
|
97
|
-
createdAt: Date.now()
|
|
98
|
-
}));
|
|
99
|
-
return async () => {
|
|
100
|
-
try {
|
|
101
|
-
const content = await readFile(join(dir, "owner.json"), "utf8");
|
|
102
|
-
const owner = JSON.parse(content);
|
|
103
|
-
if (isRecord$1(owner) && owner.token !== token) return;
|
|
104
|
-
} catch {
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
await rm(dir, {
|
|
108
|
-
force: true,
|
|
109
|
-
recursive: true
|
|
110
|
-
});
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
63
|
async function backupInstalledPackage(installRoot) {
|
|
114
64
|
const source = packageDir(installRoot);
|
|
115
65
|
if (!existsSync(source)) return void 0;
|
|
@@ -253,48 +203,36 @@ async function checkAndUpdate(options) {
|
|
|
253
203
|
reason: `cache spec is pinned or unknown (${context.cacheSpec})`
|
|
254
204
|
};
|
|
255
205
|
}
|
|
256
|
-
const
|
|
257
|
-
if (!
|
|
258
|
-
debug(
|
|
206
|
+
const latest = await fetchLatestVersion(options.fetchImpl);
|
|
207
|
+
if (!latest) {
|
|
208
|
+
debug("skipping auto-update: could not determine latest version");
|
|
259
209
|
return {
|
|
260
210
|
type: "skipped",
|
|
261
|
-
reason: "
|
|
211
|
+
reason: "could not determine latest version"
|
|
262
212
|
};
|
|
263
213
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
debug("skipping auto-update: could not determine latest version");
|
|
268
|
-
return {
|
|
269
|
-
type: "skipped",
|
|
270
|
-
reason: "could not determine latest version"
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
const installedVersion = (await installedPackageMetadata(context.installRoot))?.version ?? context.currentVersion;
|
|
274
|
-
if (latest === installedVersion) {
|
|
275
|
-
debug(`auto-update: already on latest ${latest}`);
|
|
276
|
-
return {
|
|
277
|
-
type: "up-to-date",
|
|
278
|
-
currentVersion: installedVersion
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
if (await runNpmInstall(context.installRoot, latest, options.execImpl)) {
|
|
282
|
-
debug(`updated ${PACKAGE_NAME} from ${installedVersion} to ${latest}`);
|
|
283
|
-
return {
|
|
284
|
-
type: "updated",
|
|
285
|
-
fromVersion: installedVersion,
|
|
286
|
-
toVersion: latest
|
|
287
|
-
};
|
|
288
|
-
}
|
|
214
|
+
const installedVersion = (await installedPackageMetadata(context.installRoot))?.version ?? context.currentVersion;
|
|
215
|
+
if (latest === installedVersion) {
|
|
216
|
+
debug(`auto-update: already on latest ${latest}`);
|
|
289
217
|
return {
|
|
290
|
-
type: "
|
|
291
|
-
currentVersion: installedVersion
|
|
292
|
-
latestVersion: latest,
|
|
293
|
-
reason: "npm install failed"
|
|
218
|
+
type: "up-to-date",
|
|
219
|
+
currentVersion: installedVersion
|
|
294
220
|
};
|
|
295
|
-
} finally {
|
|
296
|
-
await releaseLock();
|
|
297
221
|
}
|
|
222
|
+
if (await runNpmInstall(context.installRoot, latest, options.execImpl)) {
|
|
223
|
+
debug(`updated ${PACKAGE_NAME} from ${installedVersion} to ${latest}`);
|
|
224
|
+
return {
|
|
225
|
+
type: "updated",
|
|
226
|
+
fromVersion: installedVersion,
|
|
227
|
+
toVersion: latest
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
type: "failed",
|
|
232
|
+
currentVersion: installedVersion,
|
|
233
|
+
latestVersion: latest,
|
|
234
|
+
reason: "npm install failed"
|
|
235
|
+
};
|
|
298
236
|
}
|
|
299
237
|
//#endregion
|
|
300
238
|
//#region src/config.ts
|
|
@@ -416,80 +354,13 @@ async function loadConfig(input) {
|
|
|
416
354
|
};
|
|
417
355
|
}
|
|
418
356
|
//#endregion
|
|
419
|
-
//#region src/
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
const command = input.command.trim();
|
|
424
|
-
if (!command) throw new Error("command is required");
|
|
425
|
-
if (options.exitCodeToken) {
|
|
426
|
-
if (input.args && input.args.length > 0) return [
|
|
427
|
-
"bash",
|
|
428
|
-
"-lc",
|
|
429
|
-
directCommandExitWrapper,
|
|
430
|
-
"zellij-pty",
|
|
431
|
-
options.exitCodeToken,
|
|
432
|
-
command,
|
|
433
|
-
...input.args
|
|
434
|
-
];
|
|
435
|
-
return [
|
|
436
|
-
"bash",
|
|
437
|
-
"-lc",
|
|
438
|
-
shellCommandExitWrapper,
|
|
439
|
-
"zellij-pty",
|
|
440
|
-
options.exitCodeToken,
|
|
441
|
-
command
|
|
442
|
-
];
|
|
443
|
-
}
|
|
444
|
-
if (input.args && input.args.length > 0) return [command, ...input.args];
|
|
445
|
-
return [
|
|
446
|
-
"bash",
|
|
447
|
-
"-lc",
|
|
448
|
-
command
|
|
449
|
-
];
|
|
450
|
-
}
|
|
451
|
-
function commandLineForPolicy(input) {
|
|
452
|
-
if (!input.args || input.args.length === 0) return input.command.trim();
|
|
453
|
-
return [input.command, ...input.args].join(" ").trim();
|
|
357
|
+
//#region src/permissions/sudo-pane.ts
|
|
358
|
+
let sudoPaneAllowed = true;
|
|
359
|
+
function configureSudoPane(allowed) {
|
|
360
|
+
sudoPaneAllowed = allowed;
|
|
454
361
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
const denyPatterns = [
|
|
458
|
-
/(^|\s)rm\s+-[^\n&;r|]*r[^\n&;|]*f\s+\//,
|
|
459
|
-
/(^|\s)mkfs(?:\s|$)/,
|
|
460
|
-
/(^|\s)dd\s+(?:[^\s&;|][^\n;|&]*)?\bof=\/dev\//,
|
|
461
|
-
/:\(\)\s*\{\s*:\|:\s*&\s*\}\s*;/
|
|
462
|
-
];
|
|
463
|
-
const sudoPattern = /(?:^|[\s;&|])sudo(?:[\s;&|]|$)/;
|
|
464
|
-
let configuredDenyCommands = [];
|
|
465
|
-
let configuredAllowCommands = [];
|
|
466
|
-
let allowSudoPane = true;
|
|
467
|
-
function isStringArray(value) {
|
|
468
|
-
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
469
|
-
}
|
|
470
|
-
function escapeRegex(value) {
|
|
471
|
-
return value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
472
|
-
}
|
|
473
|
-
function wildcardMatches(pattern, commandLine) {
|
|
474
|
-
return new RegExp(`^${pattern.split("*").map(escapeRegex).join(".*")}$`).test(commandLine);
|
|
475
|
-
}
|
|
476
|
-
function configurePolicy(config) {
|
|
477
|
-
configuredDenyCommands = [];
|
|
478
|
-
configuredAllowCommands = [];
|
|
479
|
-
allowSudoPane = true;
|
|
480
|
-
if (!config || typeof config !== "object") return;
|
|
481
|
-
const object = config;
|
|
482
|
-
if (isStringArray(object.denyCommands)) configuredDenyCommands = object.denyCommands;
|
|
483
|
-
if (isStringArray(object.allowCommands)) configuredAllowCommands = object.allowCommands;
|
|
484
|
-
if (typeof object.allowSudoPane === "boolean") allowSudoPane = object.allowSudoPane;
|
|
485
|
-
}
|
|
486
|
-
function assertCommandAllowed(input) {
|
|
487
|
-
const commandLine = commandLineForPolicy(input);
|
|
488
|
-
for (const pattern of denyPatterns) if (pattern.test(commandLine)) throw new Error(`Command denied by zellij-pty policy: ${commandLine}`);
|
|
489
|
-
for (const pattern of configuredDenyCommands) if (wildcardMatches(pattern, commandLine)) throw new Error(`Command denied by zellij-pty configured deny rule: ${commandLine}`);
|
|
490
|
-
if (configuredAllowCommands.length > 0 && !configuredAllowCommands.some((pattern) => wildcardMatches(pattern, commandLine))) throw new Error(`Command denied by zellij-pty allow list: ${commandLine}`);
|
|
491
|
-
if (!input.humanInputOnly && sudoPattern.test(commandLine)) throw new Error("sudo commands must use zellij_pty_request_sudo so credentials stay human-input-only and never pass through agent tool input.");
|
|
492
|
-
if (input.humanInputOnly && !allowSudoPane) throw new Error("sudo pane is disabled by zellij-pty policy.");
|
|
362
|
+
function assertSudoPaneAllowed() {
|
|
363
|
+
if (!sudoPaneAllowed) throw new Error("sudo pane is disabled by zellij-pty config.");
|
|
493
364
|
}
|
|
494
365
|
//#endregion
|
|
495
366
|
//#region src/utils/ids.ts
|
|
@@ -540,6 +411,9 @@ var SessionManager = class {
|
|
|
540
411
|
if (!session) throw new Error(`Unknown zellij PTY session: ${id}`);
|
|
541
412
|
return session;
|
|
542
413
|
}
|
|
414
|
+
find(id) {
|
|
415
|
+
return this.sessions.get(id);
|
|
416
|
+
}
|
|
543
417
|
list() {
|
|
544
418
|
return Array.from(this.sessions.values()).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
545
419
|
}
|
|
@@ -572,6 +446,39 @@ var SessionManager = class {
|
|
|
572
446
|
};
|
|
573
447
|
const sessionManager = new SessionManager();
|
|
574
448
|
//#endregion
|
|
449
|
+
//#region src/utils/shell-args.ts
|
|
450
|
+
const directCommandExitWrapper = "token=\"$1\"; shift; set +e; \"$@\"; code=$?; printf \"\\n[zellij-pty:%s] exit-code=%s\\n\" \"$token\" \"$code\"; exit \"$code\"";
|
|
451
|
+
const shellCommandExitWrapper = "token=\"$1\"; command=\"$2\"; set +e; bash -lc \"$command\"; code=$?; printf \"\\n[zellij-pty:%s] exit-code=%s\\n\" \"$token\" \"$code\"; exit \"$code\"";
|
|
452
|
+
function buildCommandArgv(input, options = {}) {
|
|
453
|
+
const command = input.command.trim();
|
|
454
|
+
if (!command) throw new Error("command is required");
|
|
455
|
+
if (options.exitCodeToken) {
|
|
456
|
+
if (input.args && input.args.length > 0) return [
|
|
457
|
+
"bash",
|
|
458
|
+
"-lc",
|
|
459
|
+
directCommandExitWrapper,
|
|
460
|
+
"zellij-pty",
|
|
461
|
+
options.exitCodeToken,
|
|
462
|
+
command,
|
|
463
|
+
...input.args
|
|
464
|
+
];
|
|
465
|
+
return [
|
|
466
|
+
"bash",
|
|
467
|
+
"-lc",
|
|
468
|
+
shellCommandExitWrapper,
|
|
469
|
+
"zellij-pty",
|
|
470
|
+
options.exitCodeToken,
|
|
471
|
+
command
|
|
472
|
+
];
|
|
473
|
+
}
|
|
474
|
+
if (input.args && input.args.length > 0) return [command, ...input.args];
|
|
475
|
+
return [
|
|
476
|
+
"bash",
|
|
477
|
+
"-lc",
|
|
478
|
+
command
|
|
479
|
+
];
|
|
480
|
+
}
|
|
481
|
+
//#endregion
|
|
575
482
|
//#region src/zellij/cli.ts
|
|
576
483
|
const execFileAsync$1 = promisify(execFile);
|
|
577
484
|
function zellijCommandArgs(actionArgs) {
|
|
@@ -735,6 +642,7 @@ const zellijCli = new ZellijCli();
|
|
|
735
642
|
//#region src/zellij/pane-watchdog.ts
|
|
736
643
|
const instanceId = randomUUID();
|
|
737
644
|
let watchdogStarted = false;
|
|
645
|
+
let watchdogChild = null;
|
|
738
646
|
function registryDirectory() {
|
|
739
647
|
const base = process.env.XDG_RUNTIME_DIR || tmpdir();
|
|
740
648
|
return path.join(base, `opencode-zellij-${process.getuid?.() ?? "user"}`);
|
|
@@ -784,13 +692,24 @@ function writeRegistry(registry) {
|
|
|
784
692
|
renameSync(tempFile, file);
|
|
785
693
|
}
|
|
786
694
|
function ensureWatchdog() {
|
|
787
|
-
if (watchdogStarted) return;
|
|
695
|
+
if (watchdogStarted && watchdogChild) return;
|
|
788
696
|
watchdogStarted = true;
|
|
789
|
-
spawn(
|
|
697
|
+
const child = spawn(process.execPath, [watchdogRunnerPath(), watchdogRegistryPath()], {
|
|
790
698
|
detached: true,
|
|
791
699
|
stdio: "ignore",
|
|
792
700
|
env: process.env
|
|
793
|
-
})
|
|
701
|
+
});
|
|
702
|
+
watchdogChild = child;
|
|
703
|
+
child.unref();
|
|
704
|
+
child.on("error", () => {
|
|
705
|
+
watchdogStarted = false;
|
|
706
|
+
if (watchdogChild === child) watchdogChild = null;
|
|
707
|
+
});
|
|
708
|
+
child.on("exit", () => {
|
|
709
|
+
watchdogStarted = false;
|
|
710
|
+
if (watchdogChild === child) watchdogChild = null;
|
|
711
|
+
if (existsSync(watchdogRegistryPath())) ensureWatchdog();
|
|
712
|
+
});
|
|
794
713
|
}
|
|
795
714
|
function watchdogRunnerPath() {
|
|
796
715
|
return fileURLToPath(new URL("./pane-watchdog-runner.mjs", import.meta.url));
|
|
@@ -867,6 +786,7 @@ function removeWatchdogRegistry() {
|
|
|
867
786
|
try {
|
|
868
787
|
rmSync(watchdogRegistryPath(), { force: true });
|
|
869
788
|
} catch {}
|
|
789
|
+
if (!watchdogChild) watchdogStarted = false;
|
|
870
790
|
}
|
|
871
791
|
//#endregion
|
|
872
792
|
//#region src/pty/ring-buffer.ts
|
|
@@ -1011,14 +931,26 @@ function extractRenderedLines(event) {
|
|
|
1011
931
|
}
|
|
1012
932
|
var SubscriberManager = class {
|
|
1013
933
|
subscribers = /* @__PURE__ */ new Map();
|
|
934
|
+
startingSessions = /* @__PURE__ */ new Map();
|
|
1014
935
|
constructor(sessions, maxBufferLines = Number(process.env.PTY_MAX_BUFFER_LINES ?? 5e4)) {
|
|
1015
936
|
this.sessions = sessions;
|
|
1016
937
|
this.maxBufferLines = maxBufferLines;
|
|
1017
938
|
}
|
|
1018
939
|
async start(session) {
|
|
1019
|
-
|
|
1020
|
-
|
|
940
|
+
if (this.subscribers.get(session.id)?.child) return;
|
|
941
|
+
const inProgress = this.startingSessions.get(session.id);
|
|
942
|
+
if (inProgress) return inProgress;
|
|
1021
943
|
ensureZellijTarget();
|
|
944
|
+
const startPromise = this.doStart(session);
|
|
945
|
+
this.startingSessions.set(session.id, startPromise);
|
|
946
|
+
try {
|
|
947
|
+
await startPromise;
|
|
948
|
+
} finally {
|
|
949
|
+
this.startingSessions.delete(session.id);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
async doStart(session) {
|
|
953
|
+
const existing = this.subscribers.get(session.id);
|
|
1022
954
|
const state = existing ?? {
|
|
1023
955
|
child: null,
|
|
1024
956
|
buffer: new RingBuffer(this.maxBufferLines),
|
|
@@ -1028,11 +960,12 @@ var SubscriberManager = class {
|
|
|
1028
960
|
lastExitedAt: null
|
|
1029
961
|
};
|
|
1030
962
|
if (!existing) {
|
|
963
|
+
this.subscribers.set(session.id, state);
|
|
1031
964
|
try {
|
|
1032
965
|
state.buffer.appendSnapshot(await zellijCli.dumpScreen(session.paneId));
|
|
1033
966
|
this.sessions.updateLineCount(session.id, state.buffer.lineCount);
|
|
1034
967
|
} catch {}
|
|
1035
|
-
this.subscribers.
|
|
968
|
+
if (this.subscribers.get(session.id) !== state) return;
|
|
1036
969
|
}
|
|
1037
970
|
const child = spawn("zellij", zellijCommandArgs([
|
|
1038
971
|
"subscribe",
|
|
@@ -1048,14 +981,18 @@ var SubscriberManager = class {
|
|
|
1048
981
|
"pipe"
|
|
1049
982
|
] });
|
|
1050
983
|
child.stdin.end();
|
|
984
|
+
if (this.subscribers.get(session.id) !== state) {
|
|
985
|
+
child.kill("SIGTERM");
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
1051
988
|
state.child = child;
|
|
1052
989
|
state.lastExitedAt = null;
|
|
1053
990
|
child.stdout.setEncoding("utf8");
|
|
1054
|
-
child.stdout.on("data", (chunk) => this.handleStdout(session.id, chunk));
|
|
991
|
+
child.stdout.on("data", (chunk) => this.handleStdout(session.id, child, chunk));
|
|
1055
992
|
child.stderr.setEncoding("utf8");
|
|
1056
|
-
child.stderr.on("data", (chunk) => this.handleStderr(session.id, chunk));
|
|
1057
|
-
child.on("exit", () => this.handleSubscriberExit(session.id));
|
|
1058
|
-
child.on("error", (error) => this.handleSubscriberError(session.id, error));
|
|
993
|
+
child.stderr.on("data", (chunk) => this.handleStderr(session.id, child, chunk));
|
|
994
|
+
child.on("exit", () => this.handleSubscriberExit(session.id, child));
|
|
995
|
+
child.on("error", (error) => this.handleSubscriberError(session.id, child, error));
|
|
1059
996
|
}
|
|
1060
997
|
read(sessionId, input) {
|
|
1061
998
|
const state = this.subscribers.get(sessionId);
|
|
@@ -1097,16 +1034,16 @@ var SubscriberManager = class {
|
|
|
1097
1034
|
await zellijCli.closePane(session.paneId);
|
|
1098
1035
|
} catch {}
|
|
1099
1036
|
}
|
|
1100
|
-
handleStdout(sessionId, chunk) {
|
|
1037
|
+
handleStdout(sessionId, child, chunk) {
|
|
1101
1038
|
const state = this.subscribers.get(sessionId);
|
|
1102
|
-
if (!state) return;
|
|
1039
|
+
if (!state || state.child !== child) return;
|
|
1103
1040
|
const parts = `${state.stdoutRemainder}${chunk}`.split("\n");
|
|
1104
1041
|
state.stdoutRemainder = parts.pop() ?? "";
|
|
1105
|
-
for (const part of parts) this.handleJsonLine(sessionId, part);
|
|
1042
|
+
for (const part of parts) this.handleJsonLine(sessionId, child, part);
|
|
1106
1043
|
}
|
|
1107
|
-
handleJsonLine(sessionId, line) {
|
|
1044
|
+
handleJsonLine(sessionId, child, line) {
|
|
1108
1045
|
const state = this.subscribers.get(sessionId);
|
|
1109
|
-
if (!state) return;
|
|
1046
|
+
if (!state || state.child !== child) return;
|
|
1110
1047
|
const trimmed = line.trim();
|
|
1111
1048
|
if (!trimmed) return;
|
|
1112
1049
|
let event;
|
|
@@ -1119,7 +1056,13 @@ var SubscriberManager = class {
|
|
|
1119
1056
|
this.sessions.updateLineCount(sessionId, state.buffer.lineCount);
|
|
1120
1057
|
return;
|
|
1121
1058
|
}
|
|
1122
|
-
|
|
1059
|
+
let session;
|
|
1060
|
+
try {
|
|
1061
|
+
session = this.sessions.get(sessionId);
|
|
1062
|
+
} catch {
|
|
1063
|
+
this.forget(sessionId);
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1123
1066
|
const paneId = eventPaneId(event);
|
|
1124
1067
|
if (paneId && paneId !== session.paneId) return;
|
|
1125
1068
|
const type = eventType(event);
|
|
@@ -1147,24 +1090,29 @@ var SubscriberManager = class {
|
|
|
1147
1090
|
return;
|
|
1148
1091
|
}
|
|
1149
1092
|
}
|
|
1150
|
-
handleStderr(sessionId, chunk) {
|
|
1093
|
+
handleStderr(sessionId, child, chunk) {
|
|
1151
1094
|
const state = this.subscribers.get(sessionId);
|
|
1152
|
-
if (!state) return;
|
|
1095
|
+
if (!state || state.child !== child) return;
|
|
1153
1096
|
state.stderr.push(...splitLines(chunk));
|
|
1154
1097
|
if (state.stderr.length > maxStderrLines) state.stderr = state.stderr.slice(state.stderr.length - maxStderrLines);
|
|
1155
1098
|
}
|
|
1156
|
-
handleSubscriberExit(sessionId) {
|
|
1099
|
+
handleSubscriberExit(sessionId, child) {
|
|
1157
1100
|
const state = this.subscribers.get(sessionId);
|
|
1158
1101
|
if (!state) return;
|
|
1102
|
+
if (state.child !== child) return;
|
|
1159
1103
|
state.child = null;
|
|
1160
1104
|
state.lastExitedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1161
1105
|
state.stderr.push(`[zellij-pty] subscriber exited at ${state.lastExitedAt}; last buffered output is retained.`);
|
|
1162
1106
|
if (state.stderr.length > maxStderrLines) state.stderr = state.stderr.slice(state.stderr.length - maxStderrLines);
|
|
1163
1107
|
}
|
|
1164
|
-
handleSubscriberError(sessionId, error) {
|
|
1108
|
+
handleSubscriberError(sessionId, child, error) {
|
|
1165
1109
|
const state = this.subscribers.get(sessionId);
|
|
1166
|
-
if (state)
|
|
1167
|
-
|
|
1110
|
+
if (state?.child === child) {
|
|
1111
|
+
state.stderr.push(error.message);
|
|
1112
|
+
state.child = null;
|
|
1113
|
+
state.lastExitedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1114
|
+
this.sessions.updateStatus(sessionId, "unknown");
|
|
1115
|
+
}
|
|
1168
1116
|
}
|
|
1169
1117
|
};
|
|
1170
1118
|
const subscriberManager = new SubscriberManager(sessionManager);
|
|
@@ -1198,6 +1146,11 @@ function jsonResponse(value) {
|
|
|
1198
1146
|
return JSON.stringify(value, null, 2);
|
|
1199
1147
|
}
|
|
1200
1148
|
//#endregion
|
|
1149
|
+
//#region src/utils/errors.ts
|
|
1150
|
+
function errorMessage(error) {
|
|
1151
|
+
return error instanceof Error ? error.message : String(error);
|
|
1152
|
+
}
|
|
1153
|
+
//#endregion
|
|
1201
1154
|
//#region src/tools/output.ts
|
|
1202
1155
|
function emptyOutputSnapshot(lineCount = 0) {
|
|
1203
1156
|
return {
|
|
@@ -1214,7 +1167,7 @@ function validateGrep(grep) {
|
|
|
1214
1167
|
new RegExp(grep).test("");
|
|
1215
1168
|
return null;
|
|
1216
1169
|
} catch (error) {
|
|
1217
|
-
return
|
|
1170
|
+
return errorMessage(error);
|
|
1218
1171
|
}
|
|
1219
1172
|
}
|
|
1220
1173
|
function readOutputSnapshot(sessionId, options = {}) {
|
|
@@ -1327,11 +1280,12 @@ const zellijPtyReadTool = tool({
|
|
|
1327
1280
|
next: nextAdvice(false, `Invalid grep regex: ${grepError}`),
|
|
1328
1281
|
warnings: []
|
|
1329
1282
|
});
|
|
1330
|
-
if (!subscriberManager.has(session.id)) await subscriberManager.start(session);
|
|
1331
1283
|
const subscriberStatus = subscriberManager.status(session.id);
|
|
1284
|
+
if (!subscriberStatus.hasBuffer || !subscriberStatus.active && (session.status === "running" || session.status === "unknown")) await subscriberManager.start(session);
|
|
1285
|
+
const statusAfterStart = subscriberManager.status(session.id);
|
|
1332
1286
|
const warnings = [];
|
|
1333
1287
|
if (session.humanInputOnly) warnings.push("This pane is human-input-only: agent writes are forbidden, but rendered output is visible to the agent.");
|
|
1334
|
-
if (!
|
|
1288
|
+
if (!statusAfterStart.active) {
|
|
1335
1289
|
warnings.push("Subscriber is inactive; returned output may be stale.");
|
|
1336
1290
|
if (session.status === "running") sessionManager.updateStatus(session.id, "unknown");
|
|
1337
1291
|
}
|
|
@@ -1344,8 +1298,8 @@ const zellijPtyReadTool = tool({
|
|
|
1344
1298
|
session: publicSession(session),
|
|
1345
1299
|
output,
|
|
1346
1300
|
next: nextAdvice(session.status !== "exited" && session.status !== "killed", nextReadReason(session.status)),
|
|
1347
|
-
subscriberActive:
|
|
1348
|
-
subscriberLastExitedAt:
|
|
1301
|
+
subscriberActive: statusAfterStart.active,
|
|
1302
|
+
subscriberLastExitedAt: statusAfterStart.lastExitedAt,
|
|
1349
1303
|
subscriberErrors: subscriberManager.stderr(session.id),
|
|
1350
1304
|
warnings
|
|
1351
1305
|
});
|
|
@@ -1407,10 +1361,7 @@ const requestSudoTool = tool({
|
|
|
1407
1361
|
async execute(args, context) {
|
|
1408
1362
|
const cwd = context.directory;
|
|
1409
1363
|
const exitCodeToken = createExitCodeToken();
|
|
1410
|
-
|
|
1411
|
-
command: script.command,
|
|
1412
|
-
humanInputOnly: true
|
|
1413
|
-
});
|
|
1364
|
+
assertSudoPaneAllowed();
|
|
1414
1365
|
const command = buildReviewScript(args.summary, args.scripts);
|
|
1415
1366
|
const title = createOpenCodePaneTitle("zellij_pty_request_sudo");
|
|
1416
1367
|
const paneId = await zellijCli.newPane({
|
|
@@ -1536,11 +1487,6 @@ const zellijPtySpawnTool = tool({
|
|
|
1536
1487
|
async execute(args, context) {
|
|
1537
1488
|
const cwd = args.cwd ?? context.directory;
|
|
1538
1489
|
const exitCodeToken = createExitCodeToken();
|
|
1539
|
-
assertCommandAllowed({
|
|
1540
|
-
command: args.command,
|
|
1541
|
-
args: args.args,
|
|
1542
|
-
humanInputOnly: false
|
|
1543
|
-
});
|
|
1544
1490
|
const grepError = args.probe?.type === "output" ? validateGrep(args.probe.grep) : null;
|
|
1545
1491
|
if (grepError) throw new Error(`Invalid probe.grep regex: ${grepError}`);
|
|
1546
1492
|
const title = createOpenCodePaneTitle(args.title ?? args.command);
|
|
@@ -1633,16 +1579,23 @@ const zellijPtyWriteTool = tool({
|
|
|
1633
1579
|
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1634
1580
|
if (args.interruptAfterSeconds) {
|
|
1635
1581
|
await setTimeout$1(args.interruptAfterSeconds * 1e3);
|
|
1636
|
-
if (sessionManager.
|
|
1582
|
+
if (sessionManager.find(session.id)?.status === "running") {
|
|
1637
1583
|
await zellijCli.sendCtrlC(session.paneId);
|
|
1638
1584
|
await setTimeout$1(500);
|
|
1639
1585
|
}
|
|
1640
1586
|
} else await setTimeout$1(1e3);
|
|
1587
|
+
const warnings = [];
|
|
1588
|
+
let output = emptyOutputSnapshot(session.lineCount);
|
|
1589
|
+
try {
|
|
1590
|
+
output = readOutputSnapshot(session.id, { maxLines: args.maxLines });
|
|
1591
|
+
} catch (error) {
|
|
1592
|
+
warnings.push(`Session output was unavailable before the write response completed: ${errorMessage(error)}`);
|
|
1593
|
+
}
|
|
1641
1594
|
return jsonResponse({
|
|
1642
1595
|
session: publicSession(session),
|
|
1643
|
-
output
|
|
1596
|
+
output,
|
|
1644
1597
|
next: nextAdvice(true, args.interruptAfterSeconds ? "Input was sent; Ctrl-C was sent after the requested interrupt timeout if the session was still running." : "Input was sent and recent output was observed."),
|
|
1645
|
-
warnings
|
|
1598
|
+
warnings
|
|
1646
1599
|
});
|
|
1647
1600
|
}
|
|
1648
1601
|
});
|
|
@@ -2002,15 +1955,28 @@ function startAutoUpdateCheck(client, importMetaUrl, check = checkAndUpdate) {
|
|
|
2002
1955
|
try {
|
|
2003
1956
|
showUpdateToast(client, await check({ importMetaUrl }));
|
|
2004
1957
|
} catch (cause) {
|
|
2005
|
-
debug("auto-update check failed",
|
|
1958
|
+
debug("auto-update check failed", errorMessage(cause));
|
|
2006
1959
|
}
|
|
2007
1960
|
})();
|
|
2008
1961
|
}
|
|
1962
|
+
async function cleanupStep(stepName, sessionId, step) {
|
|
1963
|
+
try {
|
|
1964
|
+
await step();
|
|
1965
|
+
} catch (error) {
|
|
1966
|
+
debug(`session.deleted cleanup failed: ${stepName} for ${sessionId}`, errorMessage(error));
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
async function cleanupDeletedSession(sessionId) {
|
|
1970
|
+
await cleanupStep("close pane", sessionId, () => subscriberManager.closeSessionPane(sessionId));
|
|
1971
|
+
await cleanupStep("forget subscriber", sessionId, () => subscriberManager.forget(sessionId));
|
|
1972
|
+
await cleanupStep("unregister watchdog", sessionId, () => unregisterPaneFromWatchdog(sessionId));
|
|
1973
|
+
await cleanupStep("remove session", sessionId, () => sessionManager.remove(sessionId));
|
|
1974
|
+
}
|
|
2009
1975
|
function createZellijPtyPlugin(dependencies = {}) {
|
|
2010
1976
|
return async (input) => {
|
|
2011
1977
|
const { config, warnings } = await loadConfig(input);
|
|
2012
1978
|
for (const warning of warnings) debug(warning);
|
|
2013
|
-
|
|
1979
|
+
configureSudoPane(config.pty.sudoPane === "allow");
|
|
2014
1980
|
cleanupStaleWatchdogRegistries();
|
|
2015
1981
|
registerShutdownCleanup();
|
|
2016
1982
|
const workspaceRoot = getWorkspaceRoot(input);
|
|
@@ -2038,12 +2004,7 @@ function createZellijPtyPlugin(dependencies = {}) {
|
|
|
2038
2004
|
const sessionID = deletedSessionID(event);
|
|
2039
2005
|
if (!sessionID) return;
|
|
2040
2006
|
const sessions = sessionManager.listByOpenCodeSession(sessionID);
|
|
2041
|
-
await Promise.all(sessions.map(
|
|
2042
|
-
await subscriberManager.closeSessionPane(session.id);
|
|
2043
|
-
subscriberManager.forget(session.id);
|
|
2044
|
-
unregisterPaneFromWatchdog(session.id);
|
|
2045
|
-
sessionManager.remove(session.id);
|
|
2046
|
-
}));
|
|
2007
|
+
await Promise.all(sessions.map((session) => cleanupDeletedSession(session.id)));
|
|
2047
2008
|
}
|
|
2048
2009
|
},
|
|
2049
2010
|
tool: config.pty.enabled ? {
|