Archived
fix: make doctor platform-aware
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@jmfederico/pi-web": patch
|
||||
---
|
||||
|
||||
Improve `pi-web doctor` to report missing commands safely, skip Linux systemd checks on non-Linux platforms, and avoid misleading restart advice after the macOS node-pty permission workaround.
|
||||
+72
-10
@@ -21,6 +21,38 @@ interface InstallOptions {
|
||||
type Check = [string, string[]];
|
||||
type SupportedShell = "bash" | "zsh" | "fish";
|
||||
|
||||
function platformLabel(): string {
|
||||
if (process.platform === "darwin") return "macOS";
|
||||
if (process.platform === "linux") return "Linux";
|
||||
if (process.platform === "win32") return "Windows";
|
||||
return process.platform;
|
||||
}
|
||||
|
||||
function supportsSystemdUserServices(): boolean {
|
||||
return process.platform === "linux";
|
||||
}
|
||||
|
||||
function manualRunAdvice(): string {
|
||||
return [
|
||||
"Run PI WEB manually from a checkout:",
|
||||
" npm run start:sessiond",
|
||||
" PI_WEB_PORT=8504 npm start",
|
||||
"",
|
||||
"For development in one terminal:",
|
||||
" npm run dev",
|
||||
"",
|
||||
"For split development, keep sessiond separate and run web/API plus Vite UI separately:",
|
||||
" npm run dev:sessiond",
|
||||
" npm run dev:web",
|
||||
" npm run dev:client",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function assertSystemdUserServicesSupported(command: string): void {
|
||||
if (supportsSystemdUserServices()) return;
|
||||
throw new Error(`\`${command}\` requires Linux systemd user services and is not supported on ${platformLabel()}.\n\n${manualRunAdvice()}`);
|
||||
}
|
||||
|
||||
interface ServiceShell {
|
||||
name: SupportedShell;
|
||||
executable: string;
|
||||
@@ -45,9 +77,15 @@ function run(command: string, args: string[], options: { check?: boolean } = {})
|
||||
return status;
|
||||
}
|
||||
|
||||
function outputText(value: unknown): string {
|
||||
return typeof value === "string" ? value : "";
|
||||
}
|
||||
|
||||
function capture(command: string, args: string[]): { status: number; stdout: string; stderr: string } {
|
||||
const result = spawnSync(command, args, { encoding: "utf8" });
|
||||
return { status: result.status ?? 1, stdout: result.stdout, stderr: result.stderr };
|
||||
const errorMessage = result.error instanceof Error ? result.error.message : "";
|
||||
const stderr = outputText(result.stderr);
|
||||
return { status: result.status ?? 1, stdout: outputText(result.stdout), stderr: stderr === "" ? errorMessage : stderr };
|
||||
}
|
||||
|
||||
function hasCommand(command: string): boolean {
|
||||
@@ -252,6 +290,7 @@ async function writeInitialConfig(options: InstallOptions): Promise<string> {
|
||||
}
|
||||
|
||||
async function install(args: string[]): Promise<void> {
|
||||
assertSystemdUserServicesSupported("pi-web install");
|
||||
const options = parseInstallOptions(args);
|
||||
|
||||
const executables = resolveServiceExecutables();
|
||||
@@ -292,6 +331,7 @@ async function install(args: string[]): Promise<void> {
|
||||
}
|
||||
|
||||
async function uninstall(): Promise<void> {
|
||||
assertSystemdUserServicesSupported("pi-web uninstall");
|
||||
run("systemctl", ["--user", "disable", "--now", webServiceName]);
|
||||
run("systemctl", ["--user", "disable", "--now", sessiondServiceName]);
|
||||
await rm(join(serviceDir, webServiceName), { force: true });
|
||||
@@ -301,10 +341,12 @@ async function uninstall(): Promise<void> {
|
||||
}
|
||||
|
||||
function serviceAction(action: "start" | "stop" | "restart" | "status"): void {
|
||||
assertSystemdUserServicesSupported(`pi-web ${action}`);
|
||||
run("systemctl", ["--user", action, sessiondServiceName, webServiceName], { check: action !== "status" });
|
||||
}
|
||||
|
||||
function logs(): void {
|
||||
assertSystemdUserServicesSupported("pi-web logs");
|
||||
run("journalctl", ["--user", "-u", sessiondServiceName, "-u", webServiceName, "-f"]);
|
||||
}
|
||||
|
||||
@@ -348,6 +390,13 @@ function installPreflightChecks(executables: ServiceExecutables = resolveService
|
||||
|
||||
function doctorChecks(): Check[] {
|
||||
const shell = serviceShellLabel();
|
||||
if (!supportsSystemdUserServices()) {
|
||||
return [
|
||||
[`${shell} can find node >= 22`, serviceShellCommand(nodeVersionCheck())],
|
||||
[`${shell} can find npm`, serviceShellCommand(commandCheck("npm"))],
|
||||
[`${shell} can find pi`, serviceShellCommand(commandCheck("pi"))],
|
||||
];
|
||||
}
|
||||
const executables = resolveServiceExecutables();
|
||||
return [
|
||||
...installPreflightChecks(executables),
|
||||
@@ -393,26 +442,39 @@ function printPathSetupAdvice(): void {
|
||||
}
|
||||
|
||||
function doctor(): void {
|
||||
console.log(`Platform: ${platformLabel()}`);
|
||||
console.log(`Service shell: ${describeServiceShell()}`);
|
||||
if (!supportsSystemdUserServices()) {
|
||||
console.log(`- Linux systemd user service checks skipped on ${platformLabel()}`);
|
||||
}
|
||||
const ok = runChecks(doctorChecks());
|
||||
const nodePtySpawnHelperOk = printNodePtyDarwinSpawnHelperCheck();
|
||||
|
||||
const linger = isLingerEnabled();
|
||||
if (linger === true) {
|
||||
console.log("✓ systemd user lingering enabled");
|
||||
} else if (linger === false) {
|
||||
console.log("✗ systemd user lingering disabled");
|
||||
console.log(` Recommended on servers: sudo loginctl enable-linger ${userInfo().username}`);
|
||||
if (supportsSystemdUserServices()) {
|
||||
const linger = isLingerEnabled();
|
||||
if (linger === true) {
|
||||
console.log("✓ systemd user lingering enabled");
|
||||
} else if (linger === false) {
|
||||
console.log("✗ systemd user lingering disabled");
|
||||
console.log(` Recommended on servers: sudo loginctl enable-linger ${userInfo().username}`);
|
||||
} else {
|
||||
console.log("? systemd user lingering unknown");
|
||||
console.log(` Recommended on servers: sudo loginctl enable-linger ${userInfo().username}`);
|
||||
}
|
||||
} else {
|
||||
console.log("? systemd user lingering unknown");
|
||||
console.log(` Recommended on servers: sudo loginctl enable-linger ${userInfo().username}`);
|
||||
console.log(`- systemd user lingering skipped on ${platformLabel()}`);
|
||||
}
|
||||
|
||||
if (!ok) {
|
||||
console.log("\nIf a command works in your terminal but fails here, make sure your service shell login files set PATH the same way.");
|
||||
console.log("If a bundled entrypoint is not accessible, reinstall or update the PI WEB package.");
|
||||
if (supportsSystemdUserServices()) console.log("If a bundled entrypoint is not accessible, reinstall or update the PI WEB package.");
|
||||
printPathSetupAdvice();
|
||||
}
|
||||
|
||||
if (ok && !supportsSystemdUserServices()) {
|
||||
console.log(`\n${manualRunAdvice()}`);
|
||||
}
|
||||
|
||||
if (!ok || !nodePtySpawnHelperOk) process.exitCode = 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mkdtemp, rm, truncate, writeFile } from "node:fs/promises";
|
||||
import { mkdtemp, realpath, rm, truncate, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
@@ -15,7 +15,7 @@ let tempDir: string;
|
||||
let projectDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = await mkdtemp(join(tmpdir(), "pi-web-app-test-"));
|
||||
tempDir = await realpath(await mkdtemp(join(tmpdir(), "pi-web-app-test-")));
|
||||
projectDir = join(tempDir, "project");
|
||||
app = await buildApp({
|
||||
projects: new ProjectService(new ProjectStore(join(tempDir, "projects.json"))),
|
||||
|
||||
@@ -42,6 +42,8 @@ describe("node-pty macOS spawn-helper diagnostics", () => {
|
||||
expect(formatted.ok).toBe(false);
|
||||
expect(formatted.lines).toContain(` PI WEB tracking issue: ${PI_WEB_SPAWN_HELPER_ISSUE_URL}`);
|
||||
expect(formatted.lines).toContain(` chmod +x '${fixture.helperPath}'`);
|
||||
expect(formatted.lines).toContain(" Then run `pi-web doctor` again and retry opening a terminal.");
|
||||
expect(formatted.lines.join("\n")).not.toContain("restart");
|
||||
});
|
||||
|
||||
it("passes when the selected helper is executable", async () => {
|
||||
|
||||
@@ -98,7 +98,7 @@ export function formatNodePtyDarwinSpawnHelperCheck(check: NodePtyDarwinSpawnHel
|
||||
` PI WEB tracking issue: ${PI_WEB_SPAWN_HELPER_ISSUE_URL}`,
|
||||
" Proposed workaround:",
|
||||
` ${check.fixCommand}`,
|
||||
" Then restart the pi-web-sessiond/pi-web-server processes.",
|
||||
" Then run `pi-web doctor` again and retry opening a terminal.",
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user