You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: AGENTS.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -38,6 +38,7 @@ The `pnpm test` script intentionally runs `build` first so `tsnapi` snapshots co
38
38
39
39
## Conventions
40
40
41
+
-**Be very strict about the public API surface.** Every exported symbol on a published subpath is a contract users can depend on — additions and changes must be deliberate, not a side effect of where code happens to live. Before exporting anything new, ask whether it needs to be public at all: helpers shared between first-party packages and transports belong on **`devframe/internal`** (explicitly unstable, can change in any minor release), and module-local code should simply not be exported. Barrel files that `export *` make accidental exposure easy — when adding to a star-exported module, check what rides along. The `tsnapi` snapshots under `tests/__snapshots__/tsnapi/` guard the entire surface: review every snapshot diff as an API-design decision, never regenerate it as a chore, and treat a `TSNAPI_ALLOW_BREAKING` update as something that needs the same scrutiny as the breaking change itself.
41
42
- RPC functions must use `defineRpcFunction`; always namespace IDs `devframes:plugin:<slug>:<fn-name>` (matching the plugin's `@devframes/plugin-<slug>` package name).
42
43
-**Stay validator-neutral.**`devframe` and every `@devframes/*` package must not introduce a preferred schema validator dependency — no `valibot`, `zod`, `arktype`, etc. in their runtime `dependencies`. `args`/`returns`/flag schemas are typed against [Standard Schema](https://standardschema.dev/) (`@standard-schema/spec`, types-only); first-party code that needs to author a schema uses the built-in zero-dep `devframe/utils/simple-schema` builder (deliberately minimal — not a general validator). JSON-schema conversion uses each schema's own Standard JSON Schema converter (`~standard.jsonSchema`, implemented by e.g. zod 4) when present and degrades to a permissive object otherwise — no converter library and no vendor dependency is required. Docs, by contrast, should point *users* at a real validator for their own integrations — recommend **valibot** (lightest) or **zod** (worth reusing if they already pull it via the JSON-render or MCP integrations).
43
44
- Shared state via `devframe/utils/shared-state`; keep values serializable.
> This instance disables its WebSocket transport (`ws: false`), so there is no socket to drive upgrades into.
10
+
11
+
## Cause
12
+
13
+
`ws: false` runs the instance without a WebSocket: clients connect over the SSE endpoint instead (`backend: 'sse'`). There is therefore no socket for `attach(server)` / `handleUpgrade(req, socket, head)` to feed — the host wiring that exists solely to route `upgrade` events has nothing to route to.
14
+
15
+
## Example
16
+
17
+
```ts
18
+
import { initDevframe } from'devframe/initiate'
19
+
20
+
const sseOnly =initDevframe(def, {
21
+
base: '/__my-tool/',
22
+
ws: false,
23
+
})
24
+
sseOnly.attach(myServer) // ✗ throws DF0057 — there is no socket
25
+
26
+
// ✓ SSE needs no upgrade wiring; serve the HTTP surface and you're done.
Drop the `attach` / `handleUpgrade` wiring — the SSE endpoint rides the instance's ordinary HTTP surface (`handler` / `nodeMiddleware`), so serving requests is all a host needs to do. Remove `ws: false` if the instance should serve a WebSocket after all.
33
+
34
+
## Source
35
+
36
+
-[`packages/devframe/src/node/instance-shell.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/node/instance-shell.ts) — the shared instance shell throws this from `attach` / `handleUpgrade` when the WebSocket tier is `disabled`, for both `initDevframe` and `initHub`.
Devframe serves live RPC over two interchangeable transports — a WebSocket and an SSE endpoint — so a client connects even where the WebSocket upgrade is unavailable (serverless platforms, buffering reverse proxies, restrictive corporate networks). Both speak the identical birpc wire protocol with the same per-method serialization, auth handshake, origin policy, shared state, and streaming; switching transports changes nothing about how you write or call RPC functions.
8
+
9
+
## What the server binds
10
+
11
+
A live instance binds both by default:
12
+
13
+
-**WebSocket** at `<base>__ws` — the primary transport, one full-duplex socket.
14
+
-**SSE** at `<base>__sse` — one method-dispatched route: `GET` opens the server→client event stream, `POST` carries client→server RPC frames. It rides the same HTTP surface that serves `__connection.json`, so wherever discovery works, SSE works — including through the Vite bridge's middleware and `initDevframe`'s `handler` / `nodeMiddleware` on hosts that never see upgrade events.
15
+
16
+
`__connection.json` advertises what's bound; `backend` names the server's primary transport:
17
+
18
+
```json
19
+
{
20
+
"backend": "websocket",
21
+
"websocket": { "path": "__ws" },
22
+
"sse": { "path": "__sse" }
23
+
}
24
+
```
25
+
26
+
The SSE stream carries a keep-alive comment every 30 seconds so idle connections survive intermediaries. Both endpoints share one session space — auth trust, shared-state subscriptions, and streaming replay behave identically on either.
27
+
28
+
### Configuring
29
+
30
+
```ts
31
+
// SSE-only — hosts/proxies where the upgrade can't happen. Clients
32
+
// connect over SSE automatically (backend: 'sse').
`ws: false` together with `sse: false` runs an RPC-less shell (`backend: 'none'`) — the SPA, discovery, and MCP routes still serve. The same options apply to `createDevServer`, `initHub`, and a definition's `cli.ws` / `cli.sse` defaults.
43
+
44
+
## What the client picks
45
+
46
+
`connectDevframe` trusts the server's advertisement: it connects over the declared primary, preferring the WebSocket when both endpoints are present. A server that binds no socket advertises SSE as its primary, so the client lands there with no probing or fallback logic.
47
+
48
+
Pin a transport explicitly when you know better than the advertisement — the typical case is an intermediary that silently strips WS upgrades, which the server cannot detect:
client.transport// 'websocket' | 'sse' | 'static' — what actually connected
54
+
```
55
+
56
+
Pinning a transport the server doesn't advertise rejects with a clear error. SSE endpoints resolve with the same proxy-safe rules as WebSocket ones: relative paths against `__connection.json`'s own URL, an explicit `host`/`port` only for a genuinely cross-origin endpoint.
57
+
58
+
A dropped SSE stream ends the client exactly like a closed socket — pending calls reject, the status moves to `disconnected`, and reconnecting means calling `connectDevframe` again.
Copy file name to clipboardExpand all lines: examples/hub-next/README.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -16,7 +16,7 @@ Open the printed URL. The dock on the left lists every mounted tool with its ico
16
16
-**Git**, **Terminals**, **Code Server**, **RPC & State Inspector**, **A11y Inspector** — the built-in plugins, each an entry in `initHub`'s `devframes` list
17
17
-**Next Demo Tool** / **Next Demo Tool B** — two trivial static SPAs that show the bare mount path
18
18
19
-
Selecting a tool loads its SPA in the stage. The bottom drawer mirrors the hub's **Commands**, **Messages**, and **Terminals** subsystems, plus a button that dispatches a command through `hub:commands:execute`.
19
+
Selecting a tool loads its SPA in the stage. The bottom drawer mirrors the hub's **Commands**, **Messages**, and **Terminals** subsystems, plus a button that dispatches a command through `hub:commands:execute`, and a **Transport** section showing which RPC transport the connection runs on (`websocket` or `sse`) with a segmented Auto / WS / SSE toggle — the choice rides a `?transport=` URL param and reconnects the whole client host on the pinned transport.
20
20
21
21
The A11y Inspector shows a live axe-core report of this hub's own page: the host serves the plugin's in-page agent module (`a11yAgentBundlePath`) same-origin inside the hub namespace and attaches it as the a11y dock's `clientScript` (the `{ devframe, dock }` entry form); the hub client runtime — `createDevframeClientHost()` booted in `app/page.tsx` — imports it into the page, so the docked panel and the agent share the origin their BroadcastChannel rides.
Copy file name to clipboardExpand all lines: examples/hub-vite/README.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -16,7 +16,7 @@ Open the printed URL. The dock on the left lists every mounted tool with its ico
16
16
-**Git**, **Terminals**, **Code Server**, **RPC & State Inspector**, **A11y Inspector** — the built-in plugins, each a published `DevframeDefinition` passed to the host's `devframes` option
17
17
-**Demo Tool** / **Demo Tool B** — two trivial static SPAs that show the bare mount path
18
18
19
-
Selecting a tool loads its SPA in the stage. The bottom drawer mirrors the hub's **Commands**, **Messages**, and **Terminals** subsystems, plus a button that dispatches a command through `hub:commands:execute`.
19
+
Selecting a tool loads its SPA in the stage. The bottom drawer mirrors the hub's **Commands**, **Messages**, and **Terminals** subsystems, plus a button that dispatches a command through `hub:commands:execute`, and a **Transport** section showing which RPC transport the connection runs on (`websocket` or `sse`) with a segmented Auto / WS / SSE toggle — the choice rides a `?transport=` URL param and reconnects the whole client host on the pinned transport.
20
20
21
21
The A11y Inspector shows a live axe-core report of this hub's own page. `vite.config.ts` attaches the plugin's in-page agent as the a11y dock's `clientScript` (served via `/@fs/`), and the hub client runtime — `createDevframeClientHost()` booted in `src/client/main.ts` — imports it into the host page. Panel and agent share the Vite origin their BroadcastChannel rides; hover a violation to ring the offending element in the hub UI.
0 commit comments