@hyperdreamer/pi-webui 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -0
- package/README.md +148 -0
- package/dist/cli.js +975 -0
- package/dist/cli.js.map +1 -0
- package/dist/client/apple-touch-icon.png +0 -0
- package/dist/client/assets/CodeViewer-aB4ZVzEu.js +4 -0
- package/dist/client/assets/UnifiedDiffViewer-f8X7aTkU.js +39 -0
- package/dist/client/assets/index-Cffeusjy.js +6022 -0
- package/dist/client/assets/vendor-editor-core-vUi74DnD.js +12 -0
- package/dist/client/assets/vendor-editor-languages-DoaXUodB.js +46 -0
- package/dist/client/assets/vendor-editor-legacy-CYBnW6ZU.js +1 -0
- package/dist/client/assets/vendor-terminal-BrP-ENHg.css +1 -0
- package/dist/client/assets/vendor-terminal-D8k4UKM2.js +35 -0
- package/dist/client/favicon.svg +8 -0
- package/dist/client/index.html +65 -0
- package/dist/client/manifest.webmanifest +24 -0
- package/dist/client/pwa-icon-192.png +0 -0
- package/dist/client/pwa-icon-512.png +0 -0
- package/dist/config.js +417 -0
- package/dist/config.js.map +1 -0
- package/dist/docker/piWebUiDockerCommandPlan.js +211 -0
- package/dist/docker/piWebUiDockerCommandPlan.js.map +1 -0
- package/dist/nativeServices/serviceDoctor.js +480 -0
- package/dist/nativeServices/serviceDoctor.js.map +1 -0
- package/dist/nativeServices/serviceInstall.js +35 -0
- package/dist/nativeServices/serviceInstall.js.map +1 -0
- package/dist/nativeServices/servicePlan.js +358 -0
- package/dist/nativeServices/servicePlan.js.map +1 -0
- package/dist/nativeServices/serviceProbe.js +447 -0
- package/dist/nativeServices/serviceProbe.js.map +1 -0
- package/dist/nativeServices/serviceRendering.js +122 -0
- package/dist/nativeServices/serviceRendering.js.map +1 -0
- package/dist/pi-webui-plugins/info/package.json +9 -0
- package/dist/pi-webui-plugins/info/pi-webui-plugin.js +51 -0
- package/dist/pi-webui-plugins/updates/package.json +9 -0
- package/dist/pi-webui-plugins/updates/pi-webui-plugin.js +185 -0
- package/dist/pi-webui-plugins/updates/updatesLogic.js +97 -0
- package/dist/pi-webui-plugins/workspace-tasks/config.js +91 -0
- package/dist/pi-webui-plugins/workspace-tasks/package.json +9 -0
- package/dist/pi-webui-plugins/workspace-tasks/pi-webui-plugin.js +48 -0
- package/dist/pi-webui-plugins/workspace-tasks/taskRunner.js +12 -0
- package/dist/pi-webui-plugins/workspace-tasks/tasksPanelElement.js +288 -0
- package/dist/pi-webui-plugins/workspace-tasks/workspaceTasksClient.js +47 -0
- package/dist/piWebUiVersionReport.js +225 -0
- package/dist/piWebUiVersionReport.js.map +1 -0
- package/dist/plugin-api/unstable.d.ts +22 -0
- package/dist/plugin-api.d.ts +199 -0
- package/dist/server/activeAgentProfileProvider.js +25 -0
- package/dist/server/activeAgentProfileProvider.js.map +1 -0
- package/dist/server/activity/workspaceActivityRoutes.js +4 -0
- package/dist/server/activity/workspaceActivityRoutes.js.map +1 -0
- package/dist/server/activity/workspaceActivityService.js +98 -0
- package/dist/server/activity/workspaceActivityService.js.map +1 -0
- package/dist/server/app.js +211 -0
- package/dist/server/app.js.map +1 -0
- package/dist/server/browserMessageProjection.js +56 -0
- package/dist/server/browserMessageProjection.js.map +1 -0
- package/dist/server/configRoutes.js +309 -0
- package/dist/server/configRoutes.js.map +1 -0
- package/dist/server/diagnostics/nodePtyNativeModule.js +36 -0
- package/dist/server/diagnostics/nodePtyNativeModule.js.map +1 -0
- package/dist/server/diagnostics/nodePtySpawnHelper.js +135 -0
- package/dist/server/diagnostics/nodePtySpawnHelper.js.map +1 -0
- package/dist/server/git/gitEnv.js +15 -0
- package/dist/server/git/gitEnv.js.map +1 -0
- package/dist/server/git/gitService.js +123 -0
- package/dist/server/git/gitService.js.map +1 -0
- package/dist/server/gitRoutes.js +23 -0
- package/dist/server/gitRoutes.js.map +1 -0
- package/dist/server/index.js +7 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/machines/machineClient.js +172 -0
- package/dist/server/machines/machineClient.js.map +1 -0
- package/dist/server/machines/machinePluginProxyRoutes.js +190 -0
- package/dist/server/machines/machinePluginProxyRoutes.js.map +1 -0
- package/dist/server/machines/machineProxyRoutes.js +172 -0
- package/dist/server/machines/machineProxyRoutes.js.map +1 -0
- package/dist/server/machines/machineRoutes.js +56 -0
- package/dist/server/machines/machineRoutes.js.map +1 -0
- package/dist/server/machines/machineService.js +238 -0
- package/dist/server/machines/machineService.js.map +1 -0
- package/dist/server/machines/machineStore.js +128 -0
- package/dist/server/machines/machineStore.js.map +1 -0
- package/dist/server/models/modelsConfigRoutes.js +40 -0
- package/dist/server/models/modelsConfigRoutes.js.map +1 -0
- package/dist/server/models/modelsConfigService.js +339 -0
- package/dist/server/models/modelsConfigService.js.map +1 -0
- package/dist/server/piPackagePluginsConfigRoutes.js +86 -0
- package/dist/server/piPackagePluginsConfigRoutes.js.map +1 -0
- package/dist/server/piPackagePluginsConfigService.js +311 -0
- package/dist/server/piPackagePluginsConfigService.js.map +1 -0
- package/dist/server/piPackageRoutes.js +91 -0
- package/dist/server/piPackageRoutes.js.map +1 -0
- package/dist/server/piPackageService.js +101 -0
- package/dist/server/piPackageService.js.map +1 -0
- package/dist/server/piWebUiPluginService.js +326 -0
- package/dist/server/piWebUiPluginService.js.map +1 -0
- package/dist/server/piWebUiReleaseLookupCache.js +37 -0
- package/dist/server/piWebUiReleaseLookupCache.js.map +1 -0
- package/dist/server/piWebUiStatus.js +622 -0
- package/dist/server/piWebUiStatus.js.map +1 -0
- package/dist/server/piWebUiStatusCache.js +45 -0
- package/dist/server/piWebUiStatusCache.js.map +1 -0
- package/dist/server/projects/directorySuggestions.js +37 -0
- package/dist/server/projects/directorySuggestions.js.map +1 -0
- package/dist/server/projects/projectService.js +31 -0
- package/dist/server/projects/projectService.js.map +1 -0
- package/dist/server/realtime/sessionEventHub.js +70 -0
- package/dist/server/realtime/sessionEventHub.js.map +1 -0
- package/dist/server/sessiond/sessionDaemonStartup.js +49 -0
- package/dist/server/sessiond/sessionDaemonStartup.js.map +1 -0
- package/dist/server/sessiond/sessionProxyRoutes.js +87 -0
- package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -0
- package/dist/server/sessiond/skillsGithubToken.js +7 -0
- package/dist/server/sessiond/skillsGithubToken.js.map +1 -0
- package/dist/server/sessiond.js +152 -0
- package/dist/server/sessiond.js.map +1 -0
- package/dist/server/sessions/attachmentService.js +119 -0
- package/dist/server/sessions/attachmentService.js.map +1 -0
- package/dist/server/sessions/authProviderOptions.js +50 -0
- package/dist/server/sessions/authProviderOptions.js.map +1 -0
- package/dist/server/sessions/authRoutes.js +69 -0
- package/dist/server/sessions/authRoutes.js.map +1 -0
- package/dist/server/sessions/authService.js +134 -0
- package/dist/server/sessions/authService.js.map +1 -0
- package/dist/server/sessions/builtinCommands.js +27 -0
- package/dist/server/sessions/builtinCommands.js.map +1 -0
- package/dist/server/sessions/editPreview.js +196 -0
- package/dist/server/sessions/editPreview.js.map +1 -0
- package/dist/server/sessions/messagePaging.js +43 -0
- package/dist/server/sessions/messagePaging.js.map +1 -0
- package/dist/server/sessions/oauthLoginFlowService.js +349 -0
- package/dist/server/sessions/oauthLoginFlowService.js.map +1 -0
- package/dist/server/sessions/piSessionManagerGateway.js +115 -0
- package/dist/server/sessions/piSessionManagerGateway.js.map +1 -0
- package/dist/server/sessions/piSessionService.js +3230 -0
- package/dist/server/sessions/piSessionService.js.map +1 -0
- package/dist/server/sessions/plainTextTheme.js +112 -0
- package/dist/server/sessions/plainTextTheme.js.map +1 -0
- package/dist/server/sessions/sessionArchiveMigration.js +590 -0
- package/dist/server/sessions/sessionArchiveMigration.js.map +1 -0
- package/dist/server/sessions/sessionArchiveStore.js +262 -0
- package/dist/server/sessions/sessionArchiveStore.js.map +1 -0
- package/dist/server/sessions/sessionArchiveTree.js +35 -0
- package/dist/server/sessions/sessionArchiveTree.js.map +1 -0
- package/dist/server/sessions/sessionCleanup.js +157 -0
- package/dist/server/sessions/sessionCleanup.js.map +1 -0
- package/dist/server/sessions/sessionCommandService.js +313 -0
- package/dist/server/sessions/sessionCommandService.js.map +1 -0
- package/dist/server/sessions/sessionDefaultsRoutes.js +65 -0
- package/dist/server/sessions/sessionDefaultsRoutes.js.map +1 -0
- package/dist/server/sessions/sessionDefaultsService.js +90 -0
- package/dist/server/sessions/sessionDefaultsService.js.map +1 -0
- package/dist/server/sessions/sessionHistoryExport.js +115 -0
- package/dist/server/sessions/sessionHistoryExport.js.map +1 -0
- package/dist/server/sessions/sessionMetadataStore.js +102 -0
- package/dist/server/sessions/sessionMetadataStore.js.map +1 -0
- package/dist/server/sessions/sessionNameGenerator.js +90 -0
- package/dist/server/sessions/sessionNameGenerator.js.map +1 -0
- package/dist/server/sessions/sessionNotificationStore.js +484 -0
- package/dist/server/sessions/sessionNotificationStore.js.map +1 -0
- package/dist/server/sessions/sessionRoutes.js +590 -0
- package/dist/server/sessions/sessionRoutes.js.map +1 -0
- package/dist/server/sessions/sessionRuntimeStore.js +2 -0
- package/dist/server/sessions/sessionRuntimeStore.js.map +1 -0
- package/dist/server/sessions/sessionService.js +2 -0
- package/dist/server/sessions/sessionService.js.map +1 -0
- package/dist/server/sessions/sessionTreeProjection.js +250 -0
- package/dist/server/sessions/sessionTreeProjection.js.map +1 -0
- package/dist/server/sessions/sessionUnreadStore.js +520 -0
- package/dist/server/sessions/sessionUnreadStore.js.map +1 -0
- package/dist/server/sessions/spawnSessionTool.js +41 -0
- package/dist/server/sessions/spawnSessionTool.js.map +1 -0
- package/dist/server/sessions/spawnSubsessionTool.js +214 -0
- package/dist/server/sessions/spawnSubsessionTool.js.map +1 -0
- package/dist/server/sessions/spawnTargetResolver.js +36 -0
- package/dist/server/sessions/spawnTargetResolver.js.map +1 -0
- package/dist/server/sessions/subsessionTranscript.js +235 -0
- package/dist/server/sessions/subsessionTranscript.js.map +1 -0
- package/dist/server/skills/runNpx.js +30 -0
- package/dist/server/skills/runNpx.js.map +1 -0
- package/dist/server/skills/skillLock.js +115 -0
- package/dist/server/skills/skillLock.js.map +1 -0
- package/dist/server/skills/skillUpdates.js +194 -0
- package/dist/server/skills/skillUpdates.js.map +1 -0
- package/dist/server/skills/skillsConfigRoutes.js +64 -0
- package/dist/server/skills/skillsConfigRoutes.js.map +1 -0
- package/dist/server/skills/skillsConfigService.js +391 -0
- package/dist/server/skills/skillsConfigService.js.map +1 -0
- package/dist/server/storage/projectStore.js +88 -0
- package/dist/server/storage/projectStore.js.map +1 -0
- package/dist/server/systemInfoRoutes.js +265 -0
- package/dist/server/systemInfoRoutes.js.map +1 -0
- package/dist/server/terminalProxyRoutes.js +140 -0
- package/dist/server/terminalProxyRoutes.js.map +1 -0
- package/dist/server/terminals/terminalRoutes.js +155 -0
- package/dist/server/terminals/terminalRoutes.js.map +1 -0
- package/dist/server/terminals/terminalService.js +305 -0
- package/dist/server/terminals/terminalService.js.map +1 -0
- package/dist/server/terminals/terminalSize.js +17 -0
- package/dist/server/terminals/terminalSize.js.map +1 -0
- package/dist/server/types.js +2 -0
- package/dist/server/types.js.map +1 -0
- package/dist/server/webSocketBridge.js +32 -0
- package/dist/server/webSocketBridge.js.map +1 -0
- package/dist/server/workingDirectory.js +44 -0
- package/dist/server/workingDirectory.js.map +1 -0
- package/dist/server/workspaceExplorerRoutes.js +109 -0
- package/dist/server/workspaceExplorerRoutes.js.map +1 -0
- package/dist/server/workspaces/effectivePathAccess.js +27 -0
- package/dist/server/workspaces/effectivePathAccess.js.map +1 -0
- package/dist/server/workspaces/fileContentService.js +181 -0
- package/dist/server/workspaces/fileContentService.js.map +1 -0
- package/dist/server/workspaces/fileSuggestions.js +520 -0
- package/dist/server/workspaces/fileSuggestions.js.map +1 -0
- package/dist/server/workspaces/fileTreeService.js +35 -0
- package/dist/server/workspaces/fileTreeService.js.map +1 -0
- package/dist/server/workspaces/gitWorktreeDiscovery.js +34 -0
- package/dist/server/workspaces/gitWorktreeDiscovery.js.map +1 -0
- package/dist/server/workspaces/imagePreviewService.js +40 -0
- package/dist/server/workspaces/imagePreviewService.js.map +1 -0
- package/dist/server/workspaces/pathAccessPolicy.js +90 -0
- package/dist/server/workspaces/pathAccessPolicy.js.map +1 -0
- package/dist/server/workspaces/pathSafety.js +45 -0
- package/dist/server/workspaces/pathSafety.js.map +1 -0
- package/dist/server/workspaces/projectPiWebUiConfig.js +61 -0
- package/dist/server/workspaces/projectPiWebUiConfig.js.map +1 -0
- package/dist/server/workspaces/workspaceContext.js +8 -0
- package/dist/server/workspaces/workspaceContext.js.map +1 -0
- package/dist/server/workspaces/workspaceDeletionRoutes.js +113 -0
- package/dist/server/workspaces/workspaceDeletionRoutes.js.map +1 -0
- package/dist/server/workspaces/workspaceService.js +39 -0
- package/dist/server/workspaces/workspaceService.js.map +1 -0
- package/dist/sessiond/activeAgentProfile.js +31 -0
- package/dist/sessiond/activeAgentProfile.js.map +1 -0
- package/dist/sessiond/config.js +9 -0
- package/dist/sessiond/config.js.map +1 -0
- package/dist/sessiond/sessionDaemonClient.js +103 -0
- package/dist/sessiond/sessionDaemonClient.js.map +1 -0
- package/dist/shared/activeAgentProfile.js +76 -0
- package/dist/shared/activeAgentProfile.js.map +1 -0
- package/dist/shared/activity.js +11 -0
- package/dist/shared/activity.js.map +1 -0
- package/dist/shared/apiTypes.d.ts +1212 -0
- package/dist/shared/apiTypes.js +25 -0
- package/dist/shared/apiTypes.js.map +1 -0
- package/dist/shared/capabilities.js +69 -0
- package/dist/shared/capabilities.js.map +1 -0
- package/dist/shared/federatedRoutes.js +109 -0
- package/dist/shared/federatedRoutes.js.map +1 -0
- package/dist/shared/machinePluginIds.js +41 -0
- package/dist/shared/machinePluginIds.js.map +1 -0
- package/dist/shared/piWebUiStatusParsing.js +108 -0
- package/dist/shared/piWebUiStatusParsing.js.map +1 -0
- package/dist/shared/pluginIds.js +5 -0
- package/dist/shared/pluginIds.js.map +1 -0
- package/dist/shared/promptAttachments.js +92 -0
- package/dist/shared/promptAttachments.js.map +1 -0
- package/dist/shared/thinkingLevels.d.ts +27 -0
- package/dist/shared/thinkingLevels.js +31 -0
- package/dist/shared/thinkingLevels.js.map +1 -0
- package/dist/shared/workspaceDeletion.js +12 -0
- package/dist/shared/workspaceDeletion.js.map +1 -0
- package/dist/shared/workspaceFiles.js +3 -0
- package/dist/shared/workspaceFiles.js.map +1 -0
- package/docs/assets/favicon.svg +8 -0
- package/docs/assets/pi-webui-banner.png +0 -0
- package/docs/assets/pi-webui-desktop.png +0 -0
- package/docs/assets/pi-webui-mobile.png +0 -0
- package/docs/assets/pi-webui-tablet.png +0 -0
- package/docs/config.md +265 -0
- package/docs/plugins.md +988 -0
- package/extensions/pi-webui.ts +133 -0
- package/package.json +143 -0
- package/plugin-api/unstable.d.ts +1 -0
- package/plugin-api.d.ts +1 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,975 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
4
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
5
|
+
import { homedir, userInfo } from "node:os";
|
|
6
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { DEFAULT_PORT, defaultPiWebUiConfigPath, defaultPiWebUiDataDir, effectivePiWebUiConfig, examplePiWebUiConfig } from "./config.js";
|
|
9
|
+
import { packageVersion, printPiWebUiVersionReport } from "./piWebUiVersionReport.js";
|
|
10
|
+
import { checkNodePtyDarwinSpawnHelper, formatNodePtyDarwinSpawnHelperCheck } from "./server/diagnostics/nodePtySpawnHelper.js";
|
|
11
|
+
import { checkNodePtyNativeModule, formatNodePtyNativeModuleCheck } from "./server/diagnostics/nodePtyNativeModule.js";
|
|
12
|
+
import { installNativeServiceCandidate, nativeServiceInstallFailureNeedsPathAdvice, } from "./nativeServices/serviceInstall.js";
|
|
13
|
+
import { minimumSupportedNodeVersion, nativeServiceManagerRefs, productionNativeServiceIds, } from "./nativeServices/servicePlan.js";
|
|
14
|
+
import { formatNativeServiceDoctorResult, inferInstalledNativeServiceMode, inspectInstalledDevelopmentServiceInput, inspectInstalledProductionServiceContext, runNativeServiceDoctor, } from "./nativeServices/serviceDoctor.js";
|
|
15
|
+
import { createNativeServiceAuthoritativeProbe, nativeServicePrerequisiteShellCheck, } from "./nativeServices/serviceProbe.js";
|
|
16
|
+
import { renderLaunchdPlist, renderSystemdUnit } from "./nativeServices/serviceRendering.js";
|
|
17
|
+
const PI_WEBUI_PACKAGE_NAME = "@hyperdreamer/pi-webui";
|
|
18
|
+
const systemdServiceDir = join(homedir(), ".config", "systemd", "user");
|
|
19
|
+
const launchdServiceDir = join(homedir(), "Library", "LaunchAgents");
|
|
20
|
+
const logDir = join(defaultPiWebUiDataDir(), "logs");
|
|
21
|
+
const serviceRefs = {
|
|
22
|
+
sessiond: { id: "sessiond", ...nativeServiceManagerRefs.sessiond },
|
|
23
|
+
web: { id: "web", ...nativeServiceManagerRefs.web },
|
|
24
|
+
uiDev: { id: "uiDev", ...nativeServiceManagerRefs.uiDev },
|
|
25
|
+
};
|
|
26
|
+
const productionServiceIds = [...productionNativeServiceIds];
|
|
27
|
+
const startServiceOrder = ["sessiond", "web", "uiDev"];
|
|
28
|
+
const stopServiceOrder = ["web", "uiDev", "sessiond"];
|
|
29
|
+
// Restart web/UI before sessiond: when `pi-webui restart` runs in a pi-webui
|
|
30
|
+
// terminal (owned by sessiond), restarting sessiond kills the command, so any
|
|
31
|
+
// services handled after it would never be restarted.
|
|
32
|
+
const restartServiceOrder = ["web", "uiDev", "sessiond"];
|
|
33
|
+
function platformLabel() {
|
|
34
|
+
if (process.platform === "darwin")
|
|
35
|
+
return "macOS";
|
|
36
|
+
if (process.platform === "linux")
|
|
37
|
+
return "Linux";
|
|
38
|
+
if (process.platform === "win32")
|
|
39
|
+
return "Windows";
|
|
40
|
+
return process.platform;
|
|
41
|
+
}
|
|
42
|
+
export function serviceBackendForPlatform(platform) {
|
|
43
|
+
if (platform === "linux")
|
|
44
|
+
return { kind: "systemd", label: "systemd user services" };
|
|
45
|
+
if (platform === "darwin")
|
|
46
|
+
return { kind: "launchd", label: "LaunchAgents" };
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
function currentServiceBackend() {
|
|
50
|
+
return serviceBackendForPlatform(process.platform);
|
|
51
|
+
}
|
|
52
|
+
function requireServiceBackend(command) {
|
|
53
|
+
const backend = currentServiceBackend();
|
|
54
|
+
if (backend !== undefined)
|
|
55
|
+
return backend;
|
|
56
|
+
throw new Error(`\`${command}\` requires a supported per-user service manager (systemd user services or LaunchAgents) and is not supported on ${platformLabel()}.\n\n${manualRunAdvice()}`);
|
|
57
|
+
}
|
|
58
|
+
function supportsSystemdUserServices() {
|
|
59
|
+
return currentServiceBackend()?.kind === "systemd";
|
|
60
|
+
}
|
|
61
|
+
function manualRunAdvice() {
|
|
62
|
+
return [
|
|
63
|
+
"Run PI WEBUI manually from a checkout:",
|
|
64
|
+
" npm run start:sessiond",
|
|
65
|
+
` PI_WEBUI_PORT=${String(DEFAULT_PORT)} npm start`,
|
|
66
|
+
"",
|
|
67
|
+
"For development in one terminal:",
|
|
68
|
+
" npm run dev",
|
|
69
|
+
"",
|
|
70
|
+
"For split development, keep sessiond separate and run web/API plus Vite UI separately:",
|
|
71
|
+
" npm run dev:sessiond",
|
|
72
|
+
" npm run dev:web",
|
|
73
|
+
" npm run dev:client",
|
|
74
|
+
].join("\n");
|
|
75
|
+
}
|
|
76
|
+
function run(command, args, options = {}) {
|
|
77
|
+
const result = spawnSync(command, args, { stdio: "inherit" });
|
|
78
|
+
const status = result.status ?? 1;
|
|
79
|
+
if (options.check === true && status !== 0)
|
|
80
|
+
process.exit(status);
|
|
81
|
+
return status;
|
|
82
|
+
}
|
|
83
|
+
function outputText(value) {
|
|
84
|
+
return typeof value === "string" ? value : "";
|
|
85
|
+
}
|
|
86
|
+
function capture(command, args) {
|
|
87
|
+
const result = spawnSync(command, args, { encoding: "utf8" });
|
|
88
|
+
const errorMessage = result.error instanceof Error ? result.error.message : "";
|
|
89
|
+
const stderr = outputText(result.stderr);
|
|
90
|
+
return { status: result.status ?? 1, stdout: outputText(result.stdout), stderr: stderr === "" ? errorMessage : stderr };
|
|
91
|
+
}
|
|
92
|
+
function runQuiet(command, args) {
|
|
93
|
+
return capture(command, args).status;
|
|
94
|
+
}
|
|
95
|
+
function hasCommand(command) {
|
|
96
|
+
return capture("/usr/bin/env", ["sh", "-c", `command -v ${shellSingleQuote(command)}`]).status === 0;
|
|
97
|
+
}
|
|
98
|
+
function isLingerEnabled() {
|
|
99
|
+
if (!hasCommand("loginctl"))
|
|
100
|
+
return undefined;
|
|
101
|
+
const result = capture("loginctl", ["show-user", userInfo().username, "-p", "Linger"]);
|
|
102
|
+
if (result.status !== 0)
|
|
103
|
+
return undefined;
|
|
104
|
+
const value = result.stdout.trim();
|
|
105
|
+
if (value === "Linger=yes")
|
|
106
|
+
return true;
|
|
107
|
+
if (value === "Linger=no")
|
|
108
|
+
return false;
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
function parseInstallOptions(args) {
|
|
112
|
+
const options = { host: "127.0.0.1", port: String(DEFAULT_PORT), mode: "production" };
|
|
113
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
114
|
+
const arg = args[i];
|
|
115
|
+
if (arg === undefined)
|
|
116
|
+
continue;
|
|
117
|
+
if (arg === "--host") {
|
|
118
|
+
const value = args[i + 1];
|
|
119
|
+
if (value === undefined)
|
|
120
|
+
throw new Error("--host requires a value");
|
|
121
|
+
options.host = value;
|
|
122
|
+
i += 1;
|
|
123
|
+
}
|
|
124
|
+
else if (arg.startsWith("--host=")) {
|
|
125
|
+
options.host = arg.slice("--host=".length);
|
|
126
|
+
}
|
|
127
|
+
else if (arg === "--port") {
|
|
128
|
+
const value = args[i + 1];
|
|
129
|
+
if (value === undefined)
|
|
130
|
+
throw new Error("--port requires a value");
|
|
131
|
+
options.port = value;
|
|
132
|
+
i += 1;
|
|
133
|
+
}
|
|
134
|
+
else if (arg.startsWith("--port=")) {
|
|
135
|
+
options.port = arg.slice("--port=".length);
|
|
136
|
+
}
|
|
137
|
+
else if (arg === "--config") {
|
|
138
|
+
const value = args[i + 1];
|
|
139
|
+
if (value === undefined)
|
|
140
|
+
throw new Error("--config requires a value");
|
|
141
|
+
options.config = value;
|
|
142
|
+
i += 1;
|
|
143
|
+
}
|
|
144
|
+
else if (arg.startsWith("--config=")) {
|
|
145
|
+
options.config = arg.slice("--config=".length);
|
|
146
|
+
}
|
|
147
|
+
else if (arg === "--dev") {
|
|
148
|
+
options.mode = "dev";
|
|
149
|
+
}
|
|
150
|
+
else if (arg === "--user-systemd") {
|
|
151
|
+
// Accepted for backwards-compatible readability; PI WEBUI chooses the native user service backend automatically.
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
throw new Error(`Unknown install option: ${arg}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return options;
|
|
158
|
+
}
|
|
159
|
+
function shellSingleQuote(value) {
|
|
160
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
161
|
+
}
|
|
162
|
+
function fishSingleQuote(value) {
|
|
163
|
+
return `'${value.replaceAll("\\", "\\\\").replaceAll("'", "\\'")}'`;
|
|
164
|
+
}
|
|
165
|
+
function packageRootPath() {
|
|
166
|
+
return dirname(dirname(fileURLToPath(import.meta.url)));
|
|
167
|
+
}
|
|
168
|
+
function packageEntrypointPath(name) {
|
|
169
|
+
return join(packageRootPath(), "dist", "server", name === "server" ? "index.js" : "sessiond.js");
|
|
170
|
+
}
|
|
171
|
+
export function regularFileExists(path) {
|
|
172
|
+
return existsSync(path) && statSync(path).isFile();
|
|
173
|
+
}
|
|
174
|
+
function detectServiceShell() {
|
|
175
|
+
const userShell = userInfo().shell ?? undefined;
|
|
176
|
+
const envShell = process.env["SHELL"]?.trim();
|
|
177
|
+
const detected = envShell === undefined || envShell === "" ? userShell : envShell;
|
|
178
|
+
const name = basename(detected ?? "").replace(/^-/, "");
|
|
179
|
+
if (name === "bash" || name === "zsh" || name === "fish") {
|
|
180
|
+
return {
|
|
181
|
+
name,
|
|
182
|
+
executable: detected ?? name,
|
|
183
|
+
source: "detected",
|
|
184
|
+
detectedExecutable: detected ?? name,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
name: "bash",
|
|
189
|
+
executable: "bash",
|
|
190
|
+
source: "fallback",
|
|
191
|
+
detectedExecutable: detected ?? null,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function serviceShellCommand(command, cwd) {
|
|
195
|
+
const fullCommand = cwd === undefined ? command : `cd ${serviceShellQuote(cwd)} && ${command}`;
|
|
196
|
+
return ["/usr/bin/env", detectServiceShell().executable, "-lc", fullCommand];
|
|
197
|
+
}
|
|
198
|
+
function serviceShellQuote(value) {
|
|
199
|
+
return detectServiceShell().name === "fish" ? fishSingleQuote(value) : shellSingleQuote(value);
|
|
200
|
+
}
|
|
201
|
+
function describeServiceShell() {
|
|
202
|
+
const shell = detectServiceShell();
|
|
203
|
+
if (shell.source === "fallback") {
|
|
204
|
+
return shell.detectedExecutable === null
|
|
205
|
+
? "could not detect a supported login shell; using bash"
|
|
206
|
+
: `detected ${shell.detectedExecutable}; using bash because PI WEBUI currently supports bash, zsh, and fish`;
|
|
207
|
+
}
|
|
208
|
+
return shell.detectedExecutable === null ? shell.name : `${shell.name} (${shell.detectedExecutable})`;
|
|
209
|
+
}
|
|
210
|
+
function configEnvironment(options, configPath) {
|
|
211
|
+
return options.config === undefined ? {} : { PI_WEBUI_CONFIG: configPath };
|
|
212
|
+
}
|
|
213
|
+
function serviceRefList(ids) {
|
|
214
|
+
return ids.map((id) => serviceRefs[id]);
|
|
215
|
+
}
|
|
216
|
+
function allServiceRefs() {
|
|
217
|
+
return serviceRefList(["sessiond", "web", "uiDev"]);
|
|
218
|
+
}
|
|
219
|
+
function productionServiceRefs() {
|
|
220
|
+
return serviceRefList(productionServiceIds);
|
|
221
|
+
}
|
|
222
|
+
function orderServiceRefs(refs, order) {
|
|
223
|
+
const byId = new Map(refs.map((ref) => [ref.id, ref]));
|
|
224
|
+
return order.flatMap((id) => {
|
|
225
|
+
const ref = byId.get(id);
|
|
226
|
+
return ref === undefined ? [] : [ref];
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
function startOrder(refs) {
|
|
230
|
+
return orderServiceRefs(refs, startServiceOrder);
|
|
231
|
+
}
|
|
232
|
+
function stopOrder(refs) {
|
|
233
|
+
return orderServiceRefs(refs, stopServiceOrder);
|
|
234
|
+
}
|
|
235
|
+
function restartOrder(refs) {
|
|
236
|
+
return orderServiceRefs(refs, restartServiceOrder);
|
|
237
|
+
}
|
|
238
|
+
function devRootPath() {
|
|
239
|
+
return resolve(process.cwd());
|
|
240
|
+
}
|
|
241
|
+
function validateDevCheckout(root) {
|
|
242
|
+
const packageJsonPath = join(root, "package.json");
|
|
243
|
+
if (!existsSync(packageJsonPath)) {
|
|
244
|
+
throw new Error(`Development mode must be installed from a PI WEBUI checkout. Missing package.json: ${packageJsonPath}`);
|
|
245
|
+
}
|
|
246
|
+
const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
247
|
+
if (!isRecord(parsed) || parsed["name"] !== PI_WEBUI_PACKAGE_NAME) {
|
|
248
|
+
throw new Error(`Development mode must be installed from a PI WEBUI checkout. ${packageJsonPath} is not ${PI_WEBUI_PACKAGE_NAME}.`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function launchdLogPath(ref) {
|
|
252
|
+
return join(logDir, ref.logName);
|
|
253
|
+
}
|
|
254
|
+
function installConfigPath(options) {
|
|
255
|
+
return options.config === undefined ? defaultPiWebUiConfigPath() : resolve(options.config);
|
|
256
|
+
}
|
|
257
|
+
async function writeInitialConfig(options, configPath) {
|
|
258
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
259
|
+
if (!existsSync(configPath)) {
|
|
260
|
+
await writeFile(configPath, examplePiWebUiConfig({ host: options.host, port: Number(options.port) }));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function systemdServicePath(ref) {
|
|
264
|
+
return join(systemdServiceDir, ref.systemdName);
|
|
265
|
+
}
|
|
266
|
+
function launchdPlistPath(ref) {
|
|
267
|
+
return join(launchdServiceDir, ref.launchdPlistName);
|
|
268
|
+
}
|
|
269
|
+
function serviceFilePath(backend, ref) {
|
|
270
|
+
return backend.kind === "systemd" ? systemdServicePath(ref) : launchdPlistPath(ref);
|
|
271
|
+
}
|
|
272
|
+
function serviceFileExists(backend, ref) {
|
|
273
|
+
return existsSync(serviceFilePath(backend, ref));
|
|
274
|
+
}
|
|
275
|
+
function installedServiceIds(backend) {
|
|
276
|
+
return new Set(allServiceRefs().filter((ref) => serviceFileExists(backend, ref)).map((ref) => ref.id));
|
|
277
|
+
}
|
|
278
|
+
function installedServiceRefs(backend) {
|
|
279
|
+
const installed = startOrder(allServiceRefs().filter((ref) => serviceFileExists(backend, ref)));
|
|
280
|
+
return installed.length === 0 ? productionServiceRefs() : installed;
|
|
281
|
+
}
|
|
282
|
+
async function installSystemdServices(plan) {
|
|
283
|
+
const selected = new Set(plan.services.map((service) => service.id));
|
|
284
|
+
const obsolete = stopOrder(allServiceRefs().filter((ref) => !selected.has(ref.id)));
|
|
285
|
+
for (const ref of obsolete) {
|
|
286
|
+
runQuiet("systemctl", ["--user", "disable", "--now", ref.systemdName]);
|
|
287
|
+
await rm(systemdServicePath(ref), { force: true });
|
|
288
|
+
}
|
|
289
|
+
await mkdir(systemdServiceDir, { recursive: true });
|
|
290
|
+
for (const service of plan.services) {
|
|
291
|
+
await writeFile(join(systemdServiceDir, service.manager.systemdName), renderSystemdUnit(plan, service));
|
|
292
|
+
}
|
|
293
|
+
const names = plan.services.map((service) => service.manager.systemdName);
|
|
294
|
+
run("systemctl", ["--user", "daemon-reload"], { check: true });
|
|
295
|
+
run("systemctl", ["--user", "enable", ...names], { check: true });
|
|
296
|
+
run("systemctl", ["--user", "restart", ...names], { check: true });
|
|
297
|
+
}
|
|
298
|
+
function launchdDomain() {
|
|
299
|
+
return `gui/${String(userInfo().uid)}`;
|
|
300
|
+
}
|
|
301
|
+
function launchdServiceTarget(ref) {
|
|
302
|
+
return `${launchdDomain()}/${ref.launchdLabel}`;
|
|
303
|
+
}
|
|
304
|
+
function launchdIsLoaded(ref) {
|
|
305
|
+
return capture("launchctl", ["print", launchdServiceTarget(ref)]).status === 0;
|
|
306
|
+
}
|
|
307
|
+
function launchdBootout(ref) {
|
|
308
|
+
runQuiet("launchctl", ["bootout", launchdServiceTarget(ref)]);
|
|
309
|
+
}
|
|
310
|
+
function launchdBootstrap(ref) {
|
|
311
|
+
run("launchctl", ["bootstrap", launchdDomain(), launchdPlistPath(ref)], { check: true });
|
|
312
|
+
run("launchctl", ["enable", launchdServiceTarget(ref)], { check: true });
|
|
313
|
+
}
|
|
314
|
+
function launchdStart(ref) {
|
|
315
|
+
if (!launchdIsLoaded(ref))
|
|
316
|
+
launchdBootstrap(ref);
|
|
317
|
+
run("launchctl", ["kickstart", launchdServiceTarget(ref)], { check: true });
|
|
318
|
+
}
|
|
319
|
+
async function installLaunchdServices(plan) {
|
|
320
|
+
const selected = new Set(plan.services.map((service) => service.id));
|
|
321
|
+
await mkdir(launchdServiceDir, { recursive: true });
|
|
322
|
+
await mkdir(logDir, { recursive: true });
|
|
323
|
+
for (const ref of stopOrder(allServiceRefs()))
|
|
324
|
+
launchdBootout(ref);
|
|
325
|
+
for (const ref of allServiceRefs().filter((candidate) => !selected.has(candidate.id))) {
|
|
326
|
+
await rm(launchdPlistPath(ref), { force: true });
|
|
327
|
+
}
|
|
328
|
+
for (const service of plan.services) {
|
|
329
|
+
const plistPath = join(launchdServiceDir, service.manager.launchdPlistName);
|
|
330
|
+
await writeFile(plistPath, renderLaunchdPlist(plan, service, logDir));
|
|
331
|
+
}
|
|
332
|
+
for (const service of plan.services)
|
|
333
|
+
launchdStart(serviceRefFromPlan(service.id, service.manager));
|
|
334
|
+
}
|
|
335
|
+
async function installNativeServices(plan) {
|
|
336
|
+
if (plan.backend.kind === "systemd")
|
|
337
|
+
await installSystemdServices(plan);
|
|
338
|
+
else
|
|
339
|
+
await installLaunchdServices(plan);
|
|
340
|
+
}
|
|
341
|
+
function serviceRefFromPlan(id, manager) {
|
|
342
|
+
return { id, ...manager };
|
|
343
|
+
}
|
|
344
|
+
async function uninstallSystemdServices() {
|
|
345
|
+
for (const ref of stopOrder(allServiceRefs())) {
|
|
346
|
+
runQuiet("systemctl", ["--user", "disable", "--now", ref.systemdName]);
|
|
347
|
+
await rm(systemdServicePath(ref), { force: true });
|
|
348
|
+
}
|
|
349
|
+
runQuiet("systemctl", ["--user", "daemon-reload"]);
|
|
350
|
+
}
|
|
351
|
+
async function uninstallLaunchdServices() {
|
|
352
|
+
for (const ref of stopOrder(allServiceRefs())) {
|
|
353
|
+
launchdBootout(ref);
|
|
354
|
+
await rm(launchdPlistPath(ref), { force: true });
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
async function uninstallNativeServices(backend) {
|
|
358
|
+
if (backend.kind === "systemd")
|
|
359
|
+
await uninstallSystemdServices();
|
|
360
|
+
else
|
|
361
|
+
await uninstallLaunchdServices();
|
|
362
|
+
}
|
|
363
|
+
function serviceDisplayName(ref) {
|
|
364
|
+
if (ref.id === "sessiond")
|
|
365
|
+
return "session daemon";
|
|
366
|
+
if (ref.id === "uiDev")
|
|
367
|
+
return "UI/API dev server";
|
|
368
|
+
return "web server";
|
|
369
|
+
}
|
|
370
|
+
function statusServiceRefs(backend) {
|
|
371
|
+
const ids = installedServiceIds(backend);
|
|
372
|
+
if (ids.size === 0)
|
|
373
|
+
return [];
|
|
374
|
+
if (ids.has("web") && ids.has("uiDev"))
|
|
375
|
+
return startOrder(allServiceRefs());
|
|
376
|
+
if (ids.has("uiDev"))
|
|
377
|
+
return serviceRefList(["sessiond", "uiDev"]);
|
|
378
|
+
if (ids.has("web"))
|
|
379
|
+
return productionServiceRefs();
|
|
380
|
+
return serviceRefList(["sessiond"]);
|
|
381
|
+
}
|
|
382
|
+
function serviceInstallMode(backend) {
|
|
383
|
+
const ids = installedServiceIds(backend);
|
|
384
|
+
if (ids.size === 0)
|
|
385
|
+
return "not installed";
|
|
386
|
+
const hasSessiond = ids.has("sessiond");
|
|
387
|
+
const hasWeb = ids.has("web");
|
|
388
|
+
const hasUiDev = ids.has("uiDev");
|
|
389
|
+
if (hasWeb && hasUiDev)
|
|
390
|
+
return "mixed";
|
|
391
|
+
if (hasUiDev)
|
|
392
|
+
return hasSessiond ? "development" : "development (incomplete)";
|
|
393
|
+
if (hasWeb)
|
|
394
|
+
return hasSessiond ? "production" : "production (incomplete)";
|
|
395
|
+
return "partial";
|
|
396
|
+
}
|
|
397
|
+
function makeServiceRuntimeStatus(ref, health, detail, target, filePath, pid) {
|
|
398
|
+
return {
|
|
399
|
+
ref,
|
|
400
|
+
health,
|
|
401
|
+
detail,
|
|
402
|
+
target,
|
|
403
|
+
filePath,
|
|
404
|
+
...(pid === undefined ? {} : { pid }),
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
function firstOutputLine(...values) {
|
|
408
|
+
for (const value of values) {
|
|
409
|
+
const line = value.trim().split("\n").find((candidate) => candidate.trim() !== "");
|
|
410
|
+
if (line !== undefined)
|
|
411
|
+
return line.trim();
|
|
412
|
+
}
|
|
413
|
+
return undefined;
|
|
414
|
+
}
|
|
415
|
+
function systemdMainPid(ref) {
|
|
416
|
+
const result = capture("systemctl", ["--user", "--no-pager", "show", ref.systemdName, "--property=MainPID", "--value"]);
|
|
417
|
+
if (result.status !== 0)
|
|
418
|
+
return undefined;
|
|
419
|
+
const value = result.stdout.trim();
|
|
420
|
+
return value === "" || value === "0" ? undefined : value;
|
|
421
|
+
}
|
|
422
|
+
function systemdRuntimeStatus(backend, ref) {
|
|
423
|
+
const target = ref.systemdName;
|
|
424
|
+
const filePath = serviceFilePath(backend, ref);
|
|
425
|
+
if (!serviceFileExists(backend, ref))
|
|
426
|
+
return makeServiceRuntimeStatus(ref, "not-installed", "not installed", target, filePath);
|
|
427
|
+
const result = capture("systemctl", ["--user", "--no-pager", "is-active", target]);
|
|
428
|
+
const state = firstOutputLine(result.stdout, result.stderr) ?? "unknown";
|
|
429
|
+
if (result.status === 0 && state === "active")
|
|
430
|
+
return makeServiceRuntimeStatus(ref, "running", "running", target, filePath, systemdMainPid(ref));
|
|
431
|
+
return makeServiceRuntimeStatus(ref, state === "unknown" ? "unknown" : "stopped", state, target, filePath);
|
|
432
|
+
}
|
|
433
|
+
function parseLaunchdField(output, field) {
|
|
434
|
+
const match = new RegExp(`^\\s*${field}\\s=\\s(.+)$`, "m").exec(output);
|
|
435
|
+
return match?.[1]?.trim();
|
|
436
|
+
}
|
|
437
|
+
export function launchdRuntimeDetails(output) {
|
|
438
|
+
const state = parseLaunchdField(output, "state") ?? "unknown";
|
|
439
|
+
const pid = parseLaunchdField(output, "pid");
|
|
440
|
+
const lastExitCode = parseLaunchdField(output, "last exit code");
|
|
441
|
+
const detail = state === "running"
|
|
442
|
+
? "running"
|
|
443
|
+
: lastExitCode === undefined ? state : `${state} (last exit code ${lastExitCode})`;
|
|
444
|
+
return { state, detail, pid };
|
|
445
|
+
}
|
|
446
|
+
function launchdRuntimeStatus(backend, ref) {
|
|
447
|
+
const target = launchdServiceTarget(ref);
|
|
448
|
+
const filePath = serviceFilePath(backend, ref);
|
|
449
|
+
if (!serviceFileExists(backend, ref))
|
|
450
|
+
return makeServiceRuntimeStatus(ref, "not-installed", "not installed", target, filePath);
|
|
451
|
+
const result = capture("launchctl", ["print", target]);
|
|
452
|
+
if (result.status !== 0) {
|
|
453
|
+
return makeServiceRuntimeStatus(ref, "stopped", firstOutputLine(result.stderr, result.stdout) ?? "not loaded", target, filePath);
|
|
454
|
+
}
|
|
455
|
+
const details = launchdRuntimeDetails(result.stdout);
|
|
456
|
+
const health = details.state === "running" ? "running" : details.state === "unknown" ? "unknown" : "stopped";
|
|
457
|
+
return makeServiceRuntimeStatus(ref, health, details.detail, target, filePath, details.pid);
|
|
458
|
+
}
|
|
459
|
+
function runtimeStatus(backend, ref) {
|
|
460
|
+
return backend.kind === "systemd" ? systemdRuntimeStatus(backend, ref) : launchdRuntimeStatus(backend, ref);
|
|
461
|
+
}
|
|
462
|
+
function printServiceStatus(status) {
|
|
463
|
+
const icon = status.health === "running" ? "✓" : "✗";
|
|
464
|
+
const pid = status.pid === undefined ? "" : `, pid ${status.pid}`;
|
|
465
|
+
console.log(`${icon} ${serviceDisplayName(status.ref)}: ${status.detail} (${status.target}${pid})`);
|
|
466
|
+
if (status.health === "not-installed")
|
|
467
|
+
console.log(` missing service file: ${status.filePath}`);
|
|
468
|
+
}
|
|
469
|
+
function printServiceStatusReport(backend) {
|
|
470
|
+
const refs = statusServiceRefs(backend);
|
|
471
|
+
console.log(`PI WEBUI services: ${serviceInstallMode(backend)} (${backend.label})`);
|
|
472
|
+
if (refs.length === 0) {
|
|
473
|
+
console.log("✗ no PI WEBUI service files found");
|
|
474
|
+
console.log(" Run `pi-webui install` or `pi-webui install --dev`.");
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
const statuses = refs.map((ref) => runtimeStatus(backend, ref));
|
|
478
|
+
for (const status of statuses)
|
|
479
|
+
printServiceStatus(status);
|
|
480
|
+
console.log("\nUse `pi-webui logs` for service logs.");
|
|
481
|
+
return statuses.every((status) => status.health === "running");
|
|
482
|
+
}
|
|
483
|
+
function configuredServiceCommand(name) {
|
|
484
|
+
const value = process.env[name];
|
|
485
|
+
return value === undefined || value.trim() === "" ? undefined : value;
|
|
486
|
+
}
|
|
487
|
+
function productionNativeServicePlanInput(backend, shell, environment) {
|
|
488
|
+
return {
|
|
489
|
+
backend,
|
|
490
|
+
shell,
|
|
491
|
+
environment,
|
|
492
|
+
executables: {
|
|
493
|
+
sessiond: {
|
|
494
|
+
configuredCommand: configuredServiceCommand("PI_WEBUI_SESSIOND_EXEC"),
|
|
495
|
+
namedCommand: "pi-webui-sessiond",
|
|
496
|
+
bundledEntrypointPath: packageEntrypointPath("sessiond"),
|
|
497
|
+
},
|
|
498
|
+
web: {
|
|
499
|
+
configuredCommand: configuredServiceCommand("PI_WEBUI_SERVER_EXEC"),
|
|
500
|
+
namedCommand: "pi-webui-server",
|
|
501
|
+
bundledEntrypointPath: packageEntrypointPath("server"),
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
function nativeServiceInstallCandidate(options, backend, configPath, devRoot) {
|
|
507
|
+
const shell = detectServiceShell();
|
|
508
|
+
const environment = configEnvironment(options, configPath);
|
|
509
|
+
if (options.mode === "production") {
|
|
510
|
+
return {
|
|
511
|
+
mode: "production",
|
|
512
|
+
input: productionNativeServicePlanInput(backend, shell, environment),
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
const root = devRoot ?? devRootPath();
|
|
516
|
+
return {
|
|
517
|
+
mode: "development",
|
|
518
|
+
input: {
|
|
519
|
+
backend,
|
|
520
|
+
shell,
|
|
521
|
+
environment,
|
|
522
|
+
workingDirectory: root,
|
|
523
|
+
packageJsonPath: join(root, "package.json"),
|
|
524
|
+
},
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
function printNativeServiceInstallFailure(failure) {
|
|
528
|
+
if (failure.kind === "plan-resolution") {
|
|
529
|
+
for (const item of failure.failures) {
|
|
530
|
+
if (item.kind === "probe-infrastructure") {
|
|
531
|
+
console.log(`✗ Service-manager probe infrastructure failure (${item.reason}): ${item.message}`);
|
|
532
|
+
}
|
|
533
|
+
else if (item.kind === "entrypoint-inspection-failure") {
|
|
534
|
+
console.log(`✗ Could not inspect bundled ${item.serviceId} entrypoint ${item.entrypointPath}: ${item.message}`);
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
console.log(`✗ ${item.namedCommand} is unavailable to the service manager, and bundled entrypoint ${item.bundledEntrypointPath} is missing.`);
|
|
538
|
+
if (item.namedCommandFailure !== null)
|
|
539
|
+
console.log(` ${item.namedCommandFailure}`);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
for (const item of failure.failures) {
|
|
545
|
+
if (item.kind === "probe-infrastructure") {
|
|
546
|
+
console.log(`✗ Service-manager probe infrastructure failure (${item.reason}): ${item.message}`);
|
|
547
|
+
}
|
|
548
|
+
else {
|
|
549
|
+
console.log(`✗ ${item.prerequisite.description}`);
|
|
550
|
+
if (item.detail !== null && item.detail !== item.prerequisite.description)
|
|
551
|
+
console.log(` ${item.detail}`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
async function install(args) {
|
|
556
|
+
const backend = requireServiceBackend("pi-webui install");
|
|
557
|
+
const options = parseInstallOptions(args);
|
|
558
|
+
const devRoot = options.mode === "dev" ? devRootPath() : undefined;
|
|
559
|
+
if (devRoot !== undefined)
|
|
560
|
+
validateDevCheckout(devRoot);
|
|
561
|
+
const configPath = installConfigPath(options);
|
|
562
|
+
const candidate = nativeServiceInstallCandidate(options, backend, configPath, devRoot);
|
|
563
|
+
console.log(`Running PI WEBUI ${options.mode} install preflight checks...`);
|
|
564
|
+
console.log(`Service backend: ${backend.label}`);
|
|
565
|
+
console.log(`Service shell: ${describeServiceShell()}`);
|
|
566
|
+
if (!printNodePtyNativeModuleCheck()) {
|
|
567
|
+
throw new Error("Install preflight checks failed without changing config or services. Fix the failure above, then run `pi-webui doctor` for more detail.");
|
|
568
|
+
}
|
|
569
|
+
const result = await installNativeServiceCandidate(candidate, {
|
|
570
|
+
probe: createNativeServiceAuthoritativeProbe(),
|
|
571
|
+
fileExists: regularFileExists,
|
|
572
|
+
writeInitialConfig: () => writeInitialConfig(options, configPath),
|
|
573
|
+
replaceServices: installNativeServices,
|
|
574
|
+
});
|
|
575
|
+
if (!result.ok) {
|
|
576
|
+
printNativeServiceInstallFailure(result.failure);
|
|
577
|
+
if (nativeServiceInstallFailureNeedsPathAdvice(result.failure))
|
|
578
|
+
printPathSetupAdvice();
|
|
579
|
+
throw new Error("Install preflight checks failed without changing config or services. Fix the failure above, then run `pi-webui doctor` for more detail.");
|
|
580
|
+
}
|
|
581
|
+
for (const service of result.plan.services.filter((item) => item.strategy.kind === "configured-override")) {
|
|
582
|
+
console.log(`! ${service.description} uses a configured command override; preflight did not execute that arbitrary command.`);
|
|
583
|
+
}
|
|
584
|
+
console.log(`\nPI WEBUI ${options.mode} services are installed and starting.`);
|
|
585
|
+
console.log(`Config: ${configPath}`);
|
|
586
|
+
if (options.mode === "dev") {
|
|
587
|
+
console.log("Open: http://127.0.0.1:8809");
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
console.log(`Open: http://${options.host === "0.0.0.0" ? "127.0.0.1" : options.host}:${options.port}`);
|
|
591
|
+
}
|
|
592
|
+
if (backend.kind === "systemd") {
|
|
593
|
+
const linger = isLingerEnabled();
|
|
594
|
+
if (linger === false) {
|
|
595
|
+
console.log("\nRecommended for server use: keep user services running after logout/reboot:");
|
|
596
|
+
console.log(` sudo loginctl enable-linger ${userInfo().username}`);
|
|
597
|
+
}
|
|
598
|
+
else if (linger === undefined) {
|
|
599
|
+
console.log("\nRecommended for server use: enable systemd user lingering so services survive logout/reboot:");
|
|
600
|
+
console.log(` sudo loginctl enable-linger ${userInfo().username}`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
console.log("\nUseful commands:");
|
|
604
|
+
console.log(" pi-webui status");
|
|
605
|
+
console.log(" pi-webui logs");
|
|
606
|
+
console.log(" pi-webui restart");
|
|
607
|
+
}
|
|
608
|
+
async function uninstall() {
|
|
609
|
+
const backend = requireServiceBackend("pi-webui uninstall");
|
|
610
|
+
await uninstallNativeServices(backend);
|
|
611
|
+
console.log(`PI WEBUI ${backend.label} removed. Production and development service files were removed; config and data were left in place.`);
|
|
612
|
+
}
|
|
613
|
+
function systemdServiceAction(action, refs) {
|
|
614
|
+
const orderedRefs = action === "stop" ? stopOrder(refs) : action === "restart" ? restartOrder(refs) : startOrder(refs);
|
|
615
|
+
run("systemctl", ["--user", action, ...orderedRefs.map((ref) => ref.systemdName)], { check: true });
|
|
616
|
+
}
|
|
617
|
+
function launchdServiceAction(action, refs) {
|
|
618
|
+
if (action === "stop") {
|
|
619
|
+
for (const ref of stopOrder(refs))
|
|
620
|
+
launchdBootout(ref);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
if (action === "restart") {
|
|
624
|
+
// Restart each service fully (bootout + start) before moving to the next,
|
|
625
|
+
// so the web/UI services are back up before sessiond is restarted.
|
|
626
|
+
for (const ref of restartOrder(refs)) {
|
|
627
|
+
launchdBootout(ref);
|
|
628
|
+
launchdStart(ref);
|
|
629
|
+
}
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
for (const ref of startOrder(refs))
|
|
633
|
+
launchdStart(ref);
|
|
634
|
+
}
|
|
635
|
+
function serviceAction(action) {
|
|
636
|
+
const backend = requireServiceBackend(`pi-webui ${action}`);
|
|
637
|
+
if (action === "status") {
|
|
638
|
+
if (!printServiceStatusReport(backend))
|
|
639
|
+
process.exitCode = 1;
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
const refs = installedServiceRefs(backend);
|
|
643
|
+
if (backend.kind === "systemd")
|
|
644
|
+
systemdServiceAction(action, refs);
|
|
645
|
+
else
|
|
646
|
+
launchdServiceAction(action, refs);
|
|
647
|
+
}
|
|
648
|
+
function logs() {
|
|
649
|
+
const backend = requireServiceBackend("pi-webui logs");
|
|
650
|
+
const refs = installedServiceRefs(backend);
|
|
651
|
+
if (backend.kind === "systemd") {
|
|
652
|
+
run("journalctl", ["--user", ...refs.flatMap((ref) => ["-u", ref.systemdName]), "-f"]);
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
run("tail", ["-F", ...refs.map((ref) => launchdLogPath(ref))]);
|
|
656
|
+
}
|
|
657
|
+
function serviceShellLabel() {
|
|
658
|
+
return `${detectServiceShell().name} -lc`;
|
|
659
|
+
}
|
|
660
|
+
function commandCheck(command) {
|
|
661
|
+
return `command -v ${serviceShellQuote(command)}`;
|
|
662
|
+
}
|
|
663
|
+
export function commandWithVersionCheck(command) {
|
|
664
|
+
const found = commandCheck(command);
|
|
665
|
+
const commandWord = serviceShellQuote(command);
|
|
666
|
+
if (detectServiceShell().name === "fish") {
|
|
667
|
+
return `${found} && begin; ${commandWord} --version 2>&1 || true; end`;
|
|
668
|
+
}
|
|
669
|
+
return `${found} && (${commandWord} --version 2>&1 || true)`;
|
|
670
|
+
}
|
|
671
|
+
export function nodeVersionCheck() {
|
|
672
|
+
return nativeServicePrerequisiteShellCheck(detectServiceShell().name, {
|
|
673
|
+
id: "caller.node",
|
|
674
|
+
kind: "node-version",
|
|
675
|
+
command: "node",
|
|
676
|
+
minimumVersion: minimumSupportedNodeVersion,
|
|
677
|
+
description: `node >= ${minimumSupportedNodeVersion}`,
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
export function agentCommandForChecks(env = process.env) {
|
|
681
|
+
return effectivePiWebUiConfig({ env }).config.agent.command;
|
|
682
|
+
}
|
|
683
|
+
function generalDoctorChecks() {
|
|
684
|
+
const shell = serviceShellLabel();
|
|
685
|
+
const agentCommand = agentCommandForChecks();
|
|
686
|
+
return [
|
|
687
|
+
[`Caller login ${shell} can find node >= ${minimumSupportedNodeVersion}`, serviceShellCommand(nodeVersionCheck())],
|
|
688
|
+
[`Caller login ${shell} can find npm`, serviceShellCommand(commandWithVersionCheck("npm"))],
|
|
689
|
+
[`Caller login ${shell} can find ${agentCommand}`, serviceShellCommand(commandWithVersionCheck(agentCommand))],
|
|
690
|
+
];
|
|
691
|
+
}
|
|
692
|
+
function runChecks(checks) {
|
|
693
|
+
let failed = false;
|
|
694
|
+
for (const [label, command] of checks) {
|
|
695
|
+
const [bin, ...args] = command;
|
|
696
|
+
if (bin === undefined)
|
|
697
|
+
continue;
|
|
698
|
+
const result = capture(bin, args);
|
|
699
|
+
const ok = result.status === 0;
|
|
700
|
+
failed ||= !ok;
|
|
701
|
+
console.log(`${ok ? "✓" : "✗"} ${label}`);
|
|
702
|
+
printCheckOutput(result.stdout || result.stderr);
|
|
703
|
+
}
|
|
704
|
+
return !failed;
|
|
705
|
+
}
|
|
706
|
+
function printCheckOutput(output) {
|
|
707
|
+
const trimmed = output.trim();
|
|
708
|
+
if (trimmed === "")
|
|
709
|
+
return;
|
|
710
|
+
const lines = trimmed.split("\n");
|
|
711
|
+
for (const line of lines.slice(0, 3))
|
|
712
|
+
console.log(` ${line}`);
|
|
713
|
+
if (lines.length > 3)
|
|
714
|
+
console.log(" ...");
|
|
715
|
+
}
|
|
716
|
+
function optionalDoctorChecks() {
|
|
717
|
+
const shell = serviceShellLabel();
|
|
718
|
+
return [[`Caller login ${shell} can find optional ripgrep (rg)`, serviceShellCommand(commandCheck("rg"))]];
|
|
719
|
+
}
|
|
720
|
+
function printOptionalDoctorChecks() {
|
|
721
|
+
let missingOptionalTool = false;
|
|
722
|
+
for (const [label, command] of optionalDoctorChecks()) {
|
|
723
|
+
const [bin, ...args] = command;
|
|
724
|
+
if (bin === undefined)
|
|
725
|
+
continue;
|
|
726
|
+
const result = capture(bin, args);
|
|
727
|
+
const ok = result.status === 0;
|
|
728
|
+
missingOptionalTool ||= !ok;
|
|
729
|
+
console.log(`${ok ? "✓" : "!"} ${label}`);
|
|
730
|
+
printCheckOutput(result.stdout || result.stderr);
|
|
731
|
+
}
|
|
732
|
+
if (missingOptionalTool) {
|
|
733
|
+
console.log(" Install ripgrep, or make rg visible to the service shell, for faster all-file @ suggestions.");
|
|
734
|
+
console.log(" PI WEBUI falls back to a bounded filesystem scan when rg is unavailable.");
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
function installedServiceDefinitions(backend, ids) {
|
|
738
|
+
return ids.map((id) => ({
|
|
739
|
+
id,
|
|
740
|
+
contents: readFileSync(serviceFilePath(backend, serviceRefs[id]), "utf8"),
|
|
741
|
+
}));
|
|
742
|
+
}
|
|
743
|
+
function nativeServiceDoctorTarget(backend) {
|
|
744
|
+
const ids = installedServiceIds(backend);
|
|
745
|
+
const mode = inferInstalledNativeServiceMode(ids);
|
|
746
|
+
if (mode === "ambiguous") {
|
|
747
|
+
return {
|
|
748
|
+
kind: "inspection-failure",
|
|
749
|
+
message: `installed service IDs do not identify one mode (${[...ids].join(", ") || "none"}).`,
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
if (mode === "none") {
|
|
753
|
+
return {
|
|
754
|
+
kind: "prospective-production",
|
|
755
|
+
input: productionNativeServicePlanInput(backend, detectServiceShell(), {}),
|
|
756
|
+
reason: "no installed service strategy is available",
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
const expectedIds = mode === "production"
|
|
760
|
+
? productionNativeServiceIds
|
|
761
|
+
: ["sessiond", "uiDev"];
|
|
762
|
+
const missingId = expectedIds.find((id) => !ids.has(id));
|
|
763
|
+
if (missingId !== undefined) {
|
|
764
|
+
return {
|
|
765
|
+
kind: "inspection-failure",
|
|
766
|
+
message: `installed ${mode} service set is incomplete; ${missingId} is missing.`,
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
let definitions;
|
|
770
|
+
try {
|
|
771
|
+
definitions = installedServiceDefinitions(backend, expectedIds);
|
|
772
|
+
}
|
|
773
|
+
catch (error) {
|
|
774
|
+
return {
|
|
775
|
+
kind: "inspection-failure",
|
|
776
|
+
message: error instanceof Error ? error.message : String(error),
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
if (mode === "development") {
|
|
780
|
+
const inspection = inspectInstalledDevelopmentServiceInput(backend, definitions);
|
|
781
|
+
return inspection.ok
|
|
782
|
+
? { kind: "installed-development", input: inspection.value }
|
|
783
|
+
: { kind: "inspection-failure", message: inspection.message };
|
|
784
|
+
}
|
|
785
|
+
const inspection = inspectInstalledProductionServiceContext(backend, definitions);
|
|
786
|
+
return inspection.ok
|
|
787
|
+
? {
|
|
788
|
+
kind: "prospective-production",
|
|
789
|
+
input: productionNativeServicePlanInput(backend, inspection.value.shell, inspection.value.environment),
|
|
790
|
+
reason: "installed executable strategy is not recorded",
|
|
791
|
+
}
|
|
792
|
+
: { kind: "inspection-failure", message: inspection.message };
|
|
793
|
+
}
|
|
794
|
+
async function printNativeServiceDoctorChecks(backend) {
|
|
795
|
+
const result = await runNativeServiceDoctor(nativeServiceDoctorTarget(backend), {
|
|
796
|
+
probe: createNativeServiceAuthoritativeProbe(),
|
|
797
|
+
fileExists: regularFileExists,
|
|
798
|
+
});
|
|
799
|
+
const report = formatNativeServiceDoctorResult(result);
|
|
800
|
+
for (const line of report.lines)
|
|
801
|
+
console.log(line);
|
|
802
|
+
printCallerContextComparisons(report);
|
|
803
|
+
return report;
|
|
804
|
+
}
|
|
805
|
+
function printCallerContextComparisons(report) {
|
|
806
|
+
if (report.plan === null || report.failedPrerequisites.length === 0)
|
|
807
|
+
return;
|
|
808
|
+
const seen = new Set();
|
|
809
|
+
for (const prerequisite of report.failedPrerequisites) {
|
|
810
|
+
if (seen.has(prerequisite.id))
|
|
811
|
+
continue;
|
|
812
|
+
seen.add(prerequisite.id);
|
|
813
|
+
const service = report.plan.services.find((candidate) => candidate.prerequisites.some((item) => item.id === prerequisite.id));
|
|
814
|
+
const command = nativeServicePrerequisiteShellCheck(report.plan.shell.name, prerequisite);
|
|
815
|
+
const result = captureServiceShell(report.plan.shell, command, service?.workingDirectory ?? null);
|
|
816
|
+
console.log(` Caller-invoked ${report.plan.shell.name} -lc ${result.status === 0 ? "satisfies" : "also does not satisfy"} ${prerequisite.description}; the service-manager result is authoritative.`);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
function captureServiceShell(shell, command, workingDirectory) {
|
|
820
|
+
const fullCommand = workingDirectory === null
|
|
821
|
+
? command
|
|
822
|
+
: `cd ${shellQuoteFor(shell.name, workingDirectory)} && ${command}`;
|
|
823
|
+
return capture("/usr/bin/env", [shell.executable, "-lc", fullCommand]);
|
|
824
|
+
}
|
|
825
|
+
function shellQuoteFor(shell, value) {
|
|
826
|
+
return shell === "fish" ? fishSingleQuote(value) : shellSingleQuote(value);
|
|
827
|
+
}
|
|
828
|
+
function printPathSetupAdvice(shell = detectServiceShell()) {
|
|
829
|
+
console.log("\nPATH setup advice:");
|
|
830
|
+
if (shell.name === "bash") {
|
|
831
|
+
console.log(" Detected bash. Put PATH setup for node/version managers/tools in ~/.bash_profile or ~/.profile.");
|
|
832
|
+
console.log(" If ~/.bash_profile exists, bash will not read ~/.profile unless you source it from ~/.bash_profile.");
|
|
833
|
+
console.log(" Do not rely only on ~/.bashrc or prompt hooks for tools needed by services or agents.");
|
|
834
|
+
}
|
|
835
|
+
else if (shell.name === "zsh") {
|
|
836
|
+
console.log(" Detected zsh. Put PATH setup for node/version managers/tools in ~/.zprofile, not only ~/.zshrc.");
|
|
837
|
+
console.log(" Avoid relying on prompt hooks; PI WEBUI services run non-interactive login shells.");
|
|
838
|
+
}
|
|
839
|
+
else {
|
|
840
|
+
console.log(" Detected fish. Prefer universal PATH setup such as `fish_add_path -U ...` for tools needed by services or agents.");
|
|
841
|
+
console.log(" Avoid relying on prompt hooks; PI WEBUI services run non-interactive login shells.");
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
export function doctorExitCode(generalReadinessOk, nativeServicePlanOk, nodePtyRuntimeOk) {
|
|
845
|
+
return generalReadinessOk && nativeServicePlanOk && nodePtyRuntimeOk ? 0 : 1;
|
|
846
|
+
}
|
|
847
|
+
async function doctor() {
|
|
848
|
+
const backend = currentServiceBackend();
|
|
849
|
+
console.log(`Platform: ${platformLabel()}`);
|
|
850
|
+
console.log(`Service backend: ${backend?.label ?? "manual run only"}`);
|
|
851
|
+
console.log(`Service shell: ${describeServiceShell()}`);
|
|
852
|
+
if (backend === undefined) {
|
|
853
|
+
console.log(`- Native user service plan checks skipped on ${platformLabel()}; no native-service drift is reported.`);
|
|
854
|
+
}
|
|
855
|
+
console.log("");
|
|
856
|
+
await printPiWebUiVersionReport();
|
|
857
|
+
console.log("\nGeneral login-shell readiness (separate from native-service requirements):");
|
|
858
|
+
const generalReadinessOk = runChecks(generalDoctorChecks());
|
|
859
|
+
printOptionalDoctorChecks();
|
|
860
|
+
console.log("\nNative terminal runtime readiness:");
|
|
861
|
+
const nodePtyNativeModuleOk = printNodePtyNativeModuleCheck();
|
|
862
|
+
const nodePtySpawnHelperOk = nodePtyNativeModuleOk ? printNodePtyDarwinSpawnHelperCheck() : true;
|
|
863
|
+
let nativeServiceReport = null;
|
|
864
|
+
if (backend !== undefined) {
|
|
865
|
+
console.log("\nNative service plan checks (service-manager context):");
|
|
866
|
+
nativeServiceReport = await printNativeServiceDoctorChecks(backend);
|
|
867
|
+
}
|
|
868
|
+
if (supportsSystemdUserServices()) {
|
|
869
|
+
const linger = isLingerEnabled();
|
|
870
|
+
if (linger === true) {
|
|
871
|
+
console.log("✓ systemd user lingering enabled");
|
|
872
|
+
}
|
|
873
|
+
else if (linger === false) {
|
|
874
|
+
console.log("✗ systemd user lingering disabled");
|
|
875
|
+
console.log(` Recommended on servers: sudo loginctl enable-linger ${userInfo().username}`);
|
|
876
|
+
}
|
|
877
|
+
else {
|
|
878
|
+
console.log("? systemd user lingering unknown");
|
|
879
|
+
console.log(` Recommended on servers: sudo loginctl enable-linger ${userInfo().username}`);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
else if (backend?.kind === "launchd") {
|
|
883
|
+
console.log("- user services start at login with LaunchAgents");
|
|
884
|
+
}
|
|
885
|
+
else {
|
|
886
|
+
console.log(`- systemd user lingering skipped on ${platformLabel()}`);
|
|
887
|
+
}
|
|
888
|
+
const nativeServicePlanOk = nativeServiceReport?.ok ?? true;
|
|
889
|
+
const pathFailure = !generalReadinessOk || nativeServiceReport?.pathAdviceRecommended === true;
|
|
890
|
+
if (pathFailure) {
|
|
891
|
+
console.log("\nIf a command works in your terminal but fails in the service-manager check, compare the caller and manager contexts above.");
|
|
892
|
+
const adviceShell = nativeServiceReport?.pathAdviceRecommended === true && nativeServiceReport.adviceShell !== null
|
|
893
|
+
? nativeServiceReport.adviceShell
|
|
894
|
+
: detectServiceShell();
|
|
895
|
+
printPathSetupAdvice(adviceShell);
|
|
896
|
+
}
|
|
897
|
+
if (generalReadinessOk && backend === undefined) {
|
|
898
|
+
console.log(`\n${manualRunAdvice()}`);
|
|
899
|
+
}
|
|
900
|
+
if (doctorExitCode(generalReadinessOk, nativeServicePlanOk, nodePtyNativeModuleOk && nodePtySpawnHelperOk) !== 0)
|
|
901
|
+
process.exitCode = 1;
|
|
902
|
+
}
|
|
903
|
+
function printNodePtyNativeModuleCheck() {
|
|
904
|
+
const result = formatNodePtyNativeModuleCheck(checkNodePtyNativeModule());
|
|
905
|
+
for (const line of result.lines)
|
|
906
|
+
console.log(line);
|
|
907
|
+
return result.ok;
|
|
908
|
+
}
|
|
909
|
+
function printNodePtyDarwinSpawnHelperCheck() {
|
|
910
|
+
const result = formatNodePtyDarwinSpawnHelperCheck(checkNodePtyDarwinSpawnHelper());
|
|
911
|
+
for (const line of result.lines)
|
|
912
|
+
console.log(line);
|
|
913
|
+
return result.ok;
|
|
914
|
+
}
|
|
915
|
+
function isRecord(value) {
|
|
916
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
917
|
+
}
|
|
918
|
+
function help() {
|
|
919
|
+
console.log(`PI WEBUI
|
|
920
|
+
|
|
921
|
+
Usage:
|
|
922
|
+
pi-webui install [--dev] [--host 127.0.0.1] [--port ${String(DEFAULT_PORT)}] [--config ~/.config/pi-webui/config.json]
|
|
923
|
+
pi-webui uninstall
|
|
924
|
+
pi-webui start|stop|restart|status|logs
|
|
925
|
+
pi-webui doctor
|
|
926
|
+
pi-webui version
|
|
927
|
+
|
|
928
|
+
Recommended install:
|
|
929
|
+
npm install -g @hyperdreamer/pi-webui --allow-scripts=node-pty
|
|
930
|
+
pi-webui install
|
|
931
|
+
|
|
932
|
+
Development service install from a checkout:
|
|
933
|
+
pi-webui install --dev
|
|
934
|
+
`);
|
|
935
|
+
}
|
|
936
|
+
async function main() {
|
|
937
|
+
const [command = "help", ...args] = process.argv.slice(2);
|
|
938
|
+
if (command === "install")
|
|
939
|
+
await install(args);
|
|
940
|
+
else if (command === "uninstall")
|
|
941
|
+
await uninstall();
|
|
942
|
+
else if (command === "start" || command === "stop" || command === "restart" || command === "status")
|
|
943
|
+
serviceAction(command);
|
|
944
|
+
else if (command === "logs")
|
|
945
|
+
logs();
|
|
946
|
+
else if (command === "doctor")
|
|
947
|
+
await doctor();
|
|
948
|
+
else if (command === "version")
|
|
949
|
+
await printPiWebUiVersionReport();
|
|
950
|
+
else if (command === "--version" || command === "-v")
|
|
951
|
+
console.log(packageVersion());
|
|
952
|
+
else if (command === "help" || command === "--help" || command === "-h")
|
|
953
|
+
help();
|
|
954
|
+
else
|
|
955
|
+
throw new Error(`Unknown command: ${command}`);
|
|
956
|
+
}
|
|
957
|
+
export function isCliEntrypoint(entrypoint = process.argv[1], modulePath = fileURLToPath(import.meta.url)) {
|
|
958
|
+
if (entrypoint === undefined)
|
|
959
|
+
return false;
|
|
960
|
+
if (entrypoint === modulePath)
|
|
961
|
+
return true;
|
|
962
|
+
try {
|
|
963
|
+
return realpathSync(entrypoint) === realpathSync(modulePath);
|
|
964
|
+
}
|
|
965
|
+
catch {
|
|
966
|
+
return false;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
if (isCliEntrypoint()) {
|
|
970
|
+
main().catch((error) => {
|
|
971
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
972
|
+
process.exit(1);
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
//# sourceMappingURL=cli.js.map
|