@dxos/plugin-devtools 0.11.1 → 0.12.0-next-2800d0376abcc62d5544afdd744b16b2b960434e

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 (127) hide show
  1. package/dist/lib/{chunk-types.mjs → Devtools.mjs} +5 -14
  2. package/dist/lib/Devtools.mjs.map +1 -0
  3. package/dist/lib/DevtoolsEvents.mjs +15 -0
  4. package/dist/lib/DevtoolsEvents.mjs.map +1 -0
  5. package/dist/lib/DevtoolsPlugin.mjs +13 -36
  6. package/dist/lib/DevtoolsPlugin.mjs.map +1 -1
  7. package/dist/lib/capabilities.mjs +39 -5
  8. package/dist/lib/capabilities.mjs.map +1 -1
  9. package/dist/lib/capabilities.node.mjs +34 -0
  10. package/dist/lib/capabilities.node.mjs.map +1 -0
  11. package/dist/lib/capabilities.workerd.mjs +30 -0
  12. package/dist/lib/capabilities.workerd.mjs.map +1 -0
  13. package/dist/lib/chunk-CliPanel.mjs +103 -0
  14. package/dist/lib/chunk-CliPanel.mjs.map +1 -0
  15. package/dist/lib/chunk-DebugGraph.mjs +2 -2
  16. package/dist/lib/chunk-DebugGraph.mjs.map +1 -1
  17. package/dist/lib/chunk-DevtoolsOverviewContainer.mjs +14 -13
  18. package/dist/lib/chunk-DevtoolsOverviewContainer.mjs.map +1 -1
  19. package/dist/lib/chunk-GithubPanel.mjs +36 -30
  20. package/dist/lib/chunk-GithubPanel.mjs.map +1 -1
  21. package/dist/lib/chunk-RegistryPanel.mjs +2 -2
  22. package/dist/lib/chunk-RegistryPanel.mjs.map +1 -1
  23. package/dist/lib/chunk-app-graph-builder.mjs +66 -50
  24. package/dist/lib/chunk-app-graph-builder.mjs.map +1 -1
  25. package/dist/lib/chunk-react-context.mjs +4 -3
  26. package/dist/lib/chunk-react-context.mjs.map +1 -1
  27. package/dist/lib/chunk-react-surface.mjs +35 -28
  28. package/dist/lib/chunk-react-surface.mjs.map +1 -1
  29. package/dist/lib/chunk-rolldown-runtime.mjs +13 -0
  30. package/dist/lib/containers.mjs +2 -1
  31. package/dist/lib/containers.mjs.map +1 -1
  32. package/dist/lib/index.mjs +4 -3
  33. package/dist/lib/meta.mjs +29 -1
  34. package/dist/lib/meta.mjs.map +1 -0
  35. package/dist/lib/plugin.mjs +6 -5
  36. package/dist/lib/plugin.mjs.map +1 -1
  37. package/dist/lib/translations.mjs +1 -0
  38. package/dist/lib/translations.mjs.map +1 -1
  39. package/dist/lib/types.mjs +3 -2
  40. package/dist/types/src/DevtoolsPlugin.d.ts +36 -3
  41. package/dist/types/src/DevtoolsPlugin.d.ts.map +1 -1
  42. package/dist/types/src/capabilities/DevtoolsSurfaces.d.ts.map +1 -1
  43. package/dist/types/src/capabilities/app-graph-builder.d.ts +2 -2
  44. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -1
  45. package/dist/types/src/capabilities/gen/node.d.ts +8 -0
  46. package/dist/types/src/capabilities/gen/node.d.ts.map +1 -0
  47. package/dist/types/src/capabilities/gen/workerd.d.ts +8 -0
  48. package/dist/types/src/capabilities/gen/workerd.d.ts.map +1 -0
  49. package/dist/types/src/capabilities/index.d.ts +7 -4
  50. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  51. package/dist/types/src/capabilities/react-context.d.ts +2 -2
  52. package/dist/types/src/capabilities/react-context.d.ts.map +1 -1
  53. package/dist/types/src/capabilities/react-surface.d.ts +2 -2
  54. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -1
  55. package/dist/types/src/containers/CliPanel/CliPanel.d.ts +6 -0
  56. package/dist/types/src/containers/CliPanel/CliPanel.d.ts.map +1 -0
  57. package/dist/types/src/containers/CliPanel/CliPanel.stories.d.ts +89 -0
  58. package/dist/types/src/containers/CliPanel/CliPanel.stories.d.ts.map +1 -0
  59. package/dist/types/src/containers/CliPanel/index.d.ts +2 -0
  60. package/dist/types/src/containers/CliPanel/index.d.ts.map +1 -0
  61. package/dist/types/src/containers/CliPanel/useCliApp.d.ts +29 -0
  62. package/dist/types/src/containers/CliPanel/useCliApp.d.ts.map +1 -0
  63. package/dist/types/src/containers/DebugGraph/DebugGraph.d.ts +2 -2
  64. package/dist/types/src/containers/DebugGraph/DebugGraph.d.ts.map +1 -1
  65. package/dist/types/src/containers/GithubPanel/GithubComponent.d.ts +9 -4
  66. package/dist/types/src/containers/GithubPanel/GithubComponent.d.ts.map +1 -1
  67. package/dist/types/src/containers/GithubPanel/GithubComponent.stories.d.ts +6 -4
  68. package/dist/types/src/containers/GithubPanel/GithubComponent.stories.d.ts.map +1 -1
  69. package/dist/types/src/containers/index.d.ts +1 -0
  70. package/dist/types/src/containers/index.d.ts.map +1 -1
  71. package/dist/types/src/index.d.ts +3 -2
  72. package/dist/types/src/index.d.ts.map +1 -1
  73. package/dist/types/src/plugin.d.ts +2 -1
  74. package/dist/types/src/plugin.d.ts.map +1 -1
  75. package/dist/types/src/plugin.test.d.ts +2 -0
  76. package/dist/types/src/plugin.test.d.ts.map +1 -0
  77. package/dist/types/src/translations.d.ts +6 -4
  78. package/dist/types/src/translations.d.ts.map +1 -1
  79. package/dist/types/src/types/Devtools.d.ts +1 -0
  80. package/dist/types/src/types/Devtools.d.ts.map +1 -1
  81. package/dist/types/src/types/DevtoolsEvents.d.ts +8 -0
  82. package/dist/types/src/types/DevtoolsEvents.d.ts.map +1 -0
  83. package/dist/types/src/types/index.d.ts +1 -0
  84. package/dist/types/src/types/index.d.ts.map +1 -1
  85. package/dist/types/tsconfig.tsbuildinfo +1 -1
  86. package/package.json +73 -74
  87. package/src/DevtoolsPlugin.ts +15 -0
  88. package/src/capabilities/DevtoolsSurfaces.tsx +9 -4
  89. package/src/capabilities/app-graph-builder.ts +72 -49
  90. package/src/capabilities/gen/node.ts +49 -0
  91. package/src/capabilities/gen/workerd.ts +44 -0
  92. package/src/capabilities/index.ts +50 -4
  93. package/src/capabilities/react-context.tsx +3 -2
  94. package/src/capabilities/react-surface.ts +11 -5
  95. package/src/containers/CliPanel/CliPanel.stories.tsx +75 -0
  96. package/src/containers/CliPanel/CliPanel.tsx +45 -0
  97. package/src/containers/CliPanel/index.ts +5 -0
  98. package/src/containers/CliPanel/useCliApp.ts +103 -0
  99. package/src/containers/DebugGraph/DebugGraph.tsx +3 -3
  100. package/src/containers/GithubPanel/GithubComponent.tsx +13 -15
  101. package/src/containers/RegistryPanel/RegistryPanel.tsx +1 -1
  102. package/src/containers/index.ts +1 -0
  103. package/src/index.ts +3 -2
  104. package/src/meta.ts +1 -1
  105. package/src/{DevtoolsPlugin.test.ts → plugin.test.ts} +5 -4
  106. package/src/plugin.tsx +20 -0
  107. package/src/translations.ts +1 -0
  108. package/src/types/Devtools.ts +1 -0
  109. package/src/types/DevtoolsEvents.ts +16 -0
  110. package/src/types/index.ts +1 -0
  111. package/dist/lib/DevtoolsPlugin.node.mjs +0 -10
  112. package/dist/lib/DevtoolsPlugin.node.mjs.map +0 -1
  113. package/dist/lib/DevtoolsPlugin.workerd.mjs +0 -8
  114. package/dist/lib/DevtoolsPlugin.workerd.mjs.map +0 -1
  115. package/dist/lib/chunk-meta.mjs +0 -30
  116. package/dist/lib/chunk-meta.mjs.map +0 -1
  117. package/dist/lib/chunk-types.mjs.map +0 -1
  118. package/dist/types/src/DevtoolsPlugin.node.d.ts +0 -4
  119. package/dist/types/src/DevtoolsPlugin.node.d.ts.map +0 -1
  120. package/dist/types/src/DevtoolsPlugin.test.d.ts +0 -2
  121. package/dist/types/src/DevtoolsPlugin.test.d.ts.map +0 -1
  122. package/dist/types/src/DevtoolsPlugin.workerd.d.ts +0 -4
  123. package/dist/types/src/DevtoolsPlugin.workerd.d.ts.map +0 -1
  124. package/src/DevtoolsPlugin.node.ts +0 -16
  125. package/src/DevtoolsPlugin.tsx +0 -54
  126. package/src/DevtoolsPlugin.workerd.ts +0 -11
  127. package/src/plugin.ts +0 -9
@@ -0,0 +1,49 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ // GENERATED by dx-plugin — do not edit. Sliced from ../index.ts for the 'node' condition.
6
+
7
+ import { create } from '@bufbuild/protobuf';
8
+ import * as Effect from 'effect/Effect';
9
+
10
+ import * as Capability from '@dxos/app-framework/Capability';
11
+ import * as AppCapabilities from '@dxos/app-toolkit/AppCapabilities';
12
+ import * as AppCapability from '@dxos/app-toolkit/AppCapability';
13
+ import { Runtime_Client_StorageSchema } from '@dxos/protocols/buf/dxos/config_pb';
14
+ import { type Client } from '@dxos/react-client';
15
+
16
+ import { translations } from '#translations';
17
+
18
+ const setupDevtools = () => {
19
+ (globalThis as any).composer ??= {};
20
+
21
+ // Used to test how composer handles breaking protocol changes.
22
+ (globalThis as any).composer.changeStorageVersionInMetadata = async (version: number) => {
23
+ const { changeStorageVersionInMetadata } = await import('@dxos/client-services/testing');
24
+ const { createStorageObjects } = await import('@dxos/client-services');
25
+ const client: Client = (window as any).dxos.client;
26
+ const config = client.config;
27
+ await client.destroy();
28
+ const { storage } = createStorageObjects(
29
+ config.values?.runtime?.client?.storage ?? create(Runtime_Client_StorageSchema, {}),
30
+ );
31
+ await changeStorageVersionInMetadata(storage, version);
32
+ location.pathname = '/';
33
+ };
34
+ };
35
+
36
+ export const AppGraphBuilder = AppCapability.appGraphBuilder(() => import('../app-graph-builder'), {
37
+ requires: [AppCapabilities.AppGraph],
38
+ environments: ['node'],
39
+ });
40
+
41
+ export const SetupDevtools = Capability.inlineModule('setup-devtools', { provides: [] }, () =>
42
+ Effect.sync(() => setupDevtools()),
43
+ );
44
+
45
+ export const Translations = AppCapability.translations(translations);
46
+
47
+ export const PluginAsset = undefined;
48
+ export const ReactContext = undefined;
49
+ export const ReactSurface = undefined;
@@ -0,0 +1,44 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ // GENERATED by dx-plugin — do not edit. Sliced from ../index.ts for the 'workerd' condition.
6
+
7
+ import { create } from '@bufbuild/protobuf';
8
+ import * as Effect from 'effect/Effect';
9
+
10
+ import * as Capability from '@dxos/app-framework/Capability';
11
+ import * as AppCapability from '@dxos/app-toolkit/AppCapability';
12
+ import { Runtime_Client_StorageSchema } from '@dxos/protocols/buf/dxos/config_pb';
13
+ import { type Client } from '@dxos/react-client';
14
+
15
+ import { translations } from '#translations';
16
+
17
+ const setupDevtools = () => {
18
+ (globalThis as any).composer ??= {};
19
+
20
+ // Used to test how composer handles breaking protocol changes.
21
+ (globalThis as any).composer.changeStorageVersionInMetadata = async (version: number) => {
22
+ const { changeStorageVersionInMetadata } = await import('@dxos/client-services/testing');
23
+ const { createStorageObjects } = await import('@dxos/client-services');
24
+ const client: Client = (window as any).dxos.client;
25
+ const config = client.config;
26
+ await client.destroy();
27
+ const { storage } = createStorageObjects(
28
+ config.values?.runtime?.client?.storage ?? create(Runtime_Client_StorageSchema, {}),
29
+ );
30
+ await changeStorageVersionInMetadata(storage, version);
31
+ location.pathname = '/';
32
+ };
33
+ };
34
+
35
+ export const SetupDevtools = Capability.inlineModule('setup-devtools', { provides: [] }, () =>
36
+ Effect.sync(() => setupDevtools()),
37
+ );
38
+
39
+ export const Translations = AppCapability.translations(translations);
40
+
41
+ export const AppGraphBuilder = undefined;
42
+ export const PluginAsset = undefined;
43
+ export const ReactContext = undefined;
44
+ export const ReactSurface = undefined;
@@ -2,8 +2,54 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { Capability } from '@dxos/app-framework';
5
+ import { create } from '@bufbuild/protobuf';
6
+ import * as Effect from 'effect/Effect';
6
7
 
7
- export const AppGraphBuilder = Capability.lazy('AppGraphBuilder', () => import('./app-graph-builder'));
8
- export const ReactContext = Capability.lazy('ReactContext', () => import('./react-context'));
9
- export const ReactSurface = Capability.lazy('ReactSurface', () => import('./react-surface'));
8
+ import * as Capability from '@dxos/app-framework/Capability';
9
+ import * as AppCapabilities from '@dxos/app-toolkit/AppCapabilities';
10
+ import * as AppCapability from '@dxos/app-toolkit/AppCapability';
11
+ import { Runtime_Client_StorageSchema } from '@dxos/protocols/buf/dxos/config_pb';
12
+ import { type Client } from '@dxos/react-client';
13
+
14
+ import { meta } from '#meta';
15
+ import { translations } from '#translations';
16
+
17
+ // eslint-disable-next-line import/no-relative-packages
18
+ import pluginSpec from '../../PLUGIN.mdl?raw';
19
+
20
+ export const AppGraphBuilder = AppCapability.appGraphBuilder(() => import('./app-graph-builder'), {
21
+ requires: [AppCapabilities.AppGraph],
22
+ environments: ['node'],
23
+ });
24
+ export const ReactContext = AppCapability.reactContext(() => import('./react-context'));
25
+ export const ReactSurface = AppCapability.surface(() => import('./react-surface'), {
26
+ roles: ['org.dxos.role.article', 'org.dxos.role.deckCompanion.devtoolsOverview'],
27
+ });
28
+ export const SetupDevtools = Capability.inlineModule('setup-devtools', { provides: [] }, () =>
29
+ Effect.sync(() => setupDevtools()),
30
+ );
31
+ export const Translations = AppCapability.translations(translations);
32
+ export const PluginAsset = AppCapability.pluginAsset({
33
+ pluginId: meta.profile.key,
34
+ path: 'PLUGIN.mdl',
35
+ content: pluginSpec,
36
+ mimeType: 'application/x-mdl',
37
+ });
38
+
39
+ const setupDevtools = () => {
40
+ (globalThis as any).composer ??= {};
41
+
42
+ // Used to test how composer handles breaking protocol changes.
43
+ (globalThis as any).composer.changeStorageVersionInMetadata = async (version: number) => {
44
+ const { changeStorageVersionInMetadata } = await import('@dxos/client-services/testing');
45
+ const { createStorageObjects } = await import('@dxos/client-services');
46
+ const client: Client = (window as any).dxos.client;
47
+ const config = client.config;
48
+ await client.destroy();
49
+ const { storage } = createStorageObjects(
50
+ config.values?.runtime?.client?.storage ?? create(Runtime_Client_StorageSchema, {}),
51
+ );
52
+ await changeStorageVersionInMetadata(storage, version);
53
+ location.pathname = '/';
54
+ };
55
+ };
@@ -5,7 +5,8 @@
5
5
  import * as Effect from 'effect/Effect';
6
6
  import React from 'react';
7
7
 
8
- import { Capabilities, Capability } from '@dxos/app-framework';
8
+ import * as Capabilities from '@dxos/app-framework/Capabilities';
9
+ import * as Capability from '@dxos/app-framework/Capability';
9
10
  import { Surface } from '@dxos/app-framework/ui';
10
11
  import { DevtoolsContextProvider } from '@dxos/devtools';
11
12
 
@@ -13,7 +14,7 @@ import { meta } from '#meta';
13
14
 
14
15
  export default Capability.makeModule(() =>
15
16
  Effect.succeed(
16
- Capability.contributes(Capabilities.ReactContext, {
17
+ Capability.contribute(Capabilities.ReactContext, {
17
18
  id: meta.profile.key,
18
19
  context: ({ children }) => (
19
20
  <Surface.ProfilerProvider>
@@ -4,8 +4,10 @@
4
4
 
5
5
  import * as Effect from 'effect/Effect';
6
6
 
7
- import { Capabilities, Capability } from '@dxos/app-framework';
7
+ import * as Capabilities from '@dxos/app-framework/Capabilities';
8
+ import * as Capability from '@dxos/app-framework/Capability';
8
9
  import { Surface } from '@dxos/app-framework/ui';
10
+ import type * as AppGraph from '@dxos/app-graph/AppGraph';
9
11
  import { AppSurface } from '@dxos/app-toolkit/ui';
10
12
  import { EDGE_SERVICE_DEFAULTS, EdgeServiceName } from '@dxos/config';
11
13
  import {
@@ -31,10 +33,9 @@ import {
31
33
  SwarmPanel,
32
34
  WorkflowPanel,
33
35
  } from '@dxos/devtools';
34
- import { type Graph } from '@dxos/plugin-graph';
35
36
  import { ToolsExplorer } from '@dxos/react-ui-introspect';
36
37
 
37
- import { DebugGraph, DevtoolsOverviewContainer, RegistryPanel } from '#containers';
38
+ import { CliPanel, DebugGraph, DevtoolsOverviewContainer, RegistryPanel } from '#containers';
38
39
  import { Devtools } from '#types';
39
40
 
40
41
  import {
@@ -48,7 +49,7 @@ import {
48
49
  const MCP_SERVER_URL = EDGE_SERVICE_DEFAULTS[EdgeServiceName.Introspect];
49
50
 
50
51
  type GraphDebug = {
51
- graph: Graph.Graph;
52
+ graph: AppGraph.Graph;
52
53
  root: string;
53
54
  };
54
55
 
@@ -69,7 +70,7 @@ const isGraphDebug = (data: unknown): data is GraphDebug => {
69
70
 
70
71
  export default Capability.makeModule(
71
72
  Effect.fnUntraced(function* () {
72
- return Capability.contributes(Capabilities.ReactSurface, [
73
+ return Capability.contribute(Capabilities.ReactSurface, [
73
74
  Surface.create({
74
75
  id: 'appGraph',
75
76
  filter: AppSurface.subject(AppSurface.Article, isGraphDebug),
@@ -82,6 +83,11 @@ export default Capability.makeModule(
82
83
  component: ToolsExplorer,
83
84
  props: () => ({ serverUrl: MCP_SERVER_URL }),
84
85
  }),
86
+ Surface.create({
87
+ id: 'cli',
88
+ filter: AppSurface.literal(AppSurface.Article, Devtools.Cli),
89
+ component: CliPanel,
90
+ }),
85
91
  Surface.create({
86
92
  id: 'registry',
87
93
  filter: AppSurface.literal(AppSurface.Article, Devtools.Echo.Registry),
@@ -0,0 +1,75 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+ import * as Effect from 'effect/Effect';
7
+ import { userEvent } from 'storybook/test';
8
+
9
+ import { withPluginManager } from '@dxos/app-framework/testing';
10
+ import { ClientPlugin, initializeIdentity } from '@dxos/plugin-client/testing';
11
+ import * as SpacePlugin from '@dxos/plugin-space/SpacePlugin';
12
+ import { corePlugins } from '@dxos/plugin-testing';
13
+ import * as StorybookPlugin from '@dxos/plugin-testing/StorybookPlugin';
14
+ import { runCommand, waitForTerminal } from '@dxos/react-ui-terminal/testing';
15
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
16
+
17
+ import { translations } from '#translations';
18
+
19
+ import { CliPanel } from './CliPanel';
20
+
21
+ const meta = {
22
+ title: 'plugins/plugin-devtools/containers/CliPanel',
23
+ component: CliPanel,
24
+ decorators: [
25
+ withLayout({ layout: 'fullscreen' }),
26
+ withTheme(),
27
+ withPluginManager({
28
+ // No setup event: the panel fires `CommandsRequested` itself, which is what pulls the
29
+ // plugins' command modules in — the same path the app takes.
30
+ plugins: [
31
+ ...corePlugins(),
32
+ // The identity brings a default space, which is what the commands resolve against.
33
+ ClientPlugin.make({ onClientInitialized: ({ client }) => Effect.asVoid(initializeIdentity(client)) }),
34
+ StorybookPlugin.make({}),
35
+ // The real plugin, so the story covers it actually contributing its commands.
36
+ SpacePlugin.make({}),
37
+ ],
38
+ }),
39
+ ],
40
+ parameters: {
41
+ layout: 'fullscreen',
42
+ controls: { disable: true },
43
+ translations,
44
+ },
45
+ } satisfies Meta<typeof CliPanel>;
46
+
47
+ export default meta;
48
+
49
+ type Story = StoryObj<typeof meta>;
50
+
51
+ export const Default: Story = {};
52
+
53
+ /**
54
+ * Drives real `dx` commands against the story's client, so a break in the wiring between the shell
55
+ * and the client surfaces here rather than in the app.
56
+ */
57
+ export const Spec: Story = {
58
+ play: async ({ canvasElement }) => {
59
+ const keyboard = (text: string) => userEvent.keyboard(text);
60
+ await waitForTerminal(canvasElement, 'dx>');
61
+
62
+ // `space list` reaches the client and reports its space.
63
+ await runCommand(canvasElement, 'space list', keyboard);
64
+ await waitForTerminal(canvasElement, 'SPACE_READY');
65
+
66
+ // `database query` reaches ECHO through the layer the command provides for itself.
67
+ await runCommand(canvasElement, 'database query', keyboard);
68
+ await waitForTerminal(canvasElement, 'org.dxos.type.spaceProperties');
69
+
70
+ // A command from another plugin, whose service arrives via that plugin's contributed layer
71
+ // rather than anything this panel names.
72
+ await runCommand(canvasElement, 'config view', keyboard);
73
+ await waitForTerminal(canvasElement, 'runtime');
74
+ },
75
+ };
@@ -0,0 +1,45 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import React from 'react';
6
+
7
+ import { Placeholder } from '@dxos/devtools';
8
+ import { useClient } from '@dxos/react-client';
9
+ import { Panel } from '@dxos/react-ui';
10
+ import { Terminal } from '@dxos/react-ui-terminal';
11
+
12
+ import { useCliApp } from './useCliApp';
13
+
14
+ const BOLD = '\x1b[1m';
15
+ const DIM = '\x1b[2m';
16
+ const RESET = '\x1b[0m';
17
+
18
+ const BANNER = `${BOLD}DXOS CLI${RESET}\n${DIM}The dx commands, running against this client. Type "help" for the list.${RESET}`;
19
+
20
+ /**
21
+ * Runs the `dx` CLI against the client backing this app.
22
+ *
23
+ * The command tree is assembled from the commands the installed plugins contribute, and it and its
24
+ * services are built once per client, so state the handlers hold survives between commands the way
25
+ * it does in a long-lived shell.
26
+ */
27
+ export const CliPanel = () => {
28
+ const client = useClient();
29
+ const cli = useCliApp(client);
30
+
31
+ // No ScrollArea: xterm owns its own viewport and scrollback.
32
+ return (
33
+ <Panel.Root>
34
+ <Panel.Content>
35
+ {cli ? (
36
+ <Terminal command={cli.command} layer={cli.layer} name='dx' banner={BANNER} />
37
+ ) : (
38
+ <Placeholder label='No commands' />
39
+ )}
40
+ </Panel.Content>
41
+ </Panel.Root>
42
+ );
43
+ };
44
+
45
+ CliPanel.displayName = 'CliPanel';
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ export { CliPanel, CliPanel as default } from './CliPanel';
@@ -0,0 +1,103 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import * as Effect from 'effect/Effect';
6
+ import * as Layer from 'effect/Layer';
7
+ import * as Command from 'effect/unstable/cli/Command';
8
+ import * as Options from 'effect/unstable/cli/Flag';
9
+ import { useEffect, useMemo } from 'react';
10
+
11
+ import * as ActivationEvents from '@dxos/app-framework/ActivationEvents';
12
+ import * as Capabilities from '@dxos/app-framework/Capabilities';
13
+ import * as Capability from '@dxos/app-framework/Capability';
14
+ import * as Plugin from '@dxos/app-framework/Plugin';
15
+ import { useCapabilities, usePluginManager } from '@dxos/app-framework/ui';
16
+ import { CommandConfig, type CommandServices } from '@dxos/cli-util';
17
+ import { type Client, ClientService, ConfigService } from '@dxos/client';
18
+ import * as Operation from '@dxos/compute/Operation';
19
+ import { EffectEx } from '@dxos/effect';
20
+
21
+ const OPERATIONS_UNAVAILABLE = 'Operations are not available in the devtools terminal.';
22
+
23
+ /**
24
+ * Commands that create objects invoke operations, which this surface has no runtime for. Failing
25
+ * with a stated reason beats the bare "Service not found" a missing layer would produce.
26
+ */
27
+ const operationLayer = Layer.succeed(Operation.Service, {
28
+ invoke: () => Effect.die(OPERATIONS_UNAVAILABLE),
29
+ schedule: () => Effect.die(OPERATIONS_UNAVAILABLE),
30
+ invokePromise: async () => ({ error: new Error(OPERATIONS_UNAVAILABLE) }),
31
+ });
32
+
33
+ /**
34
+ * Builds the `dx` command tree and the services its handlers need.
35
+ *
36
+ * Mounting is the demand signal: command modules are gated on `CommandsRequested`, which nothing
37
+ * else in the app fires, so a plugin's commands stay off the critical path until a terminal opens.
38
+ * The capability is read through its atom, so commands appear as they activate and enabling or
39
+ * disabling a plugin later rebuilds the tree rather than leaving the terminal on the set that
40
+ * existed when it mounted.
41
+ *
42
+ * The root mirrors the binary's, minus the options that only mean something on a filesystem
43
+ * (`--config`, `--profile`); everything else resolves the same way, including `--space-id`
44
+ * defaulting to the first available space.
45
+ *
46
+ * Returns `undefined` until something contributes commands — there is no shell to offer yet.
47
+ */
48
+ export const useCliApp = (client: Client) => {
49
+ const manager = usePluginManager();
50
+ const commands = useCapabilities(Capabilities.Command);
51
+ const contributed = useCapabilities(Capabilities.Layer);
52
+
53
+ useEffect(() => {
54
+ EffectEx.runDetached(manager.activate(ActivationEvents.CommandsRequested));
55
+ }, [manager]);
56
+
57
+ return useMemo(() => {
58
+ // Destructured rather than passed as an array so the non-empty shape `withSubcommands` requires
59
+ // is carried by the type instead of asserted.
60
+ const [first, ...rest] = commands;
61
+ if (!first) {
62
+ return undefined;
63
+ }
64
+
65
+ const command = Command.make('dx').pipe(
66
+ // Shared so they parse anywhere in the line — a plain command config is only read when `dx`
67
+ // itself is the leaf.
68
+ Command.withSharedFlags({
69
+ json: Options.boolean('json').pipe(Options.withDescription('JSON output.')),
70
+ verbose: Options.boolean('verbose').pipe(Options.withAlias('v'), Options.withDescription('Verbose output.')),
71
+ }),
72
+ Command.withSubcommands([first, ...rest]),
73
+ // After the subcommands so the config reaches them: `provide` wraps the handler it is applied
74
+ // to, and `withSubcommands` replaces that handler with one that dispatches to the children.
75
+ // The input widens to a union across the tree, hence the presence checks.
76
+ Command.provide((input) =>
77
+ Layer.succeed(CommandConfig, {
78
+ json: 'json' in input ? input.json : false,
79
+ verbose: 'verbose' in input ? input.verbose : false,
80
+ profile: 'default',
81
+ logLevel: 'info',
82
+ }),
83
+ ),
84
+ );
85
+
86
+ // Annotated so a service this host owes is a build error naming it, rather than a
87
+ // "Service not found" the first time someone runs the command that needed it.
88
+ const hostLayer: Layer.Layer<CommandServices, never, never> = Layer.mergeAll(
89
+ ClientService.fromClient(client),
90
+ ConfigService.fromConfig(client.config),
91
+ Layer.succeed(Plugin.Service, manager),
92
+ Layer.succeed(Capability.Service, manager.capabilities),
93
+ operationLayer,
94
+ );
95
+
96
+ // Plugins carry anything further their own commands need. A contributed layer may fail to
97
+ // build, and the shell runs on a fiber with nowhere to report that, so it dies here instead
98
+ // of being smuggled into the terminal as an unhandled error channel.
99
+ const layer = Layer.orDie(Layer.mergeAll(hostLayer, ...contributed));
100
+
101
+ return { command, layer };
102
+ }, [client, manager, commands, contributed]);
103
+ };
@@ -4,11 +4,11 @@
4
4
 
5
5
  import React from 'react';
6
6
 
7
+ import * as AppGraph from '@dxos/app-graph/AppGraph';
7
8
  import { Tree } from '@dxos/devtools';
8
- import { Graph } from '@dxos/plugin-graph';
9
9
  import { Panel, ScrollArea } from '@dxos/react-ui';
10
10
 
11
- export type DebugGraphProps = { graph: Graph.Graph; root: string };
11
+ export type DebugGraphProps = { graph: AppGraph.Graph; root: string };
12
12
 
13
13
  export const DebugGraph = ({ graph, root }: DebugGraphProps) => {
14
14
  return (
@@ -16,7 +16,7 @@ export const DebugGraph = ({ graph, root }: DebugGraphProps) => {
16
16
  <Panel.Content asChild>
17
17
  <ScrollArea.Root orientation='all'>
18
18
  <ScrollArea.Viewport>
19
- <Tree data={Graph.toJSON(graph, root)} />
19
+ <Tree data={AppGraph.toJSON(graph, root)} />
20
20
  </ScrollArea.Viewport>
21
21
  </ScrollArea.Root>
22
22
  </Panel.Content>
@@ -4,7 +4,7 @@
4
4
 
5
5
  import React, { type ReactNode, createContext, useContext, useEffect, useState } from 'react';
6
6
 
7
- import { IconButton, ScrollArea, useTranslation } from '@dxos/react-ui';
7
+ import { Flex, IconButton, ScrollArea, useTranslation } from '@dxos/react-ui';
8
8
 
9
9
  import { meta } from '#meta';
10
10
 
@@ -136,20 +136,18 @@ const formatRelative = (iso: string): string => {
136
136
 
137
137
  const PullRow = ({ pull }: { pull: GithubPullRequest }) => (
138
138
  <li>
139
- <a
140
- href={pull.html_url}
141
- target='_blank'
142
- rel='noopener noreferrer'
143
- className='flex items-start gap-2 px-2 py-1 rounded-sm hover:bg-hover-surface'
144
- >
145
- <img src={pull.user.avatar_url} alt='' className='w-6 h-6 rounded-full shrink-0 mt-0.5' />
146
- <div className='flex flex-col min-w-0 flex-1'>
147
- <span className='text-sm truncate'>{pull.title}</span>
148
- <span className='text-xs text-description truncate'>
149
- #{pull.number} · {pull.user.login} · {pull.merged_at ? formatRelative(pull.merged_at) : ''}
150
- </span>
151
- </div>
152
- </a>
139
+ <Flex asChild align='start' gap='sm' classNames='px-trim-sm py-trim-xs rounded-sm hover:bg-hover-surface'>
140
+ <a href={pull.html_url} target='_blank' rel='noopener noreferrer'>
141
+ <img src={pull.user.avatar_url} alt='' className='size-6 rounded-full shrink-0' />
142
+ <Flex column classNames='min-w-0 flex-1'>
143
+ {/* `leading-6` gives the title the avatar's line box, so the two align on their own. */}
144
+ <span className='text-sm leading-6 truncate'>{pull.title}</span>
145
+ <span className='text-xs text-description truncate'>
146
+ #{pull.number} · {pull.user.login} · {pull.merged_at ? formatRelative(pull.merged_at) : ''}
147
+ </span>
148
+ </Flex>
149
+ </a>
150
+ </Flex>
153
151
  </li>
154
152
  );
155
153
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  import React, { useCallback, useEffect, useMemo, useState } from 'react';
6
6
 
7
- import { Operation } from '@dxos/compute';
7
+ import * as Operation from '@dxos/compute/Operation';
8
8
  import { JsonView, Placeholder, Searchbar } from '@dxos/devtools';
9
9
  import { Entity, Format, Obj, Type } from '@dxos/echo';
10
10
  import { useClient } from '@dxos/react-client';
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { type ComponentType, lazy } from 'react';
6
6
 
7
+ export const CliPanel: ComponentType<any> = lazy(() => import('./CliPanel'));
7
8
  export const DebugGraph: ComponentType<any> = lazy(() => import('./DebugGraph'));
8
9
  export const DevtoolsOverviewContainer: ComponentType<any> = lazy(() => import('./DevtoolsOverviewContainer'));
9
10
  export const GithubPanel: ComponentType<any> = lazy(() => import('./GithubPanel'));
package/src/index.ts CHANGED
@@ -2,5 +2,6 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- export * from './meta';
6
- export * from './types';
5
+ export * as DevtoolsPlugin from './DevtoolsPlugin';
6
+ export * from '#meta';
7
+ export * from '#types';
package/src/meta.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { Plugin } from '@dxos/app-framework';
5
+ import * as Plugin from '@dxos/app-framework/Plugin';
6
6
 
7
7
  import config from '../dx.config';
8
8
 
@@ -6,19 +6,20 @@ import { describe, test } from 'vitest';
6
6
 
7
7
  import { createComposerTestApp } from '@dxos/plugin-testing/harness';
8
8
 
9
+ import { meta } from '#meta';
9
10
  import { DevtoolsPlugin } from '#plugin';
10
11
 
11
- import { meta } from './meta';
12
-
13
12
  const moduleId = (name: string) => `${meta.profile.key}.module.${name}`;
14
13
 
15
14
  describe('DevtoolsPlugin', () => {
16
- test('modules activate on the expected events', async ({ expect }) => {
15
+ // Boot imports the app-graph-builder body (the harness fires the plugin's start event), which
16
+ // can exceed the default 15s under vite-node transform load.
17
+ test('modules activate on the expected events', { timeout: 60_000 }, async ({ expect }) => {
17
18
  await using harness = await createComposerTestApp({
18
19
  plugins: [DevtoolsPlugin()],
19
20
  });
20
21
 
21
- // AppGraphBuilder auto-cascades from Startup (SetupAppGraph).
22
+ // AppGraphBuilder rides the plugin's start event, fired by the harness after Startup.
22
23
  expect(harness.manager.getActive()).toContain(moduleId('AppGraphBuilder'));
23
24
  });
24
25
  });
package/src/plugin.tsx ADDED
@@ -0,0 +1,20 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import * as Plugin from '@dxos/app-framework/Plugin';
6
+
7
+ import { AppGraphBuilder, PluginAsset, ReactContext, ReactSurface, SetupDevtools, Translations } from '#capabilities';
8
+ import { meta } from '#meta';
9
+
10
+ export const DevtoolsPlugin = Plugin.define(meta).pipe(
11
+ Plugin.addModule(AppGraphBuilder),
12
+ Plugin.addModule(PluginAsset),
13
+ Plugin.addModule(ReactContext),
14
+ Plugin.addModule(ReactSurface),
15
+ Plugin.addModule(SetupDevtools),
16
+ Plugin.addModule(Translations),
17
+ Plugin.make,
18
+ );
19
+
20
+ export default DevtoolsPlugin;
@@ -18,6 +18,7 @@ export const translations = [
18
18
  'devtools-overview.label': 'Stats',
19
19
  'debug-app-graph.label': 'App Graph',
20
20
  'debug-tools-explorer.label': 'SDK Explorer',
21
+ 'cli.label': 'CLI',
21
22
 
22
23
  'reset-data.label': 'Reset data (ERASES ALL DATA)',
23
24
  'client.label': 'Client',
@@ -18,6 +18,7 @@ const devtoolsId = `${meta.profile.key}.devtools`;
18
18
  export const id = devtoolsId;
19
19
  export const AppGraph = `${devtoolsId}.appGraph`;
20
20
  export const ToolsExplorer = `${devtoolsId}.toolsExplorer`;
21
+ export const Cli = `${devtoolsId}.cli`;
21
22
 
22
23
  export namespace Client {
23
24
  export const id = `${devtoolsId}.client`;
@@ -0,0 +1,16 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ // @import-as-namespace
6
+
7
+ import * as ActivationEvent from '@dxos/app-framework/ActivationEvent';
8
+
9
+ import { meta } from '#meta';
10
+
11
+ /**
12
+ * The feature's start event: the plugin's start-gated modules (and cross-plugin
13
+ * contributions consumed by this feature) activate here. Fired on demand and by the
14
+ * host's idle trickle.
15
+ */
16
+ export const Start = ActivationEvent.pluginStart(meta.profile.key);
@@ -3,3 +3,4 @@
3
3
  //
4
4
 
5
5
  export * as Devtools from './Devtools';
6
+ export * as DevtoolsEvents from './DevtoolsEvents';