fix(KB-151): lazy-load node-pty to fix packaged binary execution

- Convert static node-pty import to dynamic import in TerminalService\n- Only load native PTY module when terminal session is actually created\n- Fix Bun-compiled binary failures on commands like --help, task list\n- Add changeset documenting the lazy-loading fix
This commit is contained in:
gsxdsm
2026-03-30 08:37:31 -07:00
parent e35c745fc2
commit 8b4ac5d43e
2 changed files with 29 additions and 8 deletions

View File

@@ -0,0 +1,7 @@
---
"@dustinbyrne/kb": patch
---
Fix packaged binary tests by lazy-loading node-pty
The Bun-compiled `kb` binary was failing to run `--help`, `task list`, and `dashboard` commands due to eager loading of the native `node-pty` module. The fix converts the static import to a dynamic import that only executes when a terminal session is actually being created. This allows CLI commands that don't use the terminal to work without loading the native module, while preserving full dashboard terminal functionality when running from source.

View File

@@ -5,12 +5,23 @@
* Supports cross-platform shell detection and secure session management.
*/
import * as pty from "node-pty";
// Static type-only import for types (no runtime code)
import type { IPty, IPtyForkOptions, IWindowsPtyForkOptions } from "node-pty";
import { EventEmitter } from "events";
import * as os from "os";
import * as path from "path";
import { existsSync } from "node:fs";
// Lazy-loaded node-pty module (only loaded when terminal is actually used)
let ptyModule: typeof import("node-pty") | null = null;
async function getPtyModule(): Promise<typeof import("node-pty")> {
if (!ptyModule) {
ptyModule = await import("node-pty");
}
return ptyModule;
}
// Maximum scrollback buffer size (characters)
const MAX_SCROLLBACK_SIZE = 50000; // ~50KB per terminal
@@ -50,7 +61,7 @@ const STRIP_ENV_VARS = [
export interface TerminalSession {
id: string;
pty: pty.IPty;
pty: IPty;
cwd: string;
createdAt: Date;
shell: string;
@@ -90,7 +101,7 @@ export class TerminalService extends EventEmitter {
* Kill a PTY process with platform-specific handling.
* Windows doesn't support Unix signals like SIGTERM/SIGKILL.
*/
private killPtyProcess(ptyProcess: pty.IPty, signal: string = "SIGTERM"): void {
private killPtyProcess(ptyProcess: IPty, signal: string = "SIGTERM"): void {
if (this.isWindows) {
ptyProcess.kill();
} else {
@@ -284,8 +295,11 @@ export class TerminalService extends EventEmitter {
console.info(`Creating session ${id} with shell: ${shell} in ${cwd}`);
// Lazy-load node-pty module
const pty = await getPtyModule();
// Build PTY spawn options
const ptyOptions: pty.IPtyForkOptions = {
const ptyOptions: IPtyForkOptions = {
name: "xterm-256color",
cols: options.cols || 80,
rows: options.rows || 24,
@@ -295,10 +309,10 @@ export class TerminalService extends EventEmitter {
// On Windows, use winpty instead of ConPTY for compatibility
if (this.isWindows) {
(ptyOptions as pty.IWindowsPtyForkOptions).useConpty = false;
(ptyOptions as IWindowsPtyForkOptions).useConpty = false;
}
let ptyProcess: pty.IPty;
let ptyProcess: IPty;
try {
ptyProcess = pty.spawn(shell, shellArgs, ptyOptions);
} catch (spawnError) {
@@ -340,7 +354,7 @@ export class TerminalService extends EventEmitter {
};
// Forward data events with throttling
ptyProcess.onData((data) => {
ptyProcess.onData((data: string) => {
if (session.resizeInProgress) {
return;
}
@@ -360,7 +374,7 @@ export class TerminalService extends EventEmitter {
});
// Handle exit
ptyProcess.onExit(({ exitCode }) => {
ptyProcess.onExit(({ exitCode }: { exitCode: number; signal?: number }) => {
console.info(`Session exited with code ${exitCode ?? 0} (${id})`);
this.sessions.delete(id);
this.exitCallbacks.forEach((cb) => cb(id, exitCode ?? 0));