@jmfederico/pi-web 1.202605.11 → 1.202605.13
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/README.md +47 -50
- package/dist/cli.js +514 -115
- package/dist/cli.js.map +1 -1
- package/dist/client/apple-touch-icon.png +0 -0
- package/dist/client/assets/{CodeViewer-0NuLmnBL.js → CodeViewer-ncgSvOQM.js} +1 -1
- package/dist/client/assets/TerminalPanel-DjuVNsp5.js +75 -0
- package/dist/client/assets/index-CDGTKzLD.js +1239 -0
- package/dist/client/favicon.svg +11 -0
- package/dist/client/index.html +6 -2
- 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 +4 -4
- package/dist/pi-web-plugins/pi-web/pi-web-plugin.js +13 -9
- package/dist/server/diagnostics/nodePtySpawnHelper.js +135 -0
- package/dist/server/diagnostics/nodePtySpawnHelper.js.map +1 -0
- package/dist/server/piWebPluginService.js +9 -9
- package/dist/server/piWebStatus.js +8 -8
- package/dist/server/piWebStatus.js.map +1 -1
- package/dist/server/sessions/piSessionService.js +169 -19
- package/dist/server/sessions/piSessionService.js.map +1 -1
- package/dist/server/sessions/sessionArchiveTree.js +35 -0
- package/dist/server/sessions/sessionArchiveTree.js.map +1 -0
- package/dist/server/sessions/sessionCommandService.js +88 -2
- package/dist/server/sessions/sessionCommandService.js.map +1 -1
- package/dist/server/sessions/sessionRoutes.js +8 -0
- package/dist/server/sessions/sessionRoutes.js.map +1 -1
- package/dist/server/terminalProxyRoutes.js +70 -0
- package/dist/server/terminalProxyRoutes.js.map +1 -1
- package/dist/server/terminals/terminalRoutes.js +65 -1
- package/dist/server/terminals/terminalRoutes.js.map +1 -1
- package/dist/server/terminals/terminalService.js +202 -37
- package/dist/server/terminals/terminalService.js.map +1 -1
- package/dist/server/workspaceExplorerRoutes.js +18 -0
- package/dist/server/workspaceExplorerRoutes.js.map +1 -1
- package/dist/server/workspaces/fileContentService.js +23 -3
- package/dist/server/workspaces/fileContentService.js.map +1 -1
- package/dist/server/workspaces/imagePreviewService.js +40 -0
- package/dist/server/workspaces/imagePreviewService.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/plugins.md +43 -42
- package/extensions/pi-web.ts +12 -5
- package/package.json +1 -1
- package/plugin-api.d.ts +2 -0
- package/dist/client/assets/TerminalPanel-BG6_9svt.js +0 -47
- package/dist/client/assets/index-DmVN7DQr.js +0 -1143
package/dist/cli.js
CHANGED
|
@@ -1,14 +1,85 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
4
|
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
4
5
|
import { homedir, userInfo } from "node:os";
|
|
5
6
|
import { basename, dirname, join, resolve } from "node:path";
|
|
6
|
-
import { spawnSync } from "node:child_process";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
-
import { defaultPiWebConfigPath, examplePiWebConfig } from "./config.js";
|
|
9
|
-
|
|
8
|
+
import { defaultPiWebConfigPath, defaultPiWebDataDir, examplePiWebConfig } from "./config.js";
|
|
9
|
+
import { checkNodePtyDarwinSpawnHelper, formatNodePtyDarwinSpawnHelperCheck } from "./server/diagnostics/nodePtySpawnHelper.js";
|
|
10
|
+
const PI_WEB_PACKAGE_NAME = "@jmfederico/pi-web";
|
|
11
|
+
const systemdServiceDir = join(homedir(), ".config", "systemd", "user");
|
|
12
|
+
const launchdServiceDir = join(homedir(), "Library", "LaunchAgents");
|
|
13
|
+
const logDir = join(defaultPiWebDataDir(), "logs");
|
|
10
14
|
const sessiondServiceName = "pi-web-sessiond.service";
|
|
11
15
|
const webServiceName = "pi-web.service";
|
|
16
|
+
const uiDevServiceName = "pi-web-ui-dev.service";
|
|
17
|
+
const serviceRefs = {
|
|
18
|
+
sessiond: {
|
|
19
|
+
id: "sessiond",
|
|
20
|
+
systemdName: sessiondServiceName,
|
|
21
|
+
launchdLabel: "com.pi-web.sessiond",
|
|
22
|
+
launchdPlistName: "com.pi-web.sessiond.plist",
|
|
23
|
+
logName: "sessiond.log",
|
|
24
|
+
},
|
|
25
|
+
web: {
|
|
26
|
+
id: "web",
|
|
27
|
+
systemdName: webServiceName,
|
|
28
|
+
launchdLabel: "com.pi-web.web",
|
|
29
|
+
launchdPlistName: "com.pi-web.web.plist",
|
|
30
|
+
logName: "web.log",
|
|
31
|
+
},
|
|
32
|
+
uiDev: {
|
|
33
|
+
id: "uiDev",
|
|
34
|
+
systemdName: uiDevServiceName,
|
|
35
|
+
launchdLabel: "com.pi-web.ui-dev",
|
|
36
|
+
launchdPlistName: "com.pi-web.ui-dev.plist",
|
|
37
|
+
logName: "ui-dev.log",
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
const productionServiceIds = ["sessiond", "web"];
|
|
41
|
+
const startServiceOrder = ["sessiond", "web", "uiDev"];
|
|
42
|
+
const stopServiceOrder = ["web", "uiDev", "sessiond"];
|
|
43
|
+
function platformLabel() {
|
|
44
|
+
if (process.platform === "darwin")
|
|
45
|
+
return "macOS";
|
|
46
|
+
if (process.platform === "linux")
|
|
47
|
+
return "Linux";
|
|
48
|
+
if (process.platform === "win32")
|
|
49
|
+
return "Windows";
|
|
50
|
+
return process.platform;
|
|
51
|
+
}
|
|
52
|
+
function currentServiceBackend() {
|
|
53
|
+
if (process.platform === "linux")
|
|
54
|
+
return { kind: "systemd", label: "systemd user services" };
|
|
55
|
+
if (process.platform === "darwin")
|
|
56
|
+
return { kind: "launchd", label: "LaunchAgents" };
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
function requireServiceBackend(command) {
|
|
60
|
+
const backend = currentServiceBackend();
|
|
61
|
+
if (backend !== undefined)
|
|
62
|
+
return backend;
|
|
63
|
+
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()}`);
|
|
64
|
+
}
|
|
65
|
+
function supportsSystemdUserServices() {
|
|
66
|
+
return currentServiceBackend()?.kind === "systemd";
|
|
67
|
+
}
|
|
68
|
+
function manualRunAdvice() {
|
|
69
|
+
return [
|
|
70
|
+
"Run PI WEB manually from a checkout:",
|
|
71
|
+
" npm run start:sessiond",
|
|
72
|
+
" PI_WEB_PORT=8504 npm start",
|
|
73
|
+
"",
|
|
74
|
+
"For development in one terminal:",
|
|
75
|
+
" npm run dev",
|
|
76
|
+
"",
|
|
77
|
+
"For split development, keep sessiond separate and run web/API plus Vite UI separately:",
|
|
78
|
+
" npm run dev:sessiond",
|
|
79
|
+
" npm run dev:web",
|
|
80
|
+
" npm run dev:client",
|
|
81
|
+
].join("\n");
|
|
82
|
+
}
|
|
12
83
|
function run(command, args, options = {}) {
|
|
13
84
|
const result = spawnSync(command, args, { stdio: "inherit" });
|
|
14
85
|
const status = result.status ?? 1;
|
|
@@ -16,9 +87,17 @@ function run(command, args, options = {}) {
|
|
|
16
87
|
process.exit(status);
|
|
17
88
|
return status;
|
|
18
89
|
}
|
|
90
|
+
function outputText(value) {
|
|
91
|
+
return typeof value === "string" ? value : "";
|
|
92
|
+
}
|
|
19
93
|
function capture(command, args) {
|
|
20
94
|
const result = spawnSync(command, args, { encoding: "utf8" });
|
|
21
|
-
|
|
95
|
+
const errorMessage = result.error instanceof Error ? result.error.message : "";
|
|
96
|
+
const stderr = outputText(result.stderr);
|
|
97
|
+
return { status: result.status ?? 1, stdout: outputText(result.stdout), stderr: stderr === "" ? errorMessage : stderr };
|
|
98
|
+
}
|
|
99
|
+
function runQuiet(command, args) {
|
|
100
|
+
return capture(command, args).status;
|
|
22
101
|
}
|
|
23
102
|
function hasCommand(command) {
|
|
24
103
|
return capture("/usr/bin/env", ["sh", "-c", `command -v ${command}`]).status === 0;
|
|
@@ -37,7 +116,7 @@ function isLingerEnabled() {
|
|
|
37
116
|
return undefined;
|
|
38
117
|
}
|
|
39
118
|
function parseInstallOptions(args) {
|
|
40
|
-
const options = { host: "127.0.0.1", port: "8504" };
|
|
119
|
+
const options = { host: "127.0.0.1", port: "8504", mode: "production" };
|
|
41
120
|
for (let i = 0; i < args.length; i += 1) {
|
|
42
121
|
const arg = args[i];
|
|
43
122
|
if (arg === undefined)
|
|
@@ -72,8 +151,11 @@ function parseInstallOptions(args) {
|
|
|
72
151
|
else if (arg.startsWith("--config=")) {
|
|
73
152
|
options.config = arg.slice("--config=".length);
|
|
74
153
|
}
|
|
154
|
+
else if (arg === "--dev") {
|
|
155
|
+
options.mode = "dev";
|
|
156
|
+
}
|
|
75
157
|
else if (arg === "--user-systemd") {
|
|
76
|
-
// Accepted for readability;
|
|
158
|
+
// Accepted for backwards-compatible readability; PI WEB chooses the native user service backend automatically.
|
|
77
159
|
}
|
|
78
160
|
else {
|
|
79
161
|
throw new Error(`Unknown install option: ${arg}`);
|
|
@@ -90,6 +172,17 @@ function fishSingleQuote(value) {
|
|
|
90
172
|
function systemdEscape(value) {
|
|
91
173
|
return value.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
|
|
92
174
|
}
|
|
175
|
+
function systemdQuotedValue(value) {
|
|
176
|
+
return `"${systemdEscape(value)}"`;
|
|
177
|
+
}
|
|
178
|
+
function xmlEscape(value) {
|
|
179
|
+
return value
|
|
180
|
+
.replaceAll("&", "&")
|
|
181
|
+
.replaceAll("<", "<")
|
|
182
|
+
.replaceAll(">", ">")
|
|
183
|
+
.replaceAll('"', """)
|
|
184
|
+
.replaceAll("'", "'");
|
|
185
|
+
}
|
|
93
186
|
function packageRootPath() {
|
|
94
187
|
return dirname(dirname(fileURLToPath(import.meta.url)));
|
|
95
188
|
}
|
|
@@ -100,14 +193,15 @@ function detectServiceShell() {
|
|
|
100
193
|
const userShell = userInfo().shell ?? undefined;
|
|
101
194
|
const envShell = process.env["SHELL"]?.trim();
|
|
102
195
|
const detected = envShell === undefined || envShell === "" ? userShell : envShell;
|
|
103
|
-
const name = basename(detected ?? "").replace(
|
|
196
|
+
const name = basename(detected ?? "").replace(/^-/, "");
|
|
104
197
|
if (name === "bash" || name === "zsh" || name === "fish") {
|
|
105
198
|
return { name, executable: detected ?? name, detected: detected ?? name, fallback: false };
|
|
106
199
|
}
|
|
107
200
|
return { name: "bash", executable: "bash", ...(detected === undefined ? {} : { detected }), fallback: true };
|
|
108
201
|
}
|
|
109
|
-
function serviceShellCommand(command) {
|
|
110
|
-
|
|
202
|
+
function serviceShellCommand(command, cwd) {
|
|
203
|
+
const fullCommand = cwd === undefined ? command : `cd ${serviceShellQuote(cwd)} && ${command}`;
|
|
204
|
+
return ["/usr/bin/env", detectServiceShell().executable, "-lc", fullCommand];
|
|
111
205
|
}
|
|
112
206
|
function serviceShellExecPrefix() {
|
|
113
207
|
return `/usr/bin/env ${detectServiceShell().executable} -lc`;
|
|
@@ -122,50 +216,48 @@ function checkSucceeds(command) {
|
|
|
122
216
|
const [bin, ...args] = command;
|
|
123
217
|
return bin !== undefined && capture(bin, args).status === 0;
|
|
124
218
|
}
|
|
125
|
-
function serviceShellCanFindCommand(command) {
|
|
219
|
+
function serviceShellCanFindCommand(command, backend) {
|
|
126
220
|
if (!checkSucceeds(serviceShellCommand(commandCheck(command))))
|
|
127
221
|
return false;
|
|
128
|
-
|
|
222
|
+
if (backend.kind === "systemd")
|
|
223
|
+
return checkSucceeds(systemdUserServiceShellCommand(commandCheck(command)));
|
|
224
|
+
return true;
|
|
129
225
|
}
|
|
130
226
|
function readableFileCheck(path) {
|
|
131
227
|
const quoted = serviceShellQuote(path);
|
|
132
228
|
return `test -r ${quoted} && printf '%s\\n' ${quoted}`;
|
|
133
229
|
}
|
|
134
|
-
function commandExecutable(command) {
|
|
230
|
+
function commandExecutable(command, backend) {
|
|
135
231
|
const shell = serviceShellLabel();
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
checks
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
],
|
|
142
|
-
};
|
|
232
|
+
const checks = [[`${shell} can find ${command}`, serviceShellCommand(commandCheck(command))]];
|
|
233
|
+
if (backend.kind === "systemd") {
|
|
234
|
+
checks.push([`systemd user ${shell} can find ${command}`, systemdUserServiceShellCommand(commandCheck(command))]);
|
|
235
|
+
}
|
|
236
|
+
return { command, checks };
|
|
143
237
|
}
|
|
144
|
-
function bundledExecutable(command, entrypointPath) {
|
|
238
|
+
function bundledExecutable(command, entrypointPath, backend) {
|
|
145
239
|
const shell = serviceShellLabel();
|
|
146
240
|
const check = readableFileCheck(entrypointPath);
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
checks
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
],
|
|
153
|
-
};
|
|
241
|
+
const checks = [[`${shell} can access bundled ${command} entrypoint`, serviceShellCommand(check)]];
|
|
242
|
+
if (backend.kind === "systemd") {
|
|
243
|
+
checks.push([`systemd user ${shell} can access bundled ${command} entrypoint`, systemdUserServiceShellCommand(check)]);
|
|
244
|
+
}
|
|
245
|
+
return { command: `node ${serviceShellQuote(entrypointPath)}`, checks };
|
|
154
246
|
}
|
|
155
|
-
function serviceExecutable(envName, command, entrypointPath) {
|
|
247
|
+
function serviceExecutable(envName, command, entrypointPath, backend) {
|
|
156
248
|
const configured = process.env[envName]?.trim();
|
|
157
249
|
if (configured !== undefined && configured !== "")
|
|
158
250
|
return { command: configured, checks: [] };
|
|
159
|
-
if (serviceShellCanFindCommand(command))
|
|
160
|
-
return commandExecutable(command);
|
|
251
|
+
if (serviceShellCanFindCommand(command, backend))
|
|
252
|
+
return commandExecutable(command, backend);
|
|
161
253
|
if (existsSync(entrypointPath))
|
|
162
|
-
return bundledExecutable(command, entrypointPath);
|
|
163
|
-
return commandExecutable(command);
|
|
254
|
+
return bundledExecutable(command, entrypointPath, backend);
|
|
255
|
+
return commandExecutable(command, backend);
|
|
164
256
|
}
|
|
165
|
-
function resolveServiceExecutables() {
|
|
257
|
+
function resolveServiceExecutables(backend) {
|
|
166
258
|
return {
|
|
167
|
-
sessiond: serviceExecutable("PI_WEB_SESSIOND_EXEC", "pi-web-sessiond", packageEntrypointPath("sessiond")),
|
|
168
|
-
web: serviceExecutable("PI_WEB_SERVER_EXEC", "pi-web-server", packageEntrypointPath("server")),
|
|
259
|
+
sessiond: serviceExecutable("PI_WEB_SESSIOND_EXEC", "pi-web-sessiond", packageEntrypointPath("sessiond"), backend),
|
|
260
|
+
web: serviceExecutable("PI_WEB_SERVER_EXEC", "pi-web-server", packageEntrypointPath("server"), backend),
|
|
169
261
|
};
|
|
170
262
|
}
|
|
171
263
|
function describeServiceShell() {
|
|
@@ -173,39 +265,148 @@ function describeServiceShell() {
|
|
|
173
265
|
if (shell.fallback) {
|
|
174
266
|
return shell.detected === undefined
|
|
175
267
|
? "could not detect a supported login shell; using bash"
|
|
176
|
-
: `detected ${shell.detected}; using bash because
|
|
268
|
+
: `detected ${shell.detected}; using bash because PI WEB currently supports bash, zsh, and fish`;
|
|
177
269
|
}
|
|
178
270
|
return shell.detected === undefined ? shell.name : `${shell.name} (${shell.detected})`;
|
|
179
271
|
}
|
|
180
|
-
function
|
|
272
|
+
function configEnvironment(options, configPath) {
|
|
273
|
+
return options.config === undefined ? {} : { PI_WEB_CONFIG: configPath };
|
|
274
|
+
}
|
|
275
|
+
function serviceRefList(ids) {
|
|
276
|
+
return ids.map((id) => serviceRefs[id]);
|
|
277
|
+
}
|
|
278
|
+
function allServiceRefs() {
|
|
279
|
+
return serviceRefList(["sessiond", "web", "uiDev"]);
|
|
280
|
+
}
|
|
281
|
+
function productionServiceRefs() {
|
|
282
|
+
return serviceRefList(productionServiceIds);
|
|
283
|
+
}
|
|
284
|
+
function orderServiceRefs(refs, order) {
|
|
285
|
+
const byId = new Map(refs.map((ref) => [ref.id, ref]));
|
|
286
|
+
return order.flatMap((id) => {
|
|
287
|
+
const ref = byId.get(id);
|
|
288
|
+
return ref === undefined ? [] : [ref];
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
function startOrder(refs) {
|
|
292
|
+
return orderServiceRefs(refs, startServiceOrder);
|
|
293
|
+
}
|
|
294
|
+
function stopOrder(refs) {
|
|
295
|
+
return orderServiceRefs(refs, stopServiceOrder);
|
|
296
|
+
}
|
|
297
|
+
function productionServiceDefinitions(options, configPath, executables) {
|
|
298
|
+
return [
|
|
299
|
+
{
|
|
300
|
+
...serviceRefs.sessiond,
|
|
301
|
+
description: "PI WEB session daemon",
|
|
302
|
+
shellCommand: `exec ${executables.sessiond.command}`,
|
|
303
|
+
restart: "on-failure",
|
|
304
|
+
environment: {},
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
...serviceRefs.web,
|
|
308
|
+
description: "PI WEB server",
|
|
309
|
+
shellCommand: `exec ${executables.web.command}`,
|
|
310
|
+
restart: "on-failure",
|
|
311
|
+
environment: configEnvironment(options, configPath),
|
|
312
|
+
after: ["sessiond"],
|
|
313
|
+
wants: ["sessiond"],
|
|
314
|
+
},
|
|
315
|
+
];
|
|
316
|
+
}
|
|
317
|
+
function devRootPath() {
|
|
318
|
+
return resolve(process.cwd());
|
|
319
|
+
}
|
|
320
|
+
function validateDevCheckout(root) {
|
|
321
|
+
const packageJsonPath = join(root, "package.json");
|
|
322
|
+
if (!existsSync(packageJsonPath)) {
|
|
323
|
+
throw new Error(`Development mode must be installed from a PI WEB checkout. Missing package.json: ${packageJsonPath}`);
|
|
324
|
+
}
|
|
325
|
+
const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
326
|
+
if (!isRecord(parsed) || parsed["name"] !== PI_WEB_PACKAGE_NAME) {
|
|
327
|
+
throw new Error(`Development mode must be installed from a PI WEB checkout. ${packageJsonPath} is not ${PI_WEB_PACKAGE_NAME}.`);
|
|
328
|
+
}
|
|
329
|
+
const scripts = parsed["scripts"];
|
|
330
|
+
if (!isRecord(scripts))
|
|
331
|
+
throw new Error(`Development mode requires npm scripts in ${packageJsonPath}.`);
|
|
332
|
+
const requiredScripts = ["start:sessiond", "dev:web", "dev:client"];
|
|
333
|
+
const missing = requiredScripts.filter((script) => typeof scripts[script] !== "string");
|
|
334
|
+
if (missing.length > 0)
|
|
335
|
+
throw new Error(`Development mode requires missing npm scripts: ${missing.join(", ")}.`);
|
|
336
|
+
}
|
|
337
|
+
function devServiceDefinitions(options, configPath, root) {
|
|
338
|
+
return [
|
|
339
|
+
{
|
|
340
|
+
...serviceRefs.sessiond,
|
|
341
|
+
description: "PI WEB session daemon (dev)",
|
|
342
|
+
shellCommand: "exec npm run start:sessiond",
|
|
343
|
+
restart: "never",
|
|
344
|
+
environment: {},
|
|
345
|
+
workingDirectory: root,
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
...serviceRefs.uiDev,
|
|
349
|
+
description: "PI WEB UI dev server",
|
|
350
|
+
shellCommand: `exec /usr/bin/env bash -c ${serviceShellQuote('trap "kill 0" EXIT; npm run dev:web & npm run dev:client & wait')}`,
|
|
351
|
+
restart: "never",
|
|
352
|
+
environment: configEnvironment(options, configPath),
|
|
353
|
+
after: ["sessiond"],
|
|
354
|
+
wants: ["sessiond"],
|
|
355
|
+
workingDirectory: root,
|
|
356
|
+
},
|
|
357
|
+
];
|
|
358
|
+
}
|
|
359
|
+
function dependencyLine(name, ids) {
|
|
360
|
+
if (ids === undefined || ids.length === 0)
|
|
361
|
+
return "";
|
|
362
|
+
return `${name}=${ids.map((id) => serviceRefs[id].systemdName).join(" ")}\n`;
|
|
363
|
+
}
|
|
364
|
+
function environmentLines(environment) {
|
|
365
|
+
return Object.entries(environment)
|
|
366
|
+
.map(([key, value]) => `Environment="${key}=${systemdEscape(value)}"\n`)
|
|
367
|
+
.join("");
|
|
368
|
+
}
|
|
369
|
+
function systemdUnit(service) {
|
|
370
|
+
const workingDirectory = service.workingDirectory === undefined ? "" : `WorkingDirectory=${systemdQuotedValue(service.workingDirectory)}\n`;
|
|
371
|
+
const restart = service.restart === "on-failure" ? "Restart=on-failure\nRestartSec=2\n" : "Restart=no\n";
|
|
181
372
|
return `[Unit]
|
|
182
|
-
Description
|
|
183
|
-
|
|
373
|
+
Description=${service.description}
|
|
374
|
+
${dependencyLine("After", service.after)}${dependencyLine("Wants", service.wants)}
|
|
184
375
|
[Service]
|
|
185
376
|
Type=simple
|
|
186
|
-
ExecStart=${serviceShellExecPrefix()} ${systemdServiceShellQuote(
|
|
187
|
-
|
|
188
|
-
RestartSec=2
|
|
189
|
-
|
|
377
|
+
${workingDirectory}${environmentLines(service.environment)}ExecStart=${serviceShellExecPrefix()} ${systemdServiceShellQuote(service.shellCommand)}
|
|
378
|
+
${restart}
|
|
190
379
|
[Install]
|
|
191
380
|
WantedBy=default.target
|
|
192
381
|
`;
|
|
193
382
|
}
|
|
194
|
-
function
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
383
|
+
function plistString(key, value, indent = " ") {
|
|
384
|
+
return `${indent}<key>${xmlEscape(key)}</key>\n${indent}<string>${xmlEscape(value)}</string>\n`;
|
|
385
|
+
}
|
|
386
|
+
function plistProgramArguments(service) {
|
|
387
|
+
const args = ["/usr/bin/env", detectServiceShell().executable, "-lc", service.shellCommand];
|
|
388
|
+
return ` <key>ProgramArguments</key>\n <array>\n${args.map((arg) => ` <string>${xmlEscape(arg)}</string>`).join("\n")}\n </array>\n`;
|
|
389
|
+
}
|
|
390
|
+
function plistEnvironment(environment) {
|
|
391
|
+
const entries = Object.entries(environment);
|
|
392
|
+
if (entries.length === 0)
|
|
393
|
+
return "";
|
|
394
|
+
return ` <key>EnvironmentVariables</key>\n <dict>\n${entries.map(([key, value]) => plistString(key, value, " ")).join("")} </dict>\n`;
|
|
395
|
+
}
|
|
396
|
+
function launchdLogPath(ref) {
|
|
397
|
+
return join(logDir, ref.logName);
|
|
398
|
+
}
|
|
399
|
+
function launchdPlist(service) {
|
|
400
|
+
const workingDirectory = service.workingDirectory === undefined ? "" : plistString("WorkingDirectory", service.workingDirectory);
|
|
401
|
+
const keepAlive = service.restart === "on-failure" ? " <key>KeepAlive</key>\n <dict>\n <key>SuccessfulExit</key>\n <false/>\n </dict>\n" : "";
|
|
402
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
403
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
404
|
+
<plist version="1.0">
|
|
405
|
+
<dict>
|
|
406
|
+
${plistString("Label", service.launchdLabel)}${plistProgramArguments(service)}${workingDirectory}${plistEnvironment(service.environment)} <key>RunAtLoad</key>
|
|
407
|
+
<true/>
|
|
408
|
+
${keepAlive}${plistString("StandardOutPath", launchdLogPath(service))}${plistString("StandardErrorPath", launchdLogPath(service))}</dict>
|
|
409
|
+
</plist>
|
|
209
410
|
`;
|
|
210
411
|
}
|
|
211
412
|
async function writeInitialConfig(options) {
|
|
@@ -216,33 +417,164 @@ async function writeInitialConfig(options) {
|
|
|
216
417
|
}
|
|
217
418
|
return configPath;
|
|
218
419
|
}
|
|
420
|
+
function systemdServicePath(ref) {
|
|
421
|
+
return join(systemdServiceDir, ref.systemdName);
|
|
422
|
+
}
|
|
423
|
+
function launchdPlistPath(ref) {
|
|
424
|
+
return join(launchdServiceDir, ref.launchdPlistName);
|
|
425
|
+
}
|
|
426
|
+
function serviceFileExists(backend, ref) {
|
|
427
|
+
return existsSync(backend.kind === "systemd" ? systemdServicePath(ref) : launchdPlistPath(ref));
|
|
428
|
+
}
|
|
429
|
+
function installedServiceRefs(backend) {
|
|
430
|
+
const installed = startOrder(allServiceRefs().filter((ref) => serviceFileExists(backend, ref)));
|
|
431
|
+
return installed.length === 0 ? productionServiceRefs() : installed;
|
|
432
|
+
}
|
|
433
|
+
async function installSystemdServices(services) {
|
|
434
|
+
const selected = new Set(services.map((service) => service.id));
|
|
435
|
+
const obsolete = stopOrder(allServiceRefs().filter((ref) => !selected.has(ref.id)));
|
|
436
|
+
for (const ref of obsolete) {
|
|
437
|
+
runQuiet("systemctl", ["--user", "disable", "--now", ref.systemdName]);
|
|
438
|
+
await rm(systemdServicePath(ref), { force: true });
|
|
439
|
+
}
|
|
440
|
+
await mkdir(systemdServiceDir, { recursive: true });
|
|
441
|
+
for (const service of services) {
|
|
442
|
+
await writeFile(systemdServicePath(service), systemdUnit(service));
|
|
443
|
+
}
|
|
444
|
+
const names = services.map((service) => service.systemdName);
|
|
445
|
+
run("systemctl", ["--user", "daemon-reload"], { check: true });
|
|
446
|
+
run("systemctl", ["--user", "enable", ...names], { check: true });
|
|
447
|
+
run("systemctl", ["--user", "restart", ...names], { check: true });
|
|
448
|
+
}
|
|
449
|
+
function launchdDomain() {
|
|
450
|
+
return `gui/${String(userInfo().uid)}`;
|
|
451
|
+
}
|
|
452
|
+
function launchdServiceTarget(ref) {
|
|
453
|
+
return `${launchdDomain()}/${ref.launchdLabel}`;
|
|
454
|
+
}
|
|
455
|
+
function launchdIsLoaded(ref) {
|
|
456
|
+
return capture("launchctl", ["print", launchdServiceTarget(ref)]).status === 0;
|
|
457
|
+
}
|
|
458
|
+
function launchdBootout(ref) {
|
|
459
|
+
runQuiet("launchctl", ["bootout", launchdServiceTarget(ref)]);
|
|
460
|
+
}
|
|
461
|
+
function launchdBootstrap(ref) {
|
|
462
|
+
run("launchctl", ["bootstrap", launchdDomain(), launchdPlistPath(ref)], { check: true });
|
|
463
|
+
run("launchctl", ["enable", launchdServiceTarget(ref)], { check: true });
|
|
464
|
+
}
|
|
465
|
+
function launchdStart(ref) {
|
|
466
|
+
if (!launchdIsLoaded(ref))
|
|
467
|
+
launchdBootstrap(ref);
|
|
468
|
+
run("launchctl", ["kickstart", launchdServiceTarget(ref)], { check: true });
|
|
469
|
+
}
|
|
470
|
+
async function installLaunchdServices(services) {
|
|
471
|
+
const selected = new Set(services.map((service) => service.id));
|
|
472
|
+
await mkdir(launchdServiceDir, { recursive: true });
|
|
473
|
+
await mkdir(logDir, { recursive: true });
|
|
474
|
+
for (const ref of stopOrder(allServiceRefs()))
|
|
475
|
+
launchdBootout(ref);
|
|
476
|
+
for (const ref of allServiceRefs().filter((candidate) => !selected.has(candidate.id))) {
|
|
477
|
+
await rm(launchdPlistPath(ref), { force: true });
|
|
478
|
+
}
|
|
479
|
+
for (const service of services) {
|
|
480
|
+
await writeFile(launchdPlistPath(service), launchdPlist(service));
|
|
481
|
+
}
|
|
482
|
+
for (const service of services)
|
|
483
|
+
launchdStart(service);
|
|
484
|
+
}
|
|
485
|
+
async function installNativeServices(backend, services) {
|
|
486
|
+
if (backend.kind === "systemd")
|
|
487
|
+
await installSystemdServices(services);
|
|
488
|
+
else
|
|
489
|
+
await installLaunchdServices(services);
|
|
490
|
+
}
|
|
491
|
+
async function uninstallSystemdServices() {
|
|
492
|
+
for (const ref of stopOrder(allServiceRefs())) {
|
|
493
|
+
runQuiet("systemctl", ["--user", "disable", "--now", ref.systemdName]);
|
|
494
|
+
await rm(systemdServicePath(ref), { force: true });
|
|
495
|
+
}
|
|
496
|
+
runQuiet("systemctl", ["--user", "daemon-reload"]);
|
|
497
|
+
}
|
|
498
|
+
async function uninstallLaunchdServices() {
|
|
499
|
+
for (const ref of stopOrder(allServiceRefs())) {
|
|
500
|
+
launchdBootout(ref);
|
|
501
|
+
await rm(launchdPlistPath(ref), { force: true });
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
async function uninstallNativeServices(backend) {
|
|
505
|
+
if (backend.kind === "systemd")
|
|
506
|
+
await uninstallSystemdServices();
|
|
507
|
+
else
|
|
508
|
+
await uninstallLaunchdServices();
|
|
509
|
+
}
|
|
510
|
+
function backendAvailabilityChecks(backend) {
|
|
511
|
+
if (backend.kind === "systemd")
|
|
512
|
+
return [["systemctl --user", ["systemctl", "--user", "--version"]]];
|
|
513
|
+
return [[`launchctl ${launchdDomain()}`, ["launchctl", "print", launchdDomain()]]];
|
|
514
|
+
}
|
|
515
|
+
function baseShellChecks(backend) {
|
|
516
|
+
const shell = serviceShellLabel();
|
|
517
|
+
const checks = [[`${shell} can find node >= 22`, serviceShellCommand(nodeVersionCheck())]];
|
|
518
|
+
if (backend.kind === "systemd")
|
|
519
|
+
checks.push([`systemd user ${shell} can find node >= 22`, systemdUserServiceShellCommand(nodeVersionCheck())]);
|
|
520
|
+
return checks;
|
|
521
|
+
}
|
|
522
|
+
function devInstallChecks(backend, root) {
|
|
523
|
+
const shell = serviceShellLabel();
|
|
524
|
+
const checks = [
|
|
525
|
+
[`${shell} can find npm`, serviceShellCommand(commandCheck("npm"), root)],
|
|
526
|
+
[`${shell} can find bash`, serviceShellCommand(commandCheck("bash"), root)],
|
|
527
|
+
];
|
|
528
|
+
if (backend.kind === "systemd") {
|
|
529
|
+
checks.push([`systemd user ${shell} can find npm`, systemdUserServiceShellCommand(commandCheck("npm"), root)], [`systemd user ${shell} can find bash`, systemdUserServiceShellCommand(commandCheck("bash"), root)]);
|
|
530
|
+
}
|
|
531
|
+
return checks;
|
|
532
|
+
}
|
|
533
|
+
function installPreflightChecks(backend, mode, executables, devRoot) {
|
|
534
|
+
return [
|
|
535
|
+
...backendAvailabilityChecks(backend),
|
|
536
|
+
...baseShellChecks(backend),
|
|
537
|
+
...(mode === "dev" && devRoot !== undefined ? devInstallChecks(backend, devRoot) : []),
|
|
538
|
+
...(mode === "production" && executables !== undefined ? [...executables.web.checks, ...executables.sessiond.checks] : []),
|
|
539
|
+
];
|
|
540
|
+
}
|
|
219
541
|
async function install(args) {
|
|
542
|
+
const backend = requireServiceBackend("pi-web install");
|
|
220
543
|
const options = parseInstallOptions(args);
|
|
221
|
-
const
|
|
222
|
-
|
|
544
|
+
const devRoot = options.mode === "dev" ? devRootPath() : undefined;
|
|
545
|
+
if (devRoot !== undefined)
|
|
546
|
+
validateDevCheckout(devRoot);
|
|
547
|
+
const executables = options.mode === "production" ? resolveServiceExecutables(backend) : undefined;
|
|
548
|
+
console.log(`Running PI WEB ${options.mode} install preflight checks...`);
|
|
549
|
+
console.log(`Service backend: ${backend.label}`);
|
|
223
550
|
console.log(`Service shell: ${describeServiceShell()}`);
|
|
224
|
-
if (!runChecks(installPreflightChecks(executables))) {
|
|
551
|
+
if (!runChecks(installPreflightChecks(backend, options.mode, executables, devRoot))) {
|
|
225
552
|
printPathSetupAdvice();
|
|
226
553
|
throw new Error("Install preflight checks failed. Fix the failed checks above, then run `pi-web doctor` for more detail.");
|
|
227
554
|
}
|
|
228
555
|
const configPath = await writeInitialConfig(options);
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
run("systemctl", ["--user", "enable", "--now", webServiceName], { check: true });
|
|
235
|
-
console.log(`\nPi Web is installed and starting.`);
|
|
556
|
+
const services = options.mode === "dev"
|
|
557
|
+
? devServiceDefinitions(options, configPath, devRoot ?? devRootPath())
|
|
558
|
+
: productionServiceDefinitions(options, configPath, executables ?? resolveServiceExecutables(backend));
|
|
559
|
+
await installNativeServices(backend, services);
|
|
560
|
+
console.log(`\nPI WEB ${options.mode} services are installed and starting.`);
|
|
236
561
|
console.log(`Config: ${configPath}`);
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if (linger === false) {
|
|
240
|
-
console.log("\nRecommended for server use: keep user services running after logout/reboot:");
|
|
241
|
-
console.log(` sudo loginctl enable-linger ${userInfo().username}`);
|
|
562
|
+
if (options.mode === "dev") {
|
|
563
|
+
console.log("Open: http://127.0.0.1:8505");
|
|
242
564
|
}
|
|
243
|
-
else
|
|
244
|
-
console.log(
|
|
245
|
-
|
|
565
|
+
else {
|
|
566
|
+
console.log(`Open: http://${options.host === "0.0.0.0" ? "127.0.0.1" : options.host}:${options.port}`);
|
|
567
|
+
}
|
|
568
|
+
if (backend.kind === "systemd") {
|
|
569
|
+
const linger = isLingerEnabled();
|
|
570
|
+
if (linger === false) {
|
|
571
|
+
console.log("\nRecommended for server use: keep user services running after logout/reboot:");
|
|
572
|
+
console.log(` sudo loginctl enable-linger ${userInfo().username}`);
|
|
573
|
+
}
|
|
574
|
+
else if (linger === undefined) {
|
|
575
|
+
console.log("\nRecommended for server use: enable systemd user lingering so services survive logout/reboot:");
|
|
576
|
+
console.log(` sudo loginctl enable-linger ${userInfo().username}`);
|
|
577
|
+
}
|
|
246
578
|
}
|
|
247
579
|
console.log("\nUseful commands:");
|
|
248
580
|
console.log(" pi-web status");
|
|
@@ -250,23 +582,55 @@ async function install(args) {
|
|
|
250
582
|
console.log(" pi-web restart");
|
|
251
583
|
}
|
|
252
584
|
async function uninstall() {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
585
|
+
const backend = requireServiceBackend("pi-web uninstall");
|
|
586
|
+
await uninstallNativeServices(backend);
|
|
587
|
+
console.log(`PI WEB ${backend.label} removed. Production and development service files were removed; config and data were left in place.`);
|
|
588
|
+
}
|
|
589
|
+
function systemdServiceAction(action, refs) {
|
|
590
|
+
const orderedRefs = action === "stop" ? stopOrder(refs) : startOrder(refs);
|
|
591
|
+
run("systemctl", ["--user", action, ...orderedRefs.map((ref) => ref.systemdName)], { check: action !== "status" });
|
|
592
|
+
}
|
|
593
|
+
function launchdServiceAction(action, refs) {
|
|
594
|
+
if (action === "status") {
|
|
595
|
+
for (const ref of refs) {
|
|
596
|
+
console.log(`\n== ${ref.launchdLabel} ==`);
|
|
597
|
+
run("launchctl", ["print", launchdServiceTarget(ref)]);
|
|
598
|
+
}
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
if (action === "stop") {
|
|
602
|
+
for (const ref of stopOrder(refs))
|
|
603
|
+
launchdBootout(ref);
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
if (action === "restart") {
|
|
607
|
+
for (const ref of stopOrder(refs))
|
|
608
|
+
launchdBootout(ref);
|
|
609
|
+
}
|
|
610
|
+
for (const ref of startOrder(refs))
|
|
611
|
+
launchdStart(ref);
|
|
259
612
|
}
|
|
260
613
|
function serviceAction(action) {
|
|
261
|
-
|
|
614
|
+
const backend = requireServiceBackend(`pi-web ${action}`);
|
|
615
|
+
const refs = installedServiceRefs(backend);
|
|
616
|
+
if (backend.kind === "systemd")
|
|
617
|
+
systemdServiceAction(action, refs);
|
|
618
|
+
else
|
|
619
|
+
launchdServiceAction(action, refs);
|
|
262
620
|
}
|
|
263
621
|
function logs() {
|
|
264
|
-
|
|
622
|
+
const backend = requireServiceBackend("pi-web logs");
|
|
623
|
+
const refs = installedServiceRefs(backend);
|
|
624
|
+
if (backend.kind === "systemd") {
|
|
625
|
+
run("journalctl", ["--user", ...refs.flatMap((ref) => ["-u", ref.systemdName]), "-f"]);
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
run("tail", ["-F", ...refs.map((ref) => launchdLogPath(ref))]);
|
|
265
629
|
}
|
|
266
630
|
function serviceShellLabel() {
|
|
267
631
|
return `${detectServiceShell().name} -lc`;
|
|
268
632
|
}
|
|
269
|
-
function systemdUserServiceShellCommand(command) {
|
|
633
|
+
function systemdUserServiceShellCommand(command, cwd) {
|
|
270
634
|
return [
|
|
271
635
|
"systemd-run",
|
|
272
636
|
"--user",
|
|
@@ -274,7 +638,7 @@ function systemdUserServiceShellCommand(command) {
|
|
|
274
638
|
"--collect",
|
|
275
639
|
"--pipe",
|
|
276
640
|
"--quiet",
|
|
277
|
-
...serviceShellCommand(command),
|
|
641
|
+
...serviceShellCommand(command, cwd),
|
|
278
642
|
];
|
|
279
643
|
}
|
|
280
644
|
function commandCheck(command) {
|
|
@@ -286,25 +650,28 @@ function nodeVersionCheck() {
|
|
|
286
650
|
"node -e \"const major = Number(process.versions.node.split('.')[0]); console.log(process.version); process.exit(major >= 22 ? 0 : 1);\"",
|
|
287
651
|
].join(" && ");
|
|
288
652
|
}
|
|
289
|
-
function installPreflightChecks(executables = resolveServiceExecutables()) {
|
|
290
|
-
const shell = serviceShellLabel();
|
|
291
|
-
return [
|
|
292
|
-
["systemctl --user", ["systemctl", "--user", "--version"]],
|
|
293
|
-
[`${shell} can find node >= 22`, serviceShellCommand(nodeVersionCheck())],
|
|
294
|
-
[`systemd user ${shell} can find node >= 22`, systemdUserServiceShellCommand(nodeVersionCheck())],
|
|
295
|
-
...executables.web.checks,
|
|
296
|
-
...executables.sessiond.checks,
|
|
297
|
-
];
|
|
298
|
-
}
|
|
299
653
|
function doctorChecks() {
|
|
300
654
|
const shell = serviceShellLabel();
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
655
|
+
const backend = currentServiceBackend();
|
|
656
|
+
if (backend === undefined) {
|
|
657
|
+
return [
|
|
658
|
+
[`${shell} can find node >= 22`, serviceShellCommand(nodeVersionCheck())],
|
|
659
|
+
[`${shell} can find npm`, serviceShellCommand(commandCheck("npm"))],
|
|
660
|
+
[`${shell} can find pi`, serviceShellCommand(commandCheck("pi"))],
|
|
661
|
+
];
|
|
662
|
+
}
|
|
663
|
+
const checks = [
|
|
664
|
+
...backendAvailabilityChecks(backend),
|
|
665
|
+
...baseShellChecks(backend),
|
|
304
666
|
[`${shell} can find npm`, serviceShellCommand(commandCheck("npm"))],
|
|
305
667
|
[`${shell} can find pi`, serviceShellCommand(commandCheck("pi"))],
|
|
306
|
-
[`systemd user ${shell} can find pi`, systemdUserServiceShellCommand(commandCheck("pi"))],
|
|
307
668
|
];
|
|
669
|
+
const executables = resolveServiceExecutables(backend);
|
|
670
|
+
checks.push(...executables.web.checks, ...executables.sessiond.checks);
|
|
671
|
+
if (backend.kind === "systemd") {
|
|
672
|
+
checks.push([`systemd user ${shell} can find pi`, systemdUserServiceShellCommand(commandCheck("pi"))]);
|
|
673
|
+
}
|
|
674
|
+
return checks;
|
|
308
675
|
}
|
|
309
676
|
function runChecks(checks) {
|
|
310
677
|
let failed = false;
|
|
@@ -337,40 +704,69 @@ function printPathSetupAdvice() {
|
|
|
337
704
|
}
|
|
338
705
|
else if (shell.name === "zsh") {
|
|
339
706
|
console.log(" Detected zsh. Put PATH setup for node/version managers/tools in ~/.zprofile, not only ~/.zshrc.");
|
|
340
|
-
console.log(" Avoid relying on prompt hooks;
|
|
707
|
+
console.log(" Avoid relying on prompt hooks; PI WEB services run non-interactive login shells.");
|
|
341
708
|
}
|
|
342
709
|
else {
|
|
343
710
|
console.log(" Detected fish. Prefer universal PATH setup such as `fish_add_path -U ...` for tools needed by services or agents.");
|
|
344
|
-
console.log(" Avoid relying on prompt hooks;
|
|
711
|
+
console.log(" Avoid relying on prompt hooks; PI WEB services run non-interactive login shells.");
|
|
345
712
|
}
|
|
346
713
|
}
|
|
347
714
|
function doctor() {
|
|
715
|
+
const backend = currentServiceBackend();
|
|
716
|
+
console.log(`Platform: ${platformLabel()}`);
|
|
717
|
+
console.log(`Service backend: ${backend?.label ?? "manual run only"}`);
|
|
348
718
|
console.log(`Service shell: ${describeServiceShell()}`);
|
|
719
|
+
if (backend === undefined) {
|
|
720
|
+
console.log(`- Native user service checks skipped on ${platformLabel()}`);
|
|
721
|
+
}
|
|
349
722
|
const ok = runChecks(doctorChecks());
|
|
350
|
-
const
|
|
351
|
-
if (
|
|
352
|
-
|
|
723
|
+
const nodePtySpawnHelperOk = printNodePtyDarwinSpawnHelperCheck();
|
|
724
|
+
if (supportsSystemdUserServices()) {
|
|
725
|
+
const linger = isLingerEnabled();
|
|
726
|
+
if (linger === true) {
|
|
727
|
+
console.log("✓ systemd user lingering enabled");
|
|
728
|
+
}
|
|
729
|
+
else if (linger === false) {
|
|
730
|
+
console.log("✗ systemd user lingering disabled");
|
|
731
|
+
console.log(` Recommended on servers: sudo loginctl enable-linger ${userInfo().username}`);
|
|
732
|
+
}
|
|
733
|
+
else {
|
|
734
|
+
console.log("? systemd user lingering unknown");
|
|
735
|
+
console.log(` Recommended on servers: sudo loginctl enable-linger ${userInfo().username}`);
|
|
736
|
+
}
|
|
353
737
|
}
|
|
354
|
-
else if (
|
|
355
|
-
console.log("
|
|
356
|
-
console.log(` Recommended on servers: sudo loginctl enable-linger ${userInfo().username}`);
|
|
738
|
+
else if (backend?.kind === "launchd") {
|
|
739
|
+
console.log("- user services start at login with LaunchAgents");
|
|
357
740
|
}
|
|
358
741
|
else {
|
|
359
|
-
console.log(
|
|
360
|
-
console.log(` Recommended on servers: sudo loginctl enable-linger ${userInfo().username}`);
|
|
742
|
+
console.log(`- systemd user lingering skipped on ${platformLabel()}`);
|
|
361
743
|
}
|
|
362
744
|
if (!ok) {
|
|
363
745
|
console.log("\nIf a command works in your terminal but fails here, make sure your service shell login files set PATH the same way.");
|
|
364
|
-
|
|
746
|
+
if (backend?.kind === "systemd")
|
|
747
|
+
console.log("If a bundled entrypoint is not accessible, reinstall or update the PI WEB package.");
|
|
365
748
|
printPathSetupAdvice();
|
|
366
|
-
process.exitCode = 1;
|
|
367
749
|
}
|
|
750
|
+
if (ok && backend === undefined) {
|
|
751
|
+
console.log(`\n${manualRunAdvice()}`);
|
|
752
|
+
}
|
|
753
|
+
if (!ok || !nodePtySpawnHelperOk)
|
|
754
|
+
process.exitCode = 1;
|
|
755
|
+
}
|
|
756
|
+
function printNodePtyDarwinSpawnHelperCheck() {
|
|
757
|
+
const result = formatNodePtyDarwinSpawnHelperCheck(checkNodePtyDarwinSpawnHelper());
|
|
758
|
+
for (const line of result.lines)
|
|
759
|
+
console.log(line);
|
|
760
|
+
return result.ok;
|
|
761
|
+
}
|
|
762
|
+
function isRecord(value) {
|
|
763
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
368
764
|
}
|
|
369
765
|
function help() {
|
|
370
|
-
console.log(`
|
|
766
|
+
console.log(`PI WEB
|
|
371
767
|
|
|
372
768
|
Usage:
|
|
373
|
-
pi-web install [--host 127.0.0.1] [--port 8504] [--config ~/.config/pi-web/config.json]
|
|
769
|
+
pi-web install [--dev] [--host 127.0.0.1] [--port 8504] [--config ~/.config/pi-web/config.json]
|
|
374
770
|
pi-web uninstall
|
|
375
771
|
pi-web start|stop|restart|status|logs
|
|
376
772
|
pi-web doctor
|
|
@@ -378,6 +774,9 @@ Usage:
|
|
|
378
774
|
Recommended install:
|
|
379
775
|
npm install -g @jmfederico/pi-web
|
|
380
776
|
pi-web install
|
|
777
|
+
|
|
778
|
+
Development service install from a checkout:
|
|
779
|
+
pi-web install --dev
|
|
381
780
|
`);
|
|
382
781
|
}
|
|
383
782
|
async function main() {
|