@chainingintention/pi-web-cn 1.202606.3
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 +21 -0
- package/README.md +364 -0
- package/dist/cli.js +960 -0
- package/dist/cli.js.map +1 -0
- package/dist/client/apple-touch-icon.png +0 -0
- package/dist/client/assets/CodeViewer-B4nxYc0g.js +4 -0
- package/dist/client/assets/TerminalPanel-htr2dU1I.js +122 -0
- package/dist/client/assets/index-BjUH4a8R.js +1994 -0
- package/dist/client/assets/vendor-editor-core-B4Sq6exx.js +12 -0
- package/dist/client/assets/vendor-editor-languages-DznYbTkJ.js +26 -0
- package/dist/client/assets/vendor-editor-legacy-B4QLsWF8.js +1 -0
- package/dist/client/assets/vendor-terminal-DDGTF8rc.css +1 -0
- package/dist/client/assets/vendor-terminal-DjQ08hXu.js +16 -0
- package/dist/client/favicon.svg +11 -0
- package/dist/client/index.html +60 -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 +154 -0
- package/dist/config.js.map +1 -0
- package/dist/pi-web-plugins/info/package.json +9 -0
- package/dist/pi-web-plugins/info/pi-web-plugin.js +51 -0
- package/dist/pi-web-plugins/updates/package.json +9 -0
- package/dist/pi-web-plugins/updates/pi-web-plugin.js +181 -0
- package/dist/pi-web-plugins/workspace-tasks/config.js +91 -0
- package/dist/pi-web-plugins/workspace-tasks/package.json +9 -0
- package/dist/pi-web-plugins/workspace-tasks/pi-web-plugin.js +48 -0
- package/dist/pi-web-plugins/workspace-tasks/taskRunner.js +12 -0
- package/dist/pi-web-plugins/workspace-tasks/tasksPanelElement.js +292 -0
- package/dist/pi-web-plugins/workspace-tasks/workspaceTasksClient.js +47 -0
- package/dist/piWebVersionReport.js +221 -0
- package/dist/piWebVersionReport.js.map +1 -0
- package/dist/plugin-api/unstable.d.ts +22 -0
- package/dist/plugin-api.d.ts +163 -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 +115 -0
- package/dist/server/app.js.map +1 -0
- package/dist/server/configRoutes.js +123 -0
- package/dist/server/configRoutes.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 +119 -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 +134 -0
- package/dist/server/machines/machineClient.js.map +1 -0
- package/dist/server/machines/machineProxyRoutes.js +92 -0
- package/dist/server/machines/machineProxyRoutes.js.map +1 -0
- package/dist/server/machines/machineRoutes.js +50 -0
- package/dist/server/machines/machineRoutes.js.map +1 -0
- package/dist/server/machines/machineService.js +168 -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/piWebPluginService.js +235 -0
- package/dist/server/piWebPluginService.js.map +1 -0
- package/dist/server/piWebStatus.js +462 -0
- package/dist/server/piWebStatus.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 +39 -0
- package/dist/server/realtime/sessionEventHub.js.map +1 -0
- package/dist/server/sessiond/sessionProxyRoutes.js +60 -0
- package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -0
- package/dist/server/sessiond.js +61 -0
- package/dist/server/sessiond.js.map +1 -0
- package/dist/server/sessions/authProviderOptions.js +56 -0
- package/dist/server/sessions/authProviderOptions.js.map +1 -0
- package/dist/server/sessions/authRoutes.js +59 -0
- package/dist/server/sessions/authRoutes.js.map +1 -0
- package/dist/server/sessions/authService.js +74 -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 +219 -0
- package/dist/server/sessions/oauthLoginFlowService.js.map +1 -0
- package/dist/server/sessions/piSessionService.js +1054 -0
- package/dist/server/sessions/piSessionService.js.map +1 -0
- package/dist/server/sessions/sessionArchiveStore.js +216 -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/sessionCommandService.js +234 -0
- package/dist/server/sessions/sessionCommandService.js.map +1 -0
- package/dist/server/sessions/sessionNameGenerator.js +68 -0
- package/dist/server/sessions/sessionNameGenerator.js.map +1 -0
- package/dist/server/sessions/sessionRoutes.js +184 -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/storage/projectStore.js +88 -0
- package/dist/server/storage/projectStore.js.map +1 -0
- package/dist/server/terminalProxyRoutes.js +130 -0
- package/dist/server/terminalProxyRoutes.js.map +1 -0
- package/dist/server/terminals/terminalRoutes.js +138 -0
- package/dist/server/terminals/terminalRoutes.js.map +1 -0
- package/dist/server/terminals/terminalService.js +293 -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/workspaceExplorerRoutes.js +42 -0
- package/dist/server/workspaceExplorerRoutes.js.map +1 -0
- package/dist/server/workspaces/fileContentService.js +70 -0
- package/dist/server/workspaces/fileContentService.js.map +1 -0
- package/dist/server/workspaces/fileSuggestions.js +148 -0
- package/dist/server/workspaces/fileSuggestions.js.map +1 -0
- package/dist/server/workspaces/fileTreeService.js +26 -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/pathSafety.js +45 -0
- package/dist/server/workspaces/pathSafety.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/workspaceService.js +39 -0
- package/dist/server/workspaces/workspaceService.js.map +1 -0
- package/dist/sessiond/config.js +9 -0
- package/dist/sessiond/config.js.map +1 -0
- package/dist/sessiond/sessionDaemonClient.js +65 -0
- package/dist/sessiond/sessionDaemonClient.js.map +1 -0
- package/dist/shared/activity.js +26 -0
- package/dist/shared/activity.js.map +1 -0
- package/dist/shared/apiTypes.d.ts +464 -0
- package/dist/shared/apiTypes.js +2 -0
- package/dist/shared/apiTypes.js.map +1 -0
- package/dist/shared/federatedRoutes.js +57 -0
- package/dist/shared/federatedRoutes.js.map +1 -0
- package/dist/shared/piWebStatusParsing.js +62 -0
- package/dist/shared/piWebStatusParsing.js.map +1 -0
- package/dist/shared/pluginIds.js +5 -0
- package/dist/shared/pluginIds.js.map +1 -0
- package/dist/shared/workspaceFiles.js +3 -0
- package/dist/shared/workspaceFiles.js.map +1 -0
- package/docs/assets/favicon.svg +11 -0
- package/docs/assets/pi-web-banner.png +0 -0
- package/docs/assets/pi-web-demo.gif +0 -0
- package/docs/assets/pi-web-demo.webm +0 -0
- package/docs/plugins.md +762 -0
- package/extensions/pi-web.ts +133 -0
- package/install.sh +5 -0
- package/package.json +127 -0
- package/plugin-api/unstable.d.ts +1 -0
- package/plugin-api.d.ts +1 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,960 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { existsSync, readFileSync } 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 { defaultPiWebConfigPath, defaultPiWebDataDir, examplePiWebConfig } from "./config.js";
|
|
9
|
+
import { packageVersion, printPiWebVersionReport } from "./piWebVersionReport.js";
|
|
10
|
+
import { checkNodePtyDarwinSpawnHelper, formatNodePtyDarwinSpawnHelperCheck } from "./server/diagnostics/nodePtySpawnHelper.js";
|
|
11
|
+
const PI_WEB_PACKAGE_NAME = "@chainingintention/pi-web-cn";
|
|
12
|
+
const systemdServiceDir = join(homedir(), ".config", "systemd", "user");
|
|
13
|
+
const launchdServiceDir = join(homedir(), "Library", "LaunchAgents");
|
|
14
|
+
const logDir = join(defaultPiWebDataDir(), "logs");
|
|
15
|
+
const sessiondServiceName = "pi-web-sessiond.service";
|
|
16
|
+
const webServiceName = "pi-web.service";
|
|
17
|
+
const uiDevServiceName = "pi-web-ui-dev.service";
|
|
18
|
+
const serviceRefs = {
|
|
19
|
+
sessiond: {
|
|
20
|
+
id: "sessiond",
|
|
21
|
+
systemdName: sessiondServiceName,
|
|
22
|
+
launchdLabel: "com.pi-web.sessiond",
|
|
23
|
+
launchdPlistName: "com.pi-web.sessiond.plist",
|
|
24
|
+
logName: "sessiond.log",
|
|
25
|
+
},
|
|
26
|
+
web: {
|
|
27
|
+
id: "web",
|
|
28
|
+
systemdName: webServiceName,
|
|
29
|
+
launchdLabel: "com.pi-web.web",
|
|
30
|
+
launchdPlistName: "com.pi-web.web.plist",
|
|
31
|
+
logName: "web.log",
|
|
32
|
+
},
|
|
33
|
+
uiDev: {
|
|
34
|
+
id: "uiDev",
|
|
35
|
+
systemdName: uiDevServiceName,
|
|
36
|
+
launchdLabel: "com.pi-web.ui-dev",
|
|
37
|
+
launchdPlistName: "com.pi-web.ui-dev.plist",
|
|
38
|
+
logName: "ui-dev.log",
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
const productionServiceIds = ["sessiond", "web"];
|
|
42
|
+
const startServiceOrder = ["sessiond", "web", "uiDev"];
|
|
43
|
+
const stopServiceOrder = ["web", "uiDev", "sessiond"];
|
|
44
|
+
function platformLabel() {
|
|
45
|
+
if (process.platform === "darwin")
|
|
46
|
+
return "macOS";
|
|
47
|
+
if (process.platform === "linux")
|
|
48
|
+
return "Linux";
|
|
49
|
+
if (process.platform === "win32")
|
|
50
|
+
return "Windows";
|
|
51
|
+
return process.platform;
|
|
52
|
+
}
|
|
53
|
+
function currentServiceBackend() {
|
|
54
|
+
if (process.platform === "linux")
|
|
55
|
+
return { kind: "systemd", label: "systemd user services" };
|
|
56
|
+
if (process.platform === "darwin")
|
|
57
|
+
return { kind: "launchd", label: "LaunchAgents" };
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
function requireServiceBackend(command) {
|
|
61
|
+
const backend = currentServiceBackend();
|
|
62
|
+
if (backend !== undefined)
|
|
63
|
+
return backend;
|
|
64
|
+
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()}`);
|
|
65
|
+
}
|
|
66
|
+
function supportsSystemdUserServices() {
|
|
67
|
+
return currentServiceBackend()?.kind === "systemd";
|
|
68
|
+
}
|
|
69
|
+
function manualRunAdvice() {
|
|
70
|
+
return [
|
|
71
|
+
"Run PI WEB manually from a checkout:",
|
|
72
|
+
" npm run start:sessiond",
|
|
73
|
+
" PI_WEB_PORT=8504 npm start",
|
|
74
|
+
"",
|
|
75
|
+
"For development in one terminal:",
|
|
76
|
+
" npm run dev",
|
|
77
|
+
"",
|
|
78
|
+
"For split development, keep sessiond separate and run web/API plus Vite UI separately:",
|
|
79
|
+
" npm run dev:sessiond",
|
|
80
|
+
" npm run dev:web",
|
|
81
|
+
" npm run dev:client",
|
|
82
|
+
].join("\n");
|
|
83
|
+
}
|
|
84
|
+
function run(command, args, options = {}) {
|
|
85
|
+
const result = spawnSync(command, args, { stdio: "inherit" });
|
|
86
|
+
const status = result.status ?? 1;
|
|
87
|
+
if (options.check === true && status !== 0)
|
|
88
|
+
process.exit(status);
|
|
89
|
+
return status;
|
|
90
|
+
}
|
|
91
|
+
function outputText(value) {
|
|
92
|
+
return typeof value === "string" ? value : "";
|
|
93
|
+
}
|
|
94
|
+
function capture(command, args) {
|
|
95
|
+
const result = spawnSync(command, args, { encoding: "utf8" });
|
|
96
|
+
const errorMessage = result.error instanceof Error ? result.error.message : "";
|
|
97
|
+
const stderr = outputText(result.stderr);
|
|
98
|
+
return { status: result.status ?? 1, stdout: outputText(result.stdout), stderr: stderr === "" ? errorMessage : stderr };
|
|
99
|
+
}
|
|
100
|
+
function runQuiet(command, args) {
|
|
101
|
+
return capture(command, args).status;
|
|
102
|
+
}
|
|
103
|
+
function hasCommand(command) {
|
|
104
|
+
return capture("/usr/bin/env", ["sh", "-c", `command -v ${command}`]).status === 0;
|
|
105
|
+
}
|
|
106
|
+
function isLingerEnabled() {
|
|
107
|
+
if (!hasCommand("loginctl"))
|
|
108
|
+
return undefined;
|
|
109
|
+
const result = capture("loginctl", ["show-user", userInfo().username, "-p", "Linger"]);
|
|
110
|
+
if (result.status !== 0)
|
|
111
|
+
return undefined;
|
|
112
|
+
const value = result.stdout.trim();
|
|
113
|
+
if (value === "Linger=yes")
|
|
114
|
+
return true;
|
|
115
|
+
if (value === "Linger=no")
|
|
116
|
+
return false;
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
function parseInstallOptions(args) {
|
|
120
|
+
const options = { host: "127.0.0.1", port: "8504", mode: "production" };
|
|
121
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
122
|
+
const arg = args[i];
|
|
123
|
+
if (arg === undefined)
|
|
124
|
+
continue;
|
|
125
|
+
if (arg === "--host") {
|
|
126
|
+
const value = args[i + 1];
|
|
127
|
+
if (value === undefined)
|
|
128
|
+
throw new Error("--host requires a value");
|
|
129
|
+
options.host = value;
|
|
130
|
+
i += 1;
|
|
131
|
+
}
|
|
132
|
+
else if (arg.startsWith("--host=")) {
|
|
133
|
+
options.host = arg.slice("--host=".length);
|
|
134
|
+
}
|
|
135
|
+
else if (arg === "--port") {
|
|
136
|
+
const value = args[i + 1];
|
|
137
|
+
if (value === undefined)
|
|
138
|
+
throw new Error("--port requires a value");
|
|
139
|
+
options.port = value;
|
|
140
|
+
i += 1;
|
|
141
|
+
}
|
|
142
|
+
else if (arg.startsWith("--port=")) {
|
|
143
|
+
options.port = arg.slice("--port=".length);
|
|
144
|
+
}
|
|
145
|
+
else if (arg === "--config") {
|
|
146
|
+
const value = args[i + 1];
|
|
147
|
+
if (value === undefined)
|
|
148
|
+
throw new Error("--config requires a value");
|
|
149
|
+
options.config = value;
|
|
150
|
+
i += 1;
|
|
151
|
+
}
|
|
152
|
+
else if (arg.startsWith("--config=")) {
|
|
153
|
+
options.config = arg.slice("--config=".length);
|
|
154
|
+
}
|
|
155
|
+
else if (arg === "--dev") {
|
|
156
|
+
options.mode = "dev";
|
|
157
|
+
}
|
|
158
|
+
else if (arg === "--user-systemd") {
|
|
159
|
+
// Accepted for backwards-compatible readability; PI WEB chooses the native user service backend automatically.
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
throw new Error(`Unknown install option: ${arg}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return options;
|
|
166
|
+
}
|
|
167
|
+
function shellSingleQuote(value) {
|
|
168
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
169
|
+
}
|
|
170
|
+
function fishSingleQuote(value) {
|
|
171
|
+
return `'${value.replaceAll("\\", "\\\\").replaceAll("'", "\\'")}'`;
|
|
172
|
+
}
|
|
173
|
+
function systemdEscape(value) {
|
|
174
|
+
return value.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
|
|
175
|
+
}
|
|
176
|
+
function systemdQuotedValue(value) {
|
|
177
|
+
return `"${systemdEscape(value)}"`;
|
|
178
|
+
}
|
|
179
|
+
function xmlEscape(value) {
|
|
180
|
+
return value
|
|
181
|
+
.replaceAll("&", "&")
|
|
182
|
+
.replaceAll("<", "<")
|
|
183
|
+
.replaceAll(">", ">")
|
|
184
|
+
.replaceAll('"', """)
|
|
185
|
+
.replaceAll("'", "'");
|
|
186
|
+
}
|
|
187
|
+
function packageRootPath() {
|
|
188
|
+
return dirname(dirname(fileURLToPath(import.meta.url)));
|
|
189
|
+
}
|
|
190
|
+
function packageEntrypointPath(name) {
|
|
191
|
+
return join(packageRootPath(), "dist", "server", name === "server" ? "index.js" : "sessiond.js");
|
|
192
|
+
}
|
|
193
|
+
function detectServiceShell() {
|
|
194
|
+
const userShell = userInfo().shell ?? undefined;
|
|
195
|
+
const envShell = process.env["SHELL"]?.trim();
|
|
196
|
+
const detected = envShell === undefined || envShell === "" ? userShell : envShell;
|
|
197
|
+
const name = basename(detected ?? "").replace(/^-/, "");
|
|
198
|
+
if (name === "bash" || name === "zsh" || name === "fish") {
|
|
199
|
+
return { name, executable: detected ?? name, detected: detected ?? name, fallback: false };
|
|
200
|
+
}
|
|
201
|
+
return { name: "bash", executable: "bash", ...(detected === undefined ? {} : { detected }), fallback: true };
|
|
202
|
+
}
|
|
203
|
+
function serviceShellCommand(command, cwd) {
|
|
204
|
+
const fullCommand = cwd === undefined ? command : `cd ${serviceShellQuote(cwd)} && ${command}`;
|
|
205
|
+
return ["/usr/bin/env", detectServiceShell().executable, "-lc", fullCommand];
|
|
206
|
+
}
|
|
207
|
+
function serviceShellExecPrefix() {
|
|
208
|
+
return `/usr/bin/env ${detectServiceShell().executable} -lc`;
|
|
209
|
+
}
|
|
210
|
+
function serviceShellQuote(value) {
|
|
211
|
+
return detectServiceShell().name === "fish" ? fishSingleQuote(value) : shellSingleQuote(value);
|
|
212
|
+
}
|
|
213
|
+
function systemdServiceShellQuote(value) {
|
|
214
|
+
return serviceShellQuote(value.replaceAll("%", "%%").replaceAll("$", "$$"));
|
|
215
|
+
}
|
|
216
|
+
function checkSucceeds(command) {
|
|
217
|
+
const [bin, ...args] = command;
|
|
218
|
+
return bin !== undefined && capture(bin, args).status === 0;
|
|
219
|
+
}
|
|
220
|
+
function serviceShellCanFindCommand(command, backend) {
|
|
221
|
+
if (!checkSucceeds(serviceShellCommand(commandCheck(command))))
|
|
222
|
+
return false;
|
|
223
|
+
if (backend.kind === "systemd")
|
|
224
|
+
return checkSucceeds(systemdUserServiceShellCommand(commandCheck(command)));
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
function readableFileCheck(path) {
|
|
228
|
+
const quoted = serviceShellQuote(path);
|
|
229
|
+
return `test -r ${quoted} && printf '%s\\n' ${quoted}`;
|
|
230
|
+
}
|
|
231
|
+
function commandExecutable(command, backend) {
|
|
232
|
+
const shell = serviceShellLabel();
|
|
233
|
+
const checks = [[`${shell} can find ${command}`, serviceShellCommand(commandCheck(command))]];
|
|
234
|
+
if (backend.kind === "systemd") {
|
|
235
|
+
checks.push([`systemd user ${shell} can find ${command}`, systemdUserServiceShellCommand(commandCheck(command))]);
|
|
236
|
+
}
|
|
237
|
+
return { command, checks };
|
|
238
|
+
}
|
|
239
|
+
function bundledExecutable(command, entrypointPath, backend) {
|
|
240
|
+
const shell = serviceShellLabel();
|
|
241
|
+
const check = readableFileCheck(entrypointPath);
|
|
242
|
+
const checks = [[`${shell} can access bundled ${command} entrypoint`, serviceShellCommand(check)]];
|
|
243
|
+
if (backend.kind === "systemd") {
|
|
244
|
+
checks.push([`systemd user ${shell} can access bundled ${command} entrypoint`, systemdUserServiceShellCommand(check)]);
|
|
245
|
+
}
|
|
246
|
+
return { command: `node ${serviceShellQuote(entrypointPath)}`, checks };
|
|
247
|
+
}
|
|
248
|
+
function serviceExecutable(envName, command, entrypointPath, backend) {
|
|
249
|
+
const configured = process.env[envName]?.trim();
|
|
250
|
+
if (configured !== undefined && configured !== "")
|
|
251
|
+
return { command: configured, checks: [] };
|
|
252
|
+
if (serviceShellCanFindCommand(command, backend))
|
|
253
|
+
return commandExecutable(command, backend);
|
|
254
|
+
if (existsSync(entrypointPath))
|
|
255
|
+
return bundledExecutable(command, entrypointPath, backend);
|
|
256
|
+
return commandExecutable(command, backend);
|
|
257
|
+
}
|
|
258
|
+
function resolveServiceExecutables(backend) {
|
|
259
|
+
return {
|
|
260
|
+
sessiond: serviceExecutable("PI_WEB_SESSIOND_EXEC", "pi-web-sessiond", packageEntrypointPath("sessiond"), backend),
|
|
261
|
+
web: serviceExecutable("PI_WEB_SERVER_EXEC", "pi-web-server", packageEntrypointPath("server"), backend),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function describeServiceShell() {
|
|
265
|
+
const shell = detectServiceShell();
|
|
266
|
+
if (shell.fallback) {
|
|
267
|
+
return shell.detected === undefined
|
|
268
|
+
? "could not detect a supported login shell; using bash"
|
|
269
|
+
: `detected ${shell.detected}; using bash because PI WEB currently supports bash, zsh, and fish`;
|
|
270
|
+
}
|
|
271
|
+
return shell.detected === undefined ? shell.name : `${shell.name} (${shell.detected})`;
|
|
272
|
+
}
|
|
273
|
+
function configEnvironment(options, configPath) {
|
|
274
|
+
return options.config === undefined ? {} : { PI_WEB_CONFIG: configPath };
|
|
275
|
+
}
|
|
276
|
+
function serviceRefList(ids) {
|
|
277
|
+
return ids.map((id) => serviceRefs[id]);
|
|
278
|
+
}
|
|
279
|
+
function allServiceRefs() {
|
|
280
|
+
return serviceRefList(["sessiond", "web", "uiDev"]);
|
|
281
|
+
}
|
|
282
|
+
function productionServiceRefs() {
|
|
283
|
+
return serviceRefList(productionServiceIds);
|
|
284
|
+
}
|
|
285
|
+
function orderServiceRefs(refs, order) {
|
|
286
|
+
const byId = new Map(refs.map((ref) => [ref.id, ref]));
|
|
287
|
+
return order.flatMap((id) => {
|
|
288
|
+
const ref = byId.get(id);
|
|
289
|
+
return ref === undefined ? [] : [ref];
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
function startOrder(refs) {
|
|
293
|
+
return orderServiceRefs(refs, startServiceOrder);
|
|
294
|
+
}
|
|
295
|
+
function stopOrder(refs) {
|
|
296
|
+
return orderServiceRefs(refs, stopServiceOrder);
|
|
297
|
+
}
|
|
298
|
+
function productionServiceDefinitions(options, configPath, executables) {
|
|
299
|
+
return [
|
|
300
|
+
{
|
|
301
|
+
...serviceRefs.sessiond,
|
|
302
|
+
description: "PI WEB session daemon",
|
|
303
|
+
shellCommand: `exec ${executables.sessiond.command}`,
|
|
304
|
+
restart: "on-failure",
|
|
305
|
+
environment: {},
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
...serviceRefs.web,
|
|
309
|
+
description: "PI WEB server",
|
|
310
|
+
shellCommand: `exec ${executables.web.command}`,
|
|
311
|
+
restart: "on-failure",
|
|
312
|
+
environment: configEnvironment(options, configPath),
|
|
313
|
+
after: ["sessiond"],
|
|
314
|
+
wants: ["sessiond"],
|
|
315
|
+
},
|
|
316
|
+
];
|
|
317
|
+
}
|
|
318
|
+
function devRootPath() {
|
|
319
|
+
return resolve(process.cwd());
|
|
320
|
+
}
|
|
321
|
+
function validateDevCheckout(root) {
|
|
322
|
+
const packageJsonPath = join(root, "package.json");
|
|
323
|
+
if (!existsSync(packageJsonPath)) {
|
|
324
|
+
throw new Error(`Development mode must be installed from a PI WEB checkout. Missing package.json: ${packageJsonPath}`);
|
|
325
|
+
}
|
|
326
|
+
const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
327
|
+
if (!isRecord(parsed) || parsed["name"] !== PI_WEB_PACKAGE_NAME) {
|
|
328
|
+
throw new Error(`Development mode must be installed from a PI WEB checkout. ${packageJsonPath} is not ${PI_WEB_PACKAGE_NAME}.`);
|
|
329
|
+
}
|
|
330
|
+
const scripts = parsed["scripts"];
|
|
331
|
+
if (!isRecord(scripts))
|
|
332
|
+
throw new Error(`Development mode requires npm scripts in ${packageJsonPath}.`);
|
|
333
|
+
const requiredScripts = ["start:sessiond", "dev:web", "dev:client"];
|
|
334
|
+
const missing = requiredScripts.filter((script) => typeof scripts[script] !== "string");
|
|
335
|
+
if (missing.length > 0)
|
|
336
|
+
throw new Error(`Development mode requires missing npm scripts: ${missing.join(", ")}.`);
|
|
337
|
+
}
|
|
338
|
+
function devServiceDefinitions(options, configPath, root) {
|
|
339
|
+
return [
|
|
340
|
+
{
|
|
341
|
+
...serviceRefs.sessiond,
|
|
342
|
+
description: "PI WEB session daemon (dev)",
|
|
343
|
+
shellCommand: "exec npm run start:sessiond",
|
|
344
|
+
restart: "never",
|
|
345
|
+
environment: {},
|
|
346
|
+
workingDirectory: root,
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
...serviceRefs.uiDev,
|
|
350
|
+
description: "PI WEB UI dev server",
|
|
351
|
+
shellCommand: `exec /usr/bin/env bash -c ${serviceShellQuote('trap "kill 0" EXIT; npm run dev:web & npm run dev:client & wait')}`,
|
|
352
|
+
restart: "never",
|
|
353
|
+
environment: configEnvironment(options, configPath),
|
|
354
|
+
after: ["sessiond"],
|
|
355
|
+
wants: ["sessiond"],
|
|
356
|
+
workingDirectory: root,
|
|
357
|
+
},
|
|
358
|
+
];
|
|
359
|
+
}
|
|
360
|
+
function dependencyLine(name, ids) {
|
|
361
|
+
if (ids === undefined || ids.length === 0)
|
|
362
|
+
return "";
|
|
363
|
+
return `${name}=${ids.map((id) => serviceRefs[id].systemdName).join(" ")}\n`;
|
|
364
|
+
}
|
|
365
|
+
function environmentLines(environment) {
|
|
366
|
+
return Object.entries(environment)
|
|
367
|
+
.map(([key, value]) => `Environment="${key}=${systemdEscape(value)}"\n`)
|
|
368
|
+
.join("");
|
|
369
|
+
}
|
|
370
|
+
function systemdUnit(service) {
|
|
371
|
+
const workingDirectory = service.workingDirectory === undefined ? "" : `WorkingDirectory=${systemdQuotedValue(service.workingDirectory)}\n`;
|
|
372
|
+
const restart = service.restart === "on-failure" ? "Restart=on-failure\nRestartSec=2\n" : "Restart=no\n";
|
|
373
|
+
return `[Unit]
|
|
374
|
+
Description=${service.description}
|
|
375
|
+
${dependencyLine("After", service.after)}${dependencyLine("Wants", service.wants)}
|
|
376
|
+
[Service]
|
|
377
|
+
Type=simple
|
|
378
|
+
${workingDirectory}${environmentLines(service.environment)}ExecStart=${serviceShellExecPrefix()} ${systemdServiceShellQuote(service.shellCommand)}
|
|
379
|
+
${restart}
|
|
380
|
+
[Install]
|
|
381
|
+
WantedBy=default.target
|
|
382
|
+
`;
|
|
383
|
+
}
|
|
384
|
+
function plistString(key, value, indent = " ") {
|
|
385
|
+
return `${indent}<key>${xmlEscape(key)}</key>\n${indent}<string>${xmlEscape(value)}</string>\n`;
|
|
386
|
+
}
|
|
387
|
+
function plistProgramArguments(service) {
|
|
388
|
+
const args = ["/usr/bin/env", detectServiceShell().executable, "-lc", service.shellCommand];
|
|
389
|
+
return ` <key>ProgramArguments</key>\n <array>\n${args.map((arg) => ` <string>${xmlEscape(arg)}</string>`).join("\n")}\n </array>\n`;
|
|
390
|
+
}
|
|
391
|
+
function plistEnvironment(environment) {
|
|
392
|
+
const entries = Object.entries(environment);
|
|
393
|
+
if (entries.length === 0)
|
|
394
|
+
return "";
|
|
395
|
+
return ` <key>EnvironmentVariables</key>\n <dict>\n${entries.map(([key, value]) => plistString(key, value, " ")).join("")} </dict>\n`;
|
|
396
|
+
}
|
|
397
|
+
function launchdLogPath(ref) {
|
|
398
|
+
return join(logDir, ref.logName);
|
|
399
|
+
}
|
|
400
|
+
function launchdPlist(service) {
|
|
401
|
+
const workingDirectory = service.workingDirectory === undefined ? "" : plistString("WorkingDirectory", service.workingDirectory);
|
|
402
|
+
const keepAlive = service.restart === "on-failure" ? " <key>KeepAlive</key>\n <dict>\n <key>SuccessfulExit</key>\n <false/>\n </dict>\n" : "";
|
|
403
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
404
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
405
|
+
<plist version="1.0">
|
|
406
|
+
<dict>
|
|
407
|
+
${plistString("Label", service.launchdLabel)}${plistProgramArguments(service)}${workingDirectory}${plistEnvironment(service.environment)} <key>RunAtLoad</key>
|
|
408
|
+
<true/>
|
|
409
|
+
${keepAlive}${plistString("StandardOutPath", launchdLogPath(service))}${plistString("StandardErrorPath", launchdLogPath(service))}</dict>
|
|
410
|
+
</plist>
|
|
411
|
+
`;
|
|
412
|
+
}
|
|
413
|
+
async function writeInitialConfig(options) {
|
|
414
|
+
const configPath = options.config === undefined ? defaultPiWebConfigPath() : resolve(options.config);
|
|
415
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
416
|
+
if (!existsSync(configPath)) {
|
|
417
|
+
await writeFile(configPath, examplePiWebConfig({ host: options.host, port: Number(options.port) }));
|
|
418
|
+
}
|
|
419
|
+
return configPath;
|
|
420
|
+
}
|
|
421
|
+
function systemdServicePath(ref) {
|
|
422
|
+
return join(systemdServiceDir, ref.systemdName);
|
|
423
|
+
}
|
|
424
|
+
function launchdPlistPath(ref) {
|
|
425
|
+
return join(launchdServiceDir, ref.launchdPlistName);
|
|
426
|
+
}
|
|
427
|
+
function serviceFilePath(backend, ref) {
|
|
428
|
+
return backend.kind === "systemd" ? systemdServicePath(ref) : launchdPlistPath(ref);
|
|
429
|
+
}
|
|
430
|
+
function serviceFileExists(backend, ref) {
|
|
431
|
+
return existsSync(serviceFilePath(backend, ref));
|
|
432
|
+
}
|
|
433
|
+
function installedServiceIds(backend) {
|
|
434
|
+
return new Set(allServiceRefs().filter((ref) => serviceFileExists(backend, ref)).map((ref) => ref.id));
|
|
435
|
+
}
|
|
436
|
+
function installedServiceRefs(backend) {
|
|
437
|
+
const installed = startOrder(allServiceRefs().filter((ref) => serviceFileExists(backend, ref)));
|
|
438
|
+
return installed.length === 0 ? productionServiceRefs() : installed;
|
|
439
|
+
}
|
|
440
|
+
async function installSystemdServices(services) {
|
|
441
|
+
const selected = new Set(services.map((service) => service.id));
|
|
442
|
+
const obsolete = stopOrder(allServiceRefs().filter((ref) => !selected.has(ref.id)));
|
|
443
|
+
for (const ref of obsolete) {
|
|
444
|
+
runQuiet("systemctl", ["--user", "disable", "--now", ref.systemdName]);
|
|
445
|
+
await rm(systemdServicePath(ref), { force: true });
|
|
446
|
+
}
|
|
447
|
+
await mkdir(systemdServiceDir, { recursive: true });
|
|
448
|
+
for (const service of services) {
|
|
449
|
+
await writeFile(systemdServicePath(service), systemdUnit(service));
|
|
450
|
+
}
|
|
451
|
+
const names = services.map((service) => service.systemdName);
|
|
452
|
+
run("systemctl", ["--user", "daemon-reload"], { check: true });
|
|
453
|
+
run("systemctl", ["--user", "enable", ...names], { check: true });
|
|
454
|
+
run("systemctl", ["--user", "restart", ...names], { check: true });
|
|
455
|
+
}
|
|
456
|
+
function launchdDomain() {
|
|
457
|
+
return `gui/${String(userInfo().uid)}`;
|
|
458
|
+
}
|
|
459
|
+
function launchdServiceTarget(ref) {
|
|
460
|
+
return `${launchdDomain()}/${ref.launchdLabel}`;
|
|
461
|
+
}
|
|
462
|
+
function launchdIsLoaded(ref) {
|
|
463
|
+
return capture("launchctl", ["print", launchdServiceTarget(ref)]).status === 0;
|
|
464
|
+
}
|
|
465
|
+
function launchdBootout(ref) {
|
|
466
|
+
runQuiet("launchctl", ["bootout", launchdServiceTarget(ref)]);
|
|
467
|
+
}
|
|
468
|
+
function launchdBootstrap(ref) {
|
|
469
|
+
run("launchctl", ["bootstrap", launchdDomain(), launchdPlistPath(ref)], { check: true });
|
|
470
|
+
run("launchctl", ["enable", launchdServiceTarget(ref)], { check: true });
|
|
471
|
+
}
|
|
472
|
+
function launchdStart(ref) {
|
|
473
|
+
if (!launchdIsLoaded(ref))
|
|
474
|
+
launchdBootstrap(ref);
|
|
475
|
+
run("launchctl", ["kickstart", launchdServiceTarget(ref)], { check: true });
|
|
476
|
+
}
|
|
477
|
+
async function installLaunchdServices(services) {
|
|
478
|
+
const selected = new Set(services.map((service) => service.id));
|
|
479
|
+
await mkdir(launchdServiceDir, { recursive: true });
|
|
480
|
+
await mkdir(logDir, { recursive: true });
|
|
481
|
+
for (const ref of stopOrder(allServiceRefs()))
|
|
482
|
+
launchdBootout(ref);
|
|
483
|
+
for (const ref of allServiceRefs().filter((candidate) => !selected.has(candidate.id))) {
|
|
484
|
+
await rm(launchdPlistPath(ref), { force: true });
|
|
485
|
+
}
|
|
486
|
+
for (const service of services) {
|
|
487
|
+
await writeFile(launchdPlistPath(service), launchdPlist(service));
|
|
488
|
+
}
|
|
489
|
+
for (const service of services)
|
|
490
|
+
launchdStart(service);
|
|
491
|
+
}
|
|
492
|
+
async function installNativeServices(backend, services) {
|
|
493
|
+
if (backend.kind === "systemd")
|
|
494
|
+
await installSystemdServices(services);
|
|
495
|
+
else
|
|
496
|
+
await installLaunchdServices(services);
|
|
497
|
+
}
|
|
498
|
+
async function uninstallSystemdServices() {
|
|
499
|
+
for (const ref of stopOrder(allServiceRefs())) {
|
|
500
|
+
runQuiet("systemctl", ["--user", "disable", "--now", ref.systemdName]);
|
|
501
|
+
await rm(systemdServicePath(ref), { force: true });
|
|
502
|
+
}
|
|
503
|
+
runQuiet("systemctl", ["--user", "daemon-reload"]);
|
|
504
|
+
}
|
|
505
|
+
async function uninstallLaunchdServices() {
|
|
506
|
+
for (const ref of stopOrder(allServiceRefs())) {
|
|
507
|
+
launchdBootout(ref);
|
|
508
|
+
await rm(launchdPlistPath(ref), { force: true });
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
async function uninstallNativeServices(backend) {
|
|
512
|
+
if (backend.kind === "systemd")
|
|
513
|
+
await uninstallSystemdServices();
|
|
514
|
+
else
|
|
515
|
+
await uninstallLaunchdServices();
|
|
516
|
+
}
|
|
517
|
+
function serviceDisplayName(ref) {
|
|
518
|
+
if (ref.id === "sessiond")
|
|
519
|
+
return "session daemon";
|
|
520
|
+
if (ref.id === "uiDev")
|
|
521
|
+
return "UI/API dev server";
|
|
522
|
+
return "web server";
|
|
523
|
+
}
|
|
524
|
+
function statusServiceRefs(backend) {
|
|
525
|
+
const ids = installedServiceIds(backend);
|
|
526
|
+
if (ids.size === 0)
|
|
527
|
+
return [];
|
|
528
|
+
if (ids.has("web") && ids.has("uiDev"))
|
|
529
|
+
return startOrder(allServiceRefs());
|
|
530
|
+
if (ids.has("uiDev"))
|
|
531
|
+
return serviceRefList(["sessiond", "uiDev"]);
|
|
532
|
+
if (ids.has("web"))
|
|
533
|
+
return productionServiceRefs();
|
|
534
|
+
return serviceRefList(["sessiond"]);
|
|
535
|
+
}
|
|
536
|
+
function serviceInstallMode(backend) {
|
|
537
|
+
const ids = installedServiceIds(backend);
|
|
538
|
+
if (ids.size === 0)
|
|
539
|
+
return "not installed";
|
|
540
|
+
const hasSessiond = ids.has("sessiond");
|
|
541
|
+
const hasWeb = ids.has("web");
|
|
542
|
+
const hasUiDev = ids.has("uiDev");
|
|
543
|
+
if (hasWeb && hasUiDev)
|
|
544
|
+
return "mixed";
|
|
545
|
+
if (hasUiDev)
|
|
546
|
+
return hasSessiond ? "development" : "development (incomplete)";
|
|
547
|
+
if (hasWeb)
|
|
548
|
+
return hasSessiond ? "production" : "production (incomplete)";
|
|
549
|
+
return "partial";
|
|
550
|
+
}
|
|
551
|
+
function makeServiceRuntimeStatus(ref, health, detail, target, filePath, pid) {
|
|
552
|
+
return {
|
|
553
|
+
ref,
|
|
554
|
+
health,
|
|
555
|
+
detail,
|
|
556
|
+
target,
|
|
557
|
+
filePath,
|
|
558
|
+
...(pid === undefined ? {} : { pid }),
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
function firstOutputLine(...values) {
|
|
562
|
+
for (const value of values) {
|
|
563
|
+
const line = value.trim().split("\n").find((candidate) => candidate.trim() !== "");
|
|
564
|
+
if (line !== undefined)
|
|
565
|
+
return line.trim();
|
|
566
|
+
}
|
|
567
|
+
return undefined;
|
|
568
|
+
}
|
|
569
|
+
function systemdMainPid(ref) {
|
|
570
|
+
const result = capture("systemctl", ["--user", "--no-pager", "show", ref.systemdName, "--property=MainPID", "--value"]);
|
|
571
|
+
if (result.status !== 0)
|
|
572
|
+
return undefined;
|
|
573
|
+
const value = result.stdout.trim();
|
|
574
|
+
return value === "" || value === "0" ? undefined : value;
|
|
575
|
+
}
|
|
576
|
+
function systemdRuntimeStatus(backend, ref) {
|
|
577
|
+
const target = ref.systemdName;
|
|
578
|
+
const filePath = serviceFilePath(backend, ref);
|
|
579
|
+
if (!serviceFileExists(backend, ref))
|
|
580
|
+
return makeServiceRuntimeStatus(ref, "not-installed", "not installed", target, filePath);
|
|
581
|
+
const result = capture("systemctl", ["--user", "--no-pager", "is-active", target]);
|
|
582
|
+
const state = firstOutputLine(result.stdout, result.stderr) ?? "unknown";
|
|
583
|
+
if (result.status === 0 && state === "active")
|
|
584
|
+
return makeServiceRuntimeStatus(ref, "running", "running", target, filePath, systemdMainPid(ref));
|
|
585
|
+
return makeServiceRuntimeStatus(ref, state === "unknown" ? "unknown" : "stopped", state, target, filePath);
|
|
586
|
+
}
|
|
587
|
+
function parseLaunchdField(output, field) {
|
|
588
|
+
const match = new RegExp(`^\\s*${field}\\s=\\s(.+)$`, "m").exec(output);
|
|
589
|
+
return match?.[1]?.trim();
|
|
590
|
+
}
|
|
591
|
+
function launchdRuntimeStatus(backend, ref) {
|
|
592
|
+
const target = launchdServiceTarget(ref);
|
|
593
|
+
const filePath = serviceFilePath(backend, ref);
|
|
594
|
+
if (!serviceFileExists(backend, ref))
|
|
595
|
+
return makeServiceRuntimeStatus(ref, "not-installed", "not installed", target, filePath);
|
|
596
|
+
const result = capture("launchctl", ["print", target]);
|
|
597
|
+
if (result.status !== 0) {
|
|
598
|
+
return makeServiceRuntimeStatus(ref, "stopped", firstOutputLine(result.stderr, result.stdout) ?? "not loaded", target, filePath);
|
|
599
|
+
}
|
|
600
|
+
const state = parseLaunchdField(result.stdout, "state") ?? "unknown";
|
|
601
|
+
const pid = parseLaunchdField(result.stdout, "pid");
|
|
602
|
+
const health = state === "running" ? "running" : state === "unknown" ? "unknown" : "stopped";
|
|
603
|
+
return makeServiceRuntimeStatus(ref, health, state === "running" ? "running" : state, target, filePath, pid);
|
|
604
|
+
}
|
|
605
|
+
function runtimeStatus(backend, ref) {
|
|
606
|
+
return backend.kind === "systemd" ? systemdRuntimeStatus(backend, ref) : launchdRuntimeStatus(backend, ref);
|
|
607
|
+
}
|
|
608
|
+
function printServiceStatus(status) {
|
|
609
|
+
const icon = status.health === "running" ? "✓" : "✗";
|
|
610
|
+
const pid = status.pid === undefined ? "" : `, pid ${status.pid}`;
|
|
611
|
+
console.log(`${icon} ${serviceDisplayName(status.ref)}: ${status.detail} (${status.target}${pid})`);
|
|
612
|
+
if (status.health === "not-installed")
|
|
613
|
+
console.log(` missing service file: ${status.filePath}`);
|
|
614
|
+
}
|
|
615
|
+
function printServiceStatusReport(backend) {
|
|
616
|
+
const refs = statusServiceRefs(backend);
|
|
617
|
+
console.log(`PI WEB services: ${serviceInstallMode(backend)} (${backend.label})`);
|
|
618
|
+
if (refs.length === 0) {
|
|
619
|
+
console.log("✗ no PI WEB service files found");
|
|
620
|
+
console.log(" Run `pi-web install` or `pi-web install --dev`.");
|
|
621
|
+
return false;
|
|
622
|
+
}
|
|
623
|
+
const statuses = refs.map((ref) => runtimeStatus(backend, ref));
|
|
624
|
+
for (const status of statuses)
|
|
625
|
+
printServiceStatus(status);
|
|
626
|
+
console.log("\nUse `pi-web logs` for service logs.");
|
|
627
|
+
return statuses.every((status) => status.health === "running");
|
|
628
|
+
}
|
|
629
|
+
function backendAvailabilityChecks(backend) {
|
|
630
|
+
if (backend.kind === "systemd")
|
|
631
|
+
return [["systemctl --user", ["systemctl", "--user", "--version"]]];
|
|
632
|
+
return [[`launchctl ${launchdDomain()}`, ["launchctl", "print", launchdDomain()]]];
|
|
633
|
+
}
|
|
634
|
+
function baseShellChecks(backend) {
|
|
635
|
+
const shell = serviceShellLabel();
|
|
636
|
+
const checks = [[`${shell} can find node >= 22`, serviceShellCommand(nodeVersionCheck())]];
|
|
637
|
+
if (backend.kind === "systemd")
|
|
638
|
+
checks.push([`systemd user ${shell} can find node >= 22`, systemdUserServiceShellCommand(nodeVersionCheck())]);
|
|
639
|
+
return checks;
|
|
640
|
+
}
|
|
641
|
+
function devInstallChecks(backend, root) {
|
|
642
|
+
const shell = serviceShellLabel();
|
|
643
|
+
const checks = [
|
|
644
|
+
[`${shell} can find npm`, serviceShellCommand(commandCheck("npm"), root)],
|
|
645
|
+
[`${shell} can find bash`, serviceShellCommand(commandCheck("bash"), root)],
|
|
646
|
+
];
|
|
647
|
+
if (backend.kind === "systemd") {
|
|
648
|
+
checks.push([`systemd user ${shell} can find npm`, systemdUserServiceShellCommand(commandCheck("npm"), root)], [`systemd user ${shell} can find bash`, systemdUserServiceShellCommand(commandCheck("bash"), root)]);
|
|
649
|
+
}
|
|
650
|
+
return checks;
|
|
651
|
+
}
|
|
652
|
+
function installPreflightChecks(backend, mode, executables, devRoot) {
|
|
653
|
+
return [
|
|
654
|
+
...backendAvailabilityChecks(backend),
|
|
655
|
+
...baseShellChecks(backend),
|
|
656
|
+
...(mode === "dev" && devRoot !== undefined ? devInstallChecks(backend, devRoot) : []),
|
|
657
|
+
...(mode === "production" && executables !== undefined ? [...executables.web.checks, ...executables.sessiond.checks] : []),
|
|
658
|
+
];
|
|
659
|
+
}
|
|
660
|
+
async function install(args) {
|
|
661
|
+
const backend = requireServiceBackend("pi-web install");
|
|
662
|
+
const options = parseInstallOptions(args);
|
|
663
|
+
const devRoot = options.mode === "dev" ? devRootPath() : undefined;
|
|
664
|
+
if (devRoot !== undefined)
|
|
665
|
+
validateDevCheckout(devRoot);
|
|
666
|
+
const executables = options.mode === "production" ? resolveServiceExecutables(backend) : undefined;
|
|
667
|
+
console.log(`Running PI WEB ${options.mode} install preflight checks...`);
|
|
668
|
+
console.log(`Service backend: ${backend.label}`);
|
|
669
|
+
console.log(`Service shell: ${describeServiceShell()}`);
|
|
670
|
+
if (!runChecks(installPreflightChecks(backend, options.mode, executables, devRoot))) {
|
|
671
|
+
printPathSetupAdvice();
|
|
672
|
+
throw new Error("Install preflight checks failed. Fix the failed checks above, then run `pi-web doctor` for more detail.");
|
|
673
|
+
}
|
|
674
|
+
const configPath = await writeInitialConfig(options);
|
|
675
|
+
const services = options.mode === "dev"
|
|
676
|
+
? devServiceDefinitions(options, configPath, devRoot ?? devRootPath())
|
|
677
|
+
: productionServiceDefinitions(options, configPath, executables ?? resolveServiceExecutables(backend));
|
|
678
|
+
await installNativeServices(backend, services);
|
|
679
|
+
console.log(`\nPI WEB ${options.mode} services are installed and starting.`);
|
|
680
|
+
console.log(`Config: ${configPath}`);
|
|
681
|
+
if (options.mode === "dev") {
|
|
682
|
+
console.log("Open: http://127.0.0.1:8505");
|
|
683
|
+
}
|
|
684
|
+
else {
|
|
685
|
+
console.log(`Open: http://${options.host === "0.0.0.0" ? "127.0.0.1" : options.host}:${options.port}`);
|
|
686
|
+
}
|
|
687
|
+
if (backend.kind === "systemd") {
|
|
688
|
+
const linger = isLingerEnabled();
|
|
689
|
+
if (linger === false) {
|
|
690
|
+
console.log("\nRecommended for server use: keep user services running after logout/reboot:");
|
|
691
|
+
console.log(` sudo loginctl enable-linger ${userInfo().username}`);
|
|
692
|
+
}
|
|
693
|
+
else if (linger === undefined) {
|
|
694
|
+
console.log("\nRecommended for server use: enable systemd user lingering so services survive logout/reboot:");
|
|
695
|
+
console.log(` sudo loginctl enable-linger ${userInfo().username}`);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
console.log("\nUseful commands:");
|
|
699
|
+
console.log(" pi-web status");
|
|
700
|
+
console.log(" pi-web logs");
|
|
701
|
+
console.log(" pi-web restart");
|
|
702
|
+
}
|
|
703
|
+
async function uninstall() {
|
|
704
|
+
const backend = requireServiceBackend("pi-web uninstall");
|
|
705
|
+
await uninstallNativeServices(backend);
|
|
706
|
+
console.log(`PI WEB ${backend.label} removed. Production and development service files were removed; config and data were left in place.`);
|
|
707
|
+
}
|
|
708
|
+
function systemdServiceAction(action, refs) {
|
|
709
|
+
const orderedRefs = action === "stop" ? stopOrder(refs) : startOrder(refs);
|
|
710
|
+
run("systemctl", ["--user", action, ...orderedRefs.map((ref) => ref.systemdName)], { check: true });
|
|
711
|
+
}
|
|
712
|
+
function launchdServiceAction(action, refs) {
|
|
713
|
+
if (action === "stop") {
|
|
714
|
+
for (const ref of stopOrder(refs))
|
|
715
|
+
launchdBootout(ref);
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
if (action === "restart") {
|
|
719
|
+
for (const ref of stopOrder(refs))
|
|
720
|
+
launchdBootout(ref);
|
|
721
|
+
}
|
|
722
|
+
for (const ref of startOrder(refs))
|
|
723
|
+
launchdStart(ref);
|
|
724
|
+
}
|
|
725
|
+
function serviceAction(action) {
|
|
726
|
+
const backend = requireServiceBackend(`pi-web ${action}`);
|
|
727
|
+
if (action === "status") {
|
|
728
|
+
if (!printServiceStatusReport(backend))
|
|
729
|
+
process.exitCode = 1;
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
const refs = installedServiceRefs(backend);
|
|
733
|
+
if (backend.kind === "systemd")
|
|
734
|
+
systemdServiceAction(action, refs);
|
|
735
|
+
else
|
|
736
|
+
launchdServiceAction(action, refs);
|
|
737
|
+
}
|
|
738
|
+
function logs() {
|
|
739
|
+
const backend = requireServiceBackend("pi-web logs");
|
|
740
|
+
const refs = installedServiceRefs(backend);
|
|
741
|
+
if (backend.kind === "systemd") {
|
|
742
|
+
run("journalctl", ["--user", ...refs.flatMap((ref) => ["-u", ref.systemdName]), "-f"]);
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
run("tail", ["-F", ...refs.map((ref) => launchdLogPath(ref))]);
|
|
746
|
+
}
|
|
747
|
+
function serviceShellLabel() {
|
|
748
|
+
return `${detectServiceShell().name} -lc`;
|
|
749
|
+
}
|
|
750
|
+
function systemdUserServiceShellCommand(command, cwd) {
|
|
751
|
+
return [
|
|
752
|
+
"systemd-run",
|
|
753
|
+
"--user",
|
|
754
|
+
"--wait",
|
|
755
|
+
"--collect",
|
|
756
|
+
"--pipe",
|
|
757
|
+
"--quiet",
|
|
758
|
+
...serviceShellCommand(command, cwd),
|
|
759
|
+
];
|
|
760
|
+
}
|
|
761
|
+
function commandCheck(command) {
|
|
762
|
+
return `command -v ${command}`;
|
|
763
|
+
}
|
|
764
|
+
function commandWithVersionCheck(command) {
|
|
765
|
+
return `${commandCheck(command)} && (${command} --version 2>&1 || true)`;
|
|
766
|
+
}
|
|
767
|
+
function nodeVersionCheck() {
|
|
768
|
+
return [
|
|
769
|
+
commandCheck("node"),
|
|
770
|
+
"node -e \"const major = Number(process.versions.node.split('.')[0]); console.log(process.version); process.exit(major >= 22 ? 0 : 1);\"",
|
|
771
|
+
].join(" && ");
|
|
772
|
+
}
|
|
773
|
+
function doctorChecks() {
|
|
774
|
+
const shell = serviceShellLabel();
|
|
775
|
+
const backend = currentServiceBackend();
|
|
776
|
+
if (backend === undefined) {
|
|
777
|
+
return [
|
|
778
|
+
[`${shell} can find node >= 22`, serviceShellCommand(nodeVersionCheck())],
|
|
779
|
+
[`${shell} can find npm`, serviceShellCommand(commandWithVersionCheck("npm"))],
|
|
780
|
+
[`${shell} can find pi`, serviceShellCommand(commandWithVersionCheck("pi"))],
|
|
781
|
+
];
|
|
782
|
+
}
|
|
783
|
+
const checks = [
|
|
784
|
+
...backendAvailabilityChecks(backend),
|
|
785
|
+
...baseShellChecks(backend),
|
|
786
|
+
[`${shell} can find npm`, serviceShellCommand(commandWithVersionCheck("npm"))],
|
|
787
|
+
[`${shell} can find pi`, serviceShellCommand(commandWithVersionCheck("pi"))],
|
|
788
|
+
];
|
|
789
|
+
const executables = resolveServiceExecutables(backend);
|
|
790
|
+
checks.push(...executables.web.checks, ...executables.sessiond.checks);
|
|
791
|
+
if (backend.kind === "systemd") {
|
|
792
|
+
checks.push([`systemd user ${shell} can find pi`, systemdUserServiceShellCommand(commandWithVersionCheck("pi"))]);
|
|
793
|
+
}
|
|
794
|
+
return checks;
|
|
795
|
+
}
|
|
796
|
+
function runChecks(checks) {
|
|
797
|
+
let failed = false;
|
|
798
|
+
for (const [label, command] of checks) {
|
|
799
|
+
const [bin, ...args] = command;
|
|
800
|
+
if (bin === undefined)
|
|
801
|
+
continue;
|
|
802
|
+
const result = capture(bin, args);
|
|
803
|
+
const ok = result.status === 0;
|
|
804
|
+
failed ||= !ok;
|
|
805
|
+
console.log(`${ok ? "✓" : "✗"} ${label}`);
|
|
806
|
+
printCheckOutput(result.stdout || result.stderr);
|
|
807
|
+
}
|
|
808
|
+
return !failed;
|
|
809
|
+
}
|
|
810
|
+
function printCheckOutput(output) {
|
|
811
|
+
const trimmed = output.trim();
|
|
812
|
+
if (trimmed === "")
|
|
813
|
+
return;
|
|
814
|
+
const lines = trimmed.split("\n");
|
|
815
|
+
for (const line of lines.slice(0, 3))
|
|
816
|
+
console.log(` ${line}`);
|
|
817
|
+
if (lines.length > 3)
|
|
818
|
+
console.log(" ...");
|
|
819
|
+
}
|
|
820
|
+
function optionalDoctorChecks() {
|
|
821
|
+
const shell = serviceShellLabel();
|
|
822
|
+
const backend = currentServiceBackend();
|
|
823
|
+
const checks = [[`${shell} can find optional ripgrep (rg)`, serviceShellCommand(commandCheck("rg"))]];
|
|
824
|
+
if (backend?.kind === "systemd")
|
|
825
|
+
checks.push([`systemd user ${shell} can find optional ripgrep (rg)`, systemdUserServiceShellCommand(commandCheck("rg"))]);
|
|
826
|
+
return checks;
|
|
827
|
+
}
|
|
828
|
+
function printOptionalDoctorChecks() {
|
|
829
|
+
let missingOptionalTool = false;
|
|
830
|
+
for (const [label, command] of optionalDoctorChecks()) {
|
|
831
|
+
const [bin, ...args] = command;
|
|
832
|
+
if (bin === undefined)
|
|
833
|
+
continue;
|
|
834
|
+
const result = capture(bin, args);
|
|
835
|
+
const ok = result.status === 0;
|
|
836
|
+
missingOptionalTool ||= !ok;
|
|
837
|
+
console.log(`${ok ? "✓" : "!"} ${label}`);
|
|
838
|
+
printCheckOutput(result.stdout || result.stderr);
|
|
839
|
+
}
|
|
840
|
+
if (missingOptionalTool) {
|
|
841
|
+
console.log(" Install ripgrep, or make rg visible to the service shell, for faster all-file @ suggestions.");
|
|
842
|
+
console.log(" PI WEB falls back to a bounded filesystem scan when rg is unavailable.");
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
function printPathSetupAdvice() {
|
|
846
|
+
const shell = detectServiceShell();
|
|
847
|
+
console.log("\nPATH setup advice:");
|
|
848
|
+
if (shell.name === "bash") {
|
|
849
|
+
console.log(" Detected bash. Put PATH setup for node/version managers/tools in ~/.bash_profile or ~/.profile.");
|
|
850
|
+
console.log(" If ~/.bash_profile exists, bash will not read ~/.profile unless you source it from ~/.bash_profile.");
|
|
851
|
+
console.log(" Do not rely only on ~/.bashrc or prompt hooks for tools needed by services or agents.");
|
|
852
|
+
}
|
|
853
|
+
else if (shell.name === "zsh") {
|
|
854
|
+
console.log(" Detected zsh. Put PATH setup for node/version managers/tools in ~/.zprofile, not only ~/.zshrc.");
|
|
855
|
+
console.log(" Avoid relying on prompt hooks; PI WEB services run non-interactive login shells.");
|
|
856
|
+
}
|
|
857
|
+
else {
|
|
858
|
+
console.log(" Detected fish. Prefer universal PATH setup such as `fish_add_path -U ...` for tools needed by services or agents.");
|
|
859
|
+
console.log(" Avoid relying on prompt hooks; PI WEB services run non-interactive login shells.");
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
async function doctor() {
|
|
863
|
+
const backend = currentServiceBackend();
|
|
864
|
+
console.log(`Platform: ${platformLabel()}`);
|
|
865
|
+
console.log(`Service backend: ${backend?.label ?? "manual run only"}`);
|
|
866
|
+
console.log(`Service shell: ${describeServiceShell()}`);
|
|
867
|
+
if (backend === undefined) {
|
|
868
|
+
console.log(`- Native user service checks skipped on ${platformLabel()}`);
|
|
869
|
+
}
|
|
870
|
+
console.log("");
|
|
871
|
+
await printPiWebVersionReport();
|
|
872
|
+
console.log("\nDoctor checks:");
|
|
873
|
+
const ok = runChecks(doctorChecks());
|
|
874
|
+
printOptionalDoctorChecks();
|
|
875
|
+
const nodePtySpawnHelperOk = printNodePtyDarwinSpawnHelperCheck();
|
|
876
|
+
if (supportsSystemdUserServices()) {
|
|
877
|
+
const linger = isLingerEnabled();
|
|
878
|
+
if (linger === true) {
|
|
879
|
+
console.log("✓ systemd user lingering enabled");
|
|
880
|
+
}
|
|
881
|
+
else if (linger === false) {
|
|
882
|
+
console.log("✗ systemd user lingering disabled");
|
|
883
|
+
console.log(` Recommended on servers: sudo loginctl enable-linger ${userInfo().username}`);
|
|
884
|
+
}
|
|
885
|
+
else {
|
|
886
|
+
console.log("? systemd user lingering unknown");
|
|
887
|
+
console.log(` Recommended on servers: sudo loginctl enable-linger ${userInfo().username}`);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
else if (backend?.kind === "launchd") {
|
|
891
|
+
console.log("- user services start at login with LaunchAgents");
|
|
892
|
+
}
|
|
893
|
+
else {
|
|
894
|
+
console.log(`- systemd user lingering skipped on ${platformLabel()}`);
|
|
895
|
+
}
|
|
896
|
+
if (!ok) {
|
|
897
|
+
console.log("\nIf a command works in your terminal but fails here, make sure your service shell login files set PATH the same way.");
|
|
898
|
+
if (backend?.kind === "systemd")
|
|
899
|
+
console.log("If a bundled entrypoint is not accessible, reinstall or update the PI WEB package.");
|
|
900
|
+
printPathSetupAdvice();
|
|
901
|
+
}
|
|
902
|
+
if (ok && backend === undefined) {
|
|
903
|
+
console.log(`\n${manualRunAdvice()}`);
|
|
904
|
+
}
|
|
905
|
+
if (!ok || !nodePtySpawnHelperOk)
|
|
906
|
+
process.exitCode = 1;
|
|
907
|
+
}
|
|
908
|
+
function printNodePtyDarwinSpawnHelperCheck() {
|
|
909
|
+
const result = formatNodePtyDarwinSpawnHelperCheck(checkNodePtyDarwinSpawnHelper());
|
|
910
|
+
for (const line of result.lines)
|
|
911
|
+
console.log(line);
|
|
912
|
+
return result.ok;
|
|
913
|
+
}
|
|
914
|
+
function isRecord(value) {
|
|
915
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
916
|
+
}
|
|
917
|
+
function help() {
|
|
918
|
+
console.log(`PI WEB
|
|
919
|
+
|
|
920
|
+
Usage:
|
|
921
|
+
pi-web install [--dev] [--host 127.0.0.1] [--port 8504] [--config ~/.config/pi-web/config.json]
|
|
922
|
+
pi-web uninstall
|
|
923
|
+
pi-web start|stop|restart|status|logs
|
|
924
|
+
pi-web doctor
|
|
925
|
+
pi-web version
|
|
926
|
+
|
|
927
|
+
Recommended install:
|
|
928
|
+
npm install -g @chainingintention/pi-web-cn
|
|
929
|
+
pi-web install
|
|
930
|
+
|
|
931
|
+
Development service install from a checkout:
|
|
932
|
+
pi-web install --dev
|
|
933
|
+
`);
|
|
934
|
+
}
|
|
935
|
+
async function main() {
|
|
936
|
+
const [command = "help", ...args] = process.argv.slice(2);
|
|
937
|
+
if (command === "install")
|
|
938
|
+
await install(args);
|
|
939
|
+
else if (command === "uninstall")
|
|
940
|
+
await uninstall();
|
|
941
|
+
else if (command === "start" || command === "stop" || command === "restart" || command === "status")
|
|
942
|
+
serviceAction(command);
|
|
943
|
+
else if (command === "logs")
|
|
944
|
+
logs();
|
|
945
|
+
else if (command === "doctor")
|
|
946
|
+
await doctor();
|
|
947
|
+
else if (command === "version")
|
|
948
|
+
await printPiWebVersionReport();
|
|
949
|
+
else if (command === "--version" || command === "-v")
|
|
950
|
+
console.log(packageVersion());
|
|
951
|
+
else if (command === "help" || command === "--help" || command === "-h")
|
|
952
|
+
help();
|
|
953
|
+
else
|
|
954
|
+
throw new Error(`Unknown command: ${command}`);
|
|
955
|
+
}
|
|
956
|
+
main().catch((error) => {
|
|
957
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
958
|
+
process.exit(1);
|
|
959
|
+
});
|
|
960
|
+
//# sourceMappingURL=cli.js.map
|