opencode-zellij 0.0.6 → 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.d.mts +20 -1
- package/dist/index.mjs +267 -200
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
2
|
import { execFile, spawn, spawnSync } from "node:child_process";
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { readFile, rm } from "node:fs/promises";
|
|
4
|
+
import { readFile, rename, rm } from "node:fs/promises";
|
|
5
5
|
import path, { basename, dirname, join } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { promisify } from "node:util";
|
|
@@ -27,25 +27,64 @@ const defaultExecFile = promisify(execFile);
|
|
|
27
27
|
function packageDir(installRoot) {
|
|
28
28
|
return join(installRoot, "node_modules", PACKAGE_NAME);
|
|
29
29
|
}
|
|
30
|
+
function backupDir(installRoot) {
|
|
31
|
+
return join(installRoot, "node_modules", `${PACKAGE_NAME}.update-backup`);
|
|
32
|
+
}
|
|
30
33
|
async function installedPackageMetadata(installRoot) {
|
|
31
34
|
try {
|
|
32
35
|
const content = await readFile(join(packageDir(installRoot), "package.json"), "utf8");
|
|
33
36
|
const pkg = JSON.parse(content);
|
|
34
|
-
if (isRecord$
|
|
37
|
+
if (isRecord$1(pkg)) return {
|
|
35
38
|
name: typeof pkg.name === "string" ? pkg.name : void 0,
|
|
36
|
-
version: typeof pkg.version === "string" ? pkg.version : void 0
|
|
39
|
+
version: typeof pkg.version === "string" ? pkg.version : void 0,
|
|
40
|
+
main: typeof pkg.main === "string" ? pkg.main : void 0
|
|
37
41
|
};
|
|
38
42
|
} catch {}
|
|
39
43
|
}
|
|
40
44
|
function isExpectedPackage(metadata, version) {
|
|
41
45
|
return metadata?.name === "opencode-zellij" && metadata.version === version;
|
|
42
46
|
}
|
|
47
|
+
function hasRunnableEntry(installRoot, metadata) {
|
|
48
|
+
if (!metadata) return false;
|
|
49
|
+
const dir = packageDir(installRoot);
|
|
50
|
+
if (metadata.main && existsSync(join(dir, metadata.main))) return true;
|
|
51
|
+
return existsSync(join(dir, "dist", "index.mjs"));
|
|
52
|
+
}
|
|
53
|
+
async function isVerifiedInstall(installRoot, version) {
|
|
54
|
+
const metadata = await installedPackageMetadata(installRoot);
|
|
55
|
+
return isExpectedPackage(metadata, version) && hasRunnableEntry(installRoot, metadata);
|
|
56
|
+
}
|
|
43
57
|
async function removeInstalledPackage(installRoot) {
|
|
44
58
|
await rm(packageDir(installRoot), {
|
|
45
59
|
force: true,
|
|
46
60
|
recursive: true
|
|
47
61
|
});
|
|
48
62
|
}
|
|
63
|
+
async function backupInstalledPackage(installRoot) {
|
|
64
|
+
const source = packageDir(installRoot);
|
|
65
|
+
if (!existsSync(source)) return void 0;
|
|
66
|
+
const backup = backupDir(installRoot);
|
|
67
|
+
await rm(backup, {
|
|
68
|
+
force: true,
|
|
69
|
+
recursive: true
|
|
70
|
+
});
|
|
71
|
+
await rename(source, backup);
|
|
72
|
+
return backup;
|
|
73
|
+
}
|
|
74
|
+
async function restoreInstalledPackage(installRoot, backup) {
|
|
75
|
+
if (!backup || !existsSync(backup)) return;
|
|
76
|
+
await rm(packageDir(installRoot), {
|
|
77
|
+
force: true,
|
|
78
|
+
recursive: true
|
|
79
|
+
});
|
|
80
|
+
await rename(backup, packageDir(installRoot));
|
|
81
|
+
}
|
|
82
|
+
async function discardBackup(backup) {
|
|
83
|
+
if (backup) await rm(backup, {
|
|
84
|
+
force: true,
|
|
85
|
+
recursive: true
|
|
86
|
+
});
|
|
87
|
+
}
|
|
49
88
|
async function findInstallContext(importMetaUrl) {
|
|
50
89
|
let startPath;
|
|
51
90
|
try {
|
|
@@ -61,7 +100,7 @@ async function findInstallContext(importMetaUrl) {
|
|
|
61
100
|
try {
|
|
62
101
|
const content = await readFile(packageJsonPath, "utf8");
|
|
63
102
|
const pkg = JSON.parse(content);
|
|
64
|
-
if (isRecord$
|
|
103
|
+
if (isRecord$1(pkg) && pkg.name === "opencode-zellij" && typeof pkg.version === "string" && pkg.version.length > 0) {
|
|
65
104
|
const installRoot = dirname(dirname(dir));
|
|
66
105
|
if (existsSync(join(installRoot, "package.json"))) return {
|
|
67
106
|
installRoot,
|
|
@@ -76,7 +115,7 @@ async function findInstallContext(importMetaUrl) {
|
|
|
76
115
|
dir = parent;
|
|
77
116
|
}
|
|
78
117
|
}
|
|
79
|
-
function isRecord$
|
|
118
|
+
function isRecord$1(value) {
|
|
80
119
|
return typeof value === "object" && value !== null;
|
|
81
120
|
}
|
|
82
121
|
function isAutoUpdatableSpec(spec) {
|
|
@@ -95,7 +134,7 @@ async function fetchLatestVersion(fetchImpl = globalThis.fetch) {
|
|
|
95
134
|
return;
|
|
96
135
|
}
|
|
97
136
|
const data = await response.json();
|
|
98
|
-
if (isRecord$
|
|
137
|
+
if (isRecord$1(data) && typeof data.latest === "string") return data.latest;
|
|
99
138
|
debug("npm registry response missing latest tag");
|
|
100
139
|
return;
|
|
101
140
|
} catch (cause) {
|
|
@@ -107,36 +146,42 @@ async function fetchLatestVersion(fetchImpl = globalThis.fetch) {
|
|
|
107
146
|
async function runNpmInstall(installRoot, version, execImpl = defaultExecFile) {
|
|
108
147
|
debug(`updating ${PACKAGE_NAME} to ${version} in ${installRoot}`);
|
|
109
148
|
try {
|
|
110
|
-
const shouldVerifyInstalledPackage = await installedPackageMetadata(installRoot) !== void 0;
|
|
111
149
|
const install = () => execImpl("npm", [
|
|
112
150
|
"install",
|
|
113
151
|
`${PACKAGE_NAME}@${version}`,
|
|
114
152
|
"--save-exact",
|
|
115
|
-
"--ignore-scripts"
|
|
153
|
+
"--ignore-scripts",
|
|
154
|
+
"--no-audit",
|
|
155
|
+
"--no-fund",
|
|
156
|
+
"--prefer-online"
|
|
116
157
|
], {
|
|
117
158
|
cwd: installRoot,
|
|
118
159
|
timeout: INSTALL_TIMEOUT_MS
|
|
119
160
|
});
|
|
120
161
|
await install();
|
|
121
|
-
if (
|
|
162
|
+
if (await isVerifiedInstall(installRoot, version)) {
|
|
122
163
|
debug(`updated ${PACKAGE_NAME} to ${version}`);
|
|
123
164
|
return true;
|
|
124
165
|
}
|
|
125
166
|
const installedPackage = await installedPackageMetadata(installRoot);
|
|
126
|
-
if (isExpectedPackage(installedPackage, version)) {
|
|
127
|
-
debug(`updated ${PACKAGE_NAME} to ${version}`);
|
|
128
|
-
return true;
|
|
129
|
-
}
|
|
130
167
|
debug(`npm install left stale or invalid ${PACKAGE_NAME} (${installedPackage?.name ?? "<missing>"}@${installedPackage?.version ?? "<missing>"}); reinstalling ${version}`);
|
|
131
|
-
await
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
168
|
+
const backup = await backupInstalledPackage(installRoot);
|
|
169
|
+
try {
|
|
170
|
+
await removeInstalledPackage(installRoot);
|
|
171
|
+
await install();
|
|
172
|
+
if (await isVerifiedInstall(installRoot, version)) {
|
|
173
|
+
await discardBackup(backup);
|
|
174
|
+
debug(`updated ${PACKAGE_NAME} to ${version}`);
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
const reinstalledPackage = await installedPackageMetadata(installRoot);
|
|
178
|
+
debug(`npm install verification failed: expected ${PACKAGE_NAME}@${version}, found ${reinstalledPackage?.name ?? "<missing>"}@${reinstalledPackage?.version ?? "<missing>"}`);
|
|
179
|
+
await restoreInstalledPackage(installRoot, backup);
|
|
180
|
+
return false;
|
|
181
|
+
} catch (cause) {
|
|
182
|
+
await restoreInstalledPackage(installRoot, backup);
|
|
183
|
+
throw cause;
|
|
137
184
|
}
|
|
138
|
-
debug(`npm install verification failed: expected ${PACKAGE_NAME}@${version}, found ${reinstalledPackage?.name ?? "<missing>"}@${reinstalledPackage?.version ?? "<missing>"}`);
|
|
139
|
-
return false;
|
|
140
185
|
} catch (cause) {
|
|
141
186
|
debug("npm install failed", cause instanceof Error ? cause.message : String(cause));
|
|
142
187
|
return false;
|
|
@@ -166,24 +211,25 @@ async function checkAndUpdate(options) {
|
|
|
166
211
|
reason: "could not determine latest version"
|
|
167
212
|
};
|
|
168
213
|
}
|
|
169
|
-
|
|
214
|
+
const installedVersion = (await installedPackageMetadata(context.installRoot))?.version ?? context.currentVersion;
|
|
215
|
+
if (latest === installedVersion) {
|
|
170
216
|
debug(`auto-update: already on latest ${latest}`);
|
|
171
217
|
return {
|
|
172
218
|
type: "up-to-date",
|
|
173
|
-
currentVersion:
|
|
219
|
+
currentVersion: installedVersion
|
|
174
220
|
};
|
|
175
221
|
}
|
|
176
222
|
if (await runNpmInstall(context.installRoot, latest, options.execImpl)) {
|
|
177
|
-
debug(`updated ${PACKAGE_NAME} from ${
|
|
223
|
+
debug(`updated ${PACKAGE_NAME} from ${installedVersion} to ${latest}`);
|
|
178
224
|
return {
|
|
179
225
|
type: "updated",
|
|
180
|
-
fromVersion:
|
|
226
|
+
fromVersion: installedVersion,
|
|
181
227
|
toVersion: latest
|
|
182
228
|
};
|
|
183
229
|
}
|
|
184
230
|
return {
|
|
185
231
|
type: "failed",
|
|
186
|
-
currentVersion:
|
|
232
|
+
currentVersion: installedVersion,
|
|
187
233
|
latestVersion: latest,
|
|
188
234
|
reason: "npm install failed"
|
|
189
235
|
};
|
|
@@ -308,80 +354,13 @@ async function loadConfig(input) {
|
|
|
308
354
|
};
|
|
309
355
|
}
|
|
310
356
|
//#endregion
|
|
311
|
-
//#region src/
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
const command = input.command.trim();
|
|
316
|
-
if (!command) throw new Error("command is required");
|
|
317
|
-
if (options.exitCodeToken) {
|
|
318
|
-
if (input.args && input.args.length > 0) return [
|
|
319
|
-
"bash",
|
|
320
|
-
"-lc",
|
|
321
|
-
directCommandExitWrapper,
|
|
322
|
-
"zellij-pty",
|
|
323
|
-
options.exitCodeToken,
|
|
324
|
-
command,
|
|
325
|
-
...input.args
|
|
326
|
-
];
|
|
327
|
-
return [
|
|
328
|
-
"bash",
|
|
329
|
-
"-lc",
|
|
330
|
-
shellCommandExitWrapper,
|
|
331
|
-
"zellij-pty",
|
|
332
|
-
options.exitCodeToken,
|
|
333
|
-
command
|
|
334
|
-
];
|
|
335
|
-
}
|
|
336
|
-
if (input.args && input.args.length > 0) return [command, ...input.args];
|
|
337
|
-
return [
|
|
338
|
-
"bash",
|
|
339
|
-
"-lc",
|
|
340
|
-
command
|
|
341
|
-
];
|
|
357
|
+
//#region src/permissions/sudo-pane.ts
|
|
358
|
+
let sudoPaneAllowed = true;
|
|
359
|
+
function configureSudoPane(allowed) {
|
|
360
|
+
sudoPaneAllowed = allowed;
|
|
342
361
|
}
|
|
343
|
-
function
|
|
344
|
-
if (!
|
|
345
|
-
return [input.command, ...input.args].join(" ").trim();
|
|
346
|
-
}
|
|
347
|
-
//#endregion
|
|
348
|
-
//#region src/permissions/policy.ts
|
|
349
|
-
const denyPatterns = [
|
|
350
|
-
/(^|\s)rm\s+-[^\n&;r|]*r[^\n&;|]*f\s+\//,
|
|
351
|
-
/(^|\s)mkfs(?:\s|$)/,
|
|
352
|
-
/(^|\s)dd\s+(?:[^\s&;|][^\n;|&]*)?\bof=\/dev\//,
|
|
353
|
-
/:\(\)\s*\{\s*:\|:\s*&\s*\}\s*;/
|
|
354
|
-
];
|
|
355
|
-
const sudoPattern = /(?:^|[\s;&|])sudo(?:[\s;&|]|$)/;
|
|
356
|
-
let configuredDenyCommands = [];
|
|
357
|
-
let configuredAllowCommands = [];
|
|
358
|
-
let allowSudoPane = true;
|
|
359
|
-
function isStringArray(value) {
|
|
360
|
-
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
361
|
-
}
|
|
362
|
-
function escapeRegex(value) {
|
|
363
|
-
return value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
364
|
-
}
|
|
365
|
-
function wildcardMatches(pattern, commandLine) {
|
|
366
|
-
return new RegExp(`^${pattern.split("*").map(escapeRegex).join(".*")}$`).test(commandLine);
|
|
367
|
-
}
|
|
368
|
-
function configurePolicy(config) {
|
|
369
|
-
configuredDenyCommands = [];
|
|
370
|
-
configuredAllowCommands = [];
|
|
371
|
-
allowSudoPane = true;
|
|
372
|
-
if (!config || typeof config !== "object") return;
|
|
373
|
-
const object = config;
|
|
374
|
-
if (isStringArray(object.denyCommands)) configuredDenyCommands = object.denyCommands;
|
|
375
|
-
if (isStringArray(object.allowCommands)) configuredAllowCommands = object.allowCommands;
|
|
376
|
-
if (typeof object.allowSudoPane === "boolean") allowSudoPane = object.allowSudoPane;
|
|
377
|
-
}
|
|
378
|
-
function assertCommandAllowed(input) {
|
|
379
|
-
const commandLine = commandLineForPolicy(input);
|
|
380
|
-
for (const pattern of denyPatterns) if (pattern.test(commandLine)) throw new Error(`Command denied by zellij-pty policy: ${commandLine}`);
|
|
381
|
-
for (const pattern of configuredDenyCommands) if (wildcardMatches(pattern, commandLine)) throw new Error(`Command denied by zellij-pty configured deny rule: ${commandLine}`);
|
|
382
|
-
if (configuredAllowCommands.length > 0 && !configuredAllowCommands.some((pattern) => wildcardMatches(pattern, commandLine))) throw new Error(`Command denied by zellij-pty allow list: ${commandLine}`);
|
|
383
|
-
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.");
|
|
384
|
-
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.");
|
|
385
364
|
}
|
|
386
365
|
//#endregion
|
|
387
366
|
//#region src/utils/ids.ts
|
|
@@ -432,6 +411,9 @@ var SessionManager = class {
|
|
|
432
411
|
if (!session) throw new Error(`Unknown zellij PTY session: ${id}`);
|
|
433
412
|
return session;
|
|
434
413
|
}
|
|
414
|
+
find(id) {
|
|
415
|
+
return this.sessions.get(id);
|
|
416
|
+
}
|
|
435
417
|
list() {
|
|
436
418
|
return Array.from(this.sessions.values()).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
437
419
|
}
|
|
@@ -464,6 +446,39 @@ var SessionManager = class {
|
|
|
464
446
|
};
|
|
465
447
|
const sessionManager = new SessionManager();
|
|
466
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
|
|
467
482
|
//#region src/zellij/cli.ts
|
|
468
483
|
const execFileAsync$1 = promisify(execFile);
|
|
469
484
|
function zellijCommandArgs(actionArgs) {
|
|
@@ -627,6 +642,7 @@ const zellijCli = new ZellijCli();
|
|
|
627
642
|
//#region src/zellij/pane-watchdog.ts
|
|
628
643
|
const instanceId = randomUUID();
|
|
629
644
|
let watchdogStarted = false;
|
|
645
|
+
let watchdogChild = null;
|
|
630
646
|
function registryDirectory() {
|
|
631
647
|
const base = process.env.XDG_RUNTIME_DIR || tmpdir();
|
|
632
648
|
return path.join(base, `opencode-zellij-${process.getuid?.() ?? "user"}`);
|
|
@@ -676,13 +692,24 @@ function writeRegistry(registry) {
|
|
|
676
692
|
renameSync(tempFile, file);
|
|
677
693
|
}
|
|
678
694
|
function ensureWatchdog() {
|
|
679
|
-
if (watchdogStarted) return;
|
|
695
|
+
if (watchdogStarted && watchdogChild) return;
|
|
680
696
|
watchdogStarted = true;
|
|
681
|
-
spawn(
|
|
697
|
+
const child = spawn(process.execPath, [watchdogRunnerPath(), watchdogRegistryPath()], {
|
|
682
698
|
detached: true,
|
|
683
699
|
stdio: "ignore",
|
|
684
700
|
env: process.env
|
|
685
|
-
})
|
|
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
|
+
});
|
|
686
713
|
}
|
|
687
714
|
function watchdogRunnerPath() {
|
|
688
715
|
return fileURLToPath(new URL("./pane-watchdog-runner.mjs", import.meta.url));
|
|
@@ -759,6 +786,7 @@ function removeWatchdogRegistry() {
|
|
|
759
786
|
try {
|
|
760
787
|
rmSync(watchdogRegistryPath(), { force: true });
|
|
761
788
|
} catch {}
|
|
789
|
+
if (!watchdogChild) watchdogStarted = false;
|
|
762
790
|
}
|
|
763
791
|
//#endregion
|
|
764
792
|
//#region src/pty/ring-buffer.ts
|
|
@@ -903,14 +931,26 @@ function extractRenderedLines(event) {
|
|
|
903
931
|
}
|
|
904
932
|
var SubscriberManager = class {
|
|
905
933
|
subscribers = /* @__PURE__ */ new Map();
|
|
934
|
+
startingSessions = /* @__PURE__ */ new Map();
|
|
906
935
|
constructor(sessions, maxBufferLines = Number(process.env.PTY_MAX_BUFFER_LINES ?? 5e4)) {
|
|
907
936
|
this.sessions = sessions;
|
|
908
937
|
this.maxBufferLines = maxBufferLines;
|
|
909
938
|
}
|
|
910
939
|
async start(session) {
|
|
911
|
-
|
|
912
|
-
|
|
940
|
+
if (this.subscribers.get(session.id)?.child) return;
|
|
941
|
+
const inProgress = this.startingSessions.get(session.id);
|
|
942
|
+
if (inProgress) return inProgress;
|
|
913
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);
|
|
914
954
|
const state = existing ?? {
|
|
915
955
|
child: null,
|
|
916
956
|
buffer: new RingBuffer(this.maxBufferLines),
|
|
@@ -920,11 +960,12 @@ var SubscriberManager = class {
|
|
|
920
960
|
lastExitedAt: null
|
|
921
961
|
};
|
|
922
962
|
if (!existing) {
|
|
963
|
+
this.subscribers.set(session.id, state);
|
|
923
964
|
try {
|
|
924
965
|
state.buffer.appendSnapshot(await zellijCli.dumpScreen(session.paneId));
|
|
925
966
|
this.sessions.updateLineCount(session.id, state.buffer.lineCount);
|
|
926
967
|
} catch {}
|
|
927
|
-
this.subscribers.
|
|
968
|
+
if (this.subscribers.get(session.id) !== state) return;
|
|
928
969
|
}
|
|
929
970
|
const child = spawn("zellij", zellijCommandArgs([
|
|
930
971
|
"subscribe",
|
|
@@ -940,14 +981,18 @@ var SubscriberManager = class {
|
|
|
940
981
|
"pipe"
|
|
941
982
|
] });
|
|
942
983
|
child.stdin.end();
|
|
984
|
+
if (this.subscribers.get(session.id) !== state) {
|
|
985
|
+
child.kill("SIGTERM");
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
943
988
|
state.child = child;
|
|
944
989
|
state.lastExitedAt = null;
|
|
945
990
|
child.stdout.setEncoding("utf8");
|
|
946
|
-
child.stdout.on("data", (chunk) => this.handleStdout(session.id, chunk));
|
|
991
|
+
child.stdout.on("data", (chunk) => this.handleStdout(session.id, child, chunk));
|
|
947
992
|
child.stderr.setEncoding("utf8");
|
|
948
|
-
child.stderr.on("data", (chunk) => this.handleStderr(session.id, chunk));
|
|
949
|
-
child.on("exit", () => this.handleSubscriberExit(session.id));
|
|
950
|
-
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));
|
|
951
996
|
}
|
|
952
997
|
read(sessionId, input) {
|
|
953
998
|
const state = this.subscribers.get(sessionId);
|
|
@@ -989,16 +1034,16 @@ var SubscriberManager = class {
|
|
|
989
1034
|
await zellijCli.closePane(session.paneId);
|
|
990
1035
|
} catch {}
|
|
991
1036
|
}
|
|
992
|
-
handleStdout(sessionId, chunk) {
|
|
1037
|
+
handleStdout(sessionId, child, chunk) {
|
|
993
1038
|
const state = this.subscribers.get(sessionId);
|
|
994
|
-
if (!state) return;
|
|
1039
|
+
if (!state || state.child !== child) return;
|
|
995
1040
|
const parts = `${state.stdoutRemainder}${chunk}`.split("\n");
|
|
996
1041
|
state.stdoutRemainder = parts.pop() ?? "";
|
|
997
|
-
for (const part of parts) this.handleJsonLine(sessionId, part);
|
|
1042
|
+
for (const part of parts) this.handleJsonLine(sessionId, child, part);
|
|
998
1043
|
}
|
|
999
|
-
handleJsonLine(sessionId, line) {
|
|
1044
|
+
handleJsonLine(sessionId, child, line) {
|
|
1000
1045
|
const state = this.subscribers.get(sessionId);
|
|
1001
|
-
if (!state) return;
|
|
1046
|
+
if (!state || state.child !== child) return;
|
|
1002
1047
|
const trimmed = line.trim();
|
|
1003
1048
|
if (!trimmed) return;
|
|
1004
1049
|
let event;
|
|
@@ -1011,7 +1056,13 @@ var SubscriberManager = class {
|
|
|
1011
1056
|
this.sessions.updateLineCount(sessionId, state.buffer.lineCount);
|
|
1012
1057
|
return;
|
|
1013
1058
|
}
|
|
1014
|
-
|
|
1059
|
+
let session;
|
|
1060
|
+
try {
|
|
1061
|
+
session = this.sessions.get(sessionId);
|
|
1062
|
+
} catch {
|
|
1063
|
+
this.forget(sessionId);
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1015
1066
|
const paneId = eventPaneId(event);
|
|
1016
1067
|
if (paneId && paneId !== session.paneId) return;
|
|
1017
1068
|
const type = eventType(event);
|
|
@@ -1039,24 +1090,29 @@ var SubscriberManager = class {
|
|
|
1039
1090
|
return;
|
|
1040
1091
|
}
|
|
1041
1092
|
}
|
|
1042
|
-
handleStderr(sessionId, chunk) {
|
|
1093
|
+
handleStderr(sessionId, child, chunk) {
|
|
1043
1094
|
const state = this.subscribers.get(sessionId);
|
|
1044
|
-
if (!state) return;
|
|
1095
|
+
if (!state || state.child !== child) return;
|
|
1045
1096
|
state.stderr.push(...splitLines(chunk));
|
|
1046
1097
|
if (state.stderr.length > maxStderrLines) state.stderr = state.stderr.slice(state.stderr.length - maxStderrLines);
|
|
1047
1098
|
}
|
|
1048
|
-
handleSubscriberExit(sessionId) {
|
|
1099
|
+
handleSubscriberExit(sessionId, child) {
|
|
1049
1100
|
const state = this.subscribers.get(sessionId);
|
|
1050
1101
|
if (!state) return;
|
|
1102
|
+
if (state.child !== child) return;
|
|
1051
1103
|
state.child = null;
|
|
1052
1104
|
state.lastExitedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1053
1105
|
state.stderr.push(`[zellij-pty] subscriber exited at ${state.lastExitedAt}; last buffered output is retained.`);
|
|
1054
1106
|
if (state.stderr.length > maxStderrLines) state.stderr = state.stderr.slice(state.stderr.length - maxStderrLines);
|
|
1055
1107
|
}
|
|
1056
|
-
handleSubscriberError(sessionId, error) {
|
|
1108
|
+
handleSubscriberError(sessionId, child, error) {
|
|
1057
1109
|
const state = this.subscribers.get(sessionId);
|
|
1058
|
-
if (state)
|
|
1059
|
-
|
|
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
|
+
}
|
|
1060
1116
|
}
|
|
1061
1117
|
};
|
|
1062
1118
|
const subscriberManager = new SubscriberManager(sessionManager);
|
|
@@ -1090,6 +1146,11 @@ function jsonResponse(value) {
|
|
|
1090
1146
|
return JSON.stringify(value, null, 2);
|
|
1091
1147
|
}
|
|
1092
1148
|
//#endregion
|
|
1149
|
+
//#region src/utils/errors.ts
|
|
1150
|
+
function errorMessage(error) {
|
|
1151
|
+
return error instanceof Error ? error.message : String(error);
|
|
1152
|
+
}
|
|
1153
|
+
//#endregion
|
|
1093
1154
|
//#region src/tools/output.ts
|
|
1094
1155
|
function emptyOutputSnapshot(lineCount = 0) {
|
|
1095
1156
|
return {
|
|
@@ -1106,7 +1167,7 @@ function validateGrep(grep) {
|
|
|
1106
1167
|
new RegExp(grep).test("");
|
|
1107
1168
|
return null;
|
|
1108
1169
|
} catch (error) {
|
|
1109
|
-
return
|
|
1170
|
+
return errorMessage(error);
|
|
1110
1171
|
}
|
|
1111
1172
|
}
|
|
1112
1173
|
function readOutputSnapshot(sessionId, options = {}) {
|
|
@@ -1219,11 +1280,12 @@ const zellijPtyReadTool = tool({
|
|
|
1219
1280
|
next: nextAdvice(false, `Invalid grep regex: ${grepError}`),
|
|
1220
1281
|
warnings: []
|
|
1221
1282
|
});
|
|
1222
|
-
if (!subscriberManager.has(session.id)) await subscriberManager.start(session);
|
|
1223
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);
|
|
1224
1286
|
const warnings = [];
|
|
1225
1287
|
if (session.humanInputOnly) warnings.push("This pane is human-input-only: agent writes are forbidden, but rendered output is visible to the agent.");
|
|
1226
|
-
if (!
|
|
1288
|
+
if (!statusAfterStart.active) {
|
|
1227
1289
|
warnings.push("Subscriber is inactive; returned output may be stale.");
|
|
1228
1290
|
if (session.status === "running") sessionManager.updateStatus(session.id, "unknown");
|
|
1229
1291
|
}
|
|
@@ -1236,8 +1298,8 @@ const zellijPtyReadTool = tool({
|
|
|
1236
1298
|
session: publicSession(session),
|
|
1237
1299
|
output,
|
|
1238
1300
|
next: nextAdvice(session.status !== "exited" && session.status !== "killed", nextReadReason(session.status)),
|
|
1239
|
-
subscriberActive:
|
|
1240
|
-
subscriberLastExitedAt:
|
|
1301
|
+
subscriberActive: statusAfterStart.active,
|
|
1302
|
+
subscriberLastExitedAt: statusAfterStart.lastExitedAt,
|
|
1241
1303
|
subscriberErrors: subscriberManager.stderr(session.id),
|
|
1242
1304
|
warnings
|
|
1243
1305
|
});
|
|
@@ -1299,10 +1361,7 @@ const requestSudoTool = tool({
|
|
|
1299
1361
|
async execute(args, context) {
|
|
1300
1362
|
const cwd = context.directory;
|
|
1301
1363
|
const exitCodeToken = createExitCodeToken();
|
|
1302
|
-
|
|
1303
|
-
command: script.command,
|
|
1304
|
-
humanInputOnly: true
|
|
1305
|
-
});
|
|
1364
|
+
assertSudoPaneAllowed();
|
|
1306
1365
|
const command = buildReviewScript(args.summary, args.scripts);
|
|
1307
1366
|
const title = createOpenCodePaneTitle("zellij_pty_request_sudo");
|
|
1308
1367
|
const paneId = await zellijCli.newPane({
|
|
@@ -1428,11 +1487,6 @@ const zellijPtySpawnTool = tool({
|
|
|
1428
1487
|
async execute(args, context) {
|
|
1429
1488
|
const cwd = args.cwd ?? context.directory;
|
|
1430
1489
|
const exitCodeToken = createExitCodeToken();
|
|
1431
|
-
assertCommandAllowed({
|
|
1432
|
-
command: args.command,
|
|
1433
|
-
args: args.args,
|
|
1434
|
-
humanInputOnly: false
|
|
1435
|
-
});
|
|
1436
1490
|
const grepError = args.probe?.type === "output" ? validateGrep(args.probe.grep) : null;
|
|
1437
1491
|
if (grepError) throw new Error(`Invalid probe.grep regex: ${grepError}`);
|
|
1438
1492
|
const title = createOpenCodePaneTitle(args.title ?? args.command);
|
|
@@ -1525,16 +1579,23 @@ const zellijPtyWriteTool = tool({
|
|
|
1525
1579
|
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1526
1580
|
if (args.interruptAfterSeconds) {
|
|
1527
1581
|
await setTimeout$1(args.interruptAfterSeconds * 1e3);
|
|
1528
|
-
if (sessionManager.
|
|
1582
|
+
if (sessionManager.find(session.id)?.status === "running") {
|
|
1529
1583
|
await zellijCli.sendCtrlC(session.paneId);
|
|
1530
1584
|
await setTimeout$1(500);
|
|
1531
1585
|
}
|
|
1532
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
|
+
}
|
|
1533
1594
|
return jsonResponse({
|
|
1534
1595
|
session: publicSession(session),
|
|
1535
|
-
output
|
|
1596
|
+
output,
|
|
1536
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."),
|
|
1537
|
-
warnings
|
|
1598
|
+
warnings
|
|
1538
1599
|
});
|
|
1539
1600
|
}
|
|
1540
1601
|
});
|
|
@@ -1571,7 +1632,7 @@ function exitAfterCleanup(signal, code) {
|
|
|
1571
1632
|
//#endregion
|
|
1572
1633
|
//#region src/zellij/tab-title-events.ts
|
|
1573
1634
|
const execFileAsync = promisify(execFile);
|
|
1574
|
-
function isRecord
|
|
1635
|
+
function isRecord(value) {
|
|
1575
1636
|
return typeof value === "object" && value !== null;
|
|
1576
1637
|
}
|
|
1577
1638
|
function stringProperty(object, key) {
|
|
@@ -1580,12 +1641,12 @@ function stringProperty(object, key) {
|
|
|
1580
1641
|
}
|
|
1581
1642
|
function nestedStringProperty(object, key, nestedKey) {
|
|
1582
1643
|
const nested = object[key];
|
|
1583
|
-
if (!isRecord
|
|
1644
|
+
if (!isRecord(nested)) return void 0;
|
|
1584
1645
|
return stringProperty(nested, nestedKey);
|
|
1585
1646
|
}
|
|
1586
1647
|
function sessionStatusProperty(object) {
|
|
1587
1648
|
const status = object.status;
|
|
1588
|
-
if (!isRecord
|
|
1649
|
+
if (!isRecord(status)) return void 0;
|
|
1589
1650
|
if (status.type === "idle" || status.type === "busy") return { type: status.type };
|
|
1590
1651
|
if (status.type === "retry") return {
|
|
1591
1652
|
type: "retry",
|
|
@@ -1604,7 +1665,7 @@ function isResolvedInputState(state) {
|
|
|
1604
1665
|
return state === "approved" || state === "denied" || state === "rejected" || state === "resolved" || state === "replied";
|
|
1605
1666
|
}
|
|
1606
1667
|
function deletedSessionID(event) {
|
|
1607
|
-
if (!isRecord
|
|
1668
|
+
if (!isRecord(event.properties)) return void 0;
|
|
1608
1669
|
return nestedStringProperty(event.properties, "info", "id") ?? stringProperty(event.properties, "sessionID");
|
|
1609
1670
|
}
|
|
1610
1671
|
async function readGitBranch(worktree) {
|
|
@@ -1630,7 +1691,7 @@ function shouldReadInitialBranch(zellij) {
|
|
|
1630
1691
|
return Boolean(zellij);
|
|
1631
1692
|
}
|
|
1632
1693
|
function handleTabTitleEvent(tabTitleManager, event) {
|
|
1633
|
-
if (!isRecord
|
|
1694
|
+
if (!isRecord(event.properties)) return;
|
|
1634
1695
|
const properties = event.properties;
|
|
1635
1696
|
switch (event.type) {
|
|
1636
1697
|
case "session.status": {
|
|
@@ -1875,9 +1936,6 @@ function getProjectName(path) {
|
|
|
1875
1936
|
function getWorkspaceRoot(input) {
|
|
1876
1937
|
return input.worktree || input.directory || process.cwd();
|
|
1877
1938
|
}
|
|
1878
|
-
function isRecord(value) {
|
|
1879
|
-
return typeof value === "object" && value !== null;
|
|
1880
|
-
}
|
|
1881
1939
|
function showUpdateToast(client, result) {
|
|
1882
1940
|
if (result.type === "updated") client.tui.showToast({ body: {
|
|
1883
1941
|
title: "opencode-zellij updated",
|
|
@@ -1892,63 +1950,72 @@ function showUpdateToast(client, result) {
|
|
|
1892
1950
|
duration: 8e3
|
|
1893
1951
|
} }).catch(() => {});
|
|
1894
1952
|
}
|
|
1895
|
-
function
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
}
|
|
1902
|
-
const ZellijPtyPlugin = async (input) => {
|
|
1903
|
-
const { config, warnings } = await loadConfig(input);
|
|
1904
|
-
for (const warning of warnings) debug(warning);
|
|
1905
|
-
configurePolicy({ allowSudoPane: config.pty.sudoPane === "allow" });
|
|
1906
|
-
cleanupStaleWatchdogRegistries();
|
|
1907
|
-
registerShutdownCleanup();
|
|
1908
|
-
const workspaceRoot = getWorkspaceRoot(input);
|
|
1909
|
-
const projectName = getProjectName(workspaceRoot);
|
|
1910
|
-
const branchName = config.tabTitle.enabled && shouldReadInitialBranch(process.env.ZELLIJ) ? await getInitialBranch(workspaceRoot) : void 0;
|
|
1911
|
-
const tabTitleManager = config.tabTitle.enabled ? new TabTitleManager({
|
|
1912
|
-
projectName,
|
|
1913
|
-
branchName,
|
|
1914
|
-
debounceMs: config.tabTitle.debounceMs,
|
|
1915
|
-
emojis: {
|
|
1916
|
-
idle: config.tabTitle.emojiIdle,
|
|
1917
|
-
running: config.tabTitle.emojiRunning,
|
|
1918
|
-
needsInput: config.tabTitle.emojiNeedsInput,
|
|
1919
|
-
branch: config.tabTitle.emojiBranch
|
|
1953
|
+
function startAutoUpdateCheck(client, importMetaUrl, check = checkAndUpdate) {
|
|
1954
|
+
(async () => {
|
|
1955
|
+
try {
|
|
1956
|
+
showUpdateToast(client, await check({ importMetaUrl }));
|
|
1957
|
+
} catch (cause) {
|
|
1958
|
+
debug("auto-update check failed", errorMessage(cause));
|
|
1920
1959
|
}
|
|
1921
|
-
})
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1960
|
+
})();
|
|
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
|
+
}
|
|
1975
|
+
function createZellijPtyPlugin(dependencies = {}) {
|
|
1976
|
+
return async (input) => {
|
|
1977
|
+
const { config, warnings } = await loadConfig(input);
|
|
1978
|
+
for (const warning of warnings) debug(warning);
|
|
1979
|
+
configureSudoPane(config.pty.sudoPane === "allow");
|
|
1980
|
+
cleanupStaleWatchdogRegistries();
|
|
1981
|
+
registerShutdownCleanup();
|
|
1982
|
+
const workspaceRoot = getWorkspaceRoot(input);
|
|
1983
|
+
const projectName = getProjectName(workspaceRoot);
|
|
1984
|
+
const branchName = config.tabTitle.enabled && shouldReadInitialBranch(process.env.ZELLIJ) ? await getInitialBranch(workspaceRoot) : void 0;
|
|
1985
|
+
const tabTitleManager = config.tabTitle.enabled ? new TabTitleManager({
|
|
1986
|
+
projectName,
|
|
1987
|
+
branchName,
|
|
1988
|
+
debounceMs: config.tabTitle.debounceMs,
|
|
1989
|
+
emojis: {
|
|
1990
|
+
idle: config.tabTitle.emojiIdle,
|
|
1991
|
+
running: config.tabTitle.emojiRunning,
|
|
1992
|
+
needsInput: config.tabTitle.emojiNeedsInput,
|
|
1993
|
+
branch: config.tabTitle.emojiBranch
|
|
1943
1994
|
}
|
|
1944
|
-
}
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1995
|
+
}) : void 0;
|
|
1996
|
+
tabTitleManager?.renderImmediate().catch(() => {});
|
|
1997
|
+
const client = input.client;
|
|
1998
|
+
if (config.autoUpdate.enabled) (dependencies.startAutoUpdateCheck ?? startAutoUpdateCheck)(client, dependencies.importMetaUrl ?? import.meta.url);
|
|
1999
|
+
return {
|
|
2000
|
+
async event(input) {
|
|
2001
|
+
const event = input.event;
|
|
2002
|
+
if (tabTitleManager) handleTabTitleEvent(tabTitleManager, event);
|
|
2003
|
+
if (event.type === "session.deleted") {
|
|
2004
|
+
const sessionID = deletedSessionID(event);
|
|
2005
|
+
if (!sessionID) return;
|
|
2006
|
+
const sessions = sessionManager.listByOpenCodeSession(sessionID);
|
|
2007
|
+
await Promise.all(sessions.map((session) => cleanupDeletedSession(session.id)));
|
|
2008
|
+
}
|
|
2009
|
+
},
|
|
2010
|
+
tool: config.pty.enabled ? {
|
|
2011
|
+
...ptyTools,
|
|
2012
|
+
...config.pty.sudoPane === "hide" ? {} : { zellij_pty_request_sudo: requestSudoTool }
|
|
2013
|
+
} : {}
|
|
2014
|
+
};
|
|
1949
2015
|
};
|
|
1950
|
-
}
|
|
2016
|
+
}
|
|
2017
|
+
const ZellijPtyPlugin = createZellijPtyPlugin();
|
|
1951
2018
|
//#endregion
|
|
1952
|
-
export { ZellijPtyPlugin, ZellijPtyPlugin as default, showUpdateToast };
|
|
2019
|
+
export { ZellijPtyPlugin, ZellijPtyPlugin as default, createZellijPtyPlugin, showUpdateToast, startAutoUpdateCheck };
|
|
1953
2020
|
|
|
1954
2021
|
//# sourceMappingURL=index.mjs.map
|