diff --git a/.github/workflows/ecosystem-ci.yml b/.github/workflows/ecosystem-ci.yml
index 5a6dd64..17df18a 100644
--- a/.github/workflows/ecosystem-ci.yml
+++ b/.github/workflows/ecosystem-ci.yml
@@ -27,7 +27,7 @@ jobs:
- run: pnpm install --frozen-lockfile
- run: pnpm test:ecosystem
env:
- ECOSYSTEM_DEVTOOLS_REF: ${{ inputs.ref }}
+ ECOSYSTEM_DEVFRAME_REF: ${{ inputs.ref }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: failure()
uses: actions/upload-artifact@v4
diff --git a/AGENTS.md b/AGENTS.md
index 3daeef7..2fb3123 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -4,6 +4,8 @@
**`devframe`** is the framework-neutral container for one devtool integration, portable across viewers. Build a single tool (its RPC, its SPA, its diagnostics, its CLI/build/spa/embedded outputs) without caring how it'll be displayed. A devframe app runs standalone (CLI, static deploy, embedded SPA) just as well as it mounts inside a hub.
+**`@devframes/hub`** is the framework-neutral hub layer that sits on top of devframe and provides the multi-integration orchestration (docks, terminals, messages, commands). It does not ship UI — implementers (e.g. `@vitejs/devtools-kit`) provide their own UI on top of the hub's RPC + shared-state protocol. See `examples/minimal-vite-devframe-hub/` for a working ~120-line Vite host demonstrating the protocol end to end.
+
## Stack & Structure
ESM TypeScript library. Bundled with `tsdown`. Tested with `vitest`. pnpm workspaces with catalog dependencies (`pnpm-workspace.yaml`); workspace globs reserve `playground`, `docs`, `packages/*`, `examples/*` for future additions.
@@ -50,6 +52,16 @@ All node-side warnings and errors use structured diagnostics via [`nostics`](htt
Prefix: **`DF`**. Codes are sequential 4-digit numbers (e.g. `DF0033`). Check the existing diagnostics file to find the next available number.
+Range allocation:
+- `DF00xx–DF07xx` — `devframe` core (RPC, host, storage, streams, …)
+- `DF80xx–DF89xx` — `@devframes/hub`. Sub-ranges:
+ - `DF80xx` — hub context / lifecycle
+ - `DF81xx` — docks
+ - `DF82xx` — terminals
+ - `DF83xx` — messages
+ - `DF84xx` — commands
+ - `DF85xx` — built-in RPC commands
+
### Adding a new error
1. **Define the code** in the appropriate `diagnostics.ts`:
diff --git a/README.md b/README.md
index 1c4cbb9..c505d82 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,7 @@
-Framework-neutral foundation for building generic DevTools.
+Framework-neutral foundation for building devframes.
diff --git a/alias.ts b/alias.ts
index d0cb424..e1f0e42 100644
--- a/alias.ts
+++ b/alias.ts
@@ -14,7 +14,7 @@ export const alias = {
'devframe/rpc': r('devframe/src/rpc'),
'devframe/types': r('devframe/src/types/index.ts'),
'devframe/node/auth': r('devframe/src/node/auth/index.ts'),
- 'devframe/node/internal': r('devframe/src/node/internal/index.ts'),
+ 'devframe/node/hub-internals': r('devframe/src/node/hub-internals/index.ts'),
'devframe/node': r('devframe/src/node/index.ts'),
'devframe/constants': r('devframe/src/constants.ts'),
'devframe/utils/colors': r('devframe/src/utils/colors.ts'),
@@ -36,6 +36,11 @@ export const alias = {
'devframe/helpers/vite': r('devframe/src/helpers/vite.ts'),
'devframe/adapters/embedded': r('devframe/src/adapters/embedded.ts'),
'devframe/adapters/mcp': r('devframe/src/adapters/mcp/index.ts'),
+ '@devframes/hub/client': r('hub/src/client/index.ts'),
+ '@devframes/hub/constants': r('hub/src/constants.ts'),
+ '@devframes/hub/node': r('hub/src/node/index.ts'),
+ '@devframes/hub/types': r('hub/src/types/index.ts'),
+ '@devframes/hub': r('hub/src/index.ts'),
'@devframes/nuxt/runtime/plugin.client': r('nuxt/src/runtime/plugin.client.ts'),
'@devframes/nuxt': r('nuxt/src/index.ts'),
'devframe/recipes/open-helpers': r('devframe/src/recipes/open-helpers.ts'),
diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts
index d129870..034b98b 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -28,6 +28,7 @@ function guideItems(prefix: string): DefaultTheme.NavItemWithLink[] {
{ text: 'Structured Diagnostics', link: `${prefix}/guide/diagnostics` },
{ text: 'Client', link: `${prefix}/guide/client` },
{ text: 'Standalone CLI', link: `${prefix}/guide/standalone-cli` },
+ { text: 'Hub (multi-tool)', link: `${prefix}/guide/hub` },
{ text: 'Agent-Native (experimental)', link: `${prefix}/guide/agent-native` },
]
}
@@ -98,7 +99,7 @@ export function devframeNav(prefix = ''): DefaultTheme.NavItem[] {
export default withMermaid(defineConfig({
title: 'Devframe',
- description: 'Framework-neutral foundation for building generic DevTools — RPC layer, hosts, and adapters.',
+ description: 'Framework-neutral foundation for building generic devframes — RPC layer, hosts, and adapters.',
head: [
['link', { rel: 'icon', type: 'image/svg+xml', href: '/logo.svg' }],
['link', { rel: 'apple-touch-icon', href: '/logo.svg' }],
diff --git a/docs/adapters/cli.md b/docs/adapters/cli.md
index 076108c..3e363f8 100644
--- a/docs/adapters/cli.md
+++ b/docs/adapters/cli.md
@@ -26,11 +26,11 @@ Running the resulting binary:
my-devframe # dev server at http://localhost:9999/
my-devframe --port 8080
my-devframe build --out-dir dist-static
-my-devframe build --out-dir dist-static --base /devtools/
+my-devframe build --out-dir dist-static --base /devframe/
my-devframe mcp # stdio MCP server (experimental)
```
-Standalone CLI serves the SPA at `/` by default. The `/__devtools/` prefix is for *hosted* adapters where devframe mounts alongside an existing app — see [Mount paths](./#mount-paths).
+Standalone CLI serves the SPA at `/` by default. The `/__devframe/` prefix is for *hosted* adapters where devframe mounts alongside an existing app — see [Mount paths](./#mount-paths).
## Options
diff --git a/docs/adapters/embedded.md b/docs/adapters/embedded.md
index 5d2ae0b..ef9bf64 100644
--- a/docs/adapters/embedded.md
+++ b/docs/adapters/embedded.md
@@ -15,6 +15,6 @@ await createEmbedded(devframe, { ctx: existingCtx })
| Option | Required | Description |
|--------|----------|-------------|
-| `ctx` | ✓ | Target `DevToolsNodeContext` the devframe is registered into. |
+| `ctx` | ✓ | Target `DevframeNodeContext` the devframe is registered into. |
Useful when a host loads devframes based on runtime conditions (feature flags, user opt-in, dynamic discovery) rather than static config.
diff --git a/docs/errors/DF0006.md b/docs/errors/DF0006.md
index fa848d0..d4d6076 100644
--- a/docs/errors/DF0006.md
+++ b/docs/errors/DF0006.md
@@ -18,4 +18,4 @@ Register the function with `ctx.rpc.register(defineRpcFunction({ name }))` befor
## Source
-- [`packages/devframe/src/node/host-functions.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/node/host-functions.ts) — `RpcFunctionsHost.invokeLocal()` throws `DF0006` when the requested method has not been registered on this host.
+- [`packages/devframe/src/node/host-functions.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/node/host-functions.ts) — `RpcFunctionsHost.invokeLocal()` throws `DF0006` when the requested method has not been registered on this host.
diff --git a/docs/errors/DF0007.md b/docs/errors/DF0007.md
index 7cc5e40..d95e9cd 100644
--- a/docs/errors/DF0007.md
+++ b/docs/errors/DF0007.md
@@ -6,7 +6,7 @@ outline: deep
## Message
-> AsyncLocalStorage is not set, it likely to be an internal bug of the DevTools foundation
+> AsyncLocalStorage is not set, it likely to be an internal bug of the Devframe foundation
## Cause
@@ -18,4 +18,4 @@ Only call `getCurrentRpcSession()` from RPC handlers executed by the server. Rep
## Source
-- [`packages/devframe/src/node/host-functions.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/node/host-functions.ts) — `getCurrentRpcSession()` throws `DF0007` when called outside the RPC dispatch async context.
+- [`packages/devframe/src/node/host-functions.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/node/host-functions.ts) — `getCurrentRpcSession()` throws `DF0007` when called outside the RPC dispatch async context.
diff --git a/docs/errors/DF0008.md b/docs/errors/DF0008.md
index 57db674..b423901 100644
--- a/docs/errors/DF0008.md
+++ b/docs/errors/DF0008.md
@@ -10,7 +10,7 @@ outline: deep
## Cause
-`DevToolsViewHost.hostStatic()` was asked to mount a directory that doesn't exist on disk.
+`DevframeViewHost.hostStatic()` was asked to mount a directory that doesn't exist on disk.
## Fix
@@ -18,4 +18,4 @@ Verify the `distDir` path resolves correctly (run your SPA build first, and chec
## Source
-- [`packages/devframe/src/node/host-views.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/node/host-views.ts) — `DevToolsViewHost.hostStatic()` throws `DF0008` when the resolved `distDir` does not exist on disk.
+- [`packages/devframe/src/node/host-views.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/node/host-views.ts) — `DevframeViewHost.hostStatic()` throws `DF0008` when the resolved `distDir` does not exist on disk.
diff --git a/docs/errors/DF0012.md b/docs/errors/DF0012.md
index d4f5ae6..7706726 100644
--- a/docs/errors/DF0012.md
+++ b/docs/errors/DF0012.md
@@ -18,4 +18,4 @@ Delete the file to reset to defaults, or investigate how it became malformed.
## Source
-- [`packages/devframe/src/node/storage.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/node/storage.ts) — `createStorage()` catches `JSON.parse` errors and logs `DF0012` (with the cause attached) before falling back to `initialValue`.
+- [`packages/devframe/src/node/storage.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/node/storage.ts) — `createStorage()` catches `JSON.parse` errors and logs `DF0012` (with the cause attached) before falling back to `initialValue`.
diff --git a/docs/errors/DF0013.md b/docs/errors/DF0013.md
index 00a94fd..661b593 100644
--- a/docs/errors/DF0013.md
+++ b/docs/errors/DF0013.md
@@ -18,4 +18,4 @@ Pass `initialValue` on the first call: `ctx.rpc.sharedState.get(key, { initialVa
## Source
-- [`packages/devframe/src/node/rpc-shared-state.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/node/rpc-shared-state.ts) — `RpcSharedStateHost.get()` throws `DF0013` when neither an existing entry nor an `initialValue` is provided for a key.
+- [`packages/devframe/src/node/rpc-shared-state.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/node/rpc-shared-state.ts) — `RpcSharedStateHost.get()` throws `DF0013` when neither an existing entry nor an `initialValue` is provided for a key.
diff --git a/docs/errors/DF0014.md b/docs/errors/DF0014.md
index bef3792..4798c8e 100644
--- a/docs/errors/DF0014.md
+++ b/docs/errors/DF0014.md
@@ -50,4 +50,4 @@ If you didn't intend for this function to be agent-exposed, remove the `agent` f
## Source
-- [`packages/devframe/src/node/host-agent.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/node/host-agent.ts) — agent registration throws `DF0014` when a tool's `agent.description` is missing or empty.
+- [`packages/devframe/src/node/host-agent.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/node/host-agent.ts) — agent registration throws `DF0014` when a tool's `agent.description` is missing or empty.
diff --git a/docs/errors/DF0015.md b/docs/errors/DF0015.md
index 35ae3e5..26562df 100644
--- a/docs/errors/DF0015.md
+++ b/docs/errors/DF0015.md
@@ -34,4 +34,4 @@ ctx.agent.registerTool({ id: 'my-tool', /* new config */ })
## Source
-- [`packages/devframe/src/node/host-agent.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/node/host-agent.ts) — `ctx.agent.registerTool()` throws `DF0015` when the tool id collides with an existing agent tool or an RPC function's `agent` exposure.
+- [`packages/devframe/src/node/host-agent.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/node/host-agent.ts) — `ctx.agent.registerTool()` throws `DF0015` when the tool id collides with an existing agent tool or an RPC function's `agent` exposure.
diff --git a/docs/errors/DF0016.md b/docs/errors/DF0016.md
index 1fce205..de41517 100644
--- a/docs/errors/DF0016.md
+++ b/docs/errors/DF0016.md
@@ -22,4 +22,4 @@ Pick a distinct id or unregister the existing resource first via the handle retu
## Source
-- [`packages/devframe/src/node/host-agent.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/node/host-agent.ts) — `ctx.agent.registerResource()` throws `DF0016` when the resource id is already registered on the host.
+- [`packages/devframe/src/node/host-agent.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/node/host-agent.ts) — `ctx.agent.registerResource()` throws `DF0016` when the resource id is already registered on the host.
diff --git a/docs/errors/DF0017.md b/docs/errors/DF0017.md
index 38aa747..8d60b7c 100644
--- a/docs/errors/DF0017.md
+++ b/docs/errors/DF0017.md
@@ -28,4 +28,4 @@ The agent-native surface is experimental and may change without a major version
## Source
-- [`packages/devframe/src/adapters/mcp/build-server.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/adapters/mcp/build-server.ts) — `createMcpServer()` throws `DF0017` when the requested transport is unsupported or when the underlying transport fails to `connect()`.
+- [`packages/devframe/src/adapters/mcp/build-server.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/adapters/mcp/build-server.ts) — `createMcpServer()` throws `DF0017` when the requested transport is unsupported or when the underlying transport fails to `connect()`.
diff --git a/docs/errors/DF0019.md b/docs/errors/DF0019.md
index e084aaf..ec950ab 100644
--- a/docs/errors/DF0019.md
+++ b/docs/errors/DF0019.md
@@ -49,4 +49,4 @@ defineRpcFunction({
## Source
-- [`packages/devframe/src/rpc/collector.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/rpc/collector.ts) — `RpcFunctionsCollectorBase.register()` throws `DF0019` when a definition has `agent` set but is not declared `jsonSerializable: true`.
+- [`packages/devframe/src/rpc/collector.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/rpc/collector.ts) — `RpcFunctionsCollectorBase.register()` throws `DF0019` when a definition has `agent` set but is not declared `jsonSerializable: true`.
diff --git a/docs/errors/DF0020.md b/docs/errors/DF0020.md
index 36395f2..09a883b 100644
--- a/docs/errors/DF0020.md
+++ b/docs/errors/DF0020.md
@@ -53,4 +53,4 @@ Or convert the payload to a JSON-safe shape (e.g. an array of entries, an ISO st
## Source
-- [`packages/devframe/src/rpc/serialization.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/rpc/serialization.ts) — the strict JSON serializer throws `DF0020` (with the offending path and runtime type) when a `jsonSerializable: true` payload contains a non-JSON value.
+- [`packages/devframe/src/rpc/serialization.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/rpc/serialization.ts) — the strict JSON serializer throws `DF0020` (with the offending path and runtime type) when a `jsonSerializable: true` payload contains a non-JSON value.
diff --git a/docs/errors/DF0021.md b/docs/errors/DF0021.md
index 47159f9..eac91e2 100644
--- a/docs/errors/DF0021.md
+++ b/docs/errors/DF0021.md
@@ -22,4 +22,4 @@ ctx.rpc.register(defineRpcFunction({ name: 'my-plugin:fn', handler: () => 1 }),
## Source
-- [`packages/devframe/src/rpc/collector.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/rpc/collector.ts) — `RpcFunctionsCollectorBase.register()` throws `DF0021` when an RPC name is already registered and `force` is not set.
+- [`packages/devframe/src/rpc/collector.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/rpc/collector.ts) — `RpcFunctionsCollectorBase.register()` throws `DF0021` when an RPC name is already registered and `force` is not set.
diff --git a/docs/errors/DF0022.md b/docs/errors/DF0022.md
index 5941f87..47cc11a 100644
--- a/docs/errors/DF0022.md
+++ b/docs/errors/DF0022.md
@@ -18,4 +18,4 @@ Call `ctx.rpc.register()` first, or pass `force: true` to `update()` to register
## Source
-- [`packages/devframe/src/rpc/collector.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/rpc/collector.ts) — `RpcFunctionsCollectorBase.update()` throws `DF0022` when the named function was never registered.
+- [`packages/devframe/src/rpc/collector.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/rpc/collector.ts) — `RpcFunctionsCollectorBase.update()` throws `DF0022` when the named function was never registered.
diff --git a/docs/errors/DF0023.md b/docs/errors/DF0023.md
index 5937e06..975972e 100644
--- a/docs/errors/DF0023.md
+++ b/docs/errors/DF0023.md
@@ -18,4 +18,4 @@ Confirm the function name matches a registration. RPC names are namespaced — t
## Source
-- [`packages/devframe/src/rpc/collector.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/rpc/collector.ts) — collector `get()`/lookup paths throw `DF0023` when consumers ask for a function that has not been registered.
+- [`packages/devframe/src/rpc/collector.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/rpc/collector.ts) — collector `get()`/lookup paths throw `DF0023` when consumers ask for a function that has not been registered.
diff --git a/docs/errors/DF0024.md b/docs/errors/DF0024.md
index 07ca9b7..4ecb3cb 100644
--- a/docs/errors/DF0024.md
+++ b/docs/errors/DF0024.md
@@ -18,5 +18,5 @@ Add either `handler: ...` directly on the definition, or `setup: ctx => ({ handl
## Source
-- [`packages/devframe/src/rpc/handler.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/rpc/handler.ts) — invocation throws `DF0024` when neither `handler` nor a `setup` returning `{ handler }` is provided.
-- [`packages/devframe/src/rpc/dump/index.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/rpc/dump/index.ts) — dump generation also requires a handler and throws `DF0024` if the definition is incomplete.
+- [`packages/devframe/src/rpc/handler.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/rpc/handler.ts) — invocation throws `DF0024` when neither `handler` nor a `setup` returning `{ handler }` is provided.
+- [`packages/devframe/src/rpc/dump/index.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/rpc/dump/index.ts) — dump generation also requires a handler and throws `DF0024` if the definition is incomplete.
diff --git a/docs/errors/DF0025.md b/docs/errors/DF0025.md
index fa8285b..e0af4b0 100644
--- a/docs/errors/DF0025.md
+++ b/docs/errors/DF0025.md
@@ -18,4 +18,4 @@ Re-run `createBuild` to regenerate the dump, or check that the call site uses th
## Source
-- [`packages/devframe/src/rpc/dump/index.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/rpc/dump/index.ts) — the static-mode dump resolver throws `DF0025` when a client calls a function name that is not present in the baked dump store.
+- [`packages/devframe/src/rpc/dump/index.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/rpc/dump/index.ts) — the static-mode dump resolver throws `DF0025` when a client calls a function name that is not present in the baked dump store.
diff --git a/docs/errors/DF0026.md b/docs/errors/DF0026.md
index cccd666..f7f4627 100644
--- a/docs/errors/DF0026.md
+++ b/docs/errors/DF0026.md
@@ -28,4 +28,4 @@ defineRpcFunction({
## Source
-- [`packages/devframe/src/rpc/dump/index.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/rpc/dump/index.ts) — the static-mode dump resolver throws `DF0026` when none of the pre-computed inputs matches the call's args and no `fallback` was configured.
+- [`packages/devframe/src/rpc/dump/index.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/rpc/dump/index.ts) — the static-mode dump resolver throws `DF0026` when none of the pre-computed inputs matches the call's args and no `fallback` was configured.
diff --git a/docs/errors/DF0027.md b/docs/errors/DF0027.md
index 099ed35..d9da4c3 100644
--- a/docs/errors/DF0027.md
+++ b/docs/errors/DF0027.md
@@ -18,4 +18,4 @@ Drop the `dump` field, or change the function `type` to `'static'` / `'query'` i
## Source
-- [`packages/devframe/src/rpc/validation.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/rpc/validation.ts) — definition validation throws `DF0027` when a `dump` field is attached to an `'action'` or `'event'` function.
+- [`packages/devframe/src/rpc/validation.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/rpc/validation.ts) — definition validation throws `DF0027` when a `dump` field is attached to an `'action'` or `'event'` function.
diff --git a/docs/errors/DF0028.md b/docs/errors/DF0028.md
index a0f840e..d9758fd 100644
--- a/docs/errors/DF0028.md
+++ b/docs/errors/DF0028.md
@@ -18,4 +18,4 @@ Remove `snapshot: true`, or change the function `type` to `'query'`.
## Source
-- [`packages/devframe/src/rpc/validation.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/rpc/validation.ts) — definition validation throws `DF0028` when `snapshot: true` is set on a function whose type is not `'query'`.
+- [`packages/devframe/src/rpc/validation.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/rpc/validation.ts) — definition validation throws `DF0028` when `snapshot: true` is set on a function whose type is not `'query'`.
diff --git a/docs/errors/DF0029.md b/docs/errors/DF0029.md
index a4111b4..f5c41fc 100644
--- a/docs/errors/DF0029.md
+++ b/docs/errors/DF0029.md
@@ -22,4 +22,4 @@ This is a soft warning — the stream keeps running and remaining chunks still f
## Source
-- [`packages/devframe/src/client/rpc-streaming.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/client/rpc-streaming.ts) — the client subscription queue logs `DF0029` (with the dropped chunk count) when buffered chunks exceed `highWaterMark`.
+- [`packages/devframe/src/client/rpc-streaming.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/client/rpc-streaming.ts) — the client subscription queue logs `DF0029` (with the dropped chunk count) when buffered chunks exceed `highWaterMark`.
diff --git a/docs/errors/DF0030.md b/docs/errors/DF0030.md
index b7d0d28..2065fcd 100644
--- a/docs/errors/DF0030.md
+++ b/docs/errors/DF0030.md
@@ -20,4 +20,4 @@ A client subscribed to a stream id that the server-side channel doesn't know abo
## Source
-- [`packages/devframe/src/node/rpc-streaming.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/node/rpc-streaming.ts) — the streaming subscribe/unsubscribe paths log `DF0030` when a client references an `id` that no producer has started (and no replay buffer covers).
+- [`packages/devframe/src/node/rpc-streaming.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/node/rpc-streaming.ts) — the streaming subscribe/unsubscribe paths log `DF0030` when a client references an `id` that no producer has started (and no replay buffer covers).
diff --git a/docs/errors/DF0031.md b/docs/errors/DF0031.md
index 7a80000..6e9eb10 100644
--- a/docs/errors/DF0031.md
+++ b/docs/errors/DF0031.md
@@ -33,4 +33,4 @@ catch (err) {
## Source
-- [`packages/devframe/src/utils/streaming-channel.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/utils/streaming-channel.ts) — `stream.write()` throws `DF0031` when called after the stream has been closed, errored, or aborted by the consumer.
+- [`packages/devframe/src/utils/streaming-channel.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/utils/streaming-channel.ts) — `stream.write()` throws `DF0031` when called after the stream has been closed, errored, or aborted by the consumer.
diff --git a/docs/errors/DF0032.md b/docs/errors/DF0032.md
index 109fcca..4171fee 100644
--- a/docs/errors/DF0032.md
+++ b/docs/errors/DF0032.md
@@ -19,4 +19,4 @@ Two calls to `ctx.rpc.streaming.create(name, ...)` used the same channel name. E
## Source
-- [`packages/devframe/src/node/rpc-streaming.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/node/rpc-streaming.ts) — `ctx.rpc.streaming.create()` throws `DF0032` when the requested channel name is already registered on the context.
+- [`packages/devframe/src/node/rpc-streaming.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/node/rpc-streaming.ts) — `ctx.rpc.streaming.create()` throws `DF0032` when the requested channel name is already registered on the context.
diff --git a/docs/errors/DF0033.md b/docs/errors/DF0033.md
index 8bee827..e2468e4 100644
--- a/docs/errors/DF0033.md
+++ b/docs/errors/DF0033.md
@@ -26,4 +26,4 @@ This is a soft warning — the surrounding Vite dev server keeps running, but th
## Source
-- [`packages/devframe/src/helpers/vite.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/helpers/vite.ts) — `viteDevBridge({ devMiddleware })` logs `DF0033` when port resolution or `createDevServer` throws during `configureServer`.
+- [`packages/devframe/src/helpers/vite.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/helpers/vite.ts) — `viteDevBridge({ devMiddleware })` logs `DF0033` when port resolution or `createDevServer` throws during `configureServer`.
diff --git a/docs/errors/DF8100.md b/docs/errors/DF8100.md
new file mode 100644
index 0000000..b9c46ca
--- /dev/null
+++ b/docs/errors/DF8100.md
@@ -0,0 +1,22 @@
+---
+outline: deep
+---
+
+# DF8100: Dock Already Registered
+
+## Message
+
+> Dock with id "`{id}`" is already registered
+
+## Cause
+
+`ctx.docks.register(view)` was called with an `id` that another dock already owns. Dock ids are unique per hub context, so the second registration would silently override the first if allowed.
+
+## Fix
+
+- Pick a different dock id (best practice: namespace under your tool's id, e.g. `my-tool:overview`).
+- If overwriting is intentional (e.g. a hot-reload scenario), pass `force: true`: `ctx.docks.register(view, true)`.
+
+## Source
+
+- [`packages/hub/src/node/host-docks.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-docks.ts) — `DevframeDocksHost.register()` throws when `views.has(view.id) && !force`.
diff --git a/docs/errors/DF8101.md b/docs/errors/DF8101.md
new file mode 100644
index 0000000..b04270c
--- /dev/null
+++ b/docs/errors/DF8101.md
@@ -0,0 +1,22 @@
+---
+outline: deep
+---
+
+# DF8101: Cannot Change Dock Id
+
+## Message
+
+> Cannot change the id of a dock. Use register() to add new docks.
+
+## Cause
+
+The `update` handle returned by `ctx.docks.register(view)` received a patch whose `id` differs from the original. Dock ids are immutable post-registration — they key the dock list and any shared-state references.
+
+## Fix
+
+- Drop `id` from your patch; only pass the fields you actually want to mutate.
+- To replace one dock with a different one: call `ctx.docks.register(newView)` (or `register(newView, true)` to overwrite an existing id).
+
+## Source
+
+- [`packages/hub/src/node/host-docks.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-docks.ts) — `DevframeDocksHost.register()` returns an `update` callable that throws this when the patch carries a different `id`.
diff --git a/docs/errors/DF8102.md b/docs/errors/DF8102.md
new file mode 100644
index 0000000..8049b45
--- /dev/null
+++ b/docs/errors/DF8102.md
@@ -0,0 +1,22 @@
+---
+outline: deep
+---
+
+# DF8102: Dock Not Registered
+
+## Message
+
+> Dock with id "`{id}`" is not registered. Use register() to add new docks.
+
+## Cause
+
+`ctx.docks.update(view)` was called with an `id` that has no prior registration. `update` is for mutating existing entries; new entries must go through `register`.
+
+## Fix
+
+- Use `ctx.docks.register(view)` for new entries.
+- Verify the id matches a previously registered dock — typos / case mismatches are the usual cause.
+
+## Source
+
+- [`packages/hub/src/node/host-docks.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-docks.ts) — `DevframeDocksHost.update()` throws when `views.has(view.id) === false`.
diff --git a/docs/errors/DF8200.md b/docs/errors/DF8200.md
new file mode 100644
index 0000000..dc73c70
--- /dev/null
+++ b/docs/errors/DF8200.md
@@ -0,0 +1,22 @@
+---
+outline: deep
+---
+
+# DF8200: Terminal Session Already Registered
+
+## Message
+
+> Terminal session with id "`{id}`" already registered
+
+## Cause
+
+`ctx.terminals.register(session)` or `ctx.terminals.startChildProcess(opts, terminal)` was called with an `id` that another session already owns. Terminal ids are unique per hub context.
+
+## Fix
+
+- Pick a different session id (namespace under your tool: `my-tool:build-server`, `my-tool:tests`, …).
+- If you want to reuse an existing session's slot, `ctx.terminals.remove(session)` first.
+
+## Source
+
+- [`packages/hub/src/node/host-terminals.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-terminals.ts) — `DevframeTerminalsHost.register()` and `startChildProcess()` throw when the id is already taken.
diff --git a/docs/errors/DF8201.md b/docs/errors/DF8201.md
new file mode 100644
index 0000000..d828f6a
--- /dev/null
+++ b/docs/errors/DF8201.md
@@ -0,0 +1,22 @@
+---
+outline: deep
+---
+
+# DF8201: Terminal Session Not Registered
+
+## Message
+
+> Terminal session with id "`{id}`" not registered
+
+## Cause
+
+`ctx.terminals.update(patch)` was called with an `id` for which no session has been registered.
+
+## Fix
+
+- Use `ctx.terminals.register(session)` to add new sessions.
+- Verify the id matches an existing session — common cause is updating after `remove()`.
+
+## Source
+
+- [`packages/hub/src/node/host-terminals.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-terminals.ts) — `DevframeTerminalsHost.update()` throws when `sessions.has(patch.id) === false`.
diff --git a/docs/errors/DF8400.md b/docs/errors/DF8400.md
new file mode 100644
index 0000000..c6f7257
--- /dev/null
+++ b/docs/errors/DF8400.md
@@ -0,0 +1,22 @@
+---
+outline: deep
+---
+
+# DF8400: Command Already Registered
+
+## Message
+
+> Command "`{id}`" is already registered
+
+## Cause
+
+`ctx.commands.register(command)` was called with an `id` that another command already owns. Command ids are unique per hub context.
+
+## Fix
+
+- Pick a different command id (namespace under your tool: `my-tool:reload`, `my-tool:open-settings`, …).
+- To replace, call `ctx.commands.unregister(id)` first.
+
+## Source
+
+- [`packages/hub/src/node/host-commands.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-commands.ts) — `DevframeCommandsHost.register()` throws when `commands.has(command.id)`.
diff --git a/docs/errors/DF8401.md b/docs/errors/DF8401.md
new file mode 100644
index 0000000..c6b9a09
--- /dev/null
+++ b/docs/errors/DF8401.md
@@ -0,0 +1,22 @@
+---
+outline: deep
+---
+
+# DF8401: Cannot Change Command Id
+
+## Message
+
+> Cannot change the id of a command. Use register() to add new commands.
+
+## Cause
+
+The `update` handle returned by `ctx.commands.register(cmd)` received a patch with an `id` key. Command ids are immutable post-registration.
+
+## Fix
+
+- Drop `id` from the patch object.
+- To register a new command, call `ctx.commands.register(newCmd)`.
+
+## Source
+
+- [`packages/hub/src/node/host-commands.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-commands.ts) — `DevframeCommandsHost.register()` returns a `update` callable that throws when `'id' in patch`.
diff --git a/docs/errors/DF8402.md b/docs/errors/DF8402.md
new file mode 100644
index 0000000..cc1e3e3
--- /dev/null
+++ b/docs/errors/DF8402.md
@@ -0,0 +1,23 @@
+---
+outline: deep
+---
+
+# DF8402: Command Not Registered
+
+## Message
+
+> Command "`{id}`" is not registered
+
+## Cause
+
+`ctx.commands.execute(id, …)` (or the `update()` handle returned by `register()`) was called with an `id` that has no registration. The command was either never registered or has already been `unregister()`'d.
+
+## Fix
+
+- Register the command first via `ctx.commands.register({ id, title, handler })`.
+- Verify the id — typos and stale references are the usual cause.
+- If invoking client-side, register the command on the client via `ctx.commands.register` in the dock client script.
+
+## Source
+
+- [`packages/hub/src/node/host-commands.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-commands.ts) — `DevframeCommandsHost.execute()` and the `update` handle throw when the id is missing.
diff --git a/docs/errors/DF8403.md b/docs/errors/DF8403.md
new file mode 100644
index 0000000..9310fd1
--- /dev/null
+++ b/docs/errors/DF8403.md
@@ -0,0 +1,23 @@
+---
+outline: deep
+---
+
+# DF8403: Duplicate Command Id
+
+## Message
+
+> Command id "`{id}`" is already used by another command or child command
+
+## Cause
+
+`ctx.commands.register(command)` or a command handle `update()` received a command tree where at least one id is already owned by another top-level command or child command. Command ids are globally unique per hub context, including child commands.
+
+## Fix
+
+- Namespace every command id under your tool, such as `my-tool:reload` or `my-tool:open-settings`.
+- Give each child command its own id instead of reusing the parent id.
+- Unregister the previous command before replacing it with a different command tree.
+
+## Source
+
+- [`packages/hub/src/node/host-commands.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-commands.ts) — `DevframeCommandsHost.register()` and command handle `update()` throw when a command id is duplicated.
diff --git a/docs/guide/agent-native.md b/docs/guide/agent-native.md
index 3049c7f..ade90e6 100644
--- a/docs/guide/agent-native.md
+++ b/docs/guide/agent-native.md
@@ -5,7 +5,7 @@ outline: deep
# Agent-Native Devframe
::: warning Experimental
-The agent-native surface (`agent` field on `defineRpcFunction`, `DevToolsAgentHost`, and the `devframe/adapters/mcp` adapter) is experimental and may change without a major version bump until it stabilizes.
+The agent-native surface (`agent` field on `defineRpcFunction`, `DevframeAgentHost`, and the `devframe/adapters/mcp` adapter) is experimental and may change without a major version bump until it stabilizes.
:::
Devframe can expose the same surface a browser UI consumes — RPC functions, resources, and shared state — to coding agents (Claude Desktop / Cursor / Zed / Claude Code, or any MCP-speaking client). Agent exposure is opt-in per function; functions stay private by default.
@@ -15,7 +15,7 @@ Devframe can expose the same surface a browser UI consumes — RPC functions, re
Three building blocks:
1. **An `agent` field on `defineRpcFunction`.** Add `agent: { description, ... }` to opt a function in. Functions without the field stay private.
-2. **`ctx.agent`** — a host exposed on `DevToolsNodeContext`. Plugins register tools that aren't backed by an RPC, and expose readable resources (e.g. a Markdown build summary).
+2. **`ctx.agent`** — a host exposed on `DevframeNodeContext`. Plugins register tools that aren't backed by an RPC, and expose readable resources (e.g. a Markdown build summary).
3. **The MCP adapter** (`devframe/adapters/mcp`) — translates the agent host into a [Model Context Protocol](https://modelcontextprotocol.io) server, currently over `stdio`.
## Exposing an RPC function
diff --git a/docs/guide/built-with.md b/docs/guide/built-with.md
index 7c8372b..45f7b8d 100644
--- a/docs/guide/built-with.md
+++ b/docs/guide/built-with.md
@@ -4,7 +4,7 @@ outline: deep
# Built with Devframe
-Real-world devtools shipping on Devframe:
+Real-world devframes:
- [**Vite DevTools**](https://devtools.vite.dev/) — the host that bundles multiple devframes into one UI (docks, command palette, terminals). Mount your own definition into it via the [`vite` adapter](/adapters/vite).
- [**ESLint Config Inspector**](https://github.com/eslint/config-inspector) — official ESLint tool for inspecting flat configs.
diff --git a/docs/guide/client.md b/docs/guide/client.md
index 215db22..7d3a286 100644
--- a/docs/guide/client.md
+++ b/docs/guide/client.md
@@ -8,7 +8,7 @@ The browser-side client is how a dock iframe, remote-hosted page, or standalone
## Connecting
-`devframe/client` exports `connectDevframe` (an alias of `getDevToolsRpcClient`) — use either name:
+`devframe/client` exports `connectDevframe` (an alias of `getDevframeRpcClient`) — use either name:
```ts
import { connectDevframe } from 'devframe/client'
@@ -18,7 +18,7 @@ const rpc = await connectDevframe()
const modules = await rpc.call('my-devframe:get-modules', { limit: 10 })
```
-`connectDevframe` auto-detects the backend via `__devtools/__connection.json`, with a sequence of base URLs as fallback. No arguments are needed when the client is hosted from the default mount path.
+`connectDevframe` auto-detects the backend via `__devframe/__connection.json`, with a sequence of base URLs as fallback. No arguments are needed when the client is hosted from the default mount path.
### Runtime basePath discovery
@@ -46,7 +46,7 @@ await connectDevframe({
| Option | Description |
|--------|-------------|
-| `baseURL` | Mount path to probe for `__connection.json`. Accepts an array for fallback. Default: `'./'` — resolved relative to `document.baseURI` so the SPA finds its meta wherever it was deployed. Pass an explicit absolute path (e.g. `'/__devtools/'`) when calling from outside the SPA — say, an embedded webcomponent injected into a host app. |
+| `baseURL` | Mount path to probe for `__connection.json`. Accepts an array for fallback. Default: `'./'` — resolved relative to `document.baseURI` so the SPA finds its meta wherever it was deployed. Pass an explicit absolute path (e.g. `'/__devframe/'`) when calling from outside the SPA — say, an embedded webcomponent injected into a host app. |
| `authToken` | Override the auth token. Defaults to a locally-persisted human-readable id. |
| `cacheOptions` | `true` to enable caching with defaults, or an options object. |
| `wsOptions` | Forwarded to the WebSocket transport (reconnect, heartbeat, etc.). |
@@ -55,7 +55,7 @@ await connectDevframe({
## Modes
-The client runs in one of two modes depending on the backend advertised in `__devtools/__connection.json`:
+The client runs in one of two modes depending on the backend advertised in `__devframe/__connection.json`:
| Backend | When | Capabilities |
|---------|------|--------------|
@@ -89,7 +89,7 @@ const ok = await rpc.requestTrustWithToken('another-token')
### Broadcast-channel sync
-`connectDevframe` listens on a shared `BroadcastChannel` (named `vite-devtools-auth` for cross-tab handshake interop with Vite DevTools' auth page) for `auth-update` messages. When an auth page in another tab announces a new token, every open client requests trust with it automatically — no reload required.
+`connectDevframe` listens on a shared `BroadcastChannel` (named `devframe-auth` for cross-tab handshake interop with Vite DevTools' auth page) for `auth-update` messages. When an auth page in another tab announces a new token, every open client requests trust with it automatically — no reload required.
## Calling functions
diff --git a/docs/guide/devframe-definition.md b/docs/guide/devframe-definition.md
index 27cddc8..8a51903 100644
--- a/docs/guide/devframe-definition.md
+++ b/docs/guide/devframe-definition.md
@@ -68,19 +68,19 @@ The CLI dev server sets `mode: 'dev'`; `createBuild` sets `mode: 'build'`.
## The setup context
-`setup(ctx)` receives a `DevToolsNodeContext`:
+`setup(ctx)` receives a `DevframeNodeContext`:
```ts
-interface DevToolsNodeContext {
+interface DevframeNodeContext {
readonly cwd: string
readonly workspaceRoot: string
readonly mode: 'dev' | 'build'
- host: DevToolsHost // runtime abstraction (mountStatic / resolveOrigin / getStorageDir)
+ host: DevframeHost // runtime abstraction (mountStatic / resolveOrigin / getStorageDir)
rpc: RpcFunctionsHost // register + broadcast + sharedState
- views: DevToolsViewHost // static file hosting (`hostStatic`)
- diagnostics: DevToolsDiagnosticsHost
- agent: DevToolsAgentHost // experimental
+ views: DevframeViewHost // static file hosting (`hostStatic`)
+ diagnostics: DevframeDiagnosticsHost
+ agent: DevframeAgentHost // experimental
}
```
diff --git a/docs/guide/diagnostics.md b/docs/guide/diagnostics.md
index bdbed29..5775225 100644
--- a/docs/guide/diagnostics.md
+++ b/docs/guide/diagnostics.md
@@ -4,7 +4,7 @@ outline: deep
# Structured Diagnostics
-`ctx.diagnostics` is a thin layer over [`nostics`](https://www.npmjs.com/package/nostics) that lets integrations register coded errors and warnings into a shared lookup without depending on `nostics` directly. Use it for author-defined coded diagnostics — errors, warnings, deprecations — with a stable code, a documentation URL, and a structured payload. For free-form runtime output that should appear in the DevTools UI, use [`ctx.messages`](https://devtools.vite.dev/kit/messages).
+`ctx.diagnostics` is a thin layer over [`nostics`](https://www.npmjs.com/package/nostics) that lets integrations register coded errors and warnings into a shared lookup without depending on `nostics` directly. Use it for author-defined coded diagnostics — errors, warnings, deprecations — with a stable code, a documentation URL, and a structured payload. For free-form runtime output that should appear in the Devframe UI, use [`ctx.messages`](https://devtools.vite.dev/kit/messages).
| Surface | Purpose | Example |
|---------|---------|---------|
@@ -14,7 +14,7 @@ outline: deep
## Shape
```ts
-interface DevToolsDiagnosticsHost {
+interface DevframeDiagnosticsHost {
/** Proxy-backed lookup over every registered code. */
readonly logger: Record
@@ -135,6 +135,6 @@ Each page covers the message, cause, example, and fix — see any [DF code page]
## When to use what
- **`ctx.diagnostics`** — coded conditions worth looking up: misconfiguration, deprecations, validation failures, internal invariants. Always docs-backed. Often thrown.
-- **`ctx.messages`** — user-facing activity surfaces in the DevTools UI: progress indicators, audit results, "URL copied" toasts. Just a message and a level.
+- **`ctx.messages`** — user-facing activity surfaces in the Devframe UI: progress indicators, audit results, "URL copied" toasts. Just a message and a level.
-Diagnostics target tool authors and CI; messages target the human in front of the DevTools panel.
+Diagnostics target tool authors and CI; messages target the human in front of the Devframe panel.
diff --git a/docs/guide/hub.md b/docs/guide/hub.md
new file mode 100644
index 0000000..12e315c
--- /dev/null
+++ b/docs/guide/hub.md
@@ -0,0 +1,65 @@
+---
+outline: deep
+---
+
+# Hub (multi-tool)
+
+`@devframes/hub` extends devframe with the orchestration features that only make sense when many devtools share a UI: a dock registry, terminal aggregation, message/toast queue, and a command palette. It does not ship UI — each framework kit (e.g. `@vitejs/devtools-kit`) provides its own UI on top of the hub's RPC + shared-state protocol.
+
+> [!WARNING] Experimental
+> The hub API surface is still being refined. Names may change before 1.0.
+
+## What the hub adds
+
+A hub-aware node context (`DevframeHubContext`) extends `DevframeNodeContext` with four subsystems:
+
+| Subsystem | Surface | Purpose |
+|---|---|---|
+| `ctx.docks` | `register / update / values` | Multi-tool dock entries (iframes, launchers, json-render, custom-render). |
+| `ctx.terminals` | `register / startChildProcess` | Aggregate terminal sessions, stream output over a well-known channel. |
+| `ctx.messages` | `add / update / remove / clear` | Server-side toast/notification queue (FIFO, capped at 1000). |
+| `ctx.commands` | `register / execute / list` | Hierarchical command palette with keybindings and `when` clauses. |
+
+Plus a `createJsonRenderer(spec)` factory for building remote-UI panels via the framework-neutral json-render DSL.
+
+## Built-in RPC
+
+Every hub context auto-registers this RPC function so framework kits don't reimplement it:
+
+- `hub:commands:execute` — invoke a registered server command by id. `await rpc.call('hub:commands:execute', 'my-tool:do-thing', ...args)`.
+
+Host-specific capabilities (open in editor, reveal in finder, …) ship as kit-registered RPC functions rather than as part of the hub surface.
+
+## Mounting a devframe into a hub
+
+`mountDevframe(ctx, def)` is the framework-neutral primitive that registers any `DevframeDefinition` as a dock and runs its `setup(ctx)`:
+
+```ts
+import { createHubContext, mountDevframe } from '@devframes/hub/node'
+
+const ctx = await createHubContext({ cwd, host, mode: 'dev' })
+await mountDevframe(ctx, myDevframe)
+```
+
+Framework kits typically wrap this in a plugin shell. `@vitejs/devtools-kit`'s `createPluginFromDevframe` returns a Vite `Plugin` whose `devtools.setup` calls into `mountDevframe`.
+
+## The protocol — what the UI sees
+
+A hub-aware UI doesn't import any hub classes; it reads three shared-state keys and one RPC method:
+
+| Channel | Type | What it carries |
+|---|---|---|
+| `devframe:docks` shared state | `DevframeDockEntry[]` | The full dock list, including the hub's `~terminals` / `~messages` / `~settings` builtins. |
+| `devframe:commands` shared state | `DevframeServerCommandEntry[]` | Serializable command list (handlers stripped). |
+| `devframe:user-settings` shared state | `DevframeDocksUserSettings` | Persisted per-workspace hub settings. |
+| `hub:commands:execute` RPC | `(id, ...args) => unknown` | Server-side command dispatch. |
+
+Plus broadcast notifications (`devframe:terminals:updated`, `devframe:messages:updated`) that a UI can subscribe to via `rpc.client.register(...)`.
+
+## Example
+
+See [`examples/minimal-vite-devframe-hub/`](https://github.com/devframes/devframe/tree/main/examples/minimal-vite-devframe-hub) for a ~120-line Vite plugin that wires the hub end to end with a vanilla DOM UI. Every framework's hub host follows the same shape: a thin layer that adapts the framework's dev server to the hub.
+
+## Diagnostics
+
+Hub-side diagnostic codes live in the `DF8xxx` range. See the [error reference](/errors/) for the full list.
diff --git a/docs/guide/index.md b/docs/guide/index.md
index 1c830e3..871eea0 100644
--- a/docs/guide/index.md
+++ b/docs/guide/index.md
@@ -104,7 +104,7 @@ Devframe has zero dependencies on Vite or any `@vitejs/*` package — the same d
## What's next
-- [Devframe Definition](./devframe-definition) — understand `defineDevframe` and the `DevToolsNodeContext`
+- [Devframe Definition](./devframe-definition) — understand `defineDevframe` and the `DevframeNodeContext`
- [Adapters](/adapters/) — pick the right deployment target for your tool
- [RPC](./rpc) — define type-safe server functions your client can call
- [Agent-Native](./agent-native) — expose your devframe to Claude Desktop, Cursor, or any MCP client
diff --git a/docs/guide/rpc.md b/docs/guide/rpc.md
index 9b23684..a968017 100644
--- a/docs/guide/rpc.md
+++ b/docs/guide/rpc.md
@@ -31,7 +31,7 @@ export const getModules = defineRpcFunction({
returns: v.array(v.object({ id: v.string(), size: v.number() })),
setup: ctx => ({
handler: async ({ limit }) => {
- // `ctx` is the DevToolsNodeContext.
+ // `ctx` is the DevframeNodeContext.
return loadModules().slice(0, limit)
},
}),
@@ -53,7 +53,7 @@ export default defineDevframe({
})
```
-Place each function in its own file under `src/rpc/functions/`, and barrel them in `src/rpc/index.ts` as `const serverFunctions = [...] as const`. The same array feeds the [type-safe client registry](#type-safe-client-registry) and keeps registration order explicit. When per-file functions need to share setup-time state (channels, shared state handles, loaders), expose it through a `WeakMap` in a sibling `src/context.ts`.
+Place each function in its own file under `src/rpc/functions/`, and barrel them in `src/rpc/index.ts` as `const serverFunctions = [...] as const`. The same array feeds the [type-safe client registry](#type-safe-client-registry) and keeps registration order explicit. When per-file functions need to share setup-time state (channels, shared state handles, loaders), expose it through a `WeakMap` in a sibling `src/context.ts`.
### Naming convention
@@ -95,7 +95,7 @@ Prefer a single object argument (`args: [v.object({ ... })]`) over positional ar
Two ways to wire a handler:
-- **`setup(ctx)`** — receives the `DevToolsNodeContext` and returns `{ handler, dump? }`. Use this when you need the context (shared state, logs, `ctx.mode`, etc.).
+- **`setup(ctx)`** — receives the `DevframeNodeContext` and returns `{ handler, dump? }`. Use this when you need the context (shared state, logs, `ctx.mode`, etc.).
- **`handler(...)`** — shorthand when the handler is pure and doesn't touch the context.
```ts
@@ -167,7 +167,7 @@ const modules = await ctx.rpc.invokeLocal('my-devframe:get-modules', { limit: 10
## Client-side calls
-From the browser, [`connectDevframe`](./client) (or `getDevToolsRpcClient`) returns a client for calling registered functions:
+From the browser, [`connectDevframe`](./client) (or `getDevframeRpcClient`) returns a client for calling registered functions:
```ts
import { connectDevframe } from 'devframe/client'
@@ -181,7 +181,7 @@ Client-side registration (for server→client calls) goes through `rpc.client.re
## Type-safe client registry
-Devframe exposes two augmentable interfaces — `DevToolsRpcServerFunctions` (client→server calls) and `DevToolsRpcClientFunctions` (server→client calls) — so each registered RPC name shows up on the typed client. Augment them once per devframe via `declare module 'devframe'`.
+Devframe exposes two augmentable interfaces — `DevframeRpcServerFunctions` (client→server calls) and `DevframeRpcClientFunctions` (server→client calls) — so each registered RPC name shows up on the typed client. Augment them once per devframe via `declare module 'devframe'`.
The recommended pattern collects every server-side definition into a const array and feeds it through `RpcDefinitionsToFunctions`:
@@ -192,7 +192,7 @@ import { getFile, getModules } from './rpc'
const serverFunctions = [getModules, getFile] as const
declare module 'devframe' {
- interface DevToolsRpcServerFunctions
+ interface DevframeRpcServerFunctions
extends RpcDefinitionsToFunctions {}
}
```
@@ -213,13 +213,13 @@ For one-off augmentations, declare a single key with `RpcFunctionDefinitionToFun
import type { RpcFunctionDefinitionToFunction } from 'devframe/rpc'
declare module 'devframe' {
- interface DevToolsRpcServerFunctions {
+ interface DevframeRpcServerFunctions {
'my-devframe:get-modules': RpcFunctionDefinitionToFunction
}
}
```
-For server→client calls invoked via `ctx.rpc.broadcast`, augment `DevToolsRpcClientFunctions` the same way.
+For server→client calls invoked via `ctx.rpc.broadcast`, augment `DevframeRpcClientFunctions` the same way.
## Static dumps
@@ -264,7 +264,7 @@ Devframe's WS transport ships payloads using one of two encoders, picked per RPC
| `false` (default) | `structured-clone-es` | `s:` | `Map`, `Set`, `Date`, `BigInt`, cycles, class instances |
| `true` (opt-in) | strict `JSON.stringify` | _(unprefixed)_ | JSON-only |
-The wire stays plain JSON when every participating function is JSON-flagged — debuggable in DevTools, friendly to MCP, and a good default for tools that already speak JSON.
+The wire stays plain JSON when every participating function is JSON-flagged — debuggable in Devframe, friendly to MCP, and a good default for tools that already speak JSON.
### Discovering shape errors during dev
diff --git a/docs/guide/shared-state.md b/docs/guide/shared-state.md
index ed01a75..074d9f2 100644
--- a/docs/guide/shared-state.md
+++ b/docs/guide/shared-state.md
@@ -139,11 +139,11 @@ Protocol adapters (the [MCP adapter](./agent-native), for example) use this to s
## Type-safe keys
-Augment `DevToolsRpcSharedStates` to type each shared-state key once, then both server and client lookups stay typed without per-call generics:
+Augment `DevframeRpcSharedStates` to type each shared-state key once, then both server and client lookups stay typed without per-call generics:
```ts
declare module 'devframe' {
- interface DevToolsRpcSharedStates {
+ interface DevframeRpcSharedStates {
'my-devframe:state': {
count: number
items: { id: string, name: string }[]
diff --git a/docs/helpers/nuxt.md b/docs/helpers/nuxt.md
index fb484d8..e70da89 100644
--- a/docs/helpers/nuxt.md
+++ b/docs/helpers/nuxt.md
@@ -11,7 +11,7 @@ It handles the four things every Nuxt-powered standalone devtool needs:
1. **Base-agnostic assets.** Sets `app.baseURL: './'` and `vite.base: './'` so the same production build works at `/`, `/tool/`, and any other deployment path without build-time URL rewriting.
2. **Runtime RPC connection.** Adds a client plugin that calls [`connectDevframe()`](/guide/client) once on page load and provides the result as `$rpc` on the Nuxt app.
3. **Dev-time RPC bridge.** When you pass `devframe`, `nuxt dev` spins up a separate WebSocket RPC server and serves `__connection.json` so the SPA can reach it — no hand-rolled Vite plugin required.
-4. **TypeScript augmentation.** `useNuxtApp().$rpc` is typed as `DevToolsRpcClient` out of the box.
+4. **TypeScript augmentation.** `useNuxtApp().$rpc` is typed as `DevframeRpcClient` out of the box.
## Install
diff --git a/docs/index.md b/docs/index.md
index cf5494f..c5d9e19 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -3,7 +3,7 @@ layout: home
hero:
name: Devframe
- text: Framework-neutral foundation for DevTools
+ text: Framework-neutral foundation for Devframe
tagline: One devframe definition, adapters to different environments. Managed communication layer, agent-native.
image:
src: /logo.svg
diff --git a/examples/files-inspector/src/client/app.tsx b/examples/files-inspector/src/client/app.tsx
index 59cfcd4..e2d8afa 100644
--- a/examples/files-inspector/src/client/app.tsx
+++ b/examples/files-inspector/src/client/app.tsx
@@ -1,4 +1,4 @@
-import type { DevToolsRpcClient } from 'devframe/client'
+import type { DevframeRpcClient } from 'devframe/client'
import { connectDevframe } from 'devframe/client'
import { useEffect, useState } from 'preact/hooks'
import { About } from './routes/about'
@@ -19,7 +19,7 @@ function getRoute(basePath: string): string {
export function App() {
const basePath = getBasePath()
const [route, setRoute] = useState(getRoute(basePath))
- const [rpc, setRpc] = useState(null)
+ const [rpc, setRpc] = useState(null)
useEffect(() => {
let cancelled = false
diff --git a/examples/files-inspector/src/client/routes/about.tsx b/examples/files-inspector/src/client/routes/about.tsx
index 1300666..f63d76f 100644
--- a/examples/files-inspector/src/client/routes/about.tsx
+++ b/examples/files-inspector/src/client/routes/about.tsx
@@ -1,7 +1,7 @@
-import type { DevToolsRpcClient } from 'devframe/client'
+import type { DevframeRpcClient } from 'devframe/client'
import { useEffect, useState } from 'preact/hooks'
-export function About({ rpc, basePath }: { rpc: DevToolsRpcClient, basePath: string }) {
+export function About({ rpc, basePath }: { rpc: DevframeRpcClient, basePath: string }) {
const [cwd, setCwd] = useState('')
useEffect(() => {
diff --git a/examples/files-inspector/src/client/routes/home.tsx b/examples/files-inspector/src/client/routes/home.tsx
index ca6dfd9..2f4fad1 100644
--- a/examples/files-inspector/src/client/routes/home.tsx
+++ b/examples/files-inspector/src/client/routes/home.tsx
@@ -1,7 +1,7 @@
-import type { DevToolsRpcClient } from 'devframe/client'
+import type { DevframeRpcClient } from 'devframe/client'
import { useEffect, useState } from 'preact/hooks'
-export function Home({ rpc }: { rpc: DevToolsRpcClient }) {
+export function Home({ rpc }: { rpc: DevframeRpcClient }) {
const [files, setFiles] = useState([])
const [loading, setLoading] = useState(true)
diff --git a/examples/files-inspector/src/rpc/index.ts b/examples/files-inspector/src/rpc/index.ts
index 181cb1b..e85d7d3 100644
--- a/examples/files-inspector/src/rpc/index.ts
+++ b/examples/files-inspector/src/rpc/index.ts
@@ -5,5 +5,5 @@ import { listFiles } from './functions/list-files.ts'
export const serverFunctions = [getCwd, listFiles] as const
declare module 'devframe' {
- interface DevToolsRpcServerFunctions extends RpcDefinitionsToFunctions {}
+ interface DevframeRpcServerFunctions extends RpcDefinitionsToFunctions {}
}
diff --git a/examples/files-inspector/tests/_utils.ts b/examples/files-inspector/tests/_utils.ts
index 4e1f170..269a609 100644
--- a/examples/files-inspector/tests/_utils.ts
+++ b/examples/files-inspector/tests/_utils.ts
@@ -5,10 +5,10 @@ import os from 'node:os'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import {
- DEVTOOLS_CONNECTION_META_FILENAME,
+ DEVFRAME_CONNECTION_META_FILENAME,
} from 'devframe/constants'
import {
- createH3DevToolsHost,
+ createH3DevframeHost,
createHostContext,
startHttpAndWs,
} from 'devframe/node'
@@ -68,7 +68,7 @@ export async function startInspectorServer(
const app = new H3()
const origin = `http://${host}:${port}`
- const h3Host = createH3DevToolsHost({
+ const h3Host = createH3DevframeHost({
origin,
appName: devframe.id,
mount: (base, dir) => {
@@ -79,7 +79,7 @@ export async function startInspectorServer(
const ctx = await createHostContext({ cwd, mode: 'dev', host: h3Host })
await devframe.setup(ctx)
- const metaPath = `${basePath}${DEVTOOLS_CONNECTION_META_FILENAME}`
+ const metaPath = `${basePath}${DEVFRAME_CONNECTION_META_FILENAME}`
app.use(metaPath, () => ({ backend: 'websocket', websocket: port }))
mountStaticHandler(app, basePath, resolve(distDir))
diff --git a/examples/files-inspector/tests/static-build.test.ts b/examples/files-inspector/tests/static-build.test.ts
index 4179485..97302ca 100644
--- a/examples/files-inspector/tests/static-build.test.ts
+++ b/examples/files-inspector/tests/static-build.test.ts
@@ -4,8 +4,8 @@ import os from 'node:os'
import path from 'node:path'
import { createBuild } from 'devframe/adapters/build'
import {
- DEVTOOLS_CONNECTION_META_FILENAME,
- DEVTOOLS_RPC_DUMP_MANIFEST_FILENAME,
+ DEVFRAME_CONNECTION_META_FILENAME,
+ DEVFRAME_RPC_DUMP_MANIFEST_FILENAME,
} from 'devframe/constants'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import devframe from '../src/devframe'
@@ -53,24 +53,24 @@ describe('static build (CLI build surface)', () => {
})
it('writes a static-backend connection meta next to index.html', async () => {
- // The meta sits at the SPA root (not under `__devtools/`) so any
+ // The meta sits at the SPA root (not under `__devframe/`) so any
// generic static file server (`serve`, `nginx`, `python -m http.server`)
// can serve it as a flat tree without nested-dir exclusions.
const meta = JSON.parse(
await readFile(
- path.join(outBuild, DEVTOOLS_CONNECTION_META_FILENAME),
+ path.join(outBuild, DEVFRAME_CONNECTION_META_FILENAME),
'utf-8',
),
) as { backend: string }
expect(meta).toMatchObject({ backend: 'static' })
- // Guard the design: nothing should land under a `__devtools/` subdir.
- expect(existsSync(path.join(outBuild, '__devtools'))).toBe(false)
+ // Guard the design: nothing should land under a `__devframe/` subdir.
+ expect(existsSync(path.join(outBuild, '__devframe'))).toBe(false)
})
it('dumps both RPC functions into the manifest', async () => {
const manifest = JSON.parse(
await readFile(
- path.join(outBuild, DEVTOOLS_RPC_DUMP_MANIFEST_FILENAME),
+ path.join(outBuild, DEVFRAME_RPC_DUMP_MANIFEST_FILENAME),
'utf-8',
),
) as DumpManifest
diff --git a/examples/files-inspector/tests/static-serve.test.ts b/examples/files-inspector/tests/static-serve.test.ts
index 143fb83..c35da41 100644
--- a/examples/files-inspector/tests/static-serve.test.ts
+++ b/examples/files-inspector/tests/static-serve.test.ts
@@ -88,8 +88,8 @@ describe('static serve (deployed SPA contract)', () => {
// This is the path the SPA fetches via relative `./__connection.json`
// resolved against `document.baseURI`. The 404 the user originally
// reported with `serve dist-static` was caused by the old layout
- // putting this file under `/__devtools/__connection.json`, which a
- // SPA at any non-`/__devtools/` mount could not discover.
+ // putting this file under `/__devframe/__connection.json`, which a
+ // SPA at any non-`/__devframe/` mount could not discover.
const res = await fetch(`${server.origin}${mountBase}__connection.json`)
expect(res.status).toBe(200)
const meta = await res.json() as { backend: string }
@@ -120,11 +120,11 @@ describe('static serve (deployed SPA contract)', () => {
expect(record.output).toEqual(['README.md', 'package.json', 'sample.txt'])
})
- it('does not expose a stray `__devtools/` directory at the SPA root', async () => {
+ it('does not expose a stray `__devframe/` directory at the SPA root', async () => {
// Regression guard: the build output is intentionally flat —
- // re-introducing a `__devtools/` subdir would create a nested
+ // re-introducing a `__devframe/` subdir would create a nested
// path the relative-base discovery in the SPA cannot reach.
- const res = await fetch(`${server.origin}${mountBase}__devtools/__connection.json`)
+ const res = await fetch(`${server.origin}${mountBase}__devframe/__connection.json`)
expect(res.status).toBe(404)
})
})
diff --git a/examples/minimal-next-devframe-hub/.gitignore b/examples/minimal-next-devframe-hub/.gitignore
new file mode 100644
index 0000000..999e272
--- /dev/null
+++ b/examples/minimal-next-devframe-hub/.gitignore
@@ -0,0 +1,6 @@
+.next
+dist
+next-env.d.ts
+node_modules
+out
+.turbo
diff --git a/examples/minimal-next-devframe-hub/README.md b/examples/minimal-next-devframe-hub/README.md
new file mode 100644
index 0000000..bd89a0e
--- /dev/null
+++ b/examples/minimal-next-devframe-hub/README.md
@@ -0,0 +1,36 @@
+# Minimal Next Devframe Hub
+
+A protocol-witness example. The `src/client/devframe/minimal-next-devframe-hub.ts` file wires `@devframes/hub` into a Next.js App Router app by lazily starting a side-car RPC/WS server from a Node route handler.
+
+## Run it
+
+```sh
+pnpm install
+pnpm --filter minimal-next-devframe-hub dev
+```
+
+Open the printed URL. You should see:
+
+- A status line showing the RPC backend
+- A **Docks** list with hub built-ins and the mounted demo devframe
+- A **Commands** list populated from server-side registrations
+- A **Messages** list populated via `messages.add()` on the server
+- A **Terminals** list, empty unless a devframe registers one
+- A button that exercises `hub:commands:execute` by dispatching the sample ping command
+
+## What the example proves
+
+- `createHubContext()` boots a hub without any Vite-specific code path
+- A `DevframeHost` impl plugs Next host specifics into the hub uniformly
+- `mountDevframe(ctx, def)` registers any `DevframeDefinition` as a dock
+- The built-in `hub:commands:execute` RPC dispatches any registered server command, regardless of how the host was constructed
+- The browser-side `connectDevframe({ baseURL: '/__hub/' })` discovers the WS endpoint via the Next route handler at `/__hub/__connection.json`
+
+## Files
+
+| File | Role |
+|---|---|
+| `src/client/devframe/minimal-next-devframe-hub.ts` | The Next host — creates hub context and side-car WS |
+| `src/client/app/%5F_hub/%5F_connection.json/route.ts` | Connection-meta endpoint for `/__hub/__connection.json` that starts the singleton host |
+| `src/client/devframe/demo-devframe.ts` | A sample `DevframeDefinition` that plugs into the host |
+| `src/client/app/page.tsx` | The browser-side UI that consumes the hub protocol |
diff --git a/examples/minimal-next-devframe-hub/package.json b/examples/minimal-next-devframe-hub/package.json
new file mode 100644
index 0000000..f2ef7e8
--- /dev/null
+++ b/examples/minimal-next-devframe-hub/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "minimal-next-devframe-hub",
+ "type": "module",
+ "version": "0.4.1",
+ "private": true,
+ "description": "Protocol-witness example — a tiny Next.js Devframe Hub built on @devframes/hub that exercises every hub subsystem end-to-end.",
+ "scripts": {
+ "dev": "next dev src/client",
+ "build": "next build src/client",
+ "test": "vitest run --config vitest.config.ts"
+ },
+ "dependencies": {
+ "@devframes/hub": "workspace:*",
+ "devframe": "workspace:*",
+ "next": "catalog:frontend",
+ "react": "catalog:frontend",
+ "react-dom": "catalog:frontend"
+ },
+ "devDependencies": {
+ "@types/react": "catalog:types",
+ "@types/react-dom": "catalog:types",
+ "get-port-please": "catalog:deps",
+ "pathe": "catalog:deps",
+ "vitest": "catalog:testing",
+ "ws": "catalog:deps"
+ }
+}
diff --git a/examples/minimal-next-devframe-hub/src/client/app/%5F_hub/%5F_connection.json/route.ts b/examples/minimal-next-devframe-hub/src/client/app/%5F_hub/%5F_connection.json/route.ts
new file mode 100644
index 0000000..96724f7
--- /dev/null
+++ b/examples/minimal-next-devframe-hub/src/client/app/%5F_hub/%5F_connection.json/route.ts
@@ -0,0 +1,9 @@
+import { ensureMinimalNextDevframeHub } from '../../../devframe/minimal-next-devframe-hub'
+
+export const runtime = 'nodejs'
+export const dynamic = 'force-dynamic'
+
+export async function GET() {
+ const hub = await ensureMinimalNextDevframeHub()
+ return Response.json(hub.connectionMeta)
+}
diff --git a/examples/minimal-next-devframe-hub/src/client/app/globals.css b/examples/minimal-next-devframe-hub/src/client/app/globals.css
new file mode 100644
index 0000000..7bc5dff
--- /dev/null
+++ b/examples/minimal-next-devframe-hub/src/client/app/globals.css
@@ -0,0 +1,100 @@
+:root {
+ color-scheme: light dark;
+ font-family: system-ui, sans-serif;
+ line-height: 1.5;
+}
+
+body {
+ margin: 0;
+ padding: 1.5rem 2rem;
+}
+
+main {
+ max-width: 800px;
+ margin-inline: auto;
+}
+
+header h1 {
+ margin-bottom: 0.25rem;
+}
+
+header p {
+ margin-top: 0;
+ opacity: 0.7;
+}
+
+section {
+ margin-block: 1.5rem;
+}
+
+h2 {
+ font-size: 1rem;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ opacity: 0.8;
+ border-bottom: 1px solid currentcolor;
+ padding-bottom: 0.25rem;
+}
+
+ul {
+ list-style: none;
+ padding-left: 0;
+}
+
+li {
+ padding: 0.5rem 0.75rem;
+ border: 1px solid color-mix(in srgb, currentcolor 15%, transparent);
+ border-radius: 0.5rem;
+ margin-bottom: 0.5rem;
+ font-family: ui-monospace, monospace;
+ font-size: 0.9rem;
+}
+
+li.muted {
+ opacity: 0.5;
+ font-style: italic;
+}
+
+code {
+ font-family: ui-monospace, monospace;
+ background: color-mix(in srgb, currentcolor 10%, transparent);
+ padding: 0.1em 0.35em;
+ border-radius: 0.25em;
+}
+
+button {
+ font: inherit;
+ padding: 0.5rem 1rem;
+ border-radius: 0.5rem;
+ border: 1px solid currentcolor;
+ background: transparent;
+ cursor: pointer;
+}
+
+button:hover {
+ background: color-mix(in srgb, currentcolor 10%, transparent);
+}
+
+.actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.75rem;
+}
+
+#status {
+ font-family: ui-monospace, monospace;
+ font-size: 0.85rem;
+ opacity: 0.7;
+}
+
+#status.error span {
+ color: #c33;
+}
+
+#status.ready span {
+ color: #2a7;
+}
+
+.badge {
+ margin-left: 0.35rem;
+}
diff --git a/examples/minimal-next-devframe-hub/src/client/app/layout.tsx b/examples/minimal-next-devframe-hub/src/client/app/layout.tsx
new file mode 100644
index 0000000..5090a20
--- /dev/null
+++ b/examples/minimal-next-devframe-hub/src/client/app/layout.tsx
@@ -0,0 +1,16 @@
+import type { Metadata } from 'next'
+import type { ReactNode } from 'react'
+import './globals.css'
+
+export const metadata: Metadata = {
+ title: 'Minimal Next Devframe Hub',
+ description: 'A Next.js host for the @devframes/hub protocol.',
+}
+
+export default function RootLayout({ children }: { children: ReactNode }) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/examples/minimal-next-devframe-hub/src/client/app/page.tsx b/examples/minimal-next-devframe-hub/src/client/app/page.tsx
new file mode 100644
index 0000000..65e3282
--- /dev/null
+++ b/examples/minimal-next-devframe-hub/src/client/app/page.tsx
@@ -0,0 +1,200 @@
+'use client'
+
+import type { DevframeRpcClient } from '@devframes/hub/client'
+import type {
+ DevframeCommandEntry,
+ DevframeDockEntry,
+ DevframeMessageEntry,
+ DevframeTerminalSession,
+} from '@devframes/hub/types'
+import type { ReactNode } from 'react'
+import { connectDevframe } from '@devframes/hub/client'
+import { useEffect, useRef, useState } from 'react'
+
+const HUB_BASE = '/__hub/'
+
+interface Status {
+ text: string
+ kind?: 'ready' | 'error'
+}
+
+type TerminalSummary = Pick
+
+export default function Page() {
+ const [status, setStatus] = useState({ text: 'Connecting...' })
+ const [docks, setDocks] = useState([])
+ const [commands, setCommands] = useState([])
+ const [messages, setMessages] = useState([])
+ const [terminals, setTerminals] = useState([])
+ const [pingResult, setPingResult] = useState('Run ping')
+ const rpcRef = useRef(null)
+
+ useEffect(() => {
+ let cancelled = false
+ let cleanup: (() => void) | undefined
+
+ async function run() {
+ try {
+ const rpc = await connectDevframe({ baseURL: HUB_BASE })
+ if (cancelled)
+ return
+
+ rpcRef.current = rpc
+ setStatus({ text: `Connected: backend=${rpc.connectionMeta.backend}`, kind: 'ready' })
+
+ const docksState = await rpc.sharedState.get(
+ 'devframe:docks',
+ { initialValue: [] },
+ )
+ const commandsState = await rpc.sharedState.get(
+ 'devframe:commands',
+ { initialValue: [] },
+ )
+
+ const renderDocks = () => setDocks(docksState.value() ?? [])
+ const renderCommands = () => setCommands(commandsState.value() ?? [])
+ docksState.on('updated', renderDocks)
+ commandsState.on('updated', renderCommands)
+ renderDocks()
+ renderCommands()
+
+ const refreshMessages = async () => {
+ const entries = await rpc.call(
+ 'minimal-next-devframe-hub:messages:list' as any,
+ ) as DevframeMessageEntry[]
+ if (!cancelled)
+ setMessages(entries)
+ }
+
+ const refreshTerminals = async () => {
+ const sessions = await rpc.call(
+ 'minimal-next-devframe-hub:terminals:list' as any,
+ ) as TerminalSummary[]
+ if (!cancelled)
+ setTerminals(sessions)
+ }
+
+ await refreshMessages()
+ await refreshTerminals()
+
+ const interval = window.setInterval(() => {
+ void refreshMessages()
+ void refreshTerminals()
+ }, 2000)
+
+ cleanup = () => window.clearInterval(interval)
+ }
+ catch (err) {
+ if (!cancelled)
+ setStatus({ text: `Failed: ${(err as Error).message}`, kind: 'error' })
+ }
+ }
+
+ void run()
+
+ return () => {
+ cancelled = true
+ cleanup?.()
+ rpcRef.current = null
+ }
+ }, [])
+
+ async function ping() {
+ if (!rpcRef.current)
+ return
+ try {
+ const result = await rpcRef.current.call(
+ 'hub:commands:execute' as any,
+ 'minimal-next-devframe-hub:ping',
+ )
+ setPingResult(`Ping returned ${JSON.stringify(result)}`)
+ }
+ catch (err) {
+ setPingResult(`Error: ${(err as Error).message}`)
+ }
+ }
+
+ return (
+
+
+
Minimal Next Devframe Hub
+
+ Protocol witness: verifies
+ {' '}
+ @devframes/hub
+ {' '}
+ end to end from a Next.js host.
+