@dxos/app-framework 0.8.4-main.8360d9e660 → 0.8.4-main.8baae0fced

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 (380) hide show
  1. package/LICENSE +102 -5
  2. package/README.md +1 -1
  3. package/dist/lib/browser/{capability-5OFLR7J4.mjs → capability-K5XIVCQU.mjs} +12 -11
  4. package/dist/lib/browser/capability-K5XIVCQU.mjs.map +7 -0
  5. package/dist/lib/browser/{chunk-WEBSGU5L.mjs → chunk-5AHASNDW.mjs} +20 -5
  6. package/dist/lib/browser/chunk-5AHASNDW.mjs.map +7 -0
  7. package/dist/lib/browser/chunk-5GY3YOEL.mjs +28 -0
  8. package/dist/lib/browser/chunk-5GY3YOEL.mjs.map +7 -0
  9. package/dist/lib/browser/{chunk-272IPLHQ.mjs → chunk-66IXTIVK.mjs} +3 -2
  10. package/dist/lib/browser/chunk-66IXTIVK.mjs.map +7 -0
  11. package/dist/lib/browser/{chunk-JGWCBVKJ.mjs → chunk-BRK6GYNB.mjs} +14 -42
  12. package/dist/lib/browser/chunk-BRK6GYNB.mjs.map +7 -0
  13. package/dist/lib/browser/{chunk-TGX63LTL.mjs → chunk-FJ4765WW.mjs} +1 -1
  14. package/dist/lib/browser/{chunk-TGX63LTL.mjs.map → chunk-FJ4765WW.mjs.map} +2 -2
  15. package/dist/lib/browser/chunk-FO3IYSLV.mjs +68 -0
  16. package/dist/lib/browser/chunk-FO3IYSLV.mjs.map +7 -0
  17. package/dist/lib/browser/{chunk-YNFPIQGB.mjs → chunk-IW44C7UL.mjs} +9 -2
  18. package/dist/lib/browser/chunk-IW44C7UL.mjs.map +7 -0
  19. package/dist/lib/browser/{chunk-TIEBZMTF.mjs → chunk-KFDF7KR3.mjs} +9 -11
  20. package/dist/lib/browser/{chunk-TIEBZMTF.mjs.map → chunk-KFDF7KR3.mjs.map} +3 -3
  21. package/dist/lib/browser/chunk-KLHQNYJ2.mjs +422 -0
  22. package/dist/lib/browser/chunk-KLHQNYJ2.mjs.map +7 -0
  23. package/dist/lib/browser/chunk-QLML5QFJ.mjs +581 -0
  24. package/dist/lib/browser/chunk-QLML5QFJ.mjs.map +7 -0
  25. package/dist/lib/browser/{chunk-GH3M2LIW.mjs → chunk-SLX73WRZ.mjs} +85 -15
  26. package/dist/lib/browser/chunk-SLX73WRZ.mjs.map +7 -0
  27. package/dist/lib/browser/chunk-UVTGHZQF.mjs +513 -0
  28. package/dist/lib/browser/chunk-UVTGHZQF.mjs.map +7 -0
  29. package/dist/lib/browser/chunk-VJ5PFAWC.mjs +1446 -0
  30. package/dist/lib/browser/chunk-VJ5PFAWC.mjs.map +7 -0
  31. package/dist/lib/browser/cli/index.mjs +15 -30
  32. package/dist/lib/browser/cli/index.mjs.map +3 -3
  33. package/dist/lib/browser/common/activation-events.mjs +11 -14
  34. package/dist/lib/browser/common/capabilities.mjs +19 -8
  35. package/dist/lib/browser/core/activation-event.mjs +1 -1
  36. package/dist/lib/browser/core/capability.mjs +5 -1
  37. package/dist/lib/browser/core/plugin-manager.mjs +8 -4
  38. package/dist/lib/browser/core/plugin.mjs +12 -2
  39. package/dist/lib/browser/core/url-loader.mjs +24 -0
  40. package/dist/lib/browser/index.mjs +47 -49
  41. package/dist/lib/browser/index.mjs.map +4 -4
  42. package/dist/lib/browser/meta.json +1 -1
  43. package/dist/lib/browser/process-manager-capability-JIWLN7SU.mjs +89 -0
  44. package/dist/lib/browser/process-manager-capability-JIWLN7SU.mjs.map +7 -0
  45. package/dist/lib/browser/testing/index.mjs +164 -34
  46. package/dist/lib/browser/testing/index.mjs.map +4 -4
  47. package/dist/lib/browser/testing/react.mjs +78 -0
  48. package/dist/lib/browser/testing/react.mjs.map +7 -0
  49. package/dist/lib/browser/ui/index.mjs +24 -15
  50. package/dist/lib/node-esm/{capability-CRHZKL6T.mjs → capability-RLKFFLTB.mjs} +12 -11
  51. package/dist/lib/node-esm/capability-RLKFFLTB.mjs.map +7 -0
  52. package/dist/lib/node-esm/{chunk-FKE4Z3D6.mjs → chunk-37Z53PXZ.mjs} +1 -1
  53. package/dist/lib/node-esm/{chunk-FKE4Z3D6.mjs.map → chunk-37Z53PXZ.mjs.map} +2 -2
  54. package/dist/lib/node-esm/chunk-42UNAKYO.mjs +423 -0
  55. package/dist/lib/node-esm/chunk-42UNAKYO.mjs.map +7 -0
  56. package/dist/lib/node-esm/{chunk-LHCJGNXK.mjs → chunk-6S45OMUP.mjs} +85 -15
  57. package/dist/lib/node-esm/chunk-6S45OMUP.mjs.map +7 -0
  58. package/dist/lib/node-esm/{chunk-3D66SZHP.mjs → chunk-BYHYYJZH.mjs} +14 -42
  59. package/dist/lib/node-esm/chunk-BYHYYJZH.mjs.map +7 -0
  60. package/dist/lib/node-esm/{chunk-SB5ODNPX.mjs → chunk-CTKEZHKF.mjs} +9 -2
  61. package/dist/lib/node-esm/chunk-CTKEZHKF.mjs.map +7 -0
  62. package/dist/lib/node-esm/chunk-JNT72ZCN.mjs +514 -0
  63. package/dist/lib/node-esm/chunk-JNT72ZCN.mjs.map +7 -0
  64. package/dist/lib/node-esm/chunk-KFZEB6BV.mjs +29 -0
  65. package/dist/lib/node-esm/chunk-KFZEB6BV.mjs.map +7 -0
  66. package/dist/lib/node-esm/chunk-LJNUFNDO.mjs +582 -0
  67. package/dist/lib/node-esm/chunk-LJNUFNDO.mjs.map +7 -0
  68. package/dist/lib/node-esm/{chunk-SCDGIGGU.mjs → chunk-OUEMWPIW.mjs} +9 -11
  69. package/dist/lib/node-esm/{chunk-SCDGIGGU.mjs.map → chunk-OUEMWPIW.mjs.map} +3 -3
  70. package/dist/lib/node-esm/{chunk-SQICGJBW.mjs → chunk-PW2VYGOS.mjs} +20 -5
  71. package/dist/lib/node-esm/chunk-PW2VYGOS.mjs.map +7 -0
  72. package/dist/lib/node-esm/chunk-SFYCO3PT.mjs +1447 -0
  73. package/dist/lib/node-esm/chunk-SFYCO3PT.mjs.map +7 -0
  74. package/dist/lib/node-esm/chunk-WK7OIQKI.mjs +70 -0
  75. package/dist/lib/node-esm/chunk-WK7OIQKI.mjs.map +7 -0
  76. package/dist/lib/node-esm/{chunk-6REV5DE7.mjs → chunk-XOCUANHO.mjs} +3 -2
  77. package/dist/lib/node-esm/chunk-XOCUANHO.mjs.map +7 -0
  78. package/dist/lib/node-esm/cli/index.mjs +15 -30
  79. package/dist/lib/node-esm/cli/index.mjs.map +3 -3
  80. package/dist/lib/node-esm/common/activation-events.mjs +11 -14
  81. package/dist/lib/node-esm/common/capabilities.mjs +19 -8
  82. package/dist/lib/node-esm/core/activation-event.mjs +1 -1
  83. package/dist/lib/node-esm/core/capability.mjs +5 -1
  84. package/dist/lib/node-esm/core/plugin-manager.mjs +8 -4
  85. package/dist/lib/node-esm/core/plugin.mjs +12 -2
  86. package/dist/lib/node-esm/core/url-loader.mjs +25 -0
  87. package/dist/lib/node-esm/index.mjs +47 -49
  88. package/dist/lib/node-esm/index.mjs.map +4 -4
  89. package/dist/lib/node-esm/meta.json +1 -1
  90. package/dist/lib/node-esm/process-manager-capability-PHKLO2BL.mjs +90 -0
  91. package/dist/lib/node-esm/process-manager-capability-PHKLO2BL.mjs.map +7 -0
  92. package/dist/lib/node-esm/testing/index.mjs +164 -34
  93. package/dist/lib/node-esm/testing/index.mjs.map +4 -4
  94. package/dist/lib/node-esm/testing/react.mjs +79 -0
  95. package/dist/lib/node-esm/testing/react.mjs.map +7 -0
  96. package/dist/lib/node-esm/ui/index.mjs +24 -15
  97. package/dist/plugin/node-esm/index.mjs +893 -0
  98. package/dist/plugin/node-esm/index.mjs.map +7 -0
  99. package/dist/plugin/node-esm/meta.json +1 -0
  100. package/dist/types/src/cli/cli.d.ts +1 -3
  101. package/dist/types/src/cli/cli.d.ts.map +1 -1
  102. package/dist/types/src/common/activation-events.d.ts +10 -13
  103. package/dist/types/src/common/activation-events.d.ts.map +1 -1
  104. package/dist/types/src/common/annotations.d.ts +1 -0
  105. package/dist/types/src/common/annotations.d.ts.map +1 -0
  106. package/dist/types/src/common/capabilities.d.ts +113 -12
  107. package/dist/types/src/common/capabilities.d.ts.map +1 -1
  108. package/dist/types/src/common/operations.d.ts +8 -22
  109. package/dist/types/src/common/operations.d.ts.map +1 -1
  110. package/dist/types/src/core/activation-event.d.ts +4 -4
  111. package/dist/types/src/core/activation-event.d.ts.map +1 -1
  112. package/dist/types/src/core/capability-manager.d.ts +5 -0
  113. package/dist/types/src/core/capability-manager.d.ts.map +1 -1
  114. package/dist/types/src/core/capability.d.ts +13 -2
  115. package/dist/types/src/core/capability.d.ts.map +1 -1
  116. package/dist/types/src/core/edge-registry-plugin-provider.d.ts +30 -0
  117. package/dist/types/src/core/edge-registry-plugin-provider.d.ts.map +1 -0
  118. package/dist/types/src/core/index.d.ts +6 -0
  119. package/dist/types/src/core/index.d.ts.map +1 -1
  120. package/dist/types/src/core/plugin-asset-cache.d.ts +71 -0
  121. package/dist/types/src/core/plugin-asset-cache.d.ts.map +1 -0
  122. package/dist/types/src/core/plugin-manager.d.ts +232 -7
  123. package/dist/types/src/core/plugin-manager.d.ts.map +1 -1
  124. package/dist/types/src/core/plugin-manifest.d.ts +101 -0
  125. package/dist/types/src/core/plugin-manifest.d.ts.map +1 -0
  126. package/dist/types/src/core/plugin-manifest.test.d.ts +2 -0
  127. package/dist/types/src/core/plugin-manifest.test.d.ts.map +1 -0
  128. package/dist/types/src/core/plugin.d.ts +176 -6
  129. package/dist/types/src/core/plugin.d.ts.map +1 -1
  130. package/dist/types/src/core/registry.d.ts +107 -0
  131. package/dist/types/src/core/registry.d.ts.map +1 -0
  132. package/dist/types/src/core/url-loader.d.ts +127 -0
  133. package/dist/types/src/core/url-loader.d.ts.map +1 -0
  134. package/dist/types/src/core/url-loader.test.d.ts +2 -0
  135. package/dist/types/src/core/url-loader.test.d.ts.map +1 -0
  136. package/dist/types/src/helpers.d.ts.map +1 -1
  137. package/dist/types/src/index.d.ts +1 -2
  138. package/dist/types/src/index.d.ts.map +1 -1
  139. package/dist/types/src/plugin-process-manager/ProcessManagerPlugin.d.ts +3 -0
  140. package/dist/types/src/plugin-process-manager/ProcessManagerPlugin.d.ts.map +1 -0
  141. package/dist/types/src/plugin-process-manager/history/capability.d.ts.map +1 -0
  142. package/dist/types/src/plugin-process-manager/history/errors.d.ts +32 -0
  143. package/dist/types/src/plugin-process-manager/history/errors.d.ts.map +1 -0
  144. package/dist/types/src/{plugin-operation → plugin-process-manager}/history/history-tracker.d.ts +1 -1
  145. package/dist/types/src/plugin-process-manager/history/history-tracker.d.ts.map +1 -0
  146. package/dist/types/src/plugin-process-manager/history/history-tracker.test.d.ts.map +1 -0
  147. package/dist/types/src/plugin-process-manager/history/index.d.ts.map +1 -0
  148. package/dist/types/src/{plugin-operation → plugin-process-manager}/history/types.d.ts +1 -1
  149. package/dist/types/src/plugin-process-manager/history/types.d.ts.map +1 -0
  150. package/dist/types/src/{plugin-operation → plugin-process-manager}/history/undo-mapping.d.ts +1 -1
  151. package/dist/types/src/plugin-process-manager/history/undo-mapping.d.ts.map +1 -0
  152. package/dist/types/src/{plugin-operation → plugin-process-manager}/history/undo-registry.d.ts +1 -1
  153. package/dist/types/src/plugin-process-manager/history/undo-registry.d.ts.map +1 -0
  154. package/dist/types/src/plugin-process-manager/history/undo-registry.test.d.ts.map +1 -0
  155. package/dist/types/src/plugin-process-manager/index.d.ts +3 -0
  156. package/dist/types/src/plugin-process-manager/index.d.ts.map +1 -0
  157. package/dist/types/src/plugin-process-manager/meta.d.ts.map +1 -0
  158. package/dist/types/src/plugin-process-manager/process-manager-capability.d.ts +8 -0
  159. package/dist/types/src/plugin-process-manager/process-manager-capability.d.ts.map +1 -0
  160. package/dist/types/src/plugin-process-manager/testing.d.ts +59 -0
  161. package/dist/types/src/plugin-process-manager/testing.d.ts.map +1 -0
  162. package/dist/types/src/testing/harness.d.ts +79 -0
  163. package/dist/types/src/testing/harness.d.ts.map +1 -0
  164. package/dist/types/src/testing/index.d.ts +1 -0
  165. package/dist/types/src/testing/index.d.ts.map +1 -1
  166. package/dist/types/src/testing/react.d.ts +27 -0
  167. package/dist/types/src/testing/react.d.ts.map +1 -0
  168. package/dist/types/src/testing/react.test.d.ts +2 -0
  169. package/dist/types/src/testing/react.test.d.ts.map +1 -0
  170. package/dist/types/src/testing/service.d.ts.map +1 -1
  171. package/dist/types/src/testing/withPluginManager.d.ts.map +1 -1
  172. package/dist/types/src/testing/withPluginManager.stories.d.ts.map +1 -1
  173. package/dist/types/src/ui/components/App/App.d.ts +3 -2
  174. package/dist/types/src/ui/components/App/App.d.ts.map +1 -1
  175. package/dist/types/src/ui/components/App/App.stories.d.ts +2 -2
  176. package/dist/types/src/ui/components/App/App.stories.d.ts.map +1 -1
  177. package/dist/types/src/ui/components/Placeholder/Placeholder.d.ts +64 -0
  178. package/dist/types/src/ui/components/Placeholder/Placeholder.d.ts.map +1 -0
  179. package/dist/types/src/ui/components/Placeholder/Placeholder.stories.d.ts +19 -0
  180. package/dist/types/src/ui/components/Placeholder/Placeholder.stories.d.ts.map +1 -0
  181. package/dist/types/src/ui/components/Placeholder/index.d.ts +2 -0
  182. package/dist/types/src/ui/components/Placeholder/index.d.ts.map +1 -0
  183. package/dist/types/src/ui/components/PluginManager/PluginManagerContext.stories.d.ts.map +1 -1
  184. package/dist/types/src/ui/components/Surface/SurfaceComponent.d.ts +16 -4
  185. package/dist/types/src/ui/components/Surface/SurfaceComponent.d.ts.map +1 -1
  186. package/dist/types/src/ui/components/Surface/SurfaceComponent.stories.d.ts.map +1 -1
  187. package/dist/types/src/ui/components/Surface/SurfaceInfo.d.ts.map +1 -1
  188. package/dist/types/src/ui/components/Surface/SurfaceProfilerContext.d.ts +48 -0
  189. package/dist/types/src/ui/components/Surface/SurfaceProfilerContext.d.ts.map +1 -0
  190. package/dist/types/src/ui/components/Surface/index.d.ts +22 -6
  191. package/dist/types/src/ui/components/Surface/index.d.ts.map +1 -1
  192. package/dist/types/src/ui/components/Surface/types.d.ts +110 -9
  193. package/dist/types/src/ui/components/Surface/types.d.ts.map +1 -1
  194. package/dist/types/src/ui/components/Surface/types.test.d.ts +2 -0
  195. package/dist/types/src/ui/components/Surface/types.test.d.ts.map +1 -0
  196. package/dist/types/src/ui/components/index.d.ts +1 -0
  197. package/dist/types/src/ui/components/index.d.ts.map +1 -1
  198. package/dist/types/src/ui/hooks/index.d.ts +1 -1
  199. package/dist/types/src/ui/hooks/index.d.ts.map +1 -1
  200. package/dist/types/src/ui/hooks/useApp.d.ts +47 -11
  201. package/dist/types/src/ui/hooks/useApp.d.ts.map +1 -1
  202. package/dist/types/src/ui/hooks/useCapabilities.d.ts.map +1 -1
  203. package/dist/types/src/ui/hooks/useLoading.d.ts.map +1 -1
  204. package/dist/types/src/ui/hooks/useProcessManagerRuntime.d.ts +24 -0
  205. package/dist/types/src/ui/hooks/useProcessManagerRuntime.d.ts.map +1 -0
  206. package/dist/types/src/ui/hooks/useSettingsState.d.ts.map +1 -1
  207. package/dist/types/src/vite-plugin/boot-loader/BootLoader.stories.d.ts +34 -0
  208. package/dist/types/src/vite-plugin/boot-loader/BootLoader.stories.d.ts.map +1 -0
  209. package/dist/types/src/vite-plugin/boot-loader/index.d.ts +2 -0
  210. package/dist/types/src/vite-plugin/boot-loader/index.d.ts.map +1 -0
  211. package/dist/types/src/vite-plugin/boot-loader/loader.d.ts +51 -0
  212. package/dist/types/src/vite-plugin/boot-loader/loader.d.ts.map +1 -0
  213. package/dist/types/src/vite-plugin/composer/index.d.ts +34 -0
  214. package/dist/types/src/vite-plugin/composer/index.d.ts.map +1 -0
  215. package/dist/types/src/vite-plugin/import-map/index.d.ts +28 -0
  216. package/dist/types/src/vite-plugin/import-map/index.d.ts.map +1 -0
  217. package/dist/types/src/vite-plugin/index.d.ts +5 -0
  218. package/dist/types/src/vite-plugin/index.d.ts.map +1 -0
  219. package/dist/types/src/vite-plugin/manifest.d.ts +41 -0
  220. package/dist/types/src/vite-plugin/manifest.d.ts.map +1 -0
  221. package/dist/types/src/vite-plugin/manifest.test.d.ts +2 -0
  222. package/dist/types/src/vite-plugin/manifest.test.d.ts.map +1 -0
  223. package/dist/types/src/vite-plugin/packages.d.ts +13 -0
  224. package/dist/types/src/vite-plugin/packages.d.ts.map +1 -0
  225. package/dist/types/tsconfig.tsbuildinfo +1 -1
  226. package/moon.yml +15 -0
  227. package/package.json +53 -54
  228. package/src/cli/cli.ts +2 -7
  229. package/src/common/activation-events.ts +10 -15
  230. package/src/common/annotations.ts +3 -0
  231. package/src/common/capabilities.ts +147 -16
  232. package/src/common/operations.ts +5 -8
  233. package/src/core/activation-event.ts +4 -1
  234. package/src/core/capability-manager.test.ts +1 -1
  235. package/src/core/capability-manager.ts +22 -1
  236. package/src/core/capability.ts +20 -2
  237. package/src/core/edge-registry-plugin-provider.ts +92 -0
  238. package/src/core/index.ts +6 -0
  239. package/src/core/plugin-asset-cache.ts +60 -0
  240. package/src/core/plugin-manager.test.ts +816 -10
  241. package/src/core/plugin-manager.ts +865 -46
  242. package/src/core/plugin-manifest.test.ts +75 -0
  243. package/src/core/plugin-manifest.ts +134 -0
  244. package/src/core/plugin.ts +185 -10
  245. package/src/core/registry.ts +163 -0
  246. package/src/core/url-loader.test.ts +221 -0
  247. package/src/core/url-loader.ts +388 -0
  248. package/src/index.ts +1 -2
  249. package/src/plugin-process-manager/ProcessManagerPlugin.ts +24 -0
  250. package/src/{plugin-operation → plugin-process-manager}/history/capability.ts +1 -2
  251. package/src/plugin-process-manager/history/errors.ts +7 -0
  252. package/src/{plugin-operation → plugin-process-manager}/history/history-tracker.test.ts +37 -43
  253. package/src/{plugin-operation → plugin-process-manager}/history/history-tracker.ts +1 -2
  254. package/src/{plugin-operation → plugin-process-manager}/history/types.ts +1 -1
  255. package/src/{plugin-operation → plugin-process-manager}/history/undo-mapping.ts +1 -1
  256. package/src/{plugin-operation → plugin-process-manager}/history/undo-registry.test.ts +3 -4
  257. package/src/{plugin-operation → plugin-process-manager}/history/undo-registry.ts +1 -1
  258. package/src/{plugin-operation → plugin-process-manager}/index.ts +1 -1
  259. package/src/plugin-process-manager/meta.ts +14 -0
  260. package/src/plugin-process-manager/process-manager-capability.ts +178 -0
  261. package/src/{plugin-operation → plugin-process-manager}/testing.ts +26 -45
  262. package/src/testing/harness.ts +247 -0
  263. package/src/testing/index.ts +1 -0
  264. package/src/testing/react.test.tsx +48 -0
  265. package/src/testing/react.tsx +113 -0
  266. package/src/testing/service.ts +2 -2
  267. package/src/testing/withPluginManager.stories.tsx +1 -2
  268. package/src/testing/withPluginManager.tsx +5 -4
  269. package/src/ui/components/App/App.stories.tsx +5 -11
  270. package/src/ui/components/App/App.tsx +29 -5
  271. package/src/ui/components/Placeholder/Placeholder.stories.tsx +77 -0
  272. package/src/ui/components/Placeholder/Placeholder.tsx +155 -0
  273. package/src/{plugin-runtime → ui/components/Placeholder}/index.ts +1 -1
  274. package/src/ui/components/PluginManager/PluginManagerContext.stories.tsx +5 -4
  275. package/src/ui/components/Surface/SurfaceComponent.stories.tsx +16 -15
  276. package/src/ui/components/Surface/SurfaceComponent.tsx +111 -55
  277. package/src/ui/components/Surface/SurfaceInfo.tsx +0 -1
  278. package/src/ui/components/Surface/SurfaceProfilerContext.tsx +207 -0
  279. package/src/ui/components/Surface/index.ts +35 -1
  280. package/src/ui/components/Surface/types.test.ts +126 -0
  281. package/src/ui/components/Surface/types.ts +164 -12
  282. package/src/ui/components/index.ts +1 -0
  283. package/src/ui/hooks/index.ts +1 -1
  284. package/src/ui/hooks/useApp.test.tsx +2 -2
  285. package/src/ui/hooks/useApp.tsx +216 -17
  286. package/src/ui/hooks/useLoading.tsx +14 -6
  287. package/src/ui/hooks/useProcessManagerRuntime.ts +68 -0
  288. package/src/vite-plugin/boot-loader/BootLoader.stories.tsx +270 -0
  289. package/src/vite-plugin/boot-loader/boot-loader.css +320 -0
  290. package/src/vite-plugin/boot-loader/boot-loader.js +325 -0
  291. package/src/vite-plugin/boot-loader/index.ts +5 -0
  292. package/src/vite-plugin/boot-loader/loader.ts +123 -0
  293. package/src/vite-plugin/composer/index.ts +306 -0
  294. package/src/vite-plugin/import-map/index.ts +527 -0
  295. package/src/vite-plugin/index.ts +10 -0
  296. package/src/vite-plugin/manifest.test.ts +46 -0
  297. package/src/vite-plugin/manifest.ts +57 -0
  298. package/src/vite-plugin/packages.ts +187 -0
  299. package/tsconfig.json +25 -1
  300. package/tsconfig.node.json +1 -1
  301. package/.swc/plugins/linux_x86_64_19.0.0/727453fb3a62f7f1d952a41e051ca8a6f88cadc45cee43c6a4d1aa45f9b75665.wasmer-v7 +0 -0
  302. package/dist/lib/browser/capability-5OFLR7J4.mjs.map +0 -7
  303. package/dist/lib/browser/capability-ZHQDZRP5.mjs +0 -37
  304. package/dist/lib/browser/capability-ZHQDZRP5.mjs.map +0 -7
  305. package/dist/lib/browser/chunk-272IPLHQ.mjs.map +0 -7
  306. package/dist/lib/browser/chunk-3VXJONTI.mjs +0 -933
  307. package/dist/lib/browser/chunk-3VXJONTI.mjs.map +0 -7
  308. package/dist/lib/browser/chunk-7AL6SKIV.mjs +0 -728
  309. package/dist/lib/browser/chunk-7AL6SKIV.mjs.map +0 -7
  310. package/dist/lib/browser/chunk-GH3M2LIW.mjs.map +0 -7
  311. package/dist/lib/browser/chunk-JGWCBVKJ.mjs.map +0 -7
  312. package/dist/lib/browser/chunk-M5IC326L.mjs +0 -34
  313. package/dist/lib/browser/chunk-M5IC326L.mjs.map +0 -7
  314. package/dist/lib/browser/chunk-WEBSGU5L.mjs.map +0 -7
  315. package/dist/lib/browser/chunk-YAFEA4GV.mjs +0 -1
  316. package/dist/lib/browser/chunk-YNFPIQGB.mjs.map +0 -7
  317. package/dist/lib/browser/invoker-capability-YTTQ2OBB.mjs +0 -36
  318. package/dist/lib/browser/invoker-capability-YTTQ2OBB.mjs.map +0 -7
  319. package/dist/lib/node-esm/capability-CRHZKL6T.mjs.map +0 -7
  320. package/dist/lib/node-esm/capability-W5C5464H.mjs +0 -38
  321. package/dist/lib/node-esm/capability-W5C5464H.mjs.map +0 -7
  322. package/dist/lib/node-esm/chunk-3D66SZHP.mjs.map +0 -7
  323. package/dist/lib/node-esm/chunk-6REV5DE7.mjs.map +0 -7
  324. package/dist/lib/node-esm/chunk-CMDON4NG.mjs +0 -934
  325. package/dist/lib/node-esm/chunk-CMDON4NG.mjs.map +0 -7
  326. package/dist/lib/node-esm/chunk-I7FZT4A7.mjs +0 -729
  327. package/dist/lib/node-esm/chunk-I7FZT4A7.mjs.map +0 -7
  328. package/dist/lib/node-esm/chunk-LHCJGNXK.mjs.map +0 -7
  329. package/dist/lib/node-esm/chunk-NHXBSAQR.mjs +0 -35
  330. package/dist/lib/node-esm/chunk-NHXBSAQR.mjs.map +0 -7
  331. package/dist/lib/node-esm/chunk-SB5ODNPX.mjs.map +0 -7
  332. package/dist/lib/node-esm/chunk-SQICGJBW.mjs.map +0 -7
  333. package/dist/lib/node-esm/chunk-Z4TJPSMP.mjs +0 -2
  334. package/dist/lib/node-esm/invoker-capability-BU26474T.mjs +0 -37
  335. package/dist/lib/node-esm/invoker-capability-BU26474T.mjs.map +0 -7
  336. package/dist/types/src/plugin-operation/OperationPlugin.d.ts +0 -3
  337. package/dist/types/src/plugin-operation/OperationPlugin.d.ts.map +0 -1
  338. package/dist/types/src/plugin-operation/history/capability.d.ts.map +0 -1
  339. package/dist/types/src/plugin-operation/history/errors.d.ts +0 -5
  340. package/dist/types/src/plugin-operation/history/errors.d.ts.map +0 -1
  341. package/dist/types/src/plugin-operation/history/history-tracker.d.ts.map +0 -1
  342. package/dist/types/src/plugin-operation/history/history-tracker.test.d.ts.map +0 -1
  343. package/dist/types/src/plugin-operation/history/index.d.ts.map +0 -1
  344. package/dist/types/src/plugin-operation/history/types.d.ts.map +0 -1
  345. package/dist/types/src/plugin-operation/history/undo-mapping.d.ts.map +0 -1
  346. package/dist/types/src/plugin-operation/history/undo-registry.d.ts.map +0 -1
  347. package/dist/types/src/plugin-operation/history/undo-registry.test.d.ts.map +0 -1
  348. package/dist/types/src/plugin-operation/index.d.ts +0 -3
  349. package/dist/types/src/plugin-operation/index.d.ts.map +0 -1
  350. package/dist/types/src/plugin-operation/invoker-capability.d.ts +0 -6
  351. package/dist/types/src/plugin-operation/invoker-capability.d.ts.map +0 -1
  352. package/dist/types/src/plugin-operation/meta.d.ts.map +0 -1
  353. package/dist/types/src/plugin-operation/testing.d.ts +0 -109
  354. package/dist/types/src/plugin-operation/testing.d.ts.map +0 -1
  355. package/dist/types/src/plugin-runtime/RuntimePlugin.d.ts +0 -3
  356. package/dist/types/src/plugin-runtime/RuntimePlugin.d.ts.map +0 -1
  357. package/dist/types/src/plugin-runtime/capability.d.ts +0 -6
  358. package/dist/types/src/plugin-runtime/capability.d.ts.map +0 -1
  359. package/dist/types/src/plugin-runtime/index.d.ts +0 -2
  360. package/dist/types/src/plugin-runtime/index.d.ts.map +0 -1
  361. package/dist/types/src/plugin-runtime/meta.d.ts +0 -3
  362. package/dist/types/src/plugin-runtime/meta.d.ts.map +0 -1
  363. package/dist/types/src/ui/hooks/useOperationResolver.d.ts +0 -19
  364. package/dist/types/src/ui/hooks/useOperationResolver.d.ts.map +0 -1
  365. package/src/plugin-operation/OperationPlugin.ts +0 -25
  366. package/src/plugin-operation/history/errors.ts +0 -11
  367. package/src/plugin-operation/invoker-capability.ts +0 -40
  368. package/src/plugin-operation/meta.ts +0 -11
  369. package/src/plugin-runtime/RuntimePlugin.ts +0 -20
  370. package/src/plugin-runtime/capability.ts +0 -53
  371. package/src/plugin-runtime/meta.ts +0 -11
  372. package/src/ui/hooks/useOperationResolver.ts +0 -40
  373. /package/dist/lib/browser/{chunk-YAFEA4GV.mjs.map → core/url-loader.mjs.map} +0 -0
  374. /package/dist/lib/node-esm/{chunk-Z4TJPSMP.mjs.map → core/url-loader.mjs.map} +0 -0
  375. /package/dist/types/src/{plugin-operation → plugin-process-manager}/history/capability.d.ts +0 -0
  376. /package/dist/types/src/{plugin-operation → plugin-process-manager}/history/history-tracker.test.d.ts +0 -0
  377. /package/dist/types/src/{plugin-operation → plugin-process-manager}/history/index.d.ts +0 -0
  378. /package/dist/types/src/{plugin-operation → plugin-process-manager}/history/undo-registry.test.d.ts +0 -0
  379. /package/dist/types/src/{plugin-operation → plugin-process-manager}/meta.d.ts +0 -0
  380. /package/src/{plugin-operation → plugin-process-manager}/history/index.ts +0 -0
@@ -16,12 +16,64 @@ import * as Ref from 'effect/Ref';
16
16
 
17
17
  import { runAndForwardErrors } from '@dxos/effect';
18
18
  import { Performance } from '@dxos/effect';
19
+ import { BaseError } from '@dxos/errors';
19
20
  import { log } from '@dxos/log';
20
21
 
21
22
  import * as ActivationEvent from './activation-event';
22
23
  import * as Capability from './capability';
23
24
  import * as CapabilityManager from './capability-manager';
24
25
  import * as Plugin from './plugin';
26
+ // Imported with a `PluginRegistry` alias because the unrelated `@effect-atom/atom-react`
27
+ // `Registry` is already imported above; from outside this file the namespace is
28
+ // re-exported as `Registry` via `./index.ts`.
29
+ import * as PluginRegistry from './registry';
30
+
31
+ /**
32
+ * Tagged error for failures during the constructor-launched core/enabled
33
+ * `enable()` chain. Surfaces via {@link PluginManager.activate}'s wait on
34
+ * `_initialization` so a caller blocked on initialization gets a typed
35
+ * failure (with the original error preserved as `cause`) instead of an
36
+ * untyped `Error`.
37
+ */
38
+ export class PluginInitializationError extends BaseError.extend(
39
+ 'PluginInitializationError',
40
+ 'Plugin manager initialization failed',
41
+ ) {}
42
+
43
+ /**
44
+ * Tagged error raised when a plugin exceeds its configured load or activation
45
+ * timeout. The plugin manager records the failure on the `failed` atom and
46
+ * auto-disables the plugin so that one stuck remote does not stall app boot.
47
+ * `context.id` is the plugin id, `context.phase` is `'load'` or `'activation'`.
48
+ */
49
+ export class PluginTimeoutError extends BaseError.extend('PluginTimeoutError', 'Plugin operation timed out') {}
50
+
51
+ /** Phase of the plugin lifecycle in which the failure was observed. */
52
+ export type PluginFailurePhase = 'load' | 'activation';
53
+
54
+ /** Why the plugin entered a failed state. */
55
+ export type PluginFailureReason = 'timeout' | 'error';
56
+
57
+ /**
58
+ * Record of a plugin that failed to load or activate. Surfaced via the
59
+ * {@link PluginManager.failed} atom so registry / UI consumers can flag
60
+ * unhealthy plugins (e.g. a remote host that has gone offline) rather than
61
+ * leaving the app in a half-broken state.
62
+ */
63
+ export type PluginFailure = {
64
+ readonly id: string;
65
+ readonly phase: PluginFailurePhase;
66
+ readonly reason: PluginFailureReason;
67
+ readonly error: Error;
68
+ /** `Date.now()` when the failure was recorded. */
69
+ readonly timestamp: number;
70
+ };
71
+
72
+ /** Default deadline for resolving a lazy plugin's dynamic import. */
73
+ const DEFAULT_LOAD_TIMEOUT = Duration.seconds(30);
74
+
75
+ /** Default deadline for a single module's `activate()` body. */
76
+ const DEFAULT_ACTIVATION_TIMEOUT = Duration.seconds(30);
25
77
 
26
78
  /**
27
79
  * Identifier denoting a Manager.
@@ -29,15 +81,62 @@ import * as Plugin from './plugin';
29
81
  export const ManagerTypeId: unique symbol = Symbol.for('@dxos/app-framework/Manager');
30
82
  export type ManagerTypeId = typeof ManagerTypeId;
31
83
 
84
+ /**
85
+ * Loader result that carries optional metadata about how the plugin was sourced.
86
+ *
87
+ * `dev: true` marks a plugin as session-only and triggers shadow-on-id-collision
88
+ * inside the manager: if a plugin with the same id is already registered (a
89
+ * builtin, or a previously-installed plugin from the registry), the dev plugin
90
+ * temporarily takes over that id slot. The original is restored when the dev
91
+ * plugin is removed (or on page reload, since dev plugins aren't persisted).
92
+ */
93
+ export type LoadedPlugin = {
94
+ plugin: Plugin.Plugin;
95
+ /** True when the plugin came from a dev source. See type doc for semantics. */
96
+ dev?: boolean;
97
+ };
98
+
32
99
  export type ManagerOptions = {
33
- pluginLoader: (id: string) => Effect.Effect<Plugin.Plugin, Error>;
100
+ pluginLoader: (id: string) => Effect.Effect<LoadedPlugin, Error>;
34
101
  plugins?: Plugin.Plugin[];
35
- core?: string[];
36
102
  enabled?: string[];
37
103
  registry?: Registry.Registry;
104
+ /**
105
+ * Backend for the plugin registry catalog. When omitted the manager exposes a
106
+ * no-op `pluginRegistry` (empty list, no versions endpoint). Implementations
107
+ * live in app-framework alongside the interface (e.g.
108
+ * `EdgeRegistryPluginProvider`); the host app instantiates one and passes it in.
109
+ */
110
+ pluginRegistryProvider?: PluginRegistry.PluginProvider;
111
+ /**
112
+ * Hook called when a plugin is removed via {@link PluginManager.remove}. Used by the
113
+ * host app to clean up persisted state (e.g. evict offline-cached plugin assets).
114
+ * Failures are logged and swallowed; removal still succeeds even if the hook fails.
115
+ */
116
+ onRemove?: (id: string) => Effect.Effect<void, unknown>;
117
+ /**
118
+ * Maximum time allowed for a lazy plugin's dynamic `import()` to resolve.
119
+ * Plugins that exceed this are flagged on the {@link PluginManager.failed}
120
+ * atom and auto-disabled so a stuck remote host can't stall app boot.
121
+ * Defaults to 30 seconds; pass `Duration.infinity` to disable.
122
+ */
123
+ loadTimeout?: Duration.DurationInput;
124
+ /**
125
+ * Maximum time allowed for a single module's `activate()` Effect to settle.
126
+ * Modules that exceed this fail with {@link PluginTimeoutError}; the owning
127
+ * plugin is recorded on `failed` and auto-disabled. Defaults to 30 seconds;
128
+ * pass `Duration.infinity` to disable.
129
+ */
130
+ activationTimeout?: Duration.DurationInput;
38
131
  };
39
132
 
40
- type ActivationMessage = { event: string; state: 'activating' | 'activated' | 'error'; error?: Error };
133
+ export type ActivationMessage = {
134
+ event: string;
135
+ state: 'activating' | 'activated' | 'error';
136
+ /** Module ID when the message pertains to a specific module activation. */
137
+ module?: string;
138
+ error?: Error;
139
+ };
41
140
 
42
141
  /**
43
142
  * Interface for the Plugin Manager.
@@ -47,6 +146,13 @@ export interface PluginManager {
47
146
  readonly activation: PubSub.PubSub<ActivationMessage>;
48
147
  readonly capabilities: CapabilityManager.CapabilityManager;
49
148
  readonly registry: Registry.Registry;
149
+ /**
150
+ * Cached registry catalog state plus pass-throughs for `listVersions` /
151
+ * `getPlugin`. Always present — the host supplies a `pluginRegistryProvider`
152
+ * via {@link ManagerOptions} for real backends, or it falls back to a no-op
153
+ * implementation that yields an empty catalog.
154
+ */
155
+ readonly pluginRegistry: PluginRegistry.Manager;
50
156
 
51
157
  readonly plugins: Atom.Atom<readonly Plugin.Plugin[]>;
52
158
  readonly core: Atom.Atom<readonly string[]>;
@@ -55,6 +161,19 @@ export interface PluginManager {
55
161
  readonly active: Atom.Atom<readonly string[]>;
56
162
  readonly eventsFired: Atom.Atom<readonly string[]>;
57
163
  readonly pendingReset: Atom.Atom<readonly string[]>;
164
+ /**
165
+ * Plugins that failed to load or activate. Subscribers (e.g. the registry
166
+ * UI) can use this to flag unhealthy entries; a plugin id appears here at
167
+ * most once with its most recent failure.
168
+ */
169
+ readonly failed: Atom.Atom<readonly PluginFailure[]>;
170
+ /**
171
+ * Ids of currently-registered plugins that came from a dev source (loaded
172
+ * via {@link LoadedPlugin} with `dev: true`). Subscribers can use this to
173
+ * badge dev-overridden plugins or to derive the id of the active dev plugin
174
+ * for an "uninstall dev plugin" affordance.
175
+ */
176
+ readonly devPluginIds: Atom.Atom<readonly string[]>;
58
177
 
59
178
  getPlugins(): readonly Plugin.Plugin[];
60
179
  getCore(): readonly string[];
@@ -63,11 +182,77 @@ export interface PluginManager {
63
182
  getActive(): readonly string[];
64
183
  getEventsFired(): readonly string[];
65
184
  getPendingReset(): readonly string[];
185
+ getFailed(): readonly PluginFailure[];
186
+ getDevPluginIds(): readonly string[];
187
+
188
+ /**
189
+ * Clears the failure record for a plugin so it can be retried. Returns
190
+ * whether a failure record existed and was removed.
191
+ */
192
+ clearFailure(id: string): boolean;
66
193
 
67
- add(id: string): Effect.Effect<boolean, Error>;
68
- enable(id: string): Effect.Effect<boolean, Error>;
69
- remove(id: string): boolean;
70
- disable(id: string): Effect.Effect<boolean, Error>;
194
+ /**
195
+ * Loads a plugin via the plugin loader and registers it without enabling it.
196
+ * Returns the loaded plugin so callers can enable it by its canonical id
197
+ * (which may differ from the locator used to load it, e.g. URL loaders).
198
+ */
199
+ add(id: string): Effect.Effect<Plugin.Plugin, Error>;
200
+
201
+ /**
202
+ * Enables a plugin.
203
+ *
204
+ * Default behavior auto-resolves the plugin's declared `dependsOn` closure:
205
+ * missing entries that exist in the plugin registry catalog are installed via
206
+ * {@link add}, then enabled in dependency-first order. Set `resolveDependencies`
207
+ * to `false` to enable only the named plugin and skip the closure walk
208
+ * entirely — useful when substituting an alternative plugin that satisfies
209
+ * the dependent's capability needs in its own way.
210
+ */
211
+ enable(id: string, opts?: { resolveDependencies?: boolean }): Effect.Effect<boolean, Error>;
212
+
213
+ /**
214
+ * Removes a plugin from the manager (disables then unregisters).
215
+ *
216
+ * Honors the same cascade option as {@link disable}.
217
+ */
218
+ remove(id: string, opts?: { cascade?: boolean }): Effect.Effect<boolean, Error>;
219
+
220
+ /**
221
+ * Disables a plugin.
222
+ *
223
+ * By default, cascades to currently-enabled dependents (transitively, leaves
224
+ * first) so disabling a depended-upon plugin never leaves its dependents
225
+ * stranded. Pass `cascade: false` to disable only the named plugin and leave
226
+ * its dependents enabled-but-broken — VS Code-style disable parity for
227
+ * callers that want the escape hatch (e.g. when swapping in an alternative
228
+ * implementation that satisfies the dependents' needs in its own way).
229
+ *
230
+ * Fails with {@link Plugin.PluginDependencyError} (`reason: 'core-dependent'`)
231
+ * when cascading would require disabling a core plugin; UI flows should
232
+ * surface their own confirmation before calling `disable` with the default.
233
+ */
234
+ disable(id: string, opts?: { cascade?: boolean }): Effect.Effect<boolean, Error>;
235
+
236
+ /**
237
+ * Returns the plugin ids that the given plugin declares as dependencies.
238
+ *
239
+ * Walks `meta.dependsOn` from both registered plugins and the plugin registry
240
+ * catalog so callers can preview the closure for a plugin that isn't yet
241
+ * installed. With `transitive: true` (default), returns the full dependency
242
+ * closure in dependency-first order (deps before dependents). Without it,
243
+ * returns the direct declarations only.
244
+ */
245
+ getDependencies(id: string, opts?: { transitive?: boolean }): readonly string[];
246
+
247
+ /**
248
+ * Returns the plugin ids that declare the given plugin as a dependency.
249
+ *
250
+ * Walks `meta.dependsOn` over registered plugins. With `transitive: true`
251
+ * (default), returns the full reverse closure. With `enabledOnly: true`,
252
+ * filters to currently-enabled dependents — used by UI flows to preview what
253
+ * a cascading disable would touch.
254
+ */
255
+ getDependents(id: string, opts?: { transitive?: boolean; enabledOnly?: boolean }): readonly string[];
71
256
  // TODO(wittjosiah): Improve error typing.
72
257
  activate(
73
258
  event: ActivationEvent.ActivationEvent | string,
@@ -99,6 +284,7 @@ class ManagerImpl implements PluginManager {
99
284
  readonly activation = Effect.runSync(PubSub.unbounded<ActivationMessage>());
100
285
  readonly capabilities: CapabilityManager.CapabilityManager;
101
286
  readonly registry: Registry.Registry;
287
+ readonly pluginRegistry: PluginRegistry.Manager;
102
288
 
103
289
  private readonly _pluginsAtom: Atom.Writable<Plugin.Plugin[]>;
104
290
  private readonly _coreAtom: Atom.Writable<string[]>;
@@ -107,29 +293,66 @@ class ManagerImpl implements PluginManager {
107
293
  private readonly _activeAtom: Atom.Writable<string[]>;
108
294
  private readonly _eventsFiredAtom: Atom.Writable<string[]>;
109
295
  private readonly _pendingResetAtom: Atom.Writable<string[]>;
296
+ private readonly _failedAtom: Atom.Writable<PluginFailure[]>;
110
297
  private readonly _pluginLoader: ManagerOptions['pluginLoader'];
298
+ private readonly _onRemove: ManagerOptions['onRemove'];
299
+ private readonly _loadTimeout: Duration.DurationInput;
300
+ private readonly _activationTimeout: Duration.DurationInput;
111
301
  private readonly _capabilities = new Map<string, Capability.Any[]>();
112
302
  private readonly _moduleMemoMap = new Map<Plugin.PluginModule['id'], Deferred.Deferred<Capability.Any[], Error>>();
113
303
  private readonly _moduleSemaphores = new Map<Plugin.PluginModule['id'], Effect.Semaphore>();
304
+ // Coalesces concurrent `_resolveLazyPlugin` calls per plugin id. Without
305
+ // this, two callers entering `enable(id)` before the swap completes would
306
+ // each invoke `mod.default(options)` and produce distinct module objects,
307
+ // defeating `_addModule`'s reference-equality dedupe and racing the
308
+ // `_pluginsAtom` swap.
309
+ private readonly _resolvingPlugins = new Map<string, Deferred.Deferred<Plugin.Plugin, Plugin.LazyPluginError>>();
310
+ // Tracks dev-source plugins (loaded via a Vite dev server) keyed by id.
311
+ // When `shadow` is present, the entry has displaced an existing plugin —
312
+ // `remove` reinstates it and re-enables iff `wasEnabled`. Entries without a
313
+ // shadow are dev plugins with no underlying registry/builtin to restore.
314
+ // The atom mirrors the map's keys for UI subscribers (they don't need the
315
+ // shadow internals); the two stay in sync via {@link _markDev}/{@link _unmarkDev}.
316
+ private readonly _devPlugins = new Map<string, { shadow?: { plugin: Plugin.Plugin; wasEnabled: boolean } }>();
317
+ private readonly _devPluginIdsAtom: Atom.Writable<string[]>;
114
318
  private readonly _activatingEvents = Effect.runSync(Ref.make<string[]>([]));
115
319
  private readonly _activatingModules = Effect.runSync(Ref.make<string[]>([]));
116
320
  private readonly _inFlightFibers = Effect.runSync(Ref.make<Array<Fiber.Fiber<unknown, unknown>>>([]));
117
321
  private readonly _shutdownSemaphore = Effect.runSync(Effect.makeSemaphore(1));
118
322
  private readonly _shuttingDown = Effect.runSync(Ref.make(false));
323
+ // Tracks the constructor-launched core/enabled `enable()` calls so that
324
+ // `activate` can wait for module registration before dispatching events.
325
+ // Lazy plugins make `enable` asynchronous (a dynamic `import()` happens
326
+ // inside it), so without this synchronization an `activate` triggered
327
+ // immediately after `make` could fire on an empty module set. Failures
328
+ // are wrapped in `PluginInitializationError` so awaiters get a tagged
329
+ // error rather than the wide `Error` produced by the underlying chain.
330
+ private readonly _initialization = Effect.runSync(Deferred.make<void, PluginInitializationError>());
119
331
 
120
332
  constructor({
121
333
  pluginLoader,
122
334
  plugins = [],
123
- core = plugins.map(({ meta }) => meta.id),
124
335
  enabled = [],
125
336
  registry,
337
+ pluginRegistryProvider,
338
+ onRemove,
339
+ loadTimeout = DEFAULT_LOAD_TIMEOUT,
340
+ activationTimeout = DEFAULT_ACTIVATION_TIMEOUT,
126
341
  }: ManagerOptions) {
342
+ // Core plugins are derived from `meta.tags.includes('system')`; the set is
343
+ // a snapshot of the initial `plugins` array (later `add()` calls do not
344
+ // promote plugins to core).
345
+ const core = plugins.filter(({ meta }) => meta.tags?.includes('system')).map(({ meta }) => meta.id);
127
346
  this.registry = registry ?? Registry.make();
128
347
  this.capabilities = CapabilityManager.make({
129
348
  registry: this.registry,
130
349
  });
350
+ this.pluginRegistry = new PluginRegistry.Manager(pluginRegistryProvider, this.registry);
131
351
 
132
352
  this._pluginLoader = pluginLoader;
353
+ this._onRemove = onRemove;
354
+ this._loadTimeout = loadTimeout;
355
+ this._activationTimeout = activationTimeout;
133
356
  this._pluginsAtom = Atom.make(plugins).pipe(Atom.keepAlive);
134
357
  this._coreAtom = Atom.make(core).pipe(Atom.keepAlive);
135
358
  this._enabledAtom = Atom.make(enabled).pipe(Atom.keepAlive);
@@ -137,8 +360,22 @@ class ManagerImpl implements PluginManager {
137
360
  this._activeAtom = Atom.make<string[]>([]).pipe(Atom.keepAlive);
138
361
  this._eventsFiredAtom = Atom.make<string[]>([]).pipe(Atom.keepAlive);
139
362
  this._pendingResetAtom = Atom.make<string[]>([]).pipe(Atom.keepAlive);
363
+ this._failedAtom = Atom.make<PluginFailure[]>([]).pipe(Atom.keepAlive);
364
+ this._devPluginIdsAtom = Atom.make<string[]>([]).pipe(Atom.keepAlive);
140
365
  plugins.forEach((plugin) => this._addPlugin(plugin));
141
- void Effect.all([...core, ...enabled].map((id) => this.enable(id))).pipe(runAndForwardErrors);
366
+ // Dedupe before mapping to `enable` — `core` and `enabled` may overlap (an
367
+ // app-supplied plugin can be in both), and concurrent `enable(id)` calls
368
+ // for the same id are not idempotent (each would re-run the lazy resolve
369
+ // and double-register modules). `new Set([...])` preserves first-seen
370
+ // order which matches the natural core-before-enabled precedence.
371
+ const initialIds = [...new Set([...core, ...enabled])];
372
+ void Effect.all(initialIds.map((id) => this.enable(id)))
373
+ .pipe(
374
+ Effect.mapError((cause) => new PluginInitializationError({ cause })),
375
+ Effect.tap(() => Deferred.succeed(this._initialization, undefined)),
376
+ Effect.tapErrorCause((cause) => Deferred.failCause(this._initialization, cause)),
377
+ )
378
+ .pipe(runAndForwardErrors);
142
379
  }
143
380
 
144
381
  get plugins(): Atom.Atom<readonly Plugin.Plugin[]> {
@@ -184,6 +421,20 @@ class ManagerImpl implements PluginManager {
184
421
  return this._pendingResetAtom;
185
422
  }
186
423
 
424
+ /**
425
+ * Plugins that failed to load or activate.
426
+ */
427
+ get failed(): Atom.Atom<readonly PluginFailure[]> {
428
+ return this._failedAtom;
429
+ }
430
+
431
+ /**
432
+ * Ids of currently-registered plugins that came from a dev source.
433
+ */
434
+ get devPluginIds(): Atom.Atom<readonly string[]> {
435
+ return this._devPluginIdsAtom;
436
+ }
437
+
187
438
  getPlugins(): readonly Plugin.Plugin[] {
188
439
  return this._get(this._pluginsAtom);
189
440
  }
@@ -212,31 +463,229 @@ class ManagerImpl implements PluginManager {
212
463
  return this._get(this._pendingResetAtom);
213
464
  }
214
465
 
466
+ getFailed(): readonly PluginFailure[] {
467
+ return this._get(this._failedAtom);
468
+ }
469
+
470
+ getDevPluginIds(): readonly string[] {
471
+ return this._get(this._devPluginIdsAtom);
472
+ }
473
+
474
+ /**
475
+ * Marks `id` as dev-sourced. If the plugin displaced an existing one, pass
476
+ * the shadow snapshot so `remove` can restore it. Repeat calls (e.g. a dev
477
+ * plugin reload) preserve the original shadow target — restoration always
478
+ * unwinds back to the real underlying plugin, never an intermediate dev build.
479
+ */
480
+ private _markDev(id: string, shadow?: { plugin: Plugin.Plugin; wasEnabled: boolean }): void {
481
+ if (this._devPlugins.has(id)) {
482
+ return;
483
+ }
484
+ this._devPlugins.set(id, { shadow });
485
+ this._update(this._devPluginIdsAtom, (ids) => (ids.includes(id) ? ids : [...ids, id]));
486
+ }
487
+
488
+ /** Drops the dev-plugin entry and returns its shadow data (if any) for restoration. */
489
+ private _unmarkDev(id: string): { plugin: Plugin.Plugin; wasEnabled: boolean } | undefined {
490
+ const entry = this._devPlugins.get(id);
491
+ this._devPlugins.delete(id);
492
+ this._update(this._devPluginIdsAtom, (ids) => ids.filter((existing) => existing !== id));
493
+ return entry?.shadow;
494
+ }
495
+
496
+ getDependencies(id: string, opts?: { transitive?: boolean }): readonly string[] {
497
+ const transitive = opts?.transitive !== false;
498
+ if (!transitive) {
499
+ return this._directDependencies(id);
500
+ }
501
+ const walk = this._computeDependencyClosure(id);
502
+ // Drop the target itself; callers asked for its dependencies, not the
503
+ // closure including the root.
504
+ return walk.order.filter((depId) => depId !== id);
505
+ }
506
+
507
+ getDependents(id: string, opts?: { transitive?: boolean; enabledOnly?: boolean }): readonly string[] {
508
+ return this._collectDependents(id, {
509
+ transitive: opts?.transitive !== false,
510
+ enabledOnly: opts?.enabledOnly === true,
511
+ });
512
+ }
513
+
514
+ clearFailure(id: string): boolean {
515
+ const current = this._get(this._failedAtom);
516
+ if (!current.some((failure) => failure.id === id)) {
517
+ return false;
518
+ }
519
+ this._set(
520
+ this._failedAtom,
521
+ current.filter((failure) => failure.id !== id),
522
+ );
523
+ return true;
524
+ }
525
+
215
526
  /**
216
527
  * Adds a plugin to the manager via the plugin loader.
528
+ * The plugin is registered but not enabled; call `enable` separately to activate it.
217
529
  * @param id The id of the plugin.
218
530
  */
219
- add(id: string): Effect.Effect<boolean, Error> {
531
+ add(id: string): Effect.Effect<Plugin.Plugin, Error> {
220
532
  return Effect.gen(this, function* () {
221
533
  log('add plugin', { id });
222
- const plugin = yield* this._pluginLoader(id);
223
- this._addPlugin(plugin);
224
- return yield* this.enable(id);
534
+ const { plugin, dev = false } = yield* this._pluginLoader(id);
535
+ const pluginId = plugin.meta.id;
536
+ const existing = this._getPlugin(pluginId);
537
+
538
+ if (dev && existing && existing !== plugin) {
539
+ // Shadow path: a plugin with this id is already registered (a builtin,
540
+ // a registry install, or a previous dev load). Disable it, stash it,
541
+ // and swap the dev plugin into the same id slot. The dialog will call
542
+ // `enable(pluginId)` next, which activates the dev plugin's modules.
543
+ // `_markDev` is a no-op when the id is already tracked, so a dev-plugin
544
+ // reload (after editing source) keeps the *original* shadow target —
545
+ // removal restores the real underlying plugin, not an intermediate build.
546
+ const wasEnabled = this._get(this._enabledAtom).includes(pluginId);
547
+ if (wasEnabled) {
548
+ yield* this.disable(pluginId);
549
+ }
550
+ this._markDev(pluginId, { plugin: existing, wasEnabled });
551
+ this._update(this._pluginsAtom, (plugins) => plugins.map((p) => (p.meta.id === pluginId ? plugin : p)));
552
+ } else {
553
+ this._addPlugin(plugin);
554
+ if (dev) {
555
+ this._markDev(pluginId);
556
+ }
557
+ }
558
+
559
+ return plugin;
225
560
  });
226
561
  }
227
562
 
228
563
  /**
229
564
  * Enables a plugin.
230
565
  * @param id The id of the plugin.
566
+ * @param opts See {@link PluginManager.enable}.
231
567
  */
232
- enable(id: string): Effect.Effect<boolean, Error> {
568
+ enable(id: string, opts?: { resolveDependencies?: boolean }): Effect.Effect<boolean, Error> {
569
+ const resolveDependencies = opts?.resolveDependencies !== false;
233
570
  return Effect.gen(this, function* () {
234
- log('enable plugin', { id });
235
- const plugin = this._getPlugin(id);
236
- if (!plugin) {
571
+ log('enable plugin', { id, resolveDependencies });
572
+
573
+ if (!resolveDependencies) {
574
+ return yield* this._enableOne(id);
575
+ }
576
+
577
+ // If the root id is unknown to both the registered set and the catalog,
578
+ // fall back to the silent `_enableOne` path (which returns `false`).
579
+ // This preserves the prior contract for persisted `enabled` entries
580
+ // whose plugins are no longer bundled, instead of recording a confusing
581
+ // "missing self-dependency" failure.
582
+ if (!this._getPlugin(id) && !this._getCatalogEntry(id)) {
583
+ return yield* this._enableOne(id);
584
+ }
585
+
586
+ // Compute the transitive closure across registered plugins and catalog
587
+ // entries. Missing or cyclic entries are recorded as failures and the
588
+ // target plugin is left disabled.
589
+ const walk = this._computeDependencyClosure(id);
590
+ if (walk.cycle) {
591
+ this._recordFailure(
592
+ id,
593
+ 'load',
594
+ new Plugin.PluginDependencyError({ context: { id, reason: 'cycle', path: walk.cycle } }),
595
+ );
596
+ return false;
597
+ }
598
+ if (walk.missing.length > 0) {
599
+ this._recordFailure(
600
+ id,
601
+ 'load',
602
+ new Plugin.PluginDependencyError({ context: { id, reason: 'missing', missing: walk.missing } }),
603
+ );
237
604
  return false;
238
605
  }
239
606
 
607
+ // Install any catalog-only entries before enabling them. `add` may also
608
+ // discover further declared deps once the plugin's real meta is loaded;
609
+ // we re-walk after each install to absorb those.
610
+ let queue = walk.toInstall.slice();
611
+ const installed = new Set<string>();
612
+ while (queue.length > 0) {
613
+ const next = queue.shift()!;
614
+ if (installed.has(next) || this._getPlugin(next)) {
615
+ continue;
616
+ }
617
+ const installResult = yield* this.add(next).pipe(Effect.either);
618
+ if (installResult._tag === 'Left') {
619
+ this._recordFailure(
620
+ id,
621
+ 'load',
622
+ new Plugin.PluginDependencyError({
623
+ context: { id, reason: 'install-failed', dependency: next },
624
+ cause: installResult.left,
625
+ }),
626
+ );
627
+ return false;
628
+ }
629
+ installed.add(next);
630
+ const rewalk = this._computeDependencyClosure(id);
631
+ if (rewalk.cycle) {
632
+ this._recordFailure(
633
+ id,
634
+ 'load',
635
+ new Plugin.PluginDependencyError({ context: { id, reason: 'cycle', path: rewalk.cycle } }),
636
+ );
637
+ return false;
638
+ }
639
+ if (rewalk.missing.length > 0) {
640
+ this._recordFailure(
641
+ id,
642
+ 'load',
643
+ new Plugin.PluginDependencyError({ context: { id, reason: 'missing', missing: rewalk.missing } }),
644
+ );
645
+ return false;
646
+ }
647
+ queue = rewalk.toInstall.filter((depId) => !installed.has(depId));
648
+ }
649
+
650
+ // Enable in dependency-first order. `_enableOne` is idempotent on the
651
+ // enabled atom so previously-enabled deps short-circuit.
652
+ const order = this._computeDependencyClosure(id).order;
653
+ let succeeded = false;
654
+ for (const depId of order) {
655
+ const ok = yield* this._enableOne(depId);
656
+ if (depId === id) {
657
+ succeeded = ok;
658
+ }
659
+ }
660
+ return succeeded;
661
+ });
662
+ }
663
+
664
+ /**
665
+ * Enables a single plugin without consulting its declared dependencies.
666
+ * Used by {@link enable} as the leaf step after closure resolution, and
667
+ * directly when callers pass `{ resolveDependencies: false }`.
668
+ *
669
+ * The underlying operations (`_addModule`, `_setPendingResetByModule`,
670
+ * `activate`) are all idempotent, so this method is safe to call multiple
671
+ * times for the same id. The constructor's bootstrap path relies on this:
672
+ * the persisted `enabled` ids are written into `_enabledAtom` up front, so
673
+ * the very first `enable(id)` for those plugins sees `alreadyEnabled`-style
674
+ * state but still needs to perform the module registration and activation.
675
+ */
676
+ private _enableOne(id: string): Effect.Effect<boolean, Error> {
677
+ return Effect.gen(this, function* () {
678
+ const stub = this._getPlugin(id);
679
+ if (!stub) {
680
+ return false;
681
+ }
682
+
683
+ // Clear any prior failure record so a retry starts from a clean slate.
684
+ // The failure stays on the atom only if this attempt also fails.
685
+ this.clearFailure(id);
686
+
687
+ const plugin = yield* this._resolveLazyPlugin(stub);
688
+
240
689
  this._update(this._enabledAtom, (enabled) => (enabled.includes(id) ? enabled : [...enabled, id]));
241
690
 
242
691
  plugin.modules.forEach((module) => {
@@ -254,28 +703,119 @@ class ManagerImpl implements PluginManager {
254
703
  });
255
704
  }
256
705
 
706
+ /**
707
+ * Resolves a lazy plugin stub (returned by {@link Plugin.lazy}) to its
708
+ * loaded form and swaps it into `_pluginsAtom`. Returns the input unchanged
709
+ * when the plugin is already resolved, so callers can `yield*` this
710
+ * unconditionally. The lazy stub carries `meta` synchronously but its
711
+ * `modules` list is empty until the loader resolves; the swap ensures
712
+ * subsequent enable/disable operations see the resolved plugin.
713
+ *
714
+ * Concurrent calls for the same id are coalesced via `_resolvingPlugins`:
715
+ * the first caller starts the resolution, every subsequent caller awaits
716
+ * the same `Deferred`. On failure we publish a `lazy:<id>` error message
717
+ * and skip the atom swap so the failure is observable to the activation
718
+ * subscriber and a retry can be attempted.
719
+ */
720
+ private _resolveLazyPlugin(plugin: Plugin.Plugin): Effect.Effect<Plugin.Plugin, Plugin.LazyPluginError> {
721
+ return Effect.gen(this, function* () {
722
+ if (!Plugin.isLazy(plugin)) {
723
+ return plugin;
724
+ }
725
+ const id = plugin.meta.id;
726
+
727
+ const existing = this._resolvingPlugins.get(id);
728
+ if (existing) {
729
+ return yield* Deferred.await(existing);
730
+ }
731
+ const deferred = yield* Deferred.make<Plugin.Plugin, Plugin.LazyPluginError>();
732
+ this._resolvingPlugins.set(id, deferred);
733
+
734
+ return yield* Effect.gen(this, function* () {
735
+ log('resolving lazy plugin', { id });
736
+ yield* PubSub.publish(this.activation, { event: '', state: 'activating', module: `lazy:${id}` });
737
+ const resolvedPlugin = yield* Plugin.resolveLazy(plugin).pipe(
738
+ // Cap how long a remote import can hang. Without this the host can
739
+ // sit on a pending dynamic `import()` indefinitely if the plugin's
740
+ // server is unreachable, which stalls every caller awaiting
741
+ // `enable(id)` and (transitively) the manager's initialization.
742
+ Effect.timeoutFail({
743
+ duration: this._loadTimeout,
744
+ onTimeout: () =>
745
+ new Plugin.LazyPluginError({
746
+ context: { id, reason: 'load-failed' },
747
+ cause: new PluginTimeoutError({ context: { id, phase: 'load' as PluginFailurePhase } }),
748
+ }),
749
+ }),
750
+ );
751
+ this._update(this._pluginsAtom, (plugins) => plugins.map((p) => (p.meta.id === id ? resolvedPlugin : p)));
752
+ yield* PubSub.publish(this.activation, { event: '', state: 'activated', module: `lazy:${id}` });
753
+ return resolvedPlugin;
754
+ }).pipe(
755
+ Effect.tapError((error) =>
756
+ Effect.gen(this, function* () {
757
+ yield* PubSub.publish(this.activation, { event: '', state: 'error', module: `lazy:${id}`, error });
758
+ this._recordFailure(id, 'load', error);
759
+ this._scheduleAutoDisable(id);
760
+ }),
761
+ ),
762
+ Effect.tap((value) => Deferred.succeed(deferred, value)),
763
+ Effect.tapErrorCause((cause) => Deferred.failCause(deferred, cause)),
764
+ Effect.ensuring(Effect.sync(() => this._resolvingPlugins.delete(id))),
765
+ );
766
+ });
767
+ }
768
+
257
769
  /**
258
770
  * Removes a plugin from the manager.
259
771
  * @param id The id of the plugin.
772
+ * @param opts See {@link PluginManager.remove}.
260
773
  */
261
- remove(id: string): boolean {
262
- log('remove plugin', { id });
263
- const result = this.disable(id);
264
- if (!result) {
265
- return false;
266
- }
774
+ remove(id: string, opts?: { cascade?: boolean }): Effect.Effect<boolean, Error> {
775
+ return Effect.gen(this, function* () {
776
+ log('remove plugin', { id });
777
+ const wasDev = this._devPlugins.has(id);
778
+ const disabled = yield* this.disable(id, opts);
779
+ if (!disabled) {
780
+ return false;
781
+ }
267
782
 
268
- this._removePlugin(id);
269
- return true;
783
+ this._removePlugin(id);
784
+ if (this._onRemove) {
785
+ this._runForkedFiber(
786
+ this._onRemove(id).pipe(
787
+ Effect.tapError((error) => Effect.sync(() => log.warn('plugin remove hook failed', { id, error }))),
788
+ Effect.ignore,
789
+ ),
790
+ );
791
+ }
792
+
793
+ // If a dev plugin was shadowing an existing plugin, reinstate the
794
+ // original now that the dev plugin is gone. Re-enable only if the
795
+ // original was enabled at shadow time — preserving the user's intent
796
+ // for plugins they had explicitly disabled before iterating on a dev
797
+ // build.
798
+ if (wasDev) {
799
+ const shadow = this._unmarkDev(id);
800
+ if (shadow) {
801
+ this._addPlugin(shadow.plugin);
802
+ if (shadow.wasEnabled) {
803
+ yield* this.enable(id);
804
+ }
805
+ }
806
+ }
807
+ return true;
808
+ });
270
809
  }
271
810
 
272
811
  /**
273
812
  * Disables a plugin.
274
813
  * @param id The id of the plugin.
814
+ * @param opts See {@link PluginManager.disable}.
275
815
  */
276
- disable(id: string): Effect.Effect<boolean, Error> {
816
+ disable(id: string, { cascade = true }: { cascade?: boolean } = {}): Effect.Effect<boolean, Error> {
277
817
  return Effect.gen(this, function* () {
278
- log('disable plugin', { id });
818
+ log('disable plugin', { id, cascade });
279
819
  if (this._get(this._coreAtom).includes(id)) {
280
820
  return false;
281
821
  }
@@ -285,16 +825,55 @@ class ManagerImpl implements PluginManager {
285
825
  return false;
286
826
  }
287
827
 
828
+ if (cascade) {
829
+ const enabledDependents = this._collectDependents(id, { transitive: true, enabledOnly: true });
830
+ if (enabledDependents.length > 0) {
831
+ const coreDependent = enabledDependents.find((dependentId) =>
832
+ this._get(this._coreAtom).includes(dependentId),
833
+ );
834
+ if (coreDependent) {
835
+ return yield* Effect.fail(
836
+ new Plugin.PluginDependencyError({
837
+ context: { id, reason: 'core-dependent', coreDependent },
838
+ }),
839
+ );
840
+ }
841
+ // Disable transitive dependents first (leaves before root). The
842
+ // walk returns them in dependents-before-deps order — exactly what
843
+ // we want for teardown.
844
+ for (const dependentId of enabledDependents) {
845
+ yield* this._disableOne(dependentId);
846
+ }
847
+ }
848
+ }
849
+
850
+ yield* this._disableOne(id);
851
+ return true;
852
+ });
853
+ }
854
+
855
+ /**
856
+ * Disables a single plugin without consulting its dependents. Used by
857
+ * {@link disable} after the dependents pass has run (or been skipped via
858
+ * `cascade: false`).
859
+ */
860
+ private _disableOne(id: string): Effect.Effect<boolean, Error> {
861
+ return Effect.gen(this, function* () {
862
+ if (this._get(this._coreAtom).includes(id)) {
863
+ return false;
864
+ }
865
+ const plugin = this._getPlugin(id);
866
+ if (!plugin) {
867
+ return false;
868
+ }
288
869
  const enabledIndex = this._get(this._enabledAtom).findIndex((enabled) => enabled === id);
289
870
  if (enabledIndex !== -1) {
290
871
  this._update(this._enabledAtom, (enabled) => enabled.filter((item) => item !== id));
291
872
  yield* this.deactivate(id);
292
-
293
873
  plugin.modules.forEach((module) => {
294
874
  this._removeModule(module.id);
295
875
  });
296
876
  }
297
-
298
877
  return true;
299
878
  });
300
879
  }
@@ -315,6 +894,12 @@ class ManagerImpl implements PluginManager {
315
894
  return false;
316
895
  }
317
896
 
897
+ // Wait for the constructor's core/enabled `enable()` chain — including
898
+ // any async dynamic imports for lazy plugins — to finish registering
899
+ // modules. Without this, dispatching to an empty module set is the
900
+ // observable symptom of the race.
901
+ yield* Deferred.await(this._initialization);
902
+
318
903
  return yield* Effect.withFiberRuntime<boolean, Error>((fiber) =>
319
904
  this._activateEvent(key, params, fiber).pipe(
320
905
  together(
@@ -434,6 +1019,176 @@ class ManagerImpl implements PluginManager {
434
1019
  return this._get(this._pluginsAtom).find((plugin) => plugin.meta.id === id);
435
1020
  }
436
1021
 
1022
+ private _getPluginIdForModule(moduleId: string): string | undefined {
1023
+ return this._get(this._pluginsAtom).find((plugin) => plugin.modules.some((module) => module.id === moduleId))?.meta
1024
+ .id;
1025
+ }
1026
+
1027
+ /** Looks up an id in the cached registry catalog, returning the entry or `undefined`. */
1028
+ private _getCatalogEntry(id: string): PluginRegistry.Plugin | undefined {
1029
+ return this._get(this.pluginRegistry.plugins).entries.find((entry) => entry.id === id);
1030
+ }
1031
+
1032
+ /**
1033
+ * Returns the direct `dependsOn` declarations for an id, drawing from the
1034
+ * registered plugin's meta when available and falling back to the registry
1035
+ * catalog entry. Unknown ids return an empty list (callers detect "missing"
1036
+ * separately).
1037
+ */
1038
+ private _directDependencies(id: string): string[] {
1039
+ const plugin = this._getPlugin(id);
1040
+ if (plugin) {
1041
+ return [...(plugin.meta.dependsOn ?? [])];
1042
+ }
1043
+ const catalog = this._getCatalogEntry(id);
1044
+ return catalog?.dependsOn ? [...catalog.dependsOn] : [];
1045
+ }
1046
+
1047
+ /**
1048
+ * Computes the transitive dependency closure for an id.
1049
+ *
1050
+ * Walks {@link _directDependencies} (registered plugins ∪ catalog entries).
1051
+ * Returns:
1052
+ * - `order`: closure including the root in dependency-first topological order.
1053
+ * - `missing`: ids in the closure that are neither registered nor in the catalog.
1054
+ * - `toInstall`: ids in the closure that are in the catalog but not yet registered.
1055
+ * - `cycle`: when a cycle is detected, the cycle path; otherwise `undefined`.
1056
+ */
1057
+ private _computeDependencyClosure(id: string): {
1058
+ order: string[];
1059
+ missing: string[];
1060
+ toInstall: string[];
1061
+ cycle?: string[];
1062
+ } {
1063
+ const order: string[] = [];
1064
+ const visited = new Set<string>();
1065
+ const onStack = new Set<string>();
1066
+ const stackPath: string[] = [];
1067
+ const missing: string[] = [];
1068
+ const toInstall: string[] = [];
1069
+ let cycle: string[] | undefined;
1070
+
1071
+ const knownIds = new Set<string>([
1072
+ ...this._get(this._pluginsAtom).map((plugin) => plugin.meta.id),
1073
+ ...this._get(this.pluginRegistry.plugins).entries.map((entry) => entry.id),
1074
+ ]);
1075
+
1076
+ const visit = (currentId: string): void => {
1077
+ if (cycle) {
1078
+ return;
1079
+ }
1080
+ if (visited.has(currentId)) {
1081
+ return;
1082
+ }
1083
+ if (onStack.has(currentId)) {
1084
+ const cycleStart = stackPath.indexOf(currentId);
1085
+ cycle = [...stackPath.slice(cycleStart), currentId];
1086
+ return;
1087
+ }
1088
+ onStack.add(currentId);
1089
+ stackPath.push(currentId);
1090
+
1091
+ if (!knownIds.has(currentId)) {
1092
+ missing.push(currentId);
1093
+ } else if (!this._getPlugin(currentId)) {
1094
+ toInstall.push(currentId);
1095
+ }
1096
+
1097
+ for (const depId of this._directDependencies(currentId)) {
1098
+ visit(depId);
1099
+ if (cycle) {
1100
+ break;
1101
+ }
1102
+ }
1103
+
1104
+ onStack.delete(currentId);
1105
+ stackPath.pop();
1106
+ if (!cycle) {
1107
+ visited.add(currentId);
1108
+ order.push(currentId);
1109
+ }
1110
+ };
1111
+
1112
+ visit(id);
1113
+ return { order, missing, toInstall, cycle };
1114
+ }
1115
+
1116
+ /**
1117
+ * Walks the reverse `dependsOn` edges across registered plugins. With
1118
+ * `enabledOnly`, filters the result to currently-enabled ids. Returns
1119
+ * dependents in dependents-before-deps order so callers (cascade-disable)
1120
+ * can iterate and tear down leaves first.
1121
+ */
1122
+ private _collectDependents(id: string, opts: { transitive: boolean; enabledOnly: boolean }): string[] {
1123
+ const direct = this._get(this._pluginsAtom)
1124
+ .filter((plugin) => plugin.meta.dependsOn?.includes(id))
1125
+ .map((plugin) => plugin.meta.id);
1126
+
1127
+ if (!opts.transitive) {
1128
+ return opts.enabledOnly
1129
+ ? direct.filter((dependentId) => this._get(this._enabledAtom).includes(dependentId))
1130
+ : direct;
1131
+ }
1132
+
1133
+ const result: string[] = [];
1134
+ const visited = new Set<string>();
1135
+ const visit = (currentId: string): void => {
1136
+ if (visited.has(currentId)) {
1137
+ return;
1138
+ }
1139
+ visited.add(currentId);
1140
+ const parents = this._get(this._pluginsAtom)
1141
+ .filter((plugin) => plugin.meta.dependsOn?.includes(currentId))
1142
+ .map((plugin) => plugin.meta.id);
1143
+ for (const parentId of parents) {
1144
+ visit(parentId);
1145
+ if (parentId !== id && !result.includes(parentId)) {
1146
+ result.push(parentId);
1147
+ }
1148
+ }
1149
+ };
1150
+ visit(id);
1151
+
1152
+ return opts.enabledOnly
1153
+ ? result.filter((dependentId) => this._get(this._enabledAtom).includes(dependentId))
1154
+ : result;
1155
+ }
1156
+
1157
+ /**
1158
+ * Records a failure for a plugin. Latest failure wins so the registry UI
1159
+ * always sees the most recent reason. Walks the `cause` chain when checking
1160
+ * for timeouts: lazy-load timeouts arrive wrapped in `LazyPluginError` (the
1161
+ * timeout is the cause), but the operator-visible reason should still be
1162
+ * `'timeout'`.
1163
+ */
1164
+ private _recordFailure(id: string, phase: PluginFailurePhase, error: Error): void {
1165
+ const reason: PluginFailureReason = isTimeoutCause(error) ? 'timeout' : 'error';
1166
+ const failure: PluginFailure = { id, phase, reason, error, timestamp: Date.now() };
1167
+ log.warn('plugin failed', { id, phase, reason, error: error.message });
1168
+ this._update(this._failedAtom, (current) => [...current.filter((entry) => entry.id !== id), failure]);
1169
+ }
1170
+
1171
+ /**
1172
+ * Fire-and-forget disable of a failed plugin. Forked because a failure can
1173
+ * happen mid-activation chain — yielding a `disable` inline would deadlock
1174
+ * on the shared semaphores. Core plugins are skipped (the host opted into
1175
+ * them being non-removable; the failure record is enough signal).
1176
+ */
1177
+ private _scheduleAutoDisable(id: string): void {
1178
+ if (this._get(this._coreAtom).includes(id)) {
1179
+ return;
1180
+ }
1181
+ if (!this._get(this._enabledAtom).includes(id)) {
1182
+ return;
1183
+ }
1184
+ this._runForkedFiber(
1185
+ this.disable(id).pipe(
1186
+ Effect.tapError((error) => Effect.sync(() => log.warn('auto-disable failed', { id, error }))),
1187
+ Effect.ignore,
1188
+ ),
1189
+ );
1190
+ }
1191
+
437
1192
  private _getActiveModules(): Plugin.PluginModule[] {
438
1193
  const active = this._get(this._activeAtom);
439
1194
  return this._get(this._modulesAtom).filter((module) => active.includes(module.id));
@@ -505,6 +1260,18 @@ class ManagerImpl implements PluginManager {
505
1260
  return Ref.update(ref, (fibers) => fibers.filter((trackedFiber) => trackedFiber !== fiber));
506
1261
  }
507
1262
 
1263
+ /**
1264
+ * Spawns an effect on the default runtime and registers the resulting fiber in
1265
+ * `_inFlightFibers` so {@link shutdown} can interrupt it. Used from sync entry
1266
+ * points like {@link remove} where there is no enclosing Effect to fork from;
1267
+ * inside an Effect chain prefer the existing track/await/untrack pattern.
1268
+ */
1269
+ private _runForkedFiber<E>(effect: Effect.Effect<void, E>): void {
1270
+ const fiber = Effect.runFork(effect);
1271
+ Effect.runSync(this._trackFiber(this._inFlightFibers, fiber));
1272
+ Effect.runFork(Fiber.await(fiber).pipe(Effect.andThen(() => this._untrackFiber(this._inFlightFibers, fiber))));
1273
+ }
1274
+
508
1275
  //
509
1276
  // Registration helpers
510
1277
  //
@@ -578,6 +1345,7 @@ class ManagerImpl implements PluginManager {
578
1345
  yield* Ref.update(this._activatingModules, (activating) => Array.appendAll(activating, activatingModuleIds));
579
1346
 
580
1347
  log('activating modules', { key, modules: activatingModuleIds });
1348
+ performance.mark(`event:${key}:start`);
581
1349
  yield* PubSub.publish(this.activation, { event: key, state: 'activating' });
582
1350
 
583
1351
  yield* this._activateRelatedEvents(key, this._getBeforeEvents(modules, activatingEvents), 'before');
@@ -591,6 +1359,8 @@ class ManagerImpl implements PluginManager {
591
1359
  this._update(this._eventsFiredAtom, (events) => [...events, key]);
592
1360
  }
593
1361
 
1362
+ performance.mark(`event:${key}:end`);
1363
+ performance.measure(`event:${key}`, `event:${key}:start`, `event:${key}:end`);
594
1364
  yield* PubSub.publish(this.activation, { event: key, state: 'activated' });
595
1365
  log('activated', { key });
596
1366
 
@@ -636,7 +1406,7 @@ class ManagerImpl implements PluginManager {
636
1406
  ): ActivationEvent.ActivationEvent[] {
637
1407
  return Function.pipe(
638
1408
  modules,
639
- Array.flatMap((module) => module.activatesBefore ?? []),
1409
+ Array.flatMap((module) => module.firesBeforeActivation ?? []),
640
1410
  HashSet.fromIterable,
641
1411
  HashSet.toValues,
642
1412
  Array.filter((event) => !activatingEvents.includes(ActivationEvent.eventKey(event))),
@@ -649,7 +1419,7 @@ class ManagerImpl implements PluginManager {
649
1419
  ): ActivationEvent.ActivationEvent[] {
650
1420
  return Function.pipe(
651
1421
  modules,
652
- Array.flatMap((module) => module.activatesAfter ?? []),
1422
+ Array.flatMap((module) => module.firesAfterActivation ?? []),
653
1423
  HashSet.fromIterable,
654
1424
  HashSet.toValues,
655
1425
  Array.filter((event) => !activatingEvents.includes(ActivationEvent.eventKey(event))),
@@ -661,7 +1431,7 @@ class ManagerImpl implements PluginManager {
661
1431
  events: ActivationEvent.ActivationEvent[],
662
1432
  phase: 'before' | 'after',
663
1433
  ): Effect.Effect<void, Error> {
664
- const logLabel = phase === 'before' ? 'activatesBefore' : 'activatesAfter';
1434
+ const logLabel = phase === 'before' ? 'firesBeforeActivation' : 'firesAfterActivation';
665
1435
  const eventKey = phase === 'before' ? 'beforeEvents' : 'afterEvents';
666
1436
  return Function.pipe(
667
1437
  events,
@@ -693,7 +1463,7 @@ class ManagerImpl implements PluginManager {
693
1463
  ): Effect.Effect<Capability.Any[][], Error> {
694
1464
  return Function.pipe(
695
1465
  modules,
696
- Array.map((mod) => this._loadModule(mod)),
1466
+ Array.map((mod) => this._loadModule(mod, key)),
697
1467
  Effect.allWith({ concurrency: 'unbounded' }),
698
1468
  Effect.catchAll((error) => {
699
1469
  return Effect.gen(this, function* () {
@@ -712,8 +1482,12 @@ class ManagerImpl implements PluginManager {
712
1482
  modules,
713
1483
  Array.zip(capabilities),
714
1484
  Array.map(([module, capabilitySet]) => this._contributeCapabilities(module, capabilitySet)),
715
- // TODO(wittjosiah): This currently can't be run in parallel.
716
- // Running this with concurrency causes races with `allOf` activation events.
1485
+ // TODO(wittjosiah): This currently can't be run in parallel, and inserting
1486
+ // any yield between contributions (`Effect.yieldNow()`, `Effect.sleep(0)`)
1487
+ // races the `allOf` activation-event resolver — observed as a System
1488
+ // Error dialog on warm reloads. Contributions must stay strictly
1489
+ // synchronous within an event; React paint slots have to be found at
1490
+ // event boundaries higher up the call chain.
717
1491
  Effect.all,
718
1492
  Effect.asVoid,
719
1493
  );
@@ -728,7 +1502,14 @@ class ManagerImpl implements PluginManager {
728
1502
  return semaphore;
729
1503
  }
730
1504
 
731
- private _loadModule = (module: Plugin.PluginModule): Effect.Effect<Capability.Any[], Error> =>
1505
+ // `parentEvent` is the activation event that first triggered this module
1506
+ // load — included in `activating`/`activated` PubSub messages so subscribers
1507
+ // (e.g. the boot loader's status listener) can associate a module with its
1508
+ // triggering event in the trace. The same module may be referenced by
1509
+ // multiple events, but module loads are memoized via `_moduleMemoMap`, so
1510
+ // only the first event to need it will appear here; later events await the
1511
+ // cached deferred without re-publishing.
1512
+ private _loadModule = (module: Plugin.PluginModule, parentEvent: string): Effect.Effect<Capability.Any[], Error> =>
732
1513
  Effect.gen(this, function* () {
733
1514
  const semaphore = this._getModuleSemaphore(module.id);
734
1515
 
@@ -744,18 +1525,34 @@ class ManagerImpl implements PluginManager {
744
1525
  this._moduleMemoMap.set(module.id, deferred);
745
1526
 
746
1527
  const loadEffect = Effect.gen(this, function* () {
747
- log('loading module', { module: module.id });
748
- const [duration, capabilities] = yield* module
749
- .activate()
750
- .pipe(
751
- Effect.provideService(Capability.Service, this.capabilities),
752
- Effect.provideService(Plugin.Service, this),
753
- Effect.timed,
754
- );
1528
+ log('loading module', { module: module.id, parentEvent });
1529
+ performance.mark(`module:${module.id}:start`);
1530
+ yield* PubSub.publish(this.activation, { event: parentEvent, state: 'activating', module: module.id });
1531
+ const pluginId = this._getPluginIdForModule(module.id);
1532
+ const [duration, capabilities] = yield* module.activate().pipe(
1533
+ Effect.provideService(Capability.Service, this.capabilities),
1534
+ Effect.provideService(Plugin.Service, this),
1535
+ // Cap activation so a single misbehaving module can't hold the
1536
+ // event chain open. On timeout the failure is recorded against
1537
+ // the plugin and surfaced as `PluginTimeoutError`.
1538
+ Effect.timeoutFail({
1539
+ duration: this._activationTimeout,
1540
+ onTimeout: () =>
1541
+ new PluginTimeoutError({
1542
+ context: { id: pluginId ?? module.id, module: module.id, phase: 'activation' as PluginFailurePhase },
1543
+ }),
1544
+ }),
1545
+ Effect.timed,
1546
+ );
755
1547
  const normalized = capabilities == null ? [] : Array.isArray(capabilities) ? capabilities : [capabilities];
1548
+ const elapsed = Duration.toMillis(duration);
1549
+ performance.mark(`module:${module.id}:end`);
1550
+ performance.measure(`module:${module.id}`, `module:${module.id}:start`, `module:${module.id}:end`);
1551
+ yield* PubSub.publish(this.activation, { event: parentEvent, state: 'activated', module: module.id });
756
1552
  log('loaded module', {
757
1553
  module: module.id,
758
- elapsed: Duration.toMillis(duration),
1554
+ parentEvent,
1555
+ elapsed,
759
1556
  failed: false,
760
1557
  });
761
1558
  return normalized as Capability.Any[];
@@ -791,7 +1588,13 @@ class ManagerImpl implements PluginManager {
791
1588
  stack: error instanceof Error ? error.stack : undefined,
792
1589
  isDefect: !Cause.isFailure(cause),
793
1590
  });
794
- return Deferred.fail(deferred, error instanceof Error ? error : new Error(String(error)));
1591
+ const normalizedError = error instanceof Error ? error : new Error(String(error));
1592
+ const pluginId = this._getPluginIdForModule(module.id);
1593
+ if (pluginId !== undefined) {
1594
+ this._recordFailure(pluginId, 'activation', normalizedError);
1595
+ this._scheduleAutoDisable(pluginId);
1596
+ }
1597
+ return Deferred.fail(deferred, normalizedError);
795
1598
  }),
796
1599
  ),
797
1600
  );
@@ -852,6 +1655,22 @@ class ManagerImpl implements PluginManager {
852
1655
  */
853
1656
  export const make = (options: ManagerOptions): PluginManager => new ManagerImpl(options);
854
1657
 
1658
+ /**
1659
+ * True when `error` (or anything along its `cause` chain) is a
1660
+ * {@link PluginTimeoutError}. Lazy-load timeouts wrap the timeout inside
1661
+ * `LazyPluginError`, so a shallow check on the outer error misses them.
1662
+ * Bounded depth so a circular chain can't loop forever.
1663
+ */
1664
+ const isTimeoutCause = (error: unknown, depth = 0): boolean => {
1665
+ if (depth > 5 || !(error instanceof Error)) {
1666
+ return false;
1667
+ }
1668
+ if (PluginTimeoutError.is(error)) {
1669
+ return true;
1670
+ }
1671
+ return isTimeoutCause((error as Error & { cause?: unknown }).cause, depth + 1);
1672
+ };
1673
+
855
1674
  /**
856
1675
  * Runs an effect concurrently with another effect.
857
1676
  * If the first effect completes, the second effect is interrupted.