devframe 0.9.4 → 0.9.6
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.
- package/README.md +1 -1
- package/dist/{_shared-BPovvJW8.d.mts → _shared-BJ6AmwTu.d.mts} +1 -1
- package/dist/adapters/build.d.mts +5 -4
- package/dist/adapters/build.mjs +4 -3
- package/dist/adapters/cac.d.mts +1 -1
- package/dist/adapters/cac.mjs +3 -2
- package/dist/adapters/dev.d.mts +12 -11
- package/dist/adapters/dev.mjs +1 -1
- package/dist/adapters/embedded.d.mts +1 -1
- package/dist/adapters/initiate.d.mts +9 -8
- package/dist/adapters/initiate.mjs +1 -1
- package/dist/adapters/mcp.d.mts +30 -26
- package/dist/adapters/mcp.mjs +559 -1
- package/dist/cli/main.mjs +1 -1
- package/dist/client/index.d.mts +5 -5
- package/dist/client/index.mjs +1 -64
- package/dist/constants.d.mts +13 -1
- package/dist/constants.mjs +1 -1
- package/dist/{context-_i51nYOs.d.mts → context-BhBzOXVa.d.mts} +23 -11
- package/dist/{context-CO7eZHJy.d.mts → context-HOnG-WvG.d.mts} +1 -1
- package/dist/{context-BUJGvlje.mjs → context-KUqAfawQ.mjs} +1 -1
- package/dist/{context-CQW0lSw8.mjs → context-omlma_1n.mjs} +3 -150
- package/dist/{dev-lpIvwagf.mjs → dev-Czit5IP4.mjs} +6 -5
- package/dist/{events-DKfSmoTj.mjs → events-4daw0uK9.mjs} +16 -2
- package/dist/in-page-channel/index.d.mts +306 -0
- package/dist/in-page-channel/index.mjs +816 -0
- package/dist/{index-DOg4S5wk.d.mts → index-BzyopqxV.d.mts} +1 -1
- package/dist/{index-CEvd-YXD.d.mts → index-DcKxIMoI.d.mts} +2 -2
- package/dist/index.d.mts +14 -6
- package/dist/index.mjs +11 -1
- package/dist/{instance-shell-BHvBIGP4.d.mts → instance-shell-Bh4l9Vz_.d.mts} +3 -3
- package/dist/{instance-shell-B-uevnK-.mjs → instance-shell-CpVW4zG0.mjs} +15 -2
- package/dist/internal/index.d.mts +17 -7
- package/dist/internal/index.mjs +3 -3
- package/dist/node/auth.d.mts +2 -2
- package/dist/node/auth.mjs +1 -1
- package/dist/node/hub-internals.d.mts +2 -2
- package/dist/node/hub-internals.mjs +1 -1
- package/dist/node/index.d.mts +3 -3
- package/dist/node/index.mjs +1 -1
- package/dist/recipes/common-rpc-functions.d.mts +3 -3
- package/dist/recipes/interactive-auth.d.mts +13 -2
- package/dist/recipes/interactive-auth.mjs +5 -1
- package/dist/{remote-assets-Bg4gCUZ_.d.mts → remote-assets-B13_vRkJ.d.mts} +1 -1
- package/dist/{revoke-3Q6aDKWk.mjs → revoke-COLW0cOi.mjs} +1 -1
- package/dist/rpc/dump.d.mts +1 -1
- package/dist/rpc/index.d.mts +2 -2
- package/dist/rpc/transports/sse-client.d.mts +1 -1
- package/dist/rpc/transports/sse-server.d.mts +2 -2
- package/dist/rpc/transports/ws-bun.d.mts +2 -2
- package/dist/rpc/transports/ws-client.d.mts +1 -1
- package/dist/rpc/transports/ws-deno.d.mts +2 -2
- package/dist/rpc/transports/ws-server.d.mts +1 -1
- package/dist/{rpc-core-CDZoGYiD.d.mts → rpc-core-BHUsDl7s.d.mts} +3 -3
- package/dist/{host-agent-B84av916.mjs → services-install-CqqQVls5.mjs} +150 -2
- package/dist/{sse-client-CdGD_B8_.d.mts → sse-client-DwX021ST.d.mts} +1 -1
- package/dist/types/index.d.mts +4 -4
- package/dist/{types-BBLeGfpt.d.mts → types-CMdPYhOx.d.mts} +1 -1
- package/dist/utils/events.d.mts +1 -1
- package/dist/utils/remote-assets.d.mts +1 -1
- package/dist/utils/serve-static.d.mts +1 -1
- package/dist/utils/serve-static.mjs +1 -1
- package/dist/utils/shared-state.d.mts +1 -1
- package/dist/utils/streaming-channel.d.mts +1 -1
- package/dist/{ws-client-Bo2t6hES.d.mts → ws-client-7_lZi-B9.d.mts} +1 -1
- package/dist/{ws-server-DLtEoLdR.d.mts → ws-server-Di2oqr4q.d.mts} +1 -1
- package/package.json +2 -1
- package/skills/devframe/SKILL.md +88 -84
- package/skills/devframe/templates/hub.ts +1 -1
- package/dist/http-BKiqHcXX.mjs +0 -569
- package/dist/transports-vhizgqXM.mjs +0 -15
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as DEVFRAME_EVENTS } from "../events-
|
|
1
|
+
import { t as DEVFRAME_EVENTS } from "../events-4daw0uK9.mjs";
|
|
2
2
|
import { extname, join, normalize, resolve, sep } from "pathe";
|
|
3
3
|
import { createReadStream } from "node:fs";
|
|
4
4
|
import { stat } from "node:fs/promises";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { A as SharedStateEvents, D as ImmutableObject, E as ImmutableMap, M as SharedStatePatch, N as createSharedState, O as ImmutableSet, T as ImmutableArray, j as SharedStateOptions, k as SharedState, w as Immutable } from "../context-
|
|
1
|
+
import { A as SharedStateEvents, D as ImmutableObject, E as ImmutableMap, M as SharedStatePatch, N as createSharedState, O as ImmutableSet, T as ImmutableArray, j as SharedStateOptions, k as SharedState, w as Immutable } from "../context-BhBzOXVa.mjs";
|
|
2
2
|
export { Immutable, ImmutableArray, ImmutableMap, ImmutableObject, ImmutableSet, SharedState, SharedStateEvents, SharedStateOptions, SharedStatePatch, createSharedState };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { C as createStreamSink, S as createStreamReader, _ as CreateStreamSinkOptions, b as StreamSink, g as CreateStreamReaderOptions, h as BufferedChunk, v as StreamErrorPayload, x as StreamSinkEvents, y as StreamReader } from "../context-
|
|
1
|
+
import { C as createStreamSink, S as createStreamReader, _ as CreateStreamSinkOptions, b as StreamSink, g as CreateStreamReaderOptions, h as BufferedChunk, v as StreamErrorPayload, x as StreamSinkEvents, y as StreamReader } from "../context-BhBzOXVa.mjs";
|
|
2
2
|
export { BufferedChunk, CreateStreamReaderOptions, CreateStreamSinkOptions, StreamErrorPayload, StreamReader, StreamSink, StreamSinkEvents, createStreamReader, createStreamSink };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { v as RpcFunctionDefinitionAny } from "./types-
|
|
1
|
+
import { v as RpcFunctionDefinitionAny } from "./types-CMdPYhOx.mjs";
|
|
2
2
|
import { BirpcGroup, ChannelOptions } from "birpc";
|
|
3
3
|
import { Hooks, Peer } from "crossws";
|
|
4
4
|
import { NodeAdapter } from "crossws/adapters/node";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devframe",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.9.
|
|
4
|
+
"version": "0.9.6",
|
|
5
5
|
"description": "Framework for building one portable devtool integration that runs in any viewer.",
|
|
6
6
|
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"./adapters/mcp": "./dist/adapters/mcp.mjs",
|
|
28
28
|
"./client": "./dist/client/index.mjs",
|
|
29
29
|
"./constants": "./dist/constants.mjs",
|
|
30
|
+
"./in-page-channel": "./dist/in-page-channel/index.mjs",
|
|
30
31
|
"./initiate": "./dist/adapters/initiate.mjs",
|
|
31
32
|
"./internal": "./dist/internal/index.mjs",
|
|
32
33
|
"./node": "./dist/node/index.mjs",
|
package/skills/devframe/SKILL.md
CHANGED
|
@@ -5,13 +5,13 @@ description: >
|
|
|
5
5
|
build-tool-agnostic foundation for defining a devtool once and
|
|
6
6
|
serving it anywhere. Covers the DevframeDefinition, the standard
|
|
7
7
|
web handler (`initDevframe` → `handler` / `nodeMiddleware`) that
|
|
8
|
-
mounts a tool into any host, the packaging adapters (cli /
|
|
9
|
-
dev / mcp / embedded), the framework
|
|
8
|
+
mounts a tool into any host framework, the packaging adapters (cli /
|
|
9
|
+
build / dev / mcp / embedded), the framework kits (`@devframes/vite`,
|
|
10
10
|
`@devframes/next`, `@devframes/nuxt`, each split into `single` and
|
|
11
|
-
`hub`), composing many tools into one
|
|
11
|
+
`hub`), composing many tools into one hub with
|
|
12
12
|
`@devframes/hub` (`initHub`, docks / commands / messages /
|
|
13
13
|
terminals), designing RPC contracts, exposing an agent-native
|
|
14
|
-
|
|
14
|
+
API over MCP, and wiring the author's SPA. Triggers on
|
|
15
15
|
`devframe` imports, `defineDevframe`, `initDevframe`, `initHub`,
|
|
16
16
|
`createCac`, `createMcpServer`, `connectDevframe`, `@devframes/*`
|
|
17
17
|
imports, and on migrations of existing inspectors
|
|
@@ -21,14 +21,14 @@ description: >
|
|
|
21
21
|
|
|
22
22
|
# devframe skill
|
|
23
23
|
|
|
24
|
-
**Devframe is the `unplugin` for devtools: define a tool once, mount it anywhere.** A devtool built on devframe is a single `DevframeDefinition` plus an author-provided SPA. That definition describes one tool - its RPC
|
|
24
|
+
**Devframe is the `unplugin` for devtools: define a tool once, mount it anywhere.** A devtool built on devframe is a single `DevframeDefinition` plus an author-provided SPA. That definition describes one tool - its RPC, shared state, diagnostics, web interface, and agent-facing API - independent of how it is presented. The same definition then deploys through a standard web handler, a set of packaging adapters, thin framework kits, or composed with other tools inside a hub.
|
|
25
25
|
|
|
26
26
|
Two layers, one boundary:
|
|
27
27
|
|
|
28
|
-
- **A devframe** is one portable tool. `initDevframe(def, { base })` turns it into a live instance whose `.handler` is a Web-Standard `(request: Request) => Promise<Response>` carrying the whole
|
|
28
|
+
- **A devframe** is one portable tool. `initDevframe(def, { base })` turns it into a live instance whose `.handler` is a Web-Standard `(request: Request) => Promise<Response>` carrying the whole devframe (SPA, discovery, WebSocket RPC, auth gate, optional MCP route) under one mount base. Anything that can mount a catch-all route or Connect-style middleware can serve it.
|
|
29
29
|
- **A hub** (`@devframes/hub`) composes *many* devframes behind one namespace with a shared RPC registry, one transport, one auth gate, and the orchestration features that only make sense when tools share a UI (docks, commands, messages, terminals). `initHub()` puts the whole collection behind the same kind of standard handler.
|
|
30
30
|
|
|
31
|
-
Devframe is framework- and build-tool-agnostic - it has zero dependency on Vite or any `@vitejs/*` package and makes no UI-framework assumption. [Vite DevTools](https://devtools.vite.dev/) is the first flagship
|
|
31
|
+
Devframe is framework- and build-tool-agnostic - it has zero dependency on Vite or any `@vitejs/*` package and makes no UI-framework assumption. [Vite DevTools](https://devtools.vite.dev/) is the first flagship hub UI provider built on it; the built-in devframes deliberately span Vue, Svelte, Solid, React, and Next to prove the point.
|
|
32
32
|
|
|
33
33
|
High-level concept: [Pluggable, Extensible, and Playful DevTools](https://antfu.me/posts/pluggable-extensible-playful-devtools). Full reference: [devfra.me](https://devfra.me/).
|
|
34
34
|
|
|
@@ -40,24 +40,24 @@ The same `DevframeDefinition` runs under every one of these. Choose based on whe
|
|
|
40
40
|
|
|
41
41
|
| Goal | Entry | Import |
|
|
42
42
|
|------|-------|--------|
|
|
43
|
-
| Mount into any host (the portability primitive) | `initDevframe(def, { base })` → `.handler` / `.nodeMiddleware` | `devframe/initiate` |
|
|
43
|
+
| Mount into any host framework (the portability primitive) | `initDevframe(def, { base })` → `.handler` / `.nodeMiddleware` | `devframe/initiate` |
|
|
44
44
|
| Standalone CLI (dev / build / mcp subcommands) | `createCac(def, opts?).parse()` | `devframe/adapters/cac` |
|
|
45
45
|
| Programmatic dev server | `createDevServer(def, opts?)` | `devframe/adapters/dev` |
|
|
46
46
|
| Self-contained static deploy with baked data | `createBuild(def, opts?)` | `devframe/adapters/build` |
|
|
47
47
|
| MCP server for coding agents | `createMcpServer(def, opts?)` | `devframe/adapters/mcp` |
|
|
48
|
-
| Runtime registration into an existing
|
|
48
|
+
| Runtime registration into an existing context | `createEmbedded(def, { ctx })` | `devframe/adapters/embedded` |
|
|
49
49
|
| Ride along a Vite dev server (no dock) | `devframeVitePlugin` / `devframeViteBridge` / `devframeVite` | `@devframes/vite/single` |
|
|
50
50
|
| Author one devframe's SPA with Next | `withDevframe` + `createDevframeNextHandler` | `@devframes/next/single` |
|
|
51
51
|
| Author one devframe's SPA with Nuxt | Nuxt module | `@devframes/nuxt/single` |
|
|
52
52
|
| Mount into the Vite DevTools dock | `createPluginFromDevframe(def, opts?)` | `@vitejs/devtools-kit/node` |
|
|
53
53
|
|
|
54
|
-
**Serve many devframes as a
|
|
54
|
+
**Serve many devframes as a hub:**
|
|
55
55
|
|
|
56
56
|
| Goal | Entry | Import |
|
|
57
57
|
|------|-------|--------|
|
|
58
58
|
| Compose a hub behind one handler | `initHub({ base, devframes, ui })` | `@devframes/hub/initiate` |
|
|
59
59
|
| Imperatively mount into a hub context | `createHubContext(...)` → `ctx.install(def)` | `@devframes/hub/node` |
|
|
60
|
-
| Reference
|
|
60
|
+
| Reference hub UI provider | `createUi(opts?)` | `@devframes/hub-ui` |
|
|
61
61
|
| Mount a hub inside Vite / Next / Nuxt | `viteDevframeHub` / `nextDevframeHub` / hub module | `@devframes/{vite,next,nuxt}/hub` |
|
|
62
62
|
|
|
63
63
|
`createCac`, `createDevServer`, the `@devframes/vite` bridge, and `@devframes/next` are all assembled from `initDevframe` internally - the standard handler is the one wiring underneath every serving path.
|
|
@@ -77,7 +77,7 @@ export default defineDevframe({
|
|
|
77
77
|
homepage: pkg.homepage,
|
|
78
78
|
description: pkg.description,
|
|
79
79
|
icon: 'ph:magnifying-glass-duotone',
|
|
80
|
-
|
|
80
|
+
clientAssets: './client/dist', // built SPA served as the UI
|
|
81
81
|
setup(ctx) {
|
|
82
82
|
const my = ctx.scope('my-inspector') // preferred — auto-namespaces ids
|
|
83
83
|
my.rpc.register(defineRpcFunction({
|
|
@@ -89,21 +89,21 @@ export default defineDevframe({
|
|
|
89
89
|
})
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
-
Source `version` / `packageName` / `homepage` / `description` from your published `package.json` (the JSON import-attribute form resolves under both bundlers and Node's native TypeScript execution). Always pass `importMetaUrl: import.meta.url` - it is the base the
|
|
92
|
+
Source `version` / `packageName` / `homepage` / `description` from your published `package.json` (the JSON import-attribute form resolves under both bundlers and Node's native TypeScript execution). Always pass `importMetaUrl: import.meta.url` - it is the base the node side resolves the tool's own companion packages against (a `--assets` package holding the built SPA, a wire-service package), so a devframe ships them as its own dependencies and users install nothing extra.
|
|
93
93
|
|
|
94
|
-
`setup(ctx, info?)` runs in **every** runtime and does all devframe-level wiring: RPC functions, shared state, streaming channels, diagnostics, agent
|
|
94
|
+
`setup(ctx, info?)` runs in **every** runtime and does all devframe-level wiring: RPC functions, shared state, streaming channels, diagnostics, the agent-facing API. Its optional second argument carries runtime metadata (most notably parsed CLI `flags` under `createCac`). Gate per-runtime work on `ctx.mode` (`'dev'` | `'build'`).
|
|
95
95
|
|
|
96
|
-
**A
|
|
96
|
+
**A built-in devframe's default export is its `create<X>Devframe` factory, never a pre-built instance** - `export default createMyInspectorDevframe`, so importing the module costs nothing and each consumer calls the factory (with or without options) to get its own instance.
|
|
97
97
|
|
|
98
|
-
See `templates/counter-devframe.ts` for a runnable example, `templates/hub.ts` for composing a hub, and `templates/vite-client.ts` for the author's
|
|
98
|
+
See `templates/counter-devframe.ts` for a runnable example, `templates/hub.ts` for composing a hub, and `templates/vite-client.ts` for the author's browser entry.
|
|
99
99
|
|
|
100
100
|
## The standard handler (`initDevframe`)
|
|
101
101
|
|
|
102
|
-
This is the portability trick and the thing to reach for whenever a host can mount a route. `base` is required, so the mount path is explicit at the call site.
|
|
102
|
+
This is the portability trick and the thing to reach for whenever a host framework can mount a route. `base` is required, so the mount path is explicit at the call site.
|
|
103
103
|
|
|
104
104
|
```ts
|
|
105
105
|
import { initDevframe } from 'devframe/initiate'
|
|
106
|
-
import myDevframe from './
|
|
106
|
+
import myDevframe from './my-tool'
|
|
107
107
|
|
|
108
108
|
const devtools = initDevframe(myDevframe, { base: '/__my-tool/' })
|
|
109
109
|
// devtools.base, .handler, .nodeMiddleware, .attach, .handleUpgrade,
|
|
@@ -126,43 +126,43 @@ server.middlewares.use(initDevframe(myDevframe, {
|
|
|
126
126
|
|
|
127
127
|
`devtools.base` is the normalized mount base - reference it in route guards instead of repeating the string.
|
|
128
128
|
|
|
129
|
-
**The WebSocket binding** is the host's explicit call. Fetch handlers only hand over `Request`s, so the RPC socket needs its own binding, resolved in precedence order:
|
|
129
|
+
**The WebSocket binding** is the host framework's explicit call. Fetch handlers only hand over `Request`s, so the RPC socket needs its own binding, resolved in precedence order:
|
|
130
130
|
|
|
131
131
|
1. `ws.port` — a side-car on that exact port.
|
|
132
|
-
2. `server` — share the host's `node:http` server; the upgrade binds at `<base>__ws`. Zero extra ports, follows the app through proxies/HTTPS.
|
|
133
|
-
3. `ws: { sidecar: true }` — a side-car on a free port, for
|
|
134
|
-
4. **The host's own upgrades** — with none of the above, `devtools.attach(server)` routes a server's `upgrade` events (returns a detach fn) and `devtools.handleUpgrade(req, socket, head)` completes a single one. Built lazily — an instance nobody attaches costs nothing.
|
|
132
|
+
2. `server` — share the host framework's `node:http` server; the upgrade binds at `<base>__ws`. Zero extra ports, follows the user app through proxies/HTTPS.
|
|
133
|
+
3. `ws: { sidecar: true }` — a side-car on a free port, for host frameworks whose handlers never see upgrades (Next.js route handlers, Nitro, SvelteKit, Rsbuild).
|
|
134
|
+
4. **The host framework's own upgrades** — with none of the above, `devtools.attach(server)` routes a `node:http` server's `upgrade` events (returns a detach fn) and `devtools.handleUpgrade(req, socket, head)` completes a single one. Built lazily — an instance nobody attaches costs nothing.
|
|
135
135
|
|
|
136
|
-
`ws.url` instead controls the *advertisement* (the tunnel/external-transport pattern). Whichever is active, `__connection.json` describes it and the
|
|
136
|
+
`ws.url` instead controls the *advertisement* (the tunnel/external-transport pattern). Whichever is active, `__connection.json` describes it and the RPC client follows.
|
|
137
137
|
|
|
138
|
-
|
|
138
|
+
Host frameworks that re-evaluate modules in dev (Next, Nitro, SvelteKit) must memoize the instance on `globalThis`, or every reload leaks the previous WebSocket server. The framework kits do this for you.
|
|
139
139
|
|
|
140
|
-
**Auth gates by default** - a handler mounted inside
|
|
140
|
+
**Auth gates by default** - a handler mounted inside the user app's server is reachable by anything that can open its socket. The interactive OTP handler is wired automatically and prints its code / magic-link once the public origin is known. Pass `auth: false` only for a single-user localhost setup, or a `DevframeAuthHandler` for a custom scheme.
|
|
141
141
|
|
|
142
142
|
## Scoped context (preferred)
|
|
143
143
|
|
|
144
|
-
`ctx.scope(id)` (
|
|
144
|
+
`ctx.scope(id)` (node side) and `client.scope(id)` (browser side) return a namespace-scoped view that auto-prefixes every RPC id, shared-state key, and streaming channel with `id:`, and adds a top-level persisted `settings` store. Prefer it over the raw `ctx.rpc` / RPC client - name the namespace once, register and call by bare name.
|
|
145
145
|
|
|
146
146
|
```ts
|
|
147
|
-
//
|
|
147
|
+
// node side — setup(ctx)
|
|
148
148
|
const my = ctx.scope('my-inspector')
|
|
149
149
|
my.rpc.register(getStats) // -> my-inspector:get-stats
|
|
150
150
|
await my.rpc.call('get-stats') // invokeLocal, namespaced
|
|
151
151
|
const state = await my.rpc.sharedState('view') // -> my-inspector:view
|
|
152
152
|
await my.settings.project.set('theme', 'dark')
|
|
153
153
|
|
|
154
|
-
// browser — connectDevframe()
|
|
154
|
+
// browser side — connectDevframe()
|
|
155
155
|
const my = (await connectDevframe()).scope('my-inspector')
|
|
156
156
|
const stats = await my.rpc.call('get-stats')
|
|
157
157
|
```
|
|
158
158
|
|
|
159
159
|
- **Auto-namespacing.** Bare names get `id:` prepended; a name already containing `:` is treated as fully-qualified and passed through (so `my.rpc.call('other-tool:fn')` works). `register` only accepts bare names - a namespaced one throws `DF0034`.
|
|
160
160
|
- **Typed bare calls.** Define functions with bare names and augment the registry with `RpcDefinitionsToFunctionsWithNamespace<'my-inspector', typeof serverFunctions>` so registry keys match the namespaced runtime ids; scoped `call('get-stats')` then stays typed.
|
|
161
|
-
- **`base`.** The scoped context keeps the raw context as `my.base` (and re-exposes `views` / `diagnostics` / `agent` / `services` / `host` / `cwd` / `mode` on the
|
|
161
|
+
- **`base`.** The scoped context keeps the raw context as `my.base` (and re-exposes `views` / `diagnostics` / `agent` / `services` / `host` / `cwd` / `mode` on the node side).
|
|
162
162
|
|
|
163
163
|
### Settings
|
|
164
164
|
|
|
165
|
-
`my.settings` is a persisted key-value store at the **top level** of the scoped context (a sibling of `my.rpc`). Two scopes: `project` (per-
|
|
165
|
+
`my.settings` is a persisted key-value store at the **top level** of the scoped context (a sibling of `my.rpc`). Two scopes: `project` (per-checkout) and `global` (per-user). Both are file-backed on the node side and synced to RPC clients over the shared-state protocol, so a `set` on either side propagates everywhere and survives restarts. All methods are async.
|
|
166
166
|
|
|
167
167
|
```ts
|
|
168
168
|
await my.settings.project.set('theme', 'dark')
|
|
@@ -183,21 +183,21 @@ declare module 'devframe' {
|
|
|
183
183
|
|
|
184
184
|
## DevframeNodeContext at a glance
|
|
185
185
|
|
|
186
|
-
`setup(ctx)` receives the framework-neutral
|
|
186
|
+
`setup(ctx)` receives the framework-neutral node-side API:
|
|
187
187
|
|
|
188
|
-
|
|
|
188
|
+
| Member | Purpose |
|
|
189
189
|
|------|---------|
|
|
190
190
|
| `ctx.scope(id)` | **Preferred** namespace-scoped view — auto-prefixed `rpc` + top-level `settings` store |
|
|
191
191
|
| `ctx.rpc` | Register RPC functions, broadcast, shared state, streaming channels |
|
|
192
192
|
| `ctx.views` | Serve static files via `hostStatic(base, distDir)` |
|
|
193
193
|
| `ctx.diagnostics` | Structured diagnostics host (nostics) — register custom error codes |
|
|
194
194
|
| `ctx.agent` | Expose tools + resources to coding agents |
|
|
195
|
-
| `ctx.services` | Typed cross-
|
|
195
|
+
| `ctx.services` | Typed cross-devframe service registry (`provide` / `whenAvailable`) |
|
|
196
196
|
| `ctx.staticConfig` | This context's own `ConnectionMeta.configs` — boot-time, read-only-from-browser data |
|
|
197
197
|
| `ctx.host` | Runtime abstraction — `mountStatic`, `resolveOrigin`, `getStorageDir` |
|
|
198
198
|
| `ctx.mode` | `'dev'` or `'build'` — gate setup work per runtime |
|
|
199
199
|
|
|
200
|
-
> Hub adapters augment `ctx` with extra
|
|
200
|
+
> Hub adapters augment `ctx` with extra subsystems (`docks`, `terminals`, `messages`, `commands`) — see [The Hub](#the-hub). The Vite DevTools kit exposes the same subsystems via an optional `setup` hook.
|
|
201
201
|
|
|
202
202
|
**Storage scopes** — `ctx.host.getStorageDir(scope)` places persisted state in one of three classes:
|
|
203
203
|
|
|
@@ -211,7 +211,7 @@ Scoped settings persist their `project` scope through `project` storage and thei
|
|
|
211
211
|
|
|
212
212
|
## Project layout
|
|
213
213
|
|
|
214
|
-
Once a devframe grows past a couple of RPC functions, split them out - one file per function under `src/rpc/functions/`, with `src/rpc/index.ts` as the barrel that collects them into `const serverFunctions = [...] as const` and feeds the type-safe client registry via `RpcDefinitionsToFunctionsWithNamespace<'my-tool', typeof serverFunctions>`.
|
|
214
|
+
Once a devframe grows past a couple of RPC functions, split them out - one file per function under `src/rpc/functions/`, with `src/rpc/index.ts` as the barrel that collects them into `const serverFunctions = [...] as const` and feeds the type-safe RPC client registry via `RpcDefinitionsToFunctionsWithNamespace<'my-tool', typeof serverFunctions>`.
|
|
215
215
|
|
|
216
216
|
```ts
|
|
217
217
|
// src/rpc/functions/list-files.ts
|
|
@@ -243,7 +243,7 @@ declare module 'devframe' {
|
|
|
243
243
|
```
|
|
244
244
|
|
|
245
245
|
```ts
|
|
246
|
-
// src/
|
|
246
|
+
// src/my-tool.ts
|
|
247
247
|
import { defineDevframe } from 'devframe'
|
|
248
248
|
import pkg from '../package.json' with { type: 'json' }
|
|
249
249
|
import { setMyToolContext } from './context'
|
|
@@ -267,7 +267,7 @@ export default defineDevframe({
|
|
|
267
267
|
|
|
268
268
|
### Sharing setup-time state via `src/context.ts`
|
|
269
269
|
|
|
270
|
-
When per-file RPCs need runtime values `setup(ctx)` constructs once - channels, shared-state handles, watchers, loaders, caches - expose them through a `WeakMap<DevframeNodeContext, T>` in a sibling `src/context.ts`. The WeakMap keys off the existing `DevframeNodeContext` so contexts are GC'd automatically when the host tears down.
|
|
270
|
+
When per-file RPCs need runtime values `setup(ctx)` constructs once - channels, shared-state handles, watchers, loaders, caches - expose them through a `WeakMap<DevframeNodeContext, T>` in a sibling `src/context.ts`. The WeakMap keys off the existing `DevframeNodeContext` so contexts are GC'd automatically when the host framework tears down.
|
|
271
271
|
|
|
272
272
|
```ts
|
|
273
273
|
// src/context.ts
|
|
@@ -295,10 +295,10 @@ Note `setMyToolContext(ctx, …)` keys off the raw `ctx` (the same object `setup
|
|
|
295
295
|
|
|
296
296
|
```ts
|
|
297
297
|
'my-inspector:get-modules' // ✓
|
|
298
|
-
'get-modules' // ✗ — may collide with other devframes sharing the
|
|
298
|
+
'get-modules' // ✗ — may collide with other devframes sharing the hub
|
|
299
299
|
```
|
|
300
300
|
|
|
301
|
-
A [scoped context](#scoped-context-preferred) applies this prefix for RPC / shared-state / streaming. Dock and command IDs are
|
|
301
|
+
A [scoped context](#scoped-context-preferred) applies this prefix for RPC / shared-state / streaming. Dock and command IDs are hub-level (not part of the scoped `rpc` API) - prefix those by hand.
|
|
302
302
|
|
|
303
303
|
## RPC contracts
|
|
304
304
|
|
|
@@ -324,7 +324,7 @@ const getModules = defineRpcFunction({
|
|
|
324
324
|
|------|----------|--------|-------------|
|
|
325
325
|
| `'static'` | Data constant for a given input — dump at build time | Indefinitely | Automatic |
|
|
326
326
|
| `'query'` | Read that may change; optional `dump` for build adapters | Opt-in via `cacheable` | Manual |
|
|
327
|
-
| `'action'` |
|
|
327
|
+
| `'action'` | Node-side state mutation | Never | Never |
|
|
328
328
|
| `'event'` | Fire-and-forget; no response | Never | Never |
|
|
329
329
|
|
|
330
330
|
Declared `args` / `returns` schemas are **enforced at runtime** - a failing call is rejected with `DF0043` / `DF0044`. Prefer a **single object arg** (`args: [v.object({ ... })]`) over positional args - property names self-document and agents rely on them.
|
|
@@ -338,7 +338,7 @@ Declared `args` / `returns` schemas are **enforced at runtime** - a failing call
|
|
|
338
338
|
|
|
339
339
|
Set `jsonSerializable: true` when your handler returns plain JSON - the strict serializer **throws `DF0020`** synchronously on the offending call when a value can't round-trip through JSON, surfacing next to the call in dev. `agent: {...}` requires `jsonSerializable: true` (registration throws `DF0019` otherwise) - MCP tools speak JSON.
|
|
340
340
|
|
|
341
|
-
Through the scope, `my.rpc.broadcast({ method, args, optional?, event?, filter? })` pushes to every connected client (method name namespaced), and `my.rpc.call(name, ...args)` invokes a
|
|
341
|
+
Through the scope, `my.rpc.broadcast({ method, args, optional?, event?, filter? })` pushes to every connected RPC client (method name namespaced), and `my.rpc.call(name, ...args)` invokes a node-side function locally without transport (the scoped form of `ctx.rpc.invokeLocal`, for cross-function composition).
|
|
342
342
|
|
|
343
343
|
## Shared state
|
|
344
344
|
|
|
@@ -355,7 +355,7 @@ state.mutate((draft) => {
|
|
|
355
355
|
```
|
|
356
356
|
|
|
357
357
|
- Values must be serializable — no functions, no circular refs.
|
|
358
|
-
- Mutations round-trip to
|
|
358
|
+
- Mutations round-trip to every RPC client; the node side tracks `syncIds` to avoid replay loops.
|
|
359
359
|
- Prefer shared state over ad-hoc RPC events for UI that must reappear after reconnect.
|
|
360
360
|
|
|
361
361
|
## Streaming channels
|
|
@@ -365,28 +365,28 @@ For chunk-style data in either direction - LLM deltas, log tails, build progress
|
|
|
365
365
|
```ts
|
|
366
366
|
const my = ctx.scope('my-inspector')
|
|
367
367
|
const channel = my.rpc.streaming.create<string>('tokens', { // -> my-inspector:tokens
|
|
368
|
-
replayWindow: 256, //
|
|
368
|
+
replayWindow: 256, // node side keeps last N chunks per stream id
|
|
369
369
|
closedStreamRetention: 30_000, // ms to hold finished streams for late subscribers
|
|
370
370
|
})
|
|
371
371
|
|
|
372
|
-
//
|
|
372
|
+
// Node side — typically inside an action handler that returns the stream id
|
|
373
373
|
const stream = channel.start({ id: 'optional-stream-id' })
|
|
374
374
|
stream.write(token) // imperative
|
|
375
375
|
stream.close() // terminal success; stream.error(err) for terminal failure
|
|
376
376
|
stream.signal // AbortSignal — flips when consumers cancel or all subscribers drop
|
|
377
377
|
await channel.pipeFrom(sourceReadable) // start + pipe in one call
|
|
378
378
|
|
|
379
|
-
//
|
|
379
|
+
// Browser side — my = (await connectDevframe()).scope('my-inspector')
|
|
380
380
|
const reader = my.rpc.streaming.subscribe<string>('tokens', streamId)
|
|
381
381
|
for await (const token of reader) renderToken(token)
|
|
382
|
-
reader.cancel() //
|
|
382
|
+
reader.cancel() // node-side `stream.signal` aborts
|
|
383
383
|
```
|
|
384
384
|
|
|
385
|
-
The same channel exposes `openInbound()`
|
|
385
|
+
The same channel exposes `openInbound()` — the node-side half of an upload from the browser side; pair it with an action that returns the id, and the RPC client drives `my.rpc.streaming.upload<T>('files', uploadId)`. Web Streams are the canonical API (Node 17+ ships `Readable.fromWeb` / `Writable.fromWeb` converters). Producers should poll `stream.signal.aborted` and exit cooperatively.
|
|
386
386
|
|
|
387
387
|
**Streaming vs events vs shared state:** streaming for token/chunk feeds, uploads, per-call lifecycles with cancellation, and replay-on-reconnect; `event`-typed RPC for payload-free notifications and fire-and-forget signals; shared state for long-lived UI that survives reconnect. For chat UIs, keep the conversation log in shared state and stream active responses - working example: [`examples/streaming-chat`](https://github.com/devframes/devframe/tree/main/examples/streaming-chat).
|
|
388
388
|
|
|
389
|
-
## Agent-native
|
|
389
|
+
## Agent-native API
|
|
390
390
|
|
|
391
391
|
Once a tool has a structured boundary, its visual panel is no longer the only interface: the same internal state and capabilities are consumable programmatically by coding agents, sharing one source of truth. RPC functions stay **private by default** and explicitly opt into agent exposure with an `agent` field. Agent-exposed functions must declare `jsonSerializable: true`.
|
|
392
392
|
|
|
@@ -405,19 +405,19 @@ defineRpcFunction({
|
|
|
405
405
|
})
|
|
406
406
|
```
|
|
407
407
|
|
|
408
|
-
Or register tools / resources directly on `ctx.agent.registerTool({ id, description, safety, handler })` and `ctx.agent.registerResource({ id, name, mimeType, read })`. Expose the
|
|
408
|
+
Or register tools / resources directly on `ctx.agent.registerTool({ id, description, safety, handler })` and `ctx.agent.registerResource({ id, name, mimeType, read })`. Expose the API over MCP:
|
|
409
409
|
|
|
410
410
|
```ts
|
|
411
411
|
import { createMcpServer } from 'devframe/adapters/mcp'
|
|
412
412
|
|
|
413
|
-
await createMcpServer(
|
|
413
|
+
await createMcpServer(myDevframe, { transport: 'stdio' })
|
|
414
414
|
```
|
|
415
415
|
|
|
416
|
-
`@modelcontextprotocol/server` is a peer dependency. The CLI adapter also exposes `my-
|
|
416
|
+
`@modelcontextprotocol/server` is a peer dependency. The CLI adapter also exposes `my-tool mcp` (route node-side logs to stderr - stdout is the transport). Safety classifications (`'read' | 'action' | 'destructive'`) drive MCP hint annotations that coding agents use to prompt for confirmation. In a hub, `ctx.commands` entries opt into the same agent-facing API with an `agent` field and reach MCP through the aggregate endpoint.
|
|
417
417
|
|
|
418
418
|
## Author SPA
|
|
419
419
|
|
|
420
|
-
Authors bring their own SPA (any framework or plain HTML). The
|
|
420
|
+
Authors bring their own SPA (any framework or plain HTML). The browser-side code is **byte-identical** whether the tool runs standalone, embedded, or inside a hub - that is the portability promise.
|
|
421
421
|
|
|
422
422
|
```ts
|
|
423
423
|
import { connectDevframe } from 'devframe/client'
|
|
@@ -429,16 +429,20 @@ const data = await my.rpc.call('get-stats', { limit: 10 })
|
|
|
429
429
|
|
|
430
430
|
`connectDevframe` auto-detects the backend via `./__connection.json`, resolved relative to the executing script's runtime base (so the SPA never hardcodes its mount path - build with `vite.base: './'`):
|
|
431
431
|
|
|
432
|
-
- **websocket** (dev mode) — full read/write, requires the auth handshake. `await client.ensureTrusted()` blocks until the
|
|
432
|
+
- **websocket** (dev mode) — full read/write, requires the auth handshake. `await client.ensureTrusted()` blocks until the node side accepts; listen for token updates on the `devframe-auth` BroadcastChannel.
|
|
433
433
|
- **static** (build output) — read-only, resolves calls from the baked RPC dump.
|
|
434
434
|
|
|
435
|
-
Use `my.rpc.sharedState(key)` for observable state, `my.rpc.register(...)` to receive
|
|
435
|
+
Use `my.rpc.sharedState(key)` for observable state, `my.rpc.register(...)` to receive broadcasts from the node side, `my.rpc.callOptional(...)` when a missing handler should resolve to `undefined`, and `my.settings.{project,global}` for persisted settings synced from the node side.
|
|
436
|
+
|
|
437
|
+
### In-page channel (page script ↔ panel, server-free)
|
|
438
|
+
|
|
439
|
+
For a live inspect-the-page loop, `devframe/in-page-channel` connects a devframe's **page script** (in the user app's page) to its **panels** entirely in the browser — no server, so it works identically in dev and static builds. Declare one shared protocol type; the page script is `createPageScriptChannel<P>({ name, functions })`, each panel `connectPanelChannel<P>({ name })`. Functions use `defineChannelFunction` (the `defineRpcFunction` shape: `type: 'event'` fans out to every panel, `query`/`action` are request/response); `channel.sharedState.get(key)` mirrors `rpc.sharedState` with the page script as authority (panels get automatic replay). The handshake is panel-initiated with retry — boot order and reloads don't matter — and panels expose `status`/`whenConnected(ms)` for "page script not loaded" fallbacks. Channel names follow `devframes:plugin:<slug>`. The a11y inspector's scan/highlight loop is the reference use.
|
|
436
440
|
|
|
437
441
|
## The Hub
|
|
438
442
|
|
|
439
|
-
A single devframe is one portable tool; a hub is where many tools **meet and collaborate**. `@devframes/hub` is the framework-neutral composition layer. It adds the orchestration subsystems that only make sense when tools share a UI, and it ships no UI of its own - a
|
|
443
|
+
A single devframe is one portable tool; a hub is where many tools **meet and collaborate**. `@devframes/hub` is the framework-neutral composition layer. It adds the orchestration subsystems that only make sense when tools share a UI, and it ships no UI of its own - a hub UI provider fills the `ui` slot.
|
|
440
444
|
|
|
441
|
-
`initHub()` puts the whole collection behind one standard handler with the same
|
|
445
|
+
`initHub()` puts the whole collection behind one standard handler with the same API and mount snippets as `initDevframe`:
|
|
442
446
|
|
|
443
447
|
```ts
|
|
444
448
|
import { createUi } from '@devframes/hub-ui'
|
|
@@ -449,7 +453,7 @@ import createTerminalsDevframe from '@devframes/plugin-terminals'
|
|
|
449
453
|
export const hub = initHub({
|
|
450
454
|
base: DEVFRAMES_HUB_BASE, // required — the conventional `/__devframes/`
|
|
451
455
|
devframes: [createInspectDevframe(), createTerminalsDevframe()],
|
|
452
|
-
ui: createUi(), // reference
|
|
456
|
+
ui: createUi(), // reference hub UI provider + floating dock; `ui: false` for headless
|
|
453
457
|
configure(ctx) {
|
|
454
458
|
ctx.commands.register({ id: 'app:hello', title: 'Hello', handler: () => 'hi' })
|
|
455
459
|
},
|
|
@@ -458,20 +462,20 @@ export const hub = initHub({
|
|
|
458
462
|
hub.handler // the whole devtools ecosystem as Request -> Response
|
|
459
463
|
```
|
|
460
464
|
|
|
461
|
-
Every mounted devframe runs its `setup()` against **one shared hub context**: a merged RPC registry (
|
|
465
|
+
Every mounted devframe runs its `setup()` against **one shared hub context**: a merged RPC registry (mounted devframes can call each other's functions), one shared-state store, one WebSocket transport, and one auth gate. Devframe ids become URL segments (`<base><id>/`) and are validated (reserved → `DF8000`, non-route-safe → `DF8004`).
|
|
462
466
|
|
|
463
467
|
### Hub subsystems
|
|
464
468
|
|
|
465
469
|
A hub-aware `DevframeHubContext` extends `DevframeNodeContext` with four subsystems:
|
|
466
470
|
|
|
467
|
-
| Subsystem |
|
|
471
|
+
| Subsystem | API | Purpose |
|
|
468
472
|
|-----------|---------|---------|
|
|
469
473
|
| `ctx.docks` | `register / update / values / activate` | Dock entries (iframe, launcher, custom-render, group, and opt-in types) and cross-iframe activation. |
|
|
470
474
|
| `ctx.terminals` | `register / startChildProcess` | Aggregate terminal sessions, stream output over a well-known channel. |
|
|
471
|
-
| `ctx.messages` | `add / update / remove / clear` |
|
|
475
|
+
| `ctx.messages` | `add / update / remove / clear` | Node-side toast/notification queue (FIFO, capped 1000). |
|
|
472
476
|
| `ctx.commands` | `register / execute / list` | Hierarchical command palette with keybindings and `when` clauses. |
|
|
473
477
|
|
|
474
|
-
The dock union is **open** - opt-in
|
|
478
|
+
The dock union is **open** - opt-in packages contribute their own entry types (e.g. JSON-Render adds a `json-render` dock type with no JSON-render dependency in the hub). `ctx.docks.activate(dockId, params?)` steers which dock entry the hub UI provider shows; from a mounted iframe, `rpc.call('hub:docks:activate', { dockId, params })` does the same cross-iframe. A `type: 'launcher'` dock binds a command, streams a `digest` line, and jumps to its terminal session - the pattern that lets an analyzer spawn `vite build` and navigate the user to its output.
|
|
475
479
|
|
|
476
480
|
### Mounting into a hub
|
|
477
481
|
|
|
@@ -486,27 +490,27 @@ await ctx.install(myDevframe)
|
|
|
486
490
|
|
|
487
491
|
When a devframe sharing an already-mounted `id` is installed, its `duplicationStrategy` (`'warn'` default / `'silent'` / `'throw'` / `'duplicate'`) decides the outcome.
|
|
488
492
|
|
|
489
|
-
### The protocol — what a
|
|
493
|
+
### The protocol — what a hub UI provider sees
|
|
490
494
|
|
|
491
|
-
A hub
|
|
495
|
+
A hub UI provider imports no hub classes; it reads shared-state keys and one RPC method:
|
|
492
496
|
|
|
493
497
|
| Channel | Type | Carries |
|
|
494
498
|
|---------|------|---------|
|
|
495
499
|
| `devframe:docks` (shared state) | `DevframeDockEntry[]` | Every registered dock entry. |
|
|
496
500
|
| `devframe:commands` (shared state) | `DevframeServerCommandEntry[]` | Serializable command list (handlers stripped). |
|
|
497
501
|
| `devframe:docks:active` (shared state) | `DevframeDocksActiveState` | Most recent dock-activation request. |
|
|
498
|
-
| `hub:commands:execute` (RPC) | `(id, ...args) => unknown` |
|
|
499
|
-
| `hub:docks:activate` (RPC) | `({ dockId, params? }) => void` | Switch the active dock from any client. |
|
|
502
|
+
| `hub:commands:execute` (RPC) | `(id, ...args) => unknown` | Node-side command dispatch. |
|
|
503
|
+
| `hub:docks:activate` (RPC) | `({ dockId, params? }) => void` | Switch the active dock from any RPC client. |
|
|
500
504
|
|
|
501
|
-
Plus broadcasts (`devframe:docks:activate`, `devframe:terminals:updated`, `devframe:messages:updated`). The hub also ships a headless
|
|
505
|
+
Plus broadcasts (`devframe:docks:activate`, `devframe:terminals:updated`, `devframe:messages:updated`). The hub also ships a headless client runtime, `createDevframeClientRuntime()` from `@devframes/hub/client`: booted in the host page, it assembles the shared client context from this protocol and imports each dock entry's client script into that page - how a built-in devframe like the a11y inspector runs its page script inside the user app's page.
|
|
502
506
|
|
|
503
507
|
### The `ui` slot
|
|
504
508
|
|
|
505
|
-
The hub is headless; `DevframeHubUi` is pure data (`viewer` / `embedded` / `assets` / `setup`). `@devframes/hub-ui`'s `createUi()` is the reference
|
|
509
|
+
The hub is headless; `DevframeHubUi` is pure data (`viewer` / `embedded` / `assets` / `setup`). `@devframes/hub-ui`'s `createUi()` is the reference hub UI provider - a standalone devtools page plus a floating dock injected via one `<script type="module" src="/__devframes/embedded.js">` tag. It takes `branding`, `dockPreferences`, and `embeddedVisibility` (`'normal'` / `'passive'` / `'hidden'`). Another hub UI provider supplies a different object to the same slot and reuses all the infrastructure. Renderer modules for opt-in dock types compose at the hub via `initHub({ renderers })`.
|
|
506
510
|
|
|
507
|
-
Two copyable reference hubs mount every built-in
|
|
511
|
+
Two copyable reference hubs mount every built-in devframe behind an icon dock rail - the shape Vite DevTools wears, shrunk to the smallest thing you can build your own hub UI provider from: [`examples/hub-vite/`](https://github.com/devframes/devframe/tree/main/examples/hub-vite) (~120-line Vite host, vanilla DOM UI) and [`examples/hub-next/`](https://github.com/devframes/devframe/tree/main/examples/hub-next) (Next.js App Router). The `hub-*-minimal` family shows the default `createUi()` mount across Vite, Next, Nitro, Hono, Fastify, SvelteKit, Deno, and Rsbuild.
|
|
508
512
|
|
|
509
|
-
## Framework
|
|
513
|
+
## Framework kits: two scopes
|
|
510
514
|
|
|
511
515
|
`@devframes/vite`, `@devframes/next`, and `@devframes/nuxt` each split into two clearly-scoped subpaths; the bare root import throws with a pointer to both.
|
|
512
516
|
|
|
@@ -518,13 +522,13 @@ Two copyable reference hubs mount every built-in plugin behind an icon dock - th
|
|
|
518
522
|
- `viteDevframeHub()` (shares Vite's server, injects the dock), `nextDevframeHub()` (side-car socket, App Router route), the Nuxt hub module.
|
|
519
523
|
- `@devframes/hub` and `@devframes/hub-ui` are optional peers of these packages.
|
|
520
524
|
|
|
521
|
-
Vite and Nuxt already have native hub
|
|
525
|
+
Vite and Nuxt already have native hub UI providers ([Vite DevTools](https://devtools.vite.dev/), [Nuxt DevTools](https://devtools.nuxt.com/)), so `@devframes/vite/hub` and `@devframes/nuxt/hub` print a one-time recommendation to prefer those (silence with `{ quiet: true }`); `@devframes/next/hub` has no native counterpart and stays quiet.
|
|
522
526
|
|
|
523
|
-
For Vite DevTools specifically, `createPluginFromDevframe(def, opts?)` from `@vitejs/devtools-kit/node` adapts a definition into the kit's plugin interface (`{ name, devtools: { setup, capabilities } }`), auto-deriving an iframe dock entry from `id` / `name` / `icon` / `basePath`. Pass `options.setup` for richer
|
|
527
|
+
For Vite DevTools specifically, `createPluginFromDevframe(def, opts?)` from `@vitejs/devtools-kit/node` adapts a definition into the kit's plugin interface (`{ name, devtools: { setup, capabilities } }`), auto-deriving an iframe dock entry from `id` / `name` / `icon` / `basePath`. Pass `options.setup` for richer node-side behaviour (custom-render docks, terminals, palette commands) on the kit-augmented context. The factory lives in the kit, not devframe, so devframe stays free of any `@vitejs/*` dependency.
|
|
524
528
|
|
|
525
529
|
## When clauses
|
|
526
530
|
|
|
527
|
-
Gate
|
|
531
|
+
Gate dock / command visibility with VS Code-style expressions. The runtime + types ship bundled from `devframe/utils/when` - no separate install.
|
|
528
532
|
|
|
529
533
|
```ts
|
|
530
534
|
when: 'clientType == embedded'
|
|
@@ -532,15 +536,15 @@ when: 'dockOpen && !paletteOpen'
|
|
|
532
536
|
when: 'my-inspector.ready && count >= 10'
|
|
533
537
|
```
|
|
534
538
|
|
|
535
|
-
Built-in context: `clientType` (`'embedded' | 'standalone'`), `dockOpen`, `paletteOpen`, `dockSelectedId`.
|
|
539
|
+
Built-in context: `clientType` (`'embedded' | 'standalone'`), `dockOpen`, `paletteOpen`, `dockSelectedId`. Devframes add namespaced keys (`.` or `:` separators). `when` clauses evaluate **browser-side only** and are not enforced for agent calls - only gate a command with `when` if running it outside its UI context is safe.
|
|
536
540
|
|
|
537
541
|
## CLI adapter subcommands
|
|
538
542
|
|
|
539
|
-
`createCac(
|
|
543
|
+
`createCac(myDevframe).parse()` gives three subcommands out of the box:
|
|
540
544
|
|
|
541
545
|
| Subcommand | Action |
|
|
542
546
|
|------------|--------|
|
|
543
|
-
| *(default)* | Dev server (port 9999 or `--port`) — WebSocket RPC, `
|
|
547
|
+
| *(default)* | Dev server (port 9999 or `--port`) — WebSocket RPC, `clientAssets` served at the base |
|
|
544
548
|
| `build` | Static snapshot → `./dist-static/` (`--out-dir`) |
|
|
545
549
|
| `mcp` | stdio MCP server |
|
|
546
550
|
|
|
@@ -564,7 +568,7 @@ defineRpcFunction({
|
|
|
564
568
|
})
|
|
565
569
|
```
|
|
566
570
|
|
|
567
|
-
At runtime, static clients look up the argument hash in the dump; misses resolve to `fallback` (or throw if absent). To bake an RPC this devframe doesn't own (e.g. a wire service's), declare it under the definition's `rpc.snapshot`.
|
|
571
|
+
At runtime, static RPC clients look up the argument hash in the dump; misses resolve to `fallback` (or throw if absent). To bake an RPC this devframe doesn't own (e.g. a wire service's), declare it under the definition's `rpc.snapshot`.
|
|
568
572
|
|
|
569
573
|
## Bundled utilities
|
|
570
574
|
|
|
@@ -584,25 +588,25 @@ Devframe re-exports a curated set of helpers under `devframe/utils/*`. They are
|
|
|
584
588
|
| `s` from `devframe/utils/simple-schema` | — | Zero-dep Standard Schema builder (`s.object`, `s.string`, …) for validator-neutral first-party code |
|
|
585
589
|
| `evaluateWhen` / `WhenExpression` from `devframe/utils/when` | `whenexpr` | When-clause expressions |
|
|
586
590
|
|
|
587
|
-
For "open file in editor" + "reveal in finder", prefer the `@devframes/service-open` wire service (declare `services: [{ package: '@devframes/service-open' }]` on the definition, gate
|
|
591
|
+
For "open file in editor" + "reveal in finder", prefer the `@devframes/service-open` wire service (declare `services: [{ package: '@devframes/service-open' }]` on the definition, gate browser-side UI on `rpc.services.has(...)`) - one installation at the host framework shared by every devframe.
|
|
588
592
|
|
|
589
593
|
## Security (secure by default)
|
|
590
594
|
|
|
591
|
-
RPC handlers run with the full privileges of the host process, so the boundary that matters is who may connect.
|
|
595
|
+
RPC handlers run with the full privileges of the host framework's process, so the boundary that matters is who may connect.
|
|
592
596
|
|
|
593
|
-
- **`auth` defaults to `true`** — dev-mode connections must authenticate before calls are accepted. In a **hub**, one gate at the one shared transport covers every
|
|
597
|
+
- **`auth` defaults to `true`** — dev-mode connections must authenticate before calls are accepted. In a **hub**, one gate at the one shared transport covers every mounted devframe, the hub built-ins, and the MCP route; mounted devframes have no gates of their own.
|
|
594
598
|
- **`auth: false` trusts every reachable connection.** Only for single-user `localhost` tools - never with a non-loopback bind host, a tunnel, or a shared/CI environment.
|
|
595
599
|
- **Authentication** exchanges a 6-digit one-time code (shown in the developer's terminal) for a node-issued bearer token. Single-use, expires in 5 min, constant-time compared, rotates after repeated failures.
|
|
596
|
-
- **Magic-link (optional):** the code rides the URL **fragment** (`#devframe_otp=<code>`), never sent to the
|
|
600
|
+
- **Magic-link (optional):** the code rides the URL **fragment** (`#devframe_otp=<code>`), never sent to the node side; `connectDevframe` reads, exchanges, and strips it. Only the single-use code ever rides a URL, never the bearer.
|
|
597
601
|
- **Tokens are secrets.** Serve over `wss://` / `https://` beyond loopback. Never log or bake them into build output. Revoke via `revokeAuthToken(...)`.
|
|
598
|
-
- **Authorize handlers.** Any trusted client can call any registered function - validate inputs, and mark state-changing functions `type: 'destructive'` so
|
|
602
|
+
- **Authorize handlers.** Any trusted RPC client can call any registered function - validate inputs, and mark state-changing functions `type: 'destructive'` so coding agents prompt first. `when` clauses are UI-only and not enforced for agent calls.
|
|
599
603
|
- **The MCP route requires an Origin** — the route-based MCP server rejects `Origin`-less requests, so it isn't reachable by an arbitrary local process.
|
|
600
604
|
|
|
601
605
|
See [Security](https://devfra.me/security) for the full reference.
|
|
602
606
|
|
|
603
607
|
## Testing
|
|
604
608
|
|
|
605
|
-
- Unit-test
|
|
609
|
+
- Unit-test node-side classes with fake contexts.
|
|
606
610
|
- Run `templates/counter-devframe.ts` under each adapter for integration coverage.
|
|
607
611
|
- Snapshot the build-static RPC dump (`<outDir>/.devframe/.rpc-dump/index.json`) to catch drift in `static` function outputs.
|
|
608
612
|
|
|
@@ -615,7 +619,7 @@ Devframe-level (one portable tool):
|
|
|
615
619
|
- [Scoped Context](https://devfra.me/guide/scoped-context) — `ctx.scope(id)`, auto-namespacing, `settings`
|
|
616
620
|
- [Adapters](https://devfra.me/adapters/) — cli / dev / build / mcp / embedded
|
|
617
621
|
- [RPC](https://devfra.me/guide/rpc) — types, Standard Schema, broadcasts, dumps
|
|
618
|
-
- [Shared State](https://devfra.me/guide/shared-state) · [Streaming](https://devfra.me/guide/streaming) · [When Clauses](https://devfra.me/
|
|
622
|
+
- [Shared State](https://devfra.me/guide/shared-state) · [Streaming](https://devfra.me/guide/streaming) · [When Clauses](https://devfra.me/references/when-clauses)
|
|
619
623
|
- [Diagnostics](https://devfra.me/guide/diagnostics) · [Services](https://devfra.me/guide/services) · [Client](https://devfra.me/guide/client)
|
|
620
624
|
- [Security](https://devfra.me/guide/security) · [Agent-Native](https://devfra.me/guide/agent-native)
|
|
621
625
|
|
|
@@ -623,6 +627,6 @@ Hub & frameworks (composing many tools):
|
|
|
623
627
|
|
|
624
628
|
- [Hub](https://devfra.me/guide/hub) — subsystems, docks, protocol, `ctx.install`
|
|
625
629
|
- [Serve a Hub Anywhere](https://devfra.me/guide/hub-initiate) — `initHub`, the `ui` slot, one auth
|
|
626
|
-
- [Client Scripts & Client Context](https://devfra.me/guide/client-context) — `
|
|
630
|
+
- [Client Scripts & Client Context](https://devfra.me/guide/client-context) — `createDevframeClientRuntime`, client-script contract
|
|
627
631
|
- [Frameworks](https://devfra.me/frameworks/) — [Vite](https://devfra.me/frameworks/vite) · [Next](https://devfra.me/frameworks/next) · [Nuxt](https://devfra.me/frameworks/nuxt)
|
|
628
632
|
- [Vite DevTools adapter](https://devfra.me/adapters/vite) — `createPluginFromDevframe`
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// `hub.handler` is a Web-Standard `(request: Request) => Promise<Response>`;
|
|
7
7
|
// mount it on a catch-all route (Hono, Nitro, Next, SvelteKit, ...) exactly
|
|
8
8
|
// like an `initDevframe` instance. `hub.nodeMiddleware` is the Connect-style
|
|
9
|
-
// form for Vite/Rsbuild. See docs/adapters/initiate.md for the mount snippets
|
|
9
|
+
// form for Vite/Rsbuild. See docs/content/2.adapters/1.initiate.md for the mount snippets
|
|
10
10
|
// and the WebSocket-binding precedence (`ws.port` / `server` / `ws.sidecar` /
|
|
11
11
|
// host-attached `hub.attach(server)`).
|
|
12
12
|
import { createUi } from '@devframes/hub-ui'
|