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.
- package/README.md +2 -2
- package/dist/{_shared-BcPXmcjq.mjs → _shared-iZy45oG3.mjs} +2 -2
- package/dist/adapters/build.d.mts +3 -3
- package/dist/adapters/build.mjs +12 -9
- package/dist/adapters/cli.d.mts +1 -1
- package/dist/adapters/cli.mjs +2 -2
- package/dist/adapters/dev.d.mts +2 -2
- package/dist/adapters/dev.mjs +1 -1
- package/dist/adapters/embedded.d.mts +2 -2
- package/dist/adapters/embedded.mjs +1 -1
- package/dist/adapters/mcp.d.mts +1 -1
- package/dist/adapters/mcp.mjs +6 -2
- package/dist/adapters/vite.d.mts +5 -4
- package/dist/adapters/vite.mjs +4 -3
- package/dist/client/index.d.mts +51 -145
- package/dist/client/index.mjs +1664 -2
- package/dist/constants.d.mts +12 -15
- package/dist/constants.mjs +12 -29
- package/dist/context-BJ4r2SmR.mjs +1051 -0
- package/dist/context-internal-Dx_NoSv1.mjs +199 -0
- package/dist/context-internal-saIAfNVw.d.mts +51 -0
- package/dist/{dev-Djy5hbgs.mjs → dev-CdAy400a.mjs} +7 -6
- package/dist/{devtool-CIqONpYU.d.mts → devtool-CNvLs2_Y.d.mts} +367 -530
- package/dist/diagnostics-DxPnRoXO.mjs +51 -0
- package/dist/host-h3-9jeHcltx.mjs +24 -0
- package/dist/{human-id-CHS0s28X.mjs → human-id-BLoGo_e5.mjs} +0 -1
- package/dist/{index-BwrLhNmz.d.mts → index-CuLRIMto.d.mts} +1 -1
- package/dist/index.d.mts +4 -5
- package/dist/index.mjs +1 -4
- package/dist/internal/index.d.mts +16 -0
- package/dist/internal/index.mjs +3 -0
- package/dist/node/index.d.mts +45 -140
- package/dist/node/index.mjs +7 -6
- package/dist/recipes/open-helpers.d.mts +1 -1
- package/dist/recipes/open-helpers.mjs +3 -1
- package/dist/rpc/index.d.mts +2 -2
- package/dist/rpc/index.mjs +2 -2
- package/dist/rpc/transports/ws-client.d.mts +1 -1
- package/dist/rpc/transports/ws-client.mjs +1 -1
- package/dist/rpc/transports/ws-server.d.mts +1 -1
- package/dist/rpc/transports/ws-server.mjs +1 -1
- package/dist/{rpc-D7LxIh8t.mjs → rpc-INbWfHoX.mjs} +1 -1
- package/dist/{serialization-BN2ZQEE9.mjs → serialization-DwKi05Pn.mjs} +1 -1
- package/dist/{server-CaUeMcAh.d.mts → server-BAqOajx_.d.mts} +1 -1
- package/dist/{server-DrBxa6ZV.mjs → server-CBsxXIH5.mjs} +27 -2
- package/dist/{static-dump-cyGfMmRf.mjs → static-dump-D5VH8Iqk.mjs} +1 -1
- package/dist/types/index.d.mts +4 -4
- package/dist/utils/events.d.mts +1 -1
- package/dist/utils/human-id.mjs +1 -1
- package/dist/utils/shared-state.d.mts +2 -2
- package/dist/utils/shared-state.mjs +1 -1
- package/dist/utils/state.d.mts +2 -1
- package/dist/utils/state.mjs +1 -1
- package/dist/utils/streaming-channel.d.mts +2 -0
- package/dist/utils/streaming-channel.mjs +255 -0
- package/dist/utils/when.d.mts +401 -2
- package/dist/{ws-client-DOYwYluO.d.mts → ws-client-CjPuPFbA.d.mts} +1 -1
- package/dist/{ws-server-DHMeNOY_.d.mts → ws-server-C7LnhOHi.d.mts} +13 -1
- package/package.json +10 -21
- package/skills/devframe/README.md +13 -0
- package/skills/devframe/SKILL.md +406 -0
- package/skills/devframe/templates/counter-devtool.ts +24 -0
- package/skills/devframe/templates/spa-devtool.ts +28 -0
- package/skills/devframe/templates/vite-client.ts +11 -0
- package/dist/adapters/kit.d.mts +0 -23
- package/dist/adapters/kit.mjs +0 -16
- package/dist/client-C6w_NshP.mjs +0 -1569
- package/dist/context-BsNZMCnr.mjs +0 -6827
- package/dist/helpers/nuxt/index.d.mts +0 -46
- package/dist/helpers/nuxt/index.mjs +0 -58
- package/dist/helpers/nuxt/runtime/plugin.client.d.mts +0 -8
- package/dist/helpers/nuxt/runtime/plugin.client.mjs +0 -12
- package/dist/host-h3-w3c0t8ZO.mjs +0 -18
- package/dist/immer-HjMAm3b6.mjs +0 -894
- package/dist/main-CuGOfqoX.mjs +0 -601
- package/dist/when-BAE663Hv.d.mts +0 -401
- package/dist/{open-JfacLHs0.mjs → open-Dede_w9r.mjs} +4 -4
- /package/dist/{types-gWbe-b2R.d.mts → types-C5OVe4AC.d.mts} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { g as RpcFunctionDefinitionAny } from "./types-
|
|
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.
|
|
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
|
-
"./
|
|
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.
|
|
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.
|
|
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)
|
package/dist/adapters/kit.d.mts
DELETED
|
@@ -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 };
|
package/dist/adapters/kit.mjs
DELETED
|
@@ -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 };
|