@jmfederico/pi-web 1.202607.0 → 1.202607.2
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 +12 -9
- package/dist/cli.js +305 -322
- package/dist/cli.js.map +1 -1
- package/dist/client/assets/{CodeViewer-tXF3UTsY.js → CodeViewer-B4M13447.js} +1 -1
- package/dist/client/assets/{TerminalPanel-D_ihiWAt.js → TerminalPanel-Bmvj-Ecn.js} +1 -1
- package/dist/client/assets/{UnifiedDiffViewer-BziirXBJ.js → UnifiedDiffViewer-C0hygwRo.js} +1 -1
- package/dist/client/assets/index-TvPsRyS1.js +3545 -0
- package/dist/client/index.html +6 -6
- package/dist/client/manifest.webmanifest +4 -4
- package/dist/config.js +157 -2
- package/dist/config.js.map +1 -1
- package/dist/nativeServices/serviceDoctor.js +480 -0
- package/dist/nativeServices/serviceDoctor.js.map +1 -0
- package/dist/nativeServices/serviceInstall.js +35 -0
- package/dist/nativeServices/serviceInstall.js.map +1 -0
- package/dist/nativeServices/servicePlan.js +358 -0
- package/dist/nativeServices/servicePlan.js.map +1 -0
- package/dist/nativeServices/serviceProbe.js +447 -0
- package/dist/nativeServices/serviceProbe.js.map +1 -0
- package/dist/nativeServices/serviceRendering.js +122 -0
- package/dist/nativeServices/serviceRendering.js.map +1 -0
- package/dist/pi-web-plugins/updates/pi-web-plugin.js +12 -0
- package/dist/plugin-api.d.ts +2 -0
- package/dist/server/activeAgentProfileProvider.js +25 -0
- package/dist/server/activeAgentProfileProvider.js.map +1 -0
- package/dist/server/app.js +57 -16
- package/dist/server/app.js.map +1 -1
- package/dist/server/configRoutes.js +43 -9
- package/dist/server/configRoutes.js.map +1 -1
- package/dist/server/diagnostics/nodePtyNativeModule.js +36 -0
- package/dist/server/diagnostics/nodePtyNativeModule.js.map +1 -0
- package/dist/server/git/gitService.js +176 -5
- package/dist/server/git/gitService.js.map +1 -1
- package/dist/server/machines/machinePluginProxyRoutes.js +7 -4
- package/dist/server/machines/machinePluginProxyRoutes.js.map +1 -1
- package/dist/server/machines/machineProxyRoutes.js +16 -4
- package/dist/server/machines/machineProxyRoutes.js.map +1 -1
- package/dist/server/machines/machineRoutes.js +1 -1
- package/dist/server/machines/machineRoutes.js.map +1 -1
- package/dist/server/machines/machineService.js +2 -2
- package/dist/server/machines/machineService.js.map +1 -1
- package/dist/server/piPackageRoutes.js +7 -3
- package/dist/server/piPackageRoutes.js.map +1 -1
- package/dist/server/piPackageService.js +34 -2
- package/dist/server/piPackageService.js.map +1 -1
- package/dist/server/piWebPluginService.js +48 -12
- package/dist/server/piWebPluginService.js.map +1 -1
- package/dist/server/piWebReleaseLookupCache.js +37 -0
- package/dist/server/piWebReleaseLookupCache.js.map +1 -0
- package/dist/server/piWebStatus.js +38 -39
- package/dist/server/piWebStatus.js.map +1 -1
- package/dist/server/piWebStatusCache.js +19 -6
- package/dist/server/piWebStatusCache.js.map +1 -1
- package/dist/server/realtime/sessionEventHub.js +37 -7
- package/dist/server/realtime/sessionEventHub.js.map +1 -1
- package/dist/server/sessiond/sessionDaemonStartup.js +49 -0
- package/dist/server/sessiond/sessionDaemonStartup.js.map +1 -0
- package/dist/server/sessiond.js +128 -62
- package/dist/server/sessiond.js.map +1 -1
- package/dist/server/sessions/authProviderOptions.js +30 -32
- package/dist/server/sessions/authProviderOptions.js.map +1 -1
- package/dist/server/sessions/authRoutes.js +14 -4
- package/dist/server/sessions/authRoutes.js.map +1 -1
- package/dist/server/sessions/authService.js +140 -29
- package/dist/server/sessions/authService.js.map +1 -1
- package/dist/server/sessions/globalProviderPolicy.js +98 -0
- package/dist/server/sessions/globalProviderPolicy.js.map +1 -0
- package/dist/server/sessions/modelCatalogRefresher.js +184 -0
- package/dist/server/sessions/modelCatalogRefresher.js.map +1 -0
- package/dist/server/sessions/oauthLoginFlowService.js +176 -53
- package/dist/server/sessions/oauthLoginFlowService.js.map +1 -1
- package/dist/server/sessions/piSessionManagerGateway.js +22 -21
- package/dist/server/sessions/piSessionManagerGateway.js.map +1 -1
- package/dist/server/sessions/piSessionService.js +936 -111
- package/dist/server/sessions/piSessionService.js.map +1 -1
- package/dist/server/sessions/plainTextTheme.js +112 -0
- package/dist/server/sessions/plainTextTheme.js.map +1 -0
- package/dist/server/sessions/sessionArchiveMigration.js +590 -0
- package/dist/server/sessions/sessionArchiveMigration.js.map +1 -0
- package/dist/server/sessions/sessionArchiveStore.js +7 -4
- package/dist/server/sessions/sessionArchiveStore.js.map +1 -1
- package/dist/server/sessions/sessionCommandService.js +68 -9
- package/dist/server/sessions/sessionCommandService.js.map +1 -1
- package/dist/server/sessions/sessionNotificationStore.js +484 -0
- package/dist/server/sessions/sessionNotificationStore.js.map +1 -0
- package/dist/server/sessions/sessionRoutes.js +202 -2
- package/dist/server/sessions/sessionRoutes.js.map +1 -1
- package/dist/server/sessions/sessionService.js +2 -0
- package/dist/server/sessions/sessionService.js.map +1 -0
- package/dist/server/sessions/sessionTreeProjection.js +250 -0
- package/dist/server/sessions/sessionTreeProjection.js.map +1 -0
- package/dist/server/sessions/sessionUnreadStore.js +520 -0
- package/dist/server/sessions/sessionUnreadStore.js.map +1 -0
- package/dist/server/sessions/spawnSessionTool.js +2 -2
- package/dist/server/sessions/spawnSessionTool.js.map +1 -1
- package/dist/server/sessions/spawnSubsessionTool.js +69 -31
- package/dist/server/sessions/spawnSubsessionTool.js.map +1 -1
- package/dist/server/terminals/terminalService.js +10 -4
- package/dist/server/terminals/terminalService.js.map +1 -1
- package/dist/sessiond/activeAgentProfile.js +31 -0
- package/dist/sessiond/activeAgentProfile.js.map +1 -0
- package/dist/sessiond/sessionDaemonClient.js +38 -0
- package/dist/sessiond/sessionDaemonClient.js.map +1 -1
- package/dist/shared/activeAgentProfile.js +76 -0
- package/dist/shared/activeAgentProfile.js.map +1 -0
- package/dist/shared/apiTypes.d.ts +256 -4
- package/dist/shared/apiTypes.js +12 -0
- package/dist/shared/apiTypes.js.map +1 -1
- package/dist/shared/capabilities.js +11 -0
- package/dist/shared/capabilities.js.map +1 -1
- package/dist/shared/federatedRoutes.js +12 -0
- package/dist/shared/federatedRoutes.js.map +1 -1
- package/dist/shared/piWebStatusParsing.js +6 -0
- package/dist/shared/piWebStatusParsing.js.map +1 -1
- package/docs/config.md +85 -12
- package/docs/plugins.md +29 -11
- package/install.sh +1 -1
- package/package.json +9 -8
- package/dist/client/assets/index-Brfk1lwl.js +0 -2950
package/dist/cli.js
CHANGED
|
@@ -1,44 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
|
-
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
3
|
+
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
4
4
|
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
5
5
|
import { homedir, userInfo } from "node:os";
|
|
6
6
|
import { basename, dirname, join, resolve } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
-
import { defaultPiWebConfigPath, defaultPiWebDataDir, examplePiWebConfig } from "./config.js";
|
|
8
|
+
import { defaultPiWebConfigPath, defaultPiWebDataDir, effectivePiWebConfig, examplePiWebConfig } from "./config.js";
|
|
9
9
|
import { packageVersion, printPiWebVersionReport } from "./piWebVersionReport.js";
|
|
10
10
|
import { checkNodePtyDarwinSpawnHelper, formatNodePtyDarwinSpawnHelperCheck } from "./server/diagnostics/nodePtySpawnHelper.js";
|
|
11
|
+
import { checkNodePtyNativeModule, formatNodePtyNativeModuleCheck } from "./server/diagnostics/nodePtyNativeModule.js";
|
|
12
|
+
import { installNativeServiceCandidate, nativeServiceInstallFailureNeedsPathAdvice, } from "./nativeServices/serviceInstall.js";
|
|
13
|
+
import { minimumSupportedNodeVersion, nativeServiceManagerRefs, productionNativeServiceIds, } from "./nativeServices/servicePlan.js";
|
|
14
|
+
import { formatNativeServiceDoctorResult, inferInstalledNativeServiceMode, inspectInstalledDevelopmentServiceInput, inspectInstalledProductionServiceContext, runNativeServiceDoctor, } from "./nativeServices/serviceDoctor.js";
|
|
15
|
+
import { createNativeServiceAuthoritativeProbe, nativeServicePrerequisiteShellCheck, } from "./nativeServices/serviceProbe.js";
|
|
16
|
+
import { renderLaunchdPlist, renderSystemdUnit } from "./nativeServices/serviceRendering.js";
|
|
11
17
|
const PI_WEB_PACKAGE_NAME = "@jmfederico/pi-web";
|
|
12
18
|
const systemdServiceDir = join(homedir(), ".config", "systemd", "user");
|
|
13
19
|
const launchdServiceDir = join(homedir(), "Library", "LaunchAgents");
|
|
14
20
|
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
21
|
const serviceRefs = {
|
|
19
|
-
sessiond: {
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
},
|
|
22
|
+
sessiond: { id: "sessiond", ...nativeServiceManagerRefs.sessiond },
|
|
23
|
+
web: { id: "web", ...nativeServiceManagerRefs.web },
|
|
24
|
+
uiDev: { id: "uiDev", ...nativeServiceManagerRefs.uiDev },
|
|
40
25
|
};
|
|
41
|
-
const productionServiceIds = [
|
|
26
|
+
const productionServiceIds = [...productionNativeServiceIds];
|
|
42
27
|
const startServiceOrder = ["sessiond", "web", "uiDev"];
|
|
43
28
|
const stopServiceOrder = ["web", "uiDev", "sessiond"];
|
|
44
29
|
// Restart web/UI before sessiond: when `pi-web restart` runs in a pi-web
|
|
@@ -54,13 +39,16 @@ function platformLabel() {
|
|
|
54
39
|
return "Windows";
|
|
55
40
|
return process.platform;
|
|
56
41
|
}
|
|
57
|
-
function
|
|
58
|
-
if (
|
|
42
|
+
export function serviceBackendForPlatform(platform) {
|
|
43
|
+
if (platform === "linux")
|
|
59
44
|
return { kind: "systemd", label: "systemd user services" };
|
|
60
|
-
if (
|
|
45
|
+
if (platform === "darwin")
|
|
61
46
|
return { kind: "launchd", label: "LaunchAgents" };
|
|
62
47
|
return undefined;
|
|
63
48
|
}
|
|
49
|
+
function currentServiceBackend() {
|
|
50
|
+
return serviceBackendForPlatform(process.platform);
|
|
51
|
+
}
|
|
64
52
|
function requireServiceBackend(command) {
|
|
65
53
|
const backend = currentServiceBackend();
|
|
66
54
|
if (backend !== undefined)
|
|
@@ -105,7 +93,7 @@ function runQuiet(command, args) {
|
|
|
105
93
|
return capture(command, args).status;
|
|
106
94
|
}
|
|
107
95
|
function hasCommand(command) {
|
|
108
|
-
return capture("/usr/bin/env", ["sh", "-c", `command -v ${command}`]).status === 0;
|
|
96
|
+
return capture("/usr/bin/env", ["sh", "-c", `command -v ${shellSingleQuote(command)}`]).status === 0;
|
|
109
97
|
}
|
|
110
98
|
function isLingerEnabled() {
|
|
111
99
|
if (!hasCommand("loginctl"))
|
|
@@ -174,105 +162,50 @@ function shellSingleQuote(value) {
|
|
|
174
162
|
function fishSingleQuote(value) {
|
|
175
163
|
return `'${value.replaceAll("\\", "\\\\").replaceAll("'", "\\'")}'`;
|
|
176
164
|
}
|
|
177
|
-
function systemdEscape(value) {
|
|
178
|
-
return value.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
|
|
179
|
-
}
|
|
180
|
-
function systemdQuotedValue(value) {
|
|
181
|
-
return `"${systemdEscape(value)}"`;
|
|
182
|
-
}
|
|
183
|
-
function xmlEscape(value) {
|
|
184
|
-
return value
|
|
185
|
-
.replaceAll("&", "&")
|
|
186
|
-
.replaceAll("<", "<")
|
|
187
|
-
.replaceAll(">", ">")
|
|
188
|
-
.replaceAll('"', """)
|
|
189
|
-
.replaceAll("'", "'");
|
|
190
|
-
}
|
|
191
165
|
function packageRootPath() {
|
|
192
166
|
return dirname(dirname(fileURLToPath(import.meta.url)));
|
|
193
167
|
}
|
|
194
168
|
function packageEntrypointPath(name) {
|
|
195
169
|
return join(packageRootPath(), "dist", "server", name === "server" ? "index.js" : "sessiond.js");
|
|
196
170
|
}
|
|
171
|
+
export function regularFileExists(path) {
|
|
172
|
+
return existsSync(path) && statSync(path).isFile();
|
|
173
|
+
}
|
|
197
174
|
function detectServiceShell() {
|
|
198
175
|
const userShell = userInfo().shell ?? undefined;
|
|
199
176
|
const envShell = process.env["SHELL"]?.trim();
|
|
200
177
|
const detected = envShell === undefined || envShell === "" ? userShell : envShell;
|
|
201
178
|
const name = basename(detected ?? "").replace(/^-/, "");
|
|
202
179
|
if (name === "bash" || name === "zsh" || name === "fish") {
|
|
203
|
-
return {
|
|
180
|
+
return {
|
|
181
|
+
name,
|
|
182
|
+
executable: detected ?? name,
|
|
183
|
+
source: "detected",
|
|
184
|
+
detectedExecutable: detected ?? name,
|
|
185
|
+
};
|
|
204
186
|
}
|
|
205
|
-
return {
|
|
187
|
+
return {
|
|
188
|
+
name: "bash",
|
|
189
|
+
executable: "bash",
|
|
190
|
+
source: "fallback",
|
|
191
|
+
detectedExecutable: detected ?? null,
|
|
192
|
+
};
|
|
206
193
|
}
|
|
207
194
|
function serviceShellCommand(command, cwd) {
|
|
208
195
|
const fullCommand = cwd === undefined ? command : `cd ${serviceShellQuote(cwd)} && ${command}`;
|
|
209
196
|
return ["/usr/bin/env", detectServiceShell().executable, "-lc", fullCommand];
|
|
210
197
|
}
|
|
211
|
-
function serviceShellExecPrefix() {
|
|
212
|
-
return `/usr/bin/env ${detectServiceShell().executable} -lc`;
|
|
213
|
-
}
|
|
214
198
|
function serviceShellQuote(value) {
|
|
215
199
|
return detectServiceShell().name === "fish" ? fishSingleQuote(value) : shellSingleQuote(value);
|
|
216
200
|
}
|
|
217
|
-
function systemdServiceShellQuote(value) {
|
|
218
|
-
return serviceShellQuote(value.replaceAll("%", "%%").replaceAll("$", "$$"));
|
|
219
|
-
}
|
|
220
|
-
function checkSucceeds(command) {
|
|
221
|
-
const [bin, ...args] = command;
|
|
222
|
-
return bin !== undefined && capture(bin, args).status === 0;
|
|
223
|
-
}
|
|
224
|
-
function serviceShellCanFindCommand(command, backend) {
|
|
225
|
-
if (!checkSucceeds(serviceShellCommand(commandCheck(command))))
|
|
226
|
-
return false;
|
|
227
|
-
if (backend.kind === "systemd")
|
|
228
|
-
return checkSucceeds(systemdUserServiceShellCommand(commandCheck(command)));
|
|
229
|
-
return true;
|
|
230
|
-
}
|
|
231
|
-
function readableFileCheck(path) {
|
|
232
|
-
const quoted = serviceShellQuote(path);
|
|
233
|
-
return `test -r ${quoted} && printf '%s\\n' ${quoted}`;
|
|
234
|
-
}
|
|
235
|
-
function commandExecutable(command, backend) {
|
|
236
|
-
const shell = serviceShellLabel();
|
|
237
|
-
const checks = [[`${shell} can find ${command}`, serviceShellCommand(commandCheck(command))]];
|
|
238
|
-
if (backend.kind === "systemd") {
|
|
239
|
-
checks.push([`systemd user ${shell} can find ${command}`, systemdUserServiceShellCommand(commandCheck(command))]);
|
|
240
|
-
}
|
|
241
|
-
return { command, checks };
|
|
242
|
-
}
|
|
243
|
-
function bundledExecutable(command, entrypointPath, backend) {
|
|
244
|
-
const shell = serviceShellLabel();
|
|
245
|
-
const check = readableFileCheck(entrypointPath);
|
|
246
|
-
const checks = [[`${shell} can access bundled ${command} entrypoint`, serviceShellCommand(check)]];
|
|
247
|
-
if (backend.kind === "systemd") {
|
|
248
|
-
checks.push([`systemd user ${shell} can access bundled ${command} entrypoint`, systemdUserServiceShellCommand(check)]);
|
|
249
|
-
}
|
|
250
|
-
return { command: `node ${serviceShellQuote(entrypointPath)}`, checks };
|
|
251
|
-
}
|
|
252
|
-
function serviceExecutable(envName, command, entrypointPath, backend) {
|
|
253
|
-
const configured = process.env[envName]?.trim();
|
|
254
|
-
if (configured !== undefined && configured !== "")
|
|
255
|
-
return { command: configured, checks: [] };
|
|
256
|
-
if (serviceShellCanFindCommand(command, backend))
|
|
257
|
-
return commandExecutable(command, backend);
|
|
258
|
-
if (existsSync(entrypointPath))
|
|
259
|
-
return bundledExecutable(command, entrypointPath, backend);
|
|
260
|
-
return commandExecutable(command, backend);
|
|
261
|
-
}
|
|
262
|
-
function resolveServiceExecutables(backend) {
|
|
263
|
-
return {
|
|
264
|
-
sessiond: serviceExecutable("PI_WEB_SESSIOND_EXEC", "pi-web-sessiond", packageEntrypointPath("sessiond"), backend),
|
|
265
|
-
web: serviceExecutable("PI_WEB_SERVER_EXEC", "pi-web-server", packageEntrypointPath("server"), backend),
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
201
|
function describeServiceShell() {
|
|
269
202
|
const shell = detectServiceShell();
|
|
270
|
-
if (shell.fallback) {
|
|
271
|
-
return shell.
|
|
203
|
+
if (shell.source === "fallback") {
|
|
204
|
+
return shell.detectedExecutable === null
|
|
272
205
|
? "could not detect a supported login shell; using bash"
|
|
273
|
-
: `detected ${shell.
|
|
206
|
+
: `detected ${shell.detectedExecutable}; using bash because PI WEB currently supports bash, zsh, and fish`;
|
|
274
207
|
}
|
|
275
|
-
return shell.
|
|
208
|
+
return shell.detectedExecutable === null ? shell.name : `${shell.name} (${shell.detectedExecutable})`;
|
|
276
209
|
}
|
|
277
210
|
function configEnvironment(options, configPath) {
|
|
278
211
|
return options.config === undefined ? {} : { PI_WEB_CONFIG: configPath };
|
|
@@ -302,27 +235,6 @@ function stopOrder(refs) {
|
|
|
302
235
|
function restartOrder(refs) {
|
|
303
236
|
return orderServiceRefs(refs, restartServiceOrder);
|
|
304
237
|
}
|
|
305
|
-
function productionServiceDefinitions(options, configPath, executables) {
|
|
306
|
-
const environment = configEnvironment(options, configPath);
|
|
307
|
-
return [
|
|
308
|
-
{
|
|
309
|
-
...serviceRefs.sessiond,
|
|
310
|
-
description: "PI WEB session daemon",
|
|
311
|
-
shellCommand: `exec ${executables.sessiond.command}`,
|
|
312
|
-
restart: "on-failure",
|
|
313
|
-
environment,
|
|
314
|
-
},
|
|
315
|
-
{
|
|
316
|
-
...serviceRefs.web,
|
|
317
|
-
description: "PI WEB server",
|
|
318
|
-
shellCommand: `exec ${executables.web.command}`,
|
|
319
|
-
restart: "on-failure",
|
|
320
|
-
environment,
|
|
321
|
-
after: ["sessiond"],
|
|
322
|
-
wants: ["sessiond"],
|
|
323
|
-
},
|
|
324
|
-
];
|
|
325
|
-
}
|
|
326
238
|
function devRootPath() {
|
|
327
239
|
return resolve(process.cwd());
|
|
328
240
|
}
|
|
@@ -335,97 +247,18 @@ function validateDevCheckout(root) {
|
|
|
335
247
|
if (!isRecord(parsed) || parsed["name"] !== PI_WEB_PACKAGE_NAME) {
|
|
336
248
|
throw new Error(`Development mode must be installed from a PI WEB checkout. ${packageJsonPath} is not ${PI_WEB_PACKAGE_NAME}.`);
|
|
337
249
|
}
|
|
338
|
-
const scripts = parsed["scripts"];
|
|
339
|
-
if (!isRecord(scripts))
|
|
340
|
-
throw new Error(`Development mode requires npm scripts in ${packageJsonPath}.`);
|
|
341
|
-
const requiredScripts = ["start:sessiond", "dev:web", "dev:client"];
|
|
342
|
-
const missing = requiredScripts.filter((script) => typeof scripts[script] !== "string");
|
|
343
|
-
if (missing.length > 0)
|
|
344
|
-
throw new Error(`Development mode requires missing npm scripts: ${missing.join(", ")}.`);
|
|
345
|
-
}
|
|
346
|
-
function devServiceDefinitions(options, configPath, root) {
|
|
347
|
-
const environment = configEnvironment(options, configPath);
|
|
348
|
-
return [
|
|
349
|
-
{
|
|
350
|
-
...serviceRefs.sessiond,
|
|
351
|
-
description: "PI WEB session daemon (dev)",
|
|
352
|
-
shellCommand: "exec npm run start:sessiond",
|
|
353
|
-
restart: "never",
|
|
354
|
-
environment,
|
|
355
|
-
workingDirectory: root,
|
|
356
|
-
},
|
|
357
|
-
{
|
|
358
|
-
...serviceRefs.uiDev,
|
|
359
|
-
description: "PI WEB UI dev server",
|
|
360
|
-
shellCommand: `exec /usr/bin/env bash -c ${serviceShellQuote('trap "kill 0" EXIT; npm run dev:web & npm run dev:client & wait')}`,
|
|
361
|
-
restart: "never",
|
|
362
|
-
environment,
|
|
363
|
-
after: ["sessiond"],
|
|
364
|
-
wants: ["sessiond"],
|
|
365
|
-
workingDirectory: root,
|
|
366
|
-
},
|
|
367
|
-
];
|
|
368
|
-
}
|
|
369
|
-
function dependencyLine(name, ids) {
|
|
370
|
-
if (ids === undefined || ids.length === 0)
|
|
371
|
-
return "";
|
|
372
|
-
return `${name}=${ids.map((id) => serviceRefs[id].systemdName).join(" ")}\n`;
|
|
373
|
-
}
|
|
374
|
-
function environmentLines(environment) {
|
|
375
|
-
return Object.entries(environment)
|
|
376
|
-
.map(([key, value]) => `Environment="${key}=${systemdEscape(value)}"\n`)
|
|
377
|
-
.join("");
|
|
378
|
-
}
|
|
379
|
-
function systemdUnit(service) {
|
|
380
|
-
const workingDirectory = service.workingDirectory === undefined ? "" : `WorkingDirectory=${systemdQuotedValue(service.workingDirectory)}\n`;
|
|
381
|
-
const restart = service.restart === "on-failure" ? "Restart=on-failure\nRestartSec=2\n" : "Restart=no\n";
|
|
382
|
-
return `[Unit]
|
|
383
|
-
Description=${service.description}
|
|
384
|
-
${dependencyLine("After", service.after)}${dependencyLine("Wants", service.wants)}
|
|
385
|
-
[Service]
|
|
386
|
-
Type=simple
|
|
387
|
-
${workingDirectory}${environmentLines(service.environment)}ExecStart=${serviceShellExecPrefix()} ${systemdServiceShellQuote(service.shellCommand)}
|
|
388
|
-
${restart}
|
|
389
|
-
[Install]
|
|
390
|
-
WantedBy=default.target
|
|
391
|
-
`;
|
|
392
|
-
}
|
|
393
|
-
function plistString(key, value, indent = " ") {
|
|
394
|
-
return `${indent}<key>${xmlEscape(key)}</key>\n${indent}<string>${xmlEscape(value)}</string>\n`;
|
|
395
|
-
}
|
|
396
|
-
function plistProgramArguments(service) {
|
|
397
|
-
const args = ["/usr/bin/env", detectServiceShell().executable, "-lc", service.shellCommand];
|
|
398
|
-
return ` <key>ProgramArguments</key>\n <array>\n${args.map((arg) => ` <string>${xmlEscape(arg)}</string>`).join("\n")}\n </array>\n`;
|
|
399
|
-
}
|
|
400
|
-
function plistEnvironment(environment) {
|
|
401
|
-
const entries = Object.entries(environment);
|
|
402
|
-
if (entries.length === 0)
|
|
403
|
-
return "";
|
|
404
|
-
return ` <key>EnvironmentVariables</key>\n <dict>\n${entries.map(([key, value]) => plistString(key, value, " ")).join("")} </dict>\n`;
|
|
405
250
|
}
|
|
406
251
|
function launchdLogPath(ref) {
|
|
407
252
|
return join(logDir, ref.logName);
|
|
408
253
|
}
|
|
409
|
-
function
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
414
|
-
<plist version="1.0">
|
|
415
|
-
<dict>
|
|
416
|
-
${plistString("Label", service.launchdLabel)}${plistProgramArguments(service)}${workingDirectory}${plistEnvironment(service.environment)} <key>RunAtLoad</key>
|
|
417
|
-
<true/>
|
|
418
|
-
${keepAlive}${plistString("StandardOutPath", launchdLogPath(service))}${plistString("StandardErrorPath", launchdLogPath(service))}</dict>
|
|
419
|
-
</plist>
|
|
420
|
-
`;
|
|
421
|
-
}
|
|
422
|
-
async function writeInitialConfig(options) {
|
|
423
|
-
const configPath = options.config === undefined ? defaultPiWebConfigPath() : resolve(options.config);
|
|
254
|
+
function installConfigPath(options) {
|
|
255
|
+
return options.config === undefined ? defaultPiWebConfigPath() : resolve(options.config);
|
|
256
|
+
}
|
|
257
|
+
async function writeInitialConfig(options, configPath) {
|
|
424
258
|
await mkdir(dirname(configPath), { recursive: true });
|
|
425
259
|
if (!existsSync(configPath)) {
|
|
426
260
|
await writeFile(configPath, examplePiWebConfig({ host: options.host, port: Number(options.port) }));
|
|
427
261
|
}
|
|
428
|
-
return configPath;
|
|
429
262
|
}
|
|
430
263
|
function systemdServicePath(ref) {
|
|
431
264
|
return join(systemdServiceDir, ref.systemdName);
|
|
@@ -446,18 +279,18 @@ function installedServiceRefs(backend) {
|
|
|
446
279
|
const installed = startOrder(allServiceRefs().filter((ref) => serviceFileExists(backend, ref)));
|
|
447
280
|
return installed.length === 0 ? productionServiceRefs() : installed;
|
|
448
281
|
}
|
|
449
|
-
async function installSystemdServices(
|
|
450
|
-
const selected = new Set(services.map((service) => service.id));
|
|
282
|
+
async function installSystemdServices(plan) {
|
|
283
|
+
const selected = new Set(plan.services.map((service) => service.id));
|
|
451
284
|
const obsolete = stopOrder(allServiceRefs().filter((ref) => !selected.has(ref.id)));
|
|
452
285
|
for (const ref of obsolete) {
|
|
453
286
|
runQuiet("systemctl", ["--user", "disable", "--now", ref.systemdName]);
|
|
454
287
|
await rm(systemdServicePath(ref), { force: true });
|
|
455
288
|
}
|
|
456
289
|
await mkdir(systemdServiceDir, { recursive: true });
|
|
457
|
-
for (const service of services) {
|
|
458
|
-
await writeFile(
|
|
290
|
+
for (const service of plan.services) {
|
|
291
|
+
await writeFile(join(systemdServiceDir, service.manager.systemdName), renderSystemdUnit(plan, service));
|
|
459
292
|
}
|
|
460
|
-
const names = services.map((service) => service.systemdName);
|
|
293
|
+
const names = plan.services.map((service) => service.manager.systemdName);
|
|
461
294
|
run("systemctl", ["--user", "daemon-reload"], { check: true });
|
|
462
295
|
run("systemctl", ["--user", "enable", ...names], { check: true });
|
|
463
296
|
run("systemctl", ["--user", "restart", ...names], { check: true });
|
|
@@ -483,8 +316,8 @@ function launchdStart(ref) {
|
|
|
483
316
|
launchdBootstrap(ref);
|
|
484
317
|
run("launchctl", ["kickstart", launchdServiceTarget(ref)], { check: true });
|
|
485
318
|
}
|
|
486
|
-
async function installLaunchdServices(
|
|
487
|
-
const selected = new Set(services.map((service) => service.id));
|
|
319
|
+
async function installLaunchdServices(plan) {
|
|
320
|
+
const selected = new Set(plan.services.map((service) => service.id));
|
|
488
321
|
await mkdir(launchdServiceDir, { recursive: true });
|
|
489
322
|
await mkdir(logDir, { recursive: true });
|
|
490
323
|
for (const ref of stopOrder(allServiceRefs()))
|
|
@@ -492,17 +325,21 @@ async function installLaunchdServices(services) {
|
|
|
492
325
|
for (const ref of allServiceRefs().filter((candidate) => !selected.has(candidate.id))) {
|
|
493
326
|
await rm(launchdPlistPath(ref), { force: true });
|
|
494
327
|
}
|
|
495
|
-
for (const service of services) {
|
|
496
|
-
|
|
328
|
+
for (const service of plan.services) {
|
|
329
|
+
const plistPath = join(launchdServiceDir, service.manager.launchdPlistName);
|
|
330
|
+
await writeFile(plistPath, renderLaunchdPlist(plan, service, logDir));
|
|
497
331
|
}
|
|
498
|
-
for (const service of services)
|
|
499
|
-
launchdStart(service);
|
|
332
|
+
for (const service of plan.services)
|
|
333
|
+
launchdStart(serviceRefFromPlan(service.id, service.manager));
|
|
500
334
|
}
|
|
501
|
-
async function installNativeServices(
|
|
502
|
-
if (backend.kind === "systemd")
|
|
503
|
-
await installSystemdServices(
|
|
335
|
+
async function installNativeServices(plan) {
|
|
336
|
+
if (plan.backend.kind === "systemd")
|
|
337
|
+
await installSystemdServices(plan);
|
|
504
338
|
else
|
|
505
|
-
await installLaunchdServices(
|
|
339
|
+
await installLaunchdServices(plan);
|
|
340
|
+
}
|
|
341
|
+
function serviceRefFromPlan(id, manager) {
|
|
342
|
+
return { id, ...manager };
|
|
506
343
|
}
|
|
507
344
|
async function uninstallSystemdServices() {
|
|
508
345
|
for (const ref of stopOrder(allServiceRefs())) {
|
|
@@ -597,6 +434,15 @@ function parseLaunchdField(output, field) {
|
|
|
597
434
|
const match = new RegExp(`^\\s*${field}\\s=\\s(.+)$`, "m").exec(output);
|
|
598
435
|
return match?.[1]?.trim();
|
|
599
436
|
}
|
|
437
|
+
export function launchdRuntimeDetails(output) {
|
|
438
|
+
const state = parseLaunchdField(output, "state") ?? "unknown";
|
|
439
|
+
const pid = parseLaunchdField(output, "pid");
|
|
440
|
+
const lastExitCode = parseLaunchdField(output, "last exit code");
|
|
441
|
+
const detail = state === "running"
|
|
442
|
+
? "running"
|
|
443
|
+
: lastExitCode === undefined ? state : `${state} (last exit code ${lastExitCode})`;
|
|
444
|
+
return { state, detail, pid };
|
|
445
|
+
}
|
|
600
446
|
function launchdRuntimeStatus(backend, ref) {
|
|
601
447
|
const target = launchdServiceTarget(ref);
|
|
602
448
|
const filePath = serviceFilePath(backend, ref);
|
|
@@ -606,10 +452,9 @@ function launchdRuntimeStatus(backend, ref) {
|
|
|
606
452
|
if (result.status !== 0) {
|
|
607
453
|
return makeServiceRuntimeStatus(ref, "stopped", firstOutputLine(result.stderr, result.stdout) ?? "not loaded", target, filePath);
|
|
608
454
|
}
|
|
609
|
-
const
|
|
610
|
-
const
|
|
611
|
-
|
|
612
|
-
return makeServiceRuntimeStatus(ref, health, state === "running" ? "running" : state, target, filePath, pid);
|
|
455
|
+
const details = launchdRuntimeDetails(result.stdout);
|
|
456
|
+
const health = details.state === "running" ? "running" : details.state === "unknown" ? "unknown" : "stopped";
|
|
457
|
+
return makeServiceRuntimeStatus(ref, health, details.detail, target, filePath, details.pid);
|
|
613
458
|
}
|
|
614
459
|
function runtimeStatus(backend, ref) {
|
|
615
460
|
return backend.kind === "systemd" ? systemdRuntimeStatus(backend, ref) : launchdRuntimeStatus(backend, ref);
|
|
@@ -635,36 +480,77 @@ function printServiceStatusReport(backend) {
|
|
|
635
480
|
console.log("\nUse `pi-web logs` for service logs.");
|
|
636
481
|
return statuses.every((status) => status.health === "running");
|
|
637
482
|
}
|
|
638
|
-
function
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
return [[`launchctl ${launchdDomain()}`, ["launchctl", "print", launchdDomain()]]];
|
|
483
|
+
function configuredServiceCommand(name) {
|
|
484
|
+
const value = process.env[name];
|
|
485
|
+
return value === undefined || value.trim() === "" ? undefined : value;
|
|
642
486
|
}
|
|
643
|
-
function
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
487
|
+
function productionNativeServicePlanInput(backend, shell, environment) {
|
|
488
|
+
return {
|
|
489
|
+
backend,
|
|
490
|
+
shell,
|
|
491
|
+
environment,
|
|
492
|
+
executables: {
|
|
493
|
+
sessiond: {
|
|
494
|
+
configuredCommand: configuredServiceCommand("PI_WEB_SESSIOND_EXEC"),
|
|
495
|
+
namedCommand: "pi-web-sessiond",
|
|
496
|
+
bundledEntrypointPath: packageEntrypointPath("sessiond"),
|
|
497
|
+
},
|
|
498
|
+
web: {
|
|
499
|
+
configuredCommand: configuredServiceCommand("PI_WEB_SERVER_EXEC"),
|
|
500
|
+
namedCommand: "pi-web-server",
|
|
501
|
+
bundledEntrypointPath: packageEntrypointPath("server"),
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
};
|
|
649
505
|
}
|
|
650
|
-
function
|
|
651
|
-
const shell =
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
506
|
+
function nativeServiceInstallCandidate(options, backend, configPath, devRoot) {
|
|
507
|
+
const shell = detectServiceShell();
|
|
508
|
+
const environment = configEnvironment(options, configPath);
|
|
509
|
+
if (options.mode === "production") {
|
|
510
|
+
return {
|
|
511
|
+
mode: "production",
|
|
512
|
+
input: productionNativeServicePlanInput(backend, shell, environment),
|
|
513
|
+
};
|
|
658
514
|
}
|
|
659
|
-
|
|
515
|
+
const root = devRoot ?? devRootPath();
|
|
516
|
+
return {
|
|
517
|
+
mode: "development",
|
|
518
|
+
input: {
|
|
519
|
+
backend,
|
|
520
|
+
shell,
|
|
521
|
+
environment,
|
|
522
|
+
workingDirectory: root,
|
|
523
|
+
packageJsonPath: join(root, "package.json"),
|
|
524
|
+
},
|
|
525
|
+
};
|
|
660
526
|
}
|
|
661
|
-
function
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
527
|
+
function printNativeServiceInstallFailure(failure) {
|
|
528
|
+
if (failure.kind === "plan-resolution") {
|
|
529
|
+
for (const item of failure.failures) {
|
|
530
|
+
if (item.kind === "probe-infrastructure") {
|
|
531
|
+
console.log(`✗ Service-manager probe infrastructure failure (${item.reason}): ${item.message}`);
|
|
532
|
+
}
|
|
533
|
+
else if (item.kind === "entrypoint-inspection-failure") {
|
|
534
|
+
console.log(`✗ Could not inspect bundled ${item.serviceId} entrypoint ${item.entrypointPath}: ${item.message}`);
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
console.log(`✗ ${item.namedCommand} is unavailable to the service manager, and bundled entrypoint ${item.bundledEntrypointPath} is missing.`);
|
|
538
|
+
if (item.namedCommandFailure !== null)
|
|
539
|
+
console.log(` ${item.namedCommandFailure}`);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
for (const item of failure.failures) {
|
|
545
|
+
if (item.kind === "probe-infrastructure") {
|
|
546
|
+
console.log(`✗ Service-manager probe infrastructure failure (${item.reason}): ${item.message}`);
|
|
547
|
+
}
|
|
548
|
+
else {
|
|
549
|
+
console.log(`✗ ${item.prerequisite.description}`);
|
|
550
|
+
if (item.detail !== null && item.detail !== item.prerequisite.description)
|
|
551
|
+
console.log(` ${item.detail}`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
668
554
|
}
|
|
669
555
|
async function install(args) {
|
|
670
556
|
const backend = requireServiceBackend("pi-web install");
|
|
@@ -672,19 +558,29 @@ async function install(args) {
|
|
|
672
558
|
const devRoot = options.mode === "dev" ? devRootPath() : undefined;
|
|
673
559
|
if (devRoot !== undefined)
|
|
674
560
|
validateDevCheckout(devRoot);
|
|
675
|
-
const
|
|
561
|
+
const configPath = installConfigPath(options);
|
|
562
|
+
const candidate = nativeServiceInstallCandidate(options, backend, configPath, devRoot);
|
|
676
563
|
console.log(`Running PI WEB ${options.mode} install preflight checks...`);
|
|
677
564
|
console.log(`Service backend: ${backend.label}`);
|
|
678
565
|
console.log(`Service shell: ${describeServiceShell()}`);
|
|
679
|
-
if (!
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
:
|
|
687
|
-
|
|
566
|
+
if (!printNodePtyNativeModuleCheck()) {
|
|
567
|
+
throw new Error("Install preflight checks failed without changing config or services. Fix the failure above, then run `pi-web doctor` for more detail.");
|
|
568
|
+
}
|
|
569
|
+
const result = await installNativeServiceCandidate(candidate, {
|
|
570
|
+
probe: createNativeServiceAuthoritativeProbe(),
|
|
571
|
+
fileExists: regularFileExists,
|
|
572
|
+
writeInitialConfig: () => writeInitialConfig(options, configPath),
|
|
573
|
+
replaceServices: installNativeServices,
|
|
574
|
+
});
|
|
575
|
+
if (!result.ok) {
|
|
576
|
+
printNativeServiceInstallFailure(result.failure);
|
|
577
|
+
if (nativeServiceInstallFailureNeedsPathAdvice(result.failure))
|
|
578
|
+
printPathSetupAdvice();
|
|
579
|
+
throw new Error("Install preflight checks failed without changing config or services. Fix the failure above, then run `pi-web doctor` for more detail.");
|
|
580
|
+
}
|
|
581
|
+
for (const service of result.plan.services.filter((item) => item.strategy.kind === "configured-override")) {
|
|
582
|
+
console.log(`! ${service.description} uses a configured command override; preflight did not execute that arbitrary command.`);
|
|
583
|
+
}
|
|
688
584
|
console.log(`\nPI WEB ${options.mode} services are installed and starting.`);
|
|
689
585
|
console.log(`Config: ${configPath}`);
|
|
690
586
|
if (options.mode === "dev") {
|
|
@@ -761,55 +657,37 @@ function logs() {
|
|
|
761
657
|
function serviceShellLabel() {
|
|
762
658
|
return `${detectServiceShell().name} -lc`;
|
|
763
659
|
}
|
|
764
|
-
function systemdUserServiceShellCommand(command, cwd) {
|
|
765
|
-
return [
|
|
766
|
-
"systemd-run",
|
|
767
|
-
"--user",
|
|
768
|
-
"--wait",
|
|
769
|
-
"--collect",
|
|
770
|
-
"--pipe",
|
|
771
|
-
"--quiet",
|
|
772
|
-
...serviceShellCommand(command, cwd),
|
|
773
|
-
];
|
|
774
|
-
}
|
|
775
660
|
function commandCheck(command) {
|
|
776
|
-
return `command -v ${command}`;
|
|
661
|
+
return `command -v ${serviceShellQuote(command)}`;
|
|
777
662
|
}
|
|
778
663
|
export function commandWithVersionCheck(command) {
|
|
779
664
|
const found = commandCheck(command);
|
|
665
|
+
const commandWord = serviceShellQuote(command);
|
|
780
666
|
if (detectServiceShell().name === "fish") {
|
|
781
|
-
return `${found} && begin; ${
|
|
667
|
+
return `${found} && begin; ${commandWord} --version 2>&1 || true; end`;
|
|
782
668
|
}
|
|
783
|
-
return `${found} && (${
|
|
669
|
+
return `${found} && (${commandWord} --version 2>&1 || true)`;
|
|
784
670
|
}
|
|
785
|
-
function nodeVersionCheck() {
|
|
786
|
-
return
|
|
787
|
-
|
|
788
|
-
"node
|
|
789
|
-
|
|
671
|
+
export function nodeVersionCheck() {
|
|
672
|
+
return nativeServicePrerequisiteShellCheck(detectServiceShell().name, {
|
|
673
|
+
id: "caller.node",
|
|
674
|
+
kind: "node-version",
|
|
675
|
+
command: "node",
|
|
676
|
+
minimumVersion: minimumSupportedNodeVersion,
|
|
677
|
+
description: `node >= ${minimumSupportedNodeVersion}`,
|
|
678
|
+
});
|
|
790
679
|
}
|
|
791
|
-
function
|
|
680
|
+
export function agentCommandForChecks(env = process.env) {
|
|
681
|
+
return effectivePiWebConfig({ env }).config.agent.command;
|
|
682
|
+
}
|
|
683
|
+
function generalDoctorChecks() {
|
|
792
684
|
const shell = serviceShellLabel();
|
|
793
|
-
const
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
[`${shell} can find pi`, serviceShellCommand(commandWithVersionCheck("pi"))],
|
|
799
|
-
];
|
|
800
|
-
}
|
|
801
|
-
const checks = [
|
|
802
|
-
...backendAvailabilityChecks(backend),
|
|
803
|
-
...baseShellChecks(backend),
|
|
804
|
-
[`${shell} can find npm`, serviceShellCommand(commandWithVersionCheck("npm"))],
|
|
805
|
-
[`${shell} can find pi`, serviceShellCommand(commandWithVersionCheck("pi"))],
|
|
685
|
+
const agentCommand = agentCommandForChecks();
|
|
686
|
+
return [
|
|
687
|
+
[`Caller login ${shell} can find node >= ${minimumSupportedNodeVersion}`, serviceShellCommand(nodeVersionCheck())],
|
|
688
|
+
[`Caller login ${shell} can find npm`, serviceShellCommand(commandWithVersionCheck("npm"))],
|
|
689
|
+
[`Caller login ${shell} can find ${agentCommand}`, serviceShellCommand(commandWithVersionCheck(agentCommand))],
|
|
806
690
|
];
|
|
807
|
-
const executables = resolveServiceExecutables(backend);
|
|
808
|
-
checks.push(...executables.web.checks, ...executables.sessiond.checks);
|
|
809
|
-
if (backend.kind === "systemd") {
|
|
810
|
-
checks.push([`systemd user ${shell} can find pi`, systemdUserServiceShellCommand(commandWithVersionCheck("pi"))]);
|
|
811
|
-
}
|
|
812
|
-
return checks;
|
|
813
691
|
}
|
|
814
692
|
function runChecks(checks) {
|
|
815
693
|
let failed = false;
|
|
@@ -837,11 +715,7 @@ function printCheckOutput(output) {
|
|
|
837
715
|
}
|
|
838
716
|
function optionalDoctorChecks() {
|
|
839
717
|
const shell = serviceShellLabel();
|
|
840
|
-
|
|
841
|
-
const checks = [[`${shell} can find optional ripgrep (rg)`, serviceShellCommand(commandCheck("rg"))]];
|
|
842
|
-
if (backend?.kind === "systemd")
|
|
843
|
-
checks.push([`systemd user ${shell} can find optional ripgrep (rg)`, systemdUserServiceShellCommand(commandCheck("rg"))]);
|
|
844
|
-
return checks;
|
|
718
|
+
return [[`Caller login ${shell} can find optional ripgrep (rg)`, serviceShellCommand(commandCheck("rg"))]];
|
|
845
719
|
}
|
|
846
720
|
function printOptionalDoctorChecks() {
|
|
847
721
|
let missingOptionalTool = false;
|
|
@@ -860,8 +734,98 @@ function printOptionalDoctorChecks() {
|
|
|
860
734
|
console.log(" PI WEB falls back to a bounded filesystem scan when rg is unavailable.");
|
|
861
735
|
}
|
|
862
736
|
}
|
|
863
|
-
function
|
|
864
|
-
|
|
737
|
+
function installedServiceDefinitions(backend, ids) {
|
|
738
|
+
return ids.map((id) => ({
|
|
739
|
+
id,
|
|
740
|
+
contents: readFileSync(serviceFilePath(backend, serviceRefs[id]), "utf8"),
|
|
741
|
+
}));
|
|
742
|
+
}
|
|
743
|
+
function nativeServiceDoctorTarget(backend) {
|
|
744
|
+
const ids = installedServiceIds(backend);
|
|
745
|
+
const mode = inferInstalledNativeServiceMode(ids);
|
|
746
|
+
if (mode === "ambiguous") {
|
|
747
|
+
return {
|
|
748
|
+
kind: "inspection-failure",
|
|
749
|
+
message: `installed service IDs do not identify one mode (${[...ids].join(", ") || "none"}).`,
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
if (mode === "none") {
|
|
753
|
+
return {
|
|
754
|
+
kind: "prospective-production",
|
|
755
|
+
input: productionNativeServicePlanInput(backend, detectServiceShell(), {}),
|
|
756
|
+
reason: "no installed service strategy is available",
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
const expectedIds = mode === "production"
|
|
760
|
+
? productionNativeServiceIds
|
|
761
|
+
: ["sessiond", "uiDev"];
|
|
762
|
+
const missingId = expectedIds.find((id) => !ids.has(id));
|
|
763
|
+
if (missingId !== undefined) {
|
|
764
|
+
return {
|
|
765
|
+
kind: "inspection-failure",
|
|
766
|
+
message: `installed ${mode} service set is incomplete; ${missingId} is missing.`,
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
let definitions;
|
|
770
|
+
try {
|
|
771
|
+
definitions = installedServiceDefinitions(backend, expectedIds);
|
|
772
|
+
}
|
|
773
|
+
catch (error) {
|
|
774
|
+
return {
|
|
775
|
+
kind: "inspection-failure",
|
|
776
|
+
message: error instanceof Error ? error.message : String(error),
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
if (mode === "development") {
|
|
780
|
+
const inspection = inspectInstalledDevelopmentServiceInput(backend, definitions);
|
|
781
|
+
return inspection.ok
|
|
782
|
+
? { kind: "installed-development", input: inspection.value }
|
|
783
|
+
: { kind: "inspection-failure", message: inspection.message };
|
|
784
|
+
}
|
|
785
|
+
const inspection = inspectInstalledProductionServiceContext(backend, definitions);
|
|
786
|
+
return inspection.ok
|
|
787
|
+
? {
|
|
788
|
+
kind: "prospective-production",
|
|
789
|
+
input: productionNativeServicePlanInput(backend, inspection.value.shell, inspection.value.environment),
|
|
790
|
+
reason: "installed executable strategy is not recorded",
|
|
791
|
+
}
|
|
792
|
+
: { kind: "inspection-failure", message: inspection.message };
|
|
793
|
+
}
|
|
794
|
+
async function printNativeServiceDoctorChecks(backend) {
|
|
795
|
+
const result = await runNativeServiceDoctor(nativeServiceDoctorTarget(backend), {
|
|
796
|
+
probe: createNativeServiceAuthoritativeProbe(),
|
|
797
|
+
fileExists: regularFileExists,
|
|
798
|
+
});
|
|
799
|
+
const report = formatNativeServiceDoctorResult(result);
|
|
800
|
+
for (const line of report.lines)
|
|
801
|
+
console.log(line);
|
|
802
|
+
printCallerContextComparisons(report);
|
|
803
|
+
return report;
|
|
804
|
+
}
|
|
805
|
+
function printCallerContextComparisons(report) {
|
|
806
|
+
if (report.plan === null || report.failedPrerequisites.length === 0)
|
|
807
|
+
return;
|
|
808
|
+
const seen = new Set();
|
|
809
|
+
for (const prerequisite of report.failedPrerequisites) {
|
|
810
|
+
if (seen.has(prerequisite.id))
|
|
811
|
+
continue;
|
|
812
|
+
seen.add(prerequisite.id);
|
|
813
|
+
const service = report.plan.services.find((candidate) => candidate.prerequisites.some((item) => item.id === prerequisite.id));
|
|
814
|
+
const command = nativeServicePrerequisiteShellCheck(report.plan.shell.name, prerequisite);
|
|
815
|
+
const result = captureServiceShell(report.plan.shell, command, service?.workingDirectory ?? null);
|
|
816
|
+
console.log(` Caller-invoked ${report.plan.shell.name} -lc ${result.status === 0 ? "satisfies" : "also does not satisfy"} ${prerequisite.description}; the service-manager result is authoritative.`);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
function captureServiceShell(shell, command, workingDirectory) {
|
|
820
|
+
const fullCommand = workingDirectory === null
|
|
821
|
+
? command
|
|
822
|
+
: `cd ${shellQuoteFor(shell.name, workingDirectory)} && ${command}`;
|
|
823
|
+
return capture("/usr/bin/env", [shell.executable, "-lc", fullCommand]);
|
|
824
|
+
}
|
|
825
|
+
function shellQuoteFor(shell, value) {
|
|
826
|
+
return shell === "fish" ? fishSingleQuote(value) : shellSingleQuote(value);
|
|
827
|
+
}
|
|
828
|
+
function printPathSetupAdvice(shell = detectServiceShell()) {
|
|
865
829
|
console.log("\nPATH setup advice:");
|
|
866
830
|
if (shell.name === "bash") {
|
|
867
831
|
console.log(" Detected bash. Put PATH setup for node/version managers/tools in ~/.bash_profile or ~/.profile.");
|
|
@@ -877,20 +841,30 @@ function printPathSetupAdvice() {
|
|
|
877
841
|
console.log(" Avoid relying on prompt hooks; PI WEB services run non-interactive login shells.");
|
|
878
842
|
}
|
|
879
843
|
}
|
|
844
|
+
export function doctorExitCode(generalReadinessOk, nativeServicePlanOk, nodePtyRuntimeOk) {
|
|
845
|
+
return generalReadinessOk && nativeServicePlanOk && nodePtyRuntimeOk ? 0 : 1;
|
|
846
|
+
}
|
|
880
847
|
async function doctor() {
|
|
881
848
|
const backend = currentServiceBackend();
|
|
882
849
|
console.log(`Platform: ${platformLabel()}`);
|
|
883
850
|
console.log(`Service backend: ${backend?.label ?? "manual run only"}`);
|
|
884
851
|
console.log(`Service shell: ${describeServiceShell()}`);
|
|
885
852
|
if (backend === undefined) {
|
|
886
|
-
console.log(`- Native user service checks skipped on ${platformLabel()}
|
|
853
|
+
console.log(`- Native user service plan checks skipped on ${platformLabel()}; no native-service drift is reported.`);
|
|
887
854
|
}
|
|
888
855
|
console.log("");
|
|
889
856
|
await printPiWebVersionReport();
|
|
890
|
-
console.log("\
|
|
891
|
-
const
|
|
857
|
+
console.log("\nGeneral login-shell readiness (separate from native-service requirements):");
|
|
858
|
+
const generalReadinessOk = runChecks(generalDoctorChecks());
|
|
892
859
|
printOptionalDoctorChecks();
|
|
893
|
-
|
|
860
|
+
console.log("\nNative terminal runtime readiness:");
|
|
861
|
+
const nodePtyNativeModuleOk = printNodePtyNativeModuleCheck();
|
|
862
|
+
const nodePtySpawnHelperOk = nodePtyNativeModuleOk ? printNodePtyDarwinSpawnHelperCheck() : true;
|
|
863
|
+
let nativeServiceReport = null;
|
|
864
|
+
if (backend !== undefined) {
|
|
865
|
+
console.log("\nNative service plan checks (service-manager context):");
|
|
866
|
+
nativeServiceReport = await printNativeServiceDoctorChecks(backend);
|
|
867
|
+
}
|
|
894
868
|
if (supportsSystemdUserServices()) {
|
|
895
869
|
const linger = isLingerEnabled();
|
|
896
870
|
if (linger === true) {
|
|
@@ -911,18 +885,27 @@ async function doctor() {
|
|
|
911
885
|
else {
|
|
912
886
|
console.log(`- systemd user lingering skipped on ${platformLabel()}`);
|
|
913
887
|
}
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
888
|
+
const nativeServicePlanOk = nativeServiceReport?.ok ?? true;
|
|
889
|
+
const pathFailure = !generalReadinessOk || nativeServiceReport?.pathAdviceRecommended === true;
|
|
890
|
+
if (pathFailure) {
|
|
891
|
+
console.log("\nIf a command works in your terminal but fails in the service-manager check, compare the caller and manager contexts above.");
|
|
892
|
+
const adviceShell = nativeServiceReport?.pathAdviceRecommended === true && nativeServiceReport.adviceShell !== null
|
|
893
|
+
? nativeServiceReport.adviceShell
|
|
894
|
+
: detectServiceShell();
|
|
895
|
+
printPathSetupAdvice(adviceShell);
|
|
919
896
|
}
|
|
920
|
-
if (
|
|
897
|
+
if (generalReadinessOk && backend === undefined) {
|
|
921
898
|
console.log(`\n${manualRunAdvice()}`);
|
|
922
899
|
}
|
|
923
|
-
if (
|
|
900
|
+
if (doctorExitCode(generalReadinessOk, nativeServicePlanOk, nodePtyNativeModuleOk && nodePtySpawnHelperOk) !== 0)
|
|
924
901
|
process.exitCode = 1;
|
|
925
902
|
}
|
|
903
|
+
function printNodePtyNativeModuleCheck() {
|
|
904
|
+
const result = formatNodePtyNativeModuleCheck(checkNodePtyNativeModule());
|
|
905
|
+
for (const line of result.lines)
|
|
906
|
+
console.log(line);
|
|
907
|
+
return result.ok;
|
|
908
|
+
}
|
|
926
909
|
function printNodePtyDarwinSpawnHelperCheck() {
|
|
927
910
|
const result = formatNodePtyDarwinSpawnHelperCheck(checkNodePtyDarwinSpawnHelper());
|
|
928
911
|
for (const line of result.lines)
|
|
@@ -943,7 +926,7 @@ Usage:
|
|
|
943
926
|
pi-web version
|
|
944
927
|
|
|
945
928
|
Recommended install:
|
|
946
|
-
npm install -g @jmfederico/pi-web
|
|
929
|
+
npm install -g @jmfederico/pi-web --allow-scripts=node-pty
|
|
947
930
|
pi-web install
|
|
948
931
|
|
|
949
932
|
Development service install from a checkout:
|