@dxos/plugin-registry 0.8.4-main.c85a9c8dae → 0.8.4-main.d9fc60f731

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 (265) hide show
  1. package/LICENSE +102 -5
  2. package/dist/lib/neutral/RegistryPlugin.mjs +27 -0
  3. package/dist/lib/neutral/RegistryPlugin.mjs.map +7 -0
  4. package/dist/lib/{browser/cli/index.mjs → neutral/RegistryPlugin.node.mjs} +28 -99
  5. package/dist/lib/neutral/RegistryPlugin.node.mjs.map +7 -0
  6. package/dist/lib/neutral/RegistryPlugin.workerd.mjs +10 -0
  7. package/dist/lib/neutral/RegistryPlugin.workerd.mjs.map +7 -0
  8. package/dist/lib/neutral/chunk-GRQI6MLI.mjs +36 -0
  9. package/dist/lib/neutral/chunk-GRQI6MLI.mjs.map +7 -0
  10. package/dist/lib/neutral/chunk-XK2JMH3H.mjs +33 -0
  11. package/dist/lib/neutral/chunk-XK2JMH3H.mjs.map +7 -0
  12. package/dist/lib/neutral/index.mjs +25 -0
  13. package/dist/lib/neutral/index.mjs.map +7 -0
  14. package/dist/lib/neutral/meta.json +1 -0
  15. package/dist/lib/neutral/open-plugin-registry-II735N76.mjs +15 -0
  16. package/dist/lib/neutral/open-plugin-registry-II735N76.mjs.map +7 -0
  17. package/dist/lib/neutral/plugin.mjs +18 -0
  18. package/dist/lib/neutral/plugin.mjs.map +7 -0
  19. package/dist/lib/neutral/translations.mjs +72 -0
  20. package/dist/lib/neutral/translations.mjs.map +7 -0
  21. package/dist/types/src/RegistryPlugin.d.ts +1 -0
  22. package/dist/types/src/RegistryPlugin.d.ts.map +1 -1
  23. package/dist/types/src/{cli/plugin.d.ts → RegistryPlugin.node.d.ts} +2 -1
  24. package/dist/types/src/RegistryPlugin.node.d.ts.map +1 -0
  25. package/dist/types/src/RegistryPlugin.test.d.ts +2 -0
  26. package/dist/types/src/RegistryPlugin.test.d.ts.map +1 -0
  27. package/dist/types/src/RegistryPlugin.workerd.d.ts +4 -0
  28. package/dist/types/src/RegistryPlugin.workerd.d.ts.map +1 -0
  29. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -0
  30. package/dist/types/src/capabilities/dev-plugin-loader.d.ts +11 -0
  31. package/dist/types/src/capabilities/dev-plugin-loader.d.ts.map +1 -0
  32. package/dist/types/src/capabilities/index.d.ts +15 -3
  33. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  34. package/dist/types/src/capabilities/operation-handler.d.ts +6 -0
  35. package/dist/types/src/capabilities/operation-handler.d.ts.map +1 -0
  36. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -0
  37. package/dist/types/src/capabilities/settings.d.ts +14 -0
  38. package/dist/types/src/capabilities/settings.d.ts.map +1 -0
  39. package/dist/types/src/categories.d.ts +18 -0
  40. package/dist/types/src/categories.d.ts.map +1 -0
  41. package/dist/types/src/{cli/commands → commands}/disable.d.ts +2 -2
  42. package/dist/types/src/commands/disable.d.ts.map +1 -0
  43. package/dist/types/src/{cli/commands → commands}/enable.d.ts +2 -2
  44. package/dist/types/src/commands/enable.d.ts.map +1 -0
  45. package/dist/types/src/commands/index.d.ts.map +1 -0
  46. package/dist/types/src/commands/list.d.ts +7 -0
  47. package/dist/types/src/commands/list.d.ts.map +1 -0
  48. package/dist/types/src/commands/util.d.ts.map +1 -0
  49. package/dist/types/src/components/DisableDependentsAlert/DisableDependentsAlert.d.ts +24 -0
  50. package/dist/types/src/components/DisableDependentsAlert/DisableDependentsAlert.d.ts.map +1 -0
  51. package/dist/types/src/components/DisableDependentsAlert/index.d.ts +2 -0
  52. package/dist/types/src/components/DisableDependentsAlert/index.d.ts.map +1 -0
  53. package/dist/types/src/components/PluginDetail/PluginDetail.d.ts +75 -2
  54. package/dist/types/src/components/PluginDetail/PluginDetail.d.ts.map +1 -1
  55. package/dist/types/src/components/PluginDetail/PluginDetail.stories.d.ts +62 -16
  56. package/dist/types/src/components/PluginDetail/PluginDetail.stories.d.ts.map +1 -1
  57. package/dist/types/src/components/PluginFailureBadge/PluginFailureBadge.d.ts +20 -0
  58. package/dist/types/src/components/PluginFailureBadge/PluginFailureBadge.d.ts.map +1 -0
  59. package/dist/types/src/components/PluginFailureBadge/index.d.ts +2 -0
  60. package/dist/types/src/components/PluginFailureBadge/index.d.ts.map +1 -0
  61. package/dist/types/src/components/PluginList/PluginItem.d.ts +29 -2
  62. package/dist/types/src/components/PluginList/PluginItem.d.ts.map +1 -1
  63. package/dist/types/src/components/PluginList/PluginList.d.ts +18 -3
  64. package/dist/types/src/components/PluginList/PluginList.d.ts.map +1 -1
  65. package/dist/types/src/components/PluginList/PluginList.stories.d.ts +60 -16
  66. package/dist/types/src/components/PluginList/PluginList.stories.d.ts.map +1 -1
  67. package/dist/types/src/components/RegistrySettings/RegistrySettings.d.ts +18 -0
  68. package/dist/types/src/components/RegistrySettings/RegistrySettings.d.ts.map +1 -0
  69. package/dist/types/src/components/RegistrySettings/index.d.ts +2 -0
  70. package/dist/types/src/components/RegistrySettings/index.d.ts.map +1 -0
  71. package/dist/types/src/components/index.d.ts +3 -0
  72. package/dist/types/src/components/index.d.ts.map +1 -1
  73. package/dist/types/src/containers/BaseRegistryArticle/BaseRegistryArticle.d.ts +20 -0
  74. package/dist/types/src/containers/BaseRegistryArticle/BaseRegistryArticle.d.ts.map +1 -0
  75. package/dist/types/src/containers/BaseRegistryArticle/index.d.ts +2 -0
  76. package/dist/types/src/containers/BaseRegistryArticle/index.d.ts.map +1 -0
  77. package/dist/types/src/containers/LoadPluginDialog/LoadPluginDialog.d.ts +5 -0
  78. package/dist/types/src/containers/LoadPluginDialog/LoadPluginDialog.d.ts.map +1 -0
  79. package/dist/types/src/containers/LoadPluginDialog/index.d.ts +2 -0
  80. package/dist/types/src/containers/LoadPluginDialog/index.d.ts.map +1 -0
  81. package/dist/types/src/containers/PluginArticle/PluginArticle.d.ts.map +1 -1
  82. package/dist/types/src/containers/PluginArticle/index.d.ts +1 -2
  83. package/dist/types/src/containers/PluginArticle/index.d.ts.map +1 -1
  84. package/dist/types/src/containers/PublicRegistryArticle/PublicRegistryArticle.d.ts +8 -0
  85. package/dist/types/src/containers/PublicRegistryArticle/PublicRegistryArticle.d.ts.map +1 -0
  86. package/dist/types/src/containers/PublicRegistryArticle/index.d.ts +2 -0
  87. package/dist/types/src/containers/PublicRegistryArticle/index.d.ts.map +1 -0
  88. package/dist/types/src/containers/RegistryArticle/RegistryArticle.d.ts +14 -0
  89. package/dist/types/src/containers/RegistryArticle/RegistryArticle.d.ts.map +1 -0
  90. package/dist/types/src/containers/RegistryArticle/index.d.ts +2 -0
  91. package/dist/types/src/containers/RegistryArticle/index.d.ts.map +1 -0
  92. package/dist/types/src/containers/RegistrySettingsContainer/RegistrySettingsContainer.d.ts +13 -0
  93. package/dist/types/src/containers/RegistrySettingsContainer/RegistrySettingsContainer.d.ts.map +1 -0
  94. package/dist/types/src/containers/RegistrySettingsContainer/index.d.ts +2 -0
  95. package/dist/types/src/containers/RegistrySettingsContainer/index.d.ts.map +1 -0
  96. package/dist/types/src/containers/index.d.ts +5 -1
  97. package/dist/types/src/containers/index.d.ts.map +1 -1
  98. package/dist/types/src/hooks/index.d.ts +9 -0
  99. package/dist/types/src/hooks/index.d.ts.map +1 -0
  100. package/dist/types/src/hooks/useAutoTags.d.ts +20 -0
  101. package/dist/types/src/hooks/useAutoTags.d.ts.map +1 -0
  102. package/dist/types/src/hooks/useCatalogEntry.d.ts +11 -0
  103. package/dist/types/src/hooks/useCatalogEntry.d.ts.map +1 -0
  104. package/dist/types/src/hooks/useDisableConfirmation.d.ts +19 -0
  105. package/dist/types/src/hooks/useDisableConfirmation.d.ts.map +1 -0
  106. package/dist/types/src/hooks/useInstalledVersionTag.d.ts +18 -0
  107. package/dist/types/src/hooks/useInstalledVersionTag.d.ts.map +1 -0
  108. package/dist/types/src/hooks/usePluginActions.d.ts +36 -0
  109. package/dist/types/src/hooks/usePluginActions.d.ts.map +1 -0
  110. package/dist/types/src/hooks/useRegistryPlugins.d.ts +15 -0
  111. package/dist/types/src/hooks/useRegistryPlugins.d.ts.map +1 -0
  112. package/dist/types/src/hooks/useUpdateAvailable.d.ts +18 -0
  113. package/dist/types/src/hooks/useUpdateAvailable.d.ts.map +1 -0
  114. package/dist/types/src/hooks/useVersionPicker.d.ts +21 -0
  115. package/dist/types/src/hooks/useVersionPicker.d.ts.map +1 -0
  116. package/dist/types/src/index.d.ts +1 -1
  117. package/dist/types/src/index.d.ts.map +1 -1
  118. package/dist/types/src/meta.d.ts +20 -2
  119. package/dist/types/src/meta.d.ts.map +1 -1
  120. package/dist/types/src/operations/index.d.ts +3 -0
  121. package/dist/types/src/operations/index.d.ts.map +1 -0
  122. package/dist/types/src/operations/open-plugin-registry.d.ts +5 -0
  123. package/dist/types/src/operations/open-plugin-registry.d.ts.map +1 -0
  124. package/dist/types/src/plugin.d.ts +4 -0
  125. package/dist/types/src/plugin.d.ts.map +1 -0
  126. package/dist/types/src/{cli/storage.d.ts → storage.d.ts} +4 -4
  127. package/dist/types/src/storage.d.ts.map +1 -0
  128. package/dist/types/src/translations.d.ts +59 -16
  129. package/dist/types/src/translations.d.ts.map +1 -1
  130. package/dist/types/src/types.d.ts +30 -1
  131. package/dist/types/src/types.d.ts.map +1 -1
  132. package/dist/types/tsconfig.tsbuildinfo +1 -1
  133. package/package.json +63 -39
  134. package/src/{cli/plugin.ts → RegistryPlugin.node.ts} +3 -1
  135. package/src/RegistryPlugin.test.ts +24 -0
  136. package/src/RegistryPlugin.tsx +13 -5
  137. package/src/RegistryPlugin.workerd.ts +11 -0
  138. package/src/capabilities/app-graph-builder.ts +219 -0
  139. package/src/capabilities/dev-plugin-loader.ts +39 -0
  140. package/src/capabilities/index.ts +11 -3
  141. package/src/capabilities/operation-handler.ts +16 -0
  142. package/src/capabilities/react-surface.tsx +126 -0
  143. package/src/capabilities/settings.ts +34 -0
  144. package/src/categories.ts +44 -0
  145. package/src/components/DisableDependentsAlert/DisableDependentsAlert.tsx +69 -0
  146. package/src/components/DisableDependentsAlert/index.ts +5 -0
  147. package/src/components/PluginDetail/PluginDetail.stories.tsx +18 -14
  148. package/src/components/PluginDetail/PluginDetail.tsx +323 -61
  149. package/src/components/PluginFailureBadge/PluginFailureBadge.tsx +68 -0
  150. package/src/components/PluginFailureBadge/index.ts +5 -0
  151. package/src/components/PluginList/PluginItem.tsx +124 -25
  152. package/src/components/PluginList/PluginList.stories.tsx +22 -16
  153. package/src/components/PluginList/PluginList.tsx +32 -4
  154. package/src/components/RegistrySettings/RegistrySettings.tsx +120 -0
  155. package/src/components/RegistrySettings/index.ts +5 -0
  156. package/src/components/index.ts +3 -0
  157. package/src/containers/BaseRegistryArticle/BaseRegistryArticle.tsx +178 -0
  158. package/src/containers/BaseRegistryArticle/index.ts +5 -0
  159. package/src/containers/LoadPluginDialog/LoadPluginDialog.tsx +88 -0
  160. package/src/containers/LoadPluginDialog/index.ts +5 -0
  161. package/src/containers/PluginArticle/PluginArticle.tsx +102 -13
  162. package/src/containers/PluginArticle/index.ts +1 -3
  163. package/src/containers/PublicRegistryArticle/PublicRegistryArticle.tsx +193 -0
  164. package/src/containers/PublicRegistryArticle/index.ts +5 -0
  165. package/src/containers/RegistryArticle/RegistryArticle.tsx +53 -0
  166. package/src/containers/RegistryArticle/index.ts +5 -0
  167. package/src/containers/RegistrySettingsContainer/RegistrySettingsContainer.tsx +59 -0
  168. package/src/containers/RegistrySettingsContainer/index.ts +5 -0
  169. package/src/containers/index.ts +5 -1
  170. package/src/hooks/index.ts +12 -0
  171. package/src/hooks/useAutoTags.ts +52 -0
  172. package/src/hooks/useCatalogEntry.ts +25 -0
  173. package/src/hooks/useDisableConfirmation.ts +58 -0
  174. package/src/hooks/useInstalledVersionTag.ts +41 -0
  175. package/src/hooks/usePluginActions.ts +174 -0
  176. package/src/hooks/useRegistryPlugins.ts +29 -0
  177. package/src/hooks/useUpdateAvailable.ts +31 -0
  178. package/src/hooks/useVersionPicker.ts +112 -0
  179. package/src/index.ts +1 -1
  180. package/src/meta.ts +34 -8
  181. package/src/operations/index.ts +7 -0
  182. package/src/operations/open-plugin-registry.ts +21 -0
  183. package/src/plugin.ts +11 -0
  184. package/src/{cli/storage.ts → storage.ts} +14 -21
  185. package/src/translations.ts +64 -15
  186. package/src/types.ts +33 -1
  187. package/dist/lib/browser/PluginArticle-MLBF7AFA.mjs +0 -29
  188. package/dist/lib/browser/PluginArticle-MLBF7AFA.mjs.map +0 -7
  189. package/dist/lib/browser/PluginRegistry-ENAI4SAY.mjs +0 -77
  190. package/dist/lib/browser/PluginRegistry-ENAI4SAY.mjs.map +0 -7
  191. package/dist/lib/browser/app-graph-builder-2HXD7SOS.mjs +0 -167
  192. package/dist/lib/browser/app-graph-builder-2HXD7SOS.mjs.map +0 -7
  193. package/dist/lib/browser/chunk-TSNK3SAR.mjs +0 -191
  194. package/dist/lib/browser/chunk-TSNK3SAR.mjs.map +0 -7
  195. package/dist/lib/browser/chunk-VOAOTNGS.mjs +0 -19
  196. package/dist/lib/browser/chunk-VOAOTNGS.mjs.map +0 -7
  197. package/dist/lib/browser/cli/index.mjs.map +0 -7
  198. package/dist/lib/browser/index.mjs +0 -63
  199. package/dist/lib/browser/index.mjs.map +0 -7
  200. package/dist/lib/browser/meta.json +0 -1
  201. package/dist/lib/browser/operation-resolver-7P3NNVSR.mjs +0 -25
  202. package/dist/lib/browser/operation-resolver-7P3NNVSR.mjs.map +0 -7
  203. package/dist/lib/browser/react-surface-DH25DIRH.mjs +0 -85
  204. package/dist/lib/browser/react-surface-DH25DIRH.mjs.map +0 -7
  205. package/dist/lib/node-esm/PluginArticle-TP2G6GGI.mjs +0 -30
  206. package/dist/lib/node-esm/PluginArticle-TP2G6GGI.mjs.map +0 -7
  207. package/dist/lib/node-esm/PluginRegistry-CWIZIQNX.mjs +0 -78
  208. package/dist/lib/node-esm/PluginRegistry-CWIZIQNX.mjs.map +0 -7
  209. package/dist/lib/node-esm/app-graph-builder-2LNPN72O.mjs +0 -168
  210. package/dist/lib/node-esm/app-graph-builder-2LNPN72O.mjs.map +0 -7
  211. package/dist/lib/node-esm/chunk-4XO63XB6.mjs +0 -192
  212. package/dist/lib/node-esm/chunk-4XO63XB6.mjs.map +0 -7
  213. package/dist/lib/node-esm/chunk-SALMIZ2W.mjs +0 -21
  214. package/dist/lib/node-esm/chunk-SALMIZ2W.mjs.map +0 -7
  215. package/dist/lib/node-esm/cli/index.mjs +0 -225
  216. package/dist/lib/node-esm/cli/index.mjs.map +0 -7
  217. package/dist/lib/node-esm/index.mjs +0 -64
  218. package/dist/lib/node-esm/index.mjs.map +0 -7
  219. package/dist/lib/node-esm/meta.json +0 -1
  220. package/dist/lib/node-esm/operation-resolver-B24QW6IF.mjs +0 -26
  221. package/dist/lib/node-esm/operation-resolver-B24QW6IF.mjs.map +0 -7
  222. package/dist/lib/node-esm/react-surface-3YBEVL65.mjs +0 -86
  223. package/dist/lib/node-esm/react-surface-3YBEVL65.mjs.map +0 -7
  224. package/dist/types/src/capabilities/app-graph-builder/app-graph-builder.d.ts.map +0 -1
  225. package/dist/types/src/capabilities/app-graph-builder/index.d.ts +0 -3
  226. package/dist/types/src/capabilities/app-graph-builder/index.d.ts.map +0 -1
  227. package/dist/types/src/capabilities/operation-resolver/index.d.ts +0 -3
  228. package/dist/types/src/capabilities/operation-resolver/index.d.ts.map +0 -1
  229. package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts +0 -5
  230. package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts.map +0 -1
  231. package/dist/types/src/capabilities/react-surface/index.d.ts +0 -3
  232. package/dist/types/src/capabilities/react-surface/index.d.ts.map +0 -1
  233. package/dist/types/src/capabilities/react-surface/react-surface.d.ts.map +0 -1
  234. package/dist/types/src/cli/commands/disable.d.ts.map +0 -1
  235. package/dist/types/src/cli/commands/enable.d.ts.map +0 -1
  236. package/dist/types/src/cli/commands/index.d.ts.map +0 -1
  237. package/dist/types/src/cli/commands/list.d.ts +0 -7
  238. package/dist/types/src/cli/commands/list.d.ts.map +0 -1
  239. package/dist/types/src/cli/commands/util.d.ts.map +0 -1
  240. package/dist/types/src/cli/index.d.ts +0 -3
  241. package/dist/types/src/cli/index.d.ts.map +0 -1
  242. package/dist/types/src/cli/plugin.d.ts.map +0 -1
  243. package/dist/types/src/cli/storage.d.ts.map +0 -1
  244. package/dist/types/src/containers/PluginRegistry/PluginRegistry.d.ts +0 -8
  245. package/dist/types/src/containers/PluginRegistry/PluginRegistry.d.ts.map +0 -1
  246. package/dist/types/src/containers/PluginRegistry/index.d.ts +0 -3
  247. package/dist/types/src/containers/PluginRegistry/index.d.ts.map +0 -1
  248. package/src/capabilities/app-graph-builder/app-graph-builder.ts +0 -137
  249. package/src/capabilities/app-graph-builder/index.ts +0 -7
  250. package/src/capabilities/operation-resolver/index.ts +0 -7
  251. package/src/capabilities/operation-resolver/operation-resolver.ts +0 -24
  252. package/src/capabilities/react-surface/index.ts +0 -7
  253. package/src/capabilities/react-surface/react-surface.tsx +0 -88
  254. package/src/cli/index.ts +0 -6
  255. package/src/containers/PluginRegistry/PluginRegistry.tsx +0 -86
  256. package/src/containers/PluginRegistry/index.ts +0 -7
  257. /package/dist/types/src/capabilities/{app-graph-builder/app-graph-builder.d.ts → app-graph-builder.d.ts} +0 -0
  258. /package/dist/types/src/capabilities/{react-surface/react-surface.d.ts → react-surface.d.ts} +0 -0
  259. /package/dist/types/src/{cli/commands → commands}/index.d.ts +0 -0
  260. /package/dist/types/src/{cli/commands → commands}/util.d.ts +0 -0
  261. /package/src/{cli/commands → commands}/disable.ts +0 -0
  262. /package/src/{cli/commands → commands}/enable.ts +0 -0
  263. /package/src/{cli/commands → commands}/index.ts +0 -0
  264. /package/src/{cli/commands → commands}/list.ts +0 -0
  265. /package/src/{cli/commands → commands}/util.ts +0 -0
@@ -0,0 +1,58 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { useCallback } from 'react';
6
+
7
+ import { type PluginManager } from '@dxos/app-framework';
8
+ import { useOperationInvoker } from '@dxos/app-framework/ui';
9
+ import { LayoutOperation } from '@dxos/app-toolkit';
10
+
11
+ import { DISABLE_DEPENDENTS_DIALOG } from '#meta';
12
+
13
+ /**
14
+ * Returns `requestDisable(pluginId)` that gates `manager.disable` on a
15
+ * confirmation prompt when the target has currently-enabled dependents.
16
+ *
17
+ * `PluginManager.disable` cascades by default; this hook is the UX safeguard
18
+ * around that. The caller supplies a `dispatch` callback that performs the
19
+ * actual disable (wrapping it with observability or any other side effects).
20
+ * When there are no enabled dependents, dispatch runs immediately; otherwise
21
+ * we open the shared {@link DISABLE_DEPENDENTS_DIALOG} surface — its
22
+ * `onConfirm` runs the dispatch and closes the dialog.
23
+ *
24
+ * Display-name resolution for the dialog is delegated to the registered
25
+ * plugin set (`Plugin.Meta.name`). Per-plugin i18n translations only load
26
+ * when a plugin is activated, so `meta.name` is the only label always
27
+ * available regardless of enabled state.
28
+ */
29
+ export const useDisableConfirmation = (manager: PluginManager.PluginManager, dispatch: (id: string) => void) => {
30
+ const { invokePromise } = useOperationInvoker();
31
+
32
+ return useCallback(
33
+ (pluginId: string): void => {
34
+ const enabledDependents = manager.getDependents(pluginId, { transitive: true, enabledOnly: true });
35
+ if (enabledDependents.length === 0) {
36
+ dispatch(pluginId);
37
+ return;
38
+ }
39
+ const resolveName = (id: string): string =>
40
+ manager.getPlugins().find((plugin) => plugin.meta.id === id)?.meta.name ?? id;
41
+ void invokePromise(LayoutOperation.UpdateDialog, {
42
+ subject: DISABLE_DEPENDENTS_DIALOG,
43
+ state: true,
44
+ type: 'alert',
45
+ props: {
46
+ pluginId,
47
+ dependents: enabledDependents,
48
+ onResolvePluginName: resolveName,
49
+ onConfirm: () => {
50
+ dispatch(pluginId);
51
+ void invokePromise(LayoutOperation.UpdateDialog, { state: false });
52
+ },
53
+ },
54
+ });
55
+ },
56
+ [manager, dispatch, invokePromise],
57
+ );
58
+ };
@@ -0,0 +1,41 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { useCallback, useEffect, useState } from 'react';
6
+
7
+ import { type Plugin, UrlLoader } from '@dxos/app-framework';
8
+
9
+ /**
10
+ * Tracks the per-plugin installed version (sourced from `UrlLoader` localStorage)
11
+ * as React state so post-install/update re-renders are deterministic — the value
12
+ * lives in component state rather than being read inline on every render.
13
+ *
14
+ * - Initialised from localStorage at mount.
15
+ * - Auto-resyncs when the plugin manager's plugin list changes (catches external
16
+ * installs / removes triggered by the list view).
17
+ * - `syncInstalledVersion()` is the explicit handle install/update operations
18
+ * call inside their `Effect.ensuring` so the post-mutation render sees both
19
+ * the new version and the cleared `installing`/`updating` flag together.
20
+ */
21
+ export const useInstalledVersionTag = (
22
+ pluginId: string,
23
+ plugins: readonly Plugin.Plugin[],
24
+ ): {
25
+ installedVersionTag: string | undefined;
26
+ syncInstalledVersion: () => void;
27
+ } => {
28
+ const [installedVersionTag, setInstalledVersionTag] = useState<string | undefined>(() =>
29
+ UrlLoader.getInstalledVersion(pluginId),
30
+ );
31
+
32
+ const syncInstalledVersion = useCallback(() => {
33
+ setInstalledVersionTag(UrlLoader.getInstalledVersion(pluginId));
34
+ }, [pluginId]);
35
+
36
+ useEffect(() => {
37
+ syncInstalledVersion();
38
+ }, [plugins, syncInstalledVersion]);
39
+
40
+ return { installedVersionTag, syncInstalledVersion };
41
+ };
@@ -0,0 +1,174 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import * as Effect from 'effect/Effect';
6
+ import { useCallback, useState } from 'react';
7
+
8
+ import { type PluginManager, type Registry, UrlLoader } from '@dxos/app-framework';
9
+ import { useOperationInvoker } from '@dxos/app-framework/ui';
10
+ import { LayoutOperation } from '@dxos/app-toolkit';
11
+ import { useAppGraph } from '@dxos/app-toolkit/ui';
12
+ import { runAndForwardErrors } from '@dxos/effect';
13
+ import { useNode } from '@dxos/plugin-graph';
14
+
15
+ import { getPluginSpecPath } from '#meta';
16
+
17
+ import { useDisableConfirmation } from './useDisableConfirmation';
18
+
19
+ export type PluginActionsProps = {
20
+ manager: PluginManager.PluginManager;
21
+ pluginId: string;
22
+ isInstalled: boolean;
23
+ moduleUrl: string | undefined;
24
+ catalogEntry: Registry.Plugin | undefined;
25
+ pickerVersions: readonly Registry.PluginVersion[];
26
+ selectedVersionTag: string | undefined;
27
+ syncInstalledVersion: () => void;
28
+ };
29
+
30
+ export type PluginActions = {
31
+ installing: boolean;
32
+ updating: boolean;
33
+ handleEnableChange: (enabled: boolean) => void;
34
+ handleUninstall: () => void;
35
+ handleInstall: () => void;
36
+ handleUpdate: () => void;
37
+ handleInstallVersion: () => void;
38
+ handleOpenSpec?: () => void;
39
+ };
40
+
41
+ /**
42
+ * The user-facing actions the article exposes — enable/disable, uninstall,
43
+ * install (latest), update (to latest catalog version), and install-from-picker —
44
+ * plus the `installing`/`updating` flags they drive. Each handler ends with a
45
+ * `syncInstalledVersion()` call inside its `Effect.ensuring` so the cached
46
+ * installed version snaps to the freshly-written localStorage value in the same
47
+ * render that clears the in-flight flag.
48
+ *
49
+ * `handleOpenSpec` is included only when a `spec` child graph node exists for
50
+ * this plugin — contributed by whichever plugin can render MDL (today
51
+ * `plugin-code`). When that contributor isn't enabled, the action is absent
52
+ * and the detail view hides its "Open Specification" chip.
53
+ */
54
+ export const usePluginActions = ({
55
+ manager,
56
+ pluginId,
57
+ isInstalled,
58
+ moduleUrl,
59
+ catalogEntry,
60
+ pickerVersions,
61
+ selectedVersionTag,
62
+ syncInstalledVersion,
63
+ }: PluginActionsProps): PluginActions => {
64
+ const { invokePromise } = useOperationInvoker();
65
+ const { graph } = useAppGraph();
66
+
67
+ const [installing, setInstalling] = useState(false);
68
+ const [updating, setUpdating] = useState(false);
69
+ const requestDisable = useDisableConfirmation(manager, (id) => void runAndForwardErrors(manager.disable(id)));
70
+
71
+ const specPath = getPluginSpecPath(pluginId);
72
+ const hasSpecNode = !!useNode(graph, specPath);
73
+ const handleOpenSpec = useCallback(() => {
74
+ void invokePromise(LayoutOperation.Open, { subject: [specPath] });
75
+ }, [invokePromise, specPath]);
76
+
77
+ const handleEnableChange = useCallback(
78
+ (enabled: boolean) => {
79
+ if (enabled) {
80
+ void runAndForwardErrors(manager.enable(pluginId));
81
+ return;
82
+ }
83
+
84
+ requestDisable(pluginId);
85
+ },
86
+ [manager, pluginId, requestDisable],
87
+ );
88
+
89
+ const handleUninstall = useCallback(() => {
90
+ void runAndForwardErrors(manager.remove(pluginId));
91
+ }, [manager, pluginId]);
92
+
93
+ const handleInstall = useCallback(() => {
94
+ if (!moduleUrl) {
95
+ return;
96
+ }
97
+
98
+ setInstalling(true);
99
+ void Effect.gen(function* () {
100
+ const added = yield* manager.add(moduleUrl);
101
+ yield* manager.enable(added.meta.id);
102
+ if (catalogEntry) {
103
+ UrlLoader.setInstalledVersion(added.meta.id, catalogEntry.version);
104
+ }
105
+ }).pipe(
106
+ Effect.ensuring(
107
+ Effect.sync(() => {
108
+ setInstalling(false);
109
+ syncInstalledVersion();
110
+ }),
111
+ ),
112
+ runAndForwardErrors,
113
+ );
114
+ }, [manager, moduleUrl, catalogEntry, syncInstalledVersion]);
115
+
116
+ const handleUpdate = useCallback(() => {
117
+ if (!moduleUrl || !catalogEntry) {
118
+ return;
119
+ }
120
+
121
+ setUpdating(true);
122
+ void Effect.gen(function* () {
123
+ yield* manager.remove(pluginId);
124
+ const added = yield* manager.add(moduleUrl);
125
+ yield* manager.enable(added.meta.id);
126
+ UrlLoader.setInstalledVersion(added.meta.id, catalogEntry.version);
127
+ }).pipe(
128
+ Effect.ensuring(
129
+ Effect.sync(() => {
130
+ setUpdating(false);
131
+ syncInstalledVersion();
132
+ }),
133
+ ),
134
+ runAndForwardErrors,
135
+ );
136
+ }, [manager, pluginId, moduleUrl, catalogEntry, syncInstalledVersion]);
137
+
138
+ const handleInstallVersion = useCallback(() => {
139
+ const version = pickerVersions.find((candidate) => candidate.tag === selectedVersionTag);
140
+ if (!version || !version.moduleUrl) {
141
+ return;
142
+ }
143
+
144
+ setInstalling(true);
145
+ void Effect.gen(function* () {
146
+ // Remove existing version before installing a different one.
147
+ if (isInstalled) {
148
+ yield* manager.remove(pluginId);
149
+ }
150
+ const added = yield* manager.add(version.moduleUrl);
151
+ yield* manager.enable(added.meta.id);
152
+ UrlLoader.setInstalledVersion(added.meta.id, version.tag);
153
+ }).pipe(
154
+ Effect.ensuring(
155
+ Effect.sync(() => {
156
+ setInstalling(false);
157
+ syncInstalledVersion();
158
+ }),
159
+ ),
160
+ runAndForwardErrors,
161
+ );
162
+ }, [manager, pluginId, isInstalled, selectedVersionTag, pickerVersions, syncInstalledVersion]);
163
+
164
+ return {
165
+ installing,
166
+ updating,
167
+ handleEnableChange,
168
+ handleUninstall,
169
+ handleInstall,
170
+ handleUpdate,
171
+ handleInstallVersion,
172
+ handleOpenSpec: hasSpecNode ? handleOpenSpec : undefined,
173
+ };
174
+ };
@@ -0,0 +1,29 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { useAtomValue } from '@effect-atom/atom-react';
6
+
7
+ import { type Registry } from '@dxos/app-framework';
8
+ import { usePluginManager } from '@dxos/app-framework/ui';
9
+
10
+ /**
11
+ * Reads the registry plugin catalog from `manager.pluginRegistry.plugins`.
12
+ * The catalog is loaded once when the plugin manager is constructed (with a
13
+ * provider) so this hook is purely reactive — subscribers re-render as the
14
+ * `{entries, loading, error}` state settles.
15
+ */
16
+ export const useRegistryPlugins = (): Registry.PluginsState => {
17
+ const manager = usePluginManager();
18
+ return useAtomValue(manager.pluginRegistry.plugins);
19
+ };
20
+
21
+ /**
22
+ * Returns the live `Registry.Manager` from the plugin manager so callers can
23
+ * issue `listVersions` / `getPlugin` calls directly. The catalog state itself
24
+ * is best read through {@link useRegistryPlugins} above.
25
+ */
26
+ export const useRegistryPluginProvider = (): Registry.Manager => {
27
+ const manager = usePluginManager();
28
+ return manager.pluginRegistry;
29
+ };
@@ -0,0 +1,31 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { type Registry, UrlLoader } from '@dxos/app-framework';
6
+
7
+ /**
8
+ * Returns a set of plugin ids for which the registry has a newer version
9
+ * than what was recorded at install time.
10
+ *
11
+ * Only flags plugins where a version was recorded at install time AND it differs
12
+ * from the catalog's current version. Legacy installs (no version stored) are ignored.
13
+ *
14
+ * NOTE: this is intentionally NOT memoised. The installed-version side of the
15
+ * comparison lives in localStorage and is mutated by `manager.add` /
16
+ * `manager.remove` / `UrlLoader.setInstalledVersion` outside React's view, so a
17
+ * memo keyed on `entries` would happily return stale data for the rest of the
18
+ * session after an update completes (the bug that produced "Update button
19
+ * reappears after Updating…"). The loop is O(catalog size) sync reads — cheap
20
+ * enough to do every render.
21
+ */
22
+ export const useUpdateAvailableIds = (entries: readonly Registry.Plugin[]): ReadonlySet<string> => {
23
+ const ids = new Set<string>();
24
+ for (const entry of entries) {
25
+ const installedVersion = UrlLoader.getInstalledVersion(entry.id);
26
+ if (installedVersion !== undefined && installedVersion !== entry.version) {
27
+ ids.add(entry.id);
28
+ }
29
+ }
30
+ return ids;
31
+ };
@@ -0,0 +1,112 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import * as Effect from 'effect/Effect';
6
+ import { type Dispatch, type SetStateAction, useEffect, useMemo, useState } from 'react';
7
+
8
+ import { type Registry, UrlLoader } from '@dxos/app-framework';
9
+ import { runAndForwardErrors } from '@dxos/effect';
10
+
11
+ /**
12
+ * Owns the version picker's state machine: fetches the available versions list
13
+ * from the provider, prepends the installed version when the catalog stub omits
14
+ * it, and keeps the selection clamped to whatever's currently in the list (so
15
+ * an external update doesn't strand the trigger on a tag that's no longer
16
+ * available).
17
+ */
18
+ export const useVersionPicker = ({
19
+ provider,
20
+ pluginId,
21
+ repo,
22
+ moduleUrl,
23
+ installedVersionTag,
24
+ }: {
25
+ provider: Registry.Manager;
26
+ pluginId: string;
27
+ repo: string | undefined;
28
+ moduleUrl: string | undefined;
29
+ installedVersionTag: string | undefined;
30
+ }): {
31
+ pickerVersions: readonly Registry.PluginVersion[];
32
+ selectedVersionTag: string | undefined;
33
+ setSelectedVersionTag: Dispatch<SetStateAction<string | undefined>>;
34
+ } => {
35
+ const [versions, setVersions] = useState<readonly Registry.PluginVersion[]>([]);
36
+ const [selectedVersionTag, setSelectedVersionTag] = useState<string | undefined>();
37
+
38
+ // Load version list once the catalog entry's repo is known. Reset state
39
+ // when `repo` is absent or the fetch fails so a previous plugin's
40
+ // versions don't leak into the picker for the current plugin. The
41
+ // `cancelled` flag guards against a stale in-flight `listVersions`
42
+ // response overwriting the newer state after `repo`/`pluginId` changes.
43
+ useEffect(() => {
44
+ if (!repo) {
45
+ setVersions([]);
46
+ setSelectedVersionTag(undefined);
47
+ return;
48
+ }
49
+ // Clear stale picker state before fetching so the UI doesn't display the
50
+ // previous plugin's versions/selection while `listVersions` is pending.
51
+ setVersions([]);
52
+ setSelectedVersionTag(undefined);
53
+ let cancelled = false;
54
+ void provider.listVersions(repo).pipe(
55
+ Effect.match({
56
+ onSuccess: (vs) => {
57
+ if (cancelled) {
58
+ return;
59
+ }
60
+ setVersions(vs);
61
+ // Default selection: the currently installed version, or the latest.
62
+ const installedVersion = UrlLoader.getInstalledVersion(pluginId);
63
+ setSelectedVersionTag(installedVersion ?? vs[0]?.tag);
64
+ },
65
+ onFailure: () => {
66
+ if (cancelled) {
67
+ return;
68
+ }
69
+ setVersions([]);
70
+ setSelectedVersionTag(undefined);
71
+ },
72
+ }),
73
+ runAndForwardErrors,
74
+ );
75
+ return () => {
76
+ cancelled = true;
77
+ };
78
+ }, [provider, repo, pluginId]);
79
+
80
+ // Make sure the picker always lists the installed version, even if the catalog
81
+ // hasn't surfaced it (the current `listVersions` stub only returns latest).
82
+ const pickerVersions = useMemo<readonly Registry.PluginVersion[]>(() => {
83
+ if (!installedVersionTag) {
84
+ return versions;
85
+ }
86
+ if (versions.some((entry) => entry.tag === installedVersionTag)) {
87
+ return versions;
88
+ }
89
+ const installedEntry: Registry.PluginVersion = {
90
+ tag: installedVersionTag,
91
+ // Picker won't be re-installing this entry unless the user selects + clicks Install,
92
+ // and the catalog moduleUrl is the closest stand-in we have.
93
+ moduleUrl: moduleUrl ?? '',
94
+ };
95
+ return [installedEntry, ...versions];
96
+ }, [versions, installedVersionTag, moduleUrl]);
97
+
98
+ // Keep `selectedVersionTag` in sync when picker contents change (e.g. the user
99
+ // updated the plugin from the list view — installed version moved, picker
100
+ // shrunk, prior selection is no longer in the list).
101
+ useEffect(() => {
102
+ if (pickerVersions.length === 0) {
103
+ return;
104
+ }
105
+ if (selectedVersionTag && pickerVersions.some((entry) => entry.tag === selectedVersionTag)) {
106
+ return;
107
+ }
108
+ setSelectedVersionTag(installedVersionTag ?? pickerVersions[0]?.tag);
109
+ }, [pickerVersions, selectedVersionTag, installedVersionTag]);
110
+
111
+ return { pickerVersions, selectedVersionTag, setSelectedVersionTag };
112
+ };
package/src/index.ts CHANGED
@@ -3,4 +3,4 @@
3
3
  //
4
4
 
5
5
  export * from './meta';
6
- export * from './RegistryPlugin';
6
+ export * from './storage';
package/src/meta.ts CHANGED
@@ -2,20 +2,46 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { type Plugin } from '@dxos/app-framework';
5
+ import { Plugin } from '@dxos/app-framework';
6
+ import { pinnedWorkspaceId } from '@dxos/app-toolkit';
7
+ import { DXN } from '@dxos/keys';
6
8
  import { trim } from '@dxos/util';
7
9
 
8
- // TODO(wittjosiah): Deck does not currently support `/` in ids.
9
- // TODO(wittjosiah): This is a hack to prevent the previous deck from being set for pinned items.
10
- // Ideally this should be worked into the data model in a generic way.
11
- export const REGISTRY_ID = '!dxos:plugin-registry';
10
+ export const REGISTRY_ID = pinnedWorkspaceId('dxos:plugin-registry');
12
11
  export const REGISTRY_KEY = 'plugin-registry';
13
12
 
14
- export const meta: Plugin.Meta = {
15
- id: 'dxos.org/plugin/registry',
13
+ // TODO(wittjosiah): Should this be a special separator or use the standard path separator?
14
+ const CATEGORY_SEPARATOR = '>';
15
+
16
+ /** Build a registry category node ID. */
17
+ export const registryCategoryId = (category: string): string => `${REGISTRY_KEY}${CATEGORY_SEPARATOR}${category}`;
18
+
19
+ /** Qualified graph path to a specific plugin node. */
20
+ export const getPluginPath = (pluginId: string): string => `root/${REGISTRY_ID}/${pluginId}`;
21
+
22
+ /**
23
+ * Qualified graph path to the bundled MDL spec child node for a plugin.
24
+ *
25
+ * The child is contributed by whichever plugin can render MDL (today:
26
+ * `plugin-code`). plugin-registry only knows the path convention so it can:
27
+ * - dispatch `LayoutOperation.Open` to open the spec viewer, and
28
+ * - probe the app graph for the child's existence to gate the "View
29
+ * specification" button. If no plugin contributes a renderer for the
30
+ * child, the node is absent and the button stays hidden.
31
+ */
32
+ export const getPluginSpecPath = (pluginId: string): string => `${getPluginPath(pluginId)}/spec`;
33
+
34
+ export const meta = Plugin.makeMeta({
35
+ key: DXN.make('org.dxos.plugin.registry'),
16
36
  name: 'Plugins',
37
+ author: 'DXOS',
17
38
  description: trim`
18
39
  Plugin management system for discovering, installing, and configuring workspace extensions.
19
40
  Browse available plugins and customize your workspace capabilities.
20
41
  `,
21
- };
42
+ icon: 'ph--squares-four--regular',
43
+ tags: ['system'],
44
+ });
45
+
46
+ /** Cascade-disable confirmation dialog surface id. */
47
+ export const DISABLE_DEPENDENTS_DIALOG = DXN.make(`${meta.id}.disableDependentsDialog`);
@@ -0,0 +1,7 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { OperationHandlerSet } from '@dxos/compute';
6
+
7
+ export const RegistryOperationHandlerSet = OperationHandlerSet.lazy(() => import('./open-plugin-registry'));
@@ -0,0 +1,21 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Effect from 'effect/Effect';
6
+
7
+ import { LayoutOperation, SettingsOperation, getSpacePath } from '@dxos/app-toolkit';
8
+ import { Operation } from '@dxos/compute';
9
+
10
+ import { REGISTRY_ID } from '#meta';
11
+
12
+ const handler: Operation.WithHandler<typeof SettingsOperation.OpenPluginRegistry> =
13
+ SettingsOperation.OpenPluginRegistry.pipe(
14
+ Operation.withHandler(
15
+ Effect.fnUntraced(function* () {
16
+ yield* Operation.invoke(LayoutOperation.SwitchWorkspace, { subject: getSpacePath(REGISTRY_ID) });
17
+ }),
18
+ ),
19
+ );
20
+
21
+ export default handler;
package/src/plugin.ts ADDED
@@ -0,0 +1,11 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { Plugin } from '@dxos/app-framework';
6
+
7
+ import { meta } from './meta';
8
+
9
+ export const RegistryPlugin = Plugin.lazy(meta, () => import('#plugin'));
10
+
11
+ export { RegistryOperationHandlerSet } from './operations';
@@ -6,37 +6,31 @@ import * as FileSystem from '@effect/platform/FileSystem';
6
6
  import * as Path from '@effect/platform/Path';
7
7
  import * as Effect from 'effect/Effect';
8
8
  import * as Schema from 'effect/Schema';
9
+ import * as Yaml from 'yaml';
9
10
 
10
- import { DX_CONFIG, getProfilePath } from '@dxos/client-protocol';
11
+ import { DX_CONFIG } from '@dxos/client-protocol';
11
12
 
12
13
  // TODO(wittjosiah): Factor out to app-framework?
13
14
 
14
- const PLUGINS_FILE = 'plugins.json';
15
-
16
15
  const PluginsSchema = Schema.Array(Schema.String);
17
16
 
18
- /**
19
- * Get the path to the plugins configuration file for a profile.
20
- */
21
- const getPluginsPath = (profile: string): Effect.Effect<string, never, Path.Path> =>
22
- Effect.gen(function* () {
23
- const path = yield* Path.Path;
24
- return path.join(getProfilePath(DX_CONFIG, profile), PLUGINS_FILE);
25
- });
17
+ /** CLI-only: path to plugins/<profile>.yml (sibling to profile/). */
18
+ const getPluginsConfigPath = (profile: string) => `${DX_CONFIG}/plugins/${profile}.yml`;
26
19
 
27
20
  /**
28
- * Load enabled plugins from profile storage.
21
+ * Load enabled plugins from plugins/<profile>.yml (sibling to profile/).
29
22
  * Returns the list of enabled plugin IDs, or an empty array if the file doesn't exist.
30
23
  */
31
24
  export const loadEnabledPlugins = Effect.fn(function* ({ profile }: { profile: string }) {
32
25
  const fs = yield* FileSystem.FileSystem;
33
- const pluginsPath = yield* getPluginsPath(profile);
26
+ const pluginsPath = getPluginsConfigPath(profile);
34
27
 
35
28
  const content = yield* fs
36
29
  .readFileString(pluginsPath)
37
30
  .pipe(Effect.catchTag('SystemError', () => Effect.succeed('[]')));
38
31
 
39
- const parsed = yield* Schema.decodeUnknown(Schema.parseJson(PluginsSchema))(content).pipe(
32
+ const raw = Yaml.parse(content);
33
+ const parsed = yield* Schema.decodeUnknown(PluginsSchema)(raw ?? []).pipe(
40
34
  Effect.catchAll(() => Effect.succeed([] as string[])),
41
35
  );
42
36
 
@@ -44,14 +38,13 @@ export const loadEnabledPlugins = Effect.fn(function* ({ profile }: { profile: s
44
38
  });
45
39
 
46
40
  /**
47
- * Save enabled plugins to profile storage.
41
+ * Save enabled plugins to plugins/<profile>.yml.
48
42
  */
49
43
  export const saveEnabledPlugins = Effect.fn(function* ({ profile, enabled }: { profile: string; enabled: string[] }) {
50
44
  const fs = yield* FileSystem.FileSystem;
51
- const pluginsPath = yield* getPluginsPath(profile);
52
- const profileDir = getProfilePath(DX_CONFIG, profile);
53
-
54
- yield* fs.makeDirectory(profileDir, { recursive: true });
55
- const encoded = Schema.encodeSync(Schema.parseJson(PluginsSchema))(enabled);
56
- yield* fs.writeFileString(pluginsPath, encoded + '\n');
45
+ const path = yield* Path.Path;
46
+ const pluginsPath = getPluginsConfigPath(profile);
47
+ yield* fs.makeDirectory(path.dirname(pluginsPath), { recursive: true });
48
+ const encoded = Yaml.stringify(enabled);
49
+ yield* fs.writeFileString(pluginsPath, encoded);
57
50
  });