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:
gsxdsm
2026-04-26 15:00:10 -07:00
parent fa60ada0fc
commit 328b236462
10 changed files with 383 additions and 53 deletions

View File

@@ -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;
}

View 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;
}