@devframes/hub 0.9.9 → 0.9.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,103 @@
1
+ import { a as resolveClientModuleSpecifier } from "./remote-url-UClKLQ5_.mjs";
2
+ import { b as diagnostics, v as prepareDevframe, y as skippedInStaticBuild } from "./context-BWaW6N53.mjs";
3
+ import { joinURL, withTrailingSlash } from "ufo";
4
+ import { DEVFRAME_CONNECTION_META_FILENAME, DEVFRAME_DOCK_IMPORTS_FILENAME, DEVFRAME_MCP_ROUTE, DEVFRAME_WS_ROUTE } from "devframe/constants";
5
+ import { existsSync } from "node:fs";
6
+ import { resolve } from "pathe";
7
+ //#region src/node/assemble.ts
8
+ /** Reserved filenames directly under the hub base; a frame id can't shadow them. */
9
+ const RESERVED_HUB_PATHS = [
10
+ DEVFRAME_CONNECTION_META_FILENAME,
11
+ DEVFRAME_DOCK_IMPORTS_FILENAME,
12
+ DEVFRAME_WS_ROUTE,
13
+ DEVFRAME_MCP_ROUTE,
14
+ "__index.json",
15
+ "__renderers",
16
+ "embedded.js"
17
+ ];
18
+ function normalizeDevframeEntry(entry) {
19
+ return "devframe" in entry ? entry : { devframe: entry };
20
+ }
21
+ /**
22
+ * Flatten the `devframes` input: await every thenable, call every factory,
23
+ * spread the arrays, and drop the empty slots, so a host can build the list
24
+ * conditionally (`isDev && loadInspect()`) without filtering it first.
25
+ */
26
+ function resolveDevframesInput(input) {
27
+ return Promise.all(input.map(async (entry) => {
28
+ const resolved = await (typeof entry === "function" ? entry() : entry);
29
+ return (Array.isArray(resolved) ? resolved : [resolved]).filter((slot) => slot != null).map(normalizeDevframeEntry);
30
+ })).then((arrays) => arrays.flat());
31
+ }
32
+ /**
33
+ * Validate the renderer-module registrations fail-fast: route-safe types
34
+ * (each becomes the `<base>__renderers/<type>.mjs` URL segment), one module
35
+ * per type, and an existing bundle file (renderer packages are prebuilt).
36
+ */
37
+ function resolveRendererRegistrations(registrations) {
38
+ const seen = /* @__PURE__ */ new Set();
39
+ return registrations.map((registration) => {
40
+ if (!/^[\w.-]+$/.test(registration.type)) throw diagnostics.DF8110({ type: registration.type });
41
+ if (seen.has(registration.type)) throw diagnostics.DF8108({ type: registration.type });
42
+ seen.add(registration.type);
43
+ const file = resolve(registration.file);
44
+ if (!existsSync(file)) throw diagnostics.DF8109({
45
+ type: registration.type,
46
+ file
47
+ });
48
+ return {
49
+ ...registration,
50
+ file
51
+ };
52
+ });
53
+ }
54
+ /**
55
+ * Render the dock client-script import map as an ES module: one dynamic
56
+ * import thunk per dock that carries a client script (`clientScript` on
57
+ * iframe docks, `action`, `renderer`). External viewers import this module
58
+ * from `<base>__client-imports.js` to load per-dock client code into the
59
+ * host page; `importFrom` values are URL paths the host serves, or bare npm
60
+ * specifiers resolved through the host's `clientModuleResolution` template.
61
+ */
62
+ function renderClientImportsModule(ctx) {
63
+ const template = ctx.staticConfig.dock?.clientModuleResolution;
64
+ const entries = [];
65
+ for (const [id, view] of ctx.docks.views) {
66
+ const scripts = [];
67
+ const anyView = view;
68
+ if (anyView.clientScript) scripts.push(anyView.clientScript);
69
+ if (anyView.action) scripts.push(anyView.action);
70
+ if (anyView.renderer) scripts.push(anyView.renderer);
71
+ if (scripts.length === 0) continue;
72
+ const thunks = scripts.map((script) => `() => import(${JSON.stringify(resolveClientModuleSpecifier(script.importFrom, { template }))})`);
73
+ entries.push(` ${JSON.stringify(id)}: [${thunks.join(", ")}],`);
74
+ }
75
+ return `// Generated by @devframes/hub: dock client-script import map.\nexport const clientImports = {\n${entries.join("\n")}\n}\nexport default clientImports\n`;
76
+ }
77
+ /**
78
+ * Pass 1: mount each devframe under `<base><id>/` (SPA, meta, iframe dock)
79
+ * and queue its declared services, guarding the id against reserved hub
80
+ * filenames and route-pattern characters. Returns the deferred setup thunks.
81
+ */
82
+ async function mountDevframes(ctx, devframes, base, frames, hubMcpEnabled) {
83
+ const setups = [];
84
+ for (const { devframe: def, dock } of devframes) {
85
+ if (RESERVED_HUB_PATHS.includes(def.id)) throw diagnostics.DF8000({ id: def.id });
86
+ if (!/^[\w.-]+$/.test(def.id)) throw diagnostics.DF8004({ id: def.id });
87
+ if (!hubMcpEnabled && def.cli?.mcp) diagnostics.DF8005({ id: def.id });
88
+ const frameBase = withTrailingSlash(joinURL(base, def.id));
89
+ const run = await prepareDevframe(ctx, def, {
90
+ base: frameBase,
91
+ ...dock ? { dock } : {}
92
+ });
93
+ if (run) setups.push(run);
94
+ if (!skippedInStaticBuild(ctx, def)) frames.push({
95
+ id: def.id,
96
+ base: frameBase,
97
+ title: def.name
98
+ });
99
+ }
100
+ return setups;
101
+ }
102
+ //#endregion
103
+ export { resolveRendererRegistrations as i, renderClientImportsModule as n, resolveDevframesInput as r, mountDevframes as t };
@@ -1,4 +1,4 @@
1
- import { i as DOCK_RENDERERS_STATE_KEY, n as DEFAULT_STATE_USER_SETTINGS, o as HUB_EVENTS, t as DEFAULT_CATEGORIES_ORDER } from "../constants-BUd6oMci.mjs";
1
+ import { i as DOCK_RENDERERS_STATE_KEY, n as DEFAULT_STATE_USER_SETTINGS, o as HUB_EVENTS, t as DEFAULT_CATEGORIES_ORDER } from "../constants-Da23JxCZ.mjs";
2
2
  import { a as resolveClientModuleSpecifier, n as stripRemoteConnectionFromUrl, r as clientScriptFailureHint, t as buildRemoteConnectionUrl } from "../remote-url-UClKLQ5_.mjs";
3
3
  import { REMOTE_CONNECTION_KEY } from "devframe/constants";
4
4
  import { connectDevframe, getDevframeRpcClient, resolveWsUrl } from "devframe/client";
@@ -362,6 +362,7 @@ function serializeNavQuery(query) {
362
362
  * dock client script reports into the very feed the server writes to.
363
363
  */
364
364
  function createMessagesClient(rpc, options = {}) {
365
+ if (rpc.connectionMeta?.backend === "static") return createLocalMessagesClient(options);
365
366
  const { call } = rpc;
366
367
  function makeHandle(entry) {
367
368
  let current = entry;
@@ -404,6 +405,58 @@ function createMessagesClient(rpc, options = {}) {
404
405
  debug: levelShortcut("debug")
405
406
  };
406
407
  }
408
+ /** In-page-only writes for a static backend: same surface, no wire. */
409
+ function createLocalMessagesClient(options) {
410
+ let nextId = 0;
411
+ function makeEntry(input) {
412
+ return {
413
+ timestamp: Date.now(),
414
+ ...options.defaults,
415
+ ...input,
416
+ id: input.id ?? `local-${++nextId}`,
417
+ from: "browser"
418
+ };
419
+ }
420
+ function makeHandle(entry) {
421
+ let current = entry;
422
+ return {
423
+ get entry() {
424
+ return current;
425
+ },
426
+ get id() {
427
+ return current.id;
428
+ },
429
+ async update(patch) {
430
+ current = {
431
+ ...current,
432
+ ...patch,
433
+ id: current.id,
434
+ from: current.from
435
+ };
436
+ return current;
437
+ },
438
+ dismiss: async () => {}
439
+ };
440
+ }
441
+ const add = async (input) => makeHandle(makeEntry(input));
442
+ function levelShortcut(level) {
443
+ return (message, extra) => add({
444
+ ...extra,
445
+ message,
446
+ level
447
+ });
448
+ }
449
+ return {
450
+ add,
451
+ remove: async () => {},
452
+ clear: async () => {},
453
+ info: levelShortcut("info"),
454
+ warn: levelShortcut("warn"),
455
+ error: levelShortcut("error"),
456
+ success: levelShortcut("success"),
457
+ debug: levelShortcut("debug")
458
+ };
459
+ }
407
460
  //#endregion
408
461
  //#region src/client/renderers.ts
409
462
  /**
@@ -576,6 +629,11 @@ async function createDevframeClientRuntime(options = {}) {
576
629
  type: "action",
577
630
  handler: (activation) => activateHandler(activation)
578
631
  });
632
+ if (rpc.connectionMeta?.backend === "static" && typeof BroadcastChannel !== "undefined") {
633
+ const activateChannel = new BroadcastChannel(HUB_EVENTS.broadcastChannel.docksActivate);
634
+ activateChannel.onmessage = (event) => activateHandler(event.data);
635
+ disposers.push(() => activateChannel.close());
636
+ }
579
637
  if (getDevframeClientContext()) console.warn("[@devframes/hub] A client context is already published on this page; replacing it. Boot createDevframeClientRuntime() once per page (e.g. HTML injection combined with a manual import boots it twice).");
580
638
  setDevframeClientContext(context);
581
639
  const loadedScripts = /* @__PURE__ */ new Set();
@@ -72,6 +72,15 @@ declare const HUB_EVENTS: {
72
72
  readonly postMessage: {
73
73
  readonly frameNav: "devframe:frame-nav";
74
74
  };
75
+ /**
76
+ * Same-origin `BroadcastChannel` names, `devframe:` prefix. Used on a
77
+ * `static` backend, where no live server can relay a client's request to
78
+ * its sibling browsing contexts (e.g. a panel iframe steering the host
79
+ * page's dock selection).
80
+ */
81
+ readonly broadcastChannel: {
82
+ readonly docksActivate: "devframe:docks:activate";
83
+ };
75
84
  };
76
85
  //#endregion
77
86
  //#region src/constants.d.ts
@@ -65,7 +65,14 @@ const HUB_EVENTS = {
65
65
  /** Streaming channel ids (server → client), `devframe:` prefix. */
66
66
  stream: { terminals: "devframe:terminals" },
67
67
  /** `postMessage` channels for host ↔ iframe protocols, `devframe:` prefix. */
68
- postMessage: { frameNav: "devframe:frame-nav" }
68
+ postMessage: { frameNav: "devframe:frame-nav" },
69
+ /**
70
+ * Same-origin `BroadcastChannel` names, `devframe:` prefix. Used on a
71
+ * `static` backend, where no live server can relay a client's request to
72
+ * its sibling browsing contexts (e.g. a panel iframe steering the host
73
+ * page's dock selection).
74
+ */
75
+ broadcastChannel: { docksActivate: "devframe:docks:activate" }
69
76
  };
70
77
  //#endregion
71
78
  //#region src/constants.ts
@@ -1,3 +1,3 @@
1
- import { a as normalizeHubBase, i as DOCK_RENDERERS_STATE_KEY, n as DEFAULT_STATE_USER_SETTINGS, o as HUB_EVENTS, r as DEVFRAMES_HUB_BASE, t as DEFAULT_CATEGORIES_ORDER } from "./constants-Coo1p3ja.mjs";
1
+ import { a as normalizeHubBase, i as DOCK_RENDERERS_STATE_KEY, n as DEFAULT_STATE_USER_SETTINGS, o as HUB_EVENTS, r as DEVFRAMES_HUB_BASE, t as DEFAULT_CATEGORIES_ORDER } from "./constants-Bz4yclHo.mjs";
2
2
  export * from "devframe/constants";
3
3
  export { DEFAULT_CATEGORIES_ORDER, DEFAULT_STATE_USER_SETTINGS, DEVFRAMES_HUB_BASE, DOCK_RENDERERS_STATE_KEY, HUB_EVENTS, normalizeHubBase };
@@ -1,3 +1,3 @@
1
- import { a as normalizeHubBase, i as DOCK_RENDERERS_STATE_KEY, n as DEFAULT_STATE_USER_SETTINGS, o as HUB_EVENTS, r as DEVFRAMES_HUB_BASE, t as DEFAULT_CATEGORIES_ORDER } from "./constants-BUd6oMci.mjs";
1
+ import { a as normalizeHubBase, i as DOCK_RENDERERS_STATE_KEY, n as DEFAULT_STATE_USER_SETTINGS, o as HUB_EVENTS, r as DEVFRAMES_HUB_BASE, t as DEFAULT_CATEGORIES_ORDER } from "./constants-Da23JxCZ.mjs";
2
2
  export * from "devframe/constants";
3
3
  export { DEFAULT_CATEGORIES_ORDER, DEFAULT_STATE_USER_SETTINGS, DEVFRAMES_HUB_BASE, DOCK_RENDERERS_STATE_KEY, HUB_EVENTS, normalizeHubBase };
@@ -1,17 +1,17 @@
1
1
  import { r as defineHubRpcFunction } from "./define-Ceekw2EO.mjs";
2
- import { n as DEFAULT_STATE_USER_SETTINGS, o as HUB_EVENTS } from "./constants-BUd6oMci.mjs";
2
+ import { n as DEFAULT_STATE_USER_SETTINGS, o as HUB_EVENTS } from "./constants-Da23JxCZ.mjs";
3
3
  import { i as isBareModuleSpecifier, t as buildRemoteConnectionUrl } from "./remote-url-UClKLQ5_.mjs";
4
4
  import { joinURL, withTrailingSlash } from "ufo";
5
5
  import { createEventEmitter } from "devframe/utils/events";
6
- import { createHostContext, createStorage } from "devframe/node";
7
- import { getInternalContext, resolveBasePath } from "devframe/node/hub-internals";
8
- import { debounce } from "perfect-debounce";
6
+ import { existsSync } from "node:fs";
7
+ import process from "node:process";
9
8
  import { coerceAgentPositionalArgs, resolveClientAssets } from "devframe/internal";
10
- import { defineDiagnostics } from "devframe/utils/nostics";
11
9
  import { basename, dirname, isAbsolute, join, resolve } from "pathe";
10
+ import { defineDiagnostics } from "devframe/utils/nostics";
11
+ import { getInternalContext, resolveBasePath } from "devframe/node/hub-internals";
12
+ import { createHostContext, createStorage } from "devframe/node";
13
+ import { debounce } from "perfect-debounce";
12
14
  import { nanoid } from "devframe/utils/nanoid";
13
- import process from "node:process";
14
- import { existsSync } from "node:fs";
15
15
  //#region src/node/diagnostics.ts
16
16
  /**
17
17
  * Hub-side diagnostics for docks, terminals, messages, and commands.
@@ -44,8 +44,12 @@ const diagnostics = defineDiagnostics({
44
44
  fix: "Ids become route segments, so they may only contain letters, digits, `_`, `-`, and `.`; `:` and `*` are route-pattern markers to the underlying router, and `/` would escape the segment. Set a route-safe `id` on the definition (e.g. `my_plugin` instead of `my:plugin`)."
45
45
  },
46
46
  DF8005: {
47
- why: (p) => `Devframe "${p.id}" requests an MCP route, but the hub's aggregate MCP is off, so its tools are not exposed over MCP.`,
48
- fix: "A hub exposes one aggregate MCP endpoint over every mounted devframe, so per-devframe `mcp` settings are ignored. Enable the hub's own MCP (pass `mcp` to `initHub`) to surface this devframe's tools, or drop `mcp` from the devframe to silence this warning."
47
+ why: (p) => `Devframe "${p.id}" requests an MCP route, but the hub's aggregate MCP is off (\`mcp: false\`), so its tools are not exposed over MCP.`,
48
+ fix: "A hub exposes one aggregate MCP endpoint over every mounted devframe, so per-devframe `mcp` settings are ignored. Drop `mcp: false` from `initHub` (the `'auto'` default mounts the aggregate route once agent tools exist) to surface this devframe's tools, or drop `mcp` from the devframe to silence this warning."
49
+ },
50
+ DF8006: {
51
+ why: (p) => `A static hub build can only write mounts under its own base: "${p.urlBase}" escapes "${p.base}".`,
52
+ fix: "buildHub maps each mount base to a directory under its `outDir`, so every mount must live under the hub base. Drop the `basePath` override (or the `ctx.install` base) that points outside it, or move the hub `base` up so it contains the mount."
49
53
  },
50
54
  DF8100: {
51
55
  why: (p) => `Dock with id "${p.id}" is already registered`,
@@ -128,6 +132,134 @@ const diagnostics = defineDiagnostics({
128
132
  }
129
133
  });
130
134
  //#endregion
135
+ //#region src/node/install-devframe.ts
136
+ /**
137
+ * Find the next free dock id derived from `baseId`. Returns `baseId`
138
+ * when it is unused, otherwise appends `-2`, `-3`, … until a free slot
139
+ * is found. Used by the `'duplicate'` strategy so co-existing instances
140
+ * never collide in the dock registry.
141
+ */
142
+ function nextAvailableDockId(views, baseId) {
143
+ if (!views.has(baseId)) return baseId;
144
+ let n = 2;
145
+ while (views.has(`${baseId}-${n}`)) n++;
146
+ return `${baseId}-${n}`;
147
+ }
148
+ /**
149
+ * When a dock's `clientScript.importFrom` is an absolute filesystem path, serve
150
+ * its directory under `<base>__page-script/` and rewrite `importFrom` to that
151
+ * URL. A URL or bare specifier (not existing on disk) passes through untouched.
152
+ */
153
+ async function resolvePageScriptClientScript(ctx, clientScript, base) {
154
+ if (!clientScript?.importFrom) return clientScript;
155
+ const { importFrom } = clientScript;
156
+ if (!isAbsolute(importFrom) || !existsSync(importFrom)) return clientScript;
157
+ const scriptBase = withTrailingSlash(joinURL(base, "__page-script"));
158
+ await ctx.host.mountStatic(scriptBase, dirname(importFrom));
159
+ return {
160
+ ...clientScript,
161
+ importFrom: joinURL(scriptBase, basename(importFrom))
162
+ };
163
+ }
164
+ /**
165
+ * Framework-neutral primitive backing {@link DevframeHubContext.install} -
166
+ * installs a {@link DevframeDefinition} as a dock inside a hub-aware context:
167
+ * serves the devframe's SPA at the resolved base path, synthesizes an iframe
168
+ * dock entry from the definition's metadata, and runs the definition's
169
+ * `setup(ctx)`. Reach for it through `ctx.install(devframe)` rather than
170
+ * calling it directly.
171
+ *
172
+ * Framework kits wrap `ctx.install` with their own plugin/middleware
173
+ * machinery, e.g. `@vitejs/devtools-kit`'s `createPluginFromDevframe`
174
+ * returns a Vite `Plugin` whose `devtools.setup` ultimately delegates here.
175
+ */
176
+ /**
177
+ * Phase one of an install: run the duplication guard, serve the SPA + meta,
178
+ * register the iframe dock, and queue the definition's declarative wire
179
+ * services, everything up to (but not including) `setup(ctx)`. Returns a
180
+ * deferred setup thunk, or `null` when the devframe was deduplicated.
181
+ *
182
+ * The hub's initial batch uses this to collect every devframe's services
183
+ * across the whole hub, `ready()` them once, and only then run the setups,
184
+ * so services are ready before any setup, and a plugin can consume a service
185
+ * another plugin declared regardless of mount order.
186
+ */
187
+ /**
188
+ * Serve a devframe's SPA (and the hub's connection meta) under `base`, if the
189
+ * definition ships client assets.
190
+ */
191
+ async function serveDevframeAssets(ctx, d, id, base) {
192
+ const clientAssets = resolveClientAssets(d);
193
+ if (!clientAssets) return;
194
+ if (ctx.host.mountConnectionMeta) await ctx.host.mountConnectionMeta(base);
195
+ else diagnostics.DF8106({
196
+ id,
197
+ name: d.name,
198
+ base
199
+ });
200
+ ctx.views.hostStatic(base, typeof clientAssets === "string" ? resolve(clientAssets) : clientAssets, d.importMetaUrl);
201
+ }
202
+ /**
203
+ * Whether a devframe stays out of a static hub build: `capabilities.build:
204
+ * false` declares its value inherently live (a terminal, a process proxy),
205
+ * so `buildHub` never mounts it, registers its dock, or bakes its RPCs.
206
+ */
207
+ function skippedInStaticBuild(ctx, d) {
208
+ return ctx.mode === "build" && d.capabilities?.build === false;
209
+ }
210
+ async function prepareDevframe(ctx, d, options = {}) {
211
+ if (skippedInStaticBuild(ctx, d)) return null;
212
+ const strategy = d.duplicationStrategy ?? "warn";
213
+ const isDuplicate = ctx.docks.views.has(d.id);
214
+ if (isDuplicate && strategy !== "duplicate") {
215
+ if (strategy === "throw") throw diagnostics.DF8105({
216
+ id: d.id,
217
+ name: d.name
218
+ });
219
+ if (strategy === "warn") diagnostics.DF8105({
220
+ id: d.id,
221
+ name: d.name
222
+ });
223
+ return null;
224
+ }
225
+ const id = isDuplicate ? nextAvailableDockId(ctx.docks.views, d.id) : d.id;
226
+ const base = options.base ?? (id === d.id ? resolveBasePath(d, "hosted") : resolveBasePath({
227
+ ...d,
228
+ id,
229
+ basePath: void 0
230
+ }, "hosted"));
231
+ const dockDefaults = {
232
+ ...d.dock,
233
+ ...options.dock
234
+ };
235
+ const clientScript = await resolvePageScriptClientScript(ctx, dockDefaults.clientScript, base);
236
+ if (clientScript) dockDefaults.clientScript = clientScript;
237
+ await serveDevframeAssets(ctx, d, id, base);
238
+ ctx.docks.register({
239
+ id,
240
+ title: d.name,
241
+ icon: d.icon,
242
+ ...dockDefaults,
243
+ type: "iframe",
244
+ url: base
245
+ });
246
+ for (const input of d.services ?? []) ctx.services.install(input, { resolveFrom: d.importMetaUrl });
247
+ return () => Promise.resolve(d.setup(ctx));
248
+ }
249
+ /**
250
+ * Install a {@link DevframeDefinition} into a hub in one call: serve its SPA,
251
+ * register its dock, ready its services, and run `setup(ctx)`. The imperative
252
+ * counterpart to the hub's declarative `devframes` list (which batches the
253
+ * phases via {@link prepareDevframe}); use it from `configure(ctx)` or
254
+ * wherever you hold the context to plug in an extra devframe after startup.
255
+ */
256
+ async function installDevframe(ctx, d, options = {}) {
257
+ const run = await prepareDevframe(ctx, d, options);
258
+ if (!run) return;
259
+ await ctx.services.ready();
260
+ await run();
261
+ }
262
+ //#endregion
131
263
  //#region src/node/host-commands.ts
132
264
  function findChildCommand(command, id) {
133
265
  for (const child of command.children ?? []) {
@@ -952,125 +1084,6 @@ var DevframeTerminalsHost = class {
952
1084
  }
953
1085
  };
954
1086
  //#endregion
955
- //#region src/node/install-devframe.ts
956
- /**
957
- * Find the next free dock id derived from `baseId`. Returns `baseId`
958
- * when it is unused, otherwise appends `-2`, `-3`, … until a free slot
959
- * is found. Used by the `'duplicate'` strategy so co-existing instances
960
- * never collide in the dock registry.
961
- */
962
- function nextAvailableDockId(views, baseId) {
963
- if (!views.has(baseId)) return baseId;
964
- let n = 2;
965
- while (views.has(`${baseId}-${n}`)) n++;
966
- return `${baseId}-${n}`;
967
- }
968
- /**
969
- * When a dock's `clientScript.importFrom` is an absolute filesystem path, serve
970
- * its directory under `<base>__page-script/` and rewrite `importFrom` to that
971
- * URL. A URL or bare specifier (not existing on disk) passes through untouched.
972
- */
973
- async function resolvePageScriptClientScript(ctx, clientScript, base) {
974
- if (!clientScript?.importFrom) return clientScript;
975
- const { importFrom } = clientScript;
976
- if (!isAbsolute(importFrom) || !existsSync(importFrom)) return clientScript;
977
- const scriptBase = withTrailingSlash(joinURL(base, "__page-script"));
978
- await ctx.host.mountStatic(scriptBase, dirname(importFrom));
979
- return {
980
- ...clientScript,
981
- importFrom: joinURL(scriptBase, basename(importFrom))
982
- };
983
- }
984
- /**
985
- * Framework-neutral primitive backing {@link DevframeHubContext.install} -
986
- * installs a {@link DevframeDefinition} as a dock inside a hub-aware context:
987
- * serves the devframe's SPA at the resolved base path, synthesizes an iframe
988
- * dock entry from the definition's metadata, and runs the definition's
989
- * `setup(ctx)`. Reach for it through `ctx.install(devframe)` rather than
990
- * calling it directly.
991
- *
992
- * Framework kits wrap `ctx.install` with their own plugin/middleware
993
- * machinery, e.g. `@vitejs/devtools-kit`'s `createPluginFromDevframe`
994
- * returns a Vite `Plugin` whose `devtools.setup` ultimately delegates here.
995
- */
996
- /**
997
- * Phase one of an install: run the duplication guard, serve the SPA + meta,
998
- * register the iframe dock, and queue the definition's declarative wire
999
- * services, everything up to (but not including) `setup(ctx)`. Returns a
1000
- * deferred setup thunk, or `null` when the devframe was deduplicated.
1001
- *
1002
- * The hub's initial batch uses this to collect every devframe's services
1003
- * across the whole hub, `ready()` them once, and only then run the setups,
1004
- * so services are ready before any setup, and a plugin can consume a service
1005
- * another plugin declared regardless of mount order.
1006
- */
1007
- /**
1008
- * Serve a devframe's SPA (and the hub's connection meta) under `base`, if the
1009
- * definition ships client assets.
1010
- */
1011
- async function serveDevframeAssets(ctx, d, id, base) {
1012
- const clientAssets = resolveClientAssets(d);
1013
- if (!clientAssets) return;
1014
- if (ctx.host.mountConnectionMeta) await ctx.host.mountConnectionMeta(base);
1015
- else diagnostics.DF8106({
1016
- id,
1017
- name: d.name,
1018
- base
1019
- });
1020
- ctx.views.hostStatic(base, typeof clientAssets === "string" ? resolve(clientAssets) : clientAssets, d.importMetaUrl);
1021
- }
1022
- async function prepareDevframe(ctx, d, options = {}) {
1023
- const strategy = d.duplicationStrategy ?? "warn";
1024
- const isDuplicate = ctx.docks.views.has(d.id);
1025
- if (isDuplicate && strategy !== "duplicate") {
1026
- if (strategy === "throw") throw diagnostics.DF8105({
1027
- id: d.id,
1028
- name: d.name
1029
- });
1030
- if (strategy === "warn") diagnostics.DF8105({
1031
- id: d.id,
1032
- name: d.name
1033
- });
1034
- return null;
1035
- }
1036
- const id = isDuplicate ? nextAvailableDockId(ctx.docks.views, d.id) : d.id;
1037
- const base = options.base ?? (id === d.id ? resolveBasePath(d, "hosted") : resolveBasePath({
1038
- ...d,
1039
- id,
1040
- basePath: void 0
1041
- }, "hosted"));
1042
- const dockDefaults = {
1043
- ...d.dock,
1044
- ...options.dock
1045
- };
1046
- const clientScript = await resolvePageScriptClientScript(ctx, dockDefaults.clientScript, base);
1047
- if (clientScript) dockDefaults.clientScript = clientScript;
1048
- await serveDevframeAssets(ctx, d, id, base);
1049
- ctx.docks.register({
1050
- id,
1051
- title: d.name,
1052
- icon: d.icon,
1053
- ...dockDefaults,
1054
- type: "iframe",
1055
- url: base
1056
- });
1057
- for (const input of d.services ?? []) ctx.services.install(input, { resolveFrom: d.importMetaUrl });
1058
- return () => Promise.resolve(d.setup(ctx));
1059
- }
1060
- /**
1061
- * Install a {@link DevframeDefinition} into a hub in one call: serve its SPA,
1062
- * register its dock, ready its services, and run `setup(ctx)`. The imperative
1063
- * counterpart to the hub's declarative `devframes` list (which batches the
1064
- * phases via {@link prepareDevframe}); use it from `configure(ctx)` or
1065
- * wherever you hold the context to plug in an extra devframe after startup.
1066
- */
1067
- async function installDevframe(ctx, d, options = {}) {
1068
- const run = await prepareDevframe(ctx, d, options);
1069
- if (!run) return;
1070
- await ctx.services.ready();
1071
- await run();
1072
- }
1073
- //#endregion
1074
1087
  //#region src/node/rpc-builtins.ts
1075
1088
  /**
1076
1089
  * Resolve an interactive (PTY) terminal session by id, or throw. Sessions
@@ -1324,4 +1337,4 @@ async function createHubContext(options) {
1324
1337
  return context;
1325
1338
  }
1326
1339
  //#endregion
1327
- export { DevframeDocksHost as _, hubMessagesAdd as a, hubMessagesUpdate as c, hubTerminalsRestart as d, hubTerminalsTerminate as f, DevframeMessagesHost as g, DevframeTerminalsHost as h, hubDocksActivate as i, hubTerminalsRemove as l, prepareDevframe as m, builtinHubRpcDeclarations as n, hubMessagesClear as o, hubTerminalsWrite as p, hubCommandsExecute as r, hubMessagesRemove as s, createHubContext as t, hubTerminalsResize as u, DevframeCommandsHost as v, diagnostics as y };
1340
+ export { DevframeCommandsHost as _, hubMessagesAdd as a, diagnostics as b, hubMessagesUpdate as c, hubTerminalsRestart as d, hubTerminalsTerminate as f, DevframeDocksHost as g, DevframeMessagesHost as h, hubDocksActivate as i, hubTerminalsRemove as l, DevframeTerminalsHost as m, builtinHubRpcDeclarations as n, hubMessagesClear as o, hubTerminalsWrite as p, hubCommandsExecute as r, hubMessagesRemove as s, createHubContext as t, hubTerminalsResize as u, prepareDevframe as v, skippedInStaticBuild as y };
@@ -0,0 +1,74 @@
1
+ import { n as DevframeHubContext, t as CreateHubContextOptions } from "../context-Di-fhMKP.mjs";
2
+ import { DevframeHubUi, DevframesInput, DockRendererRegistration } from "./initiate.mjs";
3
+ import { DevframeServiceInput, DevframeStorageScope } from "devframe/types";
4
+ //#region src/node/build.d.ts
5
+ interface BuildHubOptions {
6
+ /**
7
+ * Output directory the hub subtree is written into. It corresponds to the
8
+ * hub {@link BuildHubOptions.base} at serve time: building with
9
+ * `base: '/__devframes/'` into `dist/__devframes` makes the deployed app's
10
+ * `dist/` servable as-is by any static file server.
11
+ */
12
+ outDir: string;
13
+ /**
14
+ * Mount base the deployed hub answers under (baked into every absolute URL
15
+ * the build emits: page-script rewrites, renderer modules, per-frame meta
16
+ * pointers). Default: `/__devframes/`.
17
+ */
18
+ base?: string;
19
+ /** Devframes to bake as docks, same input as `initHub({ devframes })`. */
20
+ devframes?: DevframesInput;
21
+ /** Host-level wire services, same contract as `initHub({ services })`. */
22
+ services?: DevframeServiceInput[];
23
+ /** Extra RPC declarations registered at context creation. */
24
+ rpcDeclarations?: CreateHubContextOptions['builtinRpcDeclarations'];
25
+ /**
26
+ * Runs once the context exists and every `devframes` entry is mounted;
27
+ * register docks, commands, and messages surfaces here, exactly like
28
+ * `initHub({ configure })`. Everything it registers is baked into the
29
+ * static shared-state snapshot.
30
+ */
31
+ configure?: (ctx: DevframeHubContext) => void | Promise<void>;
32
+ /**
33
+ * The hub's UI slot: the viewer SPA is copied to the hub base, the
34
+ * `embedded.js` bootstrap next to it, and `setup(ctx)` runs so its static
35
+ * config (branding, dock preferences) is baked into the connection meta.
36
+ */
37
+ ui?: DevframeHubUi;
38
+ /** Prebuilt dock-renderer modules, copied to `<base>__renderers/<type>.mjs`. */
39
+ renderers?: readonly DockRendererRegistration[];
40
+ /** Name written into `<base>__index.json`. */
41
+ name?: string;
42
+ /** Version written into `<base>__index.json`. */
43
+ version?: string;
44
+ /** Working directory for the hub context. Default: `process.cwd()`. */
45
+ cwd?: string;
46
+ /** Override where persisted devframe state lives during the build. */
47
+ getStorageDir?: (scope: DevframeStorageScope) => string;
48
+ /** Pretty-print RPC dump JSON files. Default: `false` (minified shards). */
49
+ pretty?: boolean;
50
+ }
51
+ /**
52
+ * Produce a self-contained static deploy of a whole hub, the multi-devframe
53
+ * counterpart of devframe's `createBuild`:
54
+ *
55
+ * - Build a `mode: 'build'` hub context and mount every devframe: each
56
+ * SPA is copied to `<outDir>/<id>/`, an absolute-path page script to
57
+ * `<outDir>/<id>/__page-script/`, and its `setup(ctx)` runs.
58
+ * - Copy the UI slot's viewer SPA, `embedded.js`, and the renderer
59
+ * modules, and write the discovery documents (`__index.json`,
60
+ * `__client-imports.js`).
61
+ * - Write `__connection.json` (`{ backend: 'static' }`, carrying
62
+ * `ctx.staticConfig` as `configs`) at the hub base and at every frame
63
+ * base (each pointing back at the hub's own meta via `baseUrl`).
64
+ * - Bake the shared RPC dump under `<outDir>/__rpc-dump/`: every
65
+ * `static`/`snapshot` RPC plus a snapshot of each shared-state key
66
+ * (docks, commands, renderer manifest), so `createDevframeClientRuntime`
67
+ * and every frame SPA boot from the dump with no live server.
68
+ *
69
+ * Reads work from the baked dump; live writes (messages, terminals,
70
+ * commands execution) have no server and degrade to no-ops in the clients.
71
+ */
72
+ declare function buildHub(options: BuildHubOptions): Promise<void>;
73
+ //#endregion
74
+ export { BuildHubOptions, buildHub };
@@ -0,0 +1,167 @@
1
+ import { a as normalizeHubBase, i as DOCK_RENDERERS_STATE_KEY } from "../constants-Da23JxCZ.mjs";
2
+ import { b as diagnostics, t as createHubContext } from "../context-BWaW6N53.mjs";
3
+ import { i as resolveRendererRegistrations, n as renderClientImportsModule, r as resolveDevframesInput, t as mountDevframes } from "../assemble-Hj36CCzH.mjs";
4
+ import { joinURL } from "ufo";
5
+ import { DEVFRAME_CONNECTION_META_FILENAME, DEVFRAME_DOCK_IMPORTS_FILENAME } from "devframe/constants";
6
+ import { existsSync } from "node:fs";
7
+ import fs from "node:fs/promises";
8
+ import process from "node:process";
9
+ import { createH3DevframeHost } from "devframe/internal";
10
+ import { collectStaticRpcDump, writeStaticRpcDump } from "devframe/rpc/dump";
11
+ import { colors } from "devframe/utils/colors";
12
+ import { dirname, resolve } from "pathe";
13
+ //#region src/node/build.ts
14
+ /**
15
+ * Produce a self-contained static deploy of a whole hub, the multi-devframe
16
+ * counterpart of devframe's `createBuild`:
17
+ *
18
+ * - Build a `mode: 'build'` hub context and mount every devframe: each
19
+ * SPA is copied to `<outDir>/<id>/`, an absolute-path page script to
20
+ * `<outDir>/<id>/__page-script/`, and its `setup(ctx)` runs.
21
+ * - Copy the UI slot's viewer SPA, `embedded.js`, and the renderer
22
+ * modules, and write the discovery documents (`__index.json`,
23
+ * `__client-imports.js`).
24
+ * - Write `__connection.json` (`{ backend: 'static' }`, carrying
25
+ * `ctx.staticConfig` as `configs`) at the hub base and at every frame
26
+ * base (each pointing back at the hub's own meta via `baseUrl`).
27
+ * - Bake the shared RPC dump under `<outDir>/__rpc-dump/`: every
28
+ * `static`/`snapshot` RPC plus a snapshot of each shared-state key
29
+ * (docks, commands, renderer manifest), so `createDevframeClientRuntime`
30
+ * and every frame SPA boot from the dump with no live server.
31
+ *
32
+ * Reads work from the baked dump; live writes (messages, terminals,
33
+ * commands execution) have no server and degrade to no-ops in the clients.
34
+ */
35
+ async function buildHub(options) {
36
+ const base = normalizeHubBase(options.base ?? "/__devframes/");
37
+ const cwd = options.cwd ?? process.cwd();
38
+ const outDir = resolve(cwd, options.outDir);
39
+ const rendererRegistrations = resolveRendererRegistrations(options.renderers ?? []);
40
+ if (existsSync(outDir)) await fs.rm(outDir, { recursive: true });
41
+ await fs.mkdir(outDir, { recursive: true });
42
+ /** Map a hub-base-relative URL base to its on-disk location under `outDir`. */
43
+ function resolveOutPath(urlBase) {
44
+ if (!urlBase.startsWith(base)) throw diagnostics.DF8006({
45
+ urlBase,
46
+ base
47
+ });
48
+ return resolve(outDir, urlBase.slice(base.length));
49
+ }
50
+ const frameMetaBases = [];
51
+ const host = {
52
+ ...createH3DevframeHost({
53
+ origin: "http://localhost",
54
+ appName: "devframes",
55
+ workspaceRoot: cwd,
56
+ /**
57
+ * A static build "serves" by copying: a local dist verbatim, a remote
58
+ * assets source by materializing every listed file.
59
+ */
60
+ mount: async (mountBase, source) => {
61
+ const target = resolveOutPath(mountBase);
62
+ await fs.mkdir(dirname(target), { recursive: true });
63
+ if (typeof source === "string") await fs.cp(source, target, { recursive: true });
64
+ else await source.materialize(target);
65
+ }
66
+ }),
67
+ ...options.getStorageDir ? { getStorageDir: options.getStorageDir } : {},
68
+ mountConnectionMeta: (frameBase) => {
69
+ resolveOutPath(frameBase);
70
+ frameMetaBases.push(frameBase);
71
+ }
72
+ };
73
+ const ctx = await createHubContext({
74
+ cwd,
75
+ workspaceRoot: cwd,
76
+ mode: "build",
77
+ host,
78
+ ...options.rpcDeclarations ? { builtinRpcDeclarations: options.rpcDeclarations } : {}
79
+ });
80
+ const devframes = await resolveDevframesInput(options.devframes ?? []);
81
+ for (const input of options.services ?? []) ctx.services.install(input);
82
+ const frames = [];
83
+ const setups = await mountDevframes(ctx, devframes, base, frames, false);
84
+ await ctx.services.ready();
85
+ for (const run of setups) await run();
86
+ await options.configure?.(ctx);
87
+ await options.ui?.setup?.(ctx);
88
+ await publishRendererManifest(ctx, rendererRegistrations, base, outDir);
89
+ await writeUiArtifacts(options.ui, outDir);
90
+ await fs.writeFile(resolve(outDir, DEVFRAME_DOCK_IMPORTS_FILENAME), renderClientImportsModule(ctx), "utf-8");
91
+ await fs.writeFile(resolve(outDir, "__index.json"), `${JSON.stringify({
92
+ name: options.name,
93
+ version: options.version,
94
+ base,
95
+ frames,
96
+ endpoints: {
97
+ connection: DEVFRAME_CONNECTION_META_FILENAME,
98
+ clientImports: DEVFRAME_DOCK_IMPORTS_FILENAME,
99
+ index: "__index.json",
100
+ ...options.ui?.embedded ? { embedded: "embedded.js" } : {}
101
+ }
102
+ }, null, 2)}\n`, "utf-8");
103
+ await writeConnectionMetas(ctx, base, outDir, frameMetaBases.map(resolveOutPath));
104
+ console.log(colors.cyan`[devframes-hub] writing RPC dump to ${resolve(outDir, "__rpc-dump")}`);
105
+ const dump = await collectStaticRpcDump(ctx.rpc.definitions.values(), ctx);
106
+ await writeStaticRpcDump(dump, outDir, { pretty: options.pretty });
107
+ console.log(colors.green`[devframes-hub] built ${frames.length} devframe${frames.length === 1 ? "" : "s"} -> ${outDir}`);
108
+ }
109
+ /**
110
+ * Publish the renderer manifest exactly like `initHub` does, so it is baked
111
+ * into the shared-state snapshot, and copy each prebuilt module to the URL
112
+ * the manifest advertises.
113
+ */
114
+ async function publishRendererManifest(ctx, registrations, base, outDir) {
115
+ const manifest = {};
116
+ for (const registration of registrations) {
117
+ manifest[registration.type] = {
118
+ importFrom: joinURL(base, "__renderers", `${registration.type}.mjs`),
119
+ ...registration.importName ? { importName: registration.importName } : {}
120
+ };
121
+ await fs.mkdir(resolve(outDir, "__renderers"), { recursive: true });
122
+ await fs.copyFile(registration.file, resolve(outDir, "__renderers", `${registration.type}.mjs`));
123
+ }
124
+ (await ctx.rpc.sharedState.get(DOCK_RENDERERS_STATE_KEY, { initialValue: {} })).mutate(() => manifest);
125
+ }
126
+ /**
127
+ * Copy the UI slot's artifacts: the viewer SPA owns the hub base (copied
128
+ * before the discovery documents, so those win over same-named files it
129
+ * ships), `embedded.js` next to it, plus any produced assets.
130
+ */
131
+ async function writeUiArtifacts(ui, outDir) {
132
+ if (ui?.viewer) await fs.cp(resolve(ui.viewer.distDir), outDir, { recursive: true });
133
+ if (ui?.embedded) await fs.copyFile(resolve(ui.embedded.entry), resolve(outDir, "embedded.js"));
134
+ for (const [key, produce] of Object.entries(ui?.assets ?? {})) {
135
+ const target = resolve(outDir, key);
136
+ await fs.mkdir(dirname(target), { recursive: true });
137
+ await fs.writeFile(target, produce());
138
+ }
139
+ }
140
+ /**
141
+ * Write the `backend: 'static'` connection meta at the hub base, and a copy
142
+ * at every frame base whose `baseUrl` points relative resolution (the RPC
143
+ * dump) back at the hub's own meta, so a frame SPA that fetched its
144
+ * per-frame copy (instead of inheriting the host page's connection) still
145
+ * finds the shared dump.
146
+ */
147
+ async function writeConnectionMetas(ctx, base, outDir, frameDirs) {
148
+ const jsonSerializableMethods = [];
149
+ for (const def of ctx.rpc.definitions.values()) if (def.jsonSerializable === true) jsonSerializableMethods.push(def.name);
150
+ const meta = {
151
+ backend: "static",
152
+ jsonSerializableMethods,
153
+ ...Object.keys(ctx.staticConfig).length > 0 ? { configs: ctx.staticConfig } : {}
154
+ };
155
+ await fs.writeFile(resolve(outDir, DEVFRAME_CONNECTION_META_FILENAME), JSON.stringify(meta, null, 2), "utf-8");
156
+ const frameMeta = {
157
+ ...meta,
158
+ baseUrl: joinURL(base, DEVFRAME_CONNECTION_META_FILENAME)
159
+ };
160
+ for (const frameDir of frameDirs) {
161
+ const target = resolve(frameDir, DEVFRAME_CONNECTION_META_FILENAME);
162
+ await fs.mkdir(dirname(target), { recursive: true });
163
+ await fs.writeFile(target, JSON.stringify(frameMeta, null, 2), "utf-8");
164
+ }
165
+ }
166
+ //#endregion
167
+ export { buildHub };
@@ -1,8 +1,8 @@
1
- import { _ as DevframeDocksHost, a as hubMessagesAdd, c as hubMessagesUpdate, d as hubTerminalsRestart, f as hubTerminalsTerminate, g as DevframeMessagesHost, h as DevframeTerminalsHost, i as hubDocksActivate, l as hubTerminalsRemove, n as builtinHubRpcDeclarations, o as hubMessagesClear, p as hubTerminalsWrite, r as hubCommandsExecute, s as hubMessagesRemove, t as createHubContext, u as hubTerminalsResize, v as DevframeCommandsHost } from "../context-D4L966cN.mjs";
1
+ import { _ as DevframeCommandsHost, a as hubMessagesAdd, c as hubMessagesUpdate, d as hubTerminalsRestart, f as hubTerminalsTerminate, g as DevframeDocksHost, h as DevframeMessagesHost, i as hubDocksActivate, l as hubTerminalsRemove, m as DevframeTerminalsHost, n as builtinHubRpcDeclarations, o as hubMessagesClear, p as hubTerminalsWrite, r as hubCommandsExecute, s as hubMessagesRemove, t as createHubContext, u as hubTerminalsResize } from "../context-BWaW6N53.mjs";
2
2
  import "ufo";
3
- import "pathe";
4
- import "node:process";
5
3
  import "node:fs";
4
+ import "node:process";
5
+ import "pathe";
6
6
  import { builtinModules } from "node:module";
7
7
  import "node:url";
8
8
  import assert from "node:assert";
@@ -1,7 +1,7 @@
1
1
  import { i as InstallDevframeOptions, n as DevframeHubContext, t as CreateHubContextOptions } from "../context-Di-fhMKP.mjs";
2
- import { r as DEVFRAMES_HUB_BASE } from "../constants-Coo1p3ja.mjs";
2
+ import { r as DEVFRAMES_HUB_BASE } from "../constants-Bz4yclHo.mjs";
3
3
  import { DevframeInstanceRecord } from "devframe/internal";
4
- import { ConnectionMeta, DevframeDefinition, DevframeServiceInput, DevframeSseOptions, DevframeStorageScope, DevframeWsOptions, McpRouteOptions } from "devframe/types";
4
+ import { ConnectionMeta, DevframeDefinition, DevframeServiceInput, DevframeSseOptions, DevframeStorageScope, DevframeWsOptions, McpSetting } from "devframe/types";
5
5
  import { DevframeAuthHandler } from "devframe/node/auth";
6
6
  import { WsOriginRegistry } from "devframe/rpc/transports/ws-server";
7
7
  import { Buffer } from "node:buffer";
@@ -197,14 +197,16 @@ interface InitHubOptions {
197
197
  /**
198
198
  * Expose the **aggregate** MCP endpoint at `<base>__mcp`: one
199
199
  * Streamable-HTTP server over the shared context's whole tool registry
200
- * (ids are already namespaced per plugin). Disabled by default; `true`
201
- * mounts it with the loopback origin gate (trusting same-machine callers),
202
- * an object opts into an {@link McpRouteOptions.authorization} identity
203
- * check. A mounted devframe's own `mcp` setting is ignored: the hub's
204
- * aggregate route covers them all (`DF8005` warns when one asks for MCP
205
- * while this is off).
200
+ * (ids are already namespaced per plugin). Defaults to `'auto'`: the route
201
+ * mounts once any mounted devframe (or an agent-flagged hub command)
202
+ * exposes an agent surface. `true` mounts it
203
+ * unconditionally with the loopback origin gate (trusting same-machine
204
+ * callers), an object opts into an {@link McpRouteOptions.authorization}
205
+ * identity check, `false` keeps it off. A mounted devframe's own `mcp`
206
+ * setting is ignored: the hub's aggregate route covers them all (`DF8005`
207
+ * warns when one asks for MCP while this is `false`).
206
208
  */
207
- mcp?: boolean | McpRouteOptions;
209
+ mcp?: McpSetting;
208
210
  /**
209
211
  * Public origin the host app is reachable at, or a getter. Derived lazily
210
212
  * from the first request when omitted.
@@ -1,19 +1,15 @@
1
- import { a as normalizeHubBase, i as DOCK_RENDERERS_STATE_KEY, r as DEVFRAMES_HUB_BASE } from "../constants-BUd6oMci.mjs";
2
- import { a as resolveClientModuleSpecifier } from "../remote-url-UClKLQ5_.mjs";
3
- import { m as prepareDevframe, t as createHubContext, y as diagnostics } from "../context-D4L966cN.mjs";
4
- import { joinURL, withTrailingSlash, withoutLeadingSlash } from "ufo";
5
- import { DEVFRAME_CONNECTION_META_FILENAME, DEVFRAME_DOCK_IMPORTS_FILENAME, DEVFRAME_MCP_ROUTE, DEVFRAME_WS_ROUTE } from "devframe/constants";
6
- import { createH3DevframeHost, createInstanceShell, importRuntimeModule, resolveInstanceRegister, resolveMcpConfig } from "devframe/internal";
7
- import { resolve } from "pathe";
8
- import process from "node:process";
9
- import { existsSync } from "node:fs";
1
+ import { a as normalizeHubBase, i as DOCK_RENDERERS_STATE_KEY, r as DEVFRAMES_HUB_BASE } from "../constants-Da23JxCZ.mjs";
2
+ import { b as diagnostics, t as createHubContext } from "../context-BWaW6N53.mjs";
3
+ import { i as resolveRendererRegistrations, n as renderClientImportsModule, r as resolveDevframesInput, t as mountDevframes } from "../assemble-Hj36CCzH.mjs";
4
+ import { joinURL, withoutLeadingSlash } from "ufo";
5
+ import { DEVFRAME_CONNECTION_META_FILENAME, DEVFRAME_DOCK_IMPORTS_FILENAME, DEVFRAME_MCP_ROUTE } from "devframe/constants";
10
6
  import { readFile } from "node:fs/promises";
7
+ import process from "node:process";
8
+ import { createH3DevframeHost, createInstanceShell, importRuntimeModule, loadAutoMcpAdapter, resolveInstanceRegister, resolveMcpConfig } from "devframe/internal";
9
+ import { resolve } from "pathe";
11
10
  import { mountStaticHandler } from "devframe/utils/serve-static";
12
11
  import { H3 } from "h3";
13
12
  //#region src/node/initiate.ts
14
- function normalizeDevframeEntry(entry) {
15
- return "devframe" in entry ? entry : { devframe: entry };
16
- }
17
13
  /** Content-type for a UI asset key, inferred from its file extension. */
18
14
  function assetContentType(key) {
19
15
  if (key.endsWith(".json")) return "application/json; charset=utf-8";
@@ -23,72 +19,6 @@ function assetContentType(key) {
23
19
  if (key.endsWith(".html")) return "text/html; charset=utf-8";
24
20
  return "application/octet-stream";
25
21
  }
26
- /** Reserved filenames directly under the hub base; a frame id can't shadow them. */
27
- const RESERVED_HUB_PATHS = [
28
- DEVFRAME_CONNECTION_META_FILENAME,
29
- DEVFRAME_DOCK_IMPORTS_FILENAME,
30
- DEVFRAME_WS_ROUTE,
31
- DEVFRAME_MCP_ROUTE,
32
- "__index.json",
33
- "__renderers",
34
- "embedded.js"
35
- ];
36
- /**
37
- * Flatten the `devframes` input: await every thenable, call every factory,
38
- * spread the arrays, and drop the empty slots, so a host can build the list
39
- * conditionally (`isDev && loadInspect()`) without filtering it first.
40
- */
41
- function resolveDevframesInput(input) {
42
- return Promise.all(input.map(async (entry) => {
43
- const resolved = await (typeof entry === "function" ? entry() : entry);
44
- return (Array.isArray(resolved) ? resolved : [resolved]).filter((slot) => slot != null).map(normalizeDevframeEntry);
45
- })).then((arrays) => arrays.flat());
46
- }
47
- /**
48
- * Validate the renderer-module registrations fail-fast: route-safe types
49
- * (each becomes the `<base>__renderers/<type>.mjs` URL segment), one module
50
- * per type, and an existing bundle file (renderer packages are prebuilt).
51
- */
52
- function resolveRendererRegistrations(registrations) {
53
- const seen = /* @__PURE__ */ new Set();
54
- return registrations.map((registration) => {
55
- if (!/^[\w.-]+$/.test(registration.type)) throw diagnostics.DF8110({ type: registration.type });
56
- if (seen.has(registration.type)) throw diagnostics.DF8108({ type: registration.type });
57
- seen.add(registration.type);
58
- const file = resolve(registration.file);
59
- if (!existsSync(file)) throw diagnostics.DF8109({
60
- type: registration.type,
61
- file
62
- });
63
- return {
64
- ...registration,
65
- file
66
- };
67
- });
68
- }
69
- /**
70
- * Render the dock client-script import map as an ES module: one dynamic
71
- * import thunk per dock that carries a client script (`clientScript` on
72
- * iframe docks, `action`, `renderer`). External viewers import this module
73
- * from `<base>__client-imports.js` to load per-dock client code into the
74
- * host page; `importFrom` values are URL paths the host serves, or bare npm
75
- * specifiers resolved through the host's `clientModuleResolution` template.
76
- */
77
- function renderClientImportsModule(ctx) {
78
- const template = ctx.staticConfig.dock?.clientModuleResolution;
79
- const entries = [];
80
- for (const [id, view] of ctx.docks.views) {
81
- const scripts = [];
82
- const anyView = view;
83
- if (anyView.clientScript) scripts.push(anyView.clientScript);
84
- if (anyView.action) scripts.push(anyView.action);
85
- if (anyView.renderer) scripts.push(anyView.renderer);
86
- if (scripts.length === 0) continue;
87
- const thunks = scripts.map((script) => `() => import(${JSON.stringify(resolveClientModuleSpecifier(script.importFrom, { template }))})`);
88
- entries.push(` ${JSON.stringify(id)}: [${thunks.join(", ")}],`);
89
- }
90
- return `// Generated by @devframes/hub: dock client-script import map.\nexport const clientImports = {\n${entries.join("\n")}\n}\nexport default clientImports\n`;
91
- }
92
22
  /**
93
23
  * Resolve the shared hub context: reuse a caller-supplied one, or build an
94
24
  * h3-backed context that serves the hub's connection meta under every frame.
@@ -126,31 +56,6 @@ async function resolveHubContext(options, app, cwd, api) {
126
56
  });
127
57
  }
128
58
  /**
129
- * Pass 1: mount each devframe under `<base><id>/` (SPA, meta, iframe dock)
130
- * and queue its declared services, guarding the id against reserved hub
131
- * filenames and route-pattern characters. Returns the deferred setup thunks.
132
- */
133
- async function mountDevframes(ctx, devframes, base, frames, hubMcpEnabled) {
134
- const setups = [];
135
- for (const { devframe: def, dock } of devframes) {
136
- if (RESERVED_HUB_PATHS.includes(def.id)) throw diagnostics.DF8000({ id: def.id });
137
- if (!/^[\w.-]+$/.test(def.id)) throw diagnostics.DF8004({ id: def.id });
138
- if (!hubMcpEnabled && def.cli?.mcp) diagnostics.DF8005({ id: def.id });
139
- const frameBase = withTrailingSlash(joinURL(base, def.id));
140
- const run = await prepareDevframe(ctx, def, {
141
- base: frameBase,
142
- ...dock ? { dock } : {}
143
- });
144
- if (run) setups.push(run);
145
- frames.push({
146
- id: def.id,
147
- base: frameBase,
148
- title: def.name
149
- });
150
- }
151
- return setups;
152
- }
153
- /**
154
59
  * Initiate a hub instance: the whole multi-devframe devtools surface
155
60
  * behind one framework-agnostic, web-standard handler. Every mounted
156
61
  * devframe shares one context (merged RPC registry, shared state, docks /
@@ -195,7 +100,7 @@ function initHub(options) {
195
100
  */
196
101
  absoluteWsPath: true,
197
102
  resolveSidecarPort: async (sidecarHost) => {
198
- const { getPort } = await import("../dist-R0T2g9Vw.mjs");
103
+ const { getPort } = await import("../dist-BP3CGpgZ.mjs");
199
104
  return getPort({
200
105
  port: 9777,
201
106
  portRange: [9777, 9877],
@@ -213,7 +118,7 @@ function initHub(options) {
213
118
  };
214
119
  const devframes = await resolveDevframesInput(options.devframes ?? []);
215
120
  for (const input of options.services ?? []) ctx.services.install(input);
216
- const setups = await mountDevframes(ctx, devframes, base, frames, !!options.mcp);
121
+ const setups = await mountDevframes(ctx, devframes, base, frames, options.mcp !== false);
217
122
  await ctx.services.ready();
218
123
  for (const run of setups) await run();
219
124
  await options.configure?.(ctx);
@@ -224,11 +129,17 @@ function initHub(options) {
224
129
  ...registration.importName ? { importName: registration.importName } : {}
225
130
  };
226
131
  (await ctx.rpc.sharedState.get(DOCK_RENDERERS_STATE_KEY, { initialValue: {} })).mutate(() => manifest);
227
- const mcpConfig = resolveMcpConfig(options.mcp);
132
+ const mcpSetting = options.mcp ?? "auto";
133
+ let mcpModule;
134
+ let mcpConfig;
135
+ if (mcpSetting === "auto") {
136
+ mcpModule = await loadAutoMcpAdapter(ctx.agent);
137
+ if (mcpModule) mcpConfig = { authorization: false };
138
+ } else mcpConfig = resolveMcpConfig(mcpSetting);
228
139
  if (!mcpConfig) return { context: ctx };
229
140
  const mcpRoute = withoutLeadingSlash(mcpConfig.path ?? DEVFRAME_MCP_ROUTE);
230
- const { mountMcpHttp } = await importRuntimeModule("devframe/adapters/mcp");
231
- const mounted = mountMcpHttp(app, ctx, joinURL(base, mcpRoute), {
141
+ mcpModule ??= await importRuntimeModule("devframe/adapters/mcp");
142
+ const mounted = mcpModule.mountMcpHttp(app, ctx, joinURL(base, mcpRoute), {
232
143
  serverName: options.name ?? "devframes-hub",
233
144
  serverVersion: options.version ?? "0.0.0",
234
145
  exposeSharedState: true,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@devframes/hub",
3
3
  "type": "module",
4
- "version": "0.9.9",
4
+ "version": "0.9.10",
5
5
  "description": "Hub layer that orchestrates devframe docks, terminals, messages, and commands on any host.",
6
6
  "author": "Anthony Fu <anthonyfu117@hotmail.com>",
7
7
  "license": "MIT",
@@ -22,6 +22,7 @@
22
22
  "sideEffects": false,
23
23
  "exports": {
24
24
  ".": "./dist/index.mjs",
25
+ "./build": "./dist/node/build.mjs",
25
26
  "./client": "./dist/client/index.mjs",
26
27
  "./constants": "./dist/constants.mjs",
27
28
  "./initiate": "./dist/node/initiate.mjs",
@@ -34,7 +35,7 @@
34
35
  "dist"
35
36
  ],
36
37
  "peerDependencies": {
37
- "devframe": "0.9.9"
38
+ "devframe": "0.9.10"
38
39
  },
39
40
  "dependencies": {
40
41
  "@standard-schema/spec": "^1.1.0",
@@ -48,7 +49,7 @@
48
49
  },
49
50
  "devDependencies": {
50
51
  "@types/node": "^26.4.0",
51
- "devframe": "0.9.9",
52
+ "devframe": "0.9.10",
52
53
  "get-port-please": "^3.2.0",
53
54
  "mlly": "^1.8.2",
54
55
  "tsdown": "^0.22.14",
@@ -1,5 +1,5 @@
1
- import "node:path";
2
1
  import "node:fs/promises";
2
+ import "node:path";
3
3
  import { createServer } from "node:net";
4
4
  import { networkInterfaces } from "node:os";
5
5
  //#region ../../node_modules/.pnpm/get-port-please@3.2.0/node_modules/get-port-please/dist/index.mjs