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:
12
.changeset/fix-cli-native-module-packaging.md
Normal file
12
.changeset/fix-cli-native-module-packaging.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
"@dustinbyrne/kb": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix standalone CLI executable native module packaging
|
||||||
|
|
||||||
|
- Native module resolution is now lazy: only initializes when dashboard starts
|
||||||
|
- Lightweight commands (--help, task list) no longer trigger native module loading
|
||||||
|
- PTY terminal gracefully degrades to HTTP 503 when native assets unavailable
|
||||||
|
- Adds process.dlopen() fallback when /$bunfs/root symlink can't be created
|
||||||
|
- macOS signing script now signs native .node files in runtime/ directory
|
||||||
|
- Release workflows include runtime/ directory in artifacts
|
||||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -78,7 +78,7 @@ jobs:
|
|||||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
|
APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
|
||||||
run: bash scripts/sign-macos.sh packages/cli/dist/${{ matrix.binary }}
|
run: bash scripts/sign-macos.sh packages/cli/dist/${{ matrix.binary }} packages/cli/dist/runtime
|
||||||
|
|
||||||
- name: Sign Windows binary
|
- name: Sign Windows binary
|
||||||
if: runner.os == 'Windows'
|
if: runner.os == 'Windows'
|
||||||
|
|||||||
2
.github/workflows/test-release.yml
vendored
2
.github/workflows/test-release.yml
vendored
@@ -80,7 +80,7 @@ jobs:
|
|||||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
|
APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
|
||||||
run: bash scripts/sign-macos.sh packages/cli/dist/${{ matrix.binary }}
|
run: bash scripts/sign-macos.sh packages/cli/dist/${{ matrix.binary }} packages/cli/dist/runtime
|
||||||
|
|
||||||
- name: Sign Windows binary
|
- name: Sign Windows binary
|
||||||
if: runner.os == 'Windows' && env.WINDOWS_CERTIFICATE_BASE64 != ''
|
if: runner.os == 'Windows' && env.WINDOWS_CERTIFICATE_BASE64 != ''
|
||||||
|
|||||||
@@ -147,6 +147,14 @@ dist/
|
|||||||
- `linux-x64/` - Linux x64
|
- `linux-x64/` - Linux x64
|
||||||
- `win32-x64/` - Windows x64
|
- `win32-x64/` - Windows x64
|
||||||
|
|
||||||
**Important:** When distributing or moving the binary, ensure the `client/` and `runtime/` directories are copied alongside it. Terminal functionality will be unavailable if runtime assets are missing.
|
**Important:** When distributing or moving the binary, ensure the `client/` and `runtime/` directories are copied alongside it. Terminal functionality will gracefully degrade (return HTTP 503) if runtime assets are missing — the dashboard will continue to work but terminal sessions won't be available.
|
||||||
|
|
||||||
See the [GitHub repository](https://github.com/dustinbyrne/kb) for platform-specific binaries and build instructions.
|
**How it works:**
|
||||||
|
When the dashboard starts from a Bun-compiled binary, it attempts to set up native module resolution so `node-pty` can find its platform-specific `.node` files. This involves:
|
||||||
|
1. Copying native assets to a temp directory (`/tmp/kb-bunfs-<pid>/kb/prebuilds/<platform>/`)
|
||||||
|
2. Attempting to create a symlink at `/$bunfs/root` pointing to the temp directory (Unix platforms)
|
||||||
|
3. If the symlink can't be created (e.g., macOS permissions), pre-loading the native module via `process.dlopen()`
|
||||||
|
|
||||||
|
If all resolution methods fail, terminal creation gracefully returns `null`, which the HTTP layer converts to a 503 Service Unavailable response.
|
||||||
|
|
||||||
|
**Cross-compilation:** Native assets are staged per-platform during build. When cross-compiling, only the target platform's assets are included. PTY functionality requires running on a platform with matching native assets.
|
||||||
|
|||||||
@@ -15,11 +15,6 @@ import { mkdtempSync, writeFileSync, existsSync } from "node:fs";
|
|||||||
import { join, dirname } from "node:path";
|
import { join, dirname } from "node:path";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
|
|
||||||
// ── Runtime native module resolution patch ───────────────────────────
|
|
||||||
// This must be imported before any modules that load native binaries (node-pty)
|
|
||||||
// It sets up paths so the standalone binary can find staged native assets.
|
|
||||||
import "./runtime/native-patch.js";
|
|
||||||
|
|
||||||
// @ts-expect-error -- Bun-only global; undefined in Node
|
// @ts-expect-error -- Bun-only global; undefined in Node
|
||||||
const isBunBinary = typeof Bun !== "undefined" && !!Bun.embeddedFiles;
|
const isBunBinary = typeof Bun !== "undefined" && !!Bun.embeddedFiles;
|
||||||
|
|
||||||
@@ -119,6 +114,13 @@ async function main() {
|
|||||||
try {
|
try {
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case "dashboard": {
|
case "dashboard": {
|
||||||
|
// Initialize native module resolution for Bun binary before starting dashboard
|
||||||
|
// This sets up the paths so node-pty can find its native assets
|
||||||
|
if (isBunBinary) {
|
||||||
|
const { initNativePatch } = await import("./runtime/native-patch.js");
|
||||||
|
initNativePatch();
|
||||||
|
}
|
||||||
|
|
||||||
const portIdx = args.indexOf("--port");
|
const portIdx = args.indexOf("--port");
|
||||||
const portIdxShort = args.indexOf("-p");
|
const portIdxShort = args.indexOf("-p");
|
||||||
const pi = portIdx !== -1 ? portIdx : portIdxShort;
|
const pi = portIdx !== -1 ? portIdx : portIdxShort;
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
/**
|
/**
|
||||||
* Native Module Runtime Resolution Patch
|
* Native Module Runtime Resolution Patch
|
||||||
*
|
*
|
||||||
* This module creates the directory structure that Bun's compiled binary
|
* This module sets up the directory structure needed for node-pty to find its native
|
||||||
* expects for resolving relative paths to native modules.
|
* modules when running from a Bun-compiled binary.
|
||||||
*
|
*
|
||||||
* When Bun compiles a binary, it creates a virtual filesystem at /$bunfs/root/
|
* When Bun compiles a binary, it creates a virtual filesystem at /$bunfs/root/
|
||||||
* where the bundled code runs from. Node-pty tries to load its native module
|
* where bundled code runs. Node-pty looks for native modules at:
|
||||||
* using paths relative to this virtual location.
|
* /$bunfs/root/prebuilds/<platform>-<arch>/pty.node
|
||||||
*
|
*
|
||||||
* We create a real directory structure at /tmp/kb-bunfs-root/kb/ that mirrors
|
* This module creates a real directory structure at /tmp/kb-bunfs-<pid>/ that mirrors
|
||||||
* the virtual structure, and set up symlinks so the native module can be found.
|
* the expected structure, then attempts to create a symlink from /$bunfs/root to that
|
||||||
|
* temp directory (on macOS/Linux) so node-pty can find the native assets.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { join, dirname, basename } from "node:path";
|
import { join, basename, dirname } from "node:path";
|
||||||
import { existsSync, copyFileSync, mkdirSync, symlinkSync, rmSync } from "node:fs";
|
import { existsSync, copyFileSync, mkdirSync, symlinkSync, rmSync, lstatSync, readlinkSync } from "node:fs";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
|
|
||||||
// Detect Bun-compiled binary
|
// Detect Bun-compiled binary
|
||||||
@@ -21,24 +22,24 @@ import { tmpdir } from "node:os";
|
|||||||
const isBunBinary = typeof Bun !== "undefined" && !!Bun.embeddedFiles;
|
const isBunBinary = typeof Bun !== "undefined" && !!Bun.embeddedFiles;
|
||||||
|
|
||||||
let initialized = false;
|
let initialized = false;
|
||||||
|
let bunfsSymlinkPath: string | null = null;
|
||||||
// The virtual root that Bun uses
|
|
||||||
const BUNFS_ROOT = "/$bunfs/root";
|
|
||||||
|
|
||||||
function findStagedNativeDir(): string | null {
|
function findStagedNativeDir(): string | null {
|
||||||
const platform = process.platform === "darwin" ? "darwin" :
|
const platform = process.platform === "darwin" ? "darwin" :
|
||||||
process.platform === "linux" ? "linux" :
|
process.platform === "linux" ? "linux" :
|
||||||
process.platform === "win32" ? "win32" : "unknown";
|
process.platform === "win32" ? "win32" : "unknown";
|
||||||
const arch = process.arch === "arm64" ? "arm64" :
|
const arch = process.arch === "arm64" ? "arm64" :
|
||||||
process.arch === "x64" ? "x64" : "unknown";
|
process.arch === "x64" ? "x64" : "unknown";
|
||||||
const prebuildName = `${platform}-${arch}`;
|
const prebuildName = `${platform}-${arch}`;
|
||||||
|
|
||||||
|
// Look next to the executable first
|
||||||
const execDir = dirname(process.execPath);
|
const execDir = dirname(process.execPath);
|
||||||
const nextToBinary = join(execDir, "runtime", prebuildName);
|
const nextToBinary = join(execDir, "runtime", prebuildName);
|
||||||
if (existsSync(join(nextToBinary, "pty.node"))) {
|
if (existsSync(join(nextToBinary, "pty.node"))) {
|
||||||
return nextToBinary;
|
return nextToBinary;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check KB_RUNTIME_DIR env var
|
||||||
if (process.env.KB_RUNTIME_DIR) {
|
if (process.env.KB_RUNTIME_DIR) {
|
||||||
const envPath = join(process.env.KB_RUNTIME_DIR, prebuildName);
|
const envPath = join(process.env.KB_RUNTIME_DIR, prebuildName);
|
||||||
if (existsSync(join(envPath, "pty.node"))) {
|
if (existsSync(join(envPath, "pty.node"))) {
|
||||||
@@ -50,26 +51,54 @@ function findStagedNativeDir(): string | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a symlink structure that helps node-pty find its native module.
|
* Clean up any stale /$bunfs/root symlinks from previous runs.
|
||||||
*
|
* This handles cases where a previous process crashed and left a dangling symlink.
|
||||||
* The idea: Create a temp directory structure that looks like:
|
|
||||||
* /tmp/kb-bunfs-root/kb/prebuilds/darwin-arm64/pty.node -> <staged>/pty.node
|
|
||||||
*
|
|
||||||
* Then we try to influence the module loader to look here.
|
|
||||||
*/
|
*/
|
||||||
function setupNativeResolution(): void {
|
function cleanupStaleBunfsLinks(): void {
|
||||||
|
if (process.platform === "win32") return; // Windows doesn't use symlinks for this
|
||||||
|
|
||||||
|
const bunfsRoot = "/$bunfs/root";
|
||||||
|
try {
|
||||||
|
if (existsSync(bunfsRoot)) {
|
||||||
|
const stats = lstatSync(bunfsRoot);
|
||||||
|
if (stats.isSymbolicLink()) {
|
||||||
|
const target = readlinkSync(bunfsRoot);
|
||||||
|
// If the target is a temp dir that no longer exists, remove the stale link
|
||||||
|
if (target.includes("kb-bunfs-") && !existsSync(target)) {
|
||||||
|
rmSync(bunfsRoot);
|
||||||
|
console.log("[kb-native-patch] Cleaned up stale /$bunfs/root symlink");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore errors during cleanup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up the native module resolution structure.
|
||||||
|
*
|
||||||
|
* Creates:
|
||||||
|
* /tmp/kb-bunfs-<pid>/kb/prebuilds/<platform>-<arch>/
|
||||||
|
* ├── pty.node
|
||||||
|
* └── spawn-helper (Unix only)
|
||||||
|
*
|
||||||
|
* Then attempts to create a symlink at /$bunfs/root pointing to the temp directory
|
||||||
|
* so that node-pty's relative require() can find the native module.
|
||||||
|
*/
|
||||||
|
export function setupNativeResolution(): { success: boolean; nativeDir: string | null } {
|
||||||
const nativeDir = findStagedNativeDir();
|
const nativeDir = findStagedNativeDir();
|
||||||
if (!nativeDir) {
|
if (!nativeDir) {
|
||||||
console.warn("[kb-native-patch] No native assets found, terminal will be unavailable");
|
console.warn("[kb-native-patch] No native assets found, terminal will be unavailable");
|
||||||
return;
|
return { success: false, nativeDir: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set spawn-helper location
|
// Set spawn-helper location (Unix platforms)
|
||||||
if (process.platform !== "win32") {
|
if (process.platform !== "win32") {
|
||||||
process.env.NODE_PTY_SPAWN_HELPER_DIR = nativeDir;
|
process.env.NODE_PTY_SPAWN_HELPER_DIR = nativeDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store reference
|
// Store reference for other code to use
|
||||||
process.env.KB_NATIVE_ASSETS_PATH = nativeDir;
|
process.env.KB_NATIVE_ASSETS_PATH = nativeDir;
|
||||||
|
|
||||||
// Create the fake bunfs structure
|
// Create the fake bunfs structure
|
||||||
@@ -79,49 +108,116 @@ function setupNativeResolution(): void {
|
|||||||
const platformDir = join(prebuildsDir, basename(nativeDir));
|
const platformDir = join(prebuildsDir, basename(nativeDir));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Clean up any previous stale links first
|
||||||
|
cleanupStaleBunfsLinks();
|
||||||
|
|
||||||
|
// Create directory structure
|
||||||
mkdirSync(platformDir, { recursive: true });
|
mkdirSync(platformDir, { recursive: true });
|
||||||
|
|
||||||
// Copy native files to this location
|
// Copy native files to this location
|
||||||
copyFileSync(join(nativeDir, "pty.node"), join(platformDir, "pty.node"));
|
const ptyNodeDest = join(platformDir, "pty.node");
|
||||||
|
copyFileSync(join(nativeDir, "pty.node"), ptyNodeDest);
|
||||||
|
|
||||||
if (existsSync(join(nativeDir, "spawn-helper"))) {
|
if (existsSync(join(nativeDir, "spawn-helper"))) {
|
||||||
copyFileSync(join(nativeDir, "spawn-helper"), join(platformDir, "spawn-helper"));
|
copyFileSync(join(nativeDir, "spawn-helper"), join(platformDir, "spawn-helper"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the path for potential use
|
// Store the path for potential use
|
||||||
process.env.KB_FAKE_BUNFS_ROOT = tmpRoot;
|
process.env.KB_FAKE_BUNFS_ROOT = tmpRoot;
|
||||||
|
|
||||||
// We can't actually create /$bunfs/root as it's a virtual path
|
// Try to create symlink from /$bunfs/root to our temp directory
|
||||||
// But we can try to influence NODE_PATH
|
// This allows node-pty's relative require() to find the native module
|
||||||
const nodeModulesAtRoot = join(tmpRoot, "node_modules");
|
if (process.platform !== "win32") {
|
||||||
mkdirSync(nodeModulesAtRoot, { recursive: true });
|
const bunfsRoot = "/$bunfs/root";
|
||||||
|
try {
|
||||||
// Prepend to NODE_PATH
|
// Remove any existing symlink first (in case it was left by a crashed process)
|
||||||
const current = process.env.NODE_PATH || "";
|
if (existsSync(bunfsRoot)) {
|
||||||
const sep = process.platform === "win32" ? ";" : ":";
|
const stats = lstatSync(bunfsRoot);
|
||||||
process.env.NODE_PATH = tmpRoot + sep + current;
|
if (stats.isSymbolicLink()) {
|
||||||
|
rmSync(bunfsRoot);
|
||||||
console.log("[kb-native-patch] Set up native resolution at:", tmpRoot);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new symlink pointing to our temp kb directory
|
||||||
|
// We want /$bunfs/root -> /tmp/kb-bunfs-<pid>/kb
|
||||||
|
// So that /$bunfs/root/prebuilds/<platform>/pty.node resolves correctly
|
||||||
|
symlinkSync(kbDir, bunfsRoot);
|
||||||
|
bunfsSymlinkPath = bunfsRoot;
|
||||||
|
console.log("[kb-native-patch] Created /$bunfs/root symlink for native module resolution");
|
||||||
|
} catch (symlinkErr) {
|
||||||
|
// Symlink creation failed (likely permission denied) - not fatal
|
||||||
|
// The terminal service will try alternative loading methods
|
||||||
|
console.log("[kb-native-patch] Could not create /$bunfs/root symlink (permissions), using fallback");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[kb-native-patch] Native assets staged at:", tmpRoot);
|
||||||
|
return { success: true, nativeDir };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[kb-native-patch] Failed to setup resolution:", err);
|
console.error("[kb-native-patch] Failed to setup native resolution:", err);
|
||||||
|
return { success: false, nativeDir: null };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initNativePatch(): void {
|
/**
|
||||||
|
* Clean up the symlink we created (call this on process exit).
|
||||||
|
*/
|
||||||
|
export function cleanupNativeResolution(): void {
|
||||||
|
if (bunfsSymlinkPath && process.platform !== "win32") {
|
||||||
|
try {
|
||||||
|
if (existsSync(bunfsSymlinkPath)) {
|
||||||
|
const stats = lstatSync(bunfsSymlinkPath);
|
||||||
|
if (stats.isSymbolicLink()) {
|
||||||
|
rmSync(bunfsSymlinkPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore cleanup errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bunfsSymlinkPath = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the native module resolution patch.
|
||||||
|
* This should be called lazily (e.g., when dashboard starts), not at import time.
|
||||||
|
*/
|
||||||
|
export function initNativePatch(): { success: boolean; nativeDir: string | null } {
|
||||||
if (initialized || !isBunBinary) {
|
if (initialized || !isBunBinary) {
|
||||||
return;
|
return { success: true, nativeDir: process.env.KB_NATIVE_ASSETS_PATH || null };
|
||||||
}
|
}
|
||||||
|
|
||||||
setupNativeResolution();
|
const result = setupNativeResolution();
|
||||||
initialized = true;
|
initialized = true;
|
||||||
|
|
||||||
|
// Register cleanup on exit
|
||||||
|
process.on("exit", cleanupNativeResolution);
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
cleanupNativeResolution();
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
process.on("SIGTERM", () => {
|
||||||
|
cleanupNativeResolution();
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if terminal functionality is available (native assets found).
|
||||||
|
*/
|
||||||
export function isTerminalAvailable(): boolean {
|
export function isTerminalAvailable(): boolean {
|
||||||
if (!isBunBinary) return true;
|
if (!isBunBinary) return true;
|
||||||
return findStagedNativeDir() !== null;
|
return findStagedNativeDir() !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the path to the staged native assets directory.
|
||||||
|
*/
|
||||||
export function getNativeDir(): string | null {
|
export function getNativeDir(): string | null {
|
||||||
return findStagedNativeDir();
|
return findStagedNativeDir();
|
||||||
}
|
}
|
||||||
|
|
||||||
initNativePatch();
|
// Note: We do NOT auto-initialize at import time anymore.
|
||||||
|
// Callers should explicitly call initNativePatch() when needed.
|
||||||
|
|||||||
@@ -26,10 +26,10 @@ let ptyLoadError: Error | null = null;
|
|||||||
* Looks for runtime/<platform-arch>/ next to the binary.
|
* Looks for runtime/<platform-arch>/ next to the binary.
|
||||||
*/
|
*/
|
||||||
function findStagedNativeDir(): string | null {
|
function findStagedNativeDir(): string | null {
|
||||||
const platform = process.platform === "darwin" ? "darwin" :
|
const platform = process.platform === "darwin" ? "darwin" :
|
||||||
process.platform === "linux" ? "linux" :
|
process.platform === "linux" ? "linux" :
|
||||||
process.platform === "win32" ? "win32" : "unknown";
|
process.platform === "win32" ? "win32" : "unknown";
|
||||||
const arch = process.arch === "arm64" ? "arm64" :
|
const arch = process.arch === "arm64" ? "arm64" :
|
||||||
process.arch === "x64" ? "x64" : "unknown";
|
process.arch === "x64" ? "x64" : "unknown";
|
||||||
const prebuildName = `${platform}-${arch}`;
|
const prebuildName = `${platform}-${arch}`;
|
||||||
|
|
||||||
@@ -60,37 +60,37 @@ async function loadPtyModule(): Promise<typeof import("node-pty")> {
|
|||||||
throw ptyLoadError;
|
throw ptyLoadError;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// For Bun-compiled binary, set up native paths before loading
|
||||||
if (isBunBinary) {
|
if (isBunBinary) {
|
||||||
// In Bun-compiled binary, try to load with direct native path
|
const nativeDir = findStagedNativeDir();
|
||||||
const nativeDir = findStagedNativeDir();
|
if (nativeDir) {
|
||||||
if (nativeDir) {
|
// Set spawn-helper directory
|
||||||
// Set spawn-helper directory
|
if (process.platform !== "win32") {
|
||||||
process.env.NODE_PTY_SPAWN_HELPER_DIR = nativeDir;
|
process.env.NODE_PTY_SPAWN_HELPER_DIR = nativeDir;
|
||||||
|
}
|
||||||
// Try to load the native module directly first
|
// Store reference for debugging
|
||||||
const nativePath = join(nativeDir, "pty.node");
|
process.env.KB_NATIVE_ASSETS_PATH = nativeDir;
|
||||||
if (existsSync(nativePath)) {
|
|
||||||
try {
|
// Try to pre-load the native module using process.dlopen
|
||||||
// Use process.dlopen to load the native module directly
|
// This can help when the normal require() path fails
|
||||||
// This bypasses node-pty's internal resolution
|
const nativePath = join(nativeDir, "pty.node");
|
||||||
const nativeModule = { exports: {} };
|
if (existsSync(nativePath)) {
|
||||||
(process as any).dlopen(nativeModule, nativePath);
|
try {
|
||||||
|
const nativeModule: { exports?: unknown } = { exports: {} };
|
||||||
// Now that we have the native module loaded, try importing node-pty
|
// @ts-ignore - process.dlopen is Node internal
|
||||||
// which should find the already-loaded module
|
process.dlopen(nativeModule, nativePath);
|
||||||
const mod = await import("node-pty");
|
console.log("[terminal] Pre-loaded native module via dlopen");
|
||||||
ptyModule = mod;
|
} catch (dlopenErr) {
|
||||||
return ptyModule as typeof import("node-pty");
|
// dlopen failed - log but continue, normal import might still work
|
||||||
} catch (directErr) {
|
console.log("[terminal] dlopen pre-load failed (continuing):", dlopenErr);
|
||||||
// Direct loading failed, fall through to standard import
|
|
||||||
console.warn("[terminal] Direct native load failed, trying standard import:", directErr);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// 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");
|
const mod = await import("node-pty");
|
||||||
ptyModule = mod;
|
ptyModule = mod;
|
||||||
return ptyModule as typeof import("node-pty");
|
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}`);
|
console.info(`Creating session ${id} with shell: ${shell} in ${cwd}`);
|
||||||
|
|
||||||
// Lazy-load node-pty module with proper error handling
|
// 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
|
// Build PTY spawn options
|
||||||
const ptyOptions: IPtyForkOptions = {
|
const ptyOptions: IPtyForkOptions = {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
# Usage:
|
# Usage:
|
||||||
# APPLE_CERTIFICATE_BASE64=... APPLE_CERTIFICATE_PASSWORD=... \
|
# APPLE_CERTIFICATE_BASE64=... APPLE_CERTIFICATE_PASSWORD=... \
|
||||||
# APPLE_IDENTITY=... APPLE_ID=... APPLE_TEAM_ID=... APPLE_APP_PASSWORD=... \
|
# APPLE_IDENTITY=... APPLE_ID=... APPLE_TEAM_ID=... APPLE_APP_PASSWORD=... \
|
||||||
# bash scripts/sign-macos.sh path/to/binary
|
# bash scripts/sign-macos.sh path/to/binary [path/to/runtime/dir]
|
||||||
#
|
#
|
||||||
# Environment variables (all required):
|
# Environment variables (all required):
|
||||||
# APPLE_CERTIFICATE_BASE64 — Base64-encoded .p12 Developer ID Application certificate
|
# APPLE_CERTIFICATE_BASE64 — Base64-encoded .p12 Developer ID Application certificate
|
||||||
@@ -15,14 +15,17 @@
|
|||||||
# APPLE_APP_PASSWORD — App-specific password for notarization
|
# APPLE_APP_PASSWORD — App-specific password for notarization
|
||||||
#
|
#
|
||||||
# The script is idempotent — re-running on an already-signed binary will re-sign it.
|
# The script is idempotent — re-running on an already-signed binary will re-sign it.
|
||||||
|
# If runtime directory is provided, all .node files in it will also be signed.
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# ── Validate arguments ────────────────────────────────────────────────
|
# ── Validate arguments ────────────────────────────────────────────────
|
||||||
BINARY="${1:-}"
|
BINARY="${1:-}"
|
||||||
|
RUNTIME_DIR="${2:-}" # Optional: path to runtime/ directory with native assets
|
||||||
|
|
||||||
if [[ -z "$BINARY" ]]; then
|
if [[ -z "$BINARY" ]]; then
|
||||||
echo "ERROR: No binary path provided."
|
echo "ERROR: No binary path provided."
|
||||||
echo "Usage: $0 <path-to-binary>"
|
echo "Usage: $0 <path-to-binary> [path-to-runtime-dir]"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -86,6 +89,18 @@ security list-keychains -d user -s "$KEYCHAIN_NAME" $(security list-keychains -d
|
|||||||
|
|
||||||
echo "==> Keychain configured."
|
echo "==> Keychain configured."
|
||||||
|
|
||||||
|
# ── Sign native runtime assets if provided ──────────────────────────
|
||||||
|
if [[ -n "$RUNTIME_DIR" && -d "$RUNTIME_DIR" ]]; then
|
||||||
|
echo "==> Signing native runtime assets in: $RUNTIME_DIR"
|
||||||
|
find "$RUNTIME_DIR" -name "*.node" -type f | while read -r native_file; do
|
||||||
|
echo " Signing: $native_file"
|
||||||
|
codesign --force --options runtime --sign "$APPLE_IDENTITY" \
|
||||||
|
--keychain "$KEYCHAIN_NAME" \
|
||||||
|
"$native_file"
|
||||||
|
done
|
||||||
|
echo "==> Native runtime assets signed."
|
||||||
|
fi
|
||||||
|
|
||||||
# ── Codesign the binary ───────────────────────────────────────────────
|
# ── Codesign the binary ───────────────────────────────────────────────
|
||||||
echo "==> Codesigning with identity: $APPLE_IDENTITY"
|
echo "==> Codesigning with identity: $APPLE_IDENTITY"
|
||||||
codesign --force --options runtime --sign "$APPLE_IDENTITY" \
|
codesign --force --options runtime --sign "$APPLE_IDENTITY" \
|
||||||
@@ -101,7 +116,19 @@ ZIP_FILE="$(mktemp -t notarize.XXXXXX).zip"
|
|||||||
trap 'rm -f "$ZIP_FILE"; cleanup' EXIT
|
trap 'rm -f "$ZIP_FILE"; cleanup' EXIT
|
||||||
|
|
||||||
# Create a ZIP for notarization submission
|
# Create a ZIP for notarization submission
|
||||||
ditto -c -k --keepParent "$BINARY" "$ZIP_FILE"
|
# Include runtime directory if it exists
|
||||||
|
if [[ -n "$RUNTIME_DIR" && -d "$RUNTIME_DIR" ]]; then
|
||||||
|
BINARY_DIR=$(dirname "$BINARY")
|
||||||
|
BINARY_NAME=$(basename "$BINARY")
|
||||||
|
# Create temp directory with both binary and runtime
|
||||||
|
TEMP_DIR=$(mktemp -d)
|
||||||
|
cp "$BINARY" "$TEMP_DIR/"
|
||||||
|
cp -r "$RUNTIME_DIR" "$TEMP_DIR/"
|
||||||
|
ditto -c -k --keepParent "$TEMP_DIR/$BINARY_NAME" "$ZIP_FILE"
|
||||||
|
rm -rf "$TEMP_DIR"
|
||||||
|
else
|
||||||
|
ditto -c -k --keepParent "$BINARY" "$ZIP_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "==> Submitting to Apple notarization service..."
|
echo "==> Submitting to Apple notarization service..."
|
||||||
xcrun notarytool submit "$ZIP_FILE" \
|
xcrun notarytool submit "$ZIP_FILE" \
|
||||||
|
|||||||
Reference in New Issue
Block a user