@jmfederico/pi-web 1.202606.7 → 1.202607.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -5
- package/dist/cli.js +291 -320
- package/dist/cli.js.map +1 -1
- package/dist/client/assets/CodeViewer-uASb28Y1.js +4 -0
- package/dist/client/assets/TerminalPanel-DfTmykFw.js +187 -0
- package/dist/client/assets/UnifiedDiffViewer-laQN1x7P.js +39 -0
- package/dist/client/assets/index-Dhapi0oR.js +3093 -0
- package/dist/client/assets/vendor-editor-core-vUi74DnD.js +12 -0
- package/dist/client/assets/vendor-editor-languages-DoaXUodB.js +46 -0
- package/dist/client/assets/vendor-editor-legacy-CYBnW6ZU.js +1 -0
- package/dist/client/assets/vendor-terminal-BrP-ENHg.css +1 -0
- package/dist/client/assets/vendor-terminal-D8k4UKM2.js +35 -0
- package/dist/client/index.html +9 -6
- package/dist/client/manifest.webmanifest +4 -4
- package/dist/config.js +144 -2
- package/dist/config.js.map +1 -1
- package/dist/docker/piWebDockerCommandPlan.js +211 -0
- package/dist/docker/piWebDockerCommandPlan.js.map +1 -0
- package/dist/nativeServices/serviceDoctor.js +480 -0
- package/dist/nativeServices/serviceDoctor.js.map +1 -0
- package/dist/nativeServices/serviceInstall.js +35 -0
- package/dist/nativeServices/serviceInstall.js.map +1 -0
- package/dist/nativeServices/servicePlan.js +358 -0
- package/dist/nativeServices/servicePlan.js.map +1 -0
- package/dist/nativeServices/serviceProbe.js +447 -0
- package/dist/nativeServices/serviceProbe.js.map +1 -0
- package/dist/nativeServices/serviceRendering.js +122 -0
- package/dist/nativeServices/serviceRendering.js.map +1 -0
- package/dist/pi-web-plugins/updates/pi-web-plugin.js +27 -5
- package/dist/pi-web-plugins/updates/updatesLogic.js +37 -5
- package/dist/piWebVersionReport.js +4 -0
- package/dist/piWebVersionReport.js.map +1 -1
- package/dist/plugin-api.d.ts +3 -1
- package/dist/server/activeAgentProfileProvider.js +25 -0
- package/dist/server/activeAgentProfileProvider.js.map +1 -0
- package/dist/server/app.js +69 -13
- package/dist/server/app.js.map +1 -1
- package/dist/server/browserMessageProjection.js +56 -0
- package/dist/server/browserMessageProjection.js.map +1 -0
- package/dist/server/configRoutes.js +147 -6
- package/dist/server/configRoutes.js.map +1 -1
- package/dist/server/machines/machineClient.js +19 -9
- package/dist/server/machines/machineClient.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 +76 -11
- 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 +6 -28
- package/dist/server/machines/machineService.js.map +1 -1
- package/dist/server/piPackageRoutes.js +91 -0
- package/dist/server/piPackageRoutes.js.map +1 -0
- package/dist/server/piPackageService.js +101 -0
- package/dist/server/piPackageService.js.map +1 -0
- package/dist/server/piWebPluginService.js +91 -20
- 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 +90 -37
- 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 +34 -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 +88 -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 +89 -29
- package/dist/server/sessions/authService.js.map +1 -1
- package/dist/server/sessions/builtinCommands.js +1 -1
- package/dist/server/sessions/builtinCommands.js.map +1 -1
- 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 +715 -127
- package/dist/server/sessions/piSessionService.js.map +1 -1
- package/dist/server/sessions/sessionArchiveMigration.js +590 -0
- package/dist/server/sessions/sessionArchiveMigration.js.map +1 -0
- package/dist/server/sessions/sessionArchiveStore.js +58 -26
- package/dist/server/sessions/sessionArchiveStore.js.map +1 -1
- package/dist/server/sessions/sessionCommandService.js +16 -0
- package/dist/server/sessions/sessionCommandService.js.map +1 -1
- package/dist/server/sessions/sessionNameGenerator.js +27 -44
- package/dist/server/sessions/sessionNameGenerator.js.map +1 -1
- package/dist/server/sessions/sessionRoutes.js +65 -3
- 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/spawnSessionTool.js +9 -4
- package/dist/server/sessions/spawnSessionTool.js.map +1 -1
- package/dist/server/sessions/spawnSubsessionTool.js +79 -32
- 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/server/workspaces/fileSuggestions.js +7 -3
- package/dist/server/workspaces/fileSuggestions.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/activity.js +0 -15
- package/dist/shared/activity.js.map +1 -1
- package/dist/shared/apiTypes.d.ts +160 -3
- package/dist/shared/apiTypes.js +6 -0
- package/dist/shared/apiTypes.js.map +1 -1
- package/dist/shared/capabilities.js +33 -2
- package/dist/shared/capabilities.js.map +1 -1
- package/dist/shared/federatedRoutes.js +14 -0
- package/dist/shared/federatedRoutes.js.map +1 -1
- package/dist/shared/piWebStatusParsing.js +26 -8
- package/dist/shared/piWebStatusParsing.js.map +1 -1
- package/dist/shared/thinkingLevels.d.ts +1 -1
- package/dist/shared/thinkingLevels.js +1 -1
- package/dist/shared/thinkingLevels.js.map +1 -1
- package/docs/config.md +75 -19
- package/docs/plugins.md +46 -18
- package/package.json +36 -32
- package/dist/client/assets/CodeViewer-CQU9mJ74.js +0 -4
- package/dist/client/assets/TerminalPanel-DtfVBsqI.js +0 -126
- package/dist/client/assets/UnifiedDiffViewer-BZQQK99S.js +0 -34
- package/dist/client/assets/index-CbE7df66.js +0 -2742
- package/dist/client/assets/vendor-editor-core-C7aWiQy0.js +0 -12
- package/dist/client/assets/vendor-editor-languages-EW34FfVy.js +0 -26
- package/dist/client/assets/vendor-editor-legacy-B4QLsWF8.js +0 -1
- package/dist/client/assets/vendor-terminal-DDGTF8rc.css +0 -1
- package/dist/client/assets/vendor-terminal-DjQ08hXu.js +0 -16
package/dist/cli.js
CHANGED
|
@@ -1,44 +1,28 @@
|
|
|
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 { installNativeServiceCandidate, nativeServiceInstallFailureNeedsPathAdvice, } from "./nativeServices/serviceInstall.js";
|
|
12
|
+
import { minimumSupportedNodeVersion, nativeServiceManagerRefs, productionNativeServiceIds, } from "./nativeServices/servicePlan.js";
|
|
13
|
+
import { formatNativeServiceDoctorResult, inferInstalledNativeServiceMode, inspectInstalledDevelopmentServiceInput, inspectInstalledProductionServiceContext, runNativeServiceDoctor, } from "./nativeServices/serviceDoctor.js";
|
|
14
|
+
import { createNativeServiceAuthoritativeProbe, nativeServicePrerequisiteShellCheck, } from "./nativeServices/serviceProbe.js";
|
|
15
|
+
import { renderLaunchdPlist, renderSystemdUnit } from "./nativeServices/serviceRendering.js";
|
|
11
16
|
const PI_WEB_PACKAGE_NAME = "@jmfederico/pi-web";
|
|
12
17
|
const systemdServiceDir = join(homedir(), ".config", "systemd", "user");
|
|
13
18
|
const launchdServiceDir = join(homedir(), "Library", "LaunchAgents");
|
|
14
19
|
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
20
|
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
|
-
},
|
|
21
|
+
sessiond: { id: "sessiond", ...nativeServiceManagerRefs.sessiond },
|
|
22
|
+
web: { id: "web", ...nativeServiceManagerRefs.web },
|
|
23
|
+
uiDev: { id: "uiDev", ...nativeServiceManagerRefs.uiDev },
|
|
40
24
|
};
|
|
41
|
-
const productionServiceIds = [
|
|
25
|
+
const productionServiceIds = [...productionNativeServiceIds];
|
|
42
26
|
const startServiceOrder = ["sessiond", "web", "uiDev"];
|
|
43
27
|
const stopServiceOrder = ["web", "uiDev", "sessiond"];
|
|
44
28
|
// Restart web/UI before sessiond: when `pi-web restart` runs in a pi-web
|
|
@@ -54,13 +38,16 @@ function platformLabel() {
|
|
|
54
38
|
return "Windows";
|
|
55
39
|
return process.platform;
|
|
56
40
|
}
|
|
57
|
-
function
|
|
58
|
-
if (
|
|
41
|
+
export function serviceBackendForPlatform(platform) {
|
|
42
|
+
if (platform === "linux")
|
|
59
43
|
return { kind: "systemd", label: "systemd user services" };
|
|
60
|
-
if (
|
|
44
|
+
if (platform === "darwin")
|
|
61
45
|
return { kind: "launchd", label: "LaunchAgents" };
|
|
62
46
|
return undefined;
|
|
63
47
|
}
|
|
48
|
+
function currentServiceBackend() {
|
|
49
|
+
return serviceBackendForPlatform(process.platform);
|
|
50
|
+
}
|
|
64
51
|
function requireServiceBackend(command) {
|
|
65
52
|
const backend = currentServiceBackend();
|
|
66
53
|
if (backend !== undefined)
|
|
@@ -105,7 +92,7 @@ function runQuiet(command, args) {
|
|
|
105
92
|
return capture(command, args).status;
|
|
106
93
|
}
|
|
107
94
|
function hasCommand(command) {
|
|
108
|
-
return capture("/usr/bin/env", ["sh", "-c", `command -v ${command}`]).status === 0;
|
|
95
|
+
return capture("/usr/bin/env", ["sh", "-c", `command -v ${shellSingleQuote(command)}`]).status === 0;
|
|
109
96
|
}
|
|
110
97
|
function isLingerEnabled() {
|
|
111
98
|
if (!hasCommand("loginctl"))
|
|
@@ -174,105 +161,50 @@ function shellSingleQuote(value) {
|
|
|
174
161
|
function fishSingleQuote(value) {
|
|
175
162
|
return `'${value.replaceAll("\\", "\\\\").replaceAll("'", "\\'")}'`;
|
|
176
163
|
}
|
|
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
164
|
function packageRootPath() {
|
|
192
165
|
return dirname(dirname(fileURLToPath(import.meta.url)));
|
|
193
166
|
}
|
|
194
167
|
function packageEntrypointPath(name) {
|
|
195
168
|
return join(packageRootPath(), "dist", "server", name === "server" ? "index.js" : "sessiond.js");
|
|
196
169
|
}
|
|
170
|
+
export function regularFileExists(path) {
|
|
171
|
+
return existsSync(path) && statSync(path).isFile();
|
|
172
|
+
}
|
|
197
173
|
function detectServiceShell() {
|
|
198
174
|
const userShell = userInfo().shell ?? undefined;
|
|
199
175
|
const envShell = process.env["SHELL"]?.trim();
|
|
200
176
|
const detected = envShell === undefined || envShell === "" ? userShell : envShell;
|
|
201
177
|
const name = basename(detected ?? "").replace(/^-/, "");
|
|
202
178
|
if (name === "bash" || name === "zsh" || name === "fish") {
|
|
203
|
-
return {
|
|
179
|
+
return {
|
|
180
|
+
name,
|
|
181
|
+
executable: detected ?? name,
|
|
182
|
+
source: "detected",
|
|
183
|
+
detectedExecutable: detected ?? name,
|
|
184
|
+
};
|
|
204
185
|
}
|
|
205
|
-
return {
|
|
186
|
+
return {
|
|
187
|
+
name: "bash",
|
|
188
|
+
executable: "bash",
|
|
189
|
+
source: "fallback",
|
|
190
|
+
detectedExecutable: detected ?? null,
|
|
191
|
+
};
|
|
206
192
|
}
|
|
207
193
|
function serviceShellCommand(command, cwd) {
|
|
208
194
|
const fullCommand = cwd === undefined ? command : `cd ${serviceShellQuote(cwd)} && ${command}`;
|
|
209
195
|
return ["/usr/bin/env", detectServiceShell().executable, "-lc", fullCommand];
|
|
210
196
|
}
|
|
211
|
-
function serviceShellExecPrefix() {
|
|
212
|
-
return `/usr/bin/env ${detectServiceShell().executable} -lc`;
|
|
213
|
-
}
|
|
214
197
|
function serviceShellQuote(value) {
|
|
215
198
|
return detectServiceShell().name === "fish" ? fishSingleQuote(value) : shellSingleQuote(value);
|
|
216
199
|
}
|
|
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
200
|
function describeServiceShell() {
|
|
269
201
|
const shell = detectServiceShell();
|
|
270
|
-
if (shell.fallback) {
|
|
271
|
-
return shell.
|
|
202
|
+
if (shell.source === "fallback") {
|
|
203
|
+
return shell.detectedExecutable === null
|
|
272
204
|
? "could not detect a supported login shell; using bash"
|
|
273
|
-
: `detected ${shell.
|
|
205
|
+
: `detected ${shell.detectedExecutable}; using bash because PI WEB currently supports bash, zsh, and fish`;
|
|
274
206
|
}
|
|
275
|
-
return shell.
|
|
207
|
+
return shell.detectedExecutable === null ? shell.name : `${shell.name} (${shell.detectedExecutable})`;
|
|
276
208
|
}
|
|
277
209
|
function configEnvironment(options, configPath) {
|
|
278
210
|
return options.config === undefined ? {} : { PI_WEB_CONFIG: configPath };
|
|
@@ -302,27 +234,6 @@ function stopOrder(refs) {
|
|
|
302
234
|
function restartOrder(refs) {
|
|
303
235
|
return orderServiceRefs(refs, restartServiceOrder);
|
|
304
236
|
}
|
|
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
237
|
function devRootPath() {
|
|
327
238
|
return resolve(process.cwd());
|
|
328
239
|
}
|
|
@@ -335,97 +246,18 @@ function validateDevCheckout(root) {
|
|
|
335
246
|
if (!isRecord(parsed) || parsed["name"] !== PI_WEB_PACKAGE_NAME) {
|
|
336
247
|
throw new Error(`Development mode must be installed from a PI WEB checkout. ${packageJsonPath} is not ${PI_WEB_PACKAGE_NAME}.`);
|
|
337
248
|
}
|
|
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
249
|
}
|
|
406
250
|
function launchdLogPath(ref) {
|
|
407
251
|
return join(logDir, ref.logName);
|
|
408
252
|
}
|
|
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);
|
|
253
|
+
function installConfigPath(options) {
|
|
254
|
+
return options.config === undefined ? defaultPiWebConfigPath() : resolve(options.config);
|
|
255
|
+
}
|
|
256
|
+
async function writeInitialConfig(options, configPath) {
|
|
424
257
|
await mkdir(dirname(configPath), { recursive: true });
|
|
425
258
|
if (!existsSync(configPath)) {
|
|
426
259
|
await writeFile(configPath, examplePiWebConfig({ host: options.host, port: Number(options.port) }));
|
|
427
260
|
}
|
|
428
|
-
return configPath;
|
|
429
261
|
}
|
|
430
262
|
function systemdServicePath(ref) {
|
|
431
263
|
return join(systemdServiceDir, ref.systemdName);
|
|
@@ -446,18 +278,18 @@ function installedServiceRefs(backend) {
|
|
|
446
278
|
const installed = startOrder(allServiceRefs().filter((ref) => serviceFileExists(backend, ref)));
|
|
447
279
|
return installed.length === 0 ? productionServiceRefs() : installed;
|
|
448
280
|
}
|
|
449
|
-
async function installSystemdServices(
|
|
450
|
-
const selected = new Set(services.map((service) => service.id));
|
|
281
|
+
async function installSystemdServices(plan) {
|
|
282
|
+
const selected = new Set(plan.services.map((service) => service.id));
|
|
451
283
|
const obsolete = stopOrder(allServiceRefs().filter((ref) => !selected.has(ref.id)));
|
|
452
284
|
for (const ref of obsolete) {
|
|
453
285
|
runQuiet("systemctl", ["--user", "disable", "--now", ref.systemdName]);
|
|
454
286
|
await rm(systemdServicePath(ref), { force: true });
|
|
455
287
|
}
|
|
456
288
|
await mkdir(systemdServiceDir, { recursive: true });
|
|
457
|
-
for (const service of services) {
|
|
458
|
-
await writeFile(
|
|
289
|
+
for (const service of plan.services) {
|
|
290
|
+
await writeFile(join(systemdServiceDir, service.manager.systemdName), renderSystemdUnit(plan, service));
|
|
459
291
|
}
|
|
460
|
-
const names = services.map((service) => service.systemdName);
|
|
292
|
+
const names = plan.services.map((service) => service.manager.systemdName);
|
|
461
293
|
run("systemctl", ["--user", "daemon-reload"], { check: true });
|
|
462
294
|
run("systemctl", ["--user", "enable", ...names], { check: true });
|
|
463
295
|
run("systemctl", ["--user", "restart", ...names], { check: true });
|
|
@@ -483,8 +315,8 @@ function launchdStart(ref) {
|
|
|
483
315
|
launchdBootstrap(ref);
|
|
484
316
|
run("launchctl", ["kickstart", launchdServiceTarget(ref)], { check: true });
|
|
485
317
|
}
|
|
486
|
-
async function installLaunchdServices(
|
|
487
|
-
const selected = new Set(services.map((service) => service.id));
|
|
318
|
+
async function installLaunchdServices(plan) {
|
|
319
|
+
const selected = new Set(plan.services.map((service) => service.id));
|
|
488
320
|
await mkdir(launchdServiceDir, { recursive: true });
|
|
489
321
|
await mkdir(logDir, { recursive: true });
|
|
490
322
|
for (const ref of stopOrder(allServiceRefs()))
|
|
@@ -492,17 +324,21 @@ async function installLaunchdServices(services) {
|
|
|
492
324
|
for (const ref of allServiceRefs().filter((candidate) => !selected.has(candidate.id))) {
|
|
493
325
|
await rm(launchdPlistPath(ref), { force: true });
|
|
494
326
|
}
|
|
495
|
-
for (const service of services) {
|
|
496
|
-
|
|
327
|
+
for (const service of plan.services) {
|
|
328
|
+
const plistPath = join(launchdServiceDir, service.manager.launchdPlistName);
|
|
329
|
+
await writeFile(plistPath, renderLaunchdPlist(plan, service, logDir));
|
|
497
330
|
}
|
|
498
|
-
for (const service of services)
|
|
499
|
-
launchdStart(service);
|
|
331
|
+
for (const service of plan.services)
|
|
332
|
+
launchdStart(serviceRefFromPlan(service.id, service.manager));
|
|
500
333
|
}
|
|
501
|
-
async function installNativeServices(
|
|
502
|
-
if (backend.kind === "systemd")
|
|
503
|
-
await installSystemdServices(
|
|
334
|
+
async function installNativeServices(plan) {
|
|
335
|
+
if (plan.backend.kind === "systemd")
|
|
336
|
+
await installSystemdServices(plan);
|
|
504
337
|
else
|
|
505
|
-
await installLaunchdServices(
|
|
338
|
+
await installLaunchdServices(plan);
|
|
339
|
+
}
|
|
340
|
+
function serviceRefFromPlan(id, manager) {
|
|
341
|
+
return { id, ...manager };
|
|
506
342
|
}
|
|
507
343
|
async function uninstallSystemdServices() {
|
|
508
344
|
for (const ref of stopOrder(allServiceRefs())) {
|
|
@@ -597,6 +433,15 @@ function parseLaunchdField(output, field) {
|
|
|
597
433
|
const match = new RegExp(`^\\s*${field}\\s=\\s(.+)$`, "m").exec(output);
|
|
598
434
|
return match?.[1]?.trim();
|
|
599
435
|
}
|
|
436
|
+
export function launchdRuntimeDetails(output) {
|
|
437
|
+
const state = parseLaunchdField(output, "state") ?? "unknown";
|
|
438
|
+
const pid = parseLaunchdField(output, "pid");
|
|
439
|
+
const lastExitCode = parseLaunchdField(output, "last exit code");
|
|
440
|
+
const detail = state === "running"
|
|
441
|
+
? "running"
|
|
442
|
+
: lastExitCode === undefined ? state : `${state} (last exit code ${lastExitCode})`;
|
|
443
|
+
return { state, detail, pid };
|
|
444
|
+
}
|
|
600
445
|
function launchdRuntimeStatus(backend, ref) {
|
|
601
446
|
const target = launchdServiceTarget(ref);
|
|
602
447
|
const filePath = serviceFilePath(backend, ref);
|
|
@@ -606,10 +451,9 @@ function launchdRuntimeStatus(backend, ref) {
|
|
|
606
451
|
if (result.status !== 0) {
|
|
607
452
|
return makeServiceRuntimeStatus(ref, "stopped", firstOutputLine(result.stderr, result.stdout) ?? "not loaded", target, filePath);
|
|
608
453
|
}
|
|
609
|
-
const
|
|
610
|
-
const
|
|
611
|
-
|
|
612
|
-
return makeServiceRuntimeStatus(ref, health, state === "running" ? "running" : state, target, filePath, pid);
|
|
454
|
+
const details = launchdRuntimeDetails(result.stdout);
|
|
455
|
+
const health = details.state === "running" ? "running" : details.state === "unknown" ? "unknown" : "stopped";
|
|
456
|
+
return makeServiceRuntimeStatus(ref, health, details.detail, target, filePath, details.pid);
|
|
613
457
|
}
|
|
614
458
|
function runtimeStatus(backend, ref) {
|
|
615
459
|
return backend.kind === "systemd" ? systemdRuntimeStatus(backend, ref) : launchdRuntimeStatus(backend, ref);
|
|
@@ -635,36 +479,77 @@ function printServiceStatusReport(backend) {
|
|
|
635
479
|
console.log("\nUse `pi-web logs` for service logs.");
|
|
636
480
|
return statuses.every((status) => status.health === "running");
|
|
637
481
|
}
|
|
638
|
-
function
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
return [[`launchctl ${launchdDomain()}`, ["launchctl", "print", launchdDomain()]]];
|
|
482
|
+
function configuredServiceCommand(name) {
|
|
483
|
+
const value = process.env[name];
|
|
484
|
+
return value === undefined || value.trim() === "" ? undefined : value;
|
|
642
485
|
}
|
|
643
|
-
function
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
486
|
+
function productionNativeServicePlanInput(backend, shell, environment) {
|
|
487
|
+
return {
|
|
488
|
+
backend,
|
|
489
|
+
shell,
|
|
490
|
+
environment,
|
|
491
|
+
executables: {
|
|
492
|
+
sessiond: {
|
|
493
|
+
configuredCommand: configuredServiceCommand("PI_WEB_SESSIOND_EXEC"),
|
|
494
|
+
namedCommand: "pi-web-sessiond",
|
|
495
|
+
bundledEntrypointPath: packageEntrypointPath("sessiond"),
|
|
496
|
+
},
|
|
497
|
+
web: {
|
|
498
|
+
configuredCommand: configuredServiceCommand("PI_WEB_SERVER_EXEC"),
|
|
499
|
+
namedCommand: "pi-web-server",
|
|
500
|
+
bundledEntrypointPath: packageEntrypointPath("server"),
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
};
|
|
649
504
|
}
|
|
650
|
-
function
|
|
651
|
-
const shell =
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
505
|
+
function nativeServiceInstallCandidate(options, backend, configPath, devRoot) {
|
|
506
|
+
const shell = detectServiceShell();
|
|
507
|
+
const environment = configEnvironment(options, configPath);
|
|
508
|
+
if (options.mode === "production") {
|
|
509
|
+
return {
|
|
510
|
+
mode: "production",
|
|
511
|
+
input: productionNativeServicePlanInput(backend, shell, environment),
|
|
512
|
+
};
|
|
658
513
|
}
|
|
659
|
-
|
|
514
|
+
const root = devRoot ?? devRootPath();
|
|
515
|
+
return {
|
|
516
|
+
mode: "development",
|
|
517
|
+
input: {
|
|
518
|
+
backend,
|
|
519
|
+
shell,
|
|
520
|
+
environment,
|
|
521
|
+
workingDirectory: root,
|
|
522
|
+
packageJsonPath: join(root, "package.json"),
|
|
523
|
+
},
|
|
524
|
+
};
|
|
660
525
|
}
|
|
661
|
-
function
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
526
|
+
function printNativeServiceInstallFailure(failure) {
|
|
527
|
+
if (failure.kind === "plan-resolution") {
|
|
528
|
+
for (const item of failure.failures) {
|
|
529
|
+
if (item.kind === "probe-infrastructure") {
|
|
530
|
+
console.log(`✗ Service-manager probe infrastructure failure (${item.reason}): ${item.message}`);
|
|
531
|
+
}
|
|
532
|
+
else if (item.kind === "entrypoint-inspection-failure") {
|
|
533
|
+
console.log(`✗ Could not inspect bundled ${item.serviceId} entrypoint ${item.entrypointPath}: ${item.message}`);
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
console.log(`✗ ${item.namedCommand} is unavailable to the service manager, and bundled entrypoint ${item.bundledEntrypointPath} is missing.`);
|
|
537
|
+
if (item.namedCommandFailure !== null)
|
|
538
|
+
console.log(` ${item.namedCommandFailure}`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
for (const item of failure.failures) {
|
|
544
|
+
if (item.kind === "probe-infrastructure") {
|
|
545
|
+
console.log(`✗ Service-manager probe infrastructure failure (${item.reason}): ${item.message}`);
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
console.log(`✗ ${item.prerequisite.description}`);
|
|
549
|
+
if (item.detail !== null && item.detail !== item.prerequisite.description)
|
|
550
|
+
console.log(` ${item.detail}`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
668
553
|
}
|
|
669
554
|
async function install(args) {
|
|
670
555
|
const backend = requireServiceBackend("pi-web install");
|
|
@@ -672,19 +557,26 @@ async function install(args) {
|
|
|
672
557
|
const devRoot = options.mode === "dev" ? devRootPath() : undefined;
|
|
673
558
|
if (devRoot !== undefined)
|
|
674
559
|
validateDevCheckout(devRoot);
|
|
675
|
-
const
|
|
560
|
+
const configPath = installConfigPath(options);
|
|
561
|
+
const candidate = nativeServiceInstallCandidate(options, backend, configPath, devRoot);
|
|
676
562
|
console.log(`Running PI WEB ${options.mode} install preflight checks...`);
|
|
677
563
|
console.log(`Service backend: ${backend.label}`);
|
|
678
564
|
console.log(`Service shell: ${describeServiceShell()}`);
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
565
|
+
const result = await installNativeServiceCandidate(candidate, {
|
|
566
|
+
probe: createNativeServiceAuthoritativeProbe(),
|
|
567
|
+
fileExists: regularFileExists,
|
|
568
|
+
writeInitialConfig: () => writeInitialConfig(options, configPath),
|
|
569
|
+
replaceServices: installNativeServices,
|
|
570
|
+
});
|
|
571
|
+
if (!result.ok) {
|
|
572
|
+
printNativeServiceInstallFailure(result.failure);
|
|
573
|
+
if (nativeServiceInstallFailureNeedsPathAdvice(result.failure))
|
|
574
|
+
printPathSetupAdvice();
|
|
575
|
+
throw new Error("Install preflight checks failed without changing config or services. Fix the failure above, then run `pi-web doctor` for more detail.");
|
|
576
|
+
}
|
|
577
|
+
for (const service of result.plan.services.filter((item) => item.strategy.kind === "configured-override")) {
|
|
578
|
+
console.log(`! ${service.description} uses a configured command override; preflight did not execute that arbitrary command.`);
|
|
579
|
+
}
|
|
688
580
|
console.log(`\nPI WEB ${options.mode} services are installed and starting.`);
|
|
689
581
|
console.log(`Config: ${configPath}`);
|
|
690
582
|
if (options.mode === "dev") {
|
|
@@ -761,55 +653,37 @@ function logs() {
|
|
|
761
653
|
function serviceShellLabel() {
|
|
762
654
|
return `${detectServiceShell().name} -lc`;
|
|
763
655
|
}
|
|
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
656
|
function commandCheck(command) {
|
|
776
|
-
return `command -v ${command}`;
|
|
657
|
+
return `command -v ${serviceShellQuote(command)}`;
|
|
777
658
|
}
|
|
778
659
|
export function commandWithVersionCheck(command) {
|
|
779
660
|
const found = commandCheck(command);
|
|
661
|
+
const commandWord = serviceShellQuote(command);
|
|
780
662
|
if (detectServiceShell().name === "fish") {
|
|
781
|
-
return `${found} && begin; ${
|
|
663
|
+
return `${found} && begin; ${commandWord} --version 2>&1 || true; end`;
|
|
782
664
|
}
|
|
783
|
-
return `${found} && (${
|
|
665
|
+
return `${found} && (${commandWord} --version 2>&1 || true)`;
|
|
784
666
|
}
|
|
785
|
-
function nodeVersionCheck() {
|
|
786
|
-
return
|
|
787
|
-
|
|
788
|
-
"node
|
|
789
|
-
|
|
667
|
+
export function nodeVersionCheck() {
|
|
668
|
+
return nativeServicePrerequisiteShellCheck(detectServiceShell().name, {
|
|
669
|
+
id: "caller.node",
|
|
670
|
+
kind: "node-version",
|
|
671
|
+
command: "node",
|
|
672
|
+
minimumVersion: minimumSupportedNodeVersion,
|
|
673
|
+
description: `node >= ${minimumSupportedNodeVersion}`,
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
export function agentCommandForChecks(env = process.env) {
|
|
677
|
+
return effectivePiWebConfig({ env }).config.agent.command;
|
|
790
678
|
}
|
|
791
|
-
function
|
|
679
|
+
function generalDoctorChecks() {
|
|
792
680
|
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"))],
|
|
681
|
+
const agentCommand = agentCommandForChecks();
|
|
682
|
+
return [
|
|
683
|
+
[`Caller login ${shell} can find node >= ${minimumSupportedNodeVersion}`, serviceShellCommand(nodeVersionCheck())],
|
|
684
|
+
[`Caller login ${shell} can find npm`, serviceShellCommand(commandWithVersionCheck("npm"))],
|
|
685
|
+
[`Caller login ${shell} can find ${agentCommand}`, serviceShellCommand(commandWithVersionCheck(agentCommand))],
|
|
806
686
|
];
|
|
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
687
|
}
|
|
814
688
|
function runChecks(checks) {
|
|
815
689
|
let failed = false;
|
|
@@ -837,11 +711,7 @@ function printCheckOutput(output) {
|
|
|
837
711
|
}
|
|
838
712
|
function optionalDoctorChecks() {
|
|
839
713
|
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;
|
|
714
|
+
return [[`Caller login ${shell} can find optional ripgrep (rg)`, serviceShellCommand(commandCheck("rg"))]];
|
|
845
715
|
}
|
|
846
716
|
function printOptionalDoctorChecks() {
|
|
847
717
|
let missingOptionalTool = false;
|
|
@@ -860,8 +730,98 @@ function printOptionalDoctorChecks() {
|
|
|
860
730
|
console.log(" PI WEB falls back to a bounded filesystem scan when rg is unavailable.");
|
|
861
731
|
}
|
|
862
732
|
}
|
|
863
|
-
function
|
|
864
|
-
|
|
733
|
+
function installedServiceDefinitions(backend, ids) {
|
|
734
|
+
return ids.map((id) => ({
|
|
735
|
+
id,
|
|
736
|
+
contents: readFileSync(serviceFilePath(backend, serviceRefs[id]), "utf8"),
|
|
737
|
+
}));
|
|
738
|
+
}
|
|
739
|
+
function nativeServiceDoctorTarget(backend) {
|
|
740
|
+
const ids = installedServiceIds(backend);
|
|
741
|
+
const mode = inferInstalledNativeServiceMode(ids);
|
|
742
|
+
if (mode === "ambiguous") {
|
|
743
|
+
return {
|
|
744
|
+
kind: "inspection-failure",
|
|
745
|
+
message: `installed service IDs do not identify one mode (${[...ids].join(", ") || "none"}).`,
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
if (mode === "none") {
|
|
749
|
+
return {
|
|
750
|
+
kind: "prospective-production",
|
|
751
|
+
input: productionNativeServicePlanInput(backend, detectServiceShell(), {}),
|
|
752
|
+
reason: "no installed service strategy is available",
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
const expectedIds = mode === "production"
|
|
756
|
+
? productionNativeServiceIds
|
|
757
|
+
: ["sessiond", "uiDev"];
|
|
758
|
+
const missingId = expectedIds.find((id) => !ids.has(id));
|
|
759
|
+
if (missingId !== undefined) {
|
|
760
|
+
return {
|
|
761
|
+
kind: "inspection-failure",
|
|
762
|
+
message: `installed ${mode} service set is incomplete; ${missingId} is missing.`,
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
let definitions;
|
|
766
|
+
try {
|
|
767
|
+
definitions = installedServiceDefinitions(backend, expectedIds);
|
|
768
|
+
}
|
|
769
|
+
catch (error) {
|
|
770
|
+
return {
|
|
771
|
+
kind: "inspection-failure",
|
|
772
|
+
message: error instanceof Error ? error.message : String(error),
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
if (mode === "development") {
|
|
776
|
+
const inspection = inspectInstalledDevelopmentServiceInput(backend, definitions);
|
|
777
|
+
return inspection.ok
|
|
778
|
+
? { kind: "installed-development", input: inspection.value }
|
|
779
|
+
: { kind: "inspection-failure", message: inspection.message };
|
|
780
|
+
}
|
|
781
|
+
const inspection = inspectInstalledProductionServiceContext(backend, definitions);
|
|
782
|
+
return inspection.ok
|
|
783
|
+
? {
|
|
784
|
+
kind: "prospective-production",
|
|
785
|
+
input: productionNativeServicePlanInput(backend, inspection.value.shell, inspection.value.environment),
|
|
786
|
+
reason: "installed executable strategy is not recorded",
|
|
787
|
+
}
|
|
788
|
+
: { kind: "inspection-failure", message: inspection.message };
|
|
789
|
+
}
|
|
790
|
+
async function printNativeServiceDoctorChecks(backend) {
|
|
791
|
+
const result = await runNativeServiceDoctor(nativeServiceDoctorTarget(backend), {
|
|
792
|
+
probe: createNativeServiceAuthoritativeProbe(),
|
|
793
|
+
fileExists: regularFileExists,
|
|
794
|
+
});
|
|
795
|
+
const report = formatNativeServiceDoctorResult(result);
|
|
796
|
+
for (const line of report.lines)
|
|
797
|
+
console.log(line);
|
|
798
|
+
printCallerContextComparisons(report);
|
|
799
|
+
return report;
|
|
800
|
+
}
|
|
801
|
+
function printCallerContextComparisons(report) {
|
|
802
|
+
if (report.plan === null || report.failedPrerequisites.length === 0)
|
|
803
|
+
return;
|
|
804
|
+
const seen = new Set();
|
|
805
|
+
for (const prerequisite of report.failedPrerequisites) {
|
|
806
|
+
if (seen.has(prerequisite.id))
|
|
807
|
+
continue;
|
|
808
|
+
seen.add(prerequisite.id);
|
|
809
|
+
const service = report.plan.services.find((candidate) => candidate.prerequisites.some((item) => item.id === prerequisite.id));
|
|
810
|
+
const command = nativeServicePrerequisiteShellCheck(report.plan.shell.name, prerequisite);
|
|
811
|
+
const result = captureServiceShell(report.plan.shell, command, service?.workingDirectory ?? null);
|
|
812
|
+
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.`);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
function captureServiceShell(shell, command, workingDirectory) {
|
|
816
|
+
const fullCommand = workingDirectory === null
|
|
817
|
+
? command
|
|
818
|
+
: `cd ${shellQuoteFor(shell.name, workingDirectory)} && ${command}`;
|
|
819
|
+
return capture("/usr/bin/env", [shell.executable, "-lc", fullCommand]);
|
|
820
|
+
}
|
|
821
|
+
function shellQuoteFor(shell, value) {
|
|
822
|
+
return shell === "fish" ? fishSingleQuote(value) : shellSingleQuote(value);
|
|
823
|
+
}
|
|
824
|
+
function printPathSetupAdvice(shell = detectServiceShell()) {
|
|
865
825
|
console.log("\nPATH setup advice:");
|
|
866
826
|
if (shell.name === "bash") {
|
|
867
827
|
console.log(" Detected bash. Put PATH setup for node/version managers/tools in ~/.bash_profile or ~/.profile.");
|
|
@@ -877,20 +837,28 @@ function printPathSetupAdvice() {
|
|
|
877
837
|
console.log(" Avoid relying on prompt hooks; PI WEB services run non-interactive login shells.");
|
|
878
838
|
}
|
|
879
839
|
}
|
|
840
|
+
export function doctorExitCode(generalReadinessOk, nativeServicePlanOk, nodePtySpawnHelperOk) {
|
|
841
|
+
return generalReadinessOk && nativeServicePlanOk && nodePtySpawnHelperOk ? 0 : 1;
|
|
842
|
+
}
|
|
880
843
|
async function doctor() {
|
|
881
844
|
const backend = currentServiceBackend();
|
|
882
845
|
console.log(`Platform: ${platformLabel()}`);
|
|
883
846
|
console.log(`Service backend: ${backend?.label ?? "manual run only"}`);
|
|
884
847
|
console.log(`Service shell: ${describeServiceShell()}`);
|
|
885
848
|
if (backend === undefined) {
|
|
886
|
-
console.log(`- Native user service checks skipped on ${platformLabel()}
|
|
849
|
+
console.log(`- Native user service plan checks skipped on ${platformLabel()}; no native-service drift is reported.`);
|
|
887
850
|
}
|
|
888
851
|
console.log("");
|
|
889
852
|
await printPiWebVersionReport();
|
|
890
|
-
console.log("\
|
|
891
|
-
const
|
|
853
|
+
console.log("\nGeneral login-shell readiness (separate from native-service requirements):");
|
|
854
|
+
const generalReadinessOk = runChecks(generalDoctorChecks());
|
|
892
855
|
printOptionalDoctorChecks();
|
|
893
856
|
const nodePtySpawnHelperOk = printNodePtyDarwinSpawnHelperCheck();
|
|
857
|
+
let nativeServiceReport = null;
|
|
858
|
+
if (backend !== undefined) {
|
|
859
|
+
console.log("\nNative service plan checks (service-manager context):");
|
|
860
|
+
nativeServiceReport = await printNativeServiceDoctorChecks(backend);
|
|
861
|
+
}
|
|
894
862
|
if (supportsSystemdUserServices()) {
|
|
895
863
|
const linger = isLingerEnabled();
|
|
896
864
|
if (linger === true) {
|
|
@@ -911,16 +879,19 @@ async function doctor() {
|
|
|
911
879
|
else {
|
|
912
880
|
console.log(`- systemd user lingering skipped on ${platformLabel()}`);
|
|
913
881
|
}
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
882
|
+
const nativeServicePlanOk = nativeServiceReport?.ok ?? true;
|
|
883
|
+
const pathFailure = !generalReadinessOk || nativeServiceReport?.pathAdviceRecommended === true;
|
|
884
|
+
if (pathFailure) {
|
|
885
|
+
console.log("\nIf a command works in your terminal but fails in the service-manager check, compare the caller and manager contexts above.");
|
|
886
|
+
const adviceShell = nativeServiceReport?.pathAdviceRecommended === true && nativeServiceReport.adviceShell !== null
|
|
887
|
+
? nativeServiceReport.adviceShell
|
|
888
|
+
: detectServiceShell();
|
|
889
|
+
printPathSetupAdvice(adviceShell);
|
|
919
890
|
}
|
|
920
|
-
if (
|
|
891
|
+
if (generalReadinessOk && backend === undefined) {
|
|
921
892
|
console.log(`\n${manualRunAdvice()}`);
|
|
922
893
|
}
|
|
923
|
-
if (
|
|
894
|
+
if (doctorExitCode(generalReadinessOk, nativeServicePlanOk, nodePtySpawnHelperOk) !== 0)
|
|
924
895
|
process.exitCode = 1;
|
|
925
896
|
}
|
|
926
897
|
function printNodePtyDarwinSpawnHelperCheck() {
|