@volter/supercode-terminal 0.2.40 → 0.2.42

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/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { execFile, spawn, spawnSync } from 'node:child_process';
2
2
  import { randomBytes, randomUUID } from 'node:crypto';
3
- import { cpSync, existsSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { appendFileSync, cpSync, existsSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { homedir } from 'node:os';
4
5
  import { dirname, join } from 'node:path';
5
6
  import { connect as netConnect } from 'node:net';
6
7
  import { fileURLToPath } from 'node:url';
@@ -509,6 +510,18 @@ for (const event of ['end', 'close', 'error']) process.stdin.on(event, sweep);
509
510
  * disposing), so every pane here is this host's own. As with tmux's `remain-on-exit failed`, a pane whose program
510
511
  * fails stays listed and capturable, marked dead, until it is closed; one that exits cleanly leaves at once.
511
512
  */
513
+ /**
514
+ * A pane's end in the pane-exit log, as the tmux lane's hooks write it (`<time> <pane> closed`, or `died status=…
515
+ * signal=…` when it failed), under SUPERCODE_HOME or ~/.config/supercode: the machine daemon tells a clean finish from
516
+ * a failure by it, and a ConPTY pane that ended cleanly is otherwise gone without a trace.
517
+ */
518
+ function recordPaneExit(contextKey, exitCode, signal) {
519
+ if (!contextKey) return;
520
+ const at = new Date().toISOString().slice(0, 19);
521
+ const line = exitCode === 0 && !signal ? `${at} ${contextKey} closed\n` : `${at} ${contextKey} died status=${exitCode ?? ''} signal=${signal || ''}\n`;
522
+ try { appendFileSync(join(process.env.SUPERCODE_HOME || join(homedir(), '.config', 'supercode'), 'pane-exits.log'), line); } catch { /* no log is no worse than before */ }
523
+ }
524
+
512
525
  export class PtyTerminalHost {
513
526
  #attachments = new Map();
514
527
  #liveAttachments = new Set();
@@ -585,6 +598,7 @@ export class PtyTerminalHost {
585
598
  // server) outlives it, as it outlives a tmux pane. What stayed in the pane's console ends with the console.
586
599
  // Only closing a pane, or its daemon dying, ends a whole tree.
587
600
  releasePane(child);
601
+ recordPaneExit(entry.contextKey, exitCode, signal);
588
602
  this.#tellExit(entry);
589
603
  this.#writeWatchList();
590
604
  if (exitCode === 0) this.#forget(entry);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-terminal",
3
- "version": "0.2.40",
3
+ "version": "0.2.42",
4
4
  "type": "module",
5
5
  "description": "Optional leak-safe terminal and tmux host adapter for Volter Harness frontends",
6
6
  "exports": {
package/substrate/tmux.js CHANGED
@@ -75,16 +75,29 @@ export async function createSession({ command, cwd, env, name, owner, ownerOptio
75
75
  startArgs.push("-c", cwd);
76
76
  }
77
77
  startArgs.push(...paneBootstrapArgs(0));
78
- await runTmuxSessionCreateAsync(startArgs, { cwd, operation: "create tmux session", session: name, socket });
79
- if (owner) {
80
- await runAsync("tmux", tmuxArgs(socket, ["set-option", "-t", name, ownershipOption, owner]));
81
- }
78
+ // The session's own options ride the SAME invocation as its creation. A
79
+ // program that exits at once (a name not on PATH) otherwise takes the
80
+ // session with it before `remain-on-exit` or the owner stamp is set, and
81
+ // the next set-option's "no such session" replaces the program's own
82
+ // failure in what the launcher reports.
83
+ const setOnCreate = (option, value) => {
84
+ assertOptionValue(value);
85
+ // tmux splits commands at an argument that ends in `;`.
86
+ if (value.endsWith(";"))
87
+ throw new Error(`A tmux option value set at creation cannot end in ";": ${option}`);
88
+ startArgs.push(";", "set-option", "-t", name, option, value);
89
+ };
90
+ if (owner)
91
+ setOnCreate(ownershipOption, owner);
82
92
  for (const [option, value] of Object.entries(sessionOptions).sort(([left], [right]) => left.localeCompare(right))) {
83
- await setSessionOptionAsync(name, option, value, socket);
93
+ assertUserOption(option);
94
+ setOnCreate(option, value);
84
95
  }
85
96
  for (const [option, value] of Object.entries(settings).sort(([left], [right]) => left.localeCompare(right))) {
86
- await setSessionSettingAsync(name, option, value, socket);
97
+ assertSettingName(option);
98
+ setOnCreate(option, value);
87
99
  }
100
+ await runTmuxSessionCreateAsync(startArgs, { cwd, operation: "create tmux session", session: name, socket });
88
101
  await declareSessionColorEnvironmentAsync(socket);
89
102
  const target = targetForSession(name);
90
103
  for (let i = 1; i < panes; i += 1) {
@@ -150,11 +163,17 @@ export async function sessionOptionValuesAsync(option, socket) {
150
163
  export async function setSessionSettingAsync(session, option, value, socket) {
151
164
  if (!session)
152
165
  throw new Error("A session name is required.");
166
+ assertSettingName(option);
167
+ assertOptionValue(value);
168
+ await runAsync("tmux", tmuxArgs(socket, ["set-option", "-t", session, option, value]));
169
+ }
170
+ function assertSettingName(option) {
153
171
  if (!/^[a-z][a-z0-9-]*$/.test(option))
154
172
  throw new Error(`Not a tmux session option name: ${option}`);
173
+ }
174
+ function assertOptionValue(value) {
155
175
  if (/\0|\t|\r|\n/.test(value))
156
176
  throw new Error("A tmux option value cannot contain control separators.");
157
- await runAsync("tmux", tmuxArgs(socket, ["set-option", "-t", session, option, value]));
158
177
  }
159
178
  /** Set one session-scoped user option without exposing it in the session name. */
160
179
  export async function setSessionOptionAsync(session, option, value, socket) {