@jmfederico/pi-web 1.202608.0 → 1.202608.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.
Files changed (258) hide show
  1. package/README.md +4 -4
  2. package/dist/cli.js +287 -113
  3. package/dist/cli.js.map +1 -1
  4. package/dist/client/assets/CodeViewer-BMWwxG7q.js +4 -0
  5. package/dist/client/assets/{TerminalPanel-fYrKc3jh.js → TerminalPanel-CacQDIYn.js} +6 -6
  6. package/dist/client/assets/index-DUW2xnoV.js +4279 -0
  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/index.html +3 -3
  10. package/dist/config.js +110 -73
  11. package/dist/config.js.map +1 -1
  12. package/dist/docker/piWebDockerCommandPlan.js +4 -3
  13. package/dist/docker/piWebDockerCommandPlan.js.map +1 -1
  14. package/dist/environment.js +4 -0
  15. package/dist/environment.js.map +1 -0
  16. package/dist/nativeServices/installedServiceDefinitions.js +602 -0
  17. package/dist/nativeServices/installedServiceDefinitions.js.map +1 -0
  18. package/dist/nativeServices/serviceAction.js +174 -0
  19. package/dist/nativeServices/serviceAction.js.map +1 -0
  20. package/dist/nativeServices/serviceDoctor.js +374 -71
  21. package/dist/nativeServices/serviceDoctor.js.map +1 -1
  22. package/dist/nativeServices/servicePlan.js +1 -1
  23. package/dist/nativeServices/servicePlan.js.map +1 -1
  24. package/dist/{pi-web-plugins → pi-packages}/relays/markdownDocument.js +1 -1
  25. package/dist/pi-packages/relays/package.json +11 -0
  26. package/dist/{pi-web-plugins → pi-packages}/relays/pi-web-plugin.js +4 -4
  27. package/dist/pi-packages/relays/prompts/relay-worktree.md +157 -0
  28. package/dist/pi-packages/relays/prompts/relay.md +152 -0
  29. package/dist/{pi-web-plugins → pi-packages}/relays/relayDiscovery.js +1 -1
  30. package/dist/{pi-web-plugins → pi-packages}/relays/relaysPanelElement.js +1 -1
  31. package/dist/pi-packages/relays/skills/relay/SKILL.md +123 -0
  32. package/dist/pi-web-plugins/git/browser/git-contract.js +108 -0
  33. package/dist/pi-web-plugins/git/browser/git-panel.js +609 -0
  34. package/dist/pi-web-plugins/git/browser/gitFileList.js +43 -0
  35. package/dist/pi-web-plugins/git/browser/gitFileShared.js +16 -0
  36. package/dist/pi-web-plugins/git/browser/gitFileTree.js +81 -0
  37. package/dist/pi-web-plugins/git/browser/gitFileViewPreference.js +35 -0
  38. package/dist/pi-web-plugins/git/browser/gitRoute.js +42 -0
  39. package/dist/pi-web-plugins/git/browser/pi-web-plugin.js +10 -0
  40. package/dist/pi-web-plugins/git/browser/unifiedDiff.js +301 -0
  41. package/dist/{server/git/gitService.js → pi-web-plugins/git/git-backend.js} +209 -60
  42. package/dist/pi-web-plugins/git/package.json +16 -0
  43. package/dist/pi-web-plugins/git/server-plugin.js +176 -0
  44. package/dist/pi-web-plugins/info/infoInternals.js +16 -2
  45. package/dist/pi-web-plugins/info/package.json +1 -1
  46. package/dist/pi-web-plugins/info/pi-web-plugin.js +2 -2
  47. package/dist/pi-web-plugins/updates/package.json +1 -1
  48. package/dist/pi-web-plugins/updates/pi-web-plugin.js +1 -1
  49. package/dist/pi-web-plugins/workspace-tasks/package.json +1 -1
  50. package/dist/pi-web-plugins/workspace-tasks/pi-web-plugin.js +3 -3
  51. package/dist/piWebVersionReport.js +63 -10
  52. package/dist/piWebVersionReport.js.map +1 -1
  53. package/dist/plugin-api.d.ts +33 -16
  54. package/dist/pluginRecoveryCli.js +172 -0
  55. package/dist/pluginRecoveryCli.js.map +1 -0
  56. package/dist/server/activity/workspaceActivityService.js +27 -19
  57. package/dist/server/activity/workspaceActivityService.js.map +1 -1
  58. package/dist/server/app.js +41 -15
  59. package/dist/server/app.js.map +1 -1
  60. package/dist/server/configRoutes.js +3 -21
  61. package/dist/server/configRoutes.js.map +1 -1
  62. package/dist/server/knownAutoInstallPiPackages.js +30 -0
  63. package/dist/server/knownAutoInstallPiPackages.js.map +1 -0
  64. package/dist/server/machines/machineClient.js +23 -4
  65. package/dist/server/machines/machineClient.js.map +1 -1
  66. package/dist/server/machines/machinePluginProxyRoutes.js +74 -17
  67. package/dist/server/machines/machinePluginProxyRoutes.js.map +1 -1
  68. package/dist/server/machines/machineProxyRoutes.js +165 -8
  69. package/dist/server/machines/machineProxyRoutes.js.map +1 -1
  70. package/dist/server/machines/machineService.js +23 -0
  71. package/dist/server/machines/machineService.js.map +1 -1
  72. package/dist/server/piPackageIdentity.js +25 -0
  73. package/dist/server/piPackageIdentity.js.map +1 -0
  74. package/dist/server/piPackageService.js +69 -8
  75. package/dist/server/piPackageService.js.map +1 -1
  76. package/dist/server/piWebPluginCatalog.js +584 -0
  77. package/dist/server/piWebPluginCatalog.js.map +1 -0
  78. package/dist/server/piWebPluginLifecycle.js +162 -0
  79. package/dist/server/piWebPluginLifecycle.js.map +1 -0
  80. package/dist/server/piWebPluginService.js +159 -261
  81. package/dist/server/piWebPluginService.js.map +1 -1
  82. package/dist/server/piWebStatus.js +41 -9
  83. package/dist/server/piWebStatus.js.map +1 -1
  84. package/dist/server/plugins/pluginBackendProxyRoutes.js +69 -0
  85. package/dist/server/plugins/pluginBackendProxyRoutes.js.map +1 -0
  86. package/dist/server/plugins/serverPluginExec.js +184 -0
  87. package/dist/server/plugins/serverPluginExec.js.map +1 -0
  88. package/dist/server/plugins/serverPluginRuntime.js +480 -0
  89. package/dist/server/plugins/serverPluginRuntime.js.map +1 -0
  90. package/dist/server/projectTrustRoutes.js +80 -0
  91. package/dist/server/projectTrustRoutes.js.map +1 -0
  92. package/dist/server/projects/projectService.js +3 -1
  93. package/dist/server/projects/projectService.js.map +1 -1
  94. package/dist/server/realtime/sessionEventHub.js +26 -11
  95. package/dist/server/realtime/sessionEventHub.js.map +1 -1
  96. package/dist/server/requestCancellation.js +32 -0
  97. package/dist/server/requestCancellation.js.map +1 -0
  98. package/dist/server/sessiond/agentProcessEnvironment.js +29 -30
  99. package/dist/server/sessiond/agentProcessEnvironment.js.map +1 -1
  100. package/dist/server/sessiond/autoInstallPiPackages.js +63 -0
  101. package/dist/server/sessiond/autoInstallPiPackages.js.map +1 -0
  102. package/dist/server/sessiond/pluginBackendRoutes.js +64 -0
  103. package/dist/server/sessiond/pluginBackendRoutes.js.map +1 -0
  104. package/dist/server/sessiond/sessionDaemonShutdown.js +24 -0
  105. package/dist/server/sessiond/sessionDaemonShutdown.js.map +1 -0
  106. package/dist/server/sessiond/sessionProxyRoutes.js +2 -2
  107. package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
  108. package/dist/server/sessiond/sessionServiceDependencies.js +2 -1
  109. package/dist/server/sessiond/sessionServiceDependencies.js.map +1 -1
  110. package/dist/server/sessiond/sessiondStateOwnership.js +235 -0
  111. package/dist/server/sessiond/sessiondStateOwnership.js.map +1 -0
  112. package/dist/server/sessiond/workspaceCatalogRoutes.js +31 -0
  113. package/dist/server/sessiond/workspaceCatalogRoutes.js.map +1 -0
  114. package/dist/server/sessiond/workspaceRemovalRoutes.js +41 -0
  115. package/dist/server/sessiond/workspaceRemovalRoutes.js.map +1 -0
  116. package/dist/server/sessiond.js +284 -98
  117. package/dist/server/sessiond.js.map +1 -1
  118. package/dist/server/sessions/authService.js +15 -54
  119. package/dist/server/sessions/authService.js.map +1 -1
  120. package/dist/server/sessions/dockerEnvironmentFacts.js +254 -0
  121. package/dist/server/sessions/dockerEnvironmentFacts.js.map +1 -0
  122. package/dist/server/sessions/modelCatalogRefresher.js +4 -3
  123. package/dist/server/sessions/modelCatalogRefresher.js.map +1 -1
  124. package/dist/server/sessions/piSessionManagerGateway.js +18 -67
  125. package/dist/server/sessions/piSessionManagerGateway.js.map +1 -1
  126. package/dist/server/sessions/piSessionService.js +495 -142
  127. package/dist/server/sessions/piSessionService.js.map +1 -1
  128. package/dist/server/sessions/sessionCommandService.js +24 -1
  129. package/dist/server/sessions/sessionCommandService.js.map +1 -1
  130. package/dist/server/sessions/sessionEnvironmentFacts.js +48 -0
  131. package/dist/server/sessions/sessionEnvironmentFacts.js.map +1 -0
  132. package/dist/server/sessions/sessionFileFormat.js +27 -0
  133. package/dist/server/sessions/sessionFileFormat.js.map +1 -0
  134. package/dist/server/sessions/sessionFileHeader.js +1 -14
  135. package/dist/server/sessions/sessionFileHeader.js.map +1 -1
  136. package/dist/server/sessions/sessionModelScope.js +160 -0
  137. package/dist/server/sessions/sessionModelScope.js.map +1 -0
  138. package/dist/server/sessions/sessionNameGenerator.js +6 -6
  139. package/dist/server/sessions/sessionNameGenerator.js.map +1 -1
  140. package/dist/server/sessions/sessionRoutes.js +55 -0
  141. package/dist/server/sessions/sessionRoutes.js.map +1 -1
  142. package/dist/server/sessions/sessionSummaryScanner.js +96 -224
  143. package/dist/server/sessions/sessionSummaryScanner.js.map +1 -1
  144. package/dist/server/sessions/spawnSubsessionTool.js +5 -9
  145. package/dist/server/sessions/spawnSubsessionTool.js.map +1 -1
  146. package/dist/server/sessions/spawnTargetResolver.js +1 -1
  147. package/dist/server/status/machineStatusRoutes.js +8 -0
  148. package/dist/server/status/machineStatusRoutes.js.map +1 -0
  149. package/dist/server/status/machineStatusService.js +181 -0
  150. package/dist/server/status/machineStatusService.js.map +1 -0
  151. package/dist/server/status/workspaceAttribution.js +87 -0
  152. package/dist/server/status/workspaceAttribution.js.map +1 -0
  153. package/dist/server/storage/piPackageDismissalStore.js +78 -0
  154. package/dist/server/storage/piPackageDismissalStore.js.map +1 -0
  155. package/dist/server/terminalProxyRoutes.js +2 -1
  156. package/dist/server/terminalProxyRoutes.js.map +1 -1
  157. package/dist/server/workspaceExplorerRoutes.js +20 -13
  158. package/dist/server/workspaceExplorerRoutes.js.map +1 -1
  159. package/dist/server/workspaces/fileContentService.js +18 -9
  160. package/dist/server/workspaces/fileContentService.js.map +1 -1
  161. package/dist/server/workspaces/filePreviewResponseHeaders.js +18 -0
  162. package/dist/server/workspaces/filePreviewResponseHeaders.js.map +1 -0
  163. package/dist/server/workspaces/filePreviewResponsePolicy.js +55 -0
  164. package/dist/server/workspaces/filePreviewResponsePolicy.js.map +1 -0
  165. package/dist/server/workspaces/filePreviewService.js +81 -0
  166. package/dist/server/workspaces/filePreviewService.js.map +1 -0
  167. package/dist/server/workspaces/projectWorkspaceCwds.js +0 -5
  168. package/dist/server/workspaces/projectWorkspaceCwds.js.map +1 -1
  169. package/dist/server/workspaces/sessionDaemonWorkspaceCatalog.js +402 -0
  170. package/dist/server/workspaces/sessionDaemonWorkspaceCatalog.js.map +1 -0
  171. package/dist/server/workspaces/workspaceCatalog.js +47 -0
  172. package/dist/server/workspaces/workspaceCatalog.js.map +1 -0
  173. package/dist/server/workspaces/workspaceContext.js +1 -3
  174. package/dist/server/workspaces/workspaceContext.js.map +1 -1
  175. package/dist/server/workspaces/workspaceDeletionRoutes.js +35 -125
  176. package/dist/server/workspaces/workspaceDeletionRoutes.js.map +1 -1
  177. package/dist/server/workspaces/workspaceProviderRegistry.js +651 -0
  178. package/dist/server/workspaces/workspaceProviderRegistry.js.map +1 -0
  179. package/dist/server/workspaces/workspaceRemovalService.js +256 -0
  180. package/dist/server/workspaces/workspaceRemovalService.js.map +1 -0
  181. package/dist/server/workspaces/workspaceRouteErrors.js +7 -0
  182. package/dist/server/workspaces/workspaceRouteErrors.js.map +1 -0
  183. package/dist/server/workspaces/worktreePreRemoveHook.js +39 -0
  184. package/dist/server/workspaces/worktreePreRemoveHook.js.map +1 -0
  185. package/dist/server-plugin-api.d.ts +140 -0
  186. package/dist/server-plugin-api.js +2 -0
  187. package/dist/server-plugin-api.js.map +1 -0
  188. package/dist/serverPluginRecovery.js +138 -0
  189. package/dist/serverPluginRecovery.js.map +1 -0
  190. package/dist/sessiond/activeAgentProfile.js +3 -22
  191. package/dist/sessiond/activeAgentProfile.js.map +1 -1
  192. package/dist/sessiond/config.js +11 -0
  193. package/dist/sessiond/config.js.map +1 -1
  194. package/dist/sessiond/sessionDaemonClient.js +11 -9
  195. package/dist/sessiond/sessionDaemonClient.js.map +1 -1
  196. package/dist/shared/activeAgentProfile.js +2 -41
  197. package/dist/shared/activeAgentProfile.js.map +1 -1
  198. package/dist/shared/activity.js +0 -3
  199. package/dist/shared/activity.js.map +1 -1
  200. package/dist/shared/apiTypes.js +7 -5
  201. package/dist/shared/apiTypes.js.map +1 -1
  202. package/dist/shared/capabilities.js +8 -6
  203. package/dist/shared/capabilities.js.map +1 -1
  204. package/dist/shared/federatedRoutes.js +36 -5
  205. package/dist/shared/federatedRoutes.js.map +1 -1
  206. package/dist/shared/machinePluginIds.js +4 -2
  207. package/dist/shared/machinePluginIds.js.map +1 -1
  208. package/dist/shared/machineStatus.js +94 -0
  209. package/dist/shared/machineStatus.js.map +1 -0
  210. package/dist/shared/piWebStatusParsing.js +33 -0
  211. package/dist/shared/piWebStatusParsing.js.map +1 -1
  212. package/dist/shared/pluginApiTypes.d.ts +150 -0
  213. package/dist/shared/pluginApiTypes.js +3 -0
  214. package/dist/shared/pluginApiTypes.js.map +1 -0
  215. package/dist/shared/pluginBackendProtocol.js +110 -0
  216. package/dist/shared/pluginBackendProtocol.js.map +1 -0
  217. package/dist/shared/pluginIds.js +5 -0
  218. package/dist/shared/pluginIds.js.map +1 -1
  219. package/dist/shared/pluginRecoveryCommands.js +14 -0
  220. package/dist/shared/pluginRecoveryCommands.js.map +1 -0
  221. package/dist/shared/workspaceFiles.js +41 -2
  222. package/dist/shared/workspaceFiles.js.map +1 -1
  223. package/dist/shared/workspaceRemovalProtocol.js +24 -0
  224. package/dist/shared/workspaceRemovalProtocol.js.map +1 -0
  225. package/docs/config.md +105 -52
  226. package/docs/plugins.md +403 -146
  227. package/examples/workspace-provider-plugin/README.md +52 -0
  228. package/examples/workspace-provider-plugin/package.json +25 -0
  229. package/examples/workspace-provider-plugin/src/browser/index.ts +69 -0
  230. package/examples/workspace-provider-plugin/src/server.ts +64 -0
  231. package/examples/workspace-provider-plugin/tsconfig.json +21 -0
  232. package/package.json +30 -11
  233. package/server-plugin-api.d.ts +1 -0
  234. package/dist/client/assets/CodeViewer-D91Sp61M.js +0 -4
  235. package/dist/client/assets/UnifiedDiffViewer-BafLWGtF.js +0 -39
  236. package/dist/client/assets/index-CA8q9_o7.js +0 -4025
  237. package/dist/client/assets/vendor-editor-core-vUi74DnD.js +0 -12
  238. package/dist/client/assets/vendor-editor-languages-DoaXUodB.js +0 -46
  239. package/dist/pi-web-plugins/relays/package.json +0 -9
  240. package/dist/plugin-api/unstable.d.ts +0 -22
  241. package/dist/server/activity/workspaceActivityRoutes.js +0 -4
  242. package/dist/server/activity/workspaceActivityRoutes.js.map +0 -1
  243. package/dist/server/git/gitService.js.map +0 -1
  244. package/dist/server/gitRoutes.js +0 -23
  245. package/dist/server/gitRoutes.js.map +0 -1
  246. package/dist/server/sessions/parentSessionLocator.js +0 -75
  247. package/dist/server/sessions/parentSessionLocator.js.map +0 -1
  248. package/dist/server/workspaces/gitWorktreeDiscovery.js +0 -43
  249. package/dist/server/workspaces/gitWorktreeDiscovery.js.map +0 -1
  250. package/dist/server/workspaces/imagePreviewService.js +0 -40
  251. package/dist/server/workspaces/imagePreviewService.js.map +0 -1
  252. package/dist/server/workspaces/workspaceService.js +0 -52
  253. package/dist/server/workspaces/workspaceService.js.map +0 -1
  254. package/dist/shared/apiTypes.d.ts +0 -1217
  255. package/dist/shared/thinkingLevels.d.ts +0 -27
  256. package/plugin-api/unstable.d.ts +0 -1
  257. /package/dist/{pi-web-plugins → pi-packages}/relays/vendor/README.md +0 -0
  258. /package/dist/{pi-web-plugins → pi-packages}/relays/vendor/marked.esm.js +0 -0
package/README.md CHANGED
@@ -36,7 +36,7 @@ Requirements:
36
36
 
37
37
  - Node.js 22.19.0 or newer
38
38
  - npm
39
- - Pi Coding Agent `>=0.83.0`, configured for your user
39
+ - Pi Coding Agent `>=0.84.0`, configured for your user
40
40
  - git and the development tools your agents need
41
41
 
42
42
  Install and start PI WEB as per-user services:
@@ -78,7 +78,7 @@ PI WEB organizes work like this:
78
78
  ```text
79
79
  Machine a local or remote PI WEB runtime endpoint
80
80
  Project a folder on that machine
81
- Workspace a git worktree, or the project folder for non-git projects
81
+ Workspace a provider-owned working folder; bundled Git discovers worktrees, otherwise the project folder
82
82
  Session a Pi Coding Agent chat running inside a workspace
83
83
  ```
84
84
 
@@ -110,9 +110,9 @@ Read more: [Fleet and machines guide](https://pi-web.dev/machines)
110
110
 
111
111
  ## PI WEB plugins
112
112
 
113
- PI WEB supports trusted browser-side plugins that can add actions, workspace panels, and workspace metadata. Use **Settings → PI WEB plugins** to enable or disable them on the selected machine.
113
+ PI WEB supports trusted browser plugins plus optional sessiond-backed workspace providers. Bundled Git uses the same public provider and backend contracts available to installed plugins. Use **Settings → PI WEB plugins** to manage desired state on the selected machine.
114
114
 
115
- Pi packages are a separate Pi package-manager concept. A Pi package may include a PI WEB browser plugin, but installing a package and enabling its browser plugin are different operations.
115
+ Pi packages are a separate Pi package-manager concept. A Pi package may include a PI WEB plugin, but installing a package and enabling it are different operations. Server-backed changes require a session-daemon restart; see the plugin guide for lifecycle and recovery details.
116
116
 
117
117
  Read more: [PI WEB plugin guide and API](https://pi-web.dev/plugins)
118
118
 
package/dist/cli.js CHANGED
@@ -5,13 +5,18 @@ 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, effectivePiWebConfig, examplePiWebConfig } from "./config.js";
9
- import { packageVersion, printPiWebVersionReport } from "./piWebVersionReport.js";
8
+ import { defaultPiWebConfigPath, defaultPiWebDataDir, examplePiWebConfig } from "./config.js";
9
+ import { piWebDockerCommand } from "./docker/piWebDockerCommandPlan.js";
10
+ import { ownEnvironmentValue } from "./environment.js";
11
+ import { runPluginRecoveryCli } from "./pluginRecoveryCli.js";
12
+ import { packageVersion, printPiWebVersionReport, probeRunningComponentReady, runningComponentsReady, } from "./piWebVersionReport.js";
10
13
  import { checkNodePtyDarwinSpawnHelper, formatNodePtyDarwinSpawnHelperCheck } from "./server/diagnostics/nodePtySpawnHelper.js";
11
14
  import { checkNodePtyNativeModule, formatNodePtyNativeModuleCheck } from "./server/diagnostics/nodePtyNativeModule.js";
12
15
  import { installNativeServiceCandidate, nativeServiceInstallFailureNeedsPathAdvice, } from "./nativeServices/serviceInstall.js";
16
+ import { launchdBootoutArgs, launchdServiceTarget, orderServices, performServiceAction, serviceStartOrder, serviceStopOrder, settleLaunchdServiceUnload, startLaunchdService, } from "./nativeServices/serviceAction.js";
13
17
  import { minimumSupportedNodeVersion, nativeServiceManagerRefs, productionNativeServiceIds, } from "./nativeServices/servicePlan.js";
14
- 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";
15
20
  import { createNativeServiceAuthoritativeProbe, nativeServicePrerequisiteShellCheck, } from "./nativeServices/serviceProbe.js";
16
21
  import { renderLaunchdPlist, renderSystemdUnit } from "./nativeServices/serviceRendering.js";
17
22
  const PI_WEB_PACKAGE_NAME = "@jmfederico/pi-web";
@@ -24,12 +29,6 @@ const serviceRefs = {
24
29
  uiDev: { id: "uiDev", ...nativeServiceManagerRefs.uiDev },
25
30
  };
26
31
  const productionServiceIds = [...productionNativeServiceIds];
27
- const startServiceOrder = ["sessiond", "web", "uiDev"];
28
- const stopServiceOrder = ["web", "uiDev", "sessiond"];
29
- // Restart web/UI before sessiond: when `pi-web restart` runs in a pi-web
30
- // terminal (owned by sessiond), restarting sessiond kills the command, so any
31
- // services handled after it would never be restarted.
32
- const restartServiceOrder = ["web", "uiDev", "sessiond"];
33
32
  function platformLabel() {
34
33
  if (process.platform === "darwin")
35
34
  return "macOS";
@@ -55,9 +54,6 @@ function requireServiceBackend(command) {
55
54
  return backend;
56
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()}`);
57
56
  }
58
- function supportsSystemdUserServices() {
59
- return currentServiceBackend()?.kind === "systemd";
60
- }
61
57
  function manualRunAdvice() {
62
58
  return [
63
59
  "Run PI WEB manually from a checkout:",
@@ -89,6 +85,15 @@ function capture(command, args) {
89
85
  const stderr = outputText(result.stderr);
90
86
  return { status: result.status ?? 1, stdout: outputText(result.stdout), stderr: stderr === "" ? errorMessage : stderr };
91
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
+ }
92
97
  function runQuiet(command, args) {
93
98
  return capture(command, args).status;
94
99
  }
@@ -219,21 +224,11 @@ function allServiceRefs() {
219
224
  function productionServiceRefs() {
220
225
  return serviceRefList(productionServiceIds);
221
226
  }
222
- function orderServiceRefs(refs, order) {
223
- const byId = new Map(refs.map((ref) => [ref.id, ref]));
224
- return order.flatMap((id) => {
225
- const ref = byId.get(id);
226
- return ref === undefined ? [] : [ref];
227
- });
228
- }
229
227
  function startOrder(refs) {
230
- return orderServiceRefs(refs, startServiceOrder);
228
+ return orderServices(refs, serviceStartOrder);
231
229
  }
232
230
  function stopOrder(refs) {
233
- return orderServiceRefs(refs, stopServiceOrder);
234
- }
235
- function restartOrder(refs) {
236
- return orderServiceRefs(refs, restartServiceOrder);
231
+ return orderServices(refs, serviceStopOrder);
237
232
  }
238
233
  function devRootPath() {
239
234
  return resolve(process.cwd());
@@ -298,30 +293,30 @@ async function installSystemdServices(plan) {
298
293
  function launchdDomain() {
299
294
  return `gui/${String(userInfo().uid)}`;
300
295
  }
301
- function launchdServiceTarget(ref) {
302
- return `${launchdDomain()}/${ref.launchdLabel}`;
303
- }
304
- function launchdIsLoaded(ref) {
305
- return capture("launchctl", ["print", launchdServiceTarget(ref)]).status === 0;
306
- }
307
- function launchdBootout(ref) {
308
- runQuiet("launchctl", ["bootout", launchdServiceTarget(ref)]);
296
+ function currentLaunchdServiceTarget(ref) {
297
+ return launchdServiceTarget(launchdDomain(), ref);
309
298
  }
310
- function launchdBootstrap(ref) {
311
- run("launchctl", ["bootstrap", launchdDomain(), launchdPlistPath(ref)], { check: true });
312
- run("launchctl", ["enable", launchdServiceTarget(ref)], { check: true });
299
+ function launchdActionContext() {
300
+ return { domain: launchdDomain(), plistPath: launchdPlistPath };
313
301
  }
314
- function launchdStart(ref) {
315
- if (!launchdIsLoaded(ref))
316
- launchdBootstrap(ref);
317
- 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
+ };
318
312
  }
319
313
  async function installLaunchdServices(plan) {
320
314
  const selected = new Set(plan.services.map((service) => service.id));
321
315
  await mkdir(launchdServiceDir, { recursive: true });
322
316
  await mkdir(logDir, { recursive: true });
323
- for (const ref of stopOrder(allServiceRefs()))
324
- launchdBootout(ref);
317
+ for (const ref of stopOrder(allServiceRefs())) {
318
+ runQuiet("launchctl", launchdBootoutArgs(currentLaunchdServiceTarget(ref)));
319
+ }
325
320
  for (const ref of allServiceRefs().filter((candidate) => !selected.has(candidate.id))) {
326
321
  await rm(launchdPlistPath(ref), { force: true });
327
322
  }
@@ -329,8 +324,16 @@ async function installLaunchdServices(plan) {
329
324
  const plistPath = join(launchdServiceDir, service.manager.launchdPlistName);
330
325
  await writeFile(plistPath, renderLaunchdPlist(plan, service, logDir));
331
326
  }
332
- for (const service of plan.services)
333
- 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
+ }
334
337
  }
335
338
  async function installNativeServices(plan) {
336
339
  if (plan.backend.kind === "systemd")
@@ -350,7 +353,7 @@ async function uninstallSystemdServices() {
350
353
  }
351
354
  async function uninstallLaunchdServices() {
352
355
  for (const ref of stopOrder(allServiceRefs())) {
353
- launchdBootout(ref);
356
+ runQuiet("launchctl", launchdBootoutArgs(currentLaunchdServiceTarget(ref)));
354
357
  await rm(launchdPlistPath(ref), { force: true });
355
358
  }
356
359
  }
@@ -444,7 +447,7 @@ export function launchdRuntimeDetails(output) {
444
447
  return { state, detail, pid };
445
448
  }
446
449
  function launchdRuntimeStatus(backend, ref) {
447
- const target = launchdServiceTarget(ref);
450
+ const target = currentLaunchdServiceTarget(ref);
448
451
  const filePath = serviceFilePath(backend, ref);
449
452
  if (!serviceFileExists(backend, ref))
450
453
  return makeServiceRuntimeStatus(ref, "not-installed", "not installed", target, filePath);
@@ -610,40 +613,85 @@ async function uninstall() {
610
613
  await uninstallNativeServices(backend);
611
614
  console.log(`PI WEB ${backend.label} removed. Production and development service files were removed; config and data were left in place.`);
612
615
  }
613
- function systemdServiceAction(action, refs) {
614
- const orderedRefs = action === "stop" ? stopOrder(refs) : action === "restart" ? restartOrder(refs) : startOrder(refs);
615
- run("systemctl", ["--user", action, ...orderedRefs.map((ref) => ref.systemdName)], { check: true });
616
- }
617
- function launchdServiceAction(action, refs) {
618
- if (action === "stop") {
619
- for (const ref of stopOrder(refs))
620
- launchdBootout(ref);
621
- return;
622
- }
623
- if (action === "restart") {
624
- // Restart each service fully (bootout + start) before moving to the next,
625
- // so the web/UI services are back up before sessiond is restarted.
626
- for (const ref of restartOrder(refs)) {
627
- launchdBootout(ref);
628
- launchdStart(ref);
629
- }
630
- return;
631
- }
632
- for (const ref of startOrder(refs))
633
- launchdStart(ref);
634
- }
635
- function serviceAction(action) {
616
+ async function serviceAction(action) {
636
617
  const backend = requireServiceBackend(`pi-web ${action}`);
637
618
  if (action === "status") {
638
619
  if (!printServiceStatusReport(backend))
639
620
  process.exitCode = 1;
640
621
  return;
641
622
  }
623
+ await performLifecycleServiceAction(backend, action, process.env);
624
+ }
625
+ async function performLifecycleServiceAction(backend, action, probeEnvironment) {
642
626
  const refs = installedServiceRefs(backend);
643
- if (backend.kind === "systemd")
644
- systemdServiceAction(action, refs);
645
- else
646
- 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
+ }
637
+ }
638
+ /** Offline restart guidance used only after a recovery config mutation. */
639
+ export function sessionDaemonRestartPlan(options = {}) {
640
+ const env = options.env ?? process.env;
641
+ if (options.explicitConfigPath === true && options.configPath !== undefined) {
642
+ return {
643
+ kind: "manual",
644
+ guidance: `Restart the session daemon process configured with PI_WEB_CONFIG=${JSON.stringify(options.configPath)}.`,
645
+ };
646
+ }
647
+ const dockerMode = activeDockerMode(env);
648
+ const runCommand = options.runCommand ?? ((command, args) => { run(command, args, { check: true }); });
649
+ if (dockerMode !== undefined) {
650
+ const command = piWebDockerCommand(dockerMode, "restart-sessiond");
651
+ return {
652
+ kind: "automatic",
653
+ command,
654
+ guidance: "Run the Docker control command from the PI WEB Docker host.",
655
+ perform: () => {
656
+ runCommand("pi-web-docker", [...(dockerMode === "dev" ? ["--dev"] : []), "restart-sessiond"]);
657
+ },
658
+ };
659
+ }
660
+ const backend = serviceBackendForPlatform(options.platform ?? process.platform);
661
+ const fileExists = options.serviceFileExists ?? existsSync;
662
+ if (backend?.kind === "systemd" && fileExists(systemdServicePath(serviceRefs.sessiond))) {
663
+ const args = ["--user", "restart", serviceRefs.sessiond.systemdName];
664
+ return {
665
+ kind: "automatic",
666
+ command: `systemctl ${args.join(" ")}`,
667
+ guidance: "Run:",
668
+ perform: () => { runCommand("systemctl", args); },
669
+ };
670
+ }
671
+ if (backend?.kind === "launchd" && fileExists(launchdPlistPath(serviceRefs.sessiond))) {
672
+ const target = `gui/${String(options.uid ?? userInfo().uid)}/${serviceRefs.sessiond.launchdLabel}`;
673
+ const args = ["kickstart", "-k", target];
674
+ return {
675
+ kind: "automatic",
676
+ command: `launchctl ${args.join(" ")}`,
677
+ guidance: "Run:",
678
+ perform: () => { runCommand("launchctl", args); },
679
+ };
680
+ }
681
+ return {
682
+ kind: "manual",
683
+ guidance: "Stop and rerun the process that owns sessiond (from a checkout: `npm run start:sessiond`).",
684
+ };
685
+ }
686
+ function activeDockerMode(env) {
687
+ if (!truthyEnvValue(ownEnvironmentValue(env, "PI_WEB_DOCKER_RUNTIME")))
688
+ return undefined;
689
+ if (ownEnvironmentValue(env, "PI_WEB_DOCKER_MODE") === "dev")
690
+ return "dev";
691
+ return "runtime";
692
+ }
693
+ function truthyEnvValue(value) {
694
+ return value !== undefined && value !== "" && value !== "0" && value.toLowerCase() !== "false";
647
695
  }
648
696
  function logs() {
649
697
  const backend = requireServiceBackend("pi-web logs");
@@ -677,16 +725,14 @@ export function nodeVersionCheck() {
677
725
  description: `node >= ${minimumSupportedNodeVersion}`,
678
726
  });
679
727
  }
680
- export function agentCommandForChecks(env = process.env) {
681
- return effectivePiWebConfig({ env }).config.agent.command;
682
- }
683
- function generalDoctorChecks() {
728
+ export function generalDoctorChecks() {
684
729
  const shell = serviceShellLabel();
685
- const agentCommand = agentCommandForChecks();
686
730
  return [
687
731
  [`Caller login ${shell} can find node >= ${minimumSupportedNodeVersion}`, serviceShellCommand(nodeVersionCheck())],
688
732
  [`Caller login ${shell} can find npm`, serviceShellCommand(commandWithVersionCheck("npm"))],
689
- [`Caller login ${shell} can find ${agentCommand}`, serviceShellCommand(commandWithVersionCheck(agentCommand))],
733
+ // Sessions run on the bundled pi SDK; the only CLI check is a hardcoded
734
+ // `pi` PATH probe — nothing names or locates a configurable command.
735
+ [`Caller login ${shell} can find pi`, serviceShellCommand(commandWithVersionCheck("pi"))],
690
736
  ];
691
737
  }
692
738
  function runChecks(checks) {
@@ -734,14 +780,115 @@ function printOptionalDoctorChecks() {
734
780
  console.log(" PI WEB falls back to a bounded filesystem scan when rg is unavailable.");
735
781
  }
736
782
  }
737
- function installedServiceDefinitions(backend, ids) {
738
- return ids.map((id) => ({
783
+ function installedServiceDefinitions(backend, ids, purpose) {
784
+ const sources = ids.map((id) => ({
739
785
  id,
740
- contents: readFileSync(serviceFilePath(backend, serviceRefs[id]), "utf8"),
786
+ path: serviceFilePath(backend, serviceRefs[id]),
787
+ systemdName: serviceRefs[id].systemdName,
788
+ launchdTarget: currentLaunchdServiceTarget(serviceRefs[id]),
741
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);
742
827
  }
743
- function nativeServiceDoctorTarget(backend) {
744
- 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) {
745
892
  const mode = inferInstalledNativeServiceMode(ids);
746
893
  if (mode === "ambiguous") {
747
894
  return {
@@ -766,23 +913,16 @@ function nativeServiceDoctorTarget(backend) {
766
913
  message: `installed ${mode} service set is incomplete; ${missingId} is missing.`,
767
914
  };
768
915
  }
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
- }
916
+ const definitions = installedServiceDefinitions(backend, expectedIds, "doctor");
917
+ if (!definitions.ok)
918
+ return { kind: "inspection-failure", message: definitions.message };
779
919
  if (mode === "development") {
780
- const inspection = inspectInstalledDevelopmentServiceInput(backend, definitions);
920
+ const inspection = inspectInstalledDevelopmentServiceInput(backend, definitions.value);
781
921
  return inspection.ok
782
922
  ? { kind: "installed-development", input: inspection.value }
783
923
  : { kind: "inspection-failure", message: inspection.message };
784
924
  }
785
- const inspection = inspectInstalledProductionServiceContext(backend, definitions);
925
+ const inspection = inspectInstalledProductionServiceContext(backend, definitions.value);
786
926
  return inspection.ok
787
927
  ? {
788
928
  kind: "prospective-production",
@@ -791,8 +931,8 @@ function nativeServiceDoctorTarget(backend) {
791
931
  }
792
932
  : { kind: "inspection-failure", message: inspection.message };
793
933
  }
794
- async function printNativeServiceDoctorChecks(backend) {
795
- const result = await runNativeServiceDoctor(nativeServiceDoctorTarget(backend), {
934
+ async function printNativeServiceDoctorChecks(backend, installedIds) {
935
+ const result = await runNativeServiceDoctor(nativeServiceDoctorTarget(backend, installedIds), {
796
936
  probe: createNativeServiceAuthoritativeProbe(),
797
937
  fileExists: regularFileExists,
798
938
  });
@@ -841,11 +981,30 @@ function printPathSetupAdvice(shell = detectServiceShell()) {
841
981
  console.log(" Avoid relying on prompt hooks; PI WEB services run non-interactive login shells.");
842
982
  }
843
983
  }
844
- export function doctorExitCode(generalReadinessOk, nativeServicePlanOk, nodePtyRuntimeOk) {
845
- return generalReadinessOk && nativeServicePlanOk && nodePtyRuntimeOk ? 0 : 1;
846
- }
847
- async function doctor() {
848
- 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;
849
1008
  console.log(`Platform: ${platformLabel()}`);
850
1009
  console.log(`Service backend: ${backend?.label ?? "manual run only"}`);
851
1010
  console.log(`Service shell: ${describeServiceShell()}`);
@@ -853,7 +1012,13 @@ async function doctor() {
853
1012
  console.log(`- Native user service plan checks skipped on ${platformLabel()}; no native-service drift is reported.`);
854
1013
  }
855
1014
  console.log("");
856
- 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
+ }
857
1022
  console.log("\nGeneral login-shell readiness (separate from native-service requirements):");
858
1023
  const generalReadinessOk = runChecks(generalDoctorChecks());
859
1024
  printOptionalDoctorChecks();
@@ -863,9 +1028,15 @@ async function doctor() {
863
1028
  let nativeServiceReport = null;
864
1029
  if (backend !== undefined) {
865
1030
  console.log("\nNative service plan checks (service-manager context):");
866
- 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
+ }
867
1038
  }
868
- if (supportsSystemdUserServices()) {
1039
+ if (backend?.kind === "systemd") {
869
1040
  const linger = isLingerEnabled();
870
1041
  if (linger === true) {
871
1042
  console.log("✓ systemd user lingering enabled");
@@ -885,7 +1056,7 @@ async function doctor() {
885
1056
  else {
886
1057
  console.log(`- systemd user lingering skipped on ${platformLabel()}`);
887
1058
  }
888
- const nativeServicePlanOk = nativeServiceReport?.ok ?? true;
1059
+ const nativeServicePlanOk = !managedInspectionFailed && (nativeServiceReport?.ok ?? true);
889
1060
  const pathFailure = !generalReadinessOk || nativeServiceReport?.pathAdviceRecommended === true;
890
1061
  if (pathFailure) {
891
1062
  console.log("\nIf a command works in your terminal but fails in the service-manager check, compare the caller and manager contexts above.");
@@ -897,7 +1068,7 @@ async function doctor() {
897
1068
  if (generalReadinessOk && backend === undefined) {
898
1069
  console.log(`\n${manualRunAdvice()}`);
899
1070
  }
900
- if (doctorExitCode(generalReadinessOk, nativeServicePlanOk, nodePtyNativeModuleOk && nodePtySpawnHelperOk) !== 0)
1071
+ if (doctorExitCode(generalReadinessOk, nativeServicePlanOk, nodePtyNativeModuleOk && nodePtySpawnHelperOk, runningComponentsOk) !== 0)
901
1072
  process.exitCode = 1;
902
1073
  }
903
1074
  function printNodePtyNativeModuleCheck() {
@@ -922,6 +1093,8 @@ Usage:
922
1093
  pi-web install [--dev] [--host 127.0.0.1] [--port 8504] [--config ~/.config/pi-web/config.json]
923
1094
  pi-web uninstall
924
1095
  pi-web start|stop|restart|status|logs
1096
+ pi-web plugins disable <plugin-id> [--config <path>] [--restart]
1097
+ pi-web plugins safe-start show|clear|set <bundled-only|none> [--config <path>] [--restart]
925
1098
  pi-web doctor
926
1099
  pi-web version
927
1100
 
@@ -939,14 +1112,15 @@ async function main() {
939
1112
  await install(args);
940
1113
  else if (command === "uninstall")
941
1114
  await uninstall();
942
- else if (command === "start" || command === "stop" || command === "restart" || command === "status")
943
- 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);
944
1120
  else if (command === "logs")
945
1121
  logs();
946
- else if (command === "doctor")
947
- await doctor();
948
- else if (command === "version")
949
- await printPiWebVersionReport();
1122
+ else if (command === "plugins")
1123
+ runPluginRecoveryCli(args, { restartPlan: (context) => sessionDaemonRestartPlan(context) });
950
1124
  else if (command === "--version" || command === "-v")
951
1125
  console.log(packageVersion());
952
1126
  else if (command === "help" || command === "--help" || command === "-h")