feat(KB-142): fix CLI native module packaging with lazy loading

- Implement lazy native module loading with graceful degradation for standalone CLI

- Align release workflows with executable payload structure

- Update macOS signing script for single-executable bundles

- Add documentation for standalone CLI packaging (STANDALONE.md)

- Create changeset for patch release
This commit is contained in:
gsxdsm
2026-03-30 23:56:01 -07:00
parent 7eb9bddfd4
commit 9e8cf21058
8 changed files with 242 additions and 89 deletions

View File

@@ -26,10 +26,10 @@ let ptyLoadError: Error | null = null;
* Looks for runtime/<platform-arch>/ next to the binary.
*/
function findStagedNativeDir(): string | null {
const platform = process.platform === "darwin" ? "darwin" :
process.platform === "linux" ? "linux" :
const platform = process.platform === "darwin" ? "darwin" :
process.platform === "linux" ? "linux" :
process.platform === "win32" ? "win32" : "unknown";
const arch = process.arch === "arm64" ? "arm64" :
const arch = process.arch === "arm64" ? "arm64" :
process.arch === "x64" ? "x64" : "unknown";
const prebuildName = `${platform}-${arch}`;
@@ -60,37 +60,37 @@ async function loadPtyModule(): Promise<typeof import("node-pty")> {
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
// For Bun-compiled binary, set up native paths before loading
if (isBunBinary) {
const nativeDir = findStagedNativeDir();
if (nativeDir) {
// Set spawn-helper directory
if (process.platform !== "win32") {
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);
}
}
// Store reference for debugging
process.env.KB_NATIVE_ASSETS_PATH = nativeDir;
// Try to pre-load the native module using process.dlopen
// This can help when the normal require() path fails
const nativePath = join(nativeDir, "pty.node");
if (existsSync(nativePath)) {
try {
const nativeModule: { exports?: unknown } = { exports: {} };
// @ts-ignore - process.dlopen is Node internal
process.dlopen(nativeModule, nativePath);
console.log("[terminal] Pre-loaded native module via dlopen");
} catch (dlopenErr) {
// dlopen failed - log but continue, normal import might still work
console.log("[terminal] dlopen pre-load failed (continuing):", dlopenErr);
}
}
}
// Standard import path
}
try {
// Standard import path - the native-patch setup should have created
// the necessary symlink structure for node-pty to find the module
const mod = await import("node-pty");
ptyModule = mod;
return ptyModule as typeof import("node-pty");
@@ -435,7 +435,15 @@ export class TerminalService extends EventEmitter {
console.info(`Creating session ${id} with shell: ${shell} in ${cwd}`);
// Lazy-load node-pty module with proper error handling
const pty = await loadPtyModule();
let pty: typeof import("node-pty");
try {
pty = await loadPtyModule();
} catch (loadErr) {
// Native module couldn't be loaded (common in Bun binaries without proper setup)
// Return null for graceful degradation - routes will return 503
console.error(`[terminal] Failed to load PTY module: ${loadErr}`);
return null;
}
// Build PTY spawn options
const ptyOptions: IPtyForkOptions = {