diff --git a/.changeset/allow-node-pty-install-scripts.md b/.changeset/allow-node-pty-install-scripts.md new file mode 100644 index 0000000..ce66ee0 --- /dev/null +++ b/.changeset/allow-node-pty-install-scripts.md @@ -0,0 +1,5 @@ +--- +"@jmfederico/pi-web": patch +--- + +Allow npm 12 global installs and updates to run node-pty's required native-module installation scripts, and diagnose blocked native modules before installing services. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 57788c6..283cc38 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,5 +38,9 @@ jobs: - name: Build run: npm run build + - name: Smoke test npm 12 global package install + if: runner.os == 'Linux' + run: npm run smoke:package-install + - name: Check npm package contents run: npm run pack:dry diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 96c2959..430ebeb 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -34,5 +34,8 @@ jobs: - name: Build run: npm run build + - name: Smoke test npm 12 global package install + run: npm run smoke:package-install + - name: Publish run: npm publish --access public --provenance diff --git a/README.md b/README.md index d33c958..ddfdff1 100644 --- a/README.md +++ b/README.md @@ -42,11 +42,13 @@ Requirements: Install and start PI WEB as per-user services: ```bash -npm install -g @jmfederico/pi-web +npm install -g @jmfederico/pi-web --allow-scripts=node-pty pi-web install pi-web doctor ``` +On npm 12, the scoped flag lets `node-pty` prepare its required native module without enabling install scripts for other dependencies. + Then open: ```text diff --git a/docker/Dockerfile b/docker/Dockerfile index f14fa02..15fcacd 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -30,7 +30,7 @@ ARG CACHE_BUST=local RUN set -eux; \ echo "PI WEB Docker build cache bust: ${CACHE_BUST}"; \ - npm install -g --omit=dev --include=peer --no-audit --no-fund "@jmfederico/pi-web@${PI_WEB_VERSION}"; \ + npm install -g --omit=dev --include=peer --no-audit --no-fund "@jmfederico/pi-web@${PI_WEB_VERSION}" --allow-scripts=node-pty; \ global_root="$(npm root -g)"; \ global_prefix="$(npm prefix -g)"; \ peer_pi_bin="${global_root}/@jmfederico/pi-web/node_modules/.bin/pi"; \ diff --git a/docs/faq.html b/docs/faq.html index 4030bf0..1feca7c 100644 --- a/docs/faq.html +++ b/docs/faq.html @@ -94,6 +94,7 @@ Tools are failing / node not found What does doctor check? nvm, fnm, or asdf issues + node-pty native module is missing User service manager is unavailable I cannot open the web UI Can I expose this publicly? @@ -112,7 +113,7 @@ a supported per-user service manager when one is available.

@@ -166,8 +167,9 @@

Missing plan requirements fail doctor and include login-file guidance. Manager, timeout, malformed-output, and cleanup failures are reported as probe infrastructure problems rather than being mislabeled as PATH - drift. On unsupported/manual-only platforms, native-service drift checks are skipped. Doctor also prints - installed and running PI WEB versions and reports systemd lingering when relevant. + drift. On unsupported/manual-only platforms, native-service drift checks are skipped. Doctor also checks + that the node-pty native module can load, prints installed and running PI WEB versions, and reports + systemd lingering when relevant.

@@ -186,6 +188,27 @@ +
+

pi-web-sessiond cannot load pty.node

+

+ npm 12 blocks dependency installation scripts unless they are explicitly approved. If PI WEB was installed + without approving node-pty, its required native module can be missing even though npm reported a + successful installation. +

+

Reinstall PI WEB with approval limited to node-pty:

+
+
+ Allow the node-pty installation scripts + +
+
$ npm install -g @jmfederico/pi-web --allow-scripts=node-pty
+
+

+ Then run pi-web install again, or restart pi-web-sessiond if you run PI WEB manually. + The flag does not enable installation scripts for other dependencies. +

+
+

User service manager is unavailable

diff --git a/docs/index.html b/docs/index.html index d18ba83..b79e41e 100644 --- a/docs/index.html +++ b/docs/index.html @@ -337,7 +337,7 @@ User service install -

$ npm install -g @jmfederico/pi-web
+            
$ npm install -g @jmfederico/pi-web --allow-scripts=node-pty
 $ pi-web install
 $ pi-web doctor
 $ pi-web version
diff --git a/docs/install.html b/docs/install.html
index 8b3b4e2..c4985fc 100644
--- a/docs/install.html
+++ b/docs/install.html
@@ -133,10 +133,14 @@
                   User service install
                   
                 
-                
$ npm install -g @jmfederico/pi-web
+                
$ npm install -g @jmfederico/pi-web --allow-scripts=node-pty
 $ pi-web install
 $ pi-web doctor
+

+ The scoped --allow-scripts=node-pty flag lets npm 12 run the native-module installation required + by PI WEB terminals without enabling install scripts for other dependencies. +

Then open http://127.0.0.1:8504.

If preflight fails, no config or existing services are changed. Follow the detected shell guidance: zsh services read ~/.zprofile, not interactive-only ~/.zshrc; bash uses ~/.bash_profile or ~/.profile.

On Linux servers, also consider sudo loginctl enable-linger "$USER" so user services survive logout/reboot.

@@ -185,7 +189,7 @@ Manual processes -
$ npm install -g @jmfederico/pi-web
+                
$ npm install -g @jmfederico/pi-web --allow-scripts=node-pty
 
 # Terminal 1
 $ pi-web-sessiond
diff --git a/docs/machines.html b/docs/machines.html
index cf10fa5..2347b1d 100644
--- a/docs/machines.html
+++ b/docs/machines.html
@@ -167,7 +167,7 @@ PI WEB gateway you opened
                   Install on each target
                   
                 
-                
$ npm install -g @jmfederico/pi-web
+                
$ npm install -g @jmfederico/pi-web --allow-scripts=node-pty
 $ pi-web install
 $ pi-web doctor
diff --git a/install.sh b/install.sh index 6838572..f0a1622 100644 --- a/install.sh +++ b/install.sh @@ -1,5 +1,5 @@ #!/usr/bin/env sh set -eu -npm install -g @jmfederico/pi-web +npm install -g @jmfederico/pi-web --allow-scripts=node-pty pi-web install diff --git a/package.json b/package.json index 50713c9..27b7500 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"", "prepack": "npm run build", "pack:dry": "npm pack --dry-run", + "smoke:package-install": "node scripts/smoke-package-install.mjs", "prepublishOnly": "npm run verify", "publish:npm": "npm publish --access public", "prepare": "node scripts/install-git-hooks.mjs", diff --git a/scripts/smoke-package-install.mjs b/scripts/smoke-package-install.mjs new file mode 100644 index 0000000..2574347 --- /dev/null +++ b/scripts/smoke-package-install.mjs @@ -0,0 +1,132 @@ +import { execFile } from "node:child_process"; +import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { createRequire } from "node:module"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { promisify } from "node:util"; + +const NPM_VERSION = "12.0.1"; +const MARKER = "pi-web-package-pty-ok"; +const execFileAsync = promisify(execFile); +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); + +if (process.platform === "win32") { + throw new Error("The installed-package PTY smoke test requires a POSIX shell"); +} + +const npmExecPath = process.env["npm_execpath"]; +if (npmExecPath === undefined || npmExecPath === "") { + throw new Error("npm_execpath is required; run this check through `npm run smoke:package-install`"); +} + +const root = await mkdtemp(join(tmpdir(), "pi-web-package-install-")); +try { + const packDir = join(root, "pack"); + const npmToolDir = join(root, "npm-tool"); + const globalPrefix = join(root, "global"); + await Promise.all([ + mkdir(packDir, { recursive: true }), + mkdir(join(globalPrefix, "lib"), { recursive: true }), + mkdir(npmToolDir, { recursive: true }), + writeFile(join(npmToolDir, "package.json"), '{"private":true}\n'), + ]); + + const packOutput = await runNpm(npmExecPath, ["pack", "--ignore-scripts", "--json", "--pack-destination", packDir], repoRoot); + const tarballPath = join(packDir, packageTarballFilename(packOutput)); + + await runNpm(npmExecPath, [ + "install", + "--ignore-scripts", + "--no-audit", + "--no-fund", + "--no-package-lock", + "--no-save", + `npm@${NPM_VERSION}`, + ], npmToolDir); + const npm12ExecPath = join(npmToolDir, "node_modules", "npm", "bin", "npm-cli.js"); + + await runNpm(npm12ExecPath, [ + "install", + "--global", + tarballPath, + "--prefix", + globalPrefix, + "--allow-scripts=node-pty", + "--no-audit", + "--no-fund", + ], root); + + const packageRoot = join(globalPrefix, "lib", "node_modules", "@jmfederico", "pi-web"); + await smokeInstalledTerminalService(packageRoot); + console.log(`Installed-package PTY smoke test passed with npm ${NPM_VERSION}.`); +} finally { + await rm(root, { recursive: true, force: true }); +} + +async function runNpm(npmCliPath, args, cwd) { + const result = await execFileAsync(process.execPath, [npmCliPath, ...args], { + cwd, + encoding: "utf8", + maxBuffer: 10 * 1024 * 1024, + timeout: 180_000, + }); + return result.stdout; +} + +function packageTarballFilename(output) { + const parsed = JSON.parse(output); + if (!Array.isArray(parsed) || parsed.length !== 1 || typeof parsed[0]?.filename !== "string") { + throw new Error("npm pack returned an unexpected result"); + } + return parsed[0].filename; +} + +async function smokeInstalledTerminalService(packageRoot) { + const requireFromPackage = createRequire(join(packageRoot, "package.json")); + const nodePtyPackageJsonPath = requireFromPackage.resolve("node-pty/package.json"); + const nodePtyPackage = JSON.parse(await readFile(nodePtyPackageJsonPath, "utf8")); + if (typeof nodePtyPackage.version !== "string" || nodePtyPackage.version.includes("-")) { + throw new Error(`Installed package resolved a non-stable node-pty version: ${String(nodePtyPackage.version)}`); + } + + const terminalModuleUrl = pathToFileURL(join(packageRoot, "dist", "server", "terminals", "terminalService.js")).href; + const { TerminalService } = await import(terminalModuleUrl); + const previousShell = process.env["SHELL"]; + process.env["SHELL"] = "/bin/sh"; + const service = new TerminalService(); + try { + const run = service.runCommand({ + origin: "package-smoke", + projectId: "package-smoke", + workspaceId: "package-smoke", + cwd: packageRoot, + title: "Installed package PTY smoke test", + command: `printf '%s' '${MARKER}'`, + }); + let output = ""; + let detach = () => undefined; + const exitCode = await new Promise((resolvePromise, reject) => { + const timeout = setTimeout(() => reject(new Error(`Timed out waiting for installed node-pty output: ${JSON.stringify(output)}`)), 10_000); + try { + detach = service.attach(run.terminalId, { + output: (data) => { output += data; }, + exit: (code) => { + clearTimeout(timeout); + resolvePromise(code); + }, + }); + } catch (error) { + clearTimeout(timeout); + reject(error); + } + }); + detach(); + if (exitCode !== 0) throw new Error(`Installed PTY command exited with ${String(exitCode)}`); + if (!output.includes(MARKER)) throw new Error(`Installed PTY output did not contain ${MARKER}: ${JSON.stringify(output)}`); + } finally { + service.dispose(); + if (previousShell === undefined) delete process.env["SHELL"]; + else process.env["SHELL"] = previousShell; + } +} diff --git a/src/cli.ts b/src/cli.ts index d4ed1e7..16cf6af 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -8,6 +8,7 @@ import { fileURLToPath } from "node:url"; import { defaultPiWebConfigPath, defaultPiWebDataDir, effectivePiWebConfig, examplePiWebConfig } from "./config.js"; import { packageVersion, printPiWebVersionReport } from "./piWebVersionReport.js"; import { checkNodePtyDarwinSpawnHelper, formatNodePtyDarwinSpawnHelperCheck } from "./server/diagnostics/nodePtySpawnHelper.js"; +import { checkNodePtyNativeModule, formatNodePtyNativeModuleCheck } from "./server/diagnostics/nodePtyNativeModule.js"; import { installNativeServiceCandidate, nativeServiceInstallFailureNeedsPathAdvice, @@ -664,6 +665,9 @@ async function install(args: string[]): Promise { console.log(`Running PI WEB ${options.mode} install preflight checks...`); console.log(`Service backend: ${backend.label}`); console.log(`Service shell: ${describeServiceShell()}`); + if (!printNodePtyNativeModuleCheck()) { + throw new Error("Install preflight checks failed without changing config or services. Fix the failure above, then run `pi-web doctor` for more detail."); + } const result = await installNativeServiceCandidate(candidate, { probe: createNativeServiceAuthoritativeProbe(), fileExists: regularFileExists, @@ -967,9 +971,9 @@ function printPathSetupAdvice(shell: NativeServiceShell = detectServiceShell()): export function doctorExitCode( generalReadinessOk: boolean, nativeServicePlanOk: boolean, - nodePtySpawnHelperOk: boolean, + nodePtyRuntimeOk: boolean, ): 0 | 1 { - return generalReadinessOk && nativeServicePlanOk && nodePtySpawnHelperOk ? 0 : 1; + return generalReadinessOk && nativeServicePlanOk && nodePtyRuntimeOk ? 0 : 1; } async function doctor(): Promise { @@ -986,7 +990,10 @@ async function doctor(): Promise { console.log("\nGeneral login-shell readiness (separate from native-service requirements):"); const generalReadinessOk = runChecks(generalDoctorChecks()); printOptionalDoctorChecks(); - const nodePtySpawnHelperOk = printNodePtyDarwinSpawnHelperCheck(); + + console.log("\nNative terminal runtime readiness:"); + const nodePtyNativeModuleOk = printNodePtyNativeModuleCheck(); + const nodePtySpawnHelperOk = nodePtyNativeModuleOk ? printNodePtyDarwinSpawnHelperCheck() : true; let nativeServiceReport: NativeServiceDoctorReport | null = null; if (backend !== undefined) { @@ -1025,7 +1032,13 @@ async function doctor(): Promise { console.log(`\n${manualRunAdvice()}`); } - if (doctorExitCode(generalReadinessOk, nativeServicePlanOk, nodePtySpawnHelperOk) !== 0) process.exitCode = 1; + if (doctorExitCode(generalReadinessOk, nativeServicePlanOk, nodePtyNativeModuleOk && nodePtySpawnHelperOk) !== 0) process.exitCode = 1; +} + +function printNodePtyNativeModuleCheck(): boolean { + const result = formatNodePtyNativeModuleCheck(checkNodePtyNativeModule()); + for (const line of result.lines) console.log(line); + return result.ok; } function printNodePtyDarwinSpawnHelperCheck(): boolean { @@ -1049,7 +1062,7 @@ Usage: pi-web version Recommended install: - npm install -g @jmfederico/pi-web + npm install -g @jmfederico/pi-web --allow-scripts=node-pty pi-web install Development service install from a checkout: diff --git a/src/installScript.test.ts b/src/installScript.test.ts new file mode 100644 index 0000000..5b1c8ad --- /dev/null +++ b/src/installScript.test.ts @@ -0,0 +1,71 @@ +import { execFile } from "node:child_process"; +import { chmod, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { afterEach, describe, expect, it } from "vitest"; + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const installerPath = join(repoRoot, "install.sh"); +const tempRoots: string[] = []; + +afterEach(async () => { + await Promise.all(tempRoots.map((root) => rm(root, { recursive: true, force: true }))); + tempRoots.length = 0; +}); + +describe.skipIf(process.platform === "win32")("global install script", () => { + it("scopes script approval to node-pty before installing services", async () => { + const fixture = await createFixture(); + + await execUtf8("sh", [installerPath], fixture.env); + + expect((await readFile(fixture.npmArgsPath, "utf8")).trim().split("\n")).toEqual([ + "install", + "-g", + "@jmfederico/pi-web", + "--allow-scripts=node-pty", + ]); + expect((await readFile(fixture.piWebArgsPath, "utf8")).trim().split("\n")).toEqual(["install"]); + }); +}); + +async function createFixture(): Promise<{ + env: NodeJS.ProcessEnv; + npmArgsPath: string; + piWebArgsPath: string; +}> { + const root = await mkdtemp(join(tmpdir(), "pi-web-install-script-")); + tempRoots.push(root); + const npmArgsPath = join(root, "npm-args"); + const piWebArgsPath = join(root, "pi-web-args"); + const npmPath = join(root, "npm"); + const piWebPath = join(root, "pi-web"); + await Promise.all([ + writeFile(npmPath, "#!/usr/bin/env sh\nprintf '%s\\n' \"$@\" > \"$FAKE_NPM_ARGS\"\n"), + writeFile(piWebPath, "#!/usr/bin/env sh\nprintf '%s\\n' \"$@\" > \"$FAKE_PI_WEB_ARGS\"\n"), + ]); + await Promise.all([chmod(npmPath, 0o755), chmod(piWebPath, 0o755)]); + return { + env: { + ...process.env, + PATH: `${root}:${process.env["PATH"] ?? ""}`, + FAKE_NPM_ARGS: npmArgsPath, + FAKE_PI_WEB_ARGS: piWebArgsPath, + }, + npmArgsPath, + piWebArgsPath, + }; +} + +function execUtf8(file: string, args: string[], env: NodeJS.ProcessEnv): Promise { + return new Promise((resolvePromise, reject) => { + execFile(file, args, { env, encoding: "utf8" }, (error, stdout) => { + if (error !== null) { + reject(error instanceof Error ? error : new Error("Command failed")); + return; + } + resolvePromise(stdout); + }); + }); +} diff --git a/src/server/diagnostics/nodePtyNativeModule.test.ts b/src/server/diagnostics/nodePtyNativeModule.test.ts new file mode 100644 index 0000000..edb2e5e --- /dev/null +++ b/src/server/diagnostics/nodePtyNativeModule.test.ts @@ -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"); + }); +}); diff --git a/src/server/diagnostics/nodePtyNativeModule.ts b/src/server/diagnostics/nodePtyNativeModule.ts new file mode 100644 index 0000000..c352a51 --- /dev/null +++ b/src/server/diagnostics/nodePtyNativeModule.ts @@ -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(); +} diff --git a/src/server/dockerControlAssets.test.ts b/src/server/dockerControlAssets.test.ts index 07a08c6..d39b570 100644 --- a/src/server/dockerControlAssets.test.ts +++ b/src/server/dockerControlAssets.test.ts @@ -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"); diff --git a/src/server/piWebStatus.test.ts b/src/server/piWebStatus.test.ts index 0bbd969..a037698 100644 --- a/src/server/piWebStatus.test.ts +++ b/src/server/piWebStatus.test.ts @@ -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"), diff --git a/src/server/piWebStatus.ts b/src/server/piWebStatus.ts index 8f75d3c..e9ee6fd 100644 --- a/src/server/piWebStatus.ts +++ b/src/server/piWebStatus.ts @@ -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 { return version; } -async function commandsFor(components: PiWebStatusResponse["components"], options: { activeAgentProfile: ActiveAgentProfileDescriptor | undefined; hasCommand: (command: string) => Promise }): Promise { +async function commandsFor(components: PiWebStatusResponse["components"], options: { + activeAgentProfile: ActiveAgentProfileDescriptor | undefined; + hasCommand: (command: string) => Promise; +}): Promise { 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 }): Promise { +export async function updateCommandFor(installation: PiWebInstallationInfo | undefined, restartCommand: string | undefined, options: { + activeAgentProfile: ActiveAgentProfileDescriptor | undefined; + hasCommand: (command: string) => Promise; +}): Promise { 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 {