fix(pty): switch to @homebridge/node-pty-prebuilt-multiarch fork
Resolves "Failed to load PTY module" install errors on Linux/macOS by aliasing node-pty to the homebridge fork, which ships prebuilds for linux x64/arm64/arm/ia32 across many Node ABIs and uses prebuild-install for darwin/windows binaries on install. - Aliased dep so all "node-pty" import specifiers (and vi.mock calls) keep working unchanged. - Removed darwin chmod postinstall hack (fork handles permissions). - Updated cli/build.ts to dynamically resolve node-pty install root and pick prebuilds by ABI for Linux cross-compile; warn-and-skip for darwin/windows cross-compile (host-only there, as before). - Added type shim because the fork's bundled typings declare module '@homebridge/node-pty-prebuilt-multiarch', not 'node-pty'. Verified: pnpm install clean, dashboard typecheck clean, native module loads and spawns shell via fork, host + linux-x64 cross-compile staging both produce dist/runtime/<plat>/pty.node. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -811,7 +811,7 @@ Plugin management endpoints with multi-project scoping support via `projectId` q
|
||||
|
||||
- **Frontend**: React + Vite, TypeScript, xterm.js for terminal emulation, CSS custom properties for theming
|
||||
- **Backend**: Express server with REST API, badge WebSocket at `/api/ws`, terminal WebSocket at `/api/terminal/ws`, and Server-Sent Events (SSE) for task/log updates
|
||||
- **Terminal**: node-pty for PTY spawning, WebSocket for bidirectional I/O
|
||||
- **Terminal**: @homebridge/node-pty-prebuilt-multiarch (aliased as node-pty) for PTY spawning, WebSocket for bidirectional I/O
|
||||
- **Badge Updates**: `useBadgeWebSocket()` shares a single browser socket and subscribes per visible GitHub-linked task card
|
||||
- **State Management**: Custom hooks with EventSource for real-time task updates plus a dedicated WebSocket store for badge snapshots
|
||||
- **Git Integration**: Server-side git command execution with validation
|
||||
|
||||
@@ -36,8 +36,7 @@
|
||||
"build:client": "vite build",
|
||||
"dev": "pnpm build && pnpm typecheck && pnpm dev:serve",
|
||||
"dev:serve": "vite dev",
|
||||
"postinstall": "chmod +x node_modules/.pnpm/node-pty*/node_modules/node-pty/prebuilds/darwin-*/spawn-helper node_modules/.pnpm/node-pty*/node_modules/node-pty/prebuilds/darwin-*/*.node 2>/dev/null || true",
|
||||
"test": "vitest run --silent=passed-only --reporter=dot --exclude '**/build-output.test.ts'",
|
||||
"test": "vitest run --silent=passed-only --reporter=dot --exclude '**/build-output.test.ts'",
|
||||
"test:build": "vitest run --silent=passed-only --reporter=dot app/__tests__/build-output.test.ts",
|
||||
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.app.json"
|
||||
},
|
||||
@@ -64,7 +63,7 @@
|
||||
"ioredis": "^5.6.0",
|
||||
"lucide-react": "^1.7.0",
|
||||
"multer": "^2.1.1",
|
||||
"node-pty": "^1.1.0-beta22",
|
||||
"node-pty": "npm:@homebridge/node-pty-prebuilt-multiarch@^0.13.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
|
||||
@@ -44,8 +44,23 @@ function getNativePrebuildName(): string {
|
||||
function findInstalledNodePtyNativeDir(): string | null {
|
||||
try {
|
||||
const packageJsonPath = require.resolve("node-pty/package.json");
|
||||
const nativeDir = join(dirname(packageJsonPath), "prebuilds", getNativePrebuildName());
|
||||
return fs.existsSync(join(nativeDir, "pty.node")) ? nativeDir : null;
|
||||
const pkgRoot = dirname(packageJsonPath);
|
||||
|
||||
// @homebridge/node-pty-prebuilt-multiarch (aliased as node-pty) places the binary
|
||||
// in build/Release/pty.node after prebuild-install runs at install time.
|
||||
// Prefer this location as it is the fork's standard output path.
|
||||
const releaseDir = join(pkgRoot, "build", "Release");
|
||||
if (fs.existsSync(join(releaseDir, "pty.node"))) {
|
||||
return releaseDir;
|
||||
}
|
||||
|
||||
// Fallback: check the old prebuilds/<plat-arch>/ layout (upstream node-pty style).
|
||||
const prebuildDir = join(pkgRoot, "prebuilds", getNativePrebuildName());
|
||||
if (fs.existsSync(join(prebuildDir, "pty.node"))) {
|
||||
return prebuildDir;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
80
packages/dashboard/src/types/node-pty/index.d.ts
vendored
Normal file
80
packages/dashboard/src/types/node-pty/index.d.ts
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Type shim for the `node-pty` import specifier.
|
||||
*
|
||||
* The runtime package is @homebridge/node-pty-prebuilt-multiarch, aliased as
|
||||
* "node-pty" in package.json. Its bundled typings use `declare module
|
||||
* '@homebridge/node-pty-prebuilt-multiarch'` which TypeScript cannot resolve
|
||||
* via the npm alias alone. This shim re-declares the module under the `node-pty`
|
||||
* specifier so all source imports of `"node-pty"` resolve correctly.
|
||||
*
|
||||
* API surface matches node-pty 0.10.x / @homebridge/node-pty-prebuilt-multiarch 0.13.x.
|
||||
*/
|
||||
declare module "node-pty" {
|
||||
/**
|
||||
* An object that can be disposed via a dispose function.
|
||||
*/
|
||||
export interface IDisposable {
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* An event that can be listened to.
|
||||
* @returns an IDisposable to stop listening.
|
||||
*/
|
||||
export interface IEvent<T> {
|
||||
(listener: (e: T) => unknown): IDisposable;
|
||||
}
|
||||
|
||||
export interface IBasePtyForkOptions {
|
||||
name?: string;
|
||||
cols?: number;
|
||||
rows?: number;
|
||||
cwd?: string;
|
||||
env?: { [key: string]: string | undefined };
|
||||
encoding?: string | null;
|
||||
handleFlowControl?: boolean;
|
||||
flowControlPause?: string;
|
||||
flowControlResume?: string;
|
||||
}
|
||||
|
||||
export interface IPtyForkOptions extends IBasePtyForkOptions {
|
||||
uid?: number;
|
||||
gid?: number;
|
||||
}
|
||||
|
||||
export interface IWindowsPtyForkOptions extends IBasePtyForkOptions {
|
||||
useConpty?: boolean;
|
||||
useConptyDll?: boolean;
|
||||
conptyInheritCursor?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* An interface representing a pseudoterminal.
|
||||
*/
|
||||
export interface IPty {
|
||||
readonly pid: number;
|
||||
readonly cols: number;
|
||||
readonly rows: number;
|
||||
readonly process: string;
|
||||
handleFlowControl: boolean;
|
||||
readonly onData: IEvent<string>;
|
||||
readonly onExit: IEvent<{ exitCode: number; signal?: number }>;
|
||||
resize(columns: number, rows: number): void;
|
||||
on(event: "data", listener: (data: string) => void): void;
|
||||
on(event: "exit", listener: (exitCode: number, signal?: number) => void): void;
|
||||
clear(): void;
|
||||
write(data: string): void;
|
||||
kill(signal?: string): void;
|
||||
pause(): void;
|
||||
resume(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forks a process as a pseudoterminal.
|
||||
*/
|
||||
export function spawn(
|
||||
file: string,
|
||||
args: string[] | string,
|
||||
options: IPtyForkOptions | IWindowsPtyForkOptions,
|
||||
): IPty;
|
||||
}
|
||||
@@ -6,7 +6,10 @@
|
||||
"moduleResolution": "bundler",
|
||||
"module": "ESNext",
|
||||
"noEmit": true,
|
||||
"types": ["vitest/globals", "@testing-library/jest-dom", "node", "vite/client"]
|
||||
"types": ["vitest/globals", "@testing-library/jest-dom", "node", "vite/client"],
|
||||
"paths": {
|
||||
"node-pty": ["./src/types/node-pty/index.d.ts"]
|
||||
}
|
||||
},
|
||||
"include": ["app/**/*"],
|
||||
"exclude": ["app/**/*.test.ts", "app/**/*.test.tsx", "app/**/__tests__/**/*"]
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"jsx": "react-jsx",
|
||||
"types": ["node", "vitest/globals", "@testing-library/jest-dom"],
|
||||
"paths": {
|
||||
"@fusion/test-utils": ["../core/src/__test-utils__/workspace.ts"]
|
||||
"@fusion/test-utils": ["../core/src/__test-utils__/workspace.ts"],
|
||||
"node-pty": ["./src/types/node-pty/index.d.ts"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
|
||||
Reference in New Issue
Block a user