devframe 0.1.18 → 0.1.20

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 (67) hide show
  1. package/README.md +2 -2
  2. package/dist/{_shared-Bxa2_kq7.mjs → _shared-S-Ujqz_L.mjs} +1 -1
  3. package/dist/adapters/build.d.mts +1 -1
  4. package/dist/adapters/build.mjs +9 -5
  5. package/dist/adapters/cli.d.mts +2 -2
  6. package/dist/adapters/cli.mjs +78 -292
  7. package/dist/adapters/dev.d.mts +83 -0
  8. package/dist/adapters/dev.mjs +2 -0
  9. package/dist/adapters/embedded.d.mts +1 -1
  10. package/dist/adapters/kit.d.mts +1 -1
  11. package/dist/adapters/mcp.d.mts +1 -1
  12. package/dist/adapters/mcp.mjs +6 -3
  13. package/dist/adapters/vite.d.mts +1 -1
  14. package/dist/adapters/vite.mjs +1 -1
  15. package/dist/client/index.d.mts +41 -5
  16. package/dist/client/index.mjs +1673 -2
  17. package/dist/constants.d.mts +1 -1
  18. package/dist/{context-xQo1FcxX.mjs → context-BfvbAyGk.mjs} +429 -41
  19. package/dist/dev-B8i_CBlQ.mjs +325 -0
  20. package/dist/{devtool-CHT-4_OU.d.mts → devtool-Bm6zZAw5.d.mts} +371 -28
  21. package/dist/host-h3-BMvrFzIJ.mjs +24 -0
  22. package/dist/{index-Cei8vVcd.d.mts → index-BCo03GEF.d.mts} +1 -1
  23. package/dist/index.d.mts +4 -4
  24. package/dist/index.mjs +11 -2
  25. package/dist/node/index.d.mts +41 -53
  26. package/dist/node/index.mjs +5 -4
  27. package/dist/recipes/open-helpers.d.mts +1 -1
  28. package/dist/recipes/open-helpers.mjs +4 -2
  29. package/dist/rpc/index.d.mts +2 -2
  30. package/dist/rpc/index.mjs +3 -3
  31. package/dist/rpc/transports/ws-client.d.mts +1 -1
  32. package/dist/rpc/transports/ws-client.mjs +1 -1
  33. package/dist/rpc/transports/ws-server.d.mts +1 -1
  34. package/dist/rpc/transports/ws-server.mjs +1 -1
  35. package/dist/{rpc-hbRk8qtt.mjs → rpc-INbWfHoX.mjs} +1 -1
  36. package/dist/{serialization-CWcWE7x7.mjs → serialization-DwKi05Pn.mjs} +1 -1
  37. package/dist/server-DksyT-um.d.mts +54 -0
  38. package/dist/{server-DkJ2-s0Y.mjs → server-qRRM8t3v.mjs} +27 -2
  39. package/dist/{static-dump-DwFfj4U_.mjs → static-dump-C1aVSwxY.mjs} +2 -19
  40. package/dist/types/index.d.mts +4 -4
  41. package/dist/{types-BkyzI9r4.d.mts → types-ag029cAe.d.mts} +1 -1
  42. package/dist/utils/events.d.mts +1 -1
  43. package/dist/utils/shared-state.d.mts +2 -2
  44. package/dist/utils/shared-state.mjs +1 -1
  45. package/dist/utils/state.d.mts +2 -1
  46. package/dist/utils/state.mjs +1 -1
  47. package/dist/utils/streaming-channel.d.mts +2 -0
  48. package/dist/utils/streaming-channel.mjs +255 -0
  49. package/dist/{ws-client-CDGmViwt.d.mts → ws-client-DQySVfF_.d.mts} +1 -1
  50. package/dist/{ws-server-Cu8tmZcF.d.mts → ws-server-VQqESKAs.d.mts} +13 -1
  51. package/package.json +10 -17
  52. package/skills/devframe/README.md +13 -0
  53. package/skills/devframe/SKILL.md +452 -0
  54. package/skills/devframe/templates/counter-devtool.ts +29 -0
  55. package/skills/devframe/templates/spa-devtool.ts +33 -0
  56. package/skills/devframe/templates/vite-client.ts +11 -0
  57. package/dist/client-CsR1_h3o.mjs +0 -1569
  58. package/dist/helpers/nuxt/index.d.mts +0 -46
  59. package/dist/helpers/nuxt/index.mjs +0 -58
  60. package/dist/helpers/nuxt/runtime/plugin.client.d.mts +0 -8
  61. package/dist/helpers/nuxt/runtime/plugin.client.mjs +0 -12
  62. package/dist/immer-DEqg5kOd.mjs +0 -894
  63. package/dist/src-DIKqQIjp.mjs +0 -85
  64. /package/dist/{define-CW9gLnyG.mjs → define-Bb4zh-Dc.mjs} +0 -0
  65. /package/dist/{main-BJD9t8dk.mjs → main-DpINGndA.mjs} +0 -0
  66. /package/dist/{open-BMO2_-wC.mjs → open-BtOOEldu.mjs} +0 -0
  67. /package/dist/{transports-4bqw6Fqg.mjs → transports-BPUzHhI2.mjs} +0 -0
@@ -0,0 +1,452 @@
1
+ ---
2
+ name: devframe
3
+ description: >
4
+ Use when building devtools with devframe — the framework-neutral
5
+ foundation that powers Vite DevTools. Covers DevtoolDefinition,
6
+ picking the right adapter (cli / build / spa / vite / kit / embedded /
7
+ mcp), designing RPC contracts, registering docks / commands / logs /
8
+ terminals, exposing an agent-native surface over MCP, and wiring the
9
+ author's SPA client. Triggers on `devframe` imports, `defineDevtool`,
10
+ `createCli`, `createMcpServer`, `connectDevtool`, and on migrations of
11
+ existing inspectors (eslint-config-inspector, unocss-inspector,
12
+ node-modules-inspector-style tools) to devframe.
13
+ ---
14
+
15
+ # devframe skill
16
+
17
+ A devtool built on devframe is a **single `DevtoolDefinition`** plus an author-provided SPA. Use one of seven adapters to ship it. `devframe` must not depend on Vite or any `@vitejs/*` package — it's the lowest-level layer in the monorepo, and the Kit / core packages build on top.
18
+
19
+ Full reference: [devfra.me/](https://devfra.me/).
20
+
21
+ ## When to use devframe
22
+
23
+ All adapter factories share the shape `createXxx(devtoolDef, options?)`.
24
+
25
+ | Author goal | Factory | Entry |
26
+ |-------------|---------|-------|
27
+ | Standalone CLI for local use | `createCli(def, options?)` | `devframe/adapters/cli` |
28
+ | Run the dev server programmatically (any CLI framework) | `createDevServer(def, options?)` | `devframe/adapters/dev` |
29
+ | Mount a SPA in an existing Vite dev server | `createVitePlugin(def, options?)` | `devframe/adapters/vite` |
30
+ | Self-contained static deploy with baked data | `createBuild(def, options?)` | `devframe/adapters/build` |
31
+ | Integrate into Vite DevTools | `createKitPlugin(def, options?)` | `devframe/adapters/kit` |
32
+ | Register dynamically at runtime | `createEmbedded(def, { ctx })` | `devframe/adapters/embedded` |
33
+ | Expose to coding agents (MCP) | `createMcpServer(def, options?)` | `devframe/adapters/mcp` *(experimental)* |
34
+
35
+ The same `DevtoolDefinition` runs under every adapter — pick based on deployment, not on what the tool does.
36
+
37
+ ## Minimum viable devtool
38
+
39
+ ```ts
40
+ import { defineDevtool, defineRpcFunction } from 'devframe'
41
+
42
+ export default defineDevtool({
43
+ id: 'my-inspector',
44
+ name: 'My Inspector',
45
+ icon: 'ph:magnifying-glass-duotone',
46
+ cli: { distDir: './client/dist' },
47
+ setup(ctx) {
48
+ ctx.rpc.register(defineRpcFunction({
49
+ name: 'my-inspector:get-stats',
50
+ type: 'static',
51
+ handler: () => ({ count: 42 }),
52
+ }))
53
+ ctx.docks.register({
54
+ id: 'my-inspector',
55
+ title: 'My Inspector',
56
+ icon: 'ph:magnifying-glass-duotone',
57
+ type: 'iframe',
58
+ url: '/.devtools/',
59
+ })
60
+ },
61
+ })
62
+ ```
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 full 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 |
83
+ | `ctx.docks` | Dock entries (iframe / action / custom-render / launcher / json-render) |
84
+ | `ctx.views` | Serve static files via `hostStatic(base, distDir)` |
85
+ | `ctx.commands` | Command palette entries with keybindings + `when` gating |
86
+ | `ctx.messages` | Structured message entries, toasts, file / element positions |
87
+ | `ctx.diagnostics` | Structured diagnostics host (logs-sdk) — register custom error codes |
88
+ | `ctx.terminals` | Spawn and stream child processes |
89
+ | `ctx.agent` | Expose tools + resources to coding agents (experimental) |
90
+ | `ctx.host` | Runtime abstraction — `mountStatic`, `resolveOrigin`, `getStorageDir` |
91
+ | `ctx.mode` | `'dev'` or `'build'` — gate setup work per runtime |
92
+
93
+ ## RPC contracts
94
+
95
+ ```ts
96
+ import { defineRpcFunction } from 'devframe'
97
+ import * as v from 'valibot'
98
+
99
+ const getModules = defineRpcFunction({
100
+ name: 'my-inspector:get-modules',
101
+ type: 'query',
102
+ jsonSerializable: true,
103
+ args: [v.object({ limit: v.number() })],
104
+ returns: v.array(v.object({ id: v.string(), size: v.number() })),
105
+ setup: ctx => ({
106
+ handler: async ({ limit }) => loadModules().slice(0, limit),
107
+ }),
108
+ })
109
+ ```
110
+
111
+ | Type | Use when | Cached | Static dump |
112
+ |------|----------|--------|-------------|
113
+ | `'static'` | Data constant for a given input — dump at build time | Indefinitely | Automatic |
114
+ | `'query'` | Read that may change; optional `dump` for build adapters | Opt-in via `cacheable` | Manual |
115
+ | `'action'` | Server-state mutation | Never | Never |
116
+ | `'event'` | Fire-and-forget; no response | Never | Never |
117
+
118
+ 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.
119
+
120
+ ### `jsonSerializable` (wire + dump format)
121
+
122
+ `jsonSerializable` declares the on-wire / on-disk shape contract:
123
+
124
+ | Value | Encoder | Wire prefix | Round-trips |
125
+ |-------|---------|-------------|-------------|
126
+ | `false` (default) | `structured-clone-es` | `s:` | `Map`, `Set`, `Date`, `BigInt`, cycles, class instances |
127
+ | `true` (opt-in) | strict `JSON.stringify` | _(unprefixed)_ | JSON-only |
128
+
129
+ 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.
130
+
131
+ `agent: {...}` requires `jsonSerializable: true` (registration throws `DF0019` otherwise). MCP tools speak JSON — opting into the agent surface is also opting into JSON-only data.
132
+
133
+ `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).
134
+
135
+ ## Shared state
136
+
137
+ ```ts
138
+ const state = await ctx.rpc.sharedState.get('my-inspector:state', {
139
+ initialValue: { count: 0, items: [] as string[] },
140
+ })
141
+
142
+ state.mutate((draft) => {
143
+ draft.count += 1
144
+ draft.items.push('tick')
145
+ })
146
+ ```
147
+
148
+ - Values must be serializable — no functions, no circular refs.
149
+ - Mutations round-trip to all clients; the host tracks `syncIds` to avoid replay loops.
150
+ - Prefer shared state over ad-hoc RPC events for UI that must reappear after reconnect.
151
+
152
+ ## Streaming channels
153
+
154
+ 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:
155
+
156
+ ```ts
157
+ const channel = ctx.rpc.streaming.create<string>('my-inspector:tokens', {
158
+ replayWindow: 256, // server keeps last N chunks per stream id
159
+ closedStreamRetention: 30_000, // ms to hold finished streams for late subscribers
160
+ })
161
+ ```
162
+
163
+ ### Server-to-client (the common case)
164
+
165
+ ```ts
166
+ // Server — typically inside an action handler that returns the stream id
167
+ const stream = channel.start({ id: 'optional-stream-id' })
168
+ stream.write(token) // imperative
169
+ stream.error(err) // terminal failure
170
+ stream.close() // terminal success
171
+ stream.signal // AbortSignal — flips when consumers cancel or all subscribers drop
172
+ stream.writable // WritableStream<T> for `pipeTo`-style consumption
173
+
174
+ // Convenience — start + pipe in one call:
175
+ await channel.pipeFrom(sourceReadable, { id: 'optional' })
176
+
177
+ // Client
178
+ const reader = rpc.streaming.subscribe<string>('my-inspector:tokens', streamId)
179
+ for await (const token of reader) renderToken(token)
180
+ // Or: reader.readable.pipeTo(domWritable)
181
+ reader.cancel() // server `stream.signal` aborts
182
+ ```
183
+
184
+ ### Client-to-server uploads
185
+
186
+ The same channel exposes `openInbound()` for the server side of a client→server upload. Pair it with a normal action that returns the id:
187
+
188
+ ```ts
189
+ // Server
190
+ ctx.rpc.register(defineRpcFunction({
191
+ name: 'my-inspector:upload-file',
192
+ type: 'action',
193
+ args: [v.object({ name: v.string() })],
194
+ returns: v.object({ uploadId: v.string() }),
195
+ handler: async ({ name }) => {
196
+ const reader = channel.openInbound()
197
+ ;(async () => {
198
+ for await (const chunk of reader) saveChunk(chunk)
199
+ })()
200
+ return { uploadId: reader.id }
201
+ },
202
+ }))
203
+
204
+ // Client
205
+ const { uploadId } = await rpc.call('my-inspector:upload-file', { name: 'foo' })
206
+ const upload = rpc.streaming.upload<Uint8Array>('my-inspector:files', uploadId)
207
+ fileReadable.pipeTo(upload.writable, { signal: upload.signal })
208
+ ```
209
+
210
+ 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`.
211
+
212
+ ### Lifecycle
213
+
214
+ | Event | Server | Client |
215
+ |-------|--------|--------|
216
+ | `stream.close()` / `stream.error(err)` | broadcasts `end` to subscribers | `for await` resolves / throws |
217
+ | `reader.cancel()` (last subscriber) | `stream.signal` aborts | reader marked cancelled |
218
+ | WS disconnects (last subscriber drops) | `stream.signal` aborts | reader auto-resubscribes after re-trust |
219
+
220
+ Producers should always poll `stream.signal.aborted` and exit cooperatively.
221
+
222
+ ### Web / Node Streams interop
223
+
224
+ Web Streams are the canonical surface. Node 17+ ships free converters:
225
+
226
+ ```ts
227
+ import { Readable, Writable } from 'node:stream'
228
+
229
+ sourceNodeReadable.pipe(Writable.fromWeb(stream.writable))
230
+ Readable.fromWeb(reader.readable).pipe(targetNodeWritable)
231
+ ```
232
+
233
+ ### When to use streaming vs events vs shared state
234
+
235
+ | Use streaming for | Use `event`-typed RPC for | Use shared state for |
236
+ |-------------------|---------------------------|----------------------|
237
+ | Token / chunk feeds, uploads | Notifications without payload (`refresh`) | Long-lived UI state that survives reconnect |
238
+ | Per-call lifecycles with cancellation | Cross-cutting fire-and-forget signals | Diff-based sync between clients |
239
+ | Replay on reconnect | | |
240
+
241
+ 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).
242
+
243
+ ## Dock entries
244
+
245
+ Five entry types: `iframe` (full panel), `action` (client script on click), `custom-render` (mount into panel DOM), `launcher` (setup card + server callback), `json-render` (UI from a JSON spec, zero client code).
246
+
247
+ ```ts
248
+ // Iframe — most common:
249
+ ctx.docks.register({
250
+ id: 'my-inspector',
251
+ title: 'My Inspector',
252
+ icon: 'ph:magnifying-glass-duotone',
253
+ type: 'iframe',
254
+ url: '/.my-inspector/',
255
+ })
256
+
257
+ ctx.views.hostStatic('/.my-inspector/', clientDist)
258
+ ```
259
+
260
+ All entries accept `when` for conditional visibility and `badge` for short indicator text. See `/devframe/dock-system` for the full type reference.
261
+
262
+ ### Remote docks
263
+
264
+ Set `remote: true` on an iframe dock to turn a hosted URL into a live DevFrame client. DevFrame injects an auth-approved connection descriptor into the iframe URL; on the hosted page, `connectDevtool()` parses it and returns a fully connected client — no extra wiring. Dev-mode only.
265
+
266
+ ## Commands
267
+
268
+ ```ts
269
+ import { defineCommand } from 'devframe'
270
+
271
+ ctx.commands.register(defineCommand({
272
+ id: 'my-inspector:clear-cache',
273
+ title: 'Clear Cache',
274
+ icon: 'ph:trash-duotone',
275
+ keybindings: [{ key: 'Mod+Shift+C' }],
276
+ when: 'clientType == embedded',
277
+ handler: async () => clearCache(),
278
+ }))
279
+ ```
280
+
281
+ - Two-level hierarchy (parent + `children`) max.
282
+ - Use `Mod` for platform-aware modifier (Cmd on macOS, Ctrl elsewhere).
283
+ - `ctx.commands.execute(id, ...args)` runs a command programmatically.
284
+ - `when` is a whenexpr expression — see below.
285
+
286
+ ## Logs & notifications
287
+
288
+ ```ts
289
+ // Fire-and-forget
290
+ ctx.messages.add({ message: 'Scan complete', level: 'success', notify: true })
291
+
292
+ // With handle for in-place updates
293
+ const handle = await ctx.messages.add({
294
+ id: 'my-inspector:build',
295
+ message: 'Building…',
296
+ level: 'info',
297
+ status: 'loading',
298
+ })
299
+ await handle.update({ message: 'Built', level: 'success', status: 'idle' })
300
+ ```
301
+
302
+ `notify: true` also renders a toast. `filePosition: { file, line, column }` makes the entry click-to-editor. `elementPosition: { selector, boundingBox }` highlights a DOM element. Re-adding with the same `id` updates the existing entry (deduplication pattern).
303
+
304
+ ## Terminals
305
+
306
+ ```ts
307
+ const session = await ctx.terminals.startChildProcess(
308
+ { command: 'vite', args: ['build', '--watch'], cwd: process.cwd() },
309
+ { id: 'my-inspector:build', title: 'Build', icon: 'ph:terminal-duotone' },
310
+ )
311
+
312
+ await session.terminate()
313
+ await session.restart()
314
+ ```
315
+
316
+ Color is enabled automatically (`FORCE_COLOR=true`). Output streams into the built-in Terminals panel and is buffered for late-joining clients.
317
+
318
+ ## When clauses
319
+
320
+ Gate dock / command visibility with VS Code-style expressions (evaluated by the external `whenexpr` package):
321
+
322
+ ```ts
323
+ when: 'clientType == embedded'
324
+ when: 'dockOpen && !paletteOpen'
325
+ when: 'my-inspector.ready && count >= 10'
326
+ ```
327
+
328
+ 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`.
329
+
330
+ ## Agent-native surface (experimental)
331
+
332
+ 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):
333
+
334
+ ```ts
335
+ defineRpcFunction({
336
+ name: 'my-inspector:get-stats',
337
+ type: 'query',
338
+ jsonSerializable: true,
339
+ args: [v.object({ limit: v.number() })],
340
+ returns: v.object({ count: v.number() }),
341
+ agent: {
342
+ description: 'Return the top-N module stats. Safe to call freely.',
343
+ // safety inferred from type: 'query' → 'read'
344
+ },
345
+ setup: () => ({ handler: async ({ limit }) => ({ count: limit }) }),
346
+ })
347
+ ```
348
+
349
+ Or register tools / resources directly:
350
+
351
+ ```ts
352
+ ctx.agent.registerTool({
353
+ id: 'my-inspector:summarize',
354
+ description: 'Plain-text summary of the current scan.',
355
+ safety: 'read',
356
+ handler: async () => ({ markdown: buildSummary() }),
357
+ })
358
+
359
+ ctx.agent.registerResource({
360
+ id: 'current-scan',
361
+ name: 'Current scan',
362
+ mimeType: 'text/markdown',
363
+ read: () => ({ text: renderMarkdown(currentScan) }),
364
+ })
365
+ ```
366
+
367
+ Expose via MCP:
368
+
369
+ ```ts
370
+ import { createMcpServer } from 'devframe/adapters/mcp'
371
+
372
+ await createMcpServer(devtool, { transport: 'stdio' })
373
+ ```
374
+
375
+ `@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.
376
+
377
+ ## Author SPA
378
+
379
+ Authors bring their own SPA (any framework or plain HTML). Client entry:
380
+
381
+ ```ts
382
+ import { connectDevtool } from 'devframe/client'
383
+
384
+ const rpc = await connectDevtool()
385
+ // await rpc.ensureTrusted() // WS mode only — blocks until server accepts
386
+
387
+ const data = await rpc.call('my-inspector:get-stats', { limit: 10 })
388
+ ```
389
+
390
+ `connectDevtool` auto-detects the backend via `/.devtools/.connection.json`:
391
+
392
+ - **websocket** (dev mode) — full read/write, requires auth handshake. Listen for token updates on the `vite-devtools-auth` BroadcastChannel.
393
+ - **static** (build / spa output) — read-only, resolves calls from the baked RPC dump.
394
+
395
+ 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.
396
+
397
+ ## Build dumps
398
+
399
+ `createBuild` bakes `static` function results automatically. For `query` functions, supply `dump` (or `snapshot: true` for the no-args sugar):
400
+
401
+ ```ts
402
+ defineRpcFunction({
403
+ name: 'my-inspector:get-session',
404
+ type: 'query',
405
+ setup: () => ({
406
+ handler: async (id: string) => loadSession(id),
407
+ dump: {
408
+ inputs: [['session-a'], ['session-b']],
409
+ fallback: { id: 'unknown', data: null },
410
+ },
411
+ }),
412
+ })
413
+ ```
414
+
415
+ At runtime, static clients look up the argument hash in the dump; misses resolve to `fallback` (or throw if absent).
416
+
417
+ ## CLI adapter subcommands
418
+
419
+ `createCli(devtool).parse()` gives the tool four subcommands out of the box:
420
+
421
+ | Subcommand | Action |
422
+ |------------|--------|
423
+ | *(default)* | Dev server on port 9999 (or `--port`) — WebSocket RPC, `cli.distDir` served at `/.devtools/` |
424
+ | `build` | Static snapshot → `./dist-static/` (configurable via `--out-dir`) |
425
+ | `spa` | Deployable SPA → `./dist-spa/` |
426
+ | `mcp` | stdio MCP server (experimental) |
427
+
428
+ **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.
429
+
430
+ ## Testing
431
+
432
+ - Unit-test host classes with fake contexts.
433
+ - Run `templates/counter-devtool.ts` under each adapter for integration coverage.
434
+ - Snapshot the build-static RPC dump (`<outDir>/.devtools/.rpc-dump/index.json`) to catch accidental drift in `static` function outputs.
435
+
436
+ ## Further reading
437
+
438
+ All of the above has a dedicated page at [devfra.me](https://devfra.me/):
439
+
440
+ - [Devtool Definition](https://devfra.me/devtool-definition) — fields, runtime flags, multi-adapter wiring
441
+ - [Adapters](https://devfra.me/adapters) — full reference for all seven adapters
442
+ - [RPC](https://devfra.me/rpc) — types, schema, broadcasts, dumps
443
+ - [Shared State](https://devfra.me/shared-state) — patches, events, client-side mutation
444
+ - [Streaming](https://devfra.me/streaming) — chunked feeds, uploads, replay, Web/Node Streams interop
445
+ - [Dock System](https://devfra.me/dock-system) — every entry type + remote docks
446
+ - [Commands](https://devfra.me/commands) — palette, keybindings, sub-commands
447
+ - [When Clauses](https://devfra.me/when-clauses) — syntax, context, type-safe wrappers
448
+ - [Messages & Notifications](https://devfra.me/messages) — entry fields, positional hints
449
+ - [Structured Diagnostics](https://devfra.me/diagnostics) — coded errors via `ctx.diagnostics`, register custom codes
450
+ - [Terminals](https://devfra.me/terminals) — child processes, external sessions
451
+ - [Client](https://devfra.me/client) — auth handshake, modes, discovery
452
+ - [Agent-Native](https://devfra.me/agent-native) — agent field, tools/resources, MCP + Claude Desktop
@@ -0,0 +1,29 @@
1
+ // Smallest possible devtool.
2
+ import { defineDevtool, defineRpcFunction } from 'devframe'
3
+
4
+ let counter = 0
5
+
6
+ export default defineDevtool({
7
+ id: 'counter',
8
+ name: 'Counter',
9
+ icon: 'ph:counter-duotone',
10
+ setup(ctx) {
11
+ ctx.rpc.register(defineRpcFunction({
12
+ name: 'counter:get',
13
+ type: 'static',
14
+ handler: () => ({ count: counter }),
15
+ }))
16
+ ctx.rpc.register(defineRpcFunction({
17
+ name: 'counter:bump',
18
+ type: 'action',
19
+ handler: () => ({ count: ++counter }),
20
+ }))
21
+ ctx.docks.register({
22
+ id: 'counter',
23
+ title: 'Counter',
24
+ icon: 'ph:counter-duotone',
25
+ type: 'iframe',
26
+ url: '/counter/',
27
+ })
28
+ },
29
+ })
@@ -0,0 +1,33 @@
1
+ // Devtool with setupBrowser + SPA query-loader — deployable as a static site.
2
+ import { defineDevtool, defineRpcFunction } from 'devframe'
3
+ import * as v from 'valibot'
4
+
5
+ export default defineDevtool({
6
+ id: 'my-inspector',
7
+ name: 'My Inspector',
8
+ icon: 'ph:magnifying-glass-duotone',
9
+ setup(ctx) {
10
+ ctx.rpc.register(defineRpcFunction({
11
+ name: 'my-inspector:analyze',
12
+ type: 'query',
13
+ args: [v.object({ url: v.string() })],
14
+ handler: async ({ url }: { url: string }) => {
15
+ // Server-side implementation (used by CLI/build adapters).
16
+ return { url, verdict: 'ok' as const }
17
+ },
18
+ }))
19
+ ctx.docks.register({
20
+ id: 'my-inspector',
21
+ title: 'My Inspector',
22
+ icon: 'ph:magnifying-glass-duotone',
23
+ type: 'iframe',
24
+ url: '/my-inspector/',
25
+ })
26
+ },
27
+ setupBrowser() {
28
+ // Browser-side implementation — used by the SPA adapter so the
29
+ // deployed static site can answer RPC without a server.
30
+ // (Wire up an in-browser handler here once the SPA adapter lands.)
31
+ },
32
+ spa: { loader: 'query' },
33
+ })
@@ -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)