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.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$2(pkg)) return {
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$2(pkg) && pkg.name === "opencode-zellij" && typeof pkg.version === "string" && pkg.version.length > 0) {
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$2(value) {
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$2(data) && typeof data.latest === "string") return data.latest;
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 (!shouldVerifyInstalledPackage) {
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 removeInstalledPackage(installRoot);
132
- await install();
133
- const reinstalledPackage = await installedPackageMetadata(installRoot);
134
- if (isExpectedPackage(reinstalledPackage, version)) {
135
- debug(`updated ${PACKAGE_NAME} to ${version}`);
136
- return true;
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
- if (latest === context.currentVersion) {
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: context.currentVersion
219
+ currentVersion: installedVersion
174
220
  };
175
221
  }
176
222
  if (await runNpmInstall(context.installRoot, latest, options.execImpl)) {
177
- debug(`updated ${PACKAGE_NAME} from ${context.currentVersion} to ${latest}`);
223
+ debug(`updated ${PACKAGE_NAME} from ${installedVersion} to ${latest}`);
178
224
  return {
179
225
  type: "updated",
180
- fromVersion: context.currentVersion,
226
+ fromVersion: installedVersion,
181
227
  toVersion: latest
182
228
  };
183
229
  }
184
230
  return {
185
231
  type: "failed",
186
- currentVersion: context.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/utils/shell-args.ts
312
- const directCommandExitWrapper = "token=\"$1\"; shift; set +e; \"$@\"; code=$?; printf \"\\n[zellij-pty:%s] exit-code=%s\\n\" \"$token\" \"$code\"; exit \"$code\"";
313
- 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\"";
314
- function buildCommandArgv(input, options = {}) {
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 commandLineForPolicy(input) {
344
- if (!input.args || input.args.length === 0) return input.command.trim();
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("node", [watchdogRunnerPath(), watchdogRegistryPath()], {
697
+ const child = spawn(process.execPath, [watchdogRunnerPath(), watchdogRegistryPath()], {
682
698
  detached: true,
683
699
  stdio: "ignore",
684
700
  env: process.env
685
- }).unref();
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
- const existing = this.subscribers.get(session.id);
912
- if (existing?.child) return;
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.set(session.id, state);
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
- const session = this.sessions.get(sessionId);
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) state.stderr.push(error.message);
1059
- this.sessions.updateStatus(sessionId, "unknown");
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 error instanceof Error ? error.message : String(error);
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 (!subscriberStatus.active) {
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: subscriberStatus.active,
1240
- subscriberLastExitedAt: subscriberStatus.lastExitedAt,
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
- for (const script of args.scripts) assertCommandAllowed({
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.get(session.id).status === "running") {
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: readOutputSnapshot(session.id, { maxLines: args.maxLines }),
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$1(value) {
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$1(nested)) return void 0;
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$1(status)) return void 0;
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$1(event.properties)) return void 0;
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$1(event.properties)) return;
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 isRootSessionCreated(event) {
1896
- if (event.type !== "session.created") return false;
1897
- if (!isRecord(event.properties)) return true;
1898
- const info = event.properties.info;
1899
- if (!isRecord(info)) return true;
1900
- return !info.parentID;
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
- }) : void 0;
1922
- tabTitleManager?.renderImmediate().catch(() => {});
1923
- let hasCheckedUpdate = false;
1924
- const client = input.client;
1925
- return {
1926
- async event(input) {
1927
- const event = input.event;
1928
- if (!hasCheckedUpdate && config.autoUpdate.enabled && isRootSessionCreated(event)) {
1929
- hasCheckedUpdate = true;
1930
- checkAndUpdate({ importMetaUrl: import.meta.url }).then((result) => showUpdateToast(client, result), (cause) => debug("auto-update check failed", cause instanceof Error ? cause.message : String(cause)));
1931
- }
1932
- if (tabTitleManager) handleTabTitleEvent(tabTitleManager, event);
1933
- if (event.type === "session.deleted") {
1934
- const sessionID = deletedSessionID(event);
1935
- if (!sessionID) return;
1936
- const sessions = sessionManager.listByOpenCodeSession(sessionID);
1937
- await Promise.all(sessions.map(async (session) => {
1938
- await subscriberManager.closeSessionPane(session.id);
1939
- subscriberManager.forget(session.id);
1940
- unregisterPaneFromWatchdog(session.id);
1941
- sessionManager.remove(session.id);
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
- tool: config.pty.enabled ? {
1946
- ...ptyTools,
1947
- ...config.pty.sudoPane === "hide" ? {} : { zellij_pty_request_sudo: requestSudoTool }
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