Archived
fix(install): allow node-pty scripts with npm 12
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
checkNodePtyNativeModule,
|
||||
formatNodePtyNativeModuleCheck,
|
||||
NODE_PTY_GLOBAL_REINSTALL_COMMAND,
|
||||
} from "./nodePtyNativeModule.js";
|
||||
|
||||
describe("node-pty native module diagnostics", () => {
|
||||
it("passes when node-pty loads", () => {
|
||||
const check = checkNodePtyNativeModule({ load: () => ({ spawn: () => undefined }) });
|
||||
|
||||
expect(check).toEqual({ status: "ok" });
|
||||
expect(formatNodePtyNativeModuleCheck(check)).toEqual({
|
||||
ok: true,
|
||||
lines: ["✓ node-pty native module loadable"],
|
||||
});
|
||||
});
|
||||
|
||||
it("reports the scoped global reinstall command when node-pty cannot load", () => {
|
||||
const check = checkNodePtyNativeModule({
|
||||
load: () => { throw new Error("Failed to load native module: pty.node\nchecked build/Release"); },
|
||||
});
|
||||
|
||||
expect(check).toEqual({
|
||||
status: "load-failed",
|
||||
message: "Failed to load native module: pty.node checked build/Release",
|
||||
});
|
||||
const formatted = formatNodePtyNativeModuleCheck(check);
|
||||
expect(formatted.ok).toBe(false);
|
||||
expect(formatted.lines).toContain(` ${NODE_PTY_GLOBAL_REINSTALL_COMMAND}`);
|
||||
expect(formatted.lines).toContain(" Then run `pi-web doctor` again.");
|
||||
expect(formatted.lines.join("\n")).not.toContain("dangerously-allow-all-scripts");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import { createRequire } from "node:module";
|
||||
|
||||
export const NODE_PTY_GLOBAL_REINSTALL_COMMAND = "npm install -g @jmfederico/pi-web --allow-scripts=node-pty";
|
||||
|
||||
const doctorLabel = "node-pty native module loadable";
|
||||
const requireFromHere = createRequire(import.meta.url);
|
||||
|
||||
type LoadNodePty = () => unknown;
|
||||
|
||||
export interface NodePtyNativeModuleCheckOptions {
|
||||
load?: LoadNodePty;
|
||||
}
|
||||
|
||||
export type NodePtyNativeModuleCheck =
|
||||
| { status: "ok" }
|
||||
| { status: "load-failed"; message: string };
|
||||
|
||||
export interface FormattedNodePtyNativeModuleCheck {
|
||||
ok: boolean;
|
||||
lines: string[];
|
||||
}
|
||||
|
||||
export function checkNodePtyNativeModule(options: NodePtyNativeModuleCheckOptions = {}): NodePtyNativeModuleCheck {
|
||||
try {
|
||||
(options.load ?? loadNodePty)();
|
||||
return { status: "ok" };
|
||||
} catch (error) {
|
||||
return { status: "load-failed", message: errorMessage(error) };
|
||||
}
|
||||
}
|
||||
|
||||
export function formatNodePtyNativeModuleCheck(check: NodePtyNativeModuleCheck): FormattedNodePtyNativeModuleCheck {
|
||||
if (check.status === "ok") return { ok: true, lines: [`✓ ${doctorLabel}`] };
|
||||
return {
|
||||
ok: false,
|
||||
lines: [
|
||||
`✗ ${doctorLabel}`,
|
||||
` Could not load node-pty: ${check.message}`,
|
||||
" npm may have skipped node-pty's required install script.",
|
||||
" For a global npm installation, reinstall PI WEB with:",
|
||||
` ${NODE_PTY_GLOBAL_REINSTALL_COMMAND}`,
|
||||
" Then run `pi-web doctor` again.",
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function loadNodePty(): unknown {
|
||||
return requireFromHere("node-pty");
|
||||
}
|
||||
|
||||
function errorMessage(error: unknown): string {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return message.replaceAll(/\s+/g, " ").trim();
|
||||
}
|
||||
@@ -63,6 +63,7 @@ describe("Docker command assets", () => {
|
||||
expect(dockerfile).toContain("COPY internal/bin/hostexec /usr/local/bin/hostexec");
|
||||
expect(dockerfile).toContain("COPY internal/image/install-opensuse-base /usr/local/sbin/install-pi-web-opensuse-base");
|
||||
expect(dockerfile).toContain("--include=peer");
|
||||
expect(dockerfile).toContain('"@jmfederico/pi-web@${PI_WEB_VERSION}" --allow-scripts=node-pty');
|
||||
expect(dockerfile).toContain('peer_pi_bin="${global_root}/@jmfederico/pi-web/node_modules/.bin/pi"');
|
||||
expect(dockerfile).not.toContain("@earendil-works/pi-coding-agent@");
|
||||
expect(devDockerfile).toContain("COPY docker/pi-web-docker /usr/local/bin/pi-web-docker");
|
||||
|
||||
@@ -215,6 +215,26 @@ describe("PI WEB status", () => {
|
||||
expect(updateCommand).toBe("PI_CODING_AGENT_DIR='/tmp/profile'\\''s/state' '/tmp/agent'\\''s/pi' update 'npm:@jmfederico/pi-web' && pi-web restart");
|
||||
});
|
||||
|
||||
it("scopes node-pty script approval in npm-global update commands", async () => {
|
||||
const updateCommand = await updateCommandFor(
|
||||
{ kind: "npm-global", path: "/opt/npm/@jmfederico/pi-web" },
|
||||
"pi-web restart",
|
||||
{ activeAgentProfile: undefined, hasCommand: () => Promise.resolve(true) },
|
||||
);
|
||||
|
||||
expect(updateCommand).toBe("npm install -g @jmfederico/pi-web --allow-scripts=node-pty && pi-web restart");
|
||||
});
|
||||
|
||||
it("suppresses npm-global update commands when npm is unavailable", async () => {
|
||||
const updateCommand = await updateCommandFor(
|
||||
{ kind: "npm-global", path: "/opt/npm/@jmfederico/pi-web" },
|
||||
"pi-web restart",
|
||||
{ activeAgentProfile: undefined, hasCommand: () => Promise.resolve(false) },
|
||||
);
|
||||
|
||||
expect(updateCommand).toBeUndefined();
|
||||
});
|
||||
|
||||
it.each([
|
||||
activeProfile("a", "acme-agent", "/opt/acme/state"),
|
||||
activeProfile("b", "pi", "relative/state"),
|
||||
|
||||
@@ -140,7 +140,10 @@ export async function getPiWebStatus(daemon: PiWebStatusDaemon = new SessionDaem
|
||||
const { web, sessiond } = versionStatus.components;
|
||||
const release = await getLatestReleaseStatus(web.installedVersion ?? web.runtimeVersion ?? DEFAULT_VERSION, options.forceReleaseCheck === true);
|
||||
const components = { web, sessiond };
|
||||
const commands = await commandsFor(components, { activeAgentProfile: options.activeAgentProfile, hasCommand: options.hasCommand ?? hasCommand });
|
||||
const commands = await commandsFor(components, {
|
||||
activeAgentProfile: options.activeAgentProfile,
|
||||
hasCommand: options.hasCommand ?? hasCommand,
|
||||
});
|
||||
const messages = buildMessages(components, release, commands);
|
||||
return {
|
||||
...versionStatus,
|
||||
@@ -416,7 +419,10 @@ async function fetchLatestNpmVersion(currentVersion: string): Promise<string> {
|
||||
return version;
|
||||
}
|
||||
|
||||
async function commandsFor(components: PiWebStatusResponse["components"], options: { activeAgentProfile: ActiveAgentProfileDescriptor | undefined; hasCommand: (command: string) => Promise<boolean> }): Promise<PiWebStatusResponse["commands"]> {
|
||||
async function commandsFor(components: PiWebStatusResponse["components"], options: {
|
||||
activeAgentProfile: ActiveAgentProfileDescriptor | undefined;
|
||||
hasCommand: (command: string) => Promise<boolean>;
|
||||
}): Promise<PiWebStatusResponse["commands"]> {
|
||||
const installation = preferredInstallation(components);
|
||||
if (installation?.kind === "docker") return dockerCommands(installation);
|
||||
|
||||
@@ -467,7 +473,10 @@ function restartCommandFor(installation: PiWebInstallationInfo | undefined, serv
|
||||
return cliCommands.restart ?? serviceCommands.restart;
|
||||
}
|
||||
|
||||
export async function updateCommandFor(installation: PiWebInstallationInfo | undefined, restartCommand: string | undefined, options: { activeAgentProfile: ActiveAgentProfileDescriptor | undefined; hasCommand: (command: string) => Promise<boolean> }): Promise<string | undefined> {
|
||||
export async function updateCommandFor(installation: PiWebInstallationInfo | undefined, restartCommand: string | undefined, options: {
|
||||
activeAgentProfile: ActiveAgentProfileDescriptor | undefined;
|
||||
hasCommand: (command: string) => Promise<boolean>;
|
||||
}): Promise<string | undefined> {
|
||||
if (restartCommand === undefined) return undefined;
|
||||
if (installation?.kind === "pi-package") {
|
||||
const profile = options.activeAgentProfile;
|
||||
@@ -479,8 +488,8 @@ export async function updateCommandFor(installation: PiWebInstallationInfo | und
|
||||
if (!(await hasCommand("npm")) || !(await isGitCheckoutWithUpstream(installation.path))) return undefined;
|
||||
return `cd ${shellQuote(installation.path)} && git pull --ff-only && npm install && npm run build && ${restartCommand}`;
|
||||
}
|
||||
if (installation?.kind !== "npm-global" || !(await hasCommand("npm"))) return undefined;
|
||||
return `npm install -g ${PI_WEB_PACKAGE_NAME} && ${restartCommand}`;
|
||||
if (installation?.kind !== "npm-global" || !(await options.hasCommand("npm"))) return undefined;
|
||||
return `npm install -g ${PI_WEB_PACKAGE_NAME} --allow-scripts=node-pty && ${restartCommand}`;
|
||||
}
|
||||
|
||||
async function nativeServiceCommands(): Promise<NativeServiceCommands> {
|
||||
|
||||
Reference in New Issue
Block a user