@jmfederico/pi-web 1.202608.2 → 1.202609.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.
Files changed (233) hide show
  1. package/README.md +3 -3
  2. package/dist/.plugins-ready +1 -0
  3. package/dist/cli.js +5 -2
  4. package/dist/cli.js.map +1 -1
  5. package/dist/client/apple-touch-icon-dev.png +0 -0
  6. package/dist/client/assets/index-AroYaCLx.js +4130 -0
  7. package/dist/client/assets/vendor-editor-core-YWog6mAR.js +10 -0
  8. package/dist/client/assets/vendor-editor-languages-DOCw3vc9.js +28 -0
  9. package/dist/client/favicon-dev.svg +8 -0
  10. package/dist/client/index.html +5 -5
  11. package/dist/client/pwa-icon-dev-192.png +0 -0
  12. package/dist/client/pwa-icon-dev-512.png +0 -0
  13. package/dist/config.js +20 -0
  14. package/dist/config.js.map +1 -1
  15. package/dist/docker/piWebDockerCommandPlan.js +9 -1
  16. package/dist/docker/piWebDockerCommandPlan.js.map +1 -1
  17. package/dist/nativeServices/installedServiceDefinitions.js +25 -19
  18. package/dist/nativeServices/installedServiceDefinitions.js.map +1 -1
  19. package/dist/nativeServices/serviceAction.js +38 -26
  20. package/dist/nativeServices/serviceAction.js.map +1 -1
  21. package/dist/nativeServices/servicePlan.js +3 -3
  22. package/dist/nativeServices/servicePlan.js.map +1 -1
  23. package/dist/pi-packages/captains-log/README.md +11 -0
  24. package/dist/pi-packages/captains-log/dist/browser/channelProtocol.js +29 -0
  25. package/dist/pi-packages/captains-log/dist/browser/index.js +144 -0
  26. package/dist/pi-packages/captains-log/dist/browser/markdown.js +67 -0
  27. package/dist/pi-packages/captains-log/dist/browser/panel.js +105 -0
  28. package/dist/pi-packages/captains-log/dist/browser/protocol.js +26 -0
  29. package/dist/pi-packages/captains-log/dist/companion.js +126 -0
  30. package/dist/pi-packages/captains-log/dist/roundtrip.js +62 -0
  31. package/dist/pi-packages/captains-log/dist/server.js +210 -0
  32. package/dist/pi-packages/captains-log/dist/store.js +75 -0
  33. package/dist/pi-packages/captains-log/docs/usage.md +30 -0
  34. package/dist/pi-packages/captains-log/package.json +35 -0
  35. package/dist/pi-packages/captains-log/tsconfig.json +21 -0
  36. package/dist/pi-packages/captains-log/vite.config.mjs +28 -0
  37. package/dist/pi-packages/relays/package.json +1 -1
  38. package/dist/pi-packages/relays/pi-web-plugin.js +1 -1
  39. package/dist/pi-packages/relays/prompts/relay-worktree.md +13 -142
  40. package/dist/pi-packages/relays/prompts/relay.md +25 -132
  41. package/dist/pi-packages/relays/relayDiscovery.js +2 -2
  42. package/dist/pi-packages/relays/relaysPanelElement.js +1 -1
  43. package/dist/pi-packages/relays/skills/relay/SKILL.md +59 -88
  44. package/dist/pi-packages/relays/skills/relay-runner/SKILL.md +253 -0
  45. package/dist/pi-web-plugins/files/browser/assets/files-icon-DZObYhxb.svg +3 -0
  46. package/dist/pi-web-plugins/files/browser/pi-web-plugin.js +439 -0
  47. package/dist/pi-web-plugins/files/package.json +15 -0
  48. package/dist/pi-web-plugins/git/browser/git-panel.js +54 -24
  49. package/dist/pi-web-plugins/git/browser/pi-web-plugin.js +1 -1
  50. package/dist/pi-web-plugins/git/git-backend.js +5 -5
  51. package/dist/pi-web-plugins/git/server-plugin.js +5 -3
  52. package/dist/pi-web-plugins/info/pi-web-plugin.js +1 -1
  53. package/dist/pi-web-plugins/mermaid/browser/mermaid-engine.js +3590 -0
  54. package/dist/pi-web-plugins/mermaid/browser/pi-web-plugin.js +6 -0
  55. package/dist/pi-web-plugins/mermaid/package.json +13 -0
  56. package/dist/pi-web-plugins/terminal/browser/pi-web-plugin.js +210 -0
  57. package/dist/pi-web-plugins/terminal/package.json +16 -0
  58. package/dist/pi-web-plugins/terminal/server-plugin.js +365 -0
  59. package/dist/{server/terminals → pi-web-plugins/terminal}/terminalService.js +162 -65
  60. package/dist/pi-web-plugins/updates/pi-web-plugin.js +45 -35
  61. package/dist/pi-web-plugins/workspace-tasks/pi-web-plugin.js +1 -1
  62. package/dist/piWebVersionReport.js +12 -4
  63. package/dist/piWebVersionReport.js.map +1 -1
  64. package/dist/plugin-api.d.ts +222 -23
  65. package/dist/plugin-api.js +1 -0
  66. package/dist/pluginRecoveryCli.js +3 -2
  67. package/dist/pluginRecoveryCli.js.map +1 -1
  68. package/dist/server/activity/workspaceActivityService.js.map +1 -1
  69. package/dist/server/app.js +33 -12
  70. package/dist/server/app.js.map +1 -1
  71. package/dist/server/configRoutes.js +6 -1
  72. package/dist/server/configRoutes.js.map +1 -1
  73. package/dist/server/deploymentIdentity.js +67 -0
  74. package/dist/server/deploymentIdentity.js.map +1 -0
  75. package/dist/server/deploymentIdentityRoutes.js +22 -0
  76. package/dist/server/deploymentIdentityRoutes.js.map +1 -0
  77. package/dist/server/knownAutoInstallPiPackages.js +1 -1
  78. package/dist/server/knownAutoInstallPiPackages.js.map +1 -1
  79. package/dist/server/knownPiPackages.js +12 -0
  80. package/dist/server/knownPiPackages.js.map +1 -0
  81. package/dist/server/machines/machineClient.js +2 -2
  82. package/dist/server/machines/machineClient.js.map +1 -1
  83. package/dist/server/machines/machinePluginProxyRoutes.js +53 -11
  84. package/dist/server/machines/machinePluginProxyRoutes.js.map +1 -1
  85. package/dist/server/machines/machineProxyRoutes.js +70 -7
  86. package/dist/server/machines/machineProxyRoutes.js.map +1 -1
  87. package/dist/server/notices/serverNoticeRoutes.js +34 -0
  88. package/dist/server/notices/serverNoticeRoutes.js.map +1 -0
  89. package/dist/server/notices/serverNoticeService.js +26 -0
  90. package/dist/server/notices/serverNoticeService.js.map +1 -0
  91. package/dist/server/notices/serverNoticeStore.js +119 -0
  92. package/dist/server/notices/serverNoticeStore.js.map +1 -0
  93. package/dist/server/piPackageService.js +5 -5
  94. package/dist/server/piPackageService.js.map +1 -1
  95. package/dist/server/piWebPluginCatalog.js +46 -12
  96. package/dist/server/piWebPluginCatalog.js.map +1 -1
  97. package/dist/server/piWebPluginLifecycle.js +26 -3
  98. package/dist/server/piWebPluginLifecycle.js.map +1 -1
  99. package/dist/server/piWebPluginService.js +37 -2
  100. package/dist/server/piWebPluginService.js.map +1 -1
  101. package/dist/server/piWebStatus.js +1 -1
  102. package/dist/server/piWebStatus.js.map +1 -1
  103. package/dist/server/pluginCallbackDrain.js +35 -0
  104. package/dist/server/pluginCallbackDrain.js.map +1 -0
  105. package/dist/server/plugins/pluginBackendChannelProxyAdmission.js +84 -0
  106. package/dist/server/plugins/pluginBackendChannelProxyAdmission.js.map +1 -0
  107. package/dist/server/plugins/pluginBackendChannelProxyCoordinator.js +234 -0
  108. package/dist/server/plugins/pluginBackendChannelProxyCoordinator.js.map +1 -0
  109. package/dist/server/plugins/pluginBackendChannelProxyRoutes.js +52 -0
  110. package/dist/server/plugins/pluginBackendChannelProxyRoutes.js.map +1 -0
  111. package/dist/server/plugins/pluginBackendProxyRoutes.js +38 -31
  112. package/dist/server/plugins/pluginBackendProxyRoutes.js.map +1 -1
  113. package/dist/server/plugins/pluginBackendRegistry.js +740 -0
  114. package/dist/server/plugins/pluginBackendRegistry.js.map +1 -0
  115. package/dist/server/plugins/serverPluginPiSessionEventsCapability.js +34 -0
  116. package/dist/server/plugins/serverPluginPiSessionEventsCapability.js.map +1 -0
  117. package/dist/server/plugins/serverPluginPiSessionsCapability.js +185 -0
  118. package/dist/server/plugins/serverPluginPiSessionsCapability.js.map +1 -0
  119. package/dist/server/plugins/serverPluginRuntime.js +1028 -107
  120. package/dist/server/plugins/serverPluginRuntime.js.map +1 -1
  121. package/dist/server/plugins/serverPluginWorkspacesCapability.js +114 -0
  122. package/dist/server/plugins/serverPluginWorkspacesCapability.js.map +1 -0
  123. package/dist/server/sessiond/pluginBackendChannelRoutes.js +393 -0
  124. package/dist/server/sessiond/pluginBackendChannelRoutes.js.map +1 -0
  125. package/dist/server/sessiond/pluginBackendRoutes.js +10 -7
  126. package/dist/server/sessiond/pluginBackendRoutes.js.map +1 -1
  127. package/dist/server/sessiond/sessionDaemonShutdown.js +9 -2
  128. package/dist/server/sessiond/sessionDaemonShutdown.js.map +1 -1
  129. package/dist/server/sessiond/sessionProxyRoutes.js +2 -0
  130. package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
  131. package/dist/server/sessiond/sessionServiceDependencies.js +1 -0
  132. package/dist/server/sessiond/sessionServiceDependencies.js.map +1 -1
  133. package/dist/server/sessiond.js +82 -21
  134. package/dist/server/sessiond.js.map +1 -1
  135. package/dist/server/sessions/attachmentService.js +1 -5
  136. package/dist/server/sessions/attachmentService.js.map +1 -1
  137. package/dist/server/sessions/builtinCommands.js +2 -2
  138. package/dist/server/sessions/builtinCommands.js.map +1 -1
  139. package/dist/server/sessions/clientSessionPreview.js +14 -0
  140. package/dist/server/sessions/clientSessionPreview.js.map +1 -0
  141. package/dist/server/sessions/piSessionEventConnections.js +65 -0
  142. package/dist/server/sessions/piSessionEventConnections.js.map +1 -0
  143. package/dist/server/sessions/piSessionManagerGateway.js +171 -1
  144. package/dist/server/sessions/piSessionManagerGateway.js.map +1 -1
  145. package/dist/server/sessions/piSessionService.js +442 -90
  146. package/dist/server/sessions/piSessionService.js.map +1 -1
  147. package/dist/server/sessions/sessionActivityMarker.js +105 -0
  148. package/dist/server/sessions/sessionActivityMarker.js.map +1 -0
  149. package/dist/server/sessions/sessionEnvironmentFacts.js +5 -4
  150. package/dist/server/sessions/sessionEnvironmentFacts.js.map +1 -1
  151. package/dist/server/sessions/sessionNameGenerator.js +3 -2
  152. package/dist/server/sessions/sessionNameGenerator.js.map +1 -1
  153. package/dist/server/sessions/sessionRoutes.js +18 -0
  154. package/dist/server/sessions/sessionRoutes.js.map +1 -1
  155. package/dist/server/sessions/spawnSessionTool.js +1 -1
  156. package/dist/server/sessions/spawnSessionTool.js.map +1 -1
  157. package/dist/server/sessions/spawnSubsessionTool.js +1 -1
  158. package/dist/server/sessions/spawnSubsessionTool.js.map +1 -1
  159. package/dist/server/sessions/transcriptBranchCache.js +81 -0
  160. package/dist/server/sessions/transcriptBranchCache.js.map +1 -0
  161. package/dist/server/sessions/transcriptMessages.js +54 -0
  162. package/dist/server/sessions/transcriptMessages.js.map +1 -0
  163. package/dist/server/terminals/requiredTerminalService.js +103 -0
  164. package/dist/server/terminals/requiredTerminalService.js.map +1 -0
  165. package/dist/server/webSocketBridge.js +339 -0
  166. package/dist/server/webSocketBridge.js.map +1 -1
  167. package/dist/server/workspaces/projectPiWebConfig.js +6 -1
  168. package/dist/server/workspaces/projectPiWebConfig.js.map +1 -1
  169. package/dist/server/workspaces/sessionDaemonWorkspaceCatalog.js +23 -3
  170. package/dist/server/workspaces/sessionDaemonWorkspaceCatalog.js.map +1 -1
  171. package/dist/server/workspaces/workspaceCatalog.js +3 -1
  172. package/dist/server/workspaces/workspaceCatalog.js.map +1 -1
  173. package/dist/server/workspaces/workspaceProviderRegistry.js +64 -149
  174. package/dist/server/workspaces/workspaceProviderRegistry.js.map +1 -1
  175. package/dist/server/workspaces/workspaceRemovalService.js +38 -5
  176. package/dist/server/workspaces/workspaceRemovalService.js.map +1 -1
  177. package/dist/server-plugin-api.d.ts +216 -22
  178. package/dist/server-plugin-api.js +319 -2
  179. package/dist/server-plugin-api.js.map +1 -1
  180. package/dist/serverPluginRecovery.js +4 -0
  181. package/dist/serverPluginRecovery.js.map +1 -1
  182. package/dist/sessiond/sessionDaemonClient.js +3 -3
  183. package/dist/sessiond/sessionDaemonClient.js.map +1 -1
  184. package/dist/shared/apiTypes.js +1 -1
  185. package/dist/shared/apiTypes.js.map +1 -1
  186. package/dist/shared/federatedRoutes.js +47 -17
  187. package/dist/shared/federatedRoutes.js.map +1 -1
  188. package/dist/shared/machinePluginIds.js +28 -7
  189. package/dist/shared/machinePluginIds.js.map +1 -1
  190. package/dist/shared/pluginApiTypes.d.ts +21 -1
  191. package/dist/shared/pluginApiTypes.js +0 -1
  192. package/dist/shared/pluginBackendProtocol.js +163 -2
  193. package/dist/shared/pluginBackendProtocol.js.map +1 -1
  194. package/dist/shared/pluginIds.js +8 -1
  195. package/dist/shared/pluginIds.js.map +1 -1
  196. package/dist/shared/requiredTerminalPlugin.js +6 -0
  197. package/dist/shared/requiredTerminalPlugin.js.map +1 -0
  198. package/dist/shared/serverNoticeContract.js +88 -0
  199. package/dist/shared/serverNoticeContract.js.map +1 -0
  200. package/dist/shared/sessionDefaults.js +41 -0
  201. package/dist/shared/sessionDefaults.js.map +1 -0
  202. package/docs/config.md +77 -37
  203. package/docs/plugins.md +146 -1213
  204. package/examples/session-bridge-plugin/README.md +20 -0
  205. package/examples/session-bridge-plugin/docs/usage.md +54 -0
  206. package/examples/session-bridge-plugin/package.json +26 -0
  207. package/examples/session-bridge-plugin/src/browser/index.ts +84 -0
  208. package/examples/session-bridge-plugin/src/browser/protocol.ts +24 -0
  209. package/examples/session-bridge-plugin/src/companion.ts +65 -0
  210. package/examples/session-bridge-plugin/src/reviewRun.ts +39 -0
  211. package/examples/session-bridge-plugin/src/server.ts +85 -0
  212. package/examples/session-bridge-plugin/src/store.ts +57 -0
  213. package/examples/session-bridge-plugin/tsconfig.json +21 -0
  214. package/examples/workspace-provider-plugin/README.md +4 -4
  215. package/examples/workspace-provider-plugin/package.json +1 -1
  216. package/examples/workspace-provider-plugin/src/browser/index.ts +9 -9
  217. package/examples/workspace-provider-plugin/src/server.ts +15 -11
  218. package/package.json +17 -11
  219. package/dist/client/assets/CodeViewer-BMWwxG7q.js +0 -4
  220. package/dist/client/assets/TerminalPanel-CacQDIYn.js +0 -187
  221. package/dist/client/assets/index-DUW2xnoV.js +0 -4279
  222. package/dist/client/assets/vendor-editor-core-CXO8gGab.js +0 -12
  223. package/dist/client/assets/vendor-editor-languages-CpW4sJsX.js +0 -46
  224. package/dist/client/assets/vendor-editor-legacy-CYBnW6ZU.js +0 -1
  225. package/dist/client/assets/vendor-terminal-BrP-ENHg.css +0 -1
  226. package/dist/client/assets/vendor-terminal-D8k4UKM2.js +0 -35
  227. package/dist/server/terminalProxyRoutes.js +0 -141
  228. package/dist/server/terminalProxyRoutes.js.map +0 -1
  229. package/dist/server/terminals/terminalRoutes.js +0 -155
  230. package/dist/server/terminals/terminalRoutes.js.map +0 -1
  231. package/dist/server/terminals/terminalService.js.map +0 -1
  232. package/dist/server/terminals/terminalSize.js +0 -17
  233. package/dist/server/terminals/terminalSize.js.map +0 -1
@@ -1,32 +1,56 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ import { isPiWebPluginId } from "../../shared/pluginIds.js";
1
4
  import { pathToFileURL } from "node:url";
5
+ import { parseServerNoticeScope, SERVER_PLUGIN_NOTICE_CONTEXT_MAX_BYTES, SERVER_PLUGIN_NOTICE_CONTEXT_MAX_DEPTH, SERVER_PLUGIN_NOTICE_MESSAGE_MAX_BYTES, SERVER_PLUGIN_NOTICE_SOURCE_PREFIX, serverNoticeStringExceedsUtf8ByteLimit, } from "../../shared/serverNoticeContract.js";
6
+ import { REQUIRED_TERMINAL_PLUGIN_ID, REQUIRED_TERMINAL_RECOVERY_GUIDANCE, } from "../../shared/requiredTerminalPlugin.js";
7
+ import { REQUIRED_TERMINAL_SERVICE_CAPABILITY } from "../terminals/requiredTerminalService.js";
2
8
  import { createServerPluginExecFile } from "./serverPluginExec.js";
3
9
  const DEFAULT_LIFECYCLE_TIMEOUT_MS = 10_000;
4
10
  /**
5
11
  * Resolves exactly one desired catalog snapshot and activates its server
6
12
  * entries. The resulting runtime is immutable except for explicit shutdown;
7
- * v1 intentionally has no hot reload or unload path.
13
+ * this lifecycle intentionally has no hot reload or unload path.
8
14
  */
9
15
  export async function createServerPluginRuntime(options) {
10
16
  if (options.safeStart === "none") {
11
17
  return await ServerPluginRuntime.activate({ plugins: [], diagnostics: [] }, options);
12
18
  }
13
19
  const snapshot = await options.catalog.snapshot(options.safeStart === "bundled-only" ? { scope: "bundled" } : undefined);
20
+ if (options.enforceRequiredTerminal !== false)
21
+ requireTerminalCatalogEntry(snapshot);
14
22
  return await ServerPluginRuntime.activate(snapshot, options);
15
23
  }
16
24
  export class ServerPluginRuntime {
17
- constructor(safeStart, diagnostics, logger, importer, execFile, lifecycleTimeoutMs) {
25
+ constructor(dataDir, safeStart, diagnostics, logger, importer, execFile, lifecycleTimeoutMs, noticeSink, hostCapabilities, hostCapabilityFactories, lateHostCapabilities, enforceRequiredTerminal) {
26
+ this.dataDir = dataDir;
18
27
  this.safeStart = safeStart;
19
28
  this.diagnostics = diagnostics;
20
29
  this.logger = logger;
21
30
  this.importer = importer;
22
31
  this.execFile = execFile;
23
32
  this.lifecycleTimeoutMs = lifecycleTimeoutMs;
33
+ this.noticeSink = noticeSink;
34
+ this.enforceRequiredTerminal = enforceRequiredTerminal;
24
35
  this.recordsById = new Map();
36
+ this.declaredCapabilitiesByKey = new Map();
37
+ this.activeCapabilitiesByKey = new Map();
25
38
  this.activePlugins = [];
39
+ this.stagedPlugins = [];
40
+ this.lifetimesAborted = false;
41
+ this.lateHostCapabilitiesResumed = false;
26
42
  this.stopped = false;
43
+ for (const provision of hostCapabilities) {
44
+ this.registerHostCapability(provision);
45
+ this.activeCapabilitiesByKey.set(provision.key, provision);
46
+ }
47
+ for (const factory of hostCapabilityFactories)
48
+ this.registerHostCapability(factory);
49
+ for (const capability of lateHostCapabilities)
50
+ this.registerHostCapability(capability);
27
51
  }
28
52
  static async activate(snapshot, options) {
29
- const runtime = new ServerPluginRuntime(options.safeStart, Object.freeze(snapshot.diagnostics.map((diagnostic) => Object.freeze({ ...diagnostic }))), options.logger, options.importer ?? importServerPluginModule, options.execFile ?? createServerPluginExecFile(), positiveInteger(options.lifecycleTimeoutMs, DEFAULT_LIFECYCLE_TIMEOUT_MS, "lifecycleTimeoutMs"));
53
+ const runtime = new ServerPluginRuntime(resolve(options.dataDir), options.safeStart, Object.freeze(snapshot.diagnostics.map((diagnostic) => Object.freeze({ ...diagnostic }))), options.logger, options.importer ?? importServerPluginModule, options.execFile ?? createServerPluginExecFile(), positiveInteger(options.lifecycleTimeoutMs, DEFAULT_LIFECYCLE_TIMEOUT_MS, "lifecycleTimeoutMs"), options.noticeSink, snapshotHostCapabilities(options.hostCapabilities), snapshotHostCapabilityFactories(options.hostCapabilityFactories), snapshotLateHostCapabilities(options.lateHostCapabilities), options.enforceRequiredTerminal !== false && options.safeStart !== "none");
30
54
  try {
31
55
  await runtime.start(snapshot.plugins);
32
56
  return runtime;
@@ -36,6 +60,12 @@ export class ServerPluginRuntime {
36
60
  throw error;
37
61
  }
38
62
  }
63
+ registerHostCapability(declaration) {
64
+ if (this.declaredCapabilitiesByKey.has(declaration.key)) {
65
+ throw new Error(`Host capability ${formatCapability(declaration.capability)} is configured more than once`);
66
+ }
67
+ this.declaredCapabilitiesByKey.set(declaration.key, declaration);
68
+ }
39
69
  safeStartLevel() {
40
70
  return this.safeStart;
41
71
  }
@@ -48,11 +78,67 @@ export class ServerPluginRuntime {
48
78
  .map((record) => Object.freeze({ ...record })));
49
79
  }
50
80
  providerContributions() {
51
- return Object.freeze(this.activePlugins.flatMap((active) => active.contribution === undefined ? [] : [active.contribution]));
81
+ return Object.freeze(this.activePlugins.flatMap((active) => active.providerContribution === undefined ? [] : [active.providerContribution]));
82
+ }
83
+ pairedBackendContributions() {
84
+ return Object.freeze(this.activePlugins.flatMap((active) => active.pairedBackendContribution === undefined ? [] : [active.pairedBackendContribution]));
52
85
  }
53
- async inspectHealth() {
86
+ /** Resolves one exact active capability for a core host consumer. */
87
+ resolve(capability) {
88
+ const parsed = snapshotTypedCapability(capability, "Host capability request");
89
+ const provision = this.activeCapabilitiesByKey.get(capabilityKey(parsed));
90
+ if (provision === undefined) {
91
+ throw new Error(`Server capability ${formatCapability(parsed)} is not active`);
92
+ }
93
+ return parseCapabilityValue(parsed, provision.value, `Server capability ${formatCapability(parsed)}`);
94
+ }
95
+ /**
96
+ * Registers every declared late host factory atomically, then resumes the
97
+ * plugins staged on those capabilities. This boundary is intentionally one-shot.
98
+ */
99
+ async resumeWithHostCapabilityFactories(factories) {
100
+ if (this.stopped || this.lifetimesAborted) {
101
+ throw new Error("Server plugin runtime cannot resume after shutdown begins");
102
+ }
103
+ if (this.lateHostCapabilitiesResumed) {
104
+ throw new Error("Server plugin late host capabilities have already resumed");
105
+ }
106
+ const snapshottedFactories = snapshotHostCapabilityFactories(factories);
107
+ const lateDeclarations = [...this.declaredCapabilitiesByKey.values()]
108
+ .filter((declaration) => declaration.source === "late-host");
109
+ const factoriesByKey = new Map(snapshottedFactories.map((factory) => [factory.key, factory]));
110
+ for (const factory of snapshottedFactories) {
111
+ if (!lateDeclarations.some(({ key }) => key === factory.key)) {
112
+ throw new Error(`Host capability ${formatCapability(factory.capability)} was not declared for late registration`);
113
+ }
114
+ }
115
+ for (const declaration of lateDeclarations) {
116
+ if (!factoriesByKey.has(declaration.key)) {
117
+ throw new Error(`Late host capability ${formatCapability(declaration.capability)} was not registered`);
118
+ }
119
+ }
120
+ this.lateHostCapabilitiesResumed = true;
121
+ for (const factory of snapshottedFactories)
122
+ this.declaredCapabilitiesByKey.set(factory.key, factory);
123
+ await this.startStagedPlugins(false);
124
+ }
125
+ /** Cancels plugin lifetimes while retaining publications for consumer cleanup. */
126
+ beginShutdown() {
127
+ if (this.lifetimesAborted)
128
+ return;
129
+ this.lifetimesAborted = true;
130
+ const reason = new DOMException("Server plugin host is shutting down", "AbortError");
131
+ for (const plugin of [...this.activePlugins, ...this.stagedPlugins]) {
132
+ if (!plugin.lifetimeController.signal.aborted)
133
+ plugin.lifetimeController.abort(reason);
134
+ }
135
+ }
136
+ async inspectHealth(pluginIds) {
137
+ const selectedPluginIds = pluginIds === undefined ? undefined : new Set(pluginIds);
54
138
  const inspections = [];
55
139
  for (const active of this.activePlugins) {
140
+ if (selectedPluginIds !== undefined && !selectedPluginIds.has(active.entry.id))
141
+ continue;
56
142
  const callback = active.activation.health?.bind(active.activation);
57
143
  if (callback === undefined) {
58
144
  inspections.push(Object.freeze({ pluginId: active.entry.id, health: Object.freeze({ status: "healthy" }) }));
@@ -75,39 +161,43 @@ export class ServerPluginRuntime {
75
161
  }
76
162
  return Object.freeze(inspections);
77
163
  }
78
- /** Stops every successfully published plugin in reverse activation order. */
164
+ /** Disposes published plugins in reverse dependency/start order. */
79
165
  async stop() {
80
166
  if (this.stopped)
81
167
  return;
168
+ this.beginShutdown();
82
169
  this.stopped = true;
170
+ // During failed construction, activated entries may not have reached start.
171
+ // Dispose those first while any started providers remain published.
172
+ const stagedPlugins = [...this.stagedPlugins].reverse();
173
+ this.stagedPlugins = [];
174
+ for (const staged of stagedPlugins) {
175
+ await this.disposeForShutdown(staged);
176
+ }
83
177
  const activePlugins = [...this.activePlugins].reverse();
84
178
  this.activePlugins = [];
179
+ this.activeCapabilitiesByKey.clear();
85
180
  for (const active of activePlugins) {
86
- const stop = active.activation.stop?.bind(active.activation);
87
- if (stop === undefined)
88
- continue;
89
- try {
90
- await runBounded(active.entry.id, "stop", this.lifecycleTimeoutMs, (signal) => stop(signal));
91
- }
92
- catch (error) {
93
- this.recordsById.set(active.entry.id, recordFor(active.entry, {
94
- state: "failed",
95
- name: active.plugin.name,
96
- phase: "stop",
97
- message: errorMessage(error),
98
- }));
99
- this.logger.error({ err: error, pluginId: active.entry.id, phase: "stop" }, "server plugin stop failed");
100
- }
181
+ await this.disposeForShutdown(active);
101
182
  }
102
183
  }
103
184
  async start(entries) {
104
185
  const serverEntries = entries
105
186
  .filter((entry) => entry.serverModule !== undefined)
106
- .sort((left, right) => left.id.localeCompare(right.id));
187
+ .sort(requiredTerminalFirst);
107
188
  for (const entry of serverEntries)
108
- await this.activateEntry(entry);
189
+ await this.stageEntry(entry);
190
+ await this.startStagedPlugins(true);
191
+ if (this.enforceRequiredTerminal) {
192
+ try {
193
+ this.resolve(REQUIRED_TERMINAL_SERVICE_CAPABILITY);
194
+ }
195
+ catch (error) {
196
+ throw requiredTerminalError("Required Terminal service capability is not active", error);
197
+ }
198
+ }
109
199
  }
110
- async activateEntry(entry) {
200
+ async stageEntry(entry) {
111
201
  const disabledMessage = disabledReason(entry, this.safeStart);
112
202
  if (disabledMessage !== undefined) {
113
203
  this.recordsById.set(entry.id, recordFor(entry, { state: "disabled", message: disabledMessage }));
@@ -116,7 +206,9 @@ export class ServerPluginRuntime {
116
206
  }
117
207
  let phase = "validate";
118
208
  let plugin;
119
- let activation;
209
+ let rollbackDispose;
210
+ let noticeReporter;
211
+ const lifetimeController = new AbortController();
120
212
  try {
121
213
  const settings = cloneJsonObject(entry.settings, `settings for server plugin ${entry.id}`);
122
214
  phase = "import";
@@ -126,51 +218,56 @@ export class ServerPluginRuntime {
126
218
  const loadedPlugin = parsePluginExport(imported);
127
219
  plugin = loadedPlugin;
128
220
  phase = "activate";
221
+ if (!isPiWebPluginId(entry.id))
222
+ throw new Error(`Invalid PI WEB plugin id: ${entry.id}`);
223
+ const dataDirectory = resolve(this.dataDir, "plugin-data", entry.id);
224
+ await mkdir(dataDirectory, { recursive: true });
129
225
  const scopedLogger = createScopedLogger(entry.id, this.logger);
226
+ noticeReporter = createScopedNoticeReporter(entry.id, this.noticeSink);
130
227
  const activationValue = await runBounded(entry.id, phase, this.lifecycleTimeoutMs, (signal) => loadedPlugin.activate(Object.freeze({
131
- apiVersion: 1,
228
+ apiVersion: 3,
132
229
  pluginId: entry.id,
133
230
  packageRoot: entry.packageRoot,
231
+ dataDirectory,
134
232
  logger: scopedLogger,
135
233
  settings,
234
+ ...(noticeReporter === undefined ? {} : { notices: noticeReporter.reporter }),
136
235
  execFile: this.execFile,
137
236
  signal,
237
+ lifetimeSignal: lifetimeController.signal,
138
238
  })));
239
+ rollbackDispose = activationDisposeForRollback(activationValue);
139
240
  phase = "validate";
140
- const loadedActivation = parseActivation(activationValue);
141
- activation = loadedActivation;
142
- phase = "start";
143
- const start = loadedActivation.start?.bind(loadedActivation);
144
- if (start !== undefined) {
145
- await runBounded(entry.id, phase, this.lifecycleTimeoutMs, (signal) => start(signal));
241
+ const loadedActivation = parseActivation(activationValue, entry.id);
242
+ if (this.enforceRequiredTerminal && entry.id === REQUIRED_TERMINAL_PLUGIN_ID) {
243
+ requireTerminalActivation(loadedActivation);
146
244
  }
147
- const contribution = loadedActivation.workspaceProvider === undefined
148
- ? undefined
149
- : Object.freeze({
150
- pluginId: entry.id,
151
- pluginName: loadedPlugin.name,
152
- packageRoot: entry.packageRoot,
153
- source: entry.source,
154
- scope: entry.scope,
155
- moduleRevision: requireServerModule(entry).revision,
156
- provider: loadedActivation.workspaceProvider,
157
- });
158
- this.activePlugins.push(Object.freeze({
245
+ const provisions = internalProvisions(loadedActivation, "plugin");
246
+ for (const provision of provisions) {
247
+ if (this.declaredCapabilitiesByKey.has(provision.key)) {
248
+ throw new IncompatibleServerPluginError(`Server capability ${formatCapability(provision.capability)} is provided more than once`);
249
+ }
250
+ }
251
+ const staged = Object.freeze({
159
252
  entry,
160
253
  plugin: loadedPlugin,
161
254
  activation: loadedActivation,
162
- ...(contribution === undefined ? {} : { contribution }),
163
- }));
164
- this.recordsById.set(entry.id, recordFor(entry, { state: "active", name: loadedPlugin.name }));
165
- this.logger.info({ pluginId: entry.id, pluginName: loadedPlugin.name }, "server plugin activated");
255
+ lifetimeController,
256
+ ...(noticeReporter === undefined ? {} : { noticeReporter }),
257
+ provisions,
258
+ hostCapabilityInstances: [],
259
+ });
260
+ this.stagedPlugins.push(staged);
261
+ for (const provision of provisions)
262
+ this.declaredCapabilitiesByKey.set(provision.key, provision);
166
263
  }
167
264
  catch (error) {
168
- const rollbackError = phase === "start" && activation?.stop !== undefined
169
- ? await this.rollbackStart(entry.id, activation)
170
- : undefined;
171
- const message = rollbackError === undefined
172
- ? errorMessage(error)
173
- : `${errorMessage(error)}; startup rollback failed: ${errorMessage(rollbackError)}`;
265
+ abortPluginLifetime(lifetimeController, entry.id, "activation failed");
266
+ noticeReporter?.revoke();
267
+ const rollbackError = rollbackDispose === undefined
268
+ ? undefined
269
+ : await this.runDispose(entry.id, rollbackDispose);
270
+ const message = withRollbackError(errorMessage(error), rollbackError);
174
271
  const state = error instanceof IncompatibleServerPluginError ? "incompatible" : "failed";
175
272
  this.recordsById.set(entry.id, recordFor(entry, {
176
273
  state,
@@ -179,20 +276,290 @@ export class ServerPluginRuntime {
179
276
  message,
180
277
  }));
181
278
  const details = { err: error, pluginId: entry.id, phase, ...(rollbackError === undefined ? {} : { rollbackError }) };
182
- if (state === "incompatible") {
279
+ if (state === "incompatible")
183
280
  this.logger.warn(details, "server plugin is incompatible");
281
+ else
282
+ this.logger.error(details, "server plugin activation failed");
283
+ if (this.enforceRequiredTerminal && entry.id === REQUIRED_TERMINAL_PLUGIN_ID) {
284
+ throw requiredTerminalError(`Required Terminal server entry failed during ${phase}: ${message}`, error);
285
+ }
286
+ }
287
+ }
288
+ async startStagedPlugins(allowLateHostWait) {
289
+ const pending = new Map(this.stagedPlugins.map((plugin) => [plugin.entry.id, plugin]));
290
+ while (pending.size > 0) {
291
+ let progressed = false;
292
+ const candidates = [...pending.values()].sort((left, right) => requiredTerminalFirst(left.entry, right.entry));
293
+ for (const staged of candidates) {
294
+ const dependencyFailure = this.dependencyFailure(staged, pending);
295
+ if (dependencyFailure !== undefined) {
296
+ pending.delete(staged.entry.id);
297
+ const message = await this.failBeforeStart(staged, dependencyFailure);
298
+ progressed = true;
299
+ if (this.enforceRequiredTerminal && staged.entry.id === REQUIRED_TERMINAL_PLUGIN_ID) {
300
+ throw requiredTerminalError(`Required Terminal server entry failed during start: ${message}`, dependencyFailure);
301
+ }
302
+ continue;
303
+ }
304
+ if (this.waitingForDependencies(staged))
305
+ continue;
306
+ pending.delete(staged.entry.id);
307
+ const startError = await this.startStagedPlugin(staged);
308
+ progressed = true;
309
+ if (startError !== undefined && this.enforceRequiredTerminal && staged.entry.id === REQUIRED_TERMINAL_PLUGIN_ID) {
310
+ const record = this.recordsById.get(staged.entry.id);
311
+ throw requiredTerminalError(`Required Terminal server entry failed during start: ${record?.message ?? errorMessage(startError)}`, startError);
312
+ }
313
+ }
314
+ if (progressed)
315
+ continue;
316
+ const cycleIds = findCapabilityCycle(pending, this.declaredCapabilitiesByKey);
317
+ if (cycleIds.length > 0) {
318
+ for (const pluginId of cycleIds) {
319
+ const staged = pending.get(pluginId);
320
+ if (staged === undefined)
321
+ continue;
322
+ pending.delete(pluginId);
323
+ const cyclicRequirement = (staged.plugin.requires ?? []).find((requirement) => cycleIds.includes(requirement.pluginId));
324
+ const error = new Error(cyclicRequirement === undefined
325
+ ? `Server plugin ${pluginId} has an unresolved capability dependency cycle`
326
+ : `Server plugin ${pluginId} requires ${formatCapability(cyclicRequirement)} in a capability dependency cycle`);
327
+ const message = await this.failBeforeStart(staged, error);
328
+ if (this.enforceRequiredTerminal && pluginId === REQUIRED_TERMINAL_PLUGIN_ID) {
329
+ throw requiredTerminalError(`Required Terminal server entry failed during start: ${message}`, error);
330
+ }
331
+ }
332
+ continue;
333
+ }
334
+ if (allowLateHostWait) {
335
+ const providerCycles = candidates.flatMap((staged) => {
336
+ if (staged.activation.workspaceProvider === undefined)
337
+ return [];
338
+ const lateCapability = findLateHostDependency(staged, pending, this.declaredCapabilitiesByKey);
339
+ return lateCapability === undefined ? [] : [{ staged, lateCapability }];
340
+ });
341
+ if (providerCycles.length > 0) {
342
+ for (const { staged, lateCapability } of providerCycles) {
343
+ pending.delete(staged.entry.id);
344
+ const error = new Error(`Server plugin ${staged.entry.id} contributes a workspace provider that depends on late host capability ${formatCapability(lateCapability)} in a capability dependency cycle`);
345
+ const message = await this.failBeforeStart(staged, error);
346
+ if (this.enforceRequiredTerminal && staged.entry.id === REQUIRED_TERMINAL_PLUGIN_ID) {
347
+ throw requiredTerminalError(`Required Terminal server entry failed during start: ${message}`, error);
348
+ }
349
+ }
350
+ continue;
351
+ }
352
+ if (candidates.every((staged) => (findLateHostDependency(staged, pending, this.declaredCapabilitiesByKey) !== undefined)))
353
+ return;
354
+ }
355
+ const unresolved = candidates[0];
356
+ if (unresolved === undefined)
357
+ return;
358
+ pending.delete(unresolved.entry.id);
359
+ const error = new Error(`Server plugin ${unresolved.entry.id} has an unresolved capability dependency cycle`);
360
+ const message = await this.failBeforeStart(unresolved, error);
361
+ if (this.enforceRequiredTerminal && unresolved.entry.id === REQUIRED_TERMINAL_PLUGIN_ID) {
362
+ throw requiredTerminalError(`Required Terminal server entry failed during start: ${message}`, error);
363
+ }
364
+ }
365
+ }
366
+ dependencyFailure(staged, pending) {
367
+ for (const requirement of staged.plugin.requires ?? []) {
368
+ const key = capabilityKey(requirement);
369
+ const provision = this.declaredCapabilitiesByKey.get(key);
370
+ if (provision === undefined) {
371
+ return new Error(`Server plugin ${staged.entry.id} requires unavailable capability ${formatCapability(requirement)}`);
372
+ }
373
+ if (this.activeCapabilitiesByKey.has(key)
374
+ || provision.source === "host"
375
+ || provision.source === "host-factory"
376
+ || provision.source === "late-host")
377
+ continue;
378
+ if (!pending.has(requirement.pluginId)) {
379
+ return new Error(`Server plugin ${staged.entry.id} requires ${formatCapability(requirement)}, but provider plugin ${requirement.pluginId} did not start`);
380
+ }
381
+ }
382
+ return undefined;
383
+ }
384
+ waitingForDependencies(staged) {
385
+ return (staged.plugin.requires ?? []).some((requirement) => {
386
+ const key = capabilityKey(requirement);
387
+ return this.declaredCapabilitiesByKey.get(key)?.source !== "host-factory"
388
+ && !this.activeCapabilitiesByKey.has(key);
389
+ });
390
+ }
391
+ async startStagedPlugin(staged) {
392
+ try {
393
+ const capabilities = this.capabilityResolver(staged);
394
+ const start = staged.activation.start?.bind(staged.activation);
395
+ if (start !== undefined) {
396
+ await runBounded(staged.entry.id, "start", this.lifecycleTimeoutMs, (signal) => start(Object.freeze({
397
+ capabilities,
398
+ signal,
399
+ })));
400
+ }
401
+ if (this.enforceRequiredTerminal && staged.entry.id === REQUIRED_TERMINAL_PLUGIN_ID) {
402
+ await this.requireHealthyTerminal(staged);
403
+ }
404
+ this.publish(staged);
405
+ return undefined;
406
+ }
407
+ catch (error) {
408
+ await this.failBeforeStart(staged, error);
409
+ return error;
410
+ }
411
+ }
412
+ capabilityResolver(staged) {
413
+ const resolved = new Map();
414
+ for (const requirement of staged.plugin.requires ?? []) {
415
+ const key = capabilityKey(requirement);
416
+ const activeProvision = this.activeCapabilitiesByKey.get(key);
417
+ const declaration = this.declaredCapabilitiesByKey.get(key);
418
+ let provisionValue;
419
+ if (activeProvision !== undefined) {
420
+ provisionValue = activeProvision.value;
421
+ }
422
+ else if (declaration?.source === "host-factory") {
423
+ provisionValue = this.createHostCapabilityValue(staged, declaration);
184
424
  }
185
425
  else {
186
- this.logger.error(details, "server plugin activation failed");
426
+ throw new Error(`Server plugin ${staged.entry.id} requires inactive capability ${formatCapability(requirement)}`);
187
427
  }
428
+ // Validate the declared requirement before start, but retain the provider value:
429
+ // resolve() may supply a different parser for this same capability key.
430
+ parseCapabilityValue(requirement, provisionValue, `Required capability ${formatCapability(requirement)} for server plugin ${staged.entry.id}`);
431
+ resolved.set(key, { provisionValue });
188
432
  }
433
+ return createCapabilityResolver(staged.entry.id, resolved);
434
+ }
435
+ createHostCapabilityValue(staged, factory) {
436
+ const context = Object.freeze({
437
+ pluginId: staged.entry.id,
438
+ packageRoot: staged.entry.packageRoot,
439
+ lifetimeSignal: staged.lifetimeController.signal,
440
+ });
441
+ const instance = factory.create(context);
442
+ staged.hostCapabilityInstances.push(instance);
443
+ return parseCapabilityValue(factory.capability, instance.value, `Host capability ${formatCapability(factory.capability)} for server plugin ${staged.entry.id}`);
444
+ }
445
+ async requireHealthyTerminal(staged) {
446
+ const health = staged.activation.health?.bind(staged.activation);
447
+ if (health === undefined)
448
+ return;
449
+ const result = await runBounded(staged.entry.id, "health", this.lifecycleTimeoutMs, (signal) => health(signal));
450
+ const parsed = parseHealth(result);
451
+ if (parsed.status === "unhealthy") {
452
+ throw new Error(`Required Terminal server entry is unhealthy: ${parsed.message ?? "health check failed"}`);
453
+ }
454
+ }
455
+ publish(staged) {
456
+ const providerContribution = staged.activation.workspaceProvider === undefined
457
+ ? undefined
458
+ : Object.freeze({
459
+ pluginId: staged.entry.id,
460
+ pluginName: staged.plugin.name,
461
+ packageRoot: staged.entry.packageRoot,
462
+ source: staged.entry.source,
463
+ scope: staged.entry.scope,
464
+ moduleRevision: requireServerModule(staged.entry).revision,
465
+ provider: staged.activation.workspaceProvider,
466
+ });
467
+ const pairedBackendContribution = staged.activation.peer === undefined
468
+ ? undefined
469
+ : Object.freeze({
470
+ pluginId: staged.entry.id,
471
+ pluginName: staged.plugin.name,
472
+ packageRoot: staged.entry.packageRoot,
473
+ source: staged.entry.source,
474
+ scope: staged.entry.scope,
475
+ moduleRevision: requireServerModule(staged.entry).revision,
476
+ backend: staged.activation.peer,
477
+ });
478
+ const active = Object.freeze({
479
+ ...staged,
480
+ ...(providerContribution === undefined ? {} : { providerContribution }),
481
+ ...(pairedBackendContribution === undefined ? {} : { pairedBackendContribution }),
482
+ });
483
+ this.removeStaged(staged.entry.id);
484
+ this.activePlugins.push(active);
485
+ for (const provision of staged.provisions)
486
+ this.activeCapabilitiesByKey.set(provision.key, provision);
487
+ this.recordsById.set(staged.entry.id, recordFor(staged.entry, {
488
+ state: "active",
489
+ name: staged.plugin.name,
490
+ ...(pairedBackendContribution?.backend.request === undefined ? {} : { pairedRequestVersion: 1 }),
491
+ ...(pairedBackendContribution?.backend.openChannel === undefined ? {} : { pairedChannelVersion: 1 }),
492
+ }));
493
+ this.logger.info({ pluginId: staged.entry.id, pluginName: staged.plugin.name }, "server plugin activated");
494
+ }
495
+ async failBeforeStart(staged, error) {
496
+ abortPluginLifetime(staged.lifetimeController, staged.entry.id, "startup failed");
497
+ staged.noticeReporter?.revoke();
498
+ this.removeStaged(staged.entry.id);
499
+ const dispose = staged.activation.dispose?.bind(staged.activation);
500
+ const rollbackError = dispose === undefined ? undefined : await this.runDispose(staged.entry.id, dispose);
501
+ const hostCleanupError = await this.disposeHostCapabilityInstances(staged);
502
+ const message = withHostCleanupError(withRollbackError(errorMessage(error), rollbackError), hostCleanupError);
503
+ this.recordsById.set(staged.entry.id, recordFor(staged.entry, {
504
+ state: "failed",
505
+ name: staged.plugin.name,
506
+ phase: "start",
507
+ message,
508
+ }));
509
+ this.logger.error({
510
+ err: error,
511
+ pluginId: staged.entry.id,
512
+ phase: "start",
513
+ ...(rollbackError === undefined ? {} : { rollbackError }),
514
+ ...(hostCleanupError === undefined ? {} : { hostCleanupError }),
515
+ }, "server plugin start failed");
516
+ return message;
189
517
  }
190
- async rollbackStart(pluginId, activation) {
191
- const stop = activation.stop?.bind(activation);
192
- if (stop === undefined)
518
+ removeStaged(pluginId) {
519
+ this.stagedPlugins = this.stagedPlugins.filter(({ entry }) => entry.id !== pluginId);
520
+ }
521
+ async disposeForShutdown(plugin) {
522
+ plugin.noticeReporter?.revoke();
523
+ const dispose = plugin.activation.dispose?.bind(plugin.activation);
524
+ const pluginDisposeError = dispose === undefined ? undefined : await this.runDispose(plugin.entry.id, dispose);
525
+ const hostCleanupError = await this.disposeHostCapabilityInstances(plugin);
526
+ const error = combinedDisposalError(pluginDisposeError, hostCleanupError);
527
+ if (error === undefined)
528
+ return;
529
+ this.recordsById.set(plugin.entry.id, recordFor(plugin.entry, {
530
+ state: "failed",
531
+ name: plugin.plugin.name,
532
+ phase: "dispose",
533
+ message: errorMessage(error),
534
+ ...(plugin.activation.peer?.request === undefined ? {} : { pairedRequestVersion: 1 }),
535
+ ...(plugin.activation.peer?.openChannel === undefined ? {} : { pairedChannelVersion: 1 }),
536
+ }));
537
+ this.logger.error({
538
+ err: error,
539
+ pluginId: plugin.entry.id,
540
+ phase: "dispose",
541
+ ...(hostCleanupError === undefined ? {} : { hostCleanupError }),
542
+ }, "server plugin disposal failed");
543
+ }
544
+ async disposeHostCapabilityInstances(plugin) {
545
+ const instances = plugin.hostCapabilityInstances.splice(0).reverse();
546
+ const errors = [];
547
+ for (const instance of instances) {
548
+ if (instance.dispose === undefined)
549
+ continue;
550
+ const error = await this.runDispose(plugin.entry.id, instance.dispose);
551
+ if (error !== undefined)
552
+ errors.push(error);
553
+ }
554
+ if (errors.length === 0)
193
555
  return undefined;
556
+ if (errors.length === 1)
557
+ return errors[0];
558
+ return new AggregateError(errors, "Multiple host capability cleanup operations failed");
559
+ }
560
+ async runDispose(pluginId, dispose) {
194
561
  try {
195
- await runBounded(pluginId, "stop", this.lifecycleTimeoutMs, (signal) => stop(signal));
562
+ await runBounded(pluginId, "dispose", this.lifecycleTimeoutMs, (signal) => dispose(signal));
196
563
  return undefined;
197
564
  }
198
565
  catch (error) {
@@ -200,6 +567,46 @@ export class ServerPluginRuntime {
200
567
  }
201
568
  }
202
569
  }
570
+ function requireTerminalCatalogEntry(snapshot) {
571
+ const entry = snapshot.plugins.find(({ id }) => id === REQUIRED_TERMINAL_PLUGIN_ID);
572
+ if (entry === undefined)
573
+ throw requiredTerminalError("Required bundled Terminal package is missing");
574
+ if (entry.scope !== "bundled" || entry.source !== "bundled") {
575
+ throw requiredTerminalError("Required Terminal package must come from the bundled plugin scope");
576
+ }
577
+ if (entry.browserModule === undefined || entry.serverModule === undefined || !entry.machineSpecific) {
578
+ throw requiredTerminalError("Required Terminal package must provide machine-specific browser and server entries");
579
+ }
580
+ if (!entry.enabled)
581
+ throw requiredTerminalError("Required Terminal package cannot be disabled in normal startup");
582
+ return entry;
583
+ }
584
+ function requireTerminalActivation(activation) {
585
+ if (activation.peer?.request === undefined || activation.peer.openChannel === undefined) {
586
+ throw new IncompatibleServerPluginError("Required Terminal server entry must expose peer request and channel capabilities");
587
+ }
588
+ const requiredKey = capabilityKey(REQUIRED_TERMINAL_SERVICE_CAPABILITY);
589
+ const provision = activation.provides?.find(({ capability }) => capabilityKey(capability) === requiredKey);
590
+ if (provision === undefined) {
591
+ throw new IncompatibleServerPluginError(`Required Terminal server entry must provide ${formatCapability(REQUIRED_TERMINAL_SERVICE_CAPABILITY)}`);
592
+ }
593
+ try {
594
+ REQUIRED_TERMINAL_SERVICE_CAPABILITY.parse(provision.value);
595
+ }
596
+ catch (error) {
597
+ throw new IncompatibleServerPluginError(errorMessage(error), { cause: error });
598
+ }
599
+ }
600
+ function requiredTerminalFirst(left, right) {
601
+ if (left.id === REQUIRED_TERMINAL_PLUGIN_ID)
602
+ return right.id === REQUIRED_TERMINAL_PLUGIN_ID ? 0 : -1;
603
+ if (right.id === REQUIRED_TERMINAL_PLUGIN_ID)
604
+ return 1;
605
+ return left.id.localeCompare(right.id);
606
+ }
607
+ function requiredTerminalError(message, cause) {
608
+ return new RequiredTerminalPluginError(`${message}. ${REQUIRED_TERMINAL_RECOVERY_GUIDANCE}`, cause === undefined ? {} : { cause });
609
+ }
203
610
  function disabledReason(entry, safeStart) {
204
611
  if (!entry.enabled)
205
612
  return "disabled in PI WEB config";
@@ -218,6 +625,8 @@ function recordFor(entry, status) {
218
625
  ...(entry.browserModule === undefined ? {} : { browserRevision: entry.browserModule.revision }),
219
626
  settingsRevision: entry.settingsRevision,
220
627
  machineSpecific: entry.machineSpecific,
628
+ ...(status.pairedRequestVersion === undefined ? {} : { pairedRequestVersion: status.pairedRequestVersion }),
629
+ ...(status.pairedChannelVersion === undefined ? {} : { pairedChannelVersion: status.pairedChannelVersion }),
221
630
  state: status.state,
222
631
  ...(status.name === undefined ? {} : { name: status.name }),
223
632
  ...(status.phase === undefined ? {} : { phase: status.phase }),
@@ -246,44 +655,62 @@ function parsePluginExport(imported) {
246
655
  const plugin = imported["default"];
247
656
  if (!isRecord(plugin))
248
657
  throw new IncompatibleServerPluginError("Server plugin module must export a default plugin object");
249
- const candidate = {
250
- apiVersion: plugin["apiVersion"],
251
- name: plugin["name"],
252
- activate: plugin["activate"],
253
- };
254
- if (candidate.apiVersion !== 1) {
255
- throw new IncompatibleServerPluginError(`Unsupported server plugin API version: ${formatUnknown(candidate.apiVersion)}`);
658
+ const apiVersion = plugin["apiVersion"];
659
+ const name = plugin["name"];
660
+ const activateValue = plugin["activate"];
661
+ if (apiVersion !== 3) {
662
+ throw new IncompatibleServerPluginError(`Unsupported server plugin API version: ${formatUnknown(apiVersion)}`);
256
663
  }
257
- if (typeof candidate.name !== "string" || candidate.name === "") {
664
+ if (typeof name !== "string" || name === "") {
258
665
  throw new IncompatibleServerPluginError("Server plugin name must be a non-empty string");
259
666
  }
260
- if (typeof candidate.activate !== "function" || !isPiWebServerPlugin(candidate)) {
667
+ if (typeof activateValue !== "function") {
261
668
  throw new IncompatibleServerPluginError("Server plugin activate must be a function");
262
669
  }
263
- const activate = candidate.activate.bind(plugin);
264
- return Object.freeze({ apiVersion: 1, name: candidate.name, activate: (context) => activate(context) });
670
+ const requires = snapshotCapabilityRequirements(plugin["requires"], `Server plugin ${name} requirements`);
671
+ return Object.freeze({
672
+ name,
673
+ ...(requires.length === 0 ? {} : { requires }),
674
+ activate(context) {
675
+ const result = Reflect.apply(activateValue, plugin, [context]);
676
+ return result;
677
+ },
678
+ });
265
679
  }
266
- function isPiWebServerPlugin(value) {
267
- return isRecord(value)
268
- && value["apiVersion"] === 1
269
- && typeof value["name"] === "string"
270
- && value["name"] !== ""
271
- && typeof value["activate"] === "function";
680
+ function activationDisposeForRollback(value) {
681
+ if (!isRecord(value))
682
+ return undefined;
683
+ const dispose = value["dispose"];
684
+ if (typeof dispose !== "function")
685
+ return undefined;
686
+ return async (signal) => {
687
+ await Reflect.apply(dispose, value, [signal]);
688
+ };
272
689
  }
273
- function parseActivation(value) {
690
+ function parseActivation(value, pluginId) {
274
691
  if (!isRecord(value))
275
692
  throw new IncompatibleServerPluginError("Server plugin activation must be an object");
276
693
  if (value["workspaceProviders"] !== undefined) {
277
694
  throw new IncompatibleServerPluginError("Server plugins may contribute only one workspaceProvider");
278
695
  }
696
+ if (value["requiredTerminalService"] !== undefined) {
697
+ throw new IncompatibleServerPluginError("requiredTerminalService was removed in server plugin API v3; use a typed capability provision");
698
+ }
699
+ if (value["stop"] !== undefined) {
700
+ throw new IncompatibleServerPluginError("Server plugin stop was removed in API v3; use dispose");
701
+ }
279
702
  const workspaceProviderValue = value["workspaceProvider"];
703
+ const peerValue = value["peer"];
704
+ const provides = snapshotCapabilityProvisions(value["provides"], pluginId, `Server plugin ${pluginId} provisions`);
280
705
  const candidate = {
281
706
  workspaceProvider: workspaceProviderValue === undefined ? undefined : snapshotWorkspaceProvider(workspaceProviderValue),
707
+ peer: peerValue === undefined ? undefined : snapshotPluginPeer(peerValue),
708
+ provides,
282
709
  start: value["start"],
283
- stop: value["stop"],
710
+ dispose: value["dispose"],
284
711
  health: value["health"],
285
712
  };
286
- for (const callback of ["start", "stop", "health"]) {
713
+ for (const callback of ["start", "dispose", "health"]) {
287
714
  const callbackValue = candidate[callback];
288
715
  if (callbackValue !== undefined && typeof callbackValue !== "function") {
289
716
  throw new IncompatibleServerPluginError(`Server plugin ${callback} must be a function`);
@@ -292,12 +719,14 @@ function parseActivation(value) {
292
719
  if (!isServerPluginActivation(candidate))
293
720
  throw new IncompatibleServerPluginError("Server plugin activation is invalid");
294
721
  const start = candidate.start?.bind(value);
295
- const stop = candidate.stop?.bind(value);
722
+ const dispose = candidate.dispose?.bind(value);
296
723
  const health = candidate.health?.bind(value);
297
724
  return Object.freeze({
298
725
  ...(candidate.workspaceProvider === undefined ? {} : { workspaceProvider: candidate.workspaceProvider }),
299
- ...(start === undefined ? {} : { start: (signal) => start(signal) }),
300
- ...(stop === undefined ? {} : { stop: (signal) => stop(signal) }),
726
+ ...(candidate.peer === undefined ? {} : { peer: candidate.peer }),
727
+ ...(provides.length === 0 ? {} : { provides }),
728
+ ...(start === undefined ? {} : { start: (context) => start(context) }),
729
+ ...(dispose === undefined ? {} : { dispose: (signal) => dispose(signal) }),
301
730
  ...(health === undefined ? {} : { health: (signal) => health(signal) }),
302
731
  });
303
732
  }
@@ -305,14 +734,342 @@ function isServerPluginActivation(value) {
305
734
  if (!isRecord(value))
306
735
  return false;
307
736
  const workspaceProvider = value["workspaceProvider"];
737
+ const peer = value["peer"];
738
+ const provides = value["provides"];
308
739
  const start = value["start"];
309
- const stop = value["stop"];
740
+ const dispose = value["dispose"];
310
741
  const health = value["health"];
311
742
  return (workspaceProvider === undefined || isWorkspaceProvider(workspaceProvider))
743
+ && (peer === undefined || isPluginPeer(peer))
744
+ && (provides === undefined || Array.isArray(provides))
312
745
  && (start === undefined || typeof start === "function")
313
- && (stop === undefined || typeof stop === "function")
746
+ && (dispose === undefined || typeof dispose === "function")
314
747
  && (health === undefined || typeof health === "function");
315
748
  }
749
+ function snapshotCapabilityRequirements(value, label) {
750
+ if (value === undefined)
751
+ return Object.freeze([]);
752
+ if (!Array.isArray(value))
753
+ throw new IncompatibleServerPluginError(`${label} must be an array`);
754
+ const seen = new Set();
755
+ const requirements = [];
756
+ for (let index = 0; index < value.length; index += 1) {
757
+ if (!Object.hasOwn(value, index))
758
+ throw new IncompatibleServerPluginError(`${label} must not be sparse`);
759
+ const capability = snapshotUnknownCapability(value[index], `${label}[${String(index)}]`);
760
+ const key = capabilityKey(capability);
761
+ if (seen.has(key))
762
+ throw new IncompatibleServerPluginError(`${label} declares ${formatCapability(capability)} more than once`);
763
+ seen.add(key);
764
+ requirements.push(capability);
765
+ }
766
+ return Object.freeze(requirements);
767
+ }
768
+ function snapshotCapabilityProvisions(value, ownerPluginId, label) {
769
+ if (value === undefined)
770
+ return Object.freeze([]);
771
+ if (!Array.isArray(value))
772
+ throw new IncompatibleServerPluginError(`${label} must be an array`);
773
+ const seen = new Set();
774
+ const provisions = [];
775
+ for (let index = 0; index < value.length; index += 1) {
776
+ if (!Object.hasOwn(value, index))
777
+ throw new IncompatibleServerPluginError(`${label} must not be sparse`);
778
+ const candidate = value[index];
779
+ if (!isRecord(candidate))
780
+ throw new IncompatibleServerPluginError(`${label}[${String(index)}] must be an object`);
781
+ const capability = snapshotUnknownCapability(candidate["capability"], `${label}[${String(index)}].capability`);
782
+ if (ownerPluginId !== undefined && capability.pluginId !== ownerPluginId) {
783
+ throw new IncompatibleServerPluginError(`Server plugin ${ownerPluginId} cannot provide capability owned by ${capability.pluginId}`);
784
+ }
785
+ const key = capabilityKey(capability);
786
+ if (seen.has(key))
787
+ throw new IncompatibleServerPluginError(`${label} publishes ${formatCapability(capability)} more than once`);
788
+ seen.add(key);
789
+ const parsedValue = parseCapabilityValue(capability, candidate["value"], `Provided capability ${formatCapability(capability)}`);
790
+ provisions.push(Object.freeze({ capability, value: parsedValue }));
791
+ }
792
+ return Object.freeze(provisions);
793
+ }
794
+ function snapshotHostCapabilities(value) {
795
+ const provisions = snapshotCapabilityProvisions(value, undefined, "Host capability provisions");
796
+ return internalProvisions({ provides: provisions }, "host");
797
+ }
798
+ function snapshotLateHostCapabilities(value) {
799
+ if (value === undefined)
800
+ return Object.freeze([]);
801
+ if (!Array.isArray(value))
802
+ throw new Error("Late host capabilities must be an array");
803
+ const seen = new Set();
804
+ const capabilities = [];
805
+ for (let index = 0; index < value.length; index += 1) {
806
+ if (!Object.hasOwn(value, index))
807
+ throw new Error("Late host capabilities must not be sparse");
808
+ const capability = snapshotUnknownCapability(value[index], `Late host capabilities[${String(index)}]`);
809
+ const key = capabilityKey(capability);
810
+ if (seen.has(key))
811
+ throw new Error(`Late host capability ${formatCapability(capability)} is configured more than once`);
812
+ seen.add(key);
813
+ capabilities.push(Object.freeze({ capability, key, source: "late-host" }));
814
+ }
815
+ return Object.freeze(capabilities);
816
+ }
817
+ function snapshotHostCapabilityFactories(value) {
818
+ if (value === undefined)
819
+ return Object.freeze([]);
820
+ if (!Array.isArray(value))
821
+ throw new Error("Host capability factories must be an array");
822
+ const seen = new Set();
823
+ const factories = [];
824
+ for (let index = 0; index < value.length; index += 1) {
825
+ if (!Object.hasOwn(value, index))
826
+ throw new Error("Host capability factories must not be sparse");
827
+ const candidate = value[index];
828
+ if (!isRecord(candidate))
829
+ throw new Error(`Host capability factories[${String(index)}] must be an object`);
830
+ const capability = snapshotUnknownCapability(candidate["capability"], `Host capability factories[${String(index)}].capability`);
831
+ const key = capabilityKey(capability);
832
+ if (seen.has(key))
833
+ throw new Error(`Host capability factory for ${formatCapability(capability)} is configured more than once`);
834
+ const create = candidate["create"];
835
+ if (typeof create !== "function")
836
+ throw new Error(`Host capability factory for ${formatCapability(capability)} must expose create`);
837
+ seen.add(key);
838
+ factories.push(Object.freeze({
839
+ capability,
840
+ key,
841
+ source: "host-factory",
842
+ create(context) {
843
+ const result = Reflect.apply(create, candidate, [context]);
844
+ return snapshotHostCapabilityInstance(result, `Host capability ${formatCapability(capability)}`);
845
+ },
846
+ }));
847
+ }
848
+ return Object.freeze(factories);
849
+ }
850
+ function snapshotHostCapabilityInstance(value, label) {
851
+ if (!isRecord(value) || !Object.hasOwn(value, "value")) {
852
+ throw new Error(`${label} factory must return an object containing value`);
853
+ }
854
+ const dispose = value["dispose"];
855
+ if (dispose !== undefined && typeof dispose !== "function") {
856
+ throw new Error(`${label} factory dispose must be a function`);
857
+ }
858
+ return Object.freeze({
859
+ value: value["value"],
860
+ ...(dispose === undefined
861
+ ? {}
862
+ : { dispose: async (signal) => { await Reflect.apply(dispose, value, [signal]); } }),
863
+ });
864
+ }
865
+ function internalProvisions(activation, source) {
866
+ return Object.freeze((activation.provides ?? []).map((provision) => Object.freeze({
867
+ capability: provision.capability,
868
+ key: capabilityKey(provision.capability),
869
+ value: provision.value,
870
+ source,
871
+ })));
872
+ }
873
+ function snapshotUnknownCapability(value, label) {
874
+ if (!isRecord(value))
875
+ throw new IncompatibleServerPluginError(`${label} must be an object`);
876
+ const pluginId = value["pluginId"];
877
+ const id = value["id"];
878
+ const version = value["version"];
879
+ const parse = value["parse"];
880
+ const validated = validateCapabilityFields(pluginId, id, version, parse, label);
881
+ return Object.freeze({
882
+ pluginId: validated.pluginId,
883
+ id: validated.id,
884
+ version: validated.version,
885
+ parse(input) {
886
+ const result = Reflect.apply(validated.parse, value, [input]);
887
+ return result;
888
+ },
889
+ });
890
+ }
891
+ function snapshotTypedCapability(value, label) {
892
+ const validated = validateCapabilityFields(value.pluginId, value.id, value.version, value.parse, label);
893
+ const parse = value.parse.bind(value);
894
+ return Object.freeze({
895
+ pluginId: validated.pluginId,
896
+ id: validated.id,
897
+ version: validated.version,
898
+ parse: (input) => parse(input),
899
+ });
900
+ }
901
+ function validateCapabilityFields(pluginId, id, version, parse, label) {
902
+ if (typeof pluginId !== "string" || pluginId.trim() === "") {
903
+ throw new IncompatibleServerPluginError(`${label} pluginId must be a non-empty string`);
904
+ }
905
+ if (typeof id !== "string" || id.trim() === "") {
906
+ throw new IncompatibleServerPluginError(`${label} id must be a non-empty string`);
907
+ }
908
+ if (typeof version !== "number" || !Number.isInteger(version) || version <= 0) {
909
+ throw new IncompatibleServerPluginError(`${label} version must be a positive integer`);
910
+ }
911
+ if (!isCapabilityParser(parse))
912
+ throw new IncompatibleServerPluginError(`${label} parse must be a function`);
913
+ return { pluginId, id, version, parse };
914
+ }
915
+ function isCapabilityParser(value) {
916
+ return typeof value === "function";
917
+ }
918
+ function parseCapabilityValue(capability, value, label) {
919
+ try {
920
+ return capability.parse(value);
921
+ }
922
+ catch (error) {
923
+ throw new IncompatibleServerPluginError(`${label} is invalid: ${errorMessage(error)}`, { cause: error });
924
+ }
925
+ }
926
+ function capabilityKey(capability) {
927
+ return JSON.stringify([capability.pluginId, capability.id, capability.version]);
928
+ }
929
+ function formatCapability(capability) {
930
+ return `${capability.pluginId}/${capability.id} v${String(capability.version)}`;
931
+ }
932
+ function createCapabilityResolver(pluginId, resolved) {
933
+ return Object.freeze({
934
+ resolve(value) {
935
+ const capability = snapshotTypedCapability(value, `Capability request from server plugin ${pluginId}`);
936
+ const requirement = resolved.get(capabilityKey(capability));
937
+ if (requirement === undefined) {
938
+ throw new Error(`Server plugin ${pluginId} did not declare required capability ${formatCapability(capability)}`);
939
+ }
940
+ return parseCapabilityValue(capability, requirement.provisionValue, `Required capability ${formatCapability(capability)} for server plugin ${pluginId}`);
941
+ },
942
+ });
943
+ }
944
+ function findLateHostDependency(staged, pending, declared, visiting = new Set()) {
945
+ if (visiting.has(staged.entry.id))
946
+ return undefined;
947
+ const nextVisiting = new Set(visiting);
948
+ nextVisiting.add(staged.entry.id);
949
+ for (const requirement of staged.plugin.requires ?? []) {
950
+ const declaration = declared.get(capabilityKey(requirement));
951
+ if (declaration?.source === "late-host")
952
+ return declaration.capability;
953
+ if (declaration?.source !== "plugin")
954
+ continue;
955
+ const provider = pending.get(requirement.pluginId);
956
+ if (provider === undefined)
957
+ continue;
958
+ const lateCapability = findLateHostDependency(provider, pending, declared, nextVisiting);
959
+ if (lateCapability !== undefined)
960
+ return lateCapability;
961
+ }
962
+ return undefined;
963
+ }
964
+ function findCapabilityCycle(pending, declared) {
965
+ const state = new Map();
966
+ const stack = [];
967
+ const visit = (pluginId) => {
968
+ state.set(pluginId, "visiting");
969
+ stack.push(pluginId);
970
+ const plugin = pending.get(pluginId);
971
+ const providers = (plugin?.plugin.requires ?? [])
972
+ .filter((requirement) => declared.get(capabilityKey(requirement))?.source === "plugin" && pending.has(requirement.pluginId))
973
+ .map((requirement) => requirement.pluginId)
974
+ .sort((left, right) => left.localeCompare(right));
975
+ for (const providerId of providers) {
976
+ if (state.get(providerId) === "visiting") {
977
+ const start = stack.indexOf(providerId);
978
+ return stack.slice(start);
979
+ }
980
+ if (state.get(providerId) !== "visited") {
981
+ const cycle = visit(providerId);
982
+ if (cycle !== undefined)
983
+ return cycle;
984
+ }
985
+ }
986
+ stack.pop();
987
+ state.set(pluginId, "visited");
988
+ return undefined;
989
+ };
990
+ for (const pluginId of [...pending.keys()].sort((left, right) => left.localeCompare(right))) {
991
+ if (state.has(pluginId))
992
+ continue;
993
+ const cycle = visit(pluginId);
994
+ if (cycle !== undefined)
995
+ return cycle;
996
+ }
997
+ return [];
998
+ }
999
+ function abortPluginLifetime(controller, pluginId, outcome) {
1000
+ if (!controller.signal.aborted) {
1001
+ controller.abort(new DOMException(`Server plugin ${pluginId} ${outcome}`, "AbortError"));
1002
+ }
1003
+ }
1004
+ function withRollbackError(message, rollbackError) {
1005
+ return rollbackError === undefined
1006
+ ? message
1007
+ : `${message}; startup rollback failed: ${errorMessage(rollbackError)}`;
1008
+ }
1009
+ function withHostCleanupError(message, cleanupError) {
1010
+ return cleanupError === undefined
1011
+ ? message
1012
+ : `${message}; host capability cleanup failed: ${errorMessage(cleanupError)}`;
1013
+ }
1014
+ function combinedDisposalError(pluginError, hostCleanupError) {
1015
+ if (pluginError === undefined)
1016
+ return hostCleanupError;
1017
+ if (hostCleanupError === undefined)
1018
+ return pluginError;
1019
+ return new AggregateError([pluginError, hostCleanupError], `${errorMessage(pluginError)}; host capability cleanup failed: ${errorMessage(hostCleanupError)}`);
1020
+ }
1021
+ function snapshotPluginPeer(value) {
1022
+ if (!isPluginPeer(value)) {
1023
+ throw new IncompatibleServerPluginError("Server plugin peer must include a request or channel handler");
1024
+ }
1025
+ const request = value.request?.bind(value);
1026
+ const openChannel = value.openChannel?.bind(value);
1027
+ const snapshotRequest = request === undefined
1028
+ ? undefined
1029
+ : (context) => request(context);
1030
+ const snapshotOpenChannel = openChannel === undefined
1031
+ ? undefined
1032
+ : async (context) => (snapshotPluginPeerChannel(await openChannel(context)));
1033
+ if (snapshotRequest !== undefined && snapshotOpenChannel !== undefined) {
1034
+ return Object.freeze({ request: snapshotRequest, openChannel: snapshotOpenChannel });
1035
+ }
1036
+ if (snapshotRequest !== undefined)
1037
+ return Object.freeze({ request: snapshotRequest });
1038
+ if (snapshotOpenChannel !== undefined)
1039
+ return Object.freeze({ openChannel: snapshotOpenChannel });
1040
+ throw new IncompatibleServerPluginError("Server plugin peer must include a request or channel handler");
1041
+ }
1042
+ function isPluginPeer(value) {
1043
+ if (!isRecord(value))
1044
+ return false;
1045
+ const request = value["request"];
1046
+ const openChannel = value["openChannel"];
1047
+ return (typeof request === "function" || typeof openChannel === "function")
1048
+ && (request === undefined || typeof request === "function")
1049
+ && (openChannel === undefined || typeof openChannel === "function");
1050
+ }
1051
+ function snapshotPluginPeerChannel(value) {
1052
+ if (!isPluginPeerChannel(value)) {
1053
+ throw new Error("Server plugin openChannel must return a channel with receive, optional completion, and optional close callbacks");
1054
+ }
1055
+ const receive = value.receive.bind(value);
1056
+ const closed = value.closed === undefined ? undefined : Promise.resolve(value.closed);
1057
+ const close = value.close?.bind(value);
1058
+ return Object.freeze({
1059
+ receive: (data, signal) => receive(data, signal),
1060
+ ...(closed === undefined ? {} : { closed }),
1061
+ ...(close === undefined ? {} : { close: (context) => close(context) }),
1062
+ });
1063
+ }
1064
+ function isPluginPeerChannel(value) {
1065
+ return isRecord(value)
1066
+ && typeof value["receive"] === "function"
1067
+ && (value["closed"] === undefined || isPromiseLike(value["closed"]))
1068
+ && (value["close"] === undefined || typeof value["close"] === "function");
1069
+ }
1070
+ function isPromiseLike(value) {
1071
+ return isRecord(value) && typeof value["then"] === "function";
1072
+ }
316
1073
  function snapshotWorkspaceProvider(value) {
317
1074
  if (!isRecord(value))
318
1075
  throw new IncompatibleServerPluginError("Server plugin workspaceProvider is invalid");
@@ -320,20 +1077,17 @@ function snapshotWorkspaceProvider(value) {
320
1077
  fallback: value["fallback"],
321
1078
  probe: value["probe"],
322
1079
  list: value["list"],
323
- request: value["request"],
324
1080
  prepareRemove: value["prepareRemove"],
325
1081
  };
326
1082
  if (!isWorkspaceProvider(candidate))
327
1083
  throw new IncompatibleServerPluginError("Server plugin workspaceProvider is invalid");
328
1084
  const probe = candidate.probe.bind(value);
329
1085
  const list = candidate.list.bind(value);
330
- const request = candidate.request?.bind(value);
331
1086
  const prepareRemove = candidate.prepareRemove?.bind(value);
332
1087
  return Object.freeze({
333
1088
  ...(candidate.fallback === undefined ? {} : { fallback: candidate.fallback }),
334
1089
  probe: (project, signal) => probe(project, signal),
335
1090
  list: (project, signal) => list(project, signal),
336
- ...(request === undefined ? {} : { request: (context) => request(context) }),
337
1091
  ...(prepareRemove === undefined ? {} : { prepareRemove: (context) => prepareRemove(context) }),
338
1092
  });
339
1093
  }
@@ -343,12 +1097,10 @@ function isWorkspaceProvider(value) {
343
1097
  const fallback = value["fallback"];
344
1098
  const probe = value["probe"];
345
1099
  const list = value["list"];
346
- const request = value["request"];
347
1100
  const prepareRemove = value["prepareRemove"];
348
1101
  return (fallback === undefined || typeof fallback === "boolean")
349
1102
  && typeof probe === "function"
350
1103
  && typeof list === "function"
351
- && (request === undefined || typeof request === "function")
352
1104
  && (prepareRemove === undefined || typeof prepareRemove === "function");
353
1105
  }
354
1106
  function parseHealth(value) {
@@ -370,6 +1122,67 @@ function parseHealth(value) {
370
1122
  ...(clonedDetails === undefined ? {} : { details: clonedDetails }),
371
1123
  });
372
1124
  }
1125
+ function createScopedNoticeReporter(pluginId, sink) {
1126
+ if (sink === undefined)
1127
+ return undefined;
1128
+ const source = `${SERVER_PLUGIN_NOTICE_SOURCE_PREFIX}${pluginId}`;
1129
+ let active = true;
1130
+ const reporter = Object.freeze({
1131
+ version: 1,
1132
+ record(input) {
1133
+ if (!active)
1134
+ throw new Error(`Server plugin notice reporter for ${pluginId} is no longer active`);
1135
+ sink(source, parseServerPluginNoticeInput(input));
1136
+ },
1137
+ });
1138
+ return Object.freeze({
1139
+ reporter,
1140
+ revoke() { active = false; },
1141
+ });
1142
+ }
1143
+ function parseServerPluginNoticeInput(value) {
1144
+ if (!isPlainRecord(value))
1145
+ throw new Error("Server plugin notice input must be an object");
1146
+ if ("source" in value)
1147
+ throw new Error("Server plugin notices cannot set their source");
1148
+ for (const key in value) {
1149
+ if (!Object.hasOwn(value, key))
1150
+ continue;
1151
+ if (key !== "severity" && key !== "message" && key !== "scope" && key !== "context") {
1152
+ throw new Error(`Unsupported server plugin notice field: ${key}`);
1153
+ }
1154
+ }
1155
+ const severity = value["severity"];
1156
+ if (severity !== "info" && severity !== "warning" && severity !== "error") {
1157
+ throw new Error("Server plugin notice severity must be info, warning, or error");
1158
+ }
1159
+ const message = value["message"];
1160
+ if (typeof message !== "string") {
1161
+ throw new Error("Server plugin notice message must be a non-empty string");
1162
+ }
1163
+ if (serverNoticeStringExceedsUtf8ByteLimit(message, SERVER_PLUGIN_NOTICE_MESSAGE_MAX_BYTES)) {
1164
+ throw new Error(`Server plugin notice message exceeds the ${String(SERVER_PLUGIN_NOTICE_MESSAGE_MAX_BYTES)} byte limit`);
1165
+ }
1166
+ // This scan and allocation are bounded because the UTF-8 limit passed first.
1167
+ if (message.trim() === "") {
1168
+ throw new Error("Server plugin notice message must be a non-empty string");
1169
+ }
1170
+ const scope = value["scope"] === undefined
1171
+ ? undefined
1172
+ : parseServerNoticeScope(value["scope"], "Server plugin notice scope");
1173
+ const context = value["context"] === undefined
1174
+ ? undefined
1175
+ : cloneJsonObject(value["context"], "server plugin notice context", {
1176
+ maxDepth: SERVER_PLUGIN_NOTICE_CONTEXT_MAX_DEPTH,
1177
+ maxBytes: SERVER_PLUGIN_NOTICE_CONTEXT_MAX_BYTES,
1178
+ });
1179
+ return Object.freeze({
1180
+ severity,
1181
+ message,
1182
+ ...(scope === undefined ? {} : { scope }),
1183
+ ...(context === undefined ? {} : { context }),
1184
+ });
1185
+ }
373
1186
  function createScopedLogger(pluginId, logger) {
374
1187
  return Object.freeze({
375
1188
  debug(message, details) {
@@ -404,40 +1217,136 @@ async function runBounded(pluginId, phase, timeoutMs, operation) {
404
1217
  controller.abort(new DOMException("Server plugin operation completed", "AbortError"));
405
1218
  }
406
1219
  }
407
- function cloneJsonObject(value, label) {
408
- if (!isRecord(value))
1220
+ function cloneJsonObject(value, label, limits = {}) {
1221
+ if (!isPlainRecord(value))
409
1222
  throw new IncompatibleServerPluginError(`${label} must be a JSON object`);
410
- return cloneJsonRecord(value, new Set(), label);
1223
+ const byteBudget = limits.maxBytes === undefined
1224
+ ? undefined
1225
+ : { label, maxBytes: limits.maxBytes, remaining: limits.maxBytes };
1226
+ return cloneJsonRecord(value, new Set(), label, 0, limits.maxDepth, byteBudget);
411
1227
  }
412
- function cloneJsonRecord(value, ancestors, label) {
1228
+ function cloneJsonRecord(value, ancestors, label, depth, maxDepth, byteBudget) {
1229
+ requireJsonDepth(depth, maxDepth, label);
413
1230
  if (ancestors.has(value))
414
1231
  throw new IncompatibleServerPluginError(`${label} must not contain cycles`);
1232
+ consumeJsonBytes(byteBudget, 2); // Opening and closing braces.
415
1233
  ancestors.add(value);
416
1234
  const output = {};
417
- for (const [key, child] of Object.entries(value))
418
- output[key] = cloneJsonValue(child, ancestors, label);
1235
+ let propertyCount = 0;
1236
+ for (const key in value) {
1237
+ if (!Object.hasOwn(value, key))
1238
+ continue;
1239
+ if (propertyCount > 0)
1240
+ consumeJsonBytes(byteBudget, 1); // Comma.
1241
+ consumeJsonStringBytes(byteBudget, key);
1242
+ consumeJsonBytes(byteBudget, 1); // Colon.
1243
+ defineJsonProperty(output, key, cloneJsonValue(value[key], ancestors, label, depth + 1, maxDepth, byteBudget));
1244
+ propertyCount += 1;
1245
+ }
419
1246
  ancestors.delete(value);
420
1247
  return Object.freeze(output);
421
1248
  }
422
- function cloneJsonValue(value, ancestors, label) {
423
- if (value === null || typeof value === "string" || typeof value === "boolean")
1249
+ function cloneJsonValue(value, ancestors, label, depth, maxDepth, byteBudget) {
1250
+ requireJsonDepth(depth, maxDepth, label);
1251
+ if (value === null) {
1252
+ consumeJsonBytes(byteBudget, 4);
1253
+ return value;
1254
+ }
1255
+ if (typeof value === "string") {
1256
+ consumeJsonStringBytes(byteBudget, value);
1257
+ return value;
1258
+ }
1259
+ if (typeof value === "boolean") {
1260
+ consumeJsonBytes(byteBudget, value ? 4 : 5);
424
1261
  return value;
1262
+ }
425
1263
  if (typeof value === "number") {
426
1264
  if (!Number.isFinite(value))
427
1265
  throw new IncompatibleServerPluginError(`${label} must contain only finite JSON numbers`);
1266
+ consumeJsonBytes(byteBudget, String(value).length);
428
1267
  return value;
429
1268
  }
430
- if (Array.isArray(value)) {
431
- if (ancestors.has(value))
432
- throw new IncompatibleServerPluginError(`${label} must not contain cycles`);
433
- ancestors.add(value);
434
- const output = value.map((child) => cloneJsonValue(child, ancestors, label));
435
- ancestors.delete(value);
1269
+ if (Array.isArray(value))
1270
+ return cloneJsonArray(value, ancestors, label, depth, maxDepth, byteBudget);
1271
+ if (isPlainRecord(value))
1272
+ return cloneJsonRecord(value, ancestors, label, depth, maxDepth, byteBudget);
1273
+ throw new IncompatibleServerPluginError(`${label} must contain only JSON values`);
1274
+ }
1275
+ function cloneJsonArray(value, ancestors, label, depth, maxDepth, byteBudget) {
1276
+ requireJsonDepth(depth, maxDepth, label);
1277
+ if (ancestors.has(value))
1278
+ throw new IncompatibleServerPluginError(`${label} must not contain cycles`);
1279
+ consumeJsonBytes(byteBudget, 2); // Opening and closing brackets.
1280
+ ancestors.add(value);
1281
+ try {
1282
+ const output = [];
1283
+ const length = value.length;
1284
+ // Plugin-owned arrays may override iteration helpers; inspect each dense element directly.
1285
+ for (let index = 0; index < length; index += 1) {
1286
+ if (!Object.hasOwn(value, index)) {
1287
+ throw new IncompatibleServerPluginError(`${label} must not contain sparse arrays`);
1288
+ }
1289
+ if (index > 0)
1290
+ consumeJsonBytes(byteBudget, 1); // Comma.
1291
+ output[index] = cloneJsonValue(value[index], ancestors, label, depth + 1, maxDepth, byteBudget);
1292
+ }
436
1293
  return Object.freeze(output);
437
1294
  }
438
- if (isRecord(value))
439
- return cloneJsonRecord(value, ancestors, label);
440
- throw new IncompatibleServerPluginError(`${label} must contain only JSON values`);
1295
+ finally {
1296
+ ancestors.delete(value);
1297
+ }
1298
+ }
1299
+ function consumeJsonStringBytes(byteBudget, value) {
1300
+ if (byteBudget === undefined)
1301
+ return;
1302
+ consumeJsonBytes(byteBudget, 2); // Opening and closing quotes.
1303
+ for (let index = 0; index < value.length; index += 1) {
1304
+ const codeUnit = value.charCodeAt(index);
1305
+ if (codeUnit === 0x22 || codeUnit === 0x5c
1306
+ || codeUnit === 0x08 || codeUnit === 0x09 || codeUnit === 0x0a
1307
+ || codeUnit === 0x0c || codeUnit === 0x0d) {
1308
+ consumeJsonBytes(byteBudget, 2);
1309
+ }
1310
+ else if (codeUnit <= 0x1f) {
1311
+ consumeJsonBytes(byteBudget, 6);
1312
+ }
1313
+ else if (codeUnit <= 0x7f) {
1314
+ consumeJsonBytes(byteBudget, 1);
1315
+ }
1316
+ else if (codeUnit <= 0x7ff) {
1317
+ consumeJsonBytes(byteBudget, 2);
1318
+ }
1319
+ else if (codeUnit >= 0xd800 && codeUnit <= 0xdbff
1320
+ && index + 1 < value.length
1321
+ && value.charCodeAt(index + 1) >= 0xdc00
1322
+ && value.charCodeAt(index + 1) <= 0xdfff) {
1323
+ consumeJsonBytes(byteBudget, 4);
1324
+ index += 1;
1325
+ }
1326
+ else if (codeUnit >= 0xd800 && codeUnit <= 0xdfff) {
1327
+ // Well-formed JSON.stringify escapes isolated UTF-16 surrogates as \uXXXX.
1328
+ consumeJsonBytes(byteBudget, 6);
1329
+ }
1330
+ else {
1331
+ consumeJsonBytes(byteBudget, 3);
1332
+ }
1333
+ }
1334
+ }
1335
+ function consumeJsonBytes(byteBudget, count) {
1336
+ if (byteBudget === undefined)
1337
+ return;
1338
+ byteBudget.remaining -= count;
1339
+ if (byteBudget.remaining < 0) {
1340
+ throw new Error(`${byteBudget.label} exceeds the ${String(byteBudget.maxBytes)} byte limit`);
1341
+ }
1342
+ }
1343
+ function requireJsonDepth(depth, maxDepth, label) {
1344
+ if (maxDepth !== undefined && depth > maxDepth) {
1345
+ throw new IncompatibleServerPluginError(`${label} exceeds the maximum JSON depth of ${String(maxDepth)}`);
1346
+ }
1347
+ }
1348
+ function defineJsonProperty(record, key, value) {
1349
+ Object.defineProperty(record, key, { value, enumerable: true, configurable: true, writable: true });
441
1350
  }
442
1351
  function abortError(signal) {
443
1352
  const reason = signal.reason;
@@ -449,6 +1358,12 @@ class IncompatibleServerPluginError extends Error {
449
1358
  this.name = "IncompatibleServerPluginError";
450
1359
  }
451
1360
  }
1361
+ export class RequiredTerminalPluginError extends Error {
1362
+ constructor() {
1363
+ super(...arguments);
1364
+ this.name = "RequiredTerminalPluginError";
1365
+ }
1366
+ }
452
1367
  class ServerPluginTimeoutError extends Error {
453
1368
  constructor() {
454
1369
  super(...arguments);
@@ -477,4 +1392,10 @@ function formatUnknown(value) {
477
1392
  function isRecord(value) {
478
1393
  return typeof value === "object" && value !== null && !Array.isArray(value);
479
1394
  }
1395
+ function isPlainRecord(value) {
1396
+ if (!isRecord(value))
1397
+ return false;
1398
+ const prototype = Object.getPrototypeOf(value);
1399
+ return prototype === Object.prototype || prototype === null;
1400
+ }
480
1401
  //# sourceMappingURL=serverPluginRuntime.js.map