@vincenthanxiaodu/pi-web 1.202608.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/LICENSE +21 -0
- package/README.md +185 -0
- package/dist/cli.js +1149 -0
- package/dist/cli.js.map +1 -0
- package/dist/client/apple-touch-icon.png +0 -0
- package/dist/client/assets/CodeViewer-D_Q6ljLV.js +4 -0
- package/dist/client/assets/TerminalPanel-CTGixJIz.js +187 -0
- package/dist/client/assets/index-C2phil_t.js +5637 -0
- package/dist/client/assets/vendor-editor-core-CXO8gGab.js +12 -0
- package/dist/client/assets/vendor-editor-languages-CpW4sJsX.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/favicon.svg +11 -0
- package/dist/client/index.html +124 -0
- package/dist/client/manifest.webmanifest +24 -0
- package/dist/client/pwa-icon-192.png +0 -0
- package/dist/client/pwa-icon-512.png +0 -0
- package/dist/config.js +505 -0
- package/dist/config.js.map +1 -0
- package/dist/docker/piWebDockerCommandPlan.js +212 -0
- package/dist/docker/piWebDockerCommandPlan.js.map +1 -0
- package/dist/environment.js +4 -0
- package/dist/environment.js.map +1 -0
- package/dist/nativeServices/installedServiceDefinitions.js +602 -0
- package/dist/nativeServices/installedServiceDefinitions.js.map +1 -0
- package/dist/nativeServices/serviceAction.js +174 -0
- package/dist/nativeServices/serviceAction.js.map +1 -0
- package/dist/nativeServices/serviceDoctor.js +783 -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 +132 -0
- package/dist/nativeServices/serviceRendering.js.map +1 -0
- package/dist/pi-web-plugins/git/browser/git-contract.js +108 -0
- package/dist/pi-web-plugins/git/browser/git-panel.js +609 -0
- package/dist/pi-web-plugins/git/browser/gitFileList.js +43 -0
- package/dist/pi-web-plugins/git/browser/gitFileShared.js +16 -0
- package/dist/pi-web-plugins/git/browser/gitFileTree.js +81 -0
- package/dist/pi-web-plugins/git/browser/gitFileViewPreference.js +35 -0
- package/dist/pi-web-plugins/git/browser/gitRoute.js +42 -0
- package/dist/pi-web-plugins/git/browser/pi-web-plugin.js +10 -0
- package/dist/pi-web-plugins/git/browser/unifiedDiff.js +301 -0
- package/dist/pi-web-plugins/git/git-backend.js +439 -0
- package/dist/pi-web-plugins/git/package.json +16 -0
- package/dist/pi-web-plugins/git/server-plugin.js +176 -0
- package/dist/pi-web-plugins/info/infoInternals.js +222 -0
- package/dist/pi-web-plugins/info/package.json +9 -0
- package/dist/pi-web-plugins/info/pi-web-plugin.js +48 -0
- package/dist/pi-web-plugins/relays/markdownDocument.js +81 -0
- package/dist/pi-web-plugins/relays/package.json +9 -0
- package/dist/pi-web-plugins/relays/pi-web-plugin.js +44 -0
- package/dist/pi-web-plugins/relays/relayDiscovery.js +228 -0
- package/dist/pi-web-plugins/relays/relaysPanelElement.js +550 -0
- package/dist/pi-web-plugins/relays/vendor/README.md +24 -0
- package/dist/pi-web-plugins/relays/vendor/marked.esm.js +76 -0
- package/dist/pi-web-plugins/updates/package.json +9 -0
- package/dist/pi-web-plugins/updates/pi-web-plugin.js +185 -0
- package/dist/pi-web-plugins/updates/updatesLogic.js +97 -0
- package/dist/pi-web-plugins/workspace-tasks/config.js +91 -0
- package/dist/pi-web-plugins/workspace-tasks/package.json +9 -0
- package/dist/pi-web-plugins/workspace-tasks/pi-web-plugin.js +48 -0
- package/dist/pi-web-plugins/workspace-tasks/taskRunner.js +12 -0
- package/dist/pi-web-plugins/workspace-tasks/tasksPanelElement.js +288 -0
- package/dist/pi-web-plugins/workspace-tasks/workspaceTasksClient.js +47 -0
- package/dist/piWebVersionReport.js +278 -0
- package/dist/piWebVersionReport.js.map +1 -0
- package/dist/plugin-api.d.ts +222 -0
- package/dist/pluginRecoveryCli.js +172 -0
- package/dist/pluginRecoveryCli.js.map +1 -0
- package/dist/server/activeAgentProfileProvider.js +25 -0
- package/dist/server/activeAgentProfileProvider.js.map +1 -0
- package/dist/server/activity/workspaceActivityService.js +106 -0
- package/dist/server/activity/workspaceActivityService.js.map +1 -0
- package/dist/server/app.js +218 -0
- package/dist/server/app.js.map +1 -0
- package/dist/server/browserMessageProjection.js +54 -0
- package/dist/server/browserMessageProjection.js.map +1 -0
- package/dist/server/configRoutes.js +293 -0
- package/dist/server/configRoutes.js.map +1 -0
- package/dist/server/diagnostics/nodePtyNativeModule.js +36 -0
- package/dist/server/diagnostics/nodePtyNativeModule.js.map +1 -0
- package/dist/server/diagnostics/nodePtySpawnHelper.js +135 -0
- package/dist/server/diagnostics/nodePtySpawnHelper.js.map +1 -0
- package/dist/server/git/gitEnv.js +15 -0
- package/dist/server/git/gitEnv.js.map +1 -0
- package/dist/server/goals/goalArchive.js +184 -0
- package/dist/server/goals/goalArchive.js.map +1 -0
- package/dist/server/goals/goalFile.js +200 -0
- package/dist/server/goals/goalFile.js.map +1 -0
- package/dist/server/goals/goalStore.js +65 -0
- package/dist/server/goals/goalStore.js.map +1 -0
- package/dist/server/index.js +7 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/machines/machineClient.js +191 -0
- package/dist/server/machines/machineClient.js.map +1 -0
- package/dist/server/machines/machinePluginProxyRoutes.js +247 -0
- package/dist/server/machines/machinePluginProxyRoutes.js.map +1 -0
- package/dist/server/machines/machineProxyRoutes.js +318 -0
- package/dist/server/machines/machineProxyRoutes.js.map +1 -0
- package/dist/server/machines/machineRoutes.js +56 -0
- package/dist/server/machines/machineRoutes.js.map +1 -0
- package/dist/server/machines/machineService.js +274 -0
- package/dist/server/machines/machineService.js.map +1 -0
- package/dist/server/machines/machineStore.js +143 -0
- package/dist/server/machines/machineStore.js.map +1 -0
- 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/piWebPluginCatalog.js +584 -0
- package/dist/server/piWebPluginCatalog.js.map +1 -0
- package/dist/server/piWebPluginLifecycle.js +162 -0
- package/dist/server/piWebPluginLifecycle.js.map +1 -0
- package/dist/server/piWebPluginService.js +224 -0
- package/dist/server/piWebPluginService.js.map +1 -0
- package/dist/server/piWebReleaseLookupCache.js +37 -0
- package/dist/server/piWebReleaseLookupCache.js.map +1 -0
- package/dist/server/piWebStatus.js +624 -0
- package/dist/server/piWebStatus.js.map +1 -0
- package/dist/server/piWebStatusCache.js +45 -0
- package/dist/server/piWebStatusCache.js.map +1 -0
- package/dist/server/plugins/pluginBackendProxyRoutes.js +69 -0
- package/dist/server/plugins/pluginBackendProxyRoutes.js.map +1 -0
- package/dist/server/plugins/serverPluginExec.js +184 -0
- package/dist/server/plugins/serverPluginExec.js.map +1 -0
- package/dist/server/plugins/serverPluginRuntime.js +480 -0
- package/dist/server/plugins/serverPluginRuntime.js.map +1 -0
- package/dist/server/projectTrustRoutes.js +80 -0
- package/dist/server/projectTrustRoutes.js.map +1 -0
- package/dist/server/projects/directorySuggestions.js +207 -0
- package/dist/server/projects/directorySuggestions.js.map +1 -0
- package/dist/server/projects/projectService.js +33 -0
- package/dist/server/projects/projectService.js.map +1 -0
- package/dist/server/realtime/sessionEventHub.js +93 -0
- package/dist/server/realtime/sessionEventHub.js.map +1 -0
- package/dist/server/requestCancellation.js +32 -0
- package/dist/server/requestCancellation.js.map +1 -0
- package/dist/server/sessiond/agentHttpDispatcher.js +108 -0
- package/dist/server/sessiond/agentHttpDispatcher.js.map +1 -0
- package/dist/server/sessiond/agentProcessEnvironment.js +61 -0
- package/dist/server/sessiond/agentProcessEnvironment.js.map +1 -0
- package/dist/server/sessiond/pluginBackendRoutes.js +64 -0
- package/dist/server/sessiond/pluginBackendRoutes.js.map +1 -0
- package/dist/server/sessiond/sessionDaemonShutdown.js +31 -0
- package/dist/server/sessiond/sessionDaemonShutdown.js.map +1 -0
- package/dist/server/sessiond/sessionProxyRoutes.js +61 -0
- package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -0
- package/dist/server/sessiond/sessionServiceDependencies.js +34 -0
- package/dist/server/sessiond/sessionServiceDependencies.js.map +1 -0
- package/dist/server/sessiond/sessiondStateOwnership.js +235 -0
- package/dist/server/sessiond/sessiondStateOwnership.js.map +1 -0
- package/dist/server/sessiond/workspaceCatalogRoutes.js +31 -0
- package/dist/server/sessiond/workspaceCatalogRoutes.js.map +1 -0
- package/dist/server/sessiond/workspaceRemovalRoutes.js +41 -0
- package/dist/server/sessiond/workspaceRemovalRoutes.js.map +1 -0
- package/dist/server/sessiond.js +387 -0
- package/dist/server/sessiond.js.map +1 -0
- package/dist/server/sessions/askUserTool.js +102 -0
- package/dist/server/sessions/askUserTool.js.map +1 -0
- package/dist/server/sessions/attachmentService.js +119 -0
- package/dist/server/sessions/attachmentService.js.map +1 -0
- package/dist/server/sessions/authProviderOptions.js +50 -0
- package/dist/server/sessions/authProviderOptions.js.map +1 -0
- package/dist/server/sessions/authRoutes.js +59 -0
- package/dist/server/sessions/authRoutes.js.map +1 -0
- package/dist/server/sessions/authService.js +122 -0
- package/dist/server/sessions/authService.js.map +1 -0
- package/dist/server/sessions/builtinCommands.js +27 -0
- package/dist/server/sessions/builtinCommands.js.map +1 -0
- package/dist/server/sessions/dockerEnvironmentFacts.js +254 -0
- package/dist/server/sessions/dockerEnvironmentFacts.js.map +1 -0
- package/dist/server/sessions/editPreview.js +196 -0
- package/dist/server/sessions/editPreview.js.map +1 -0
- package/dist/server/sessions/extensionDialogWaiters.js +76 -0
- package/dist/server/sessions/extensionDialogWaiters.js.map +1 -0
- package/dist/server/sessions/globalProviderPolicy.js +167 -0
- package/dist/server/sessions/globalProviderPolicy.js.map +1 -0
- package/dist/server/sessions/interruptedRunStore.js +147 -0
- package/dist/server/sessions/interruptedRunStore.js.map +1 -0
- package/dist/server/sessions/messagePaging.js +43 -0
- package/dist/server/sessions/messagePaging.js.map +1 -0
- package/dist/server/sessions/modelCatalogRefresher.js +193 -0
- package/dist/server/sessions/modelCatalogRefresher.js.map +1 -0
- package/dist/server/sessions/oauthLoginFlowService.js +346 -0
- package/dist/server/sessions/oauthLoginFlowService.js.map +1 -0
- package/dist/server/sessions/pendingAskStore.js +275 -0
- package/dist/server/sessions/pendingAskStore.js.map +1 -0
- package/dist/server/sessions/pendingExtensionDialogStore.js +196 -0
- package/dist/server/sessions/pendingExtensionDialogStore.js.map +1 -0
- package/dist/server/sessions/piSessionManagerGateway.js +246 -0
- package/dist/server/sessions/piSessionManagerGateway.js.map +1 -0
- package/dist/server/sessions/piSessionService.js +4059 -0
- package/dist/server/sessions/piSessionService.js.map +1 -0
- package/dist/server/sessions/plainTextTheme.js +112 -0
- package/dist/server/sessions/plainTextTheme.js.map +1 -0
- package/dist/server/sessions/sessionArchiveStore.js +273 -0
- package/dist/server/sessions/sessionArchiveStore.js.map +1 -0
- package/dist/server/sessions/sessionArchiveTree.js +35 -0
- package/dist/server/sessions/sessionArchiveTree.js.map +1 -0
- package/dist/server/sessions/sessionCleanup.js +157 -0
- package/dist/server/sessions/sessionCleanup.js.map +1 -0
- package/dist/server/sessions/sessionCommandService.js +336 -0
- package/dist/server/sessions/sessionCommandService.js.map +1 -0
- package/dist/server/sessions/sessionEnvironmentFacts.js +48 -0
- package/dist/server/sessions/sessionEnvironmentFacts.js.map +1 -0
- package/dist/server/sessions/sessionFileFormat.js +27 -0
- package/dist/server/sessions/sessionFileFormat.js.map +1 -0
- package/dist/server/sessions/sessionFileHeader.js +104 -0
- package/dist/server/sessions/sessionFileHeader.js.map +1 -0
- package/dist/server/sessions/sessionModelScope.js +141 -0
- package/dist/server/sessions/sessionModelScope.js.map +1 -0
- package/dist/server/sessions/sessionNameGenerator.js +90 -0
- package/dist/server/sessions/sessionNameGenerator.js.map +1 -0
- package/dist/server/sessions/sessionNotificationStore.js +484 -0
- package/dist/server/sessions/sessionNotificationStore.js.map +1 -0
- package/dist/server/sessions/sessionRoutes.js +744 -0
- package/dist/server/sessions/sessionRoutes.js.map +1 -0
- package/dist/server/sessions/sessionRuntimeStore.js +2 -0
- package/dist/server/sessions/sessionRuntimeStore.js.map +1 -0
- package/dist/server/sessions/sessionService.js +2 -0
- package/dist/server/sessions/sessionService.js.map +1 -0
- package/dist/server/sessions/sessionSummaryScanner.js +491 -0
- package/dist/server/sessions/sessionSummaryScanner.js.map +1 -0
- package/dist/server/sessions/sessionTreeProjection.js +250 -0
- package/dist/server/sessions/sessionTreeProjection.js.map +1 -0
- package/dist/server/sessions/sessionUnreadStore.js +520 -0
- package/dist/server/sessions/sessionUnreadStore.js.map +1 -0
- package/dist/server/sessions/shutdownDrain.js +73 -0
- package/dist/server/sessions/shutdownDrain.js.map +1 -0
- package/dist/server/sessions/spawnSessionTool.js +48 -0
- package/dist/server/sessions/spawnSessionTool.js.map +1 -0
- package/dist/server/sessions/spawnSubsessionTool.js +216 -0
- package/dist/server/sessions/spawnSubsessionTool.js.map +1 -0
- package/dist/server/sessions/spawnTargetResolver.js +23 -0
- package/dist/server/sessions/spawnTargetResolver.js.map +1 -0
- package/dist/server/sessions/subsessionTranscript.js +235 -0
- package/dist/server/sessions/subsessionTranscript.js.map +1 -0
- package/dist/server/status/machineStatusRoutes.js +8 -0
- package/dist/server/status/machineStatusRoutes.js.map +1 -0
- package/dist/server/status/machineStatusService.js +181 -0
- package/dist/server/status/machineStatusService.js.map +1 -0
- package/dist/server/status/workspaceAttribution.js +87 -0
- package/dist/server/status/workspaceAttribution.js.map +1 -0
- package/dist/server/storage/projectStore.js +88 -0
- package/dist/server/storage/projectStore.js.map +1 -0
- package/dist/server/terminalProxyRoutes.js +141 -0
- package/dist/server/terminalProxyRoutes.js.map +1 -0
- package/dist/server/terminals/terminalRoutes.js +155 -0
- package/dist/server/terminals/terminalRoutes.js.map +1 -0
- package/dist/server/terminals/terminalService.js +305 -0
- package/dist/server/terminals/terminalService.js.map +1 -0
- package/dist/server/terminals/terminalSize.js +17 -0
- package/dist/server/terminals/terminalSize.js.map +1 -0
- package/dist/server/types.js +2 -0
- package/dist/server/types.js.map +1 -0
- package/dist/server/updates/fleetRoutes.js +111 -0
- package/dist/server/updates/fleetRoutes.js.map +1 -0
- package/dist/server/updates/restartRoutes.js +48 -0
- package/dist/server/updates/restartRoutes.js.map +1 -0
- package/dist/server/updates/selfUpdateRoutes.js +145 -0
- package/dist/server/updates/selfUpdateRoutes.js.map +1 -0
- package/dist/server/webSocketBridge.js +32 -0
- package/dist/server/webSocketBridge.js.map +1 -0
- package/dist/server/workingDirectory.js +44 -0
- package/dist/server/workingDirectory.js.map +1 -0
- package/dist/server/workspaceExplorerRoutes.js +147 -0
- package/dist/server/workspaceExplorerRoutes.js.map +1 -0
- package/dist/server/workspaces/effectivePathAccess.js +8 -0
- package/dist/server/workspaces/effectivePathAccess.js.map +1 -0
- package/dist/server/workspaces/fileContentService.js +190 -0
- package/dist/server/workspaces/fileContentService.js.map +1 -0
- package/dist/server/workspaces/filePreviewResponseHeaders.js +18 -0
- package/dist/server/workspaces/filePreviewResponseHeaders.js.map +1 -0
- package/dist/server/workspaces/filePreviewResponsePolicy.js +55 -0
- package/dist/server/workspaces/filePreviewResponsePolicy.js.map +1 -0
- package/dist/server/workspaces/filePreviewService.js +81 -0
- package/dist/server/workspaces/filePreviewService.js.map +1 -0
- package/dist/server/workspaces/fileSuggestions.js +520 -0
- package/dist/server/workspaces/fileSuggestions.js.map +1 -0
- package/dist/server/workspaces/fileTreeService.js +35 -0
- package/dist/server/workspaces/fileTreeService.js.map +1 -0
- package/dist/server/workspaces/pathAccessPolicy.js +90 -0
- package/dist/server/workspaces/pathAccessPolicy.js.map +1 -0
- package/dist/server/workspaces/pathSafety.js +45 -0
- package/dist/server/workspaces/pathSafety.js.map +1 -0
- package/dist/server/workspaces/projectPiWebConfig.js +61 -0
- package/dist/server/workspaces/projectPiWebConfig.js.map +1 -0
- package/dist/server/workspaces/projectWorkspaceCwds.js +17 -0
- package/dist/server/workspaces/projectWorkspaceCwds.js.map +1 -0
- package/dist/server/workspaces/sessionDaemonWorkspaceCatalog.js +402 -0
- package/dist/server/workspaces/sessionDaemonWorkspaceCatalog.js.map +1 -0
- package/dist/server/workspaces/workspaceCatalog.js +47 -0
- package/dist/server/workspaces/workspaceCatalog.js.map +1 -0
- package/dist/server/workspaces/workspaceContext.js +6 -0
- package/dist/server/workspaces/workspaceContext.js.map +1 -0
- package/dist/server/workspaces/workspaceDeletionRoutes.js +49 -0
- package/dist/server/workspaces/workspaceDeletionRoutes.js.map +1 -0
- package/dist/server/workspaces/workspaceProviderRegistry.js +651 -0
- package/dist/server/workspaces/workspaceProviderRegistry.js.map +1 -0
- package/dist/server/workspaces/workspaceRemovalService.js +256 -0
- package/dist/server/workspaces/workspaceRemovalService.js.map +1 -0
- package/dist/server/workspaces/workspaceRouteErrors.js +7 -0
- package/dist/server/workspaces/workspaceRouteErrors.js.map +1 -0
- package/dist/server/workspaces/worktreePreRemoveHook.js +39 -0
- package/dist/server/workspaces/worktreePreRemoveHook.js.map +1 -0
- package/dist/server-plugin-api.d.ts +140 -0
- package/dist/server-plugin-api.js +2 -0
- package/dist/server-plugin-api.js.map +1 -0
- package/dist/serverPluginRecovery.js +138 -0
- package/dist/serverPluginRecovery.js.map +1 -0
- package/dist/sessiond/activeAgentProfile.js +12 -0
- package/dist/sessiond/activeAgentProfile.js.map +1 -0
- package/dist/sessiond/config.js +20 -0
- package/dist/sessiond/config.js.map +1 -0
- package/dist/sessiond/sessionDaemonClient.js +105 -0
- package/dist/sessiond/sessionDaemonClient.js.map +1 -0
- package/dist/shared/activeAgentProfile.js +37 -0
- package/dist/shared/activeAgentProfile.js.map +1 -0
- package/dist/shared/activity.js +16 -0
- package/dist/shared/activity.js.map +1 -0
- package/dist/shared/apiTypes.js +41 -0
- package/dist/shared/apiTypes.js.map +1 -0
- package/dist/shared/capabilities.js +37 -0
- package/dist/shared/capabilities.js.map +1 -0
- package/dist/shared/federatedRoutes.js +127 -0
- package/dist/shared/federatedRoutes.js.map +1 -0
- package/dist/shared/machinePluginIds.js +43 -0
- package/dist/shared/machinePluginIds.js.map +1 -0
- package/dist/shared/machineStatus.js +94 -0
- package/dist/shared/machineStatus.js.map +1 -0
- package/dist/shared/piWebStatusParsing.js +141 -0
- package/dist/shared/piWebStatusParsing.js.map +1 -0
- package/dist/shared/pluginApiTypes.d.ts +150 -0
- package/dist/shared/pluginApiTypes.js +3 -0
- package/dist/shared/pluginApiTypes.js.map +1 -0
- package/dist/shared/pluginBackendProtocol.js +110 -0
- package/dist/shared/pluginBackendProtocol.js.map +1 -0
- package/dist/shared/pluginIds.js +10 -0
- package/dist/shared/pluginIds.js.map +1 -0
- package/dist/shared/pluginRecoveryCommands.js +14 -0
- package/dist/shared/pluginRecoveryCommands.js.map +1 -0
- package/dist/shared/promptAttachments.js +92 -0
- package/dist/shared/promptAttachments.js.map +1 -0
- package/dist/shared/selfUpdate.js +3 -0
- package/dist/shared/selfUpdate.js.map +1 -0
- package/dist/shared/thinkingLevels.js +31 -0
- package/dist/shared/thinkingLevels.js.map +1 -0
- package/dist/shared/workspaceDeletion.js +12 -0
- package/dist/shared/workspaceDeletion.js.map +1 -0
- package/dist/shared/workspaceFiles.js +42 -0
- package/dist/shared/workspaceFiles.js.map +1 -0
- package/dist/shared/workspaceRemovalProtocol.js +24 -0
- package/dist/shared/workspaceRemovalProtocol.js.map +1 -0
- package/docs/assets/favicon.svg +11 -0
- package/docs/assets/pi-web-banner.png +0 -0
- package/docs/assets/pi-web-desktop.png +0 -0
- package/docs/assets/pi-web-mobile.png +0 -0
- package/docs/assets/pi-web-tablet.png +0 -0
- package/docs/config.md +444 -0
- package/docs/plugins.md +1305 -0
- package/examples/workspace-provider-plugin/README.md +52 -0
- package/examples/workspace-provider-plugin/package.json +25 -0
- package/examples/workspace-provider-plugin/src/browser/index.ts +69 -0
- package/examples/workspace-provider-plugin/src/server.ts +64 -0
- package/examples/workspace-provider-plugin/tsconfig.json +21 -0
- package/extensions/README-pi-web-restart-migration.md +45 -0
- package/extensions/pi-web.ts +324 -0
- package/install.sh +5 -0
- package/package.json +173 -0
- package/plugin-api.d.ts +1 -0
- package/server-plugin-api.d.ts +1 -0
|
@@ -0,0 +1,783 @@
|
|
|
1
|
+
import { posix as posixPath } from "node:path";
|
|
2
|
+
import { TextDecoder, TextEncoder } from "node:util";
|
|
3
|
+
import { ownEnvironmentValue } from "../environment.js";
|
|
4
|
+
import { createDevelopmentNativeServicePlan, nativeServiceManagerRefs, nativeServicePrerequisiteNeedsPathAdvice, resolveProductionNativeServicePlan, validateNativeServicePlan, } from "./servicePlan.js";
|
|
5
|
+
export function inferInstalledNativeServiceMode(serviceIds) {
|
|
6
|
+
if (serviceIds.size === 0)
|
|
7
|
+
return "none";
|
|
8
|
+
const hasProductionWeb = serviceIds.has("web");
|
|
9
|
+
const hasDevelopmentUi = serviceIds.has("uiDev");
|
|
10
|
+
if (hasProductionWeb && !hasDevelopmentUi)
|
|
11
|
+
return "production";
|
|
12
|
+
if (hasDevelopmentUi && !hasProductionWeb)
|
|
13
|
+
return "development";
|
|
14
|
+
return "ambiguous";
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Select the config path for a caller that is about to probe managed native
|
|
18
|
+
* services. A nonempty caller value is an explicit override and deliberately
|
|
19
|
+
* avoids interpreting installed files. Otherwise every relevant definition
|
|
20
|
+
* must parse to one consistent environment before its persisted path is used.
|
|
21
|
+
*/
|
|
22
|
+
export function selectManagedNativeServiceConfig(backend, definitions, callerConfigPath) {
|
|
23
|
+
if (callerConfigPath !== undefined && callerConfigPath !== "") {
|
|
24
|
+
return { ok: true, value: { source: "caller", configPath: callerConfigPath } };
|
|
25
|
+
}
|
|
26
|
+
if (definitions.length === 0)
|
|
27
|
+
return { ok: true, value: { source: "default" } };
|
|
28
|
+
const parsed = parseConsistentDefinitions(backend, definitions);
|
|
29
|
+
if (!parsed.ok)
|
|
30
|
+
return parsed;
|
|
31
|
+
const firstEnvironment = parsed.value[0]?.environment;
|
|
32
|
+
const installedConfigPath = firstEnvironment === undefined
|
|
33
|
+
? undefined
|
|
34
|
+
: ownEnvironmentValue(firstEnvironment, "PI_WEB_CONFIG");
|
|
35
|
+
if (installedConfigPath === undefined || installedConfigPath === "") {
|
|
36
|
+
return { ok: true, value: { source: "default" } };
|
|
37
|
+
}
|
|
38
|
+
if (!posixPath.isAbsolute(installedConfigPath)) {
|
|
39
|
+
return {
|
|
40
|
+
ok: false,
|
|
41
|
+
message: `Installed service definitions declare relative PI_WEB_CONFIG ${JSON.stringify(installedConfigPath)}; managed config paths must be absolute.`,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return { ok: true, value: { source: "installed", configPath: installedConfigPath } };
|
|
45
|
+
}
|
|
46
|
+
export function inspectInstalledNativeServiceDefinitionEnvironment(backend, definition) {
|
|
47
|
+
const parsed = backend.kind === "systemd"
|
|
48
|
+
? parseSystemdDefinition(definition)
|
|
49
|
+
: parseLaunchdDefinition(definition);
|
|
50
|
+
return parsed.ok ? { ok: true, value: parsed.value.environment } : parsed;
|
|
51
|
+
}
|
|
52
|
+
export function inspectInstalledProductionServiceContext(backend, definitions) {
|
|
53
|
+
const parsed = parseConsistentDefinitions(backend, definitions);
|
|
54
|
+
if (!parsed.ok)
|
|
55
|
+
return parsed;
|
|
56
|
+
const withWorkingDirectory = parsed.value.find((definition) => definition.workingDirectory !== null);
|
|
57
|
+
if (withWorkingDirectory !== undefined) {
|
|
58
|
+
return {
|
|
59
|
+
ok: false,
|
|
60
|
+
message: `Installed production service ${withWorkingDirectory.id} unexpectedly has working directory ${withWorkingDirectory.workingDirectory ?? ""}.`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
ok: true,
|
|
65
|
+
value: {
|
|
66
|
+
shell: parsed.value[0]?.shell ?? impossibleMissingDefinition(),
|
|
67
|
+
environment: parsed.value[0]?.environment ?? impossibleMissingDefinition(),
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export function inspectInstalledDevelopmentServiceInput(backend, definitions) {
|
|
72
|
+
const parsed = parseConsistentDefinitions(backend, definitions);
|
|
73
|
+
if (!parsed.ok)
|
|
74
|
+
return parsed;
|
|
75
|
+
const first = parsed.value[0] ?? impossibleMissingDefinition();
|
|
76
|
+
if (first.workingDirectory === null) {
|
|
77
|
+
return { ok: false, message: "Installed development services do not declare a working directory." };
|
|
78
|
+
}
|
|
79
|
+
const input = {
|
|
80
|
+
backend,
|
|
81
|
+
shell: first.shell,
|
|
82
|
+
environment: first.environment,
|
|
83
|
+
workingDirectory: first.workingDirectory,
|
|
84
|
+
packageJsonPath: posixPath.join(first.workingDirectory, "package.json"),
|
|
85
|
+
};
|
|
86
|
+
const expectedPlan = createDevelopmentNativeServicePlan(input);
|
|
87
|
+
for (const definition of parsed.value) {
|
|
88
|
+
const expected = expectedPlan.services.find((service) => service.id === definition.id);
|
|
89
|
+
if (expected?.shellCommand !== definition.shellCommand) {
|
|
90
|
+
return {
|
|
91
|
+
ok: false,
|
|
92
|
+
message: `Installed ${definition.id} service command does not match the canonical development plan.`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { ok: true, value: input };
|
|
97
|
+
}
|
|
98
|
+
export async function runNativeServiceDoctor(target, dependencies) {
|
|
99
|
+
if (target.kind === "inspection-failure")
|
|
100
|
+
return target;
|
|
101
|
+
const scope = target.kind === "installed-development"
|
|
102
|
+
? { kind: target.kind, reason: null, shell: target.input.shell }
|
|
103
|
+
: { kind: target.kind, reason: target.reason, shell: target.input.shell };
|
|
104
|
+
let plan;
|
|
105
|
+
if (target.kind === "installed-development") {
|
|
106
|
+
plan = createDevelopmentNativeServicePlan(target.input);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
const resolution = await resolveProductionNativeServicePlan(target.input, dependencies);
|
|
110
|
+
if (!resolution.ok) {
|
|
111
|
+
return { kind: "plan-resolution-failure", scope, failures: resolution.failures };
|
|
112
|
+
}
|
|
113
|
+
plan = resolution.plan;
|
|
114
|
+
}
|
|
115
|
+
const validation = await validateNativeServicePlan(plan, dependencies.probe);
|
|
116
|
+
return { kind: "plan-validation", scope, plan, validation };
|
|
117
|
+
}
|
|
118
|
+
export function formatNativeServiceDoctorResult(result) {
|
|
119
|
+
if (result.kind === "inspection-failure") {
|
|
120
|
+
return {
|
|
121
|
+
ok: false,
|
|
122
|
+
failureKind: "inspection",
|
|
123
|
+
lines: [
|
|
124
|
+
`✗ Installed native-service plan could not be inspected: ${result.message}`,
|
|
125
|
+
" Run `pi-web install` or `pi-web install --dev` to replace mixed, partial, or outdated service definitions.",
|
|
126
|
+
],
|
|
127
|
+
plan: null,
|
|
128
|
+
adviceShell: null,
|
|
129
|
+
pathAdviceRecommended: false,
|
|
130
|
+
failedPrerequisites: [],
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const lines = [scopeHeading(result.scope)];
|
|
134
|
+
if (result.kind === "plan-resolution-failure") {
|
|
135
|
+
let infrastructure = false;
|
|
136
|
+
for (const failure of result.failures) {
|
|
137
|
+
if (failure.kind === "probe-infrastructure") {
|
|
138
|
+
infrastructure = true;
|
|
139
|
+
lines.push(`✗ Native service probe infrastructure failure (${failure.reason}): ${failure.message}`);
|
|
140
|
+
}
|
|
141
|
+
else if (failure.kind === "entrypoint-inspection-failure") {
|
|
142
|
+
infrastructure = true;
|
|
143
|
+
lines.push(`✗ Could not inspect bundled ${failure.serviceId} entrypoint ${failure.entrypointPath}: ${failure.message}`);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
lines.push(`✗ ${failure.namedCommand} is unavailable to the native service manager, and bundled entrypoint ${failure.bundledEntrypointPath} is missing.`);
|
|
147
|
+
if (failure.namedCommandFailure !== null)
|
|
148
|
+
lines.push(` ${failure.namedCommandFailure}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (infrastructure)
|
|
152
|
+
lines.push(" This infrastructure failure is not proof of a PATH mismatch.");
|
|
153
|
+
return {
|
|
154
|
+
ok: false,
|
|
155
|
+
failureKind: infrastructure ? "infrastructure" : "requirements",
|
|
156
|
+
lines,
|
|
157
|
+
plan: null,
|
|
158
|
+
adviceShell: result.scope.shell,
|
|
159
|
+
pathAdviceRecommended: !infrastructure
|
|
160
|
+
&& result.failures.some((failure) => failure.kind === "executable-unavailable"),
|
|
161
|
+
failedPrerequisites: [],
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const configuredOverrides = result.plan.services.filter((service) => service.strategy.kind === "configured-override");
|
|
165
|
+
for (const service of configuredOverrides) {
|
|
166
|
+
lines.push(`! ${service.description} uses a configured command override; doctor does not execute arbitrary configured commands.`);
|
|
167
|
+
}
|
|
168
|
+
if (result.validation.ok) {
|
|
169
|
+
lines.push("✓ All verifiable native-service plan requirements are satisfied in the service-manager context.");
|
|
170
|
+
return {
|
|
171
|
+
ok: true,
|
|
172
|
+
failureKind: "none",
|
|
173
|
+
lines,
|
|
174
|
+
plan: result.plan,
|
|
175
|
+
adviceShell: result.plan.shell,
|
|
176
|
+
pathAdviceRecommended: false,
|
|
177
|
+
failedPrerequisites: [],
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
const failedPrerequisites = [];
|
|
181
|
+
let infrastructure = false;
|
|
182
|
+
for (const failure of result.validation.failures) {
|
|
183
|
+
if (failure.kind === "probe-infrastructure") {
|
|
184
|
+
infrastructure = true;
|
|
185
|
+
lines.push(`✗ Native service probe infrastructure failure (${failure.reason}): ${failure.message}`);
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
failedPrerequisites.push(failure.prerequisite);
|
|
189
|
+
lines.push(`✗ Native service requirement failed: ${failure.prerequisite.description}`);
|
|
190
|
+
if (failure.detail !== null && failure.detail !== failure.prerequisite.description)
|
|
191
|
+
lines.push(` ${failure.detail}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (infrastructure)
|
|
195
|
+
lines.push(" This infrastructure failure is not proof of a PATH mismatch.");
|
|
196
|
+
return {
|
|
197
|
+
ok: false,
|
|
198
|
+
failureKind: infrastructure ? "infrastructure" : "requirements",
|
|
199
|
+
lines,
|
|
200
|
+
plan: result.plan,
|
|
201
|
+
adviceShell: result.plan.shell,
|
|
202
|
+
pathAdviceRecommended: !infrastructure
|
|
203
|
+
&& failedPrerequisites.some(nativeServicePrerequisiteNeedsPathAdvice),
|
|
204
|
+
failedPrerequisites,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function scopeHeading(scope) {
|
|
208
|
+
if (scope.kind === "installed-development")
|
|
209
|
+
return "Installed development native-service plan:";
|
|
210
|
+
return `Prospective production native-service plan (${scope.reason ?? "installed strategy is unknown"}):`;
|
|
211
|
+
}
|
|
212
|
+
function parseConsistentDefinitions(backend, definitions) {
|
|
213
|
+
if (definitions.length === 0)
|
|
214
|
+
return { ok: false, message: "No installed service definitions were provided." };
|
|
215
|
+
const parsed = [];
|
|
216
|
+
for (const definition of definitions) {
|
|
217
|
+
const result = backend.kind === "systemd"
|
|
218
|
+
? parseSystemdDefinition(definition)
|
|
219
|
+
: parseLaunchdDefinition(definition);
|
|
220
|
+
if (!result.ok)
|
|
221
|
+
return result;
|
|
222
|
+
parsed.push(result.value);
|
|
223
|
+
}
|
|
224
|
+
const first = parsed[0] ?? impossibleMissingDefinition();
|
|
225
|
+
for (const definition of parsed.slice(1)) {
|
|
226
|
+
if (definition.shell.executable !== first.shell.executable) {
|
|
227
|
+
return { ok: false, message: "Installed service definitions use different login shells." };
|
|
228
|
+
}
|
|
229
|
+
if (!recordsEqual(definition.environment, first.environment)) {
|
|
230
|
+
return { ok: false, message: "Installed service definitions use different environments." };
|
|
231
|
+
}
|
|
232
|
+
if (definition.workingDirectory !== first.workingDirectory) {
|
|
233
|
+
return { ok: false, message: "Installed service definitions use different working directories." };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return { ok: true, value: parsed };
|
|
237
|
+
}
|
|
238
|
+
function systemdServiceDirectives(contents) {
|
|
239
|
+
const allowed = new Set(["Type", "WorkingDirectory", "Environment", "ExecStart", "Restart", "RestartSec", "KillMode", "TimeoutStopSec"]);
|
|
240
|
+
const directives = [];
|
|
241
|
+
let inServiceSection = false;
|
|
242
|
+
let foundServiceSection = false;
|
|
243
|
+
for (const line of contents.split(/\r?\n/u)) {
|
|
244
|
+
// systemd syntax trims ASCII spaces and tabs here; JavaScript's trim/\s
|
|
245
|
+
// would also consume Unicode whitespace that systemd treats as key text.
|
|
246
|
+
const trimmed = line.replace(/^[ \t]+|[ \t]+$/gu, "");
|
|
247
|
+
if (/^\[[^\]]+\]$/u.test(trimmed)) {
|
|
248
|
+
inServiceSection = trimmed === "[Service]";
|
|
249
|
+
foundServiceSection ||= inServiceSection;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (!inServiceSection || trimmed === "" || trimmed.startsWith("#") || trimmed.startsWith(";"))
|
|
253
|
+
continue;
|
|
254
|
+
// Slice after the recognized prefix so JavaScript-only line separators remain directive data.
|
|
255
|
+
const match = /^[ \t]*([A-Za-z][A-Za-z0-9]*)=/u.exec(line);
|
|
256
|
+
const name = match?.[1];
|
|
257
|
+
if (match === null || name === undefined || !allowed.has(name))
|
|
258
|
+
return undefined;
|
|
259
|
+
directives.push({ name, value: line.slice(match[0].length) });
|
|
260
|
+
}
|
|
261
|
+
return foundServiceSection ? directives : undefined;
|
|
262
|
+
}
|
|
263
|
+
function hasSystemdPhysicalLineContinuation(line) {
|
|
264
|
+
let trailingBackslashes = 0;
|
|
265
|
+
for (let index = line.length - 1; index >= 0 && line[index] === "\\"; index -= 1)
|
|
266
|
+
trailingBackslashes += 1;
|
|
267
|
+
return trailingBackslashes % 2 === 1;
|
|
268
|
+
}
|
|
269
|
+
/** Parse PI WEB's bounded ExecStart argument structure, including persisted legacy encodings. */
|
|
270
|
+
function parseSystemdExecStart(value) {
|
|
271
|
+
const environmentPrefix = "/usr/bin/env ";
|
|
272
|
+
const shellOffset = value.startsWith(environmentPrefix) ? environmentPrefix.length : 0;
|
|
273
|
+
const shell = parseSystemdExecStartArgument(value, shellOffset);
|
|
274
|
+
if (shell === undefined)
|
|
275
|
+
return undefined;
|
|
276
|
+
const loginShellSeparator = " -lc ";
|
|
277
|
+
if (!value.startsWith(loginShellSeparator, shell.endOffset))
|
|
278
|
+
return undefined;
|
|
279
|
+
const shellCommandOffset = shell.endOffset + loginShellSeparator.length;
|
|
280
|
+
if (shellCommandOffset >= value.length)
|
|
281
|
+
return undefined;
|
|
282
|
+
return {
|
|
283
|
+
shellArgument: shell.value,
|
|
284
|
+
shellCommandArgument: value.slice(shellCommandOffset),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
/** Scan one systemd argument linearly so delimiters inside quoted values remain data. */
|
|
288
|
+
function parseSystemdExecStartArgument(value, offset) {
|
|
289
|
+
if (offset >= value.length || value[offset] === " " || value[offset] === "\t")
|
|
290
|
+
return undefined;
|
|
291
|
+
let quote = null;
|
|
292
|
+
for (let index = offset; index < value.length; index += 1) {
|
|
293
|
+
const character = value[index];
|
|
294
|
+
if (character === "\\") {
|
|
295
|
+
index += 1;
|
|
296
|
+
if (index >= value.length)
|
|
297
|
+
return undefined;
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (quote === null && (character === " " || character === "\t")) {
|
|
301
|
+
return { value: value.slice(offset, index), endOffset: index };
|
|
302
|
+
}
|
|
303
|
+
if (character !== "\"" && character !== "'")
|
|
304
|
+
continue;
|
|
305
|
+
if (quote === null)
|
|
306
|
+
quote = character;
|
|
307
|
+
else if (character === quote)
|
|
308
|
+
quote = null;
|
|
309
|
+
}
|
|
310
|
+
return quote === null ? { value: value.slice(offset), endOffset: value.length } : undefined;
|
|
311
|
+
}
|
|
312
|
+
// Keep malformed installed input linear; overlapping escaped/raw regex alternatives can backtrack exponentially.
|
|
313
|
+
function isSingleSystemdQuotedValue(value) {
|
|
314
|
+
if (value.length < 2 || !value.startsWith('"') || !value.endsWith('"'))
|
|
315
|
+
return false;
|
|
316
|
+
const closingQuoteIndex = value.length - 1;
|
|
317
|
+
for (let index = 1; index < closingQuoteIndex; index += 1) {
|
|
318
|
+
const character = value[index];
|
|
319
|
+
if (character === '"')
|
|
320
|
+
return false;
|
|
321
|
+
if (character !== "\\")
|
|
322
|
+
continue;
|
|
323
|
+
index += 1;
|
|
324
|
+
if (index >= closingQuoteIndex)
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
function parseSystemdDefinition(definition) {
|
|
330
|
+
if (definition.contents.split(/\r?\n/u).some(hasSystemdPhysicalLineContinuation)) {
|
|
331
|
+
return {
|
|
332
|
+
ok: false,
|
|
333
|
+
message: `Installed ${definition.id} systemd unit uses physical-line continuation, which cannot be inspected safely.`,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
const directives = systemdServiceDirectives(definition.contents);
|
|
337
|
+
if (directives === undefined) {
|
|
338
|
+
return { ok: false, message: `Installed ${definition.id} systemd unit has unrecognized service directives.` };
|
|
339
|
+
}
|
|
340
|
+
const execStarts = directives.filter((directive) => directive.name === "ExecStart");
|
|
341
|
+
const execStart = execStarts.length === 1
|
|
342
|
+
? parseSystemdExecStart(execStarts[0]?.value ?? "")
|
|
343
|
+
: undefined;
|
|
344
|
+
if (execStart === undefined) {
|
|
345
|
+
return { ok: false, message: `Installed ${definition.id} systemd unit must have exactly one recognized ExecStart.` };
|
|
346
|
+
}
|
|
347
|
+
const shellExecutable = parseSystemdExecArgument(execStart.shellArgument);
|
|
348
|
+
if (shellExecutable === undefined) {
|
|
349
|
+
return { ok: false, message: `Installed ${definition.id} systemd unit has an unrecognized login shell argument.` };
|
|
350
|
+
}
|
|
351
|
+
const shell = installedShell(shellExecutable);
|
|
352
|
+
if (!shell.ok)
|
|
353
|
+
return shell;
|
|
354
|
+
const shellCommand = parseSystemdShellCommand(shell.value.name, execStart.shellCommandArgument);
|
|
355
|
+
if (shellCommand === undefined) {
|
|
356
|
+
return { ok: false, message: `Installed ${definition.id} systemd unit has an unrecognized shell command.` };
|
|
357
|
+
}
|
|
358
|
+
const environment = new Map();
|
|
359
|
+
for (const directive of directives.filter((item) => item.name === "Environment")) {
|
|
360
|
+
const rawValue = directive.value;
|
|
361
|
+
if (!isSingleSystemdQuotedValue(rawValue)) {
|
|
362
|
+
return { ok: false, message: `Installed ${definition.id} systemd unit has an unrecognized environment entry.` };
|
|
363
|
+
}
|
|
364
|
+
const assignment = parseSystemdDirectiveValue(rawValue);
|
|
365
|
+
const separator = assignment?.indexOf("=") ?? -1;
|
|
366
|
+
const key = assignment?.slice(0, separator) ?? "";
|
|
367
|
+
if (separator <= 0 || !/^[A-Za-z_][A-Za-z0-9_]*$/u.test(key) || environment.has(key)) {
|
|
368
|
+
return { ok: false, message: `Installed ${definition.id} systemd unit has a malformed environment entry.` };
|
|
369
|
+
}
|
|
370
|
+
environment.set(key, assignment?.slice(separator + 1) ?? "");
|
|
371
|
+
}
|
|
372
|
+
const workingDirectories = directives.filter((directive) => directive.name === "WorkingDirectory");
|
|
373
|
+
if (workingDirectories.length > 1) {
|
|
374
|
+
return { ok: false, message: `Installed ${definition.id} systemd unit has duplicate working directories.` };
|
|
375
|
+
}
|
|
376
|
+
const rawWorkingDirectory = workingDirectories[0]?.value;
|
|
377
|
+
if (rawWorkingDirectory?.startsWith('"') === true || rawWorkingDirectory?.startsWith("'") === true) {
|
|
378
|
+
return { ok: false, message: `Installed ${definition.id} systemd unit has an invalid quoted working directory.` };
|
|
379
|
+
}
|
|
380
|
+
const workingDirectory = rawWorkingDirectory === undefined
|
|
381
|
+
? null
|
|
382
|
+
: parseSystemdDirectiveValue(rawWorkingDirectory);
|
|
383
|
+
if (workingDirectories.length === 1 && workingDirectory === undefined) {
|
|
384
|
+
return { ok: false, message: `Installed ${definition.id} systemd unit has a malformed working directory.` };
|
|
385
|
+
}
|
|
386
|
+
return {
|
|
387
|
+
ok: true,
|
|
388
|
+
value: {
|
|
389
|
+
id: definition.id,
|
|
390
|
+
shell: shell.value,
|
|
391
|
+
environment: Object.fromEntries(environment),
|
|
392
|
+
workingDirectory: workingDirectory ?? null,
|
|
393
|
+
shellCommand,
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
function parseLaunchdDefinition(definition) {
|
|
398
|
+
const plist = parseLaunchdPlistDocument(definition.contents);
|
|
399
|
+
if (plist === undefined) {
|
|
400
|
+
return { ok: false, message: `Installed ${definition.id} LaunchAgent is not a structurally valid property list.` };
|
|
401
|
+
}
|
|
402
|
+
const label = plistStringValue(plist.get("Label"));
|
|
403
|
+
if (label === undefined) {
|
|
404
|
+
return { ok: false, message: `Installed ${definition.id} LaunchAgent has an unrecognized Label.` };
|
|
405
|
+
}
|
|
406
|
+
const expectedLabel = nativeServiceManagerRefs[definition.id].launchdLabel;
|
|
407
|
+
if (label !== expectedLabel) {
|
|
408
|
+
return {
|
|
409
|
+
ok: false,
|
|
410
|
+
message: `Installed ${definition.id} LaunchAgent declares Label ${JSON.stringify(label)} instead of ${JSON.stringify(expectedLabel)}.`,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
const arguments_ = plistStringArray(plist.get("ProgramArguments"));
|
|
414
|
+
if (arguments_?.length !== 4 || arguments_[0] !== "/usr/bin/env" || arguments_[2] !== "-lc") {
|
|
415
|
+
return { ok: false, message: `Installed ${definition.id} LaunchAgent has unrecognized ProgramArguments.` };
|
|
416
|
+
}
|
|
417
|
+
const shell = installedShell(arguments_[1] ?? "");
|
|
418
|
+
if (!shell.ok)
|
|
419
|
+
return shell;
|
|
420
|
+
const rawEnvironment = plist.get("EnvironmentVariables");
|
|
421
|
+
const environment = rawEnvironment === undefined ? {} : plistStringDictionary(rawEnvironment);
|
|
422
|
+
if (environment === undefined) {
|
|
423
|
+
return { ok: false, message: `Installed ${definition.id} LaunchAgent has a malformed environment dictionary.` };
|
|
424
|
+
}
|
|
425
|
+
const rawWorkingDirectory = plist.get("WorkingDirectory");
|
|
426
|
+
const workingDirectory = rawWorkingDirectory === undefined ? null : plistStringValue(rawWorkingDirectory);
|
|
427
|
+
if (rawWorkingDirectory !== undefined && workingDirectory === undefined) {
|
|
428
|
+
return { ok: false, message: `Installed ${definition.id} LaunchAgent has a malformed working directory.` };
|
|
429
|
+
}
|
|
430
|
+
return {
|
|
431
|
+
ok: true,
|
|
432
|
+
value: {
|
|
433
|
+
id: definition.id,
|
|
434
|
+
shell: shell.value,
|
|
435
|
+
environment,
|
|
436
|
+
workingDirectory: workingDirectory ?? null,
|
|
437
|
+
shellCommand: arguments_[3] ?? "",
|
|
438
|
+
},
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
function installedShell(executable) {
|
|
442
|
+
const name = posixPath.basename(executable).replace(/^-/, "");
|
|
443
|
+
if (name !== "bash" && name !== "zsh" && name !== "fish") {
|
|
444
|
+
return { ok: false, message: `Installed service definition uses unsupported login shell ${executable}.` };
|
|
445
|
+
}
|
|
446
|
+
return {
|
|
447
|
+
ok: true,
|
|
448
|
+
value: { name, executable, source: "detected", detectedExecutable: executable },
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
function parseSystemdExecArgument(value) {
|
|
452
|
+
const quoted = value.startsWith('"') || value.endsWith('"');
|
|
453
|
+
if (quoted ? !isSingleSystemdQuotedValue(value) : /["']/u.test(value))
|
|
454
|
+
return undefined;
|
|
455
|
+
const decoded = decodeSystemdEscapes(quoted ? value.slice(1, -1) : value);
|
|
456
|
+
return decoded === undefined ? undefined : decodeSystemdSubstitutions(decoded, true);
|
|
457
|
+
}
|
|
458
|
+
function parseSystemdDirectiveValue(value) {
|
|
459
|
+
const decoded = parseSystemdEscapedValue(value);
|
|
460
|
+
return decoded === undefined ? undefined : decodeSystemdSubstitutions(decoded, false);
|
|
461
|
+
}
|
|
462
|
+
function parseSystemdEscapedValue(value) {
|
|
463
|
+
const quoted = value.startsWith('"') || value.endsWith('"');
|
|
464
|
+
if (quoted && (!value.startsWith('"') || !value.endsWith('"')))
|
|
465
|
+
return undefined;
|
|
466
|
+
return decodeSystemdEscapes(quoted ? value.slice(1, -1) : value);
|
|
467
|
+
}
|
|
468
|
+
export function decodeSystemdEscapes(value) {
|
|
469
|
+
const bytes = [];
|
|
470
|
+
const encoder = new TextEncoder();
|
|
471
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
472
|
+
const character = value[index];
|
|
473
|
+
if (character !== "\\") {
|
|
474
|
+
const codePoint = value.codePointAt(index);
|
|
475
|
+
if (codePoint === undefined || codePoint === 0 || (codePoint >= 0xd800 && codePoint <= 0xdfff))
|
|
476
|
+
return undefined;
|
|
477
|
+
bytes.push(...encoder.encode(String.fromCodePoint(codePoint)));
|
|
478
|
+
if (codePoint > 0xffff)
|
|
479
|
+
index += 1;
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
const escape = value[index + 1];
|
|
483
|
+
if (escape === undefined)
|
|
484
|
+
return undefined;
|
|
485
|
+
const simpleEscapes = {
|
|
486
|
+
"\\": "\\",
|
|
487
|
+
'"': '"',
|
|
488
|
+
"'": "'",
|
|
489
|
+
a: "\u0007",
|
|
490
|
+
b: "\b",
|
|
491
|
+
e: "\u001b",
|
|
492
|
+
f: "\f",
|
|
493
|
+
n: "\n",
|
|
494
|
+
r: "\r",
|
|
495
|
+
s: " ",
|
|
496
|
+
t: "\t",
|
|
497
|
+
v: "\v",
|
|
498
|
+
};
|
|
499
|
+
const simple = simpleEscapes[escape];
|
|
500
|
+
if (simple !== undefined) {
|
|
501
|
+
bytes.push(...encoder.encode(simple));
|
|
502
|
+
index += 1;
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
if (/^[0-7]$/u.test(escape)) {
|
|
506
|
+
const encoded = value.slice(index + 1, index + 4);
|
|
507
|
+
if (!/^[0-7]{3}$/u.test(encoded))
|
|
508
|
+
return undefined;
|
|
509
|
+
const decoded = Number.parseInt(encoded, 8);
|
|
510
|
+
if (decoded === 0 || decoded > 0xff)
|
|
511
|
+
return undefined;
|
|
512
|
+
bytes.push(decoded);
|
|
513
|
+
index += 3;
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
const length = escape === "x" ? 2 : escape === "u" ? 4 : escape === "U" ? 8 : 0;
|
|
517
|
+
if (length === 0)
|
|
518
|
+
return undefined;
|
|
519
|
+
const encoded = value.slice(index + 2, index + 2 + length);
|
|
520
|
+
if (encoded.length !== length || !new RegExp(`^[0-9a-fA-F]{${String(length)}}$`, "u").test(encoded))
|
|
521
|
+
return undefined;
|
|
522
|
+
const decoded = Number.parseInt(encoded, 16);
|
|
523
|
+
if (decoded === 0 || decoded > 0x10ffff || (decoded >= 0xd800 && decoded <= 0xdfff))
|
|
524
|
+
return undefined;
|
|
525
|
+
if (escape === "x")
|
|
526
|
+
bytes.push(decoded);
|
|
527
|
+
else
|
|
528
|
+
bytes.push(...encoder.encode(String.fromCodePoint(decoded)));
|
|
529
|
+
index += length + 1;
|
|
530
|
+
}
|
|
531
|
+
try {
|
|
532
|
+
// Keep a BOM produced by an escape sequence as U+FEFF. The default
|
|
533
|
+
// decoder behavior strips leading UTF-8 BOM bytes, which could turn an
|
|
534
|
+
// invalid BOM-prefixed environment key into PI_WEB_CONFIG.
|
|
535
|
+
return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(Uint8Array.from(bytes));
|
|
536
|
+
}
|
|
537
|
+
catch {
|
|
538
|
+
return undefined;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
function decodeSystemdSubstitutions(value, decodeDollars) {
|
|
542
|
+
let result = "";
|
|
543
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
544
|
+
const character = value[index];
|
|
545
|
+
if (character !== "%" && !(decodeDollars && character === "$")) {
|
|
546
|
+
result += character ?? "";
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
if (value[index + 1] !== character)
|
|
550
|
+
return undefined;
|
|
551
|
+
result += character;
|
|
552
|
+
index += 1;
|
|
553
|
+
}
|
|
554
|
+
return result;
|
|
555
|
+
}
|
|
556
|
+
function parseSystemdShellCommand(shell, value) {
|
|
557
|
+
if (value.startsWith('"') || value.endsWith('"'))
|
|
558
|
+
return parseSystemdExecArgument(value);
|
|
559
|
+
if (!value.startsWith("'") || !value.endsWith("'"))
|
|
560
|
+
return undefined;
|
|
561
|
+
const inner = value.slice(1, -1);
|
|
562
|
+
const unquoted = shell === "fish" ? fishSingleQuoteUnescape(inner) : inner.replaceAll("'\\''", "'");
|
|
563
|
+
if (legacySystemdShellQuote(shell, unquoted) !== value)
|
|
564
|
+
return undefined;
|
|
565
|
+
// The pre-hardening renderer doubled percent specifiers, but its
|
|
566
|
+
// replacement-string "$$" left dollar characters unchanged.
|
|
567
|
+
return decodeSystemdSubstitutions(unquoted, false);
|
|
568
|
+
}
|
|
569
|
+
function legacySystemdShellQuote(shell, value) {
|
|
570
|
+
const escaped = shell === "fish"
|
|
571
|
+
? value.replaceAll("\\", "\\\\").replaceAll("'", "\\'")
|
|
572
|
+
: value.replaceAll("'", "'\\''");
|
|
573
|
+
return `'${escaped}'`;
|
|
574
|
+
}
|
|
575
|
+
function fishSingleQuoteUnescape(value) {
|
|
576
|
+
let result = "";
|
|
577
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
578
|
+
const character = value[index];
|
|
579
|
+
if (character === "\\" && index + 1 < value.length) {
|
|
580
|
+
result += value[index + 1] ?? "";
|
|
581
|
+
index += 1;
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
result += character ?? "";
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
return result;
|
|
588
|
+
}
|
|
589
|
+
const plistXmlDeclaration = '<?xml version="1.0" encoding="UTF-8"?>';
|
|
590
|
+
const plistDoctype = '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">';
|
|
591
|
+
const maximumPlistDepth = 32;
|
|
592
|
+
/** Parse the complete XML plist subset emitted by PI WEB, rejecting fragments and duplicate dictionary keys. */
|
|
593
|
+
function parseLaunchdPlistDocument(contents) {
|
|
594
|
+
const normalizedContents = normalizePlistXml(contents);
|
|
595
|
+
if (normalizedContents === undefined)
|
|
596
|
+
return undefined;
|
|
597
|
+
const cursor = {
|
|
598
|
+
contents: normalizedContents,
|
|
599
|
+
offset: normalizedContents.startsWith("\uFEFF") ? 1 : 0,
|
|
600
|
+
};
|
|
601
|
+
skipPlistWhitespace(cursor);
|
|
602
|
+
if (consumePlistToken(cursor, plistXmlDeclaration))
|
|
603
|
+
skipPlistWhitespace(cursor);
|
|
604
|
+
else if (cursor.contents.startsWith("<?xml", cursor.offset))
|
|
605
|
+
return undefined;
|
|
606
|
+
if (consumePlistToken(cursor, plistDoctype))
|
|
607
|
+
skipPlistWhitespace(cursor);
|
|
608
|
+
else if (cursor.contents.startsWith("<!DOCTYPE", cursor.offset))
|
|
609
|
+
return undefined;
|
|
610
|
+
if (!consumePlistToken(cursor, '<plist version="1.0">'))
|
|
611
|
+
return undefined;
|
|
612
|
+
const root = parsePlistValue(cursor, 0);
|
|
613
|
+
if (root?.kind !== "dictionary")
|
|
614
|
+
return undefined;
|
|
615
|
+
skipPlistWhitespace(cursor);
|
|
616
|
+
if (!consumePlistToken(cursor, "</plist>"))
|
|
617
|
+
return undefined;
|
|
618
|
+
skipPlistWhitespace(cursor);
|
|
619
|
+
return cursor.offset === cursor.contents.length ? root.value : undefined;
|
|
620
|
+
}
|
|
621
|
+
function parsePlistValue(cursor, depth) {
|
|
622
|
+
if (depth > maximumPlistDepth)
|
|
623
|
+
return undefined;
|
|
624
|
+
skipPlistWhitespace(cursor);
|
|
625
|
+
if (cursor.contents.startsWith("<string>", cursor.offset)) {
|
|
626
|
+
const value = parsePlistTextElement(cursor, "string");
|
|
627
|
+
return value === undefined ? undefined : { kind: "string", value };
|
|
628
|
+
}
|
|
629
|
+
for (const tag of ["data", "date", "integer", "real"]) {
|
|
630
|
+
if (!cursor.contents.startsWith(`<${tag}>`, cursor.offset))
|
|
631
|
+
continue;
|
|
632
|
+
const value = parsePlistTextElement(cursor, tag);
|
|
633
|
+
return value === undefined || !isValidPlistScalarValue(tag, value)
|
|
634
|
+
? undefined
|
|
635
|
+
: { kind: "scalar", value };
|
|
636
|
+
}
|
|
637
|
+
if (cursor.contents.startsWith("<array", cursor.offset))
|
|
638
|
+
return parsePlistArray(cursor, depth);
|
|
639
|
+
if (cursor.contents.startsWith("<dict", cursor.offset))
|
|
640
|
+
return parsePlistDictionary(cursor, depth);
|
|
641
|
+
if (consumePlistToken(cursor, "<true/>"))
|
|
642
|
+
return { kind: "boolean", value: true };
|
|
643
|
+
if (consumePlistToken(cursor, "<false/>"))
|
|
644
|
+
return { kind: "boolean", value: false };
|
|
645
|
+
return undefined;
|
|
646
|
+
}
|
|
647
|
+
function parsePlistArray(cursor, depth) {
|
|
648
|
+
if (consumePlistToken(cursor, "<array/>"))
|
|
649
|
+
return { kind: "array", value: [] };
|
|
650
|
+
if (!consumePlistToken(cursor, "<array>"))
|
|
651
|
+
return undefined;
|
|
652
|
+
const values = [];
|
|
653
|
+
for (;;) {
|
|
654
|
+
skipPlistWhitespace(cursor);
|
|
655
|
+
if (consumePlistToken(cursor, "</array>"))
|
|
656
|
+
return { kind: "array", value: values };
|
|
657
|
+
const value = parsePlistValue(cursor, depth + 1);
|
|
658
|
+
if (value === undefined)
|
|
659
|
+
return undefined;
|
|
660
|
+
values.push(value);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
function parsePlistDictionary(cursor, depth) {
|
|
664
|
+
if (consumePlistToken(cursor, "<dict/>"))
|
|
665
|
+
return { kind: "dictionary", value: new Map() };
|
|
666
|
+
if (!consumePlistToken(cursor, "<dict>"))
|
|
667
|
+
return undefined;
|
|
668
|
+
const values = new Map();
|
|
669
|
+
for (;;) {
|
|
670
|
+
skipPlistWhitespace(cursor);
|
|
671
|
+
if (consumePlistToken(cursor, "</dict>"))
|
|
672
|
+
return { kind: "dictionary", value: values };
|
|
673
|
+
const key = parsePlistTextElement(cursor, "key");
|
|
674
|
+
if (key === undefined || values.has(key))
|
|
675
|
+
return undefined;
|
|
676
|
+
const value = parsePlistValue(cursor, depth + 1);
|
|
677
|
+
if (value === undefined)
|
|
678
|
+
return undefined;
|
|
679
|
+
values.set(key, value);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
function parsePlistTextElement(cursor, tag) {
|
|
683
|
+
const openingTag = `<${tag}>`;
|
|
684
|
+
const closingTag = `</${tag}>`;
|
|
685
|
+
if (!consumePlistToken(cursor, openingTag))
|
|
686
|
+
return undefined;
|
|
687
|
+
const closingOffset = cursor.contents.indexOf(closingTag, cursor.offset);
|
|
688
|
+
if (closingOffset < 0)
|
|
689
|
+
return undefined;
|
|
690
|
+
const encoded = cursor.contents.slice(cursor.offset, closingOffset);
|
|
691
|
+
cursor.offset = closingOffset + closingTag.length;
|
|
692
|
+
return xmlUnescapeStrict(encoded);
|
|
693
|
+
}
|
|
694
|
+
function normalizePlistXml(contents) {
|
|
695
|
+
for (const character of contents) {
|
|
696
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
697
|
+
const isXmlCharacter = codePoint === 0x09
|
|
698
|
+
|| codePoint === 0x0a
|
|
699
|
+
|| codePoint === 0x0d
|
|
700
|
+
|| (codePoint >= 0x20 && codePoint <= 0xd7ff)
|
|
701
|
+
|| (codePoint >= 0xe000 && codePoint <= 0xfffd)
|
|
702
|
+
|| (codePoint >= 0x10000 && codePoint <= 0x10ffff);
|
|
703
|
+
if (!isXmlCharacter)
|
|
704
|
+
return undefined;
|
|
705
|
+
}
|
|
706
|
+
return contents.replace(/\r\n?/gu, "\n");
|
|
707
|
+
}
|
|
708
|
+
function isValidPlistScalarValue(tag, value) {
|
|
709
|
+
if (tag === "data") {
|
|
710
|
+
const compact = value.replace(/[\t\n\r ]/gu, "");
|
|
711
|
+
return /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(compact);
|
|
712
|
+
}
|
|
713
|
+
if (tag === "date") {
|
|
714
|
+
const match = /^(\d{4})-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/u.exec(value);
|
|
715
|
+
if (match?.[1] === undefined || Number(match[1]) === 0)
|
|
716
|
+
return false;
|
|
717
|
+
const timestamp = Date.parse(value);
|
|
718
|
+
return Number.isFinite(timestamp)
|
|
719
|
+
&& new Date(timestamp).toISOString() === `${value.slice(0, -1)}.000Z`;
|
|
720
|
+
}
|
|
721
|
+
if (tag === "integer") {
|
|
722
|
+
if (!/^[+-]?(?:0|[1-9][0-9]*)$/u.test(value))
|
|
723
|
+
return false;
|
|
724
|
+
const parsed = BigInt(value);
|
|
725
|
+
return parsed >= -(2n ** 63n) && parsed <= (2n ** 63n) - 1n;
|
|
726
|
+
}
|
|
727
|
+
return /^[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?$/u.test(value)
|
|
728
|
+
&& Number.isFinite(Number(value));
|
|
729
|
+
}
|
|
730
|
+
function skipPlistWhitespace(cursor) {
|
|
731
|
+
while (/[\t\n\r ]/u.test(cursor.contents[cursor.offset] ?? ""))
|
|
732
|
+
cursor.offset += 1;
|
|
733
|
+
}
|
|
734
|
+
function consumePlistToken(cursor, token) {
|
|
735
|
+
if (!cursor.contents.startsWith(token, cursor.offset))
|
|
736
|
+
return false;
|
|
737
|
+
cursor.offset += token.length;
|
|
738
|
+
return true;
|
|
739
|
+
}
|
|
740
|
+
function plistStringValue(value) {
|
|
741
|
+
return value?.kind === "string" ? value.value : undefined;
|
|
742
|
+
}
|
|
743
|
+
function plistStringArray(value) {
|
|
744
|
+
if (value?.kind !== "array")
|
|
745
|
+
return undefined;
|
|
746
|
+
const strings = [];
|
|
747
|
+
for (const item of value.value) {
|
|
748
|
+
if (item.kind !== "string")
|
|
749
|
+
return undefined;
|
|
750
|
+
strings.push(item.value);
|
|
751
|
+
}
|
|
752
|
+
return strings;
|
|
753
|
+
}
|
|
754
|
+
function plistStringDictionary(value) {
|
|
755
|
+
if (value.kind !== "dictionary")
|
|
756
|
+
return undefined;
|
|
757
|
+
const entries = [];
|
|
758
|
+
for (const [key, item] of value.value) {
|
|
759
|
+
if (item.kind !== "string")
|
|
760
|
+
return undefined;
|
|
761
|
+
entries.push([key, item.value]);
|
|
762
|
+
}
|
|
763
|
+
return Object.fromEntries(entries);
|
|
764
|
+
}
|
|
765
|
+
function xmlUnescapeStrict(value) {
|
|
766
|
+
if (/[<>]/u.test(value) || /&(?!(?:apos|quot|gt|lt|amp);)/u.test(value))
|
|
767
|
+
return undefined;
|
|
768
|
+
return value
|
|
769
|
+
.replaceAll("'", "'")
|
|
770
|
+
.replaceAll(""", '"')
|
|
771
|
+
.replaceAll(">", ">")
|
|
772
|
+
.replaceAll("<", "<")
|
|
773
|
+
.replaceAll("&", "&");
|
|
774
|
+
}
|
|
775
|
+
function recordsEqual(left, right) {
|
|
776
|
+
const leftEntries = Object.entries(left);
|
|
777
|
+
return leftEntries.length === Object.keys(right).length
|
|
778
|
+
&& leftEntries.every(([key, value]) => Object.hasOwn(right, key) && right[key] === value);
|
|
779
|
+
}
|
|
780
|
+
function impossibleMissingDefinition() {
|
|
781
|
+
throw new Error("Expected at least one installed native service definition");
|
|
782
|
+
}
|
|
783
|
+
//# sourceMappingURL=serviceDoctor.js.map
|