Archived
fix: improve PI WEB updates panel commands
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@jmfederico/pi-web": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Avoid suggesting unavailable `pi-web` restart commands for local checkout installs, and show native service commands only when PI WEB can detect matching service files.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@jmfederico/pi-web": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Rename the PI WEB status workspace tab to Updates so version and restart guidance is easier to find.
|
||||||
@@ -69,12 +69,30 @@ function renderCommand(html: HtmlTemplateTag, label: string, command: string): T
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderCommands(html: HtmlTemplateTag, status: PiWebStatusResponse): TemplateResult | undefined {
|
||||||
|
const commands = [
|
||||||
|
["Update", status.commands.update],
|
||||||
|
["Restart all", status.commands.restart],
|
||||||
|
["Restart Web/UI", status.commands.restartWeb],
|
||||||
|
["Restart session daemon", status.commands.restartSessiond],
|
||||||
|
["Status", status.commands.status],
|
||||||
|
].filter((entry): entry is [string, string] => typeof entry[1] === "string" && entry[1] !== "");
|
||||||
|
|
||||||
|
if (commands.length === 0) return undefined;
|
||||||
|
return html`
|
||||||
|
<section>
|
||||||
|
<strong>Suggested commands</strong>
|
||||||
|
${commands.map(([label, command]) => renderCommand(html, label, command))}
|
||||||
|
</section>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
function renderStatusPanel(html: HtmlTemplateTag, state: AppState): TemplateResult {
|
function renderStatusPanel(html: HtmlTemplateTag, state: AppState): TemplateResult {
|
||||||
const status = statusFor(state);
|
const status = statusFor(state);
|
||||||
if (status === undefined) {
|
if (status === undefined) {
|
||||||
return html`
|
return html`
|
||||||
<section class="toolbar"><strong>PI WEB</strong></section>
|
<section class="toolbar"><strong>Updates</strong></section>
|
||||||
<section class="viewer"><p class="muted">Checking PI WEB status…</p></section>
|
<section class="viewer"><p class="muted">Checking PI WEB update status…</p></section>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +116,7 @@ function renderStatusPanel(html: HtmlTemplateTag, state: AppState): TemplateResu
|
|||||||
.pi-web-command > span { grid-column: 1 / -1; }
|
.pi-web-command > span { grid-column: 1 / -1; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<section class="toolbar"><strong>PI WEB</strong><span class="stale">beta</span>${messages.length > 0 ? html`<span class="stale">${String(messages.length)}</span>` : null}</section>
|
<section class="toolbar"><strong>Updates</strong><span class="stale">beta</span>${messages.length > 0 ? html`<span class="stale">${String(messages.length)}</span>` : null}</section>
|
||||||
<section class="viewer pi-web-status">
|
<section class="viewer pi-web-status">
|
||||||
<section>
|
<section>
|
||||||
${messages.length === 0 ? html`<p class="muted">No PI WEB update or restart messages.</p>` : messages.map((message) => html`
|
${messages.length === 0 ? html`<p class="muted">No PI WEB update or restart messages.</p>` : messages.map((message) => html`
|
||||||
@@ -116,13 +134,7 @@ function renderStatusPanel(html: HtmlTemplateTag, state: AppState): TemplateResu
|
|||||||
${renderComponent(html, status.components.sessiond)}
|
${renderComponent(html, status.components.sessiond)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
${renderCommands(html, status)}
|
||||||
<strong>Commands</strong>
|
|
||||||
${renderCommand(html, "Update", status.commands.update)}
|
|
||||||
${renderCommand(html, "Restart", status.commands.restart)}
|
|
||||||
${renderCommand(html, "systemd", status.commands.restartSystemd)}
|
|
||||||
${renderCommand(html, "dev", status.commands.restartDev)}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="pi-web-meta">
|
<section class="pi-web-meta">
|
||||||
<span>Generated ${status.generatedAt}</span>
|
<span>Generated ${status.generatedAt}</span>
|
||||||
@@ -136,13 +148,13 @@ function renderStatusPanel(html: HtmlTemplateTag, state: AppState): TemplateResu
|
|||||||
|
|
||||||
const plugin: PiWebPlugin = {
|
const plugin: PiWebPlugin = {
|
||||||
apiVersion: 1,
|
apiVersion: 1,
|
||||||
name: "PI WEB Status",
|
name: "PI WEB Updates",
|
||||||
activate: ({ html }) => ({
|
activate: ({ html }) => ({
|
||||||
contributions: {
|
contributions: {
|
||||||
workspacePanels: [
|
workspacePanels: [
|
||||||
{
|
{
|
||||||
id: "workspace.status",
|
id: "workspace.status",
|
||||||
title: "PI WEB",
|
title: "Updates",
|
||||||
order: 100,
|
order: 100,
|
||||||
visible: (context) => shouldShowStatusPanel(context.state),
|
visible: (context) => shouldShowStatusPanel(context.state),
|
||||||
badge: (context) => {
|
badge: (context) => {
|
||||||
|
|||||||
@@ -418,7 +418,13 @@ function parsePiWebReleaseStatus(value: unknown): PiWebReleaseStatus {
|
|||||||
|
|
||||||
function parsePiWebCommands(value: unknown): PiWebStatusResponse["commands"] {
|
function parsePiWebCommands(value: unknown): PiWebStatusResponse["commands"] {
|
||||||
const record = requireRecord(value);
|
const record = requireRecord(value);
|
||||||
return { update: requireString(record, "update"), restart: requireString(record, "restart"), restartSystemd: requireString(record, "restartSystemd"), restartDev: requireString(record, "restartDev") };
|
return {
|
||||||
|
...optionalField("update", optionalString(record, "update")),
|
||||||
|
...optionalField("restart", optionalString(record, "restart")),
|
||||||
|
...optionalField("restartWeb", optionalString(record, "restartWeb")),
|
||||||
|
...optionalField("restartSessiond", optionalString(record, "restartSessiond")),
|
||||||
|
...optionalField("status", optionalString(record, "status")),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function parsePiWebStatusMessage(value: unknown): PiWebStatusMessage {
|
function parsePiWebStatusMessage(value: unknown): PiWebStatusMessage {
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
|
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
import { comparePackageVersions, getPiWebStatus, getPiWebVersionStatus } from "./piWebStatus.js";
|
import { comparePackageVersions, getPiWebStatus, getPiWebVersionStatus } from "./piWebStatus.js";
|
||||||
import { SessionDaemonClient } from "../sessiond/sessionDaemonClient.js";
|
import { SessionDaemonClient } from "../sessiond/sessionDaemonClient.js";
|
||||||
|
import type { PiWebComponentStatus } from "../shared/apiTypes.js";
|
||||||
|
|
||||||
const originalSkipVersionCheck = process.env["PI_WEB_SKIP_VERSION_CHECK"];
|
const originalSkipVersionCheck = process.env["PI_WEB_SKIP_VERSION_CHECK"];
|
||||||
|
const originalHome = process.env["HOME"];
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
if (originalSkipVersionCheck === undefined) delete process.env["PI_WEB_SKIP_VERSION_CHECK"];
|
restoreEnv("PI_WEB_SKIP_VERSION_CHECK", originalSkipVersionCheck);
|
||||||
else process.env["PI_WEB_SKIP_VERSION_CHECK"] = originalSkipVersionCheck;
|
restoreEnv("HOME", originalHome);
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -18,20 +23,13 @@ describe("PI WEB status", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("returns installed and running version components without release metadata", async () => {
|
it("returns installed and running version components without release metadata", async () => {
|
||||||
const daemon = new SessionDaemonClient();
|
const daemon = daemonWithComponent({
|
||||||
vi.spyOn(daemon, "request").mockResolvedValue({
|
component: "sessiond",
|
||||||
statusCode: 200,
|
label: "Session daemon",
|
||||||
headers: { "content-type": "application/json" },
|
runtimeVersion: "1.202605.7",
|
||||||
body: JSON.stringify({
|
installedVersion: "1.202605.8",
|
||||||
version: {
|
stale: true,
|
||||||
component: "sessiond",
|
available: true,
|
||||||
label: "Session daemon",
|
|
||||||
runtimeVersion: "1.202605.7",
|
|
||||||
installedVersion: "1.202605.8",
|
|
||||||
stale: true,
|
|
||||||
available: true,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const status = await getPiWebVersionStatus(daemon);
|
const status = await getPiWebVersionStatus(daemon);
|
||||||
@@ -44,21 +42,14 @@ describe("PI WEB status", () => {
|
|||||||
|
|
||||||
it("reports stale session daemon versions as messages", async () => {
|
it("reports stale session daemon versions as messages", async () => {
|
||||||
process.env["PI_WEB_SKIP_VERSION_CHECK"] = "1";
|
process.env["PI_WEB_SKIP_VERSION_CHECK"] = "1";
|
||||||
const daemon = new SessionDaemonClient();
|
const daemon = daemonWithComponent({
|
||||||
vi.spyOn(daemon, "request").mockResolvedValue({
|
component: "sessiond",
|
||||||
statusCode: 200,
|
label: "Session daemon",
|
||||||
headers: { "content-type": "application/json" },
|
runtimeVersion: "1.202605.7",
|
||||||
body: JSON.stringify({
|
installedVersion: "1.202605.8",
|
||||||
version: {
|
stale: true,
|
||||||
component: "sessiond",
|
available: true,
|
||||||
label: "Session daemon",
|
installation: { kind: "pi-package", source: "npm:@jmfederico/pi-web", scope: "user", path: "/tmp/pi-web" },
|
||||||
runtimeVersion: "1.202605.7",
|
|
||||||
installedVersion: "1.202605.8",
|
|
||||||
stale: true,
|
|
||||||
available: true,
|
|
||||||
installation: { kind: "pi-package", source: "npm:@jmfederico/pi-web", scope: "user", path: "/tmp/pi-web" },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const status = await getPiWebStatus(daemon);
|
const status = await getPiWebStatus(daemon);
|
||||||
@@ -66,7 +57,81 @@ describe("PI WEB status", () => {
|
|||||||
expect(status.release.skipped).toBe(true);
|
expect(status.release.skipped).toBe(true);
|
||||||
expect(status.components.sessiond.stale).toBe(true);
|
expect(status.components.sessiond.stale).toBe(true);
|
||||||
expect(status.components.sessiond.installation).toMatchObject({ kind: "pi-package", source: "npm:@jmfederico/pi-web", scope: "user" });
|
expect(status.components.sessiond.installation).toMatchObject({ kind: "pi-package", source: "npm:@jmfederico/pi-web", scope: "user" });
|
||||||
expect(status.commands.update).not.toBe("");
|
|
||||||
expect(status.messages.map((message) => message.id)).toContain("sessiond-stale");
|
expect(status.messages.map((message) => message.id)).toContain("sessiond-stale");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("suggests native systemd commands for local development services", async () => {
|
||||||
|
if (process.platform !== "linux") return;
|
||||||
|
process.env["PI_WEB_SKIP_VERSION_CHECK"] = "1";
|
||||||
|
const home = await tempHome();
|
||||||
|
try {
|
||||||
|
process.env["HOME"] = home;
|
||||||
|
await installSystemdServiceFiles(home, ["pi-web-sessiond.service", "pi-web-ui-dev.service"]);
|
||||||
|
const daemon = daemonWithComponent(staleLocalSessiond());
|
||||||
|
|
||||||
|
const status = await getPiWebStatus(daemon);
|
||||||
|
|
||||||
|
expect(status.commands.restart).toBe("systemctl --user restart pi-web-sessiond.service pi-web-ui-dev.service");
|
||||||
|
expect(status.commands.restartWeb).toBe("systemctl --user restart pi-web-ui-dev.service");
|
||||||
|
expect(status.commands.restartSessiond).toBe("systemctl --user restart pi-web-sessiond.service");
|
||||||
|
expect(status.messages.find((message) => message.id === "sessiond-stale")?.command).toBe("systemctl --user restart pi-web-sessiond.service");
|
||||||
|
} finally {
|
||||||
|
await rm(home, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("omits local restart commands when no native service command is known", async () => {
|
||||||
|
process.env["PI_WEB_SKIP_VERSION_CHECK"] = "1";
|
||||||
|
const home = await tempHome();
|
||||||
|
try {
|
||||||
|
process.env["HOME"] = home;
|
||||||
|
const daemon = daemonWithComponent(staleLocalSessiond());
|
||||||
|
|
||||||
|
const status = await getPiWebStatus(daemon);
|
||||||
|
const staleMessage = status.messages.find((message) => message.id === "sessiond-stale");
|
||||||
|
|
||||||
|
expect(status.commands.restart).toBeUndefined();
|
||||||
|
expect(staleMessage?.command).toBeUndefined();
|
||||||
|
expect(JSON.stringify(status)).not.toContain("pi-web restart");
|
||||||
|
} finally {
|
||||||
|
await rm(home, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function daemonWithComponent(component: PiWebComponentStatus): SessionDaemonClient {
|
||||||
|
const daemon = new SessionDaemonClient();
|
||||||
|
vi.spyOn(daemon, "request").mockResolvedValue({
|
||||||
|
statusCode: 200,
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify({ version: component }),
|
||||||
|
});
|
||||||
|
return daemon;
|
||||||
|
}
|
||||||
|
|
||||||
|
function staleLocalSessiond(): PiWebComponentStatus {
|
||||||
|
return {
|
||||||
|
component: "sessiond",
|
||||||
|
label: "Session daemon",
|
||||||
|
runtimeVersion: "1.202605.7",
|
||||||
|
installedVersion: "1.202605.8",
|
||||||
|
stale: true,
|
||||||
|
available: true,
|
||||||
|
installation: { kind: "local", path: "/srv/dev/pi-web" },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tempHome(): Promise<string> {
|
||||||
|
return await mkdtemp(join(tmpdir(), "pi-web-status-"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function installSystemdServiceFiles(home: string, names: string[]): Promise<void> {
|
||||||
|
const dir = join(home, ".config", "systemd", "user");
|
||||||
|
await mkdir(dir, { recursive: true });
|
||||||
|
await Promise.all(names.map((name) => writeFile(join(dir, name), "")));
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreEnv(key: string, value: string | undefined): void {
|
||||||
|
if (value === undefined) Reflect.deleteProperty(process.env, key);
|
||||||
|
else process.env[key] = value;
|
||||||
|
}
|
||||||
|
|||||||
+171
-21
@@ -1,6 +1,7 @@
|
|||||||
import { spawnSync } from "node:child_process";
|
import { spawnSync } from "node:child_process";
|
||||||
import { readFileSync } from "node:fs";
|
import { existsSync, readFileSync } from "node:fs";
|
||||||
import { readFile, realpath, stat } from "node:fs/promises";
|
import { readFile, realpath, stat } from "node:fs/promises";
|
||||||
|
import { homedir } from "node:os";
|
||||||
import { dirname, join, relative, resolve, sep } from "node:path";
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { DefaultPackageManager, getAgentDir, SettingsManager } from "@earendil-works/pi-coding-agent";
|
import { DefaultPackageManager, getAgentDir, SettingsManager } from "@earendil-works/pi-coding-agent";
|
||||||
@@ -14,12 +15,46 @@ const DEFAULT_VERSION = "0.0.0-dev";
|
|||||||
const LATEST_RELEASE_CACHE_MS = 6 * 60 * 60 * 1000;
|
const LATEST_RELEASE_CACHE_MS = 6 * 60 * 60 * 1000;
|
||||||
const VERSION_CHECK_TIMEOUT_MS = 5000;
|
const VERSION_CHECK_TIMEOUT_MS = 5000;
|
||||||
|
|
||||||
const restartCommands = {
|
type ServiceId = "sessiond" | "web" | "uiDev";
|
||||||
restart: "pi-web restart",
|
type NativeServiceBackendKind = "systemd" | "launchd";
|
||||||
restartSystemd: "pi-web restart",
|
|
||||||
restartDev: "pi-web restart",
|
interface NativeServiceRef {
|
||||||
|
id: ServiceId;
|
||||||
|
systemdName: string;
|
||||||
|
launchdLabel: string;
|
||||||
|
launchdPlistName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NativeServiceCommands {
|
||||||
|
restart?: string;
|
||||||
|
restartWeb?: string;
|
||||||
|
restartSessiond?: string;
|
||||||
|
status?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const serviceRefs: Record<ServiceId, NativeServiceRef> = {
|
||||||
|
sessiond: {
|
||||||
|
id: "sessiond",
|
||||||
|
systemdName: "pi-web-sessiond.service",
|
||||||
|
launchdLabel: "com.pi-web.sessiond",
|
||||||
|
launchdPlistName: "com.pi-web.sessiond.plist",
|
||||||
|
},
|
||||||
|
web: {
|
||||||
|
id: "web",
|
||||||
|
systemdName: "pi-web.service",
|
||||||
|
launchdLabel: "com.pi-web.web",
|
||||||
|
launchdPlistName: "com.pi-web.web.plist",
|
||||||
|
},
|
||||||
|
uiDev: {
|
||||||
|
id: "uiDev",
|
||||||
|
systemdName: "pi-web-ui-dev.service",
|
||||||
|
launchdLabel: "com.pi-web.ui-dev",
|
||||||
|
launchdPlistName: "com.pi-web.ui-dev.plist",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const startServiceOrder: ServiceId[] = ["sessiond", "web", "uiDev"];
|
||||||
|
|
||||||
interface PackageInfo {
|
interface PackageInfo {
|
||||||
name: string;
|
name: string;
|
||||||
version: string;
|
version: string;
|
||||||
@@ -65,7 +100,7 @@ export async function getPiWebStatus(daemon = new SessionDaemonClient()): Promis
|
|||||||
const { web, sessiond } = versionStatus.components;
|
const { web, sessiond } = versionStatus.components;
|
||||||
const release = await getLatestReleaseStatus(web.installedVersion ?? web.runtimeVersion ?? DEFAULT_VERSION);
|
const release = await getLatestReleaseStatus(web.installedVersion ?? web.runtimeVersion ?? DEFAULT_VERSION);
|
||||||
const components = { web, sessiond };
|
const components = { web, sessiond };
|
||||||
const commands = commandsFor(web.installation ?? sessiond.installation);
|
const commands = commandsFor(components);
|
||||||
const messages = buildMessages(components, release, commands);
|
const messages = buildMessages(components, release, commands);
|
||||||
return {
|
return {
|
||||||
...versionStatus,
|
...versionStatus,
|
||||||
@@ -247,21 +282,122 @@ async function fetchLatestNpmVersion(currentVersion: string): Promise<string> {
|
|||||||
return version;
|
return version;
|
||||||
}
|
}
|
||||||
|
|
||||||
function commandsFor(installation: PiWebInstallationInfo | undefined): PiWebStatusResponse["commands"] {
|
function commandsFor(components: PiWebStatusResponse["components"]): PiWebStatusResponse["commands"] {
|
||||||
|
const installation = preferredInstallation(components);
|
||||||
|
const serviceCommands = nativeServiceCommands();
|
||||||
|
const cliCommands = piWebCliCommands(installation);
|
||||||
|
const restart = restartCommandFor(installation, serviceCommands, cliCommands);
|
||||||
|
const restartWeb = serviceCommands.restartWeb ?? cliCommands.restart;
|
||||||
|
const restartSessiond = serviceCommands.restartSessiond ?? cliCommands.restart;
|
||||||
|
const status = serviceCommands.status ?? cliCommands.status;
|
||||||
|
const update = updateCommandFor(installation, restart);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
update: updateCommandFor(installation),
|
...(update === undefined ? {} : { update }),
|
||||||
...restartCommands,
|
...(restart === undefined ? {} : { restart }),
|
||||||
|
...(restartWeb === undefined ? {} : { restartWeb }),
|
||||||
|
...(restartSessiond === undefined ? {} : { restartSessiond }),
|
||||||
|
...(status === undefined ? {} : { status }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCommandFor(installation: PiWebInstallationInfo | undefined): string {
|
function preferredInstallation(components: PiWebStatusResponse["components"]): PiWebInstallationInfo | undefined {
|
||||||
|
const web = components.web.installation;
|
||||||
|
const sessiond = components.sessiond.installation;
|
||||||
|
if (web?.kind === "local" || sessiond?.kind === "local") return web?.kind === "local" ? web : sessiond;
|
||||||
|
return web ?? sessiond;
|
||||||
|
}
|
||||||
|
|
||||||
|
function piWebCliCommands(installation: PiWebInstallationInfo | undefined): NativeServiceCommands {
|
||||||
|
if (installation?.kind !== "npm-global" || !hasCommand("pi-web")) return {};
|
||||||
|
return { restart: "pi-web restart", status: "pi-web status" };
|
||||||
|
}
|
||||||
|
|
||||||
|
function restartCommandFor(installation: PiWebInstallationInfo | undefined, serviceCommands: NativeServiceCommands, cliCommands: NativeServiceCommands): string | undefined {
|
||||||
|
if (installation?.kind === "local" || installation?.kind === "pi-package") return serviceCommands.restart ?? cliCommands.restart;
|
||||||
|
return cliCommands.restart ?? serviceCommands.restart;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCommandFor(installation: PiWebInstallationInfo | undefined, restartCommand: string | undefined): string | undefined {
|
||||||
|
if (restartCommand === undefined) return undefined;
|
||||||
if (installation?.kind === "pi-package") {
|
if (installation?.kind === "pi-package") {
|
||||||
return `pi update ${installation.source ?? PI_WEB_NPM_SOURCE} && ${restartCommands.restart}`;
|
if (!hasCommand("pi")) return undefined;
|
||||||
|
return `pi update ${installation.source ?? PI_WEB_NPM_SOURCE} && ${restartCommand}`;
|
||||||
}
|
}
|
||||||
if (installation?.kind === "local" && installation.path !== undefined) {
|
if (installation?.kind === "local" && installation.path !== undefined) {
|
||||||
return `cd ${shellQuote(installation.path)} && git pull && npm install && npm run build && ${restartCommands.restart}`;
|
if (!hasCommand("npm") || !isGitCheckoutWithUpstream(installation.path)) return undefined;
|
||||||
|
return `cd ${shellQuote(installation.path)} && git pull --ff-only && npm install && npm run build && ${restartCommand}`;
|
||||||
}
|
}
|
||||||
return `npm install -g ${PI_WEB_PACKAGE_NAME} && ${restartCommands.restart}`;
|
if (installation?.kind !== "npm-global" || !hasCommand("npm")) return undefined;
|
||||||
|
return `npm install -g ${PI_WEB_PACKAGE_NAME} && ${restartCommand}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function nativeServiceCommands(): NativeServiceCommands {
|
||||||
|
const backend = nativeServiceBackend();
|
||||||
|
if (backend === undefined) return {};
|
||||||
|
const installed = installedServiceIds(backend);
|
||||||
|
if (installed.size === 0) return {};
|
||||||
|
const web = installedServiceRefs(installed, ["web", "uiDev"]);
|
||||||
|
const sessiond = installedServiceRefs(installed, ["sessiond"]);
|
||||||
|
const restartable = web.length === 0 ? [] : installedServiceRefs(installed);
|
||||||
|
const status = installedServiceRefs(installed);
|
||||||
|
return {
|
||||||
|
...(restartable.length === 0 ? {} : { restart: restartNativeServicesCommand(backend, restartable) }),
|
||||||
|
...(web.length === 0 ? {} : { restartWeb: restartNativeServicesCommand(backend, web) }),
|
||||||
|
...(sessiond.length === 0 ? {} : { restartSessiond: restartNativeServicesCommand(backend, sessiond) }),
|
||||||
|
...(status.length === 0 ? {} : { status: statusNativeServicesCommand(backend, status) }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function nativeServiceBackend(): NativeServiceBackendKind | undefined {
|
||||||
|
if (process.platform === "linux" && hasCommand("systemctl")) return "systemd";
|
||||||
|
if (process.platform === "darwin" && hasCommand("launchctl")) return "launchd";
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function installedServiceIds(backend: NativeServiceBackendKind): Set<ServiceId> {
|
||||||
|
return new Set(startServiceOrder.filter((id) => existsSync(serviceFilePath(backend, serviceRefs[id]))));
|
||||||
|
}
|
||||||
|
|
||||||
|
function installedServiceRefs(installed: Set<ServiceId>, candidates: ServiceId[] = startServiceOrder): NativeServiceRef[] {
|
||||||
|
return startServiceOrder.filter((id) => candidates.includes(id) && installed.has(id)).map((id) => serviceRefs[id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function serviceFilePath(backend: NativeServiceBackendKind, ref: NativeServiceRef): string {
|
||||||
|
return backend === "systemd" ? join(systemdServiceDir(), ref.systemdName) : join(launchdServiceDir(), ref.launchdPlistName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function systemdServiceDir(): string {
|
||||||
|
return join(homedir(), ".config", "systemd", "user");
|
||||||
|
}
|
||||||
|
|
||||||
|
function launchdServiceDir(): string {
|
||||||
|
return join(homedir(), "Library", "LaunchAgents");
|
||||||
|
}
|
||||||
|
|
||||||
|
function restartNativeServicesCommand(backend: NativeServiceBackendKind, refs: NativeServiceRef[]): string {
|
||||||
|
if (backend === "systemd") return `systemctl --user restart ${refs.map((ref) => ref.systemdName).join(" ")}`;
|
||||||
|
return refs.map((ref) => `launchctl kickstart -k gui/$(id -u)/${ref.launchdLabel}`).join(" && ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusNativeServicesCommand(backend: NativeServiceBackendKind, refs: NativeServiceRef[]): string {
|
||||||
|
if (backend === "systemd") return `systemctl --user status ${refs.map((ref) => ref.systemdName).join(" ")}`;
|
||||||
|
return refs.map((ref) => `launchctl print gui/$(id -u)/${ref.launchdLabel}`).join(" && ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function isGitCheckoutWithUpstream(path: string): boolean {
|
||||||
|
return hasCommand("git")
|
||||||
|
&& commandSucceeds("git", ["-C", path, "rev-parse", "--is-inside-work-tree"])
|
||||||
|
&& commandSucceeds("git", ["-C", path, "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasCommand(command: string): boolean {
|
||||||
|
return commandSucceeds("/usr/bin/env", ["sh", "-c", `command -v ${command}`]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function commandSucceeds(command: string, args: string[]): boolean {
|
||||||
|
const result = spawnSync(command, args, { encoding: "utf8" });
|
||||||
|
return result.status === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function shellQuote(value: string): string {
|
function shellQuote(value: string): string {
|
||||||
@@ -277,18 +413,23 @@ function buildMessages(components: PiWebStatusResponse["components"], release: P
|
|||||||
id: "update-available",
|
id: "update-available",
|
||||||
severity: "info",
|
severity: "info",
|
||||||
title: "PI WEB update available",
|
title: "PI WEB update available",
|
||||||
body: `PI WEB ${release.latestVersion} is available${installedVersion === undefined ? "" : `; installed version is ${installedVersion}`}. Update PI WEB, then restart PI WEB services.`,
|
body: commands.update === undefined
|
||||||
command: commands.update,
|
? `PI WEB ${release.latestVersion} is available${installedVersion === undefined ? "" : `; installed version is ${installedVersion}`}. Update PI WEB, then restart the services or processes for this installation.`
|
||||||
|
: `PI WEB ${release.latestVersion} is available${installedVersion === undefined ? "" : `; installed version is ${installedVersion}`}. Run the update command to update PI WEB and restart its services.`,
|
||||||
|
...optionalMessageCommand(commands.update),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (components.web.stale) {
|
if (components.web.stale) {
|
||||||
|
const command = commands.restartWeb ?? commands.restart;
|
||||||
messages.push({
|
messages.push({
|
||||||
id: "web-stale",
|
id: "web-stale",
|
||||||
severity: "warning",
|
severity: "warning",
|
||||||
title: "Web/UI service restart needed",
|
title: "Web/UI service restart needed",
|
||||||
body: `The Web/UI service is running ${formatVersion(components.web.runtimeVersion)}, but ${formatVersion(components.web.installedVersion)} is installed. Restart the service to use the installed version.`,
|
body: command === undefined
|
||||||
command: commands.restart,
|
? `The Web/UI service is running ${formatVersion(components.web.runtimeVersion)}, but ${formatVersion(components.web.installedVersion)} is installed. Restart the Web/UI service or process to use the installed version.`
|
||||||
|
: `The Web/UI service is running ${formatVersion(components.web.runtimeVersion)}, but ${formatVersion(components.web.installedVersion)} is installed. Restart the service to use the installed version.`,
|
||||||
|
...optionalMessageCommand(command),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,22 +438,31 @@ function buildMessages(components: PiWebStatusResponse["components"], release: P
|
|||||||
id: "sessiond-unavailable",
|
id: "sessiond-unavailable",
|
||||||
severity: "warning",
|
severity: "warning",
|
||||||
title: "Session daemon version unavailable",
|
title: "Session daemon version unavailable",
|
||||||
body: `PI WEB could not check the session daemon version${components.sessiond.error === undefined ? "." : `: ${components.sessiond.error}`}`,
|
body: commands.status === undefined
|
||||||
command: "pi-web status",
|
? `PI WEB could not check the session daemon version${components.sessiond.error === undefined ? "." : `: ${components.sessiond.error}`}. Check the session daemon service or process that runs this installation.`
|
||||||
|
: `PI WEB could not check the session daemon version${components.sessiond.error === undefined ? "." : `: ${components.sessiond.error}`}`,
|
||||||
|
...optionalMessageCommand(commands.status),
|
||||||
});
|
});
|
||||||
} else if (components.sessiond.stale) {
|
} else if (components.sessiond.stale) {
|
||||||
|
const command = commands.restartSessiond ?? commands.restart;
|
||||||
messages.push({
|
messages.push({
|
||||||
id: "sessiond-stale",
|
id: "sessiond-stale",
|
||||||
severity: "warning",
|
severity: "warning",
|
||||||
title: "Session daemon restart needed",
|
title: "Session daemon restart needed",
|
||||||
body: `The session daemon is running ${formatVersion(components.sessiond.runtimeVersion)}, but ${formatVersion(components.sessiond.installedVersion)} is installed. Restart the daemon to use the installed version.`,
|
body: command === undefined
|
||||||
command: commands.restart,
|
? `The session daemon is running ${formatVersion(components.sessiond.runtimeVersion)}, but ${formatVersion(components.sessiond.installedVersion)} is installed. Restart the session daemon service or process to use the installed version.`
|
||||||
|
: `The session daemon is running ${formatVersion(components.sessiond.runtimeVersion)}, but ${formatVersion(components.sessiond.installedVersion)} is installed. Restart the daemon to use the installed version.`,
|
||||||
|
...optionalMessageCommand(command),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return messages;
|
return messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function optionalMessageCommand(command: string | undefined): Pick<PiWebStatusMessage, "command"> | object {
|
||||||
|
return command === undefined ? {} : { command };
|
||||||
|
}
|
||||||
|
|
||||||
function skipVersionCheck(): boolean {
|
function skipVersionCheck(): boolean {
|
||||||
return ["PI_WEB_SKIP_VERSION_CHECK", "PI_WEB_OFFLINE", "PI_SKIP_VERSION_CHECK", "PI_OFFLINE"].some((key) => {
|
return ["PI_WEB_SKIP_VERSION_CHECK", "PI_WEB_OFFLINE", "PI_SKIP_VERSION_CHECK", "PI_OFFLINE"].some((key) => {
|
||||||
const value = process.env[key];
|
const value = process.env[key];
|
||||||
|
|||||||
@@ -296,10 +296,11 @@ export interface PiWebVersionResponse {
|
|||||||
export interface PiWebStatusResponse extends PiWebVersionResponse {
|
export interface PiWebStatusResponse extends PiWebVersionResponse {
|
||||||
release: PiWebReleaseStatus;
|
release: PiWebReleaseStatus;
|
||||||
commands: {
|
commands: {
|
||||||
update: string;
|
update?: string;
|
||||||
restart: string;
|
restart?: string;
|
||||||
restartSystemd: string;
|
restartWeb?: string;
|
||||||
restartDev: string;
|
restartSessiond?: string;
|
||||||
|
status?: string;
|
||||||
};
|
};
|
||||||
messages: PiWebStatusMessage[];
|
messages: PiWebStatusMessage[];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user