@jmfederico/pi-web 1.202608.1 → 1.202609.0

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.
Files changed (107) hide show
  1. package/dist/cli.js +220 -108
  2. package/dist/cli.js.map +1 -1
  3. package/dist/client/apple-touch-icon-dev.png +0 -0
  4. package/dist/client/assets/{CodeViewer-mEiBZFA5.js → CodeViewer-CGAlg9S8.js} +1 -1
  5. package/dist/client/assets/{TerminalPanel-O9381Eme.js → TerminalPanel-xhJRhOas.js} +1 -1
  6. package/dist/client/assets/{index-uI2AGFUy.js → index-DXQKhn1P.js} +570 -335
  7. package/dist/client/assets/vendor-editor-core-CXO8gGab.js +12 -0
  8. package/dist/client/assets/vendor-editor-languages-CpW4sJsX.js +46 -0
  9. package/dist/client/favicon-dev.svg +8 -0
  10. package/dist/client/index.html +4 -4
  11. package/dist/client/pwa-icon-dev-192.png +0 -0
  12. package/dist/client/pwa-icon-dev-512.png +0 -0
  13. package/dist/config.js +21 -0
  14. package/dist/config.js.map +1 -1
  15. package/dist/environment.js +4 -0
  16. package/dist/environment.js.map +1 -0
  17. package/dist/nativeServices/installedServiceDefinitions.js +601 -0
  18. package/dist/nativeServices/installedServiceDefinitions.js.map +1 -0
  19. package/dist/nativeServices/serviceAction.js +174 -0
  20. package/dist/nativeServices/serviceAction.js.map +1 -0
  21. package/dist/nativeServices/serviceDoctor.js +374 -71
  22. package/dist/nativeServices/serviceDoctor.js.map +1 -1
  23. package/dist/{pi-web-plugins → pi-packages}/relays/markdownDocument.js +1 -1
  24. package/dist/pi-packages/relays/package.json +11 -0
  25. package/dist/{pi-web-plugins → pi-packages}/relays/pi-web-plugin.js +1 -1
  26. package/dist/pi-packages/relays/prompts/relay-worktree.md +28 -0
  27. package/dist/pi-packages/relays/prompts/relay.md +45 -0
  28. package/dist/{pi-web-plugins → pi-packages}/relays/relayDiscovery.js +3 -3
  29. package/dist/{pi-web-plugins → pi-packages}/relays/relaysPanelElement.js +2 -2
  30. package/dist/pi-packages/relays/skills/relay/SKILL.md +94 -0
  31. package/dist/pi-packages/relays/skills/relay-runner/SKILL.md +245 -0
  32. package/dist/pi-web-plugins/info/infoInternals.js +15 -0
  33. package/dist/pi-web-plugins/updates/pi-web-plugin.js +44 -34
  34. package/dist/piWebVersionReport.js +63 -10
  35. package/dist/piWebVersionReport.js.map +1 -1
  36. package/dist/server/app.js +19 -3
  37. package/dist/server/app.js.map +1 -1
  38. package/dist/server/configRoutes.js +6 -1
  39. package/dist/server/configRoutes.js.map +1 -1
  40. package/dist/server/deploymentIdentity.js +67 -0
  41. package/dist/server/deploymentIdentity.js.map +1 -0
  42. package/dist/server/deploymentIdentityRoutes.js +22 -0
  43. package/dist/server/deploymentIdentityRoutes.js.map +1 -0
  44. package/dist/server/knownAutoInstallPiPackages.js +30 -0
  45. package/dist/server/knownAutoInstallPiPackages.js.map +1 -0
  46. package/dist/server/machines/machineService.js +1 -0
  47. package/dist/server/machines/machineService.js.map +1 -1
  48. package/dist/server/notices/serverNoticeRoutes.js +34 -0
  49. package/dist/server/notices/serverNoticeRoutes.js.map +1 -0
  50. package/dist/server/notices/serverNoticeService.js +26 -0
  51. package/dist/server/notices/serverNoticeService.js.map +1 -0
  52. package/dist/server/notices/serverNoticeStore.js +78 -0
  53. package/dist/server/notices/serverNoticeStore.js.map +1 -0
  54. package/dist/server/piPackageIdentity.js +25 -0
  55. package/dist/server/piPackageIdentity.js.map +1 -0
  56. package/dist/server/piPackageService.js +69 -8
  57. package/dist/server/piPackageService.js.map +1 -1
  58. package/dist/server/piWebStatus.js +14 -3
  59. package/dist/server/piWebStatus.js.map +1 -1
  60. package/dist/server/projectTrustRoutes.js +80 -0
  61. package/dist/server/projectTrustRoutes.js.map +1 -0
  62. package/dist/server/projects/projectService.js +3 -1
  63. package/dist/server/projects/projectService.js.map +1 -1
  64. package/dist/server/sessiond/autoInstallPiPackages.js +63 -0
  65. package/dist/server/sessiond/autoInstallPiPackages.js.map +1 -0
  66. package/dist/server/sessiond/sessionProxyRoutes.js +2 -0
  67. package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
  68. package/dist/server/sessiond/sessionServiceDependencies.js +1 -0
  69. package/dist/server/sessiond/sessionServiceDependencies.js.map +1 -1
  70. package/dist/server/sessiond.js +36 -5
  71. package/dist/server/sessiond.js.map +1 -1
  72. package/dist/server/sessions/attachmentService.js +1 -5
  73. package/dist/server/sessions/attachmentService.js.map +1 -1
  74. package/dist/server/sessions/piSessionManagerGateway.js +171 -1
  75. package/dist/server/sessions/piSessionManagerGateway.js.map +1 -1
  76. package/dist/server/sessions/piSessionService.js +535 -20
  77. package/dist/server/sessions/piSessionService.js.map +1 -1
  78. package/dist/server/sessions/sessionModelScope.js +160 -0
  79. package/dist/server/sessions/sessionModelScope.js.map +1 -0
  80. package/dist/server/sessions/sessionNameGenerator.js +6 -6
  81. package/dist/server/sessions/sessionNameGenerator.js.map +1 -1
  82. package/dist/server/sessions/sessionRoutes.js +41 -0
  83. package/dist/server/sessions/sessionRoutes.js.map +1 -1
  84. package/dist/server/sessions/transcriptBranchCache.js +81 -0
  85. package/dist/server/sessions/transcriptBranchCache.js.map +1 -0
  86. package/dist/server/storage/piPackageDismissalStore.js +78 -0
  87. package/dist/server/storage/piPackageDismissalStore.js.map +1 -0
  88. package/dist/server/terminals/terminalService.js +18 -1
  89. package/dist/server/terminals/terminalService.js.map +1 -1
  90. package/dist/server/workspaces/projectPiWebConfig.js +6 -1
  91. package/dist/server/workspaces/projectPiWebConfig.js.map +1 -1
  92. package/dist/server/workspaces/workspaceRemovalService.js +16 -5
  93. package/dist/server/workspaces/workspaceRemovalService.js.map +1 -1
  94. package/dist/shared/apiTypes.js.map +1 -1
  95. package/dist/shared/federatedRoutes.js +10 -0
  96. package/dist/shared/federatedRoutes.js.map +1 -1
  97. package/dist/shared/piWebStatusParsing.js +4 -0
  98. package/dist/shared/piWebStatusParsing.js.map +1 -1
  99. package/dist/shared/pluginApiTypes.d.ts +2 -0
  100. package/docs/config.md +45 -4
  101. package/docs/plugins.md +25 -11
  102. package/package.json +6 -6
  103. package/dist/client/assets/vendor-editor-core-vUi74DnD.js +0 -12
  104. package/dist/client/assets/vendor-editor-languages-DoaXUodB.js +0 -46
  105. package/dist/pi-web-plugins/relays/package.json +0 -9
  106. /package/dist/{pi-web-plugins → pi-packages}/relays/vendor/README.md +0 -0
  107. /package/dist/{pi-web-plugins → pi-packages}/relays/vendor/marked.esm.js +0 -0
package/dist/cli.js CHANGED
@@ -7,13 +7,16 @@ import { basename, dirname, join, resolve } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { defaultPiWebConfigPath, defaultPiWebDataDir, examplePiWebConfig } from "./config.js";
9
9
  import { piWebDockerCommand } from "./docker/piWebDockerCommandPlan.js";
10
+ import { ownEnvironmentValue } from "./environment.js";
10
11
  import { runPluginRecoveryCli } from "./pluginRecoveryCli.js";
11
- import { packageVersion, printPiWebVersionReport } from "./piWebVersionReport.js";
12
+ import { packageVersion, printPiWebVersionReport, probeRunningComponentReady, runningComponentsReady, } from "./piWebVersionReport.js";
12
13
  import { checkNodePtyDarwinSpawnHelper, formatNodePtyDarwinSpawnHelperCheck } from "./server/diagnostics/nodePtySpawnHelper.js";
13
14
  import { checkNodePtyNativeModule, formatNodePtyNativeModuleCheck } from "./server/diagnostics/nodePtyNativeModule.js";
14
15
  import { installNativeServiceCandidate, nativeServiceInstallFailureNeedsPathAdvice, } from "./nativeServices/serviceInstall.js";
16
+ import { launchdBootoutArgs, launchdServiceTarget, orderServices, performServiceAction, serviceStartOrder, serviceStopOrder, settleLaunchdServiceUnload, startLaunchdService, } from "./nativeServices/serviceAction.js";
15
17
  import { minimumSupportedNodeVersion, nativeServiceManagerRefs, productionNativeServiceIds, } from "./nativeServices/servicePlan.js";
16
- import { formatNativeServiceDoctorResult, inferInstalledNativeServiceMode, inspectInstalledDevelopmentServiceInput, inspectInstalledProductionServiceContext, runNativeServiceDoctor, } from "./nativeServices/serviceDoctor.js";
18
+ import { inspectInstalledNativeServiceDefinitions, } from "./nativeServices/installedServiceDefinitions.js";
19
+ import { formatNativeServiceDoctorResult, inferInstalledNativeServiceMode, inspectInstalledDevelopmentServiceInput, inspectInstalledProductionServiceContext, runNativeServiceDoctor, selectManagedNativeServiceConfig, } from "./nativeServices/serviceDoctor.js";
17
20
  import { createNativeServiceAuthoritativeProbe, nativeServicePrerequisiteShellCheck, } from "./nativeServices/serviceProbe.js";
18
21
  import { renderLaunchdPlist, renderSystemdUnit } from "./nativeServices/serviceRendering.js";
19
22
  const PI_WEB_PACKAGE_NAME = "@jmfederico/pi-web";
@@ -26,12 +29,6 @@ const serviceRefs = {
26
29
  uiDev: { id: "uiDev", ...nativeServiceManagerRefs.uiDev },
27
30
  };
28
31
  const productionServiceIds = [...productionNativeServiceIds];
29
- const startServiceOrder = ["sessiond", "web", "uiDev"];
30
- const stopServiceOrder = ["web", "uiDev", "sessiond"];
31
- // Restart web/UI before sessiond: when `pi-web restart` runs in a pi-web
32
- // terminal (owned by sessiond), restarting sessiond kills the command, so any
33
- // services handled after it would never be restarted.
34
- const restartServiceOrder = ["web", "uiDev", "sessiond"];
35
32
  function platformLabel() {
36
33
  if (process.platform === "darwin")
37
34
  return "macOS";
@@ -57,9 +54,6 @@ function requireServiceBackend(command) {
57
54
  return backend;
58
55
  throw new Error(`\`${command}\` requires a supported per-user service manager (systemd user services or LaunchAgents) and is not supported on ${platformLabel()}.\n\n${manualRunAdvice()}`);
59
56
  }
60
- function supportsSystemdUserServices() {
61
- return currentServiceBackend()?.kind === "systemd";
62
- }
63
57
  function manualRunAdvice() {
64
58
  return [
65
59
  "Run PI WEB manually from a checkout:",
@@ -91,6 +85,15 @@ function capture(command, args) {
91
85
  const stderr = outputText(result.stderr);
92
86
  return { status: result.status ?? 1, stdout: outputText(result.stdout), stderr: stderr === "" ? errorMessage : stderr };
93
87
  }
88
+ function captureInstalledServiceDefinitionCommand(command, args) {
89
+ const result = spawnSync(command, args);
90
+ return {
91
+ status: result.status ?? 1,
92
+ stdout: result.stdout,
93
+ stderr: result.stderr,
94
+ spawnError: result.error instanceof Error ? result.error.message : "",
95
+ };
96
+ }
94
97
  function runQuiet(command, args) {
95
98
  return capture(command, args).status;
96
99
  }
@@ -221,21 +224,11 @@ function allServiceRefs() {
221
224
  function productionServiceRefs() {
222
225
  return serviceRefList(productionServiceIds);
223
226
  }
224
- function orderServiceRefs(refs, order) {
225
- const byId = new Map(refs.map((ref) => [ref.id, ref]));
226
- return order.flatMap((id) => {
227
- const ref = byId.get(id);
228
- return ref === undefined ? [] : [ref];
229
- });
230
- }
231
227
  function startOrder(refs) {
232
- return orderServiceRefs(refs, startServiceOrder);
228
+ return orderServices(refs, serviceStartOrder);
233
229
  }
234
230
  function stopOrder(refs) {
235
- return orderServiceRefs(refs, stopServiceOrder);
236
- }
237
- function restartOrder(refs) {
238
- return orderServiceRefs(refs, restartServiceOrder);
231
+ return orderServices(refs, serviceStopOrder);
239
232
  }
240
233
  function devRootPath() {
241
234
  return resolve(process.cwd());
@@ -300,30 +293,30 @@ async function installSystemdServices(plan) {
300
293
  function launchdDomain() {
301
294
  return `gui/${String(userInfo().uid)}`;
302
295
  }
303
- function launchdServiceTarget(ref) {
304
- return `${launchdDomain()}/${ref.launchdLabel}`;
305
- }
306
- function launchdIsLoaded(ref) {
307
- return capture("launchctl", ["print", launchdServiceTarget(ref)]).status === 0;
308
- }
309
- function launchdBootout(ref) {
310
- runQuiet("launchctl", ["bootout", launchdServiceTarget(ref)]);
296
+ function currentLaunchdServiceTarget(ref) {
297
+ return launchdServiceTarget(launchdDomain(), ref);
311
298
  }
312
- function launchdBootstrap(ref) {
313
- run("launchctl", ["bootstrap", launchdDomain(), launchdPlistPath(ref)], { check: true });
314
- run("launchctl", ["enable", launchdServiceTarget(ref)], { check: true });
299
+ function launchdActionContext() {
300
+ return { domain: launchdDomain(), plistPath: launchdPlistPath };
315
301
  }
316
- function launchdStart(ref) {
317
- if (!launchdIsLoaded(ref))
318
- launchdBootstrap(ref);
319
- run("launchctl", ["kickstart", launchdServiceTarget(ref)], { check: true });
302
+ function serviceActionDeps(backend, probeEnvironment = process.env) {
303
+ return {
304
+ run: (command, args) => run(command, args),
305
+ runQuiet,
306
+ sleep: (ms) => new Promise((resolve) => {
307
+ setTimeout(resolve, ms);
308
+ }),
309
+ isServiceRunning: (ref) => runtimeStatus(backend, ref).health === "running",
310
+ isComponentReady: (component) => probeRunningComponentReady(component, { configEnv: probeEnvironment }),
311
+ };
320
312
  }
321
313
  async function installLaunchdServices(plan) {
322
314
  const selected = new Set(plan.services.map((service) => service.id));
323
315
  await mkdir(launchdServiceDir, { recursive: true });
324
316
  await mkdir(logDir, { recursive: true });
325
- for (const ref of stopOrder(allServiceRefs()))
326
- launchdBootout(ref);
317
+ for (const ref of stopOrder(allServiceRefs())) {
318
+ runQuiet("launchctl", launchdBootoutArgs(currentLaunchdServiceTarget(ref)));
319
+ }
327
320
  for (const ref of allServiceRefs().filter((candidate) => !selected.has(candidate.id))) {
328
321
  await rm(launchdPlistPath(ref), { force: true });
329
322
  }
@@ -331,8 +324,16 @@ async function installLaunchdServices(plan) {
331
324
  const plistPath = join(launchdServiceDir, service.manager.launchdPlistName);
332
325
  await writeFile(plistPath, renderLaunchdPlist(plan, service, logDir));
333
326
  }
334
- for (const service of plan.services)
335
- launchdStart(serviceRefFromPlan(service.id, service.manager));
327
+ // Settle each bootout before deciding bootstrap vs kickstart: acting on a
328
+ // label launchd is still asynchronously unloading races the teardown and can
329
+ // lose the service entirely (the same defect the restart path had).
330
+ const context = launchdActionContext();
331
+ const deps = serviceActionDeps(plan.backend);
332
+ for (const service of plan.services) {
333
+ const ref = serviceRefFromPlan(service.id, service.manager);
334
+ await settleLaunchdServiceUnload(currentLaunchdServiceTarget(ref), deps);
335
+ startLaunchdService(ref, context, deps);
336
+ }
336
337
  }
337
338
  async function installNativeServices(plan) {
338
339
  if (plan.backend.kind === "systemd")
@@ -352,7 +353,7 @@ async function uninstallSystemdServices() {
352
353
  }
353
354
  async function uninstallLaunchdServices() {
354
355
  for (const ref of stopOrder(allServiceRefs())) {
355
- launchdBootout(ref);
356
+ runQuiet("launchctl", launchdBootoutArgs(currentLaunchdServiceTarget(ref)));
356
357
  await rm(launchdPlistPath(ref), { force: true });
357
358
  }
358
359
  }
@@ -446,7 +447,7 @@ export function launchdRuntimeDetails(output) {
446
447
  return { state, detail, pid };
447
448
  }
448
449
  function launchdRuntimeStatus(backend, ref) {
449
- const target = launchdServiceTarget(ref);
450
+ const target = currentLaunchdServiceTarget(ref);
450
451
  const filePath = serviceFilePath(backend, ref);
451
452
  if (!serviceFileExists(backend, ref))
452
453
  return makeServiceRuntimeStatus(ref, "not-installed", "not installed", target, filePath);
@@ -612,40 +613,27 @@ async function uninstall() {
612
613
  await uninstallNativeServices(backend);
613
614
  console.log(`PI WEB ${backend.label} removed. Production and development service files were removed; config and data were left in place.`);
614
615
  }
615
- function systemdServiceAction(action, refs) {
616
- const orderedRefs = action === "stop" ? stopOrder(refs) : action === "restart" ? restartOrder(refs) : startOrder(refs);
617
- run("systemctl", ["--user", action, ...orderedRefs.map((ref) => ref.systemdName)], { check: true });
618
- }
619
- function launchdServiceAction(action, refs) {
620
- if (action === "stop") {
621
- for (const ref of stopOrder(refs))
622
- launchdBootout(ref);
623
- return;
624
- }
625
- if (action === "restart") {
626
- // Restart each service fully (bootout + start) before moving to the next,
627
- // so the web/UI services are back up before sessiond is restarted.
628
- for (const ref of restartOrder(refs)) {
629
- launchdBootout(ref);
630
- launchdStart(ref);
631
- }
632
- return;
633
- }
634
- for (const ref of startOrder(refs))
635
- launchdStart(ref);
636
- }
637
- function serviceAction(action) {
616
+ async function serviceAction(action) {
638
617
  const backend = requireServiceBackend(`pi-web ${action}`);
639
618
  if (action === "status") {
640
619
  if (!printServiceStatusReport(backend))
641
620
  process.exitCode = 1;
642
621
  return;
643
622
  }
623
+ await performLifecycleServiceAction(backend, action, process.env);
624
+ }
625
+ async function performLifecycleServiceAction(backend, action, probeEnvironment) {
644
626
  const refs = installedServiceRefs(backend);
645
- if (backend.kind === "systemd")
646
- systemdServiceAction(action, refs);
647
- else
648
- launchdServiceAction(action, refs);
627
+ const input = { backend, action, refs, launchdContext: launchdActionContext() };
628
+ const result = await performServiceAction(input, serviceActionDeps(backend, probeEnvironment));
629
+ if (result.unreadyServices.length > 0) {
630
+ for (const ref of result.unreadyServices) {
631
+ const target = backend.kind === "systemd" ? ref.systemdName : currentLaunchdServiceTarget(ref);
632
+ console.log(`✗ ${serviceDisplayName(ref)} did not become ready (${target}).`);
633
+ }
634
+ console.log(" Check `pi-web status` and `pi-web logs` for details.");
635
+ process.exitCode = 1;
636
+ }
649
637
  }
650
638
  /** Offline restart guidance used only after a recovery config mutation. */
651
639
  export function sessionDaemonRestartPlan(options = {}) {
@@ -696,9 +684,9 @@ export function sessionDaemonRestartPlan(options = {}) {
696
684
  };
697
685
  }
698
686
  function activeDockerMode(env) {
699
- if (!truthyEnvValue(env["PI_WEB_DOCKER_RUNTIME"]))
687
+ if (!truthyEnvValue(ownEnvironmentValue(env, "PI_WEB_DOCKER_RUNTIME")))
700
688
  return undefined;
701
- if (env["PI_WEB_DOCKER_MODE"] === "dev")
689
+ if (ownEnvironmentValue(env, "PI_WEB_DOCKER_MODE") === "dev")
702
690
  return "dev";
703
691
  return "runtime";
704
692
  }
@@ -792,14 +780,115 @@ function printOptionalDoctorChecks() {
792
780
  console.log(" PI WEB falls back to a bounded filesystem scan when rg is unavailable.");
793
781
  }
794
782
  }
795
- function installedServiceDefinitions(backend, ids) {
796
- return ids.map((id) => ({
783
+ function installedServiceDefinitions(backend, ids, purpose) {
784
+ const sources = ids.map((id) => ({
797
785
  id,
798
- contents: readFileSync(serviceFilePath(backend, serviceRefs[id]), "utf8"),
786
+ path: serviceFilePath(backend, serviceRefs[id]),
787
+ systemdName: serviceRefs[id].systemdName,
788
+ launchdTarget: currentLaunchdServiceTarget(serviceRefs[id]),
799
789
  }));
790
+ return inspectInstalledNativeServiceDefinitions(backend, sources, {
791
+ readFile: (path) => readFileSync(path),
792
+ realpath: realpathSync,
793
+ capture: captureInstalledServiceDefinitionCommand,
794
+ }, purpose);
795
+ }
796
+ /** Build an isolated probe environment without mutating the caller or process. */
797
+ export function managedServiceProbeEnvironment(backend, definitions, callerEnvironment) {
798
+ const selection = selectManagedNativeServiceConfig(backend, definitions, ownEnvironmentValue(callerEnvironment, "PI_WEB_CONFIG"));
799
+ if (!selection.ok)
800
+ return selection;
801
+ if (selection.value.source === "installed") {
802
+ return {
803
+ ok: true,
804
+ value: { ...callerEnvironment, PI_WEB_CONFIG: selection.value.configPath },
805
+ };
806
+ }
807
+ if (selection.value.source === "default" && definitions.length > 0) {
808
+ // An own empty value preserves default resolution while masking any
809
+ // prototype-inherited PI_WEB_CONFIG from downstream environment lookups.
810
+ return { ok: true, value: { ...callerEnvironment, PI_WEB_CONFIG: "" } };
811
+ }
812
+ return { ok: true, value: callerEnvironment };
813
+ }
814
+ function installedServiceProbeEnvironment(backend, ids, callerEnvironment, purpose, inspectDefinitions) {
815
+ // The explicit caller override wins without requiring installed definitions
816
+ // to be readable or canonical.
817
+ const callerConfigPath = ownEnvironmentValue(callerEnvironment, "PI_WEB_CONFIG");
818
+ if (callerConfigPath !== undefined && callerConfigPath !== "") {
819
+ return { ok: true, value: callerEnvironment };
820
+ }
821
+ if (ids.size === 0)
822
+ return { ok: true, value: callerEnvironment };
823
+ const definitions = inspectDefinitions(backend, [...ids], purpose);
824
+ if (!definitions.ok)
825
+ return definitions;
826
+ return managedServiceProbeEnvironment(backend, definitions.value, callerEnvironment);
800
827
  }
801
- function nativeServiceDoctorTarget(backend) {
802
- const ids = installedServiceIds(backend);
828
+ function managedServiceProbeFailure(backend, message) {
829
+ return [
830
+ `Could not select managed PI_WEB_CONFIG from installed ${backend.label}: ${message}`,
831
+ "Repair the reported service-manager override or definition, then re-run `pi-web install` (or `pi-web install --dev`); a nonempty PI_WEB_CONFIG explicitly overrides managed config selection for this command.",
832
+ ].join("\n");
833
+ }
834
+ /**
835
+ * Dispatch commands whose probes may need managed config. The service-manager,
836
+ * definition, lifecycle, doctor, and version boundaries are injected so this
837
+ * exact orchestration can be tested without mutating live services or process
838
+ * environment.
839
+ */
840
+ export async function runReadinessCliCommand(command, dependencies) {
841
+ if (command === "version") {
842
+ await dependencies.printVersion();
843
+ return;
844
+ }
845
+ const environment = dependencies.environment;
846
+ if (command === "start" || command === "restart") {
847
+ const backend = dependencies.requireBackend(`pi-web ${command}`);
848
+ const installedIds = dependencies.installedServiceIds(backend);
849
+ const probeEnvironment = installedServiceProbeEnvironment(backend, installedIds, environment, command, dependencies.inspectDefinitions);
850
+ if (!probeEnvironment.ok)
851
+ throw new Error(managedServiceProbeFailure(backend, probeEnvironment.message));
852
+ await dependencies.runLifecycle(backend, command, probeEnvironment.value);
853
+ return;
854
+ }
855
+ const backend = dependencies.currentBackend();
856
+ const installedIds = backend === undefined
857
+ ? new Set()
858
+ : dependencies.installedServiceIds(backend);
859
+ let versionReportOptions = { configEnv: environment };
860
+ let managedInspectionFailed = false;
861
+ if (backend !== undefined && activeDockerMode(environment) === undefined) {
862
+ const probeEnvironment = installedServiceProbeEnvironment(backend, installedIds, environment, "doctor", dependencies.inspectDefinitions);
863
+ if (probeEnvironment.ok) {
864
+ versionReportOptions = { configEnv: probeEnvironment.value };
865
+ }
866
+ else {
867
+ managedInspectionFailed = true;
868
+ versionReportOptions = { webEndpointError: managedServiceProbeFailure(backend, probeEnvironment.message) };
869
+ }
870
+ }
871
+ await dependencies.runDoctor({
872
+ backend,
873
+ installedIds,
874
+ environment,
875
+ versionReportOptions,
876
+ managedInspectionFailed,
877
+ });
878
+ }
879
+ function readinessCliCommandDependencies() {
880
+ return {
881
+ environment: process.env,
882
+ currentBackend: currentServiceBackend,
883
+ requireBackend: requireServiceBackend,
884
+ installedServiceIds,
885
+ inspectDefinitions: installedServiceDefinitions,
886
+ runLifecycle: performLifecycleServiceAction,
887
+ runDoctor: doctor,
888
+ printVersion: async () => { await printPiWebVersionReport(); },
889
+ };
890
+ }
891
+ function nativeServiceDoctorTarget(backend, ids) {
803
892
  const mode = inferInstalledNativeServiceMode(ids);
804
893
  if (mode === "ambiguous") {
805
894
  return {
@@ -824,23 +913,16 @@ function nativeServiceDoctorTarget(backend) {
824
913
  message: `installed ${mode} service set is incomplete; ${missingId} is missing.`,
825
914
  };
826
915
  }
827
- let definitions;
828
- try {
829
- definitions = installedServiceDefinitions(backend, expectedIds);
830
- }
831
- catch (error) {
832
- return {
833
- kind: "inspection-failure",
834
- message: error instanceof Error ? error.message : String(error),
835
- };
836
- }
916
+ const definitions = installedServiceDefinitions(backend, expectedIds, "doctor");
917
+ if (!definitions.ok)
918
+ return { kind: "inspection-failure", message: definitions.message };
837
919
  if (mode === "development") {
838
- const inspection = inspectInstalledDevelopmentServiceInput(backend, definitions);
920
+ const inspection = inspectInstalledDevelopmentServiceInput(backend, definitions.value);
839
921
  return inspection.ok
840
922
  ? { kind: "installed-development", input: inspection.value }
841
923
  : { kind: "inspection-failure", message: inspection.message };
842
924
  }
843
- const inspection = inspectInstalledProductionServiceContext(backend, definitions);
925
+ const inspection = inspectInstalledProductionServiceContext(backend, definitions.value);
844
926
  return inspection.ok
845
927
  ? {
846
928
  kind: "prospective-production",
@@ -849,8 +931,8 @@ function nativeServiceDoctorTarget(backend) {
849
931
  }
850
932
  : { kind: "inspection-failure", message: inspection.message };
851
933
  }
852
- async function printNativeServiceDoctorChecks(backend) {
853
- const result = await runNativeServiceDoctor(nativeServiceDoctorTarget(backend), {
934
+ async function printNativeServiceDoctorChecks(backend, installedIds) {
935
+ const result = await runNativeServiceDoctor(nativeServiceDoctorTarget(backend, installedIds), {
854
936
  probe: createNativeServiceAuthoritativeProbe(),
855
937
  fileExists: regularFileExists,
856
938
  });
@@ -899,11 +981,30 @@ function printPathSetupAdvice(shell = detectServiceShell()) {
899
981
  console.log(" Avoid relying on prompt hooks; PI WEB services run non-interactive login shells.");
900
982
  }
901
983
  }
902
- export function doctorExitCode(generalReadinessOk, nativeServicePlanOk, nodePtyRuntimeOk) {
903
- return generalReadinessOk && nativeServicePlanOk && nodePtyRuntimeOk ? 0 : 1;
904
- }
905
- async function doctor() {
906
- const backend = currentServiceBackend();
984
+ /**
985
+ * Running components an installed deployment must keep ready: the API-serving
986
+ * component whenever the web or UI dev service file is installed (the version
987
+ * endpoint probes the API port in both modes), and the session daemon whenever
988
+ * its service file is installed. Docker deployments have no native service
989
+ * files, so the Docker runtime marker expects both components. A machine with
990
+ * nothing installed expects nothing: doctor's manual-run guidance stays a
991
+ * passing outcome there.
992
+ */
993
+ export function expectedRunningComponents(installedServices, env) {
994
+ if (activeDockerMode(env) !== undefined)
995
+ return ["web", "sessiond"];
996
+ const expected = [];
997
+ if (installedServices.has("web") || installedServices.has("uiDev"))
998
+ expected.push("web");
999
+ if (installedServices.has("sessiond"))
1000
+ expected.push("sessiond");
1001
+ return expected;
1002
+ }
1003
+ export function doctorExitCode(generalReadinessOk, nativeServicePlanOk, nodePtyRuntimeOk, runningComponentsOk) {
1004
+ return generalReadinessOk && nativeServicePlanOk && nodePtyRuntimeOk && runningComponentsOk ? 0 : 1;
1005
+ }
1006
+ async function doctor(context) {
1007
+ const { backend, installedIds, environment, versionReportOptions, managedInspectionFailed } = context;
907
1008
  console.log(`Platform: ${platformLabel()}`);
908
1009
  console.log(`Service backend: ${backend?.label ?? "manual run only"}`);
909
1010
  console.log(`Service shell: ${describeServiceShell()}`);
@@ -911,7 +1012,13 @@ async function doctor() {
911
1012
  console.log(`- Native user service plan checks skipped on ${platformLabel()}; no native-service drift is reported.`);
912
1013
  }
913
1014
  console.log("");
914
- await printPiWebVersionReport();
1015
+ const runningVersionInfo = await printPiWebVersionReport(versionReportOptions);
1016
+ const expectedComponents = expectedRunningComponents(installedIds, environment);
1017
+ const runningComponentsOk = runningComponentsReady(runningVersionInfo, expectedComponents);
1018
+ if (!runningComponentsOk) {
1019
+ console.log("\n✗ Installed PI WEB services are unavailable or stale (see \"Running services\" above).");
1020
+ console.log(" Run `pi-web restart`, then check `pi-web status`.");
1021
+ }
915
1022
  console.log("\nGeneral login-shell readiness (separate from native-service requirements):");
916
1023
  const generalReadinessOk = runChecks(generalDoctorChecks());
917
1024
  printOptionalDoctorChecks();
@@ -921,9 +1028,15 @@ async function doctor() {
921
1028
  let nativeServiceReport = null;
922
1029
  if (backend !== undefined) {
923
1030
  console.log("\nNative service plan checks (service-manager context):");
924
- nativeServiceReport = await printNativeServiceDoctorChecks(backend);
1031
+ if (managedInspectionFailed) {
1032
+ console.log("✗ Installed native-service plan could not be inspected consistently with the readiness probe (see \"Running services\" above).");
1033
+ console.log(" Repair the reported service-manager definition before relying on doctor results.");
1034
+ }
1035
+ else {
1036
+ nativeServiceReport = await printNativeServiceDoctorChecks(backend, installedIds);
1037
+ }
925
1038
  }
926
- if (supportsSystemdUserServices()) {
1039
+ if (backend?.kind === "systemd") {
927
1040
  const linger = isLingerEnabled();
928
1041
  if (linger === true) {
929
1042
  console.log("✓ systemd user lingering enabled");
@@ -943,7 +1056,7 @@ async function doctor() {
943
1056
  else {
944
1057
  console.log(`- systemd user lingering skipped on ${platformLabel()}`);
945
1058
  }
946
- const nativeServicePlanOk = nativeServiceReport?.ok ?? true;
1059
+ const nativeServicePlanOk = !managedInspectionFailed && (nativeServiceReport?.ok ?? true);
947
1060
  const pathFailure = !generalReadinessOk || nativeServiceReport?.pathAdviceRecommended === true;
948
1061
  if (pathFailure) {
949
1062
  console.log("\nIf a command works in your terminal but fails in the service-manager check, compare the caller and manager contexts above.");
@@ -955,7 +1068,7 @@ async function doctor() {
955
1068
  if (generalReadinessOk && backend === undefined) {
956
1069
  console.log(`\n${manualRunAdvice()}`);
957
1070
  }
958
- if (doctorExitCode(generalReadinessOk, nativeServicePlanOk, nodePtyNativeModuleOk && nodePtySpawnHelperOk) !== 0)
1071
+ if (doctorExitCode(generalReadinessOk, nativeServicePlanOk, nodePtyNativeModuleOk && nodePtySpawnHelperOk, runningComponentsOk) !== 0)
959
1072
  process.exitCode = 1;
960
1073
  }
961
1074
  function printNodePtyNativeModuleCheck() {
@@ -999,16 +1112,15 @@ async function main() {
999
1112
  await install(args);
1000
1113
  else if (command === "uninstall")
1001
1114
  await uninstall();
1002
- else if (command === "start" || command === "stop" || command === "restart" || command === "status")
1003
- serviceAction(command);
1115
+ else if (command === "start" || command === "restart" || command === "doctor" || command === "version") {
1116
+ await runReadinessCliCommand(command, readinessCliCommandDependencies());
1117
+ }
1118
+ else if (command === "stop" || command === "status")
1119
+ await serviceAction(command);
1004
1120
  else if (command === "logs")
1005
1121
  logs();
1006
1122
  else if (command === "plugins")
1007
1123
  runPluginRecoveryCli(args, { restartPlan: (context) => sessionDaemonRestartPlan(context) });
1008
- else if (command === "doctor")
1009
- await doctor();
1010
- else if (command === "version")
1011
- await printPiWebVersionReport();
1012
1124
  else if (command === "--version" || command === "-v")
1013
1125
  console.log(packageVersion());
1014
1126
  else if (command === "help" || command === "--help" || command === "-h")