@dxos/plugin-registry 0.8.4-main.70d3990 → 0.8.4-main.74a063c4e0

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 (133) hide show
  1. package/dist/lib/browser/cli/index.mjs +219 -0
  2. package/dist/lib/browser/cli/index.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +52 -50
  4. package/dist/lib/browser/index.mjs.map +4 -4
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/node-esm/cli/index.mjs +221 -0
  7. package/dist/lib/node-esm/cli/index.mjs.map +7 -0
  8. package/dist/lib/node-esm/index.mjs +73 -0
  9. package/dist/lib/node-esm/index.mjs.map +7 -0
  10. package/dist/lib/node-esm/meta.json +1 -0
  11. package/dist/types/src/RegistryPlugin.d.ts +2 -1
  12. package/dist/types/src/RegistryPlugin.d.ts.map +1 -1
  13. package/dist/types/src/capabilities/app-graph-builder.d.ts +4 -2
  14. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -1
  15. package/dist/types/src/capabilities/index.d.ts +5 -3
  16. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  17. package/dist/types/src/capabilities/operation-handler.d.ts +6 -0
  18. package/dist/types/src/capabilities/operation-handler.d.ts.map +1 -0
  19. package/dist/types/src/capabilities/react-surface.d.ts +3 -2
  20. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -1
  21. package/dist/types/src/cli/commands/disable.d.ts +11 -0
  22. package/dist/types/src/cli/commands/disable.d.ts.map +1 -0
  23. package/dist/types/src/cli/commands/enable.d.ts +11 -0
  24. package/dist/types/src/cli/commands/enable.d.ts.map +1 -0
  25. package/dist/types/src/cli/commands/index.d.ts +3 -0
  26. package/dist/types/src/cli/commands/index.d.ts.map +1 -0
  27. package/dist/types/src/cli/commands/list.d.ts +7 -0
  28. package/dist/types/src/cli/commands/list.d.ts.map +1 -0
  29. package/dist/types/src/cli/commands/util.d.ts +11 -0
  30. package/dist/types/src/cli/commands/util.d.ts.map +1 -0
  31. package/dist/types/src/cli/index.d.ts +3 -0
  32. package/dist/types/src/cli/index.d.ts.map +1 -0
  33. package/dist/types/src/cli/plugin.d.ts +3 -0
  34. package/dist/types/src/cli/plugin.d.ts.map +1 -0
  35. package/dist/types/src/cli/storage.d.ts +18 -0
  36. package/dist/types/src/cli/storage.d.ts.map +1 -0
  37. package/dist/types/src/components/PluginDetail/PluginDetail.d.ts +15 -0
  38. package/dist/types/src/components/PluginDetail/PluginDetail.d.ts.map +1 -0
  39. package/dist/types/src/components/PluginDetail/PluginDetail.stories.d.ts +43 -0
  40. package/dist/types/src/components/PluginDetail/PluginDetail.stories.d.ts.map +1 -0
  41. package/dist/types/src/components/PluginDetail/index.d.ts +2 -0
  42. package/dist/types/src/components/PluginDetail/index.d.ts.map +1 -0
  43. package/dist/types/src/components/{PluginItem.d.ts → PluginList/PluginItem.d.ts} +2 -2
  44. package/dist/types/src/components/PluginList/PluginItem.d.ts.map +1 -0
  45. package/dist/types/src/components/{PluginList.d.ts → PluginList/PluginList.d.ts} +1 -1
  46. package/dist/types/src/components/PluginList/PluginList.d.ts.map +1 -0
  47. package/dist/types/src/components/PluginList/PluginList.stories.d.ts +38 -0
  48. package/dist/types/src/components/PluginList/PluginList.stories.d.ts.map +1 -0
  49. package/dist/types/src/components/PluginList/index.d.ts +2 -0
  50. package/dist/types/src/components/PluginList/index.d.ts.map +1 -0
  51. package/dist/types/src/components/index.d.ts +0 -1
  52. package/dist/types/src/components/index.d.ts.map +1 -1
  53. package/dist/types/src/containers/LoadPluginDialog/LoadPluginDialog.d.ts +4 -0
  54. package/dist/types/src/containers/LoadPluginDialog/LoadPluginDialog.d.ts.map +1 -0
  55. package/dist/types/src/containers/LoadPluginDialog/index.d.ts +2 -0
  56. package/dist/types/src/containers/LoadPluginDialog/index.d.ts.map +1 -0
  57. package/dist/types/src/containers/PluginArticle/PluginArticle.d.ts +7 -0
  58. package/dist/types/src/containers/PluginArticle/PluginArticle.d.ts.map +1 -0
  59. package/dist/types/src/containers/PluginArticle/index.d.ts +2 -0
  60. package/dist/types/src/containers/PluginArticle/index.d.ts.map +1 -0
  61. package/dist/types/src/containers/PluginRegistry/PluginRegistry.d.ts +14 -0
  62. package/dist/types/src/containers/PluginRegistry/PluginRegistry.d.ts.map +1 -0
  63. package/dist/types/src/containers/PluginRegistry/index.d.ts +2 -0
  64. package/dist/types/src/containers/PluginRegistry/index.d.ts.map +1 -0
  65. package/dist/types/src/containers/index.d.ts +6 -0
  66. package/dist/types/src/containers/index.d.ts.map +1 -0
  67. package/dist/types/src/meta.d.ts +7 -3
  68. package/dist/types/src/meta.d.ts.map +1 -1
  69. package/dist/types/src/operations/index.d.ts +3 -0
  70. package/dist/types/src/operations/index.d.ts.map +1 -0
  71. package/dist/types/src/operations/open-plugin-registry.d.ts +5 -0
  72. package/dist/types/src/operations/open-plugin-registry.d.ts.map +1 -0
  73. package/dist/types/src/translations.d.ts +19 -15
  74. package/dist/types/src/translations.d.ts.map +1 -1
  75. package/dist/types/tsconfig.tsbuildinfo +1 -1
  76. package/package.json +58 -32
  77. package/src/RegistryPlugin.tsx +12 -25
  78. package/src/capabilities/app-graph-builder.ts +128 -135
  79. package/src/capabilities/index.ts +8 -4
  80. package/src/capabilities/operation-handler.ts +16 -0
  81. package/src/capabilities/react-surface.tsx +84 -75
  82. package/src/cli/commands/disable.ts +47 -0
  83. package/src/cli/commands/enable.ts +44 -0
  84. package/src/cli/commands/index.ts +14 -0
  85. package/src/cli/commands/list.ts +41 -0
  86. package/src/cli/commands/util.ts +24 -0
  87. package/src/cli/index.ts +6 -0
  88. package/src/cli/plugin.ts +17 -0
  89. package/src/cli/storage.ts +50 -0
  90. package/src/components/PluginDetail/PluginDetail.stories.tsx +43 -0
  91. package/src/components/PluginDetail/PluginDetail.tsx +84 -0
  92. package/src/components/PluginDetail/index.ts +5 -0
  93. package/src/components/{PluginItem.tsx → PluginList/PluginItem.tsx} +20 -16
  94. package/src/components/PluginList/PluginList.stories.tsx +87 -0
  95. package/src/components/{PluginList.tsx → PluginList/PluginList.tsx} +2 -2
  96. package/src/components/PluginList/index.ts +5 -0
  97. package/src/components/index.ts +0 -1
  98. package/src/containers/LoadPluginDialog/LoadPluginDialog.tsx +84 -0
  99. package/src/containers/LoadPluginDialog/index.ts +5 -0
  100. package/src/containers/PluginArticle/PluginArticle.tsx +28 -0
  101. package/src/containers/PluginArticle/index.ts +5 -0
  102. package/src/containers/PluginRegistry/PluginRegistry.tsx +94 -0
  103. package/src/containers/PluginRegistry/index.ts +5 -0
  104. package/src/containers/index.ts +10 -0
  105. package/src/meta.ts +14 -7
  106. package/src/operations/index.ts +7 -0
  107. package/src/operations/open-plugin-registry.ts +21 -0
  108. package/src/translations.ts +19 -15
  109. package/dist/lib/browser/app-graph-builder-6CNBP3A4.mjs +0 -162
  110. package/dist/lib/browser/app-graph-builder-6CNBP3A4.mjs.map +0 -7
  111. package/dist/lib/browser/chunk-XPEFEID2.mjs +0 -19
  112. package/dist/lib/browser/chunk-XPEFEID2.mjs.map +0 -7
  113. package/dist/lib/browser/intent-resolver-LKNUYBTN.mjs +0 -23
  114. package/dist/lib/browser/intent-resolver-LKNUYBTN.mjs.map +0 -7
  115. package/dist/lib/browser/react-surface-IGCXOVE2.mjs +0 -359
  116. package/dist/lib/browser/react-surface-IGCXOVE2.mjs.map +0 -7
  117. package/dist/types/src/capabilities/intent-resolver.d.ts +0 -4
  118. package/dist/types/src/capabilities/intent-resolver.d.ts.map +0 -1
  119. package/dist/types/src/components/PluginDetail.d.ts +0 -9
  120. package/dist/types/src/components/PluginDetail.d.ts.map +0 -1
  121. package/dist/types/src/components/PluginDetail.stories.d.ts +0 -32
  122. package/dist/types/src/components/PluginDetail.stories.d.ts.map +0 -1
  123. package/dist/types/src/components/PluginItem.d.ts.map +0 -1
  124. package/dist/types/src/components/PluginList.d.ts.map +0 -1
  125. package/dist/types/src/components/PluginList.stories.d.ts +0 -34
  126. package/dist/types/src/components/PluginList.stories.d.ts.map +0 -1
  127. package/dist/types/src/components/RegistryContainer.d.ts +0 -7
  128. package/dist/types/src/components/RegistryContainer.d.ts.map +0 -1
  129. package/src/capabilities/intent-resolver.ts +0 -27
  130. package/src/components/PluginDetail.stories.tsx +0 -45
  131. package/src/components/PluginDetail.tsx +0 -82
  132. package/src/components/PluginList.stories.tsx +0 -80
  133. package/src/components/RegistryContainer.tsx +0 -76
@@ -2,85 +2,94 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import React, { useCallback, useMemo } from 'react';
5
+ import * as Effect from 'effect/Effect';
6
+ import React, { useMemo } from 'react';
6
7
 
7
- import { Capabilities, Plugin, contributes, createSurface } from '@dxos/app-framework';
8
- import { usePluginManager } from '@dxos/app-framework/react';
8
+ import { Capabilities, Capability } from '@dxos/app-framework';
9
+ import { Surface, usePluginManager } from '@dxos/app-framework/ui';
10
+ import { AppSurface } from '@dxos/app-toolkit/ui';
9
11
 
10
- import { PluginDetail, RegistryContainer } from '../components';
11
- import { REGISTRY_KEY, meta } from '../meta';
12
+ import { LOAD_PLUGIN_DIALOG, LoadPluginDialog, PluginArticle, PluginRegistry } from '#containers';
13
+ import { registryCategoryId } from '#meta';
12
14
 
13
- export default () =>
14
- contributes(Capabilities.ReactSurface, [
15
- createSurface({
16
- id: `${meta.id}/all`,
17
- role: 'article',
18
- filter: (data): data is any => data.subject === `${REGISTRY_KEY}+all`,
19
- component: () => {
20
- const manager = usePluginManager();
21
- const filtered = useMemo(() => manager.plugins.filter(({ meta }) => !manager.core.includes(meta.id)), []);
15
+ export default Capability.makeModule(() =>
16
+ Effect.succeed(
17
+ Capability.contributes(Capabilities.ReactSurface, [
18
+ Surface.create({
19
+ id: 'all',
20
+ role: 'article',
21
+ filter: AppSurface.literalSection(registryCategoryId('all')),
22
+ component: () => {
23
+ const manager = usePluginManager();
24
+ const filtered = useMemo(
25
+ () => manager.getPlugins().filter(({ meta }) => !manager.getCore().includes(meta.id)),
26
+ [],
27
+ );
22
28
 
23
- return <RegistryContainer id={`${REGISTRY_KEY}+all`} plugins={filtered} />;
24
- },
25
- }),
26
- createSurface({
27
- id: `${meta.id}/installed`,
28
- role: 'article',
29
- filter: (data): data is any => data.subject === `${REGISTRY_KEY}+installed`,
30
- component: () => {
31
- const manager = usePluginManager();
32
- const filtered = useMemo(
33
- () =>
34
- manager.plugins
35
- .filter(({ meta }) => !manager.core.includes(meta.id))
36
- .filter(({ meta }) => manager.enabled.includes(meta.id)),
37
- [],
38
- );
29
+ return <PluginRegistry id={registryCategoryId('all')} plugins={filtered} />;
30
+ },
31
+ }),
32
+ Surface.create({
33
+ id: 'installed',
34
+ role: 'article',
35
+ filter: AppSurface.literalSection(registryCategoryId('installed')),
36
+ component: () => {
37
+ const manager = usePluginManager();
38
+ const filtered = useMemo(
39
+ () =>
40
+ manager
41
+ .getPlugins()
42
+ .filter(({ meta }) => !manager.getCore().includes(meta.id))
43
+ .filter(({ meta }) => manager.getEnabled().includes(meta.id)),
44
+ [],
45
+ );
39
46
 
40
- return <RegistryContainer id={`${REGISTRY_KEY}+installed`} plugins={filtered} />;
41
- },
42
- }),
43
- createSurface({
44
- id: `${meta.id}/recommended`,
45
- role: 'article',
46
- filter: (data): data is any => data.subject === `${REGISTRY_KEY}+recommended`,
47
- component: () => {
48
- const manager = usePluginManager();
49
- const filtered = useMemo(
50
- () =>
51
- manager.plugins
52
- .filter(({ meta }) => !manager.core.includes(meta.id))
53
- .filter(({ meta }) => !meta.tags?.includes('labs')),
54
- [],
55
- );
47
+ return <PluginRegistry id={registryCategoryId('installed')} plugins={filtered} />;
48
+ },
49
+ }),
50
+ Surface.create({
51
+ id: 'recommended',
52
+ role: 'article',
53
+ filter: AppSurface.literalSection(registryCategoryId('recommended')),
54
+ component: () => {
55
+ const manager = usePluginManager();
56
+ const filtered = useMemo(
57
+ () =>
58
+ manager
59
+ .getPlugins()
60
+ .filter(({ meta }) => !manager.getCore().includes(meta.id))
61
+ .filter(({ meta }) => !meta.tags?.includes('labs')),
62
+ [],
63
+ );
56
64
 
57
- return <RegistryContainer id={`${REGISTRY_KEY}+recommended`} plugins={filtered} />;
58
- },
59
- }),
60
- createSurface({
61
- id: `${meta.id}/labs`,
62
- role: 'article',
63
- filter: (data): data is any => data.subject === `${REGISTRY_KEY}+labs`,
64
- component: () => {
65
- const manager = usePluginManager();
66
- const filtered = useMemo(() => manager.plugins.filter(({ meta }) => meta.tags?.includes('labs')), []);
65
+ return <PluginRegistry id={registryCategoryId('recommended')} plugins={filtered} />;
66
+ },
67
+ }),
68
+ Surface.create({
69
+ id: 'labs',
70
+ role: 'article',
71
+ filter: AppSurface.literalSection(registryCategoryId('labs')),
72
+ component: () => {
73
+ const manager = usePluginManager();
74
+ const filtered = useMemo(() => manager.getPlugins().filter(({ meta }) => meta.tags?.includes('labs')), []);
67
75
 
68
- return <RegistryContainer id={`${REGISTRY_KEY}+labs`} plugins={filtered} />;
69
- },
70
- }),
71
- createSurface({
72
- id: `${meta.id}/plugin-details`,
73
- role: 'article',
74
- filter: (data): data is { subject: Plugin } => data.subject instanceof Plugin,
75
- component: ({ data: { subject } }) => {
76
- const manager = usePluginManager();
77
- const enabled = manager.enabled.includes(subject.meta.id);
78
- const handleEnable = useCallback(
79
- () => (enabled ? manager.disable(subject.meta.id) : manager.enable(subject.meta.id)),
80
- [manager, subject.meta.id, enabled],
81
- );
82
-
83
- return <PluginDetail plugin={subject} enabled={enabled} onEnable={handleEnable} />;
84
- },
85
- }),
86
- ]);
76
+ return <PluginRegistry id={registryCategoryId('labs')} plugins={filtered} />;
77
+ },
78
+ }),
79
+ Surface.create({
80
+ id: 'plugin-details',
81
+ role: 'article',
82
+ filter: AppSurface.pluginSection(),
83
+ component: ({ data: { subject } }) => {
84
+ return <PluginArticle subject={subject} />;
85
+ },
86
+ }),
87
+ Surface.create({
88
+ id: LOAD_PLUGIN_DIALOG,
89
+ role: 'dialog',
90
+ filter: AppSurface.componentDialog(LOAD_PLUGIN_DIALOG),
91
+ component: () => <LoadPluginDialog />,
92
+ }),
93
+ ]),
94
+ ),
95
+ );
@@ -0,0 +1,47 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Args from '@effect/cli/Args';
6
+ import * as Command from '@effect/cli/Command';
7
+ import * as Console from 'effect/Console';
8
+ import * as Effect from 'effect/Effect';
9
+
10
+ import { Plugin } from '@dxos/app-framework';
11
+ import { CommandConfig } from '@dxos/cli-util';
12
+ import { invariant } from '@dxos/invariant';
13
+
14
+ import { saveEnabledPlugins } from '../storage';
15
+
16
+ export const handler = Effect.fn(function* ({ id }: { id: string }) {
17
+ const { json, profile } = yield* CommandConfig;
18
+ const manager = yield* Plugin.Service;
19
+
20
+ const plugins = manager.getPlugins();
21
+ const plugin = plugins.find((p: Plugin.Plugin) => p.meta.id === id);
22
+ invariant(plugin, `Plugin not found: ${id}`);
23
+
24
+ const core = manager.getCore();
25
+ invariant(!core.includes(id), `Cannot disable core plugin: ${id}`);
26
+
27
+ const disabled = yield* manager.disable(id);
28
+ invariant(disabled, `Failed to disable plugin: ${id}`);
29
+
30
+ // Save enabled plugins to storage
31
+ const enabledPlugins = manager.getEnabled();
32
+ yield* saveEnabledPlugins({ profile, enabled: [...enabledPlugins] });
33
+
34
+ if (json) {
35
+ yield* Console.log(JSON.stringify({ id, enabled: false }, null, 2));
36
+ } else {
37
+ yield* Console.log(`Plugin "${plugin.meta.name ?? id}" disabled.`);
38
+ }
39
+ });
40
+
41
+ export const disable = Command.make(
42
+ 'disable',
43
+ {
44
+ id: Args.text({ name: 'id' }).pipe(Args.withDescription('The ID of the plugin to disable.')),
45
+ },
46
+ handler,
47
+ ).pipe(Command.withDescription('Disable a plugin.'));
@@ -0,0 +1,44 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Args from '@effect/cli/Args';
6
+ import * as Command from '@effect/cli/Command';
7
+ import * as Console from 'effect/Console';
8
+ import * as Effect from 'effect/Effect';
9
+
10
+ import { Plugin } from '@dxos/app-framework';
11
+ import { CommandConfig } from '@dxos/cli-util';
12
+ import { invariant } from '@dxos/invariant';
13
+
14
+ import { saveEnabledPlugins } from '../storage';
15
+
16
+ export const handler = Effect.fn(function* ({ id }: { id: string }) {
17
+ const { json, profile } = yield* CommandConfig;
18
+ const manager = yield* Plugin.Service;
19
+
20
+ const plugins = manager.getPlugins();
21
+ const plugin = plugins.find((p: Plugin.Plugin) => p.meta.id === id);
22
+ invariant(plugin, `Plugin not found: ${id}`);
23
+
24
+ const enabled = yield* manager.enable(id);
25
+ invariant(enabled, `Failed to enable plugin: ${id}`);
26
+
27
+ // Save enabled plugins to storage
28
+ const enabledPlugins = manager.getEnabled();
29
+ yield* saveEnabledPlugins({ profile, enabled: [...enabledPlugins] });
30
+
31
+ if (json) {
32
+ yield* Console.log(JSON.stringify({ id, enabled: true }, null, 2));
33
+ } else {
34
+ yield* Console.log(`Plugin "${plugin.meta.name ?? id}" enabled.`);
35
+ }
36
+ });
37
+
38
+ export const enable = Command.make(
39
+ 'enable',
40
+ {
41
+ id: Args.text({ name: 'id' }).pipe(Args.withDescription('The ID of the plugin to enable.')),
42
+ },
43
+ handler,
44
+ ).pipe(Command.withDescription('Enable a plugin.'));
@@ -0,0 +1,14 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Command from '@effect/cli/Command';
6
+
7
+ import { disable } from './disable';
8
+ import { enable } from './enable';
9
+ import { list } from './list';
10
+
11
+ export const plugin: Command.Command<any, any, any, any> = Command.make('plugin').pipe(
12
+ Command.withDescription('Manage plugins.'),
13
+ Command.withSubcommands([enable, disable, list]),
14
+ );
@@ -0,0 +1,41 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Command from '@effect/cli/Command';
6
+ import * as Console from 'effect/Console';
7
+ import * as Effect from 'effect/Effect';
8
+
9
+ import { Plugin } from '@dxos/app-framework';
10
+ import { CommandConfig, printList } from '@dxos/cli-util';
11
+
12
+ import { type FormattedPlugin, printPlugin } from './util';
13
+
14
+ export const handler = Effect.fn(function* () {
15
+ const { json } = yield* CommandConfig;
16
+ const manager = yield* Plugin.Service;
17
+
18
+ const plugins = manager.getPlugins();
19
+ const enabled = manager.getEnabled();
20
+ const core = manager.getCore();
21
+
22
+ const formattedPlugins: FormattedPlugin[] = plugins.map((plugin: Plugin.Plugin) => {
23
+ const isEnabled = enabled.includes(plugin.meta.id);
24
+ const isCore = core.includes(plugin.meta.id);
25
+ return {
26
+ id: plugin.meta.id,
27
+ name: plugin.meta.name ?? plugin.meta.id,
28
+ enabled: isEnabled,
29
+ core: isCore,
30
+ };
31
+ });
32
+
33
+ if (json) {
34
+ yield* Console.log(JSON.stringify(formattedPlugins, null, 2));
35
+ } else {
36
+ const formatted = formattedPlugins.map(printPlugin);
37
+ yield* Console.log(printList(formatted));
38
+ }
39
+ });
40
+
41
+ export const list = Command.make('list', {}, handler).pipe(Command.withDescription('List all available plugins.'));
@@ -0,0 +1,24 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { FormBuilder } from '@dxos/cli-util';
6
+
7
+ export type FormattedPlugin = {
8
+ id: string;
9
+ name: string;
10
+ enabled: boolean;
11
+ core: boolean;
12
+ };
13
+
14
+ /**
15
+ * Pretty prints a plugin with ANSI colors.
16
+ */
17
+ export const printPlugin = (plugin: FormattedPlugin) => {
18
+ const status = plugin.core ? 'core' : plugin.enabled ? 'enabled' : 'disabled';
19
+ return FormBuilder.make({ title: plugin.name }).pipe(
20
+ FormBuilder.set('id', plugin.id),
21
+ FormBuilder.set('status', status),
22
+ FormBuilder.build,
23
+ );
24
+ };
@@ -0,0 +1,6 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './plugin';
6
+ export * from './storage';
@@ -0,0 +1,17 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { Plugin } from '@dxos/app-framework';
6
+ import { AppPlugin } from '@dxos/app-toolkit';
7
+
8
+ import { meta } from '#meta';
9
+
10
+ import { plugin } from './commands';
11
+
12
+ export const RegistryPlugin = Plugin.define(meta).pipe(
13
+ AppPlugin.addCommandModule({
14
+ commands: [plugin],
15
+ }),
16
+ Plugin.make,
17
+ );
@@ -0,0 +1,50 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as FileSystem from '@effect/platform/FileSystem';
6
+ import * as Path from '@effect/platform/Path';
7
+ import * as Effect from 'effect/Effect';
8
+ import * as Schema from 'effect/Schema';
9
+ import * as Yaml from 'yaml';
10
+
11
+ import { DX_CONFIG } from '@dxos/client-protocol';
12
+
13
+ // TODO(wittjosiah): Factor out to app-framework?
14
+
15
+ const PluginsSchema = Schema.Array(Schema.String);
16
+
17
+ /** CLI-only: path to plugins/<profile>.yml (sibling to profile/). */
18
+ const getPluginsConfigPath = (profile: string) => `${DX_CONFIG}/plugins/${profile}.yml`;
19
+
20
+ /**
21
+ * Load enabled plugins from plugins/<profile>.yml (sibling to profile/).
22
+ * Returns the list of enabled plugin IDs, or an empty array if the file doesn't exist.
23
+ */
24
+ export const loadEnabledPlugins = Effect.fn(function* ({ profile }: { profile: string }) {
25
+ const fs = yield* FileSystem.FileSystem;
26
+ const pluginsPath = getPluginsConfigPath(profile);
27
+
28
+ const content = yield* fs
29
+ .readFileString(pluginsPath)
30
+ .pipe(Effect.catchTag('SystemError', () => Effect.succeed('[]')));
31
+
32
+ const raw = Yaml.parse(content);
33
+ const parsed = yield* Schema.decodeUnknown(PluginsSchema)(raw ?? []).pipe(
34
+ Effect.catchAll(() => Effect.succeed([] as string[])),
35
+ );
36
+
37
+ return parsed;
38
+ });
39
+
40
+ /**
41
+ * Save enabled plugins to plugins/<profile>.yml.
42
+ */
43
+ export const saveEnabledPlugins = Effect.fn(function* ({ profile, enabled }: { profile: string; enabled: string[] }) {
44
+ const fs = yield* FileSystem.FileSystem;
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);
50
+ });
@@ -0,0 +1,43 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+
7
+ import { Plugin } from '@dxos/app-framework';
8
+ import { random } from '@dxos/random';
9
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
10
+
11
+ import { translations } from '../../translations';
12
+ import { PluginDetail } from './PluginDetail';
13
+
14
+ const meta = {
15
+ title: 'plugins/plugin-registry/components/PluginDetail',
16
+ component: PluginDetail,
17
+ decorators: [withTheme(), withLayout({ layout: 'column' })],
18
+ parameters: {
19
+ layout: 'fullscreen',
20
+ translations,
21
+ },
22
+ } satisfies Meta<typeof PluginDetail>;
23
+
24
+ export default meta;
25
+
26
+ type Story = StoryObj<typeof meta>;
27
+
28
+ export const Default: Story = {
29
+ args: {
30
+ plugin: Plugin.define({
31
+ id: 'example.com/plugin/test-plugin',
32
+ name: 'Test Plugin',
33
+ description: random.lorem.paragraphs(2),
34
+ icon: 'ph--bug--regular',
35
+ iconHue: 'sky',
36
+ homePage: 'https://example.com',
37
+ source: 'https://github.com/example/test-plugin',
38
+ screenshots: [
39
+ 'https://media.gcflearnfree.org/content/55e073de7dd48174331f51b3_01_17_2014/getting_started_interactive2.png',
40
+ ],
41
+ }).pipe(Plugin.make)(),
42
+ },
43
+ };
@@ -0,0 +1,84 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React from 'react';
6
+
7
+ import { type Plugin } from '@dxos/app-framework';
8
+ import { Icon, Input, Link, ScrollArea, useTranslation } from '@dxos/react-ui';
9
+ import { composable, composableProps, getStyles, mx } from '@dxos/ui-theme';
10
+
11
+ import { meta } from '#meta';
12
+
13
+ export type PluginDetailProps = {
14
+ plugin: Plugin.Plugin;
15
+ enabled?: boolean;
16
+ onEnabledChange?: (enabled: boolean) => void;
17
+ };
18
+
19
+ export const PluginDetail = composable<HTMLDivElement, PluginDetailProps>(
20
+ ({ plugin, enabled, onEnabledChange, ...props }, forwardedRef) => {
21
+ const { t } = useTranslation(meta.id);
22
+ const {
23
+ id,
24
+ name,
25
+ description,
26
+ homePage,
27
+ source,
28
+ screenshots,
29
+ icon = 'ph--circle--regular',
30
+ iconHue = 'neutral',
31
+ } = plugin.meta;
32
+ const styles = getStyles(iconHue);
33
+
34
+ return (
35
+ <ScrollArea.Root {...composableProps(props)} orientation='vertical' ref={forwardedRef}>
36
+ <ScrollArea.Viewport>
37
+ <div role='none' className='dx-document grid grid-cols-[min-content_1fr] gap-4 p-4'>
38
+ <div role='none'>
39
+ <Icon classNames={mx('p-1 rounded-md', styles.fill, styles.surfaceText)} icon={icon} size={14} />
40
+ </div>
41
+ <div role='none' className='flex flex-col gap-6'>
42
+ <div role='none' className='grid grid-cols-[1fr_min-content] gap-x-3 w-full pt-1'>
43
+ <h2 className='text-xl'>{name}</h2>
44
+ <Input.Root>
45
+ <Input.Switch classNames='self-center' checked={enabled} onCheckedChange={onEnabledChange} />
46
+ </Input.Root>
47
+ <p className='pt-0.5 text-sm text-description'>{id}</p>
48
+ </div>
49
+ <div role='none'>
50
+ <p className='text-description'>{description}</p>
51
+ </div>
52
+ {screenshots && screenshots.length > 0 && (
53
+ <div role='none' className='flex flex-col gap-2'>
54
+ <h2>Preview</h2>
55
+ <img src={screenshots[0]} alt={name} className='aspect-video object-fit' />
56
+ </div>
57
+ )}
58
+ <div role='none' className='flex flex-col gap-2'>
59
+ <h2>Resources</h2>
60
+ <div className='flex gap-2'>
61
+ {homePage && (
62
+ <Link href={homePage} target='_blank' rel='noreferrer' classNames='text-sm text-description'>
63
+ {t('home-page.label')}
64
+ <Icon icon='ph--arrow-square-out--bold' size={3} classNames='inline-block leading-none mx-1' />
65
+ </Link>
66
+ )}
67
+
68
+ {source && (
69
+ <Link href={source} target='_blank' rel='noreferrer' classNames='text-sm text-description'>
70
+ {t('source.label')}
71
+ <Icon icon='ph--arrow-square-out--bold' size={3} classNames='inline-block leading-none mx-1' />
72
+ </Link>
73
+ )}
74
+ </div>
75
+ </div>
76
+ </div>
77
+ </div>
78
+ </ScrollArea.Viewport>
79
+ </ScrollArea.Root>
80
+ );
81
+ },
82
+ );
83
+
84
+ PluginDetail.displayName = 'PluginDetail';
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './PluginDetail';
@@ -6,14 +6,14 @@ import React, { type MouseEvent, useCallback } from 'react';
6
6
 
7
7
  import { type Plugin } from '@dxos/app-framework';
8
8
  import { type ChromaticPalette, Icon, IconButton, Input, Link, ListItem, Tag, useTranslation } from '@dxos/react-ui';
9
- import { descriptionText, mx } from '@dxos/react-ui-theme';
10
- import { getStyles } from '@dxos/react-ui-theme';
9
+ import { mx } from '@dxos/ui-theme';
10
+ import { getStyles } from '@dxos/ui-theme';
11
11
 
12
- import { meta } from '../meta';
13
- import { type RegistryTagType } from '../types';
12
+ import { meta } from '#meta';
13
+ import { type RegistryTagType } from '#types';
14
14
 
15
15
  export type PluginItemProps = {
16
- plugin: Plugin;
16
+ plugin: Plugin.Plugin;
17
17
  installed?: readonly string[];
18
18
  enabled?: readonly string[];
19
19
  onClick?: (id: string) => void;
@@ -27,7 +27,7 @@ export const PluginItem = ({
27
27
  enabled = [],
28
28
  onClick,
29
29
  onChange,
30
- hasSettings: hasSettingsParam,
30
+ hasSettings: hasSettingsProp,
31
31
  onSettings,
32
32
  }: PluginItemProps) => {
33
33
  const { t } = useTranslation(meta.id);
@@ -46,7 +46,7 @@ export const PluginItem = ({
46
46
  [id, isEnabled, onChange],
47
47
  );
48
48
 
49
- const hasSettings = hasSettingsParam?.(id) ?? false;
49
+ const hasSettings = hasSettingsProp?.(id) ?? false;
50
50
  const handleSettings = useCallback(() => onSettings?.(id), [id, onSettings]);
51
51
  const styles = getStyles(iconHue);
52
52
  const gridCols = 'grid grid-cols-[5rem_1fr]';
@@ -58,11 +58,15 @@ export const PluginItem = ({
58
58
  labelId={labelId}
59
59
  data-testid={`pluginList.${id}`}
60
60
  aria-describedby={descriptionId}
61
- classNames={mx(gridCols, 'bs-[12rem] is-full gap-3 pie-2 border border-separator rounded-md overflow-hidden')}
61
+ classNames={mx(gridCols, 'h-[14rem] w-full gap-3 pe-2 bg-modal-surface rounded-md overflow-hidden')}
62
62
  >
63
- <div className={mx(gridRows, 'justify-center rounded-l-md', styles.bg)}>
64
- <div />
65
- <Icon classNames={mx('cursor-pointer', styles.icon)} icon={icon} size={14} onClick={handleClick} />
63
+ <div className={mx(gridRows, 'justify-center rounded-l-md', styles.surface)}>
64
+ <Icon
65
+ classNames={mx('row-start-2 cursor-pointer', styles.surfaceText)}
66
+ icon={icon}
67
+ size={14}
68
+ onClick={handleClick}
69
+ />
66
70
  </div>
67
71
 
68
72
  <div className={mx(gridRows)}>
@@ -71,10 +75,10 @@ export const PluginItem = ({
71
75
  </div>
72
76
 
73
77
  <div>
74
- <p className={mx(descriptionText, 'line-clamp-4 min-is-0')}>{description}</p>
78
+ <p className={mx('text-description', 'line-clamp-4 min-w-0')}>{description}</p>
75
79
  </div>
76
80
 
77
- <div className='flex -mis-0.5 overflow-x-auto scrollbar-none'>
81
+ <div className='flex -ms-0.5 overflow-x-auto scrollbar-none'>
78
82
  {tags?.map((tag) => (
79
83
  <Tag key={tag} palette={tagColors[tag as RegistryTagType]} classNames='text-xs uppercase font-thin'>
80
84
  {tag}
@@ -87,7 +91,7 @@ export const PluginItem = ({
87
91
  aria-describedby={descriptionId}
88
92
  classNames='cursor-pointer'
89
93
  icon='ph--gear--regular'
90
- label={t('settings label')}
94
+ label={t('settings.label')}
91
95
  iconOnly
92
96
  size={4}
93
97
  onClick={handleSettings}
@@ -95,11 +99,11 @@ export const PluginItem = ({
95
99
  />
96
100
 
97
101
  <Link aria-describedby={descriptionId} classNames='text-description cursor-pointer' onClick={handleClick}>
98
- {t('details label')}
102
+ {t('details.label')}
99
103
  </Link>
100
104
 
101
105
  <div className='grow' />
102
- <div className='pie-1'>
106
+ <div className='pe-1'>
103
107
  <Input.Root id={inputId}>
104
108
  <Input.Switch classNames='self-center' checked={isEnabled} onClick={handleChange} />
105
109
  </Input.Root>