fix(KB-140): fix standalone CLI native asset packaging for terminal support

- Stage node-pty native assets (pty.node, spawn-helper) alongside compiled binary

- Add runtime native-patch.ts to resolve native assets in Bun-compiled binaries

- Fix eager native module loading crash for non-terminal commands

- Enable terminal functionality in isolated standalone deployments

- Update release workflows and add STANDALONE.md documentation
This commit is contained in:
gsxdsm
2026-03-30 23:32:30 -07:00
parent 748f9440b9
commit 3c87514f95
13 changed files with 458 additions and 43 deletions

View File

@@ -11,15 +11,93 @@ import { EventEmitter } from "events";
import * as os from "os";
import * as path from "path";
import { existsSync } from "node:fs";
import { join, dirname } from "node:path";
// Detect if we're running as a Bun-compiled binary
// @ts-expect-error - Bun global is only available in Bun runtime
const isBunBinary = typeof Bun !== "undefined" && !!Bun.embeddedFiles;
// Lazy-loaded node-pty module (only loaded when terminal is actually used)
let ptyModule: typeof import("node-pty") | null = null;
let ptyLoadError: Error | null = null;
async function getPtyModule(): Promise<typeof import("node-pty")> {
if (!ptyModule) {
ptyModule = await import("node-pty");
/**
* Find the staged native assets directory for Bun-compiled binaries.
* Looks for runtime/<platform-arch>/ next to the binary.
*/
function findStagedNativeDir(): string | null {
const platform = process.platform === "darwin" ? "darwin" :
process.platform === "linux" ? "linux" :
process.platform === "win32" ? "win32" : "unknown";
const arch = process.arch === "arm64" ? "arm64" :
process.arch === "x64" ? "x64" : "unknown";
const prebuildName = `${platform}-${arch}`;
// Check KB_RUNTIME_DIR env var first
if (process.env.KB_RUNTIME_DIR) {
const envPath = join(process.env.KB_RUNTIME_DIR, prebuildName);
if (existsSync(join(envPath, "pty.node"))) {
return envPath;
}
}
// Look next to the executable
const execDir = dirname(process.execPath);
const nextToBinary = join(execDir, "runtime", prebuildName);
if (existsSync(join(nextToBinary, "pty.node"))) {
return nextToBinary;
}
return null;
}
async function loadPtyModule(): Promise<typeof import("node-pty")> {
if (ptyModule) {
return ptyModule;
}
if (ptyLoadError) {
throw ptyLoadError;
}
try {
if (isBunBinary) {
// In Bun-compiled binary, try to load with direct native path
const nativeDir = findStagedNativeDir();
if (nativeDir) {
// Set spawn-helper directory
process.env.NODE_PTY_SPAWN_HELPER_DIR = nativeDir;
// Try to load the native module directly first
const nativePath = join(nativeDir, "pty.node");
if (existsSync(nativePath)) {
try {
// Use process.dlopen to load the native module directly
// This bypasses node-pty's internal resolution
const nativeModule = { exports: {} };
(process as any).dlopen(nativeModule, nativePath);
// Now that we have the native module loaded, try importing node-pty
// which should find the already-loaded module
const mod = await import("node-pty");
ptyModule = mod;
return ptyModule as typeof import("node-pty");
} catch (directErr) {
// Direct loading failed, fall through to standard import
console.warn("[terminal] Direct native load failed, trying standard import:", directErr);
}
}
}
}
// Standard import path
const mod = await import("node-pty");
ptyModule = mod;
return ptyModule as typeof import("node-pty");
} catch (err) {
ptyLoadError = err instanceof Error ? err : new Error(String(err));
throw ptyLoadError;
}
return ptyModule;
}
// Maximum scrollback buffer size (characters)
@@ -356,8 +434,8 @@ export class TerminalService extends EventEmitter {
console.info(`Creating session ${id} with shell: ${shell} in ${cwd}`);
// Lazy-load node-pty module
const pty = await getPtyModule();
// Lazy-load node-pty module with proper error handling
const pty = await loadPtyModule();
// Build PTY spawn options
const ptyOptions: IPtyForkOptions = {