@devframes/hub 0.8.2 → 0.9.0-beta.10

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.
@@ -0,0 +1,274 @@
1
+ import { i as InstallDevframeOptions, n as DevframeHubContext, t as CreateHubContextOptions } from "../context-D5TJIHwp.mjs";
2
+ import { DEVFRAMES_HUB_BASE } from "../constants.mjs";
3
+ import { DevframeInstanceRecord } from "devframe/internal";
4
+ import { ConnectionMeta, DevframeDefinition, DevframeSseOptions, DevframeStorageScope, DevframeWsOptions, McpRouteOptions } from "devframe/types";
5
+ import { DevframeAuthHandler } from "devframe/node/auth";
6
+ import { WsOriginRegistry } from "devframe/rpc/transports/ws-server";
7
+ import { Buffer } from "node:buffer";
8
+ import { IncomingMessage, Server, ServerResponse } from "node:http";
9
+ import { Duplex } from "node:stream";
10
+ //#region src/node/initiate.d.ts
11
+ /** A `devframes` entry with per-mount dock customization. */
12
+ interface HubDevframeEntry {
13
+ devframe: DevframeDefinition;
14
+ /** Per-mount overrides for the auto-synthesized iframe dock entry. */
15
+ dock?: InstallDevframeOptions['dock'];
16
+ }
17
+ type Thenable<T> = T | Promise<T>;
18
+ type Arrayable<T> = T | readonly T[];
19
+ /**
20
+ * One dock-type → prebuilt renderer-module registration for
21
+ * {@link InitHubOptions.renderers}. The hub serves the module at
22
+ * `<base>__renderers/<type>.mjs` and publishes it in the renderer manifest
23
+ * (the `devframe:dock-renderers` shared-state slot), so any viewer — the
24
+ * reference UI, a community viewer, a hand-rolled host page — lazily imports
25
+ * it the first time a dock of that `type` needs rendering.
26
+ *
27
+ * The module must be a **self-contained browser ES module** (its framework
28
+ * and styles bundled in) whose {@link DockRendererRegistration.importName}
29
+ * export is a ready `DockRenderer`. Renderer packages ship a node helper
30
+ * returning this shape — e.g. `jsonRenderUiRenderer()` from
31
+ * `@devframes/json-render-ui/hub`.
32
+ */
33
+ interface DockRendererRegistration {
34
+ /** Dock `type` this renderer handles (e.g. `'json-render'`). */
35
+ type: string;
36
+ /** Absolute path of the prebuilt, self-contained browser ES module. */
37
+ file: string;
38
+ /**
39
+ * Named export carrying the renderer.
40
+ *
41
+ * @default 'default'
42
+ */
43
+ importName?: string;
44
+ }
45
+ /**
46
+ * The UI slot of a hub instance — pure data, zero policy. The hub itself is
47
+ * headless: whoever fills this slot decides what a viewer looks like.
48
+ * `@devframes/hub-ui` ships the reference implementation (`createUi()`);
49
+ * Vite DevTools or any community viewer supplies its own object to the same
50
+ * slot and reuses all the infrastructure.
51
+ */
52
+ interface DevframeHubUi {
53
+ /**
54
+ * A standalone viewer SPA (built with relative asset paths) served at the
55
+ * hub base itself — open `<base>` in a tab and the devtools are there.
56
+ */
57
+ viewer?: {
58
+ /** Directory of the prebuilt viewer SPA. */
59
+ distDir: string;
60
+ };
61
+ /**
62
+ * A prebuilt, self-contained script served at `<base>embedded.js` — the
63
+ * floating-devtools bootstrap a host page loads with one
64
+ * `<script type="module" src="<base>embedded.js">` tag. Visibility policy
65
+ * (always-on, keyboard-summoned, …) belongs entirely to this entry.
66
+ */
67
+ embedded?: {
68
+ /** File path of the prebuilt single-file module. */
69
+ entry: string;
70
+ };
71
+ /**
72
+ * Extra UI-owned files the hub serves at `<base><key>`, each produced lazily
73
+ * from memory. Keys are base-relative paths; the content-type is inferred
74
+ * from the key's extension. A generic seam a viewer uses to publish
75
+ * arbitrary runtime documents without teaching the hub anything about
76
+ * their meaning.
77
+ */
78
+ assets?: Record<string, () => string | Uint8Array>;
79
+ /**
80
+ * A setup hook run once during hub init with the hub context — the UI
81
+ * slot's chance to publish its own static, boot-time config through the
82
+ * generic `ctx.staticConfig` (serialized into `ConnectionMeta.configs`).
83
+ * The reference UI's `createUi()` uses it to set
84
+ * `ctx.staticConfig.ui = { branding, … }`, reaching every mounted frame and
85
+ * the standalone viewer through the one connection handshake they perform.
86
+ * The hub stays policy-free about what the UI writes.
87
+ */
88
+ setup?: (ctx: DevframeHubContext) => void | Promise<void>;
89
+ }
90
+ type DevframesInput = Array<DevframeDefinition | HubDevframeEntry | Thenable<Arrayable<DevframeDefinition | HubDevframeEntry | null | undefined>> | (() => Thenable<Arrayable<DevframeDefinition | HubDevframeEntry | null | undefined>>)>;
91
+ interface InitHubOptions {
92
+ /**
93
+ * Name for the hub instance, used in logs and diagnostics, and mcp server.
94
+ */
95
+ name?: string;
96
+ /**
97
+ * Version for the hub instance, used in logs and diagnostics, and mcp server.
98
+ */
99
+ version?: string;
100
+ /**
101
+ * Mount base the hub answers under — required so the mount path is
102
+ * explicit at the call site (pass the exported {@link DEVFRAMES_HUB_BASE}
103
+ * for the conventional `/__devframes/`). Every mounted devframe lives at
104
+ * `<base><id>/`, so the host app needs exactly one catch-all route. The
105
+ * resolved value is echoed back as {@link HubInstance.base} so route and
106
+ * middleware code references it instead of repeating the string.
107
+ */
108
+ base: string;
109
+ /**
110
+ * Devframes to mount: each runs its `setup()` against the shared hub
111
+ * context (one merged RPC registry, one WebSocket, one auth gate), serves
112
+ * its SPA at `<base><id>/`, and is auto-registered as an iframe dock.
113
+ * Wrap an entry in `{ devframe, dock }` to customize its synthesized dock
114
+ * (category, icon, a `clientScript` to run in the host page, …).
115
+ */
116
+ devframes?: DevframesInput;
117
+ /**
118
+ * Extra RPC declarations registered at context creation, alongside the
119
+ * hub built-ins — forwarded to `createHubContext`'s
120
+ * `builtinRpcDeclarations`. Declarative mode only (a pre-built `context`
121
+ * already made this choice).
122
+ */
123
+ rpcDeclarations?: CreateHubContextOptions['builtinRpcDeclarations'];
124
+ /**
125
+ * Bring your own hub context instead of `devframes` — for hosts that
126
+ * assemble `createHubContext` + `ctx.install` themselves (with their own
127
+ * `DevframeHost` serving the frames). The instance then serves only the
128
+ * hub-level endpoints (`__connection.json`, `__index.json`,
129
+ * `__client-imports.js`, the WS transport, MCP, and the `ui` slot);
130
+ * serve each frame's meta yourself from {@link HubInstance.connectionMeta}.
131
+ */
132
+ context?: DevframeHubContext;
133
+ /**
134
+ * Runs once the context exists and every `devframes` entry is mounted —
135
+ * register docks, commands, terminals, and messages surfaces here.
136
+ */
137
+ configure?: (ctx: DevframeHubContext) => void | Promise<void>;
138
+ /** See {@link DevframeHubUi} — omitted, the hub stays fully headless. */
139
+ ui?: DevframeHubUi;
140
+ /**
141
+ * Prebuilt dock-renderer modules to serve and advertise — the composition
142
+ * seam that hands a renderer package (e.g. `@devframes/json-render-ui`) to
143
+ * a prebuilt viewer. Each {@link DockRendererRegistration} is served at
144
+ * `<base>__renderers/<type>.mjs` and published in the renderer manifest;
145
+ * clients import a module lazily the first time a dock of its `type`
146
+ * mounts. Renderers registered directly in client code
147
+ * (`createDevframeClientHost({ renderers })`) take precedence.
148
+ *
149
+ * ```ts
150
+ * import { jsonRenderUiRenderer } from '@devframes/json-render-ui/hub'
151
+ *
152
+ * initHub({ ui: createUi(), renderers: [jsonRenderUiRenderer()] })
153
+ * ```
154
+ */
155
+ renderers?: readonly DockRendererRegistration[];
156
+ /**
157
+ * Share the host's `node:http` server for the WebSocket RPC endpoint
158
+ * (upgrade bound at `<base>__ws`). Hosts whose handlers never see upgrades
159
+ * (Next.js route handlers, Nitro, Rsbuild) ask for a side-car instead with
160
+ * `ws: { sidecar: true }`; hosts that get their server later wire
161
+ * {@link HubInstance.attach} / {@link HubInstance.handleUpgrade}.
162
+ */
163
+ server?: Server;
164
+ /**
165
+ * Explicit WebSocket control, same contract as `initDevframe`: the local
166
+ * binding resolves `ws.port` (pinned side-car) > `server` (shared upgrade)
167
+ * > `ws.sidecar` (auto-port side-car) > the host driving upgrades itself,
168
+ * while `url` overrides the advertisement (tunnel pattern) and `route`
169
+ * renames the upgrade segment (default `__ws`). Pass `false` to serve no
170
+ * WebSocket at all — clients connect over SSE instead (`backend: 'sse'`).
171
+ */
172
+ ws?: DevframeWsOptions | false;
173
+ /**
174
+ * SSE RPC endpoint control, same contract as `initDevframe` — enabled by
175
+ * default at `<base>__sse` as the more portable transport alongside the
176
+ * WebSocket. Pass `false` to disable, or a {@link DevframeSseOptions} to
177
+ * rename the route.
178
+ */
179
+ sse?: boolean | DevframeSseOptions;
180
+ /** Bind host for a side-car WebSocket server. Default: `localhost`. */
181
+ host?: string;
182
+ /**
183
+ * The hub's **single Auth**: one gate at the one shared transport covers
184
+ * every mounted frame, the MCP route, and the hub built-ins. Gates by
185
+ * default (devframe's interactive OTP); `false` opts out; a
186
+ * {@link DevframeAuthHandler} installs a custom scheme.
187
+ */
188
+ auth?: boolean | DevframeAuthHandler;
189
+ /**
190
+ * Expose the **aggregate** MCP endpoint at `<base>__mcp` — one
191
+ * Streamable-HTTP server over the shared context's whole tool registry
192
+ * (ids are already namespaced per plugin). Disabled by default.
193
+ */
194
+ mcp?: boolean | McpRouteOptions;
195
+ /**
196
+ * Public origin the host app is reachable at, or a getter. Derived lazily
197
+ * from the first request when omitted.
198
+ */
199
+ origin?: string | (() => string);
200
+ /**
201
+ * Publish this hub in the global instance registry
202
+ * (`~/.devframe/instances/`) so discovery tooling (`devframe connect`, the
203
+ * inspect plugin's Instances tab) lists it like any standalone devframe.
204
+ * Registration is a dynamic import that fires once the public origin
205
+ * resolves and is torn down on {@link HubInstance.close}. Defaults to off;
206
+ * pass `true` to enable, or an object to override individual record fields
207
+ * (`id`, `name`, `basePath`, …).
208
+ */
209
+ register?: boolean | Partial<DevframeInstanceRecord>;
210
+ /** Working directory for the hub context. Default: `process.cwd()`. */
211
+ cwd?: string;
212
+ /** Override where persisted devframe state lives. */
213
+ getStorageDir?: (scope: DevframeStorageScope) => string;
214
+ /** Extra WS-upgrade origins beyond the loopback default; `false` disables the gate. */
215
+ allowedOrigins?: readonly string[] | WsOriginRegistry | false;
216
+ /** Destroy off-route upgrades on a shared `server` devframe's adapter owns outright. */
217
+ destroyUnmatchedUpgrades?: boolean;
218
+ }
219
+ interface HubInstance {
220
+ /**
221
+ * The normalized mount base this hub answers under (leading and trailing
222
+ * slash, e.g. `/__devframes/`). Reference it when wiring the mount — route
223
+ * guards, middleware path checks — instead of repeating the string literal.
224
+ */
225
+ base: string;
226
+ /**
227
+ * Web-standard request handler for the whole hub — mount it on one
228
+ * catch-all route under {@link HubInstance.base}.
229
+ */
230
+ handler: (request: Request) => Promise<Response>;
231
+ /** Connect/Express-style middleware over the same surface; `next()`s outside the base. */
232
+ nodeMiddleware: (req: IncomingMessage, res: ServerResponse, next?: (err?: unknown) => void) => void;
233
+ /**
234
+ * Route a host server's `upgrade` events to the shared RPC socket,
235
+ * returning a detach function — the manual counterpart to the `server`
236
+ * option, for hosts that get their `node:http` server only after the hub
237
+ * exists. Available on the default tier; a configured transport (`server`,
238
+ * `ws.port`, `ws.sidecar`, `ws.url`) already owns the socket and reports
239
+ * `DF0055` / `DF0056` instead.
240
+ */
241
+ attach: (server: Server) => () => void;
242
+ /**
243
+ * Complete a single `upgrade` event on the shared RPC socket, for hosts
244
+ * that already own an `upgrade` listener. Same availability as
245
+ * {@link HubInstance.attach}.
246
+ */
247
+ handleUpgrade: (req: IncomingMessage, socket: Duplex, head: Buffer) => void;
248
+ /** Resolves once every frame is mounted and the WebSocket binding is live. */
249
+ ready: Promise<void>;
250
+ /** The shared hub context, once initialized. */
251
+ context: Promise<DevframeHubContext>;
252
+ /** The `ConnectionMeta` served at `<base>__connection.json` (and every frame base). */
253
+ connectionMeta: () => ConnectionMeta;
254
+ /** Tear down: WS transport/side-car, MCP sessions. */
255
+ close: () => Promise<void>;
256
+ }
257
+ /**
258
+ * Initiate a hub instance — the whole multi-devframe devtools surface
259
+ * behind one framework-agnostic, web-standard handler. Every mounted
260
+ * devframe shares one context (merged RPC registry, shared state, docks /
261
+ * terminals / messages / commands), one WebSocket transport, and one Auth;
262
+ * the instance serves each frame's SPA at `<base><id>/`, the discovery
263
+ * endpoints (`__connection.json`, `__index.json`, `__client-imports.js`),
264
+ * the aggregate MCP route, and whatever the {@link DevframeHubUi} slot
265
+ * provides — the hub itself stays headless.
266
+ *
267
+ * The factory is synchronous and initializes eagerly, and binds no port of
268
+ * its own: the WebSocket follows `server` / `ws` (see
269
+ * {@link InitHubOptions.ws}), or waits for the host to hand upgrades over
270
+ * through {@link HubInstance.attach}.
271
+ */
272
+ declare function initHub(options: InitHubOptions): HubInstance;
273
+ //#endregion
274
+ export { DEVFRAMES_HUB_BASE, DevframeHubUi, DevframesInput, DockRendererRegistration, HubDevframeEntry, HubInstance, InitHubOptions, initHub };
@@ -0,0 +1,267 @@
1
+ import { DEVFRAMES_HUB_BASE, DOCK_RENDERERS_STATE_KEY, normalizeHubBase } from "../constants.mjs";
2
+ import { t as createHubContext, v as diagnostics } from "../context-DzqMQ2mI.mjs";
3
+ import { joinURL, withTrailingSlash, withoutLeadingSlash } from "ufo";
4
+ import { DEVFRAME_CONNECTION_META_FILENAME, DEVFRAME_DOCK_IMPORTS_FILENAME, DEVFRAME_MCP_ROUTE, DEVFRAME_WS_ROUTE } from "devframe/constants";
5
+ import { createH3DevframeHost, createInstanceShell, resolveInstanceRegister } from "devframe/internal";
6
+ import { resolve } from "pathe";
7
+ import process from "node:process";
8
+ import { existsSync } from "node:fs";
9
+ import { readFile } from "node:fs/promises";
10
+ import { mountStaticHandler } from "devframe/utils/serve-static";
11
+ import { H3 } from "h3";
12
+ //#region src/node/initiate.ts
13
+ function normalizeDevframeEntry(entry) {
14
+ return "devframe" in entry ? entry : { devframe: entry };
15
+ }
16
+ /** Content-type for a UI asset key, inferred from its file extension. */
17
+ function assetContentType(key) {
18
+ if (key.endsWith(".json")) return "application/json; charset=utf-8";
19
+ if (key.endsWith(".js") || key.endsWith(".mjs")) return "text/javascript; charset=utf-8";
20
+ if (key.endsWith(".css")) return "text/css; charset=utf-8";
21
+ if (key.endsWith(".svg")) return "image/svg+xml";
22
+ if (key.endsWith(".html")) return "text/html; charset=utf-8";
23
+ return "application/octet-stream";
24
+ }
25
+ /** Reserved filenames directly under the hub base — a frame id can't shadow them. */
26
+ const RESERVED_HUB_PATHS = [
27
+ DEVFRAME_CONNECTION_META_FILENAME,
28
+ DEVFRAME_DOCK_IMPORTS_FILENAME,
29
+ DEVFRAME_WS_ROUTE,
30
+ DEVFRAME_MCP_ROUTE,
31
+ "__index.json",
32
+ "__renderers",
33
+ "embedded.js"
34
+ ];
35
+ /**
36
+ * Flatten the `devframes` input: await every thenable, call every factory,
37
+ * spread the arrays, and drop the empty slots — so a host can build the list
38
+ * conditionally (`isDev && loadInspect()`) without filtering it first.
39
+ */
40
+ function resolveDevframesInput(input) {
41
+ return Promise.all(input.map(async (entry) => {
42
+ const resolved = await (typeof entry === "function" ? entry() : entry);
43
+ return (Array.isArray(resolved) ? resolved : [resolved]).filter((slot) => slot != null).map(normalizeDevframeEntry);
44
+ })).then((arrays) => arrays.flat());
45
+ }
46
+ /**
47
+ * Validate the renderer-module registrations fail-fast: route-safe types
48
+ * (each becomes the `<base>__renderers/<type>.mjs` URL segment), one module
49
+ * per type, and an existing bundle file (renderer packages are prebuilt).
50
+ */
51
+ function resolveRendererRegistrations(registrations) {
52
+ const seen = /* @__PURE__ */ new Set();
53
+ return registrations.map((registration) => {
54
+ if (!/^[\w.-]+$/.test(registration.type)) throw diagnostics.DF8110({ type: registration.type });
55
+ if (seen.has(registration.type)) throw diagnostics.DF8108({ type: registration.type });
56
+ seen.add(registration.type);
57
+ const file = resolve(registration.file);
58
+ if (!existsSync(file)) throw diagnostics.DF8109({
59
+ type: registration.type,
60
+ file
61
+ });
62
+ return {
63
+ ...registration,
64
+ file
65
+ };
66
+ });
67
+ }
68
+ /**
69
+ * Render the dock client-script import map as an ES module — one dynamic
70
+ * import thunk per dock that carries a client script (`clientScript` on
71
+ * iframe docks, `action`, `renderer`). External viewers import this module
72
+ * from `<base>__client-imports.js` to load per-dock client code into the
73
+ * host page; `importFrom` values must be URL paths the host serves.
74
+ */
75
+ function renderClientImportsModule(ctx) {
76
+ const entries = [];
77
+ for (const [id, view] of ctx.docks.views) {
78
+ const scripts = [];
79
+ const anyView = view;
80
+ if (anyView.clientScript) scripts.push(anyView.clientScript);
81
+ if (anyView.action) scripts.push(anyView.action);
82
+ if (anyView.renderer) scripts.push(anyView.renderer);
83
+ if (scripts.length === 0) continue;
84
+ const thunks = scripts.map((script) => `() => import(${JSON.stringify(script.importFrom)})`);
85
+ entries.push(` ${JSON.stringify(id)}: [${thunks.join(", ")}],`);
86
+ }
87
+ return `// Generated by @devframes/hub — dock client-script import map.\nexport const clientImports = {\n${entries.join("\n")}\n}\nexport default clientImports\n`;
88
+ }
89
+ /**
90
+ * Initiate a hub instance — the whole multi-devframe devtools surface
91
+ * behind one framework-agnostic, web-standard handler. Every mounted
92
+ * devframe shares one context (merged RPC registry, shared state, docks /
93
+ * terminals / messages / commands), one WebSocket transport, and one Auth;
94
+ * the instance serves each frame's SPA at `<base><id>/`, the discovery
95
+ * endpoints (`__connection.json`, `__index.json`, `__client-imports.js`),
96
+ * the aggregate MCP route, and whatever the {@link DevframeHubUi} slot
97
+ * provides — the hub itself stays headless.
98
+ *
99
+ * The factory is synchronous and initializes eagerly, and binds no port of
100
+ * its own: the WebSocket follows `server` / `ws` (see
101
+ * {@link InitHubOptions.ws}), or waits for the host to hand upgrades over
102
+ * through {@link HubInstance.attach}.
103
+ */
104
+ function initHub(options) {
105
+ const base = normalizeHubBase(options.base);
106
+ const baseNoSlash = base.slice(0, -1);
107
+ const app = new H3();
108
+ const cwd = options.cwd ?? process.cwd();
109
+ const frames = [];
110
+ const rendererRegistrations = resolveRendererRegistrations(options.renderers ?? []);
111
+ const shell = createInstanceShell({
112
+ base,
113
+ app,
114
+ host: options.host,
115
+ origin: options.origin,
116
+ auth: options.auth,
117
+ server: options.server,
118
+ ws: options.ws,
119
+ sse: options.sse,
120
+ allowedOrigins: options.allowedOrigins,
121
+ destroyUnmatchedUpgrades: options.destroyUnmatchedUpgrades,
122
+ register: resolveInstanceRegister(options.register, {
123
+ id: options.name ?? "devframes-hub",
124
+ ...options.name !== void 0 ? { name: options.name } : {},
125
+ rootDir: cwd
126
+ }),
127
+ absoluteWsPath: true,
128
+ resolveSidecarPort: async (sidecarHost) => {
129
+ const { getPort } = await import("../dist-R0T2g9Vw.mjs");
130
+ return getPort({
131
+ port: 9777,
132
+ portRange: [9777, 9877],
133
+ host: sidecarHost
134
+ });
135
+ },
136
+ onMetaUnavailable: () => {
137
+ throw diagnostics.DF8003();
138
+ },
139
+ async init(api) {
140
+ if (options.context && options.devframes?.length) throw diagnostics.DF8002();
141
+ let ctx;
142
+ if (options.context) ctx = options.context;
143
+ else {
144
+ const host = {
145
+ ...createH3DevframeHost({
146
+ origin: () => api.origin() ?? "http://localhost",
147
+ appName: "devframes",
148
+ workspaceRoot: cwd,
149
+ mount: (mountBase, dir) => {
150
+ mountStaticHandler(app, mountBase, dir);
151
+ }
152
+ }),
153
+ ...options.getStorageDir ? { getStorageDir: options.getStorageDir } : {},
154
+ mountConnectionMeta: (frameBase) => {
155
+ app.use(joinURL(frameBase, DEVFRAME_CONNECTION_META_FILENAME), () => api.connectionMeta());
156
+ }
157
+ };
158
+ ctx = await createHubContext({
159
+ cwd,
160
+ workspaceRoot: cwd,
161
+ mode: "dev",
162
+ host,
163
+ ...options.rpcDeclarations ? { builtinRpcDeclarations: options.rpcDeclarations } : {}
164
+ });
165
+ }
166
+ const devframes = await resolveDevframesInput(options.devframes ?? []);
167
+ for (const { devframe: def, dock } of devframes) {
168
+ if (RESERVED_HUB_PATHS.includes(def.id)) throw diagnostics.DF8000({ id: def.id });
169
+ if (!/^[\w.-]+$/.test(def.id)) throw diagnostics.DF8004({ id: def.id });
170
+ const frameBase = withTrailingSlash(joinURL(base, def.id));
171
+ await ctx.install(def, {
172
+ base: frameBase,
173
+ ...dock ? { dock } : {}
174
+ });
175
+ frames.push({
176
+ id: def.id,
177
+ base: frameBase,
178
+ title: def.name
179
+ });
180
+ }
181
+ await options.configure?.(ctx);
182
+ await options.ui?.setup?.(ctx);
183
+ if (rendererRegistrations.length > 0) {
184
+ const manifest = {};
185
+ for (const registration of rendererRegistrations) manifest[registration.type] = {
186
+ importFrom: joinURL(base, "__renderers", `${registration.type}.mjs`),
187
+ ...registration.importName ? { importName: registration.importName } : {}
188
+ };
189
+ (await ctx.rpc.sharedState.get(DOCK_RENDERERS_STATE_KEY, { initialValue: {} })).mutate(() => manifest);
190
+ }
191
+ const mcpConfig = options.mcp === true ? {} : options.mcp;
192
+ if (!mcpConfig) return { context: ctx };
193
+ const mcpRoute = withoutLeadingSlash(mcpConfig.path ?? DEVFRAME_MCP_ROUTE);
194
+ const { mountMcpHttp } = await import("devframe/adapters/mcp");
195
+ const mounted = mountMcpHttp(app, ctx, joinURL(base, mcpRoute), {
196
+ serverName: options.name ?? "devframes-hub",
197
+ serverVersion: options.version ?? "0.0.0",
198
+ exposeSharedState: true,
199
+ allowedOrigins: mcpConfig.allowedOrigins
200
+ });
201
+ return {
202
+ context: ctx,
203
+ mcp: { path: mcpRoute },
204
+ dispose: mounted.dispose
205
+ };
206
+ },
207
+ mount(ctx, meta) {
208
+ app.use(joinURL(base, DEVFRAME_CONNECTION_META_FILENAME), () => meta);
209
+ const indexDocument = () => ({
210
+ name: options.name,
211
+ version: options.version,
212
+ base,
213
+ frames,
214
+ endpoints: {
215
+ connection: DEVFRAME_CONNECTION_META_FILENAME,
216
+ clientImports: DEVFRAME_DOCK_IMPORTS_FILENAME,
217
+ index: "__index.json",
218
+ websocket: meta.websocket,
219
+ ...meta.mcp ? { mcp: meta.mcp.path } : {},
220
+ ...options.ui?.embedded ? { embedded: "embedded.js" } : {}
221
+ }
222
+ });
223
+ app.use(joinURL(base, "__index.json"), () => indexDocument());
224
+ app.use(joinURL(base, DEVFRAME_DOCK_IMPORTS_FILENAME), (event) => {
225
+ event.res.headers.set("Content-Type", "text/javascript; charset=utf-8");
226
+ event.res.headers.set("Cache-Control", "no-store");
227
+ return renderClientImportsModule(ctx);
228
+ });
229
+ if (options.ui?.embedded) {
230
+ const entry = resolve(options.ui.embedded.entry);
231
+ app.use(joinURL(base, "embedded.js"), async (event) => {
232
+ event.res.headers.set("Content-Type", "text/javascript; charset=utf-8");
233
+ event.res.headers.set("Cache-Control", "no-store");
234
+ return await readFile(entry);
235
+ });
236
+ }
237
+ for (const registration of rendererRegistrations) app.use(joinURL(base, "__renderers", `${registration.type}.mjs`), async (event) => {
238
+ event.res.headers.set("Content-Type", "text/javascript; charset=utf-8");
239
+ event.res.headers.set("Cache-Control", "no-store");
240
+ return await readFile(registration.file);
241
+ });
242
+ for (const [key, produce] of Object.entries(options.ui?.assets ?? {})) app.use(joinURL(base, key), (event) => {
243
+ event.res.headers.set("Content-Type", assetContentType(key));
244
+ event.res.headers.set("Cache-Control", "no-store");
245
+ return produce();
246
+ });
247
+ if (options.ui?.viewer) mountStaticHandler(app, base, resolve(options.ui.viewer.distDir));
248
+ else {
249
+ app.use(baseNoSlash, () => indexDocument());
250
+ app.use(base, () => indexDocument());
251
+ }
252
+ }
253
+ });
254
+ return {
255
+ base: shell.base,
256
+ handler: shell.handler,
257
+ nodeMiddleware: shell.nodeMiddleware,
258
+ attach: shell.attach,
259
+ handleUpgrade: shell.handleUpgrade,
260
+ ready: shell.ready,
261
+ context: shell.context,
262
+ connectionMeta: shell.connectionMeta,
263
+ close: shell.close
264
+ };
265
+ }
266
+ //#endregion
267
+ export { DEVFRAMES_HUB_BASE, initHub };
@@ -1,37 +1,5 @@
1
- import { StandardSchemaV1 } from "@standard-schema/spec";
2
1
  import { ConnectionMeta, EventEmitter } from "devframe/types";
3
- //#region src/types/json-render.d.ts
4
- /** @deprecated Use `DevframeJsonRenderSpec`'s element shape from `@devframes/json-render` instead. Removed in 0.8. */
5
- interface JsonRenderElement {
6
- type: string;
7
- props?: Record<string, unknown>;
8
- children?: string[];
9
- /** json-render event bindings (e.g. `{ press: { action: "my:action" } }`) */
10
- on?: Record<string, unknown>;
11
- /** json-render visibility condition */
12
- visible?: unknown;
13
- /** json-render repeat binding */
14
- repeat?: unknown;
15
- /** Allow additional json-render element fields */
16
- [key: string]: unknown;
17
- }
18
- /** @deprecated Use `DevframeJsonRenderSpec` from `@devframes/json-render` instead. Removed in 0.8. */
19
- interface JsonRenderSpec {
20
- root: string;
21
- elements: Record<string, JsonRenderElement>;
22
- /** Initial client-side state model for $state/$bindState expressions */
23
- state?: Record<string, unknown>;
24
- }
25
- /** @deprecated Use `JsonRenderView` from `@devframes/json-render` instead. Removed in 0.8. */
26
- interface JsonRenderer {
27
- /** Replace the entire spec */
28
- updateSpec: (spec: JsonRenderSpec) => void | Promise<void>;
29
- /** Update json-render state values (shallow merge into spec.state) */
30
- updateState: (state: Record<string, unknown>) => void | Promise<void>;
31
- /** Internal: shared state key used by the client to subscribe */
32
- readonly _stateKey: string;
33
- }
34
- //#endregion
2
+ import { StandardSchemaV1 } from "@standard-schema/spec";
35
3
  //#region src/types/docks.d.ts
36
4
  interface DevframeDocksHost {
37
5
  readonly views: Map<string, DevframeDockUserEntry>;
@@ -82,6 +50,8 @@ type DevframeDockEntryIcon = string | {
82
50
  light: string;
83
51
  dark: string;
84
52
  };
53
+ /** Color for a dock-bar entry's {@link DevframeDockEntryBase.badge} — mirrors json-render's `TabDescriptor.badgeVariant`. */
54
+ type DevframeDockBadgeVariant = 'default' | 'info' | 'success' | 'warning' | 'danger';
85
55
  interface DevframeDockEntryBase {
86
56
  id: string;
87
57
  title: string;
@@ -146,6 +116,11 @@ interface DevframeDockEntryBase {
146
116
  * Badge text to display on the dock icon (e.g., unread count)
147
117
  */
148
118
  badge?: string;
119
+ /**
120
+ * Colors {@link badge}. Omitted (or `'default'`) keeps the existing neutral
121
+ * fill, so every pre-existing badge consumer keeps its current look.
122
+ */
123
+ badgeVariant?: DevframeDockBadgeVariant;
149
124
  /**
150
125
  * Id of the group this entry belongs to. When set, hosts collapse this entry
151
126
  * under the matching group's button instead of showing it directly on the
@@ -357,20 +332,6 @@ interface DevframeViewBuiltin extends DevframeDockEntryBase {
357
332
  type: '~builtin';
358
333
  id: string;
359
334
  }
360
- /**
361
- * @deprecated json-render moved out of the hub into the opt-in
362
- * `@devframes/json-render` integration in 0.7, which contributes its own
363
- * `'json-render'` entry (carrying a serializable view ref, not a live
364
- * `JsonRenderer` handle) to {@link DevframeDockEntryRegistry} via declaration
365
- * merging. This type is kept for compatibility but is no longer a member of
366
- * {@link DevframeDockUserEntry} — use `@devframes/json-render/hub` instead.
367
- * Removed in 0.8.
368
- */
369
- interface DevframeViewJsonRender extends DevframeDockEntryBase {
370
- type: 'json-render';
371
- /** JsonRenderer handle created by the deprecated ctx.createJsonRenderer() */
372
- ui: JsonRenderer;
373
- }
374
335
  /**
375
336
  * A dock group: a single dock-bar button that collapses every entry whose
376
337
  * {@link DevframeDockEntryBase.groupId} matches this group's `id`.
@@ -506,9 +467,6 @@ interface DevframeCommandBase {
506
467
  * `when` clauses are evaluated client-side only and are **not** enforced for
507
468
  * agent calls — opt in a `when`-gated command only if running it outside its
508
469
  * UI context is safe.
509
- *
510
- * @experimental The agent-native surface is experimental and may change
511
- * without a major version bump until it stabilizes.
512
470
  */
513
471
  interface DevframeCommandAgentOptions {
514
472
  /**
@@ -545,8 +503,6 @@ interface DevframeServerCommandInput extends DevframeCommandBase {
545
503
  /**
546
504
  * Opt this command in to the agent surface (`ctx.agent` → MCP). Requires a
547
505
  * `handler`. See {@link DevframeCommandAgentOptions}.
548
- *
549
- * @experimental
550
506
  */
551
507
  agent?: DevframeCommandAgentOptions;
552
508
  /**
@@ -619,14 +575,35 @@ interface DevframeCommandShortcutOverrides {
619
575
  }
620
576
  //#endregion
621
577
  //#region src/types/settings.d.ts
578
+ /**
579
+ * Persisted per-user dock + command settings, synced over the
580
+ * `devframe:user-settings` shared state. The generic base carries the hub's
581
+ * own dock-registry and command model that every viewer shares; a viewer
582
+ * augments it with its own reference-UI toggles via declaration merging:
583
+ *
584
+ * ```ts
585
+ * declare module '@devframes/hub/types' {
586
+ * interface DevframeDocksUserSettings {
587
+ * myViewerToggle?: boolean
588
+ * }
589
+ * }
590
+ * ```
591
+ *
592
+ * Augmented fields are optional so the hub's `DEFAULT_STATE_USER_SETTINGS()`
593
+ * (which knows nothing about them) stays assignable — an absent value reads
594
+ * as the viewer's documented default.
595
+ */
622
596
  interface DevframeDocksUserSettings {
597
+ /** Ids of dock entries the user has hidden from the bar. */
623
598
  docksHidden: string[];
599
+ /** Category ids the user has collapsed/hidden on the bar. */
624
600
  docksCategoriesHidden: string[];
601
+ /** Ids of dock entries the user has pinned. */
625
602
  docksPinned: string[];
603
+ /** Per-entry sort weight overriding the registry's default order. */
626
604
  docksCustomOrder: Record<string, number>;
627
- showIframeAddressBar: boolean;
628
- closeOnOutsideClick: boolean;
605
+ /** Per-command keybinding overrides (empty array = shortcut disabled). */
629
606
  commandShortcuts: DevframeCommandShortcutOverrides;
630
607
  }
631
608
  //#endregion
632
- export { DevframeViewLauncher as A, DevframeDocksHost as C, DevframeViewGroup as D, DevframeViewCustomRender as E, RemoteDockOptions as F, JsonRenderElement as I, JsonRenderSpec as L, FrameSubTabsConfig as M, NavTarget as N, DevframeViewIframe as O, RemoteConnectionInfo as P, JsonRenderer as R, DevframeDocksActiveState as S, DevframeViewBuiltin as T, DevframeDockEntryBase as _, DevframeCommandEntry as a, DevframeDockEntryRegistry as b, DevframeCommandShortcutOverrides as c, DevframeServerCommandEntry as d, DevframeServerCommandInput as f, DevframeDockEntry as g, DevframeDockEntriesGrouped as h, DevframeCommandBase as i, DevframeViewLauncherStatus as j, DevframeViewJsonRender as k, DevframeCommandsHost as l, DevframeDockActivation as m, DevframeClientCommand as n, DevframeCommandHandle as o, ClientScriptEntry as p, DevframeCommandAgentOptions as r, DevframeCommandKeybinding as s, DevframeDocksUserSettings as t, DevframeCommandsHostEvents as u, DevframeDockEntryCategory as v, DevframeViewAction as w, DevframeDockUserEntry as x, DevframeDockEntryIcon as y };
609
+ export { DevframeViewLauncher as A, DevframeDocksActiveState as C, DevframeViewCustomRender as D, DevframeViewBuiltin as E, RemoteDockOptions as F, FrameSubTabsConfig as M, NavTarget as N, DevframeViewGroup as O, RemoteConnectionInfo as P, DevframeDockUserEntry as S, DevframeViewAction as T, DevframeDockEntry as _, DevframeCommandEntry as a, DevframeDockEntryIcon as b, DevframeCommandShortcutOverrides as c, DevframeServerCommandEntry as d, DevframeServerCommandInput as f, DevframeDockEntriesGrouped as g, DevframeDockBadgeVariant as h, DevframeCommandBase as i, DevframeViewLauncherStatus as j, DevframeViewIframe as k, DevframeCommandsHost as l, DevframeDockActivation as m, DevframeClientCommand as n, DevframeCommandHandle as o, ClientScriptEntry as p, DevframeCommandAgentOptions as r, DevframeCommandKeybinding as s, DevframeDocksUserSettings as t, DevframeCommandsHostEvents as u, DevframeDockEntryBase as v, DevframeDocksHost as w, DevframeDockEntryRegistry as x, DevframeDockEntryCategory as y };