devframe 0.1.19 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +2 -2
  2. package/dist/{_shared-BcPXmcjq.mjs → _shared-iZy45oG3.mjs} +2 -2
  3. package/dist/adapters/build.d.mts +3 -3
  4. package/dist/adapters/build.mjs +12 -9
  5. package/dist/adapters/cli.d.mts +1 -1
  6. package/dist/adapters/cli.mjs +2 -2
  7. package/dist/adapters/dev.d.mts +2 -2
  8. package/dist/adapters/dev.mjs +1 -1
  9. package/dist/adapters/embedded.d.mts +2 -2
  10. package/dist/adapters/embedded.mjs +1 -1
  11. package/dist/adapters/mcp.d.mts +1 -1
  12. package/dist/adapters/mcp.mjs +6 -2
  13. package/dist/adapters/vite.d.mts +5 -4
  14. package/dist/adapters/vite.mjs +4 -3
  15. package/dist/client/index.d.mts +51 -145
  16. package/dist/client/index.mjs +1664 -2
  17. package/dist/constants.d.mts +12 -15
  18. package/dist/constants.mjs +12 -29
  19. package/dist/context-BJ4r2SmR.mjs +1051 -0
  20. package/dist/context-internal-Dx_NoSv1.mjs +199 -0
  21. package/dist/context-internal-saIAfNVw.d.mts +51 -0
  22. package/dist/{dev-Djy5hbgs.mjs → dev-CdAy400a.mjs} +7 -6
  23. package/dist/{devtool-CIqONpYU.d.mts → devtool-CNvLs2_Y.d.mts} +367 -530
  24. package/dist/diagnostics-DxPnRoXO.mjs +51 -0
  25. package/dist/host-h3-9jeHcltx.mjs +24 -0
  26. package/dist/{human-id-CHS0s28X.mjs → human-id-BLoGo_e5.mjs} +0 -1
  27. package/dist/{index-BwrLhNmz.d.mts → index-CuLRIMto.d.mts} +1 -1
  28. package/dist/index.d.mts +4 -5
  29. package/dist/index.mjs +1 -4
  30. package/dist/internal/index.d.mts +16 -0
  31. package/dist/internal/index.mjs +3 -0
  32. package/dist/node/index.d.mts +45 -140
  33. package/dist/node/index.mjs +7 -6
  34. package/dist/recipes/open-helpers.d.mts +1 -1
  35. package/dist/recipes/open-helpers.mjs +3 -1
  36. package/dist/rpc/index.d.mts +2 -2
  37. package/dist/rpc/index.mjs +2 -2
  38. package/dist/rpc/transports/ws-client.d.mts +1 -1
  39. package/dist/rpc/transports/ws-client.mjs +1 -1
  40. package/dist/rpc/transports/ws-server.d.mts +1 -1
  41. package/dist/rpc/transports/ws-server.mjs +1 -1
  42. package/dist/{rpc-D7LxIh8t.mjs → rpc-INbWfHoX.mjs} +1 -1
  43. package/dist/{serialization-BN2ZQEE9.mjs → serialization-DwKi05Pn.mjs} +1 -1
  44. package/dist/{server-CaUeMcAh.d.mts → server-BAqOajx_.d.mts} +1 -1
  45. package/dist/{server-DrBxa6ZV.mjs → server-CBsxXIH5.mjs} +27 -2
  46. package/dist/{static-dump-cyGfMmRf.mjs → static-dump-D5VH8Iqk.mjs} +1 -1
  47. package/dist/types/index.d.mts +4 -4
  48. package/dist/utils/events.d.mts +1 -1
  49. package/dist/utils/human-id.mjs +1 -1
  50. package/dist/utils/shared-state.d.mts +2 -2
  51. package/dist/utils/shared-state.mjs +1 -1
  52. package/dist/utils/state.d.mts +2 -1
  53. package/dist/utils/state.mjs +1 -1
  54. package/dist/utils/streaming-channel.d.mts +2 -0
  55. package/dist/utils/streaming-channel.mjs +255 -0
  56. package/dist/utils/when.d.mts +401 -2
  57. package/dist/{ws-client-DOYwYluO.d.mts → ws-client-CjPuPFbA.d.mts} +1 -1
  58. package/dist/{ws-server-DHMeNOY_.d.mts → ws-server-C7LnhOHi.d.mts} +13 -1
  59. package/package.json +10 -21
  60. package/skills/devframe/README.md +13 -0
  61. package/skills/devframe/SKILL.md +406 -0
  62. package/skills/devframe/templates/counter-devtool.ts +24 -0
  63. package/skills/devframe/templates/spa-devtool.ts +28 -0
  64. package/skills/devframe/templates/vite-client.ts +11 -0
  65. package/dist/adapters/kit.d.mts +0 -23
  66. package/dist/adapters/kit.mjs +0 -16
  67. package/dist/client-C6w_NshP.mjs +0 -1569
  68. package/dist/context-BsNZMCnr.mjs +0 -6827
  69. package/dist/helpers/nuxt/index.d.mts +0 -46
  70. package/dist/helpers/nuxt/index.mjs +0 -58
  71. package/dist/helpers/nuxt/runtime/plugin.client.d.mts +0 -8
  72. package/dist/helpers/nuxt/runtime/plugin.client.mjs +0 -12
  73. package/dist/host-h3-w3c0t8ZO.mjs +0 -18
  74. package/dist/immer-HjMAm3b6.mjs +0 -894
  75. package/dist/main-CuGOfqoX.mjs +0 -601
  76. package/dist/when-BAE663Hv.d.mts +0 -401
  77. package/dist/{open-JfacLHs0.mjs → open-Dede_w9r.mjs} +4 -4
  78. /package/dist/{types-gWbe-b2R.d.mts → types-C5OVe4AC.d.mts} +0 -0
@@ -1,4 +1,4 @@
1
- import { g as RpcFunctionDefinitionAny } from "./types-gWbe-b2R.mjs";
1
+ import { g as RpcFunctionDefinitionAny } from "./types-C5OVe4AC.mjs";
2
2
  import { ChannelOptions } from "birpc";
3
3
 
4
4
  //#region src/rpc/transports/ws-client.d.ts
@@ -1,4 +1,4 @@
1
- import { g as RpcFunctionDefinitionAny } from "./types-gWbe-b2R.mjs";
1
+ import { g as RpcFunctionDefinitionAny } from "./types-C5OVe4AC.mjs";
2
2
  import { BirpcGroup, ChannelOptions } from "birpc";
3
3
  import { ServerOptions } from "node:https";
4
4
  import { WebSocket, WebSocketServer } from "ws";
@@ -11,6 +11,18 @@ interface DevToolsNodeRpcSessionMeta {
11
11
  clientAuthToken?: string;
12
12
  isTrusted?: boolean;
13
13
  subscribedStates: Set<string>;
14
+ /**
15
+ * Streams this session has subscribed to via
16
+ * `rpc.streaming.subscribe(channel, id)`. Tracked here for O(1) cleanup
17
+ * on disconnect; the wire format is `${channel}\x1F${id}`.
18
+ */
19
+ subscribedStreams?: Set<string>;
20
+ /**
21
+ * Inbound streams this session is currently uploading to (via
22
+ * `rpc.streaming.upload(channel, id)`). Tracked for cleanup on
23
+ * disconnect; same wire format as `subscribedStreams`.
24
+ */
25
+ uploadingStreams?: Set<string>;
14
26
  }
15
27
  interface WsRpcTransportOptions {
16
28
  /** Attach to an existing WebSocketServer. When provided, `port`, `host`, and `https` are ignored. */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "devframe",
3
3
  "type": "module",
4
- "version": "0.1.19",
4
+ "version": "0.1.21",
5
5
  "description": "Framework for building generic DevTools",
6
6
  "author": "Anthony Fu <anthonyfu117@hotmail.com>",
7
7
  "license": "MIT",
@@ -24,13 +24,11 @@
24
24
  "./adapters/cli": "./dist/adapters/cli.mjs",
25
25
  "./adapters/dev": "./dist/adapters/dev.mjs",
26
26
  "./adapters/embedded": "./dist/adapters/embedded.mjs",
27
- "./adapters/kit": "./dist/adapters/kit.mjs",
28
27
  "./adapters/mcp": "./dist/adapters/mcp.mjs",
29
28
  "./adapters/vite": "./dist/adapters/vite.mjs",
30
29
  "./client": "./dist/client/index.mjs",
31
30
  "./constants": "./dist/constants.mjs",
32
- "./helpers/nuxt": "./dist/helpers/nuxt/index.mjs",
33
- "./helpers/nuxt/runtime/plugin.client": "./dist/helpers/nuxt/runtime/plugin.client.mjs",
31
+ "./internal": "./dist/internal/index.mjs",
34
32
  "./node": "./dist/node/index.mjs",
35
33
  "./recipes/open-helpers": "./dist/recipes/open-helpers.mjs",
36
34
  "./rpc": "./dist/rpc/index.mjs",
@@ -45,71 +43,62 @@
45
43
  "./utils/promise": "./dist/utils/promise.mjs",
46
44
  "./utils/shared-state": "./dist/utils/shared-state.mjs",
47
45
  "./utils/state": "./dist/utils/state.mjs",
46
+ "./utils/streaming-channel": "./dist/utils/streaming-channel.mjs",
48
47
  "./utils/when": "./dist/utils/when.mjs",
49
48
  "./package.json": "./package.json"
50
49
  },
51
50
  "types": "./dist/index.d.ts",
52
51
  "files": [
53
- "dist"
52
+ "dist",
53
+ "skills"
54
54
  ],
55
55
  "peerDependencies": {
56
- "@modelcontextprotocol/sdk": "^1.0.0",
57
- "@nuxt/kit": "^3.0.0 || ^4.0.0",
58
- "launch-editor": "^2.0.0"
56
+ "@modelcontextprotocol/sdk": "^1.0.0"
59
57
  },
60
58
  "peerDependenciesMeta": {
61
59
  "@modelcontextprotocol/sdk": {
62
60
  "optional": true
63
- },
64
- "@nuxt/kit": {
65
- "optional": true
66
- },
67
- "launch-editor": {
68
- "optional": true
69
61
  }
70
62
  },
71
63
  "dependencies": {
72
- "@valibot/to-json-schema": "^1.6.0",
64
+ "@valibot/to-json-schema": "^1.7.0",
73
65
  "ansis": "^4.2.0",
74
66
  "birpc": "^4.0.0",
75
67
  "cac": "^7.0.0",
76
68
  "h3": "^1.15.11",
69
+ "immer": "^11.1.7",
70
+ "launch-editor": "^2.13.2",
77
71
  "logs-sdk": "^0.0.6",
78
72
  "ohash": "^2.0.11",
79
73
  "pathe": "^2.0.3",
80
74
  "sirv": "^3.0.2",
81
75
  "structured-clone-es": "^2.0.0",
82
- "valibot": "^1.3.1",
76
+ "valibot": "^1.4.0",
83
77
  "ws": "^8.20.0"
84
78
  },
85
79
  "devDependencies": {
86
80
  "@modelcontextprotocol/sdk": "^1.29.0",
87
- "@nuxt/kit": "^4.4.4",
88
81
  "launch-editor": "^2.13.2",
89
82
  "tsdown": "^0.21.10",
90
83
  "whenexpr": "^0.1.2"
91
84
  },
92
85
  "inlinedDependencies": {
93
- "acorn": "8.16.0",
94
86
  "bundle-name": "4.1.0",
95
87
  "default-browser": "5.4.0",
96
88
  "default-browser-id": "5.0.1",
97
89
  "define-lazy-prop": "3.0.0",
98
90
  "get-port-please": "3.2.0",
99
91
  "human-id": "4.1.3",
100
- "immer": "11.1.4",
101
92
  "is-docker": "3.0.0",
102
93
  "is-in-ssh": "1.0.0",
103
94
  "is-inside-container": "1.0.0",
104
95
  "is-wsl": "3.1.0",
105
- "mlly": "1.8.2",
106
96
  "obug": "2.1.1",
107
97
  "open": "11.0.0",
108
98
  "p-limit": "7.3.0",
109
99
  "perfect-debounce": "2.1.0",
110
100
  "powershell-utils": "0.1.0",
111
101
  "run-applescript": "7.1.0",
112
- "tinyexec": "1.1.2",
113
102
  "ua-parser-modern": "0.1.1",
114
103
  "whenexpr": "0.1.2",
115
104
  "wsl-utils": "0.3.1",
@@ -0,0 +1,13 @@
1
+ # devframe skill
2
+
3
+ An agent skill that teaches Claude Code how to build a full-featured
4
+ devtool with devframe.
5
+
6
+ ## Opt-in
7
+
8
+ Until devframe publishes an installable skill, link this directory from
9
+ your Claude Code skills path. Example for the CLI:
10
+
11
+ ```sh
12
+ ln -s "$PWD/devframe/skills/devframe" "$HOME/.claude/skills/devframe"
13
+ ```
@@ -0,0 +1,406 @@
1
+ ---
2
+ name: devframe
3
+ description: >
4
+ Use when building one devtool integration with devframe — the
5
+ portable, framework-neutral container for a single tool. Covers
6
+ DevtoolDefinition, picking the right deployment adapter
7
+ (cli / build / spa / vite / embedded / mcp), designing RPC
8
+ contracts, exposing an agent-native surface over MCP, and wiring
9
+ the author's SPA client. Hub-only concerns (docks, terminals,
10
+ commands, the unified messages dock) belong to
11
+ `@vitejs/devtools-kit` — see the `vite-devtools-kit` skill for
12
+ those. Triggers on `devframe` imports, `defineDevtool`,
13
+ `createCli`, `createMcpServer`, `connectDevtool`, and on
14
+ migrations of existing inspectors (eslint-config-inspector,
15
+ unocss-inspector, node-modules-inspector-style tools) to devframe.
16
+ ---
17
+
18
+ # devframe skill
19
+
20
+ **Devframe is the container for one devtool integration, portable across viewers.** A devtool built on devframe is a single `DevtoolDefinition` plus an author-provided SPA — the same definition deploys as a standalone CLI, a static report, an embedded SPA, an MCP server, or as a dock entry inside the Vite DevTools hub via `createPluginFromDevframe`.
21
+
22
+ Devframe deliberately stops at the boundary of one tool. Anything that only matters across multiple integrations — docks, terminals, command palette, cross-tool toasts — lives in `@vitejs/devtools-kit`, the hub layer. `devframe` must not depend on Vite, any `@vitejs/*` package, or hub-only concepts; it's the lowest-level layer in the monorepo.
23
+
24
+ Full reference: [devfra.me/](https://devfra.me/).
25
+
26
+ ## When to use devframe
27
+
28
+ All adapter factories share the shape `createXxx(devtoolDef, options?)`.
29
+
30
+ | Author goal | Factory | Entry |
31
+ |-------------|---------|-------|
32
+ | Standalone CLI for local use | `createCli(def, options?)` | `devframe/adapters/cli` |
33
+ | Run the dev server programmatically (any CLI framework) | `createDevServer(def, options?)` | `devframe/adapters/dev` |
34
+ | Mount a SPA in an existing Vite dev server | `createVitePlugin(def, options?)` | `devframe/adapters/vite` |
35
+ | Self-contained static deploy with baked data | `createBuild(def, options?)` | `devframe/adapters/build` |
36
+ | Integrate into Vite DevTools | `createPluginFromDevframe(def, options?)` | `@vitejs/devtools-kit/node` |
37
+ | Register dynamically at runtime | `createEmbedded(def, { ctx })` | `devframe/adapters/embedded` |
38
+ | Expose to coding agents (MCP) | `createMcpServer(def, options?)` | `devframe/adapters/mcp` *(experimental)* |
39
+
40
+ The same `DevtoolDefinition` runs under every adapter — pick based on deployment, not on what the tool does.
41
+
42
+ ## Minimum viable devtool
43
+
44
+ ```ts
45
+ import { defineDevtool, defineRpcFunction } from 'devframe'
46
+
47
+ export default defineDevtool({
48
+ id: 'my-inspector',
49
+ name: 'My Inspector',
50
+ icon: 'ph:magnifying-glass-duotone',
51
+ cli: { distDir: './client/dist' },
52
+ setup(ctx) {
53
+ ctx.rpc.register(defineRpcFunction({
54
+ name: 'my-inspector:get-stats',
55
+ type: 'static',
56
+ handler: () => ({ count: 42 }),
57
+ }))
58
+ },
59
+ })
60
+ ```
61
+
62
+ `setup(ctx)` registers RPC functions, shared state, diagnostics, and any other devframe-level wiring. It does **not** receive `docks` / `terminals` / `messages` / `commands` — those are hub features. When mounted into Vite DevTools via `createPluginFromDevframe(d)`, the kit auto-derives an iframe dock entry from `id` / `name` / `icon` / `basePath`; for richer hub-side behaviour (custom-render, terminals, palette commands) pass `options.setup` to `createPluginFromDevframe`.
63
+
64
+ See `templates/counter-devtool.ts` for a runnable counter example, `templates/spa-devtool.ts` for an SPA-ready shape, and `templates/vite-client.ts` for the author's client entry.
65
+
66
+ ## Namespacing
67
+
68
+ **Always prefix** RPC names, dock IDs, command IDs, shared-state keys, and agent tool IDs with the devtool `id`:
69
+
70
+ ```ts
71
+ 'my-inspector:get-modules' // ✓
72
+ 'my-inspector:state' // ✓
73
+ 'get-modules' // ✗ — may collide with other devtools sharing the host
74
+ ```
75
+
76
+ ## DevToolsNodeContext at a glance
77
+
78
+ `setup(ctx)` receives the framework-neutral server-side surface. Each host corresponds to a [docs](https://devfra.me/) page:
79
+
80
+ | Host | Purpose |
81
+ |------|---------|
82
+ | `ctx.rpc` | Register RPC functions, broadcast, shared state, streaming channels |
83
+ | `ctx.views` | Serve static files via `hostStatic(base, distDir)` |
84
+ | `ctx.diagnostics` | Structured diagnostics host (logs-sdk) — register custom error codes |
85
+ | `ctx.agent` | Expose tools + resources to coding agents (experimental) |
86
+ | `ctx.host` | Runtime abstraction — `mountStatic`, `resolveOrigin`, `getStorageDir` |
87
+ | `ctx.mode` | `'dev'` or `'build'` — gate setup work per runtime |
88
+
89
+ > Hub-only hosts (`ctx.docks`, `ctx.terminals`, `ctx.messages`, `ctx.commands`) only exist when the devtool is mounted into Vite DevTools via `createPluginFromDevframe`. See the [`vite-devtools-kit` skill](../../skills/vite-devtools-kit) for those.
90
+
91
+ ## RPC contracts
92
+
93
+ ```ts
94
+ import { defineRpcFunction } from 'devframe'
95
+ import * as v from 'valibot'
96
+
97
+ const getModules = defineRpcFunction({
98
+ name: 'my-inspector:get-modules',
99
+ type: 'query',
100
+ jsonSerializable: true,
101
+ args: [v.object({ limit: v.number() })],
102
+ returns: v.array(v.object({ id: v.string(), size: v.number() })),
103
+ setup: ctx => ({
104
+ handler: async ({ limit }) => loadModules().slice(0, limit),
105
+ }),
106
+ })
107
+ ```
108
+
109
+ | Type | Use when | Cached | Static dump |
110
+ |------|----------|--------|-------------|
111
+ | `'static'` | Data constant for a given input — dump at build time | Indefinitely | Automatic |
112
+ | `'query'` | Read that may change; optional `dump` for build adapters | Opt-in via `cacheable` | Manual |
113
+ | `'action'` | Server-state mutation | Never | Never |
114
+ | `'event'` | Fire-and-forget; no response | Never | Never |
115
+
116
+ Add valibot schemas when the RPC is user-facing, when you want static dumps, or when you expose it to agents. Prefer a **single object arg** (`args: [v.object({ ... })]`) over positional args — property names self-document and agents rely on them.
117
+
118
+ ### `jsonSerializable` (wire + dump format)
119
+
120
+ `jsonSerializable` declares the on-wire / on-disk shape contract:
121
+
122
+ | Value | Encoder | Wire prefix | Round-trips |
123
+ |-------|---------|-------------|-------------|
124
+ | `false` (default) | `structured-clone-es` | `s:` | `Map`, `Set`, `Date`, `BigInt`, cycles, class instances |
125
+ | `true` (opt-in) | strict `JSON.stringify` | _(unprefixed)_ | JSON-only |
126
+
127
+ Set `jsonSerializable: true` when your handler returns plain JSON shapes — the strict serializer **throws `DF0020`** synchronously on the offending call when it sees a value JSON cannot round-trip (Map/Set/Date/BigInt/class instance/`undefined`-in-array). Errors surface in dev next to the call that introduced them, not silently at build time.
128
+
129
+ `agent: {...}` requires `jsonSerializable: true` (registration throws `DF0019` otherwise). MCP tools speak JSON — opting into the agent surface is also opting into JSON-only data.
130
+
131
+ `ctx.rpc.broadcast({ method, args, optional?, event?, filter? })` pushes to every connected client. `ctx.rpc.invokeLocal(name, ...args)` calls a server function without going through transport (useful for cross-function composition).
132
+
133
+ ## Shared state
134
+
135
+ ```ts
136
+ const state = await ctx.rpc.sharedState.get('my-inspector:state', {
137
+ initialValue: { count: 0, items: [] as string[] },
138
+ })
139
+
140
+ state.mutate((draft) => {
141
+ draft.count += 1
142
+ draft.items.push('tick')
143
+ })
144
+ ```
145
+
146
+ - Values must be serializable — no functions, no circular refs.
147
+ - Mutations round-trip to all clients; the host tracks `syncIds` to avoid replay loops.
148
+ - Prefer shared state over ad-hoc RPC events for UI that must reappear after reconnect.
149
+
150
+ ## Streaming channels
151
+
152
+ For chunk-style data flowing in either direction — LLM deltas, log tails, build progress, file uploads, mic / screen-share frames — use a streaming channel instead of inventing `action + delta/end` events. The same `channel` object handles both directions:
153
+
154
+ ```ts
155
+ const channel = ctx.rpc.streaming.create<string>('my-inspector:tokens', {
156
+ replayWindow: 256, // server keeps last N chunks per stream id
157
+ closedStreamRetention: 30_000, // ms to hold finished streams for late subscribers
158
+ })
159
+ ```
160
+
161
+ ### Server-to-client (the common case)
162
+
163
+ ```ts
164
+ // Server — typically inside an action handler that returns the stream id
165
+ const stream = channel.start({ id: 'optional-stream-id' })
166
+ stream.write(token) // imperative
167
+ stream.error(err) // terminal failure
168
+ stream.close() // terminal success
169
+ stream.signal // AbortSignal — flips when consumers cancel or all subscribers drop
170
+ stream.writable // WritableStream<T> for `pipeTo`-style consumption
171
+
172
+ // Convenience — start + pipe in one call:
173
+ await channel.pipeFrom(sourceReadable, { id: 'optional' })
174
+
175
+ // Client
176
+ const reader = rpc.streaming.subscribe<string>('my-inspector:tokens', streamId)
177
+ for await (const token of reader) renderToken(token)
178
+ // Or: reader.readable.pipeTo(domWritable)
179
+ reader.cancel() // server `stream.signal` aborts
180
+ ```
181
+
182
+ ### Client-to-server uploads
183
+
184
+ The same channel exposes `openInbound()` for the server side of a client→server upload. Pair it with a normal action that returns the id:
185
+
186
+ ```ts
187
+ // Server
188
+ ctx.rpc.register(defineRpcFunction({
189
+ name: 'my-inspector:upload-file',
190
+ type: 'action',
191
+ args: [v.object({ name: v.string() })],
192
+ returns: v.object({ uploadId: v.string() }),
193
+ handler: async ({ name }) => {
194
+ const reader = channel.openInbound()
195
+ ;(async () => {
196
+ for await (const chunk of reader) saveChunk(chunk)
197
+ })()
198
+ return { uploadId: reader.id }
199
+ },
200
+ }))
201
+
202
+ // Client
203
+ const { uploadId } = await rpc.call('my-inspector:upload-file', { name: 'foo' })
204
+ const upload = rpc.streaming.upload<Uint8Array>('my-inspector:files', uploadId)
205
+ fileReadable.pipeTo(upload.writable, { signal: upload.signal })
206
+ ```
207
+
208
+ Client disconnect surfaces as `UploadDisconnected` to the server's `for await`. Server-side `reader.cancel()` broadcasts `upload-cancel` to the uploading session, flipping `upload.signal`.
209
+
210
+ ### Lifecycle
211
+
212
+ | Event | Server | Client |
213
+ |-------|--------|--------|
214
+ | `stream.close()` / `stream.error(err)` | broadcasts `end` to subscribers | `for await` resolves / throws |
215
+ | `reader.cancel()` (last subscriber) | `stream.signal` aborts | reader marked cancelled |
216
+ | WS disconnects (last subscriber drops) | `stream.signal` aborts | reader auto-resubscribes after re-trust |
217
+
218
+ Producers should always poll `stream.signal.aborted` and exit cooperatively.
219
+
220
+ ### Web / Node Streams interop
221
+
222
+ Web Streams are the canonical surface. Node 17+ ships free converters:
223
+
224
+ ```ts
225
+ import { Readable, Writable } from 'node:stream'
226
+
227
+ sourceNodeReadable.pipe(Writable.fromWeb(stream.writable))
228
+ Readable.fromWeb(reader.readable).pipe(targetNodeWritable)
229
+ ```
230
+
231
+ ### When to use streaming vs events vs shared state
232
+
233
+ | Use streaming for | Use `event`-typed RPC for | Use shared state for |
234
+ |-------------------|---------------------------|----------------------|
235
+ | Token / chunk feeds, uploads | Notifications without payload (`refresh`) | Long-lived UI state that survives reconnect |
236
+ | Per-call lifecycles with cancellation | Cross-cutting fire-and-forget signals | Diff-based sync between clients |
237
+ | Replay on reconnect | | |
238
+
239
+ For chat-style UIs that combine both: keep the **conversation log** in shared state (survives reconnects), and use a streaming channel for **active responses**. The action that starts a response appends a placeholder to shared state; on producer close, commit the joined content back to shared state. Working example: [`devframe/examples/devframe-streaming-chat`](https://github.com/vitejs/devtools/tree/main/devframe/examples/devframe-streaming-chat).
240
+
241
+ ## Mounting into Vite DevTools
242
+
243
+ A portable devframe definition is dropped into the Vite DevTools hub via `createPluginFromDevframe`:
244
+
245
+ ```ts
246
+ // vite.config.ts
247
+ import { createPluginFromDevframe } from '@vitejs/devtools-kit/node'
248
+ import myInspector from './my-inspector'
249
+
250
+ export default {
251
+ plugins: [
252
+ createPluginFromDevframe(myInspector, {
253
+ // Optional kit-only setup — runs after the auto-derived dock entry.
254
+ setup(kitCtx) {
255
+ kitCtx.commands.register({
256
+ id: 'my-inspector:clear-cache',
257
+ title: 'Clear Cache',
258
+ handler: () => { /* ... */ },
259
+ })
260
+ },
261
+ }),
262
+ ],
263
+ }
264
+ ```
265
+
266
+ The kit auto-derives an iframe dock entry from `id` / `name` / `icon` / `basePath`. For dock variations (custom-render, launcher, action, json-render), terminals, palette commands, and toasts, use the `options.setup` hook — those APIs live on the kit-augmented context, not on the devframe-level `setup`. See the [`vite-devtools-kit` skill](../../skills/vite-devtools-kit) for the hub-side reference.
267
+
268
+ ## When clauses
269
+
270
+ Gate kit-side dock / command visibility with VS Code-style expressions evaluated by the external `whenexpr` package. The runtime + types ship from `devframe/utils/when`, but the consumers (`when` field on docks and commands) live in the kit:
271
+
272
+ ```ts
273
+ when: 'clientType == embedded'
274
+ when: 'dockOpen && !paletteOpen'
275
+ when: 'my-inspector.ready && count >= 10'
276
+ ```
277
+
278
+ Built-in context: `clientType` (`'embedded' | 'standalone'`), `dockOpen`, `paletteOpen`, `dockSelectedId`. Plugins can add namespaced keys (`.` or `:` separators). Both the types (`WhenExpression<Ctx, S>`) and runtime (`evaluateWhen`, `resolveContextValue`) are re-exported from `devframe/utils/when`.
279
+
280
+ ## Agent-native surface (experimental)
281
+
282
+ Opt an RPC function into the agent surface with an `agent` field — default-deny otherwise. Agent-exposed functions **must declare `jsonSerializable: true`** (registration throws `DF0019` otherwise):
283
+
284
+ ```ts
285
+ defineRpcFunction({
286
+ name: 'my-inspector:get-stats',
287
+ type: 'query',
288
+ jsonSerializable: true,
289
+ args: [v.object({ limit: v.number() })],
290
+ returns: v.object({ count: v.number() }),
291
+ agent: {
292
+ description: 'Return the top-N module stats. Safe to call freely.',
293
+ // safety inferred from type: 'query' → 'read'
294
+ },
295
+ setup: () => ({ handler: async ({ limit }) => ({ count: limit }) }),
296
+ })
297
+ ```
298
+
299
+ Or register tools / resources directly:
300
+
301
+ ```ts
302
+ ctx.agent.registerTool({
303
+ id: 'my-inspector:summarize',
304
+ description: 'Plain-text summary of the current scan.',
305
+ safety: 'read',
306
+ handler: async () => ({ markdown: buildSummary() }),
307
+ })
308
+
309
+ ctx.agent.registerResource({
310
+ id: 'current-scan',
311
+ name: 'Current scan',
312
+ mimeType: 'text/markdown',
313
+ read: () => ({ text: renderMarkdown(currentScan) }),
314
+ })
315
+ ```
316
+
317
+ Expose via MCP:
318
+
319
+ ```ts
320
+ import { createMcpServer } from 'devframe/adapters/mcp'
321
+
322
+ await createMcpServer(devtool, { transport: 'stdio' })
323
+ ```
324
+
325
+ `@modelcontextprotocol/sdk` is a peer dependency. The CLI adapter also exposes `my-devtool mcp` — route host logs to stderr (stdout is the MCP transport). Safety classifications (`'read' | 'action' | 'destructive'`) drive MCP hint annotations that agent clients use to prompt for confirmation.
326
+
327
+ ## Author SPA
328
+
329
+ Authors bring their own SPA (any framework or plain HTML). Client entry:
330
+
331
+ ```ts
332
+ import { connectDevtool } from 'devframe/client'
333
+
334
+ const rpc = await connectDevtool()
335
+ // await rpc.ensureTrusted() // WS mode only — blocks until server accepts
336
+
337
+ const data = await rpc.call('my-inspector:get-stats', { limit: 10 })
338
+ ```
339
+
340
+ `connectDevtool` auto-detects the backend via `/.devtools/.connection.json`:
341
+
342
+ - **websocket** (dev mode) — full read/write, requires auth handshake. Listen for token updates on the `vite-devtools-auth` BroadcastChannel.
343
+ - **static** (build / spa output) — read-only, resolves calls from the baked RPC dump.
344
+
345
+ Use `rpc.sharedState.get(key)` for observable state, `rpc.client.register(defineRpcFunction(...))` to receive server broadcasts, and `rpc.callOptional(...)` when a missing handler should resolve to `undefined` instead of throwing.
346
+
347
+ ## Build dumps
348
+
349
+ `createBuild` bakes `static` function results automatically. For `query` functions, supply `dump` (or `snapshot: true` for the no-args sugar):
350
+
351
+ ```ts
352
+ defineRpcFunction({
353
+ name: 'my-inspector:get-session',
354
+ type: 'query',
355
+ setup: () => ({
356
+ handler: async (id: string) => loadSession(id),
357
+ dump: {
358
+ inputs: [['session-a'], ['session-b']],
359
+ fallback: { id: 'unknown', data: null },
360
+ },
361
+ }),
362
+ })
363
+ ```
364
+
365
+ At runtime, static clients look up the argument hash in the dump; misses resolve to `fallback` (or throw if absent).
366
+
367
+ ## CLI adapter subcommands
368
+
369
+ `createCli(devtool).parse()` gives the tool four subcommands out of the box:
370
+
371
+ | Subcommand | Action |
372
+ |------------|--------|
373
+ | *(default)* | Dev server on port 9999 (or `--port`) — WebSocket RPC, `cli.distDir` served at `/.devtools/` |
374
+ | `build` | Static snapshot → `./dist-static/` (configurable via `--out-dir`) |
375
+ | `spa` | Deployable SPA → `./dist-spa/` |
376
+ | `mcp` | stdio MCP server (experimental) |
377
+
378
+ **Bring your own CLI framework?** `createCli` is just a cac wrapper around three peer factories — `createDevServer` (`devframe/adapters/dev`), `createBuild` (`devframe/adapters/build`), and `createMcpServer` (`devframe/adapters/mcp`). Use them directly with commander/yargs/oclif when `createCli`'s baked-in command structure doesn't fit. `createDevServer` returns a `StartedServer` handle (`origin`, `port`, `app`, `wss`, `close()`) so you can wire SIGINT / hot-reload teardown into the surrounding program. `parseCliFlags(schema, raw)` and `defineCliFlags(...)` (both from `devframe/adapters/cli`) validate an arbitrary flag bag against a `CliFlagsSchema` — typed flags aren't tied to cac.
379
+
380
+ ## Testing
381
+
382
+ - Unit-test host classes with fake contexts.
383
+ - Run `templates/counter-devtool.ts` under each adapter for integration coverage.
384
+ - Snapshot the build-static RPC dump (`<outDir>/.devtools/.rpc-dump/index.json`) to catch accidental drift in `static` function outputs.
385
+
386
+ ## Further reading
387
+
388
+ Devframe-level pages (one-tool, portable surface):
389
+
390
+ - [Devtool Definition](https://devfra.me/devtool-definition) — fields, runtime flags, multi-adapter wiring
391
+ - [Adapters](https://devfra.me/adapters) — full reference for all deployment adapters
392
+ - [RPC](https://devfra.me/rpc) — types, schema, broadcasts, dumps
393
+ - [Shared State](https://devfra.me/shared-state) — patches, events, client-side mutation
394
+ - [Streaming](https://devfra.me/streaming) — chunked feeds, uploads, replay, Web/Node Streams interop
395
+ - [When Clauses](https://devfra.me/when-clauses) — syntax, context, type-safe wrappers
396
+ - [Structured Diagnostics](https://devfra.me/diagnostics) — coded errors via `ctx.diagnostics`, register custom codes
397
+ - [Client](https://devfra.me/client) — auth handshake, modes, discovery
398
+ - [Agent-Native](https://devfra.me/agent-native) — agent field, tools/resources, MCP + Claude Desktop
399
+
400
+ Hub-only surfaces (Vite DevTools Kit — only available when mounted into the hub):
401
+
402
+ - [Vite DevTools Kit overview](https://devtools.vite.dev/kit/)
403
+ - [Dock System](https://devtools.vite.dev/kit/dock-system) — every entry type + remote docks
404
+ - [Commands](https://devtools.vite.dev/kit/commands) — palette, keybindings, sub-commands
405
+ - [Messages & Notifications](https://devtools.vite.dev/kit/messages) — entry fields, positional hints
406
+ - [Terminals](https://devtools.vite.dev/kit/terminals) — child processes, external sessions
@@ -0,0 +1,24 @@
1
+ // Smallest possible devtool. The dock entry is auto-derived from
2
+ // `id` / `name` / `icon` when this definition is mounted into Vite
3
+ // DevTools via `createPluginFromDevframe(devtool)`.
4
+ import { defineDevtool, defineRpcFunction } from 'devframe'
5
+
6
+ let counter = 0
7
+
8
+ export default defineDevtool({
9
+ id: 'counter',
10
+ name: 'Counter',
11
+ icon: 'ph:counter-duotone',
12
+ setup(ctx) {
13
+ ctx.rpc.register(defineRpcFunction({
14
+ name: 'counter:get',
15
+ type: 'static',
16
+ handler: () => ({ count: counter }),
17
+ }))
18
+ ctx.rpc.register(defineRpcFunction({
19
+ name: 'counter:bump',
20
+ type: 'action',
21
+ handler: () => ({ count: ++counter }),
22
+ }))
23
+ },
24
+ })
@@ -0,0 +1,28 @@
1
+ // Devtool with setupBrowser + SPA query-loader — deployable as a static site.
2
+ // When mounted into Vite DevTools via `createPluginFromDevframe`, the kit
3
+ // auto-derives an iframe dock from `id` / `name` / `icon`.
4
+ import { defineDevtool, defineRpcFunction } from 'devframe'
5
+ import * as v from 'valibot'
6
+
7
+ export default defineDevtool({
8
+ id: 'my-inspector',
9
+ name: 'My Inspector',
10
+ icon: 'ph:magnifying-glass-duotone',
11
+ setup(ctx) {
12
+ ctx.rpc.register(defineRpcFunction({
13
+ name: 'my-inspector:analyze',
14
+ type: 'query',
15
+ args: [v.object({ url: v.string() })],
16
+ handler: async ({ url }: { url: string }) => {
17
+ // Server-side implementation (used by CLI/build adapters).
18
+ return { url, verdict: 'ok' as const }
19
+ },
20
+ }))
21
+ },
22
+ setupBrowser() {
23
+ // Browser-side implementation — used by the SPA adapter so the
24
+ // deployed static site can answer RPC without a server.
25
+ // (Wire up an in-browser handler here once the SPA adapter lands.)
26
+ },
27
+ spa: { loader: 'query' },
28
+ })
@@ -0,0 +1,11 @@
1
+ // Minimum SPA client that talks to a devframe-hosted RPC.
2
+ import { connectDevtool } from 'devframe/client'
3
+
4
+ async function main() {
5
+ const rpc = await connectDevtool()
6
+ // The method names below are just examples — replace with your own.
7
+ const data = await rpc.call('my-inspector:getStats' as any)
8
+ document.getElementById('root')!.textContent = JSON.stringify(data)
9
+ }
10
+
11
+ main().catch(console.error)
@@ -1,23 +0,0 @@
1
- import { r as DevtoolDefinition } from "../devtool-CIqONpYU.mjs";
2
-
3
- //#region src/adapters/kit.d.ts
4
- interface CreateKitPluginOptions {
5
- /**
6
- * Optional plugin name override. Defaults to `devframe:<devtool-id>`.
7
- */
8
- name?: string;
9
- }
10
- interface KitPlugin {
11
- name: string;
12
- devtools: {
13
- setup: DevtoolDefinition['setup'];
14
- capabilities?: DevtoolDefinition['capabilities'];
15
- };
16
- }
17
- /**
18
- * Produce a Vite plugin object that Kit's plugin-scan picks up via
19
- * `Plugin.devtools`.
20
- */
21
- declare function createKitPlugin(d: DevtoolDefinition, options?: CreateKitPluginOptions): KitPlugin;
22
- //#endregion
23
- export { CreateKitPluginOptions, KitPlugin, createKitPlugin };
@@ -1,16 +0,0 @@
1
- //#region src/adapters/kit.ts
2
- /**
3
- * Produce a Vite plugin object that Kit's plugin-scan picks up via
4
- * `Plugin.devtools`.
5
- */
6
- function createKitPlugin(d, options = {}) {
7
- return {
8
- name: options.name ?? `devframe:${d.id}`,
9
- devtools: {
10
- setup: d.setup,
11
- capabilities: d.capabilities
12
- }
13
- };
14
- }
15
- //#endregion
16
- export { createKitPlugin };