@particle-academy/fancy-term-host 0.1.0 → 0.1.2

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.cts CHANGED
@@ -366,6 +366,23 @@ declare class HostClient extends EventEmitter implements PtyBackend {
366
366
  * the reattach (renderer remounts these specs, replaying host scrollback). */
367
367
  liveIds(): string[];
368
368
  isConnected(): boolean;
369
+ /**
370
+ * Gracefully shut the host DOWN — the deliberate counterpart to
371
+ * `disconnect()` (which leaves the host running). Asks the host to kill its
372
+ * ptys, remove its pidfile/socket, and exit cleanly, then drops the local
373
+ * socket. Use this when a consumer needs the host genuinely gone — e.g.
374
+ * before an Electron auto-update whose installer must overwrite the binary
375
+ * the host runs on — INSTEAD of SIGKILLing by pidfile pid, which skips the
376
+ * host's own cleanup.
377
+ *
378
+ * The client mirror already holds the live scrollback, so snapshot via the
379
+ * SnapshotStore BEFORE calling this if you want T1 history to survive.
380
+ *
381
+ * Resolves once the host acknowledges (`shutdown-ok`) or the connection
382
+ * closes (the host exited), whichever comes first; never rejects — a host
383
+ * that's already gone is a successful shutdown.
384
+ */
385
+ shutdownHost(timeoutMs?: number): Promise<void>;
369
386
  /** Disconnect WITHOUT killing host ptys (before-quit leave-running). */
370
387
  disconnect(): void;
371
388
  create(opts: CreateTerminalOpts): AttachResult;
@@ -450,6 +467,19 @@ declare function initTerminalBackend(): Promise<{
450
467
  * the host running so the next launch reattaches.
451
468
  */
452
469
  declare function disconnectHostLeaveRunning(): void;
470
+ /**
471
+ * Gracefully STOP the detached host (the opposite of leave-running): ask it to
472
+ * kill its ptys, clean up its pidfile/socket, and exit, then drop our client and
473
+ * revert to the in-process backend so any later create() still works.
474
+ *
475
+ * Intended for the case where a consumer needs the host genuinely gone — most
476
+ * notably before an Electron auto-update whose installer must overwrite the
477
+ * binary the detached host is running on. Snapshot first (the normal before-quit
478
+ * T1 path) if you want history to survive; this is a clean shutdown, NOT a
479
+ * SIGKILL-by-pidfile, so the host runs its own cleanup. No-op (resolves) when no
480
+ * host is active. NEVER throws.
481
+ */
482
+ declare function shutdownHost(timeoutMs?: number): Promise<void>;
453
483
 
454
484
  /**
455
485
  * Absolute path to the bundled detached pty-host script (Tier 3), so a
@@ -544,7 +574,7 @@ declare function resolveHostScript(dirname: string): string | null;
544
574
  * refuses to attach to a host whose pidfile reports a different version and
545
575
  * spawns a fresh host instead — see host-client.ts connect-or-spawn.
546
576
  */
547
- declare const PROTOCOL_VERSION = 1;
577
+ declare const PROTOCOL_VERSION = 2;
548
578
  /** Requests the client sends to the host. `seq` correlates a reply. */
549
579
  type ClientMessage = {
550
580
  kind: 'hello';
@@ -588,6 +618,9 @@ type ClientMessage = {
588
618
  } | {
589
619
  kind: 'ping';
590
620
  seq: number;
621
+ } | {
622
+ kind: 'shutdown';
623
+ seq: number;
591
624
  };
592
625
  /** Pushes + replies the host sends to the client. */
593
626
  type HostMessage = {
@@ -620,6 +653,9 @@ type HostMessage = {
620
653
  } | {
621
654
  kind: 'pong';
622
655
  seq: number;
656
+ } | {
657
+ kind: 'shutdown-ok';
658
+ seq: number;
623
659
  } | {
624
660
  kind: 'data';
625
661
  id: string;
@@ -774,4 +810,38 @@ declare function parseFileUrl(payload: string): string | null;
774
810
  */
775
811
  declare function scanOsc7Cwd(chunk: string): string | null;
776
812
 
777
- export { type AttachResult, type BackendDeps, type ClientMessage, type CreateTerminalOpts, type CwdHook, type Encryptor, type Frame, FrameDecoder, HostClient, type HostMessage, type HostSpawner, type HostStatus, PROTOCOL_VERSION, type Pidfile, type PtyBackend, type SettingsProvider, type ShellInfo, type ShellKind, type SnapshotRead, type SnapshotStore, type SnapshotStoreConfig, type TerminalInfo, type TerminalManager, configureHostLifecycle, configureInProcessBackend, createSnapshotStore, cwdHookEnv, cwdHookSpawn, defaultShell, defaultShellId, deletePidfile, detectShells, disconnectHostLeaveRunning, encodeFrame, getHostClient, inProcessBackend, initTerminalBackend, isHostBacked, isPidAlive, parseCommandLine, parseFileUrl, pidfilePath, pidfileUsable, ptyHostScriptPath, readPidfile, resolveDefaultShell, resolveHostScript, scanOsc7Cwd, setActiveBackend, shellKind, socketPathFor, subscribeBackendEvents, terminalManager, userHash, writePidfile };
813
+ /**
814
+ * Spawn-cwd normalization (Tier 1.5 companion to osc7.ts).
815
+ *
816
+ * Git Bash / MSYS reports `$PWD` in MSYS form (`/c/Users/me`), not native
817
+ * Windows (`C:\Users\me`). The OSC-7 hook emits that raw, and `parseFileUrl`
818
+ * only converts the drive-colon form (`/C:/...`), so an MSYS path flows through
819
+ * unchanged. Handing `/c/Users/me` to node-pty as a working dir makes Windows
820
+ * fail with ERROR_DIRECTORY (error code 267) — terminal creation crashes.
821
+ *
822
+ * Two small, OS-agnostic helpers fix this at the source:
823
+ * - `toNativeCwd` converts an MSYS path to native Windows form (no-op on
824
+ * POSIX, or when already native).
825
+ * - `resolveSpawnCwd` native-converts the requested dir AND validates it,
826
+ * falling back to the home directory so a stale/foreign/deleted cwd can
827
+ * never crash spawn.
828
+ *
829
+ * Used at both spawn sites (manager.ts, pty-host.ts) and at the OSC-7 capture
830
+ * so the persisted `live_cwd` is already a valid native path.
831
+ */
832
+ /**
833
+ * Convert an MSYS/Git-Bash cwd to a native Windows path.
834
+ * /c/Users/me -> C:\Users\me
835
+ * /d/work -> D:\work
836
+ * /c -> C:\ (bare drive root)
837
+ * No-op on non-win32, on an empty string, or on an already-native path.
838
+ */
839
+ declare function toNativeCwd(p: string): string;
840
+ /**
841
+ * Resolve the directory a pty should actually spawn in. Prefer the requested
842
+ * dir (native-converted); if it isn't an existing directory, fall back to the
843
+ * home directory — so a stale, foreign, or deleted cwd can't crash spawn.
844
+ */
845
+ declare function resolveSpawnCwd(requested: string | undefined | null): string;
846
+
847
+ export { type AttachResult, type BackendDeps, type ClientMessage, type CreateTerminalOpts, type CwdHook, type Encryptor, type Frame, FrameDecoder, HostClient, type HostMessage, type HostSpawner, type HostStatus, PROTOCOL_VERSION, type Pidfile, type PtyBackend, type SettingsProvider, type ShellInfo, type ShellKind, type SnapshotRead, type SnapshotStore, type SnapshotStoreConfig, type TerminalInfo, type TerminalManager, configureHostLifecycle, configureInProcessBackend, createSnapshotStore, cwdHookEnv, cwdHookSpawn, defaultShell, defaultShellId, deletePidfile, detectShells, disconnectHostLeaveRunning, encodeFrame, getHostClient, inProcessBackend, initTerminalBackend, isHostBacked, isPidAlive, parseCommandLine, parseFileUrl, pidfilePath, pidfileUsable, ptyHostScriptPath, readPidfile, resolveDefaultShell, resolveHostScript, resolveSpawnCwd, scanOsc7Cwd, setActiveBackend, shellKind, shutdownHost, socketPathFor, subscribeBackendEvents, terminalManager, toNativeCwd, userHash, writePidfile };
package/dist/index.d.ts CHANGED
@@ -366,6 +366,23 @@ declare class HostClient extends EventEmitter implements PtyBackend {
366
366
  * the reattach (renderer remounts these specs, replaying host scrollback). */
367
367
  liveIds(): string[];
368
368
  isConnected(): boolean;
369
+ /**
370
+ * Gracefully shut the host DOWN — the deliberate counterpart to
371
+ * `disconnect()` (which leaves the host running). Asks the host to kill its
372
+ * ptys, remove its pidfile/socket, and exit cleanly, then drops the local
373
+ * socket. Use this when a consumer needs the host genuinely gone — e.g.
374
+ * before an Electron auto-update whose installer must overwrite the binary
375
+ * the host runs on — INSTEAD of SIGKILLing by pidfile pid, which skips the
376
+ * host's own cleanup.
377
+ *
378
+ * The client mirror already holds the live scrollback, so snapshot via the
379
+ * SnapshotStore BEFORE calling this if you want T1 history to survive.
380
+ *
381
+ * Resolves once the host acknowledges (`shutdown-ok`) or the connection
382
+ * closes (the host exited), whichever comes first; never rejects — a host
383
+ * that's already gone is a successful shutdown.
384
+ */
385
+ shutdownHost(timeoutMs?: number): Promise<void>;
369
386
  /** Disconnect WITHOUT killing host ptys (before-quit leave-running). */
370
387
  disconnect(): void;
371
388
  create(opts: CreateTerminalOpts): AttachResult;
@@ -450,6 +467,19 @@ declare function initTerminalBackend(): Promise<{
450
467
  * the host running so the next launch reattaches.
451
468
  */
452
469
  declare function disconnectHostLeaveRunning(): void;
470
+ /**
471
+ * Gracefully STOP the detached host (the opposite of leave-running): ask it to
472
+ * kill its ptys, clean up its pidfile/socket, and exit, then drop our client and
473
+ * revert to the in-process backend so any later create() still works.
474
+ *
475
+ * Intended for the case where a consumer needs the host genuinely gone — most
476
+ * notably before an Electron auto-update whose installer must overwrite the
477
+ * binary the detached host is running on. Snapshot first (the normal before-quit
478
+ * T1 path) if you want history to survive; this is a clean shutdown, NOT a
479
+ * SIGKILL-by-pidfile, so the host runs its own cleanup. No-op (resolves) when no
480
+ * host is active. NEVER throws.
481
+ */
482
+ declare function shutdownHost(timeoutMs?: number): Promise<void>;
453
483
 
454
484
  /**
455
485
  * Absolute path to the bundled detached pty-host script (Tier 3), so a
@@ -544,7 +574,7 @@ declare function resolveHostScript(dirname: string): string | null;
544
574
  * refuses to attach to a host whose pidfile reports a different version and
545
575
  * spawns a fresh host instead — see host-client.ts connect-or-spawn.
546
576
  */
547
- declare const PROTOCOL_VERSION = 1;
577
+ declare const PROTOCOL_VERSION = 2;
548
578
  /** Requests the client sends to the host. `seq` correlates a reply. */
549
579
  type ClientMessage = {
550
580
  kind: 'hello';
@@ -588,6 +618,9 @@ type ClientMessage = {
588
618
  } | {
589
619
  kind: 'ping';
590
620
  seq: number;
621
+ } | {
622
+ kind: 'shutdown';
623
+ seq: number;
591
624
  };
592
625
  /** Pushes + replies the host sends to the client. */
593
626
  type HostMessage = {
@@ -620,6 +653,9 @@ type HostMessage = {
620
653
  } | {
621
654
  kind: 'pong';
622
655
  seq: number;
656
+ } | {
657
+ kind: 'shutdown-ok';
658
+ seq: number;
623
659
  } | {
624
660
  kind: 'data';
625
661
  id: string;
@@ -774,4 +810,38 @@ declare function parseFileUrl(payload: string): string | null;
774
810
  */
775
811
  declare function scanOsc7Cwd(chunk: string): string | null;
776
812
 
777
- export { type AttachResult, type BackendDeps, type ClientMessage, type CreateTerminalOpts, type CwdHook, type Encryptor, type Frame, FrameDecoder, HostClient, type HostMessage, type HostSpawner, type HostStatus, PROTOCOL_VERSION, type Pidfile, type PtyBackend, type SettingsProvider, type ShellInfo, type ShellKind, type SnapshotRead, type SnapshotStore, type SnapshotStoreConfig, type TerminalInfo, type TerminalManager, configureHostLifecycle, configureInProcessBackend, createSnapshotStore, cwdHookEnv, cwdHookSpawn, defaultShell, defaultShellId, deletePidfile, detectShells, disconnectHostLeaveRunning, encodeFrame, getHostClient, inProcessBackend, initTerminalBackend, isHostBacked, isPidAlive, parseCommandLine, parseFileUrl, pidfilePath, pidfileUsable, ptyHostScriptPath, readPidfile, resolveDefaultShell, resolveHostScript, scanOsc7Cwd, setActiveBackend, shellKind, socketPathFor, subscribeBackendEvents, terminalManager, userHash, writePidfile };
813
+ /**
814
+ * Spawn-cwd normalization (Tier 1.5 companion to osc7.ts).
815
+ *
816
+ * Git Bash / MSYS reports `$PWD` in MSYS form (`/c/Users/me`), not native
817
+ * Windows (`C:\Users\me`). The OSC-7 hook emits that raw, and `parseFileUrl`
818
+ * only converts the drive-colon form (`/C:/...`), so an MSYS path flows through
819
+ * unchanged. Handing `/c/Users/me` to node-pty as a working dir makes Windows
820
+ * fail with ERROR_DIRECTORY (error code 267) — terminal creation crashes.
821
+ *
822
+ * Two small, OS-agnostic helpers fix this at the source:
823
+ * - `toNativeCwd` converts an MSYS path to native Windows form (no-op on
824
+ * POSIX, or when already native).
825
+ * - `resolveSpawnCwd` native-converts the requested dir AND validates it,
826
+ * falling back to the home directory so a stale/foreign/deleted cwd can
827
+ * never crash spawn.
828
+ *
829
+ * Used at both spawn sites (manager.ts, pty-host.ts) and at the OSC-7 capture
830
+ * so the persisted `live_cwd` is already a valid native path.
831
+ */
832
+ /**
833
+ * Convert an MSYS/Git-Bash cwd to a native Windows path.
834
+ * /c/Users/me -> C:\Users\me
835
+ * /d/work -> D:\work
836
+ * /c -> C:\ (bare drive root)
837
+ * No-op on non-win32, on an empty string, or on an already-native path.
838
+ */
839
+ declare function toNativeCwd(p: string): string;
840
+ /**
841
+ * Resolve the directory a pty should actually spawn in. Prefer the requested
842
+ * dir (native-converted); if it isn't an existing directory, fall back to the
843
+ * home directory — so a stale, foreign, or deleted cwd can't crash spawn.
844
+ */
845
+ declare function resolveSpawnCwd(requested: string | undefined | null): string;
846
+
847
+ export { type AttachResult, type BackendDeps, type ClientMessage, type CreateTerminalOpts, type CwdHook, type Encryptor, type Frame, FrameDecoder, HostClient, type HostMessage, type HostSpawner, type HostStatus, PROTOCOL_VERSION, type Pidfile, type PtyBackend, type SettingsProvider, type ShellInfo, type ShellKind, type SnapshotRead, type SnapshotStore, type SnapshotStoreConfig, type TerminalInfo, type TerminalManager, configureHostLifecycle, configureInProcessBackend, createSnapshotStore, cwdHookEnv, cwdHookSpawn, defaultShell, defaultShellId, deletePidfile, detectShells, disconnectHostLeaveRunning, encodeFrame, getHostClient, inProcessBackend, initTerminalBackend, isHostBacked, isPidAlive, parseCommandLine, parseFileUrl, pidfilePath, pidfileUsable, ptyHostScriptPath, readPidfile, resolveDefaultShell, resolveHostScript, resolveSpawnCwd, scanOsc7Cwd, setActiveBackend, shellKind, shutdownHost, socketPathFor, subscribeBackendEvents, terminalManager, toNativeCwd, userHash, writePidfile };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { FrameDecoder, PROTOCOL_VERSION, encodeFrame, readPidfile, pidfileUsable, deletePidfile, resolveHostScript } from './chunk-2DQJKTG5.js';
2
- export { FrameDecoder, PROTOCOL_VERSION, deletePidfile, encodeFrame, isPidAlive, pidfilePath, pidfileUsable, readPidfile, resolveHostScript, socketPathFor, userHash, writePidfile } from './chunk-2DQJKTG5.js';
1
+ import { FrameDecoder, PROTOCOL_VERSION, encodeFrame, readPidfile, pidfileUsable, deletePidfile, resolveHostScript, resolveSpawnCwd, toNativeCwd } from './chunk-M5TFZDJA.js';
2
+ export { FrameDecoder, PROTOCOL_VERSION, deletePidfile, encodeFrame, isPidAlive, pidfilePath, pidfileUsable, readPidfile, resolveHostScript, resolveSpawnCwd, socketPathFor, toNativeCwd, userHash, writePidfile } from './chunk-M5TFZDJA.js';
3
3
  import { spawn } from 'node-pty';
4
4
  import { EventEmitter } from 'events';
5
5
  import fs2 from 'fs';
@@ -342,7 +342,10 @@ var InProcessBackend = class extends EventEmitter {
342
342
  env.TERM = env.TERM || "xterm-256color";
343
343
  const pty = spawn(shell, args, {
344
344
  name: "xterm-color",
345
- cwd: opts.cwd,
345
+ // Native-convert + validate the requested dir; a stale/foreign/MSYS
346
+ // cwd (e.g. Git Bash's /c/Users/me) would otherwise crash spawn with
347
+ // Windows ERROR_DIRECTORY (267). Falls back to home if unusable.
348
+ cwd: resolveSpawnCwd(opts.cwd),
346
349
  cols: opts.cols ?? 80,
347
350
  rows: opts.rows ?? 24,
348
351
  env
@@ -357,7 +360,8 @@ var InProcessBackend = class extends EventEmitter {
357
360
  opts.id,
358
361
  next.length > SCROLLBACK_MAX ? next.slice(-SCROLLBACK_MAX) : next
359
362
  );
360
- const cwd = scanOsc7Cwd(data);
363
+ const raw = scanOsc7Cwd(data);
364
+ const cwd = raw ? toNativeCwd(raw) : null;
361
365
  if (cwd && cwd !== this.liveCwd.get(opts.id)) {
362
366
  this.liveCwd.set(opts.id, cwd);
363
367
  this.scheduleCwdPersist(opts.id, cwd);
@@ -801,6 +805,42 @@ var HostClient = class _HostClient extends EventEmitter {
801
805
  isConnected() {
802
806
  return this.connected;
803
807
  }
808
+ /**
809
+ * Gracefully shut the host DOWN — the deliberate counterpart to
810
+ * `disconnect()` (which leaves the host running). Asks the host to kill its
811
+ * ptys, remove its pidfile/socket, and exit cleanly, then drops the local
812
+ * socket. Use this when a consumer needs the host genuinely gone — e.g.
813
+ * before an Electron auto-update whose installer must overwrite the binary
814
+ * the host runs on — INSTEAD of SIGKILLing by pidfile pid, which skips the
815
+ * host's own cleanup.
816
+ *
817
+ * The client mirror already holds the live scrollback, so snapshot via the
818
+ * SnapshotStore BEFORE calling this if you want T1 history to survive.
819
+ *
820
+ * Resolves once the host acknowledges (`shutdown-ok`) or the connection
821
+ * closes (the host exited), whichever comes first; never rejects — a host
822
+ * that's already gone is a successful shutdown.
823
+ */
824
+ shutdownHost(timeoutMs = 2e3) {
825
+ if (!this.socket || !this.connected) {
826
+ this.disconnect();
827
+ return Promise.resolve();
828
+ }
829
+ this.connected = false;
830
+ return new Promise((resolve) => {
831
+ let done = false;
832
+ const finish = () => {
833
+ if (done) return;
834
+ done = true;
835
+ clearTimeout(timer);
836
+ this.disconnect();
837
+ resolve();
838
+ };
839
+ const timer = setTimeout(finish, timeoutMs);
840
+ this.socket?.once("close", finish);
841
+ this.request({ kind: "shutdown", seq: this.nextSeq() }).then(finish).catch(finish);
842
+ });
843
+ }
804
844
  /** Disconnect WITHOUT killing host ptys (before-quit leave-running). */
805
845
  disconnect() {
806
846
  this.connected = false;
@@ -1005,11 +1045,22 @@ function disconnectHostLeaveRunning() {
1005
1045
  }
1006
1046
  }
1007
1047
  }
1048
+ async function shutdownHost(timeoutMs = 2e3) {
1049
+ const c = client;
1050
+ usingHost = false;
1051
+ client = null;
1052
+ setActiveBackend(inProcessBackend());
1053
+ if (!c) return;
1054
+ try {
1055
+ await c.shutdownHost(timeoutMs);
1056
+ } catch {
1057
+ }
1058
+ }
1008
1059
  function ptyHostScriptPath() {
1009
1060
  const here = typeof __dirname !== "undefined" ? __dirname : path.dirname(fileURLToPath(import.meta.url));
1010
1061
  return resolveHostScript(here) ?? path.join(here, "pty-host.js");
1011
1062
  }
1012
1063
 
1013
- export { HostClient, configureHostLifecycle, configureInProcessBackend, createSnapshotStore, cwdHookEnv, cwdHookSpawn, defaultShell, defaultShellId, detectShells, disconnectHostLeaveRunning, getHostClient, inProcessBackend, initTerminalBackend, isHostBacked, parseCommandLine, parseFileUrl, ptyHostScriptPath, resolveDefaultShell, scanOsc7Cwd, setActiveBackend, shellKind, subscribeBackendEvents, terminalManager };
1064
+ export { HostClient, configureHostLifecycle, configureInProcessBackend, createSnapshotStore, cwdHookEnv, cwdHookSpawn, defaultShell, defaultShellId, detectShells, disconnectHostLeaveRunning, getHostClient, inProcessBackend, initTerminalBackend, isHostBacked, parseCommandLine, parseFileUrl, ptyHostScriptPath, resolveDefaultShell, scanOsc7Cwd, setActiveBackend, shellKind, shutdownHost, subscribeBackendEvents, terminalManager };
1014
1065
  //# sourceMappingURL=index.js.map
1015
1066
  //# sourceMappingURL=index.js.map