@volter/supercode-terminal 0.2.38 → 0.2.40

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.
Files changed (2) hide show
  1. package/index.mjs +53 -3
  2. package/package.json +1 -1
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 { existsSync } from 'node:fs';
3
+ import { cpSync, existsSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { dirname, join } from 'node:path';
4
5
  import { connect as netConnect } from 'node:net';
5
6
  import { fileURLToPath } from 'node:url';
6
7
  import { createServer as defaultCreateServer } from 'node:http';
@@ -810,6 +811,8 @@ export class PtyTerminalHost {
810
811
  // The launch directory: another process's current directory is not readable here as tmux reads it.
811
812
  cwd: entry.cwd,
812
813
  dead: entry.exit !== null,
814
+ // How a dead pane's program ended (a failed one is kept so its last screen can be read).
815
+ exitCode: entry.exit ? entry.exit.exitCode ?? null : null,
813
816
  id: entry.id,
814
817
  label: entry.name,
815
818
  owned: true,
@@ -902,6 +905,20 @@ export class PtyTerminalHost {
902
905
  * connection; the panes and their programs go on, and the next daemon finds them there. The server starts on first
903
906
  * use, detached, and leaves a minute after it holds no pane and no client.
904
907
  */
908
+ /** What the pane server loads beyond this package (PtyTerminalHost's dynamic imports). */
909
+ const PANE_RUNTIME_PACKAGES = ['@homebridge/node-pty-prebuilt-multiarch', '@xterm/headless', '@xterm/addon-serialize'];
910
+
911
+ /** A dependency's package directory: the nearest one above its entry named for it (ESM resolution, as it is loaded). */
912
+ function packageDir(name) {
913
+ let dir = dirname(fileURLToPath(import.meta.resolve(name)));
914
+ for (;;) {
915
+ try { if (JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')).name === name) return dir; } catch { /* not a package root */ }
916
+ const up = dirname(dir);
917
+ if (up === dir) throw new Error(`no package directory for ${name}`);
918
+ dir = up;
919
+ }
920
+ }
921
+
905
922
  export class PaneServerHost {
906
923
  #connection = null;
907
924
  #connecting = null;
@@ -910,11 +927,44 @@ export class PaneServerHost {
910
927
  #streams = new Map();
911
928
  #pipe;
912
929
  #serverScript;
930
+ #runtimeDir;
913
931
 
932
+ /**
933
+ * `runtimeDir`: where the pane server runs from, a copy of this package and its runtime dependencies per version
934
+ * (`<runtimeDir>/<version>`). A running pane server holds node-pty's native module open, and Windows cannot replace
935
+ * a file in use, so a server started from the installed package would stop the next install of it; started from its
936
+ * own copy, it leaves the installed package free to change under it. Without one, it runs from the package.
937
+ */
914
938
  constructor(options = {}) {
915
939
  if (!options.pipe) throw new TypeError('PaneServerHost requires a pipe name');
916
940
  this.#pipe = options.pipe;
917
- this.#serverScript = options.serverScript ?? fileURLToPath(new URL('./substrate/pane-server.mjs', import.meta.url));
941
+ this.#serverScript = options.serverScript ?? null;
942
+ this.#runtimeDir = options.runtimeDir ?? null;
943
+ }
944
+
945
+ /** The pane server's script: its own versioned copy under runtimeDir, made once, else the package's. */
946
+ #script() {
947
+ if (this.#serverScript) return this.#serverScript;
948
+ const installed = fileURLToPath(new URL('./substrate/pane-server.mjs', import.meta.url));
949
+ if (!this.#runtimeDir) return installed;
950
+ const root = fileURLToPath(new URL('.', import.meta.url));
951
+ const version = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).version;
952
+ const target = join(this.#runtimeDir, version);
953
+ const script = join(target, 'node_modules', '@volter', 'supercode-terminal', 'substrate', 'pane-server.mjs');
954
+ if (existsSync(join(target, '.complete'))) return script;
955
+ try {
956
+ const staging = `${target}.${process.pid}.staging`;
957
+ rmSync(staging, { recursive: true, force: true });
958
+ cpSync(root, join(staging, 'node_modules', '@volter', 'supercode-terminal'), { recursive: true, filter: (path) => !path.slice(root.length).split(/[\\/]/).includes('node_modules') });
959
+ for (const name of PANE_RUNTIME_PACKAGES) cpSync(packageDir(name), join(staging, 'node_modules', ...name.split('/')), { recursive: true });
960
+ writeFileSync(join(staging, '.complete'), `${version}\n`);
961
+ rmSync(target, { recursive: true, force: true });
962
+ renameSync(staging, target);
963
+ return script;
964
+ } catch {
965
+ // A copy that cannot be made (a full disk, a racing daemon) leaves the package's own script, as before.
966
+ return existsSync(join(target, '.complete')) ? script : installed;
967
+ }
918
968
  }
919
969
 
920
970
  listSessions() { return this.#request('listSessions', []); }
@@ -996,7 +1046,7 @@ export class PaneServerHost {
996
1046
  } catch (error) {
997
1047
  // No server on the pipe yet: start one, detached from this daemon, and try again as it comes up.
998
1048
  if (attempt === 0 && (error.code === 'ENOENT' || error.code === 'ECONNREFUSED')) {
999
- const child = spawn(process.execPath, [this.#serverScript, this.#pipe], { detached: true, stdio: 'ignore', windowsHide: true, env: process.env });
1049
+ const child = spawn(process.execPath, [this.#script(), this.#pipe], { detached: true, stdio: 'ignore', windowsHide: true, env: process.env });
1000
1050
  child.on('error', () => {});
1001
1051
  child.unref();
1002
1052
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-terminal",
3
- "version": "0.2.38",
3
+ "version": "0.2.40",
4
4
  "type": "module",
5
5
  "description": "Optional leak-safe terminal and tmux host adapter for Volter Harness frontends",
6
6
  "exports": {