@devframes/hub 0.9.8 → 0.9.9

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.
@@ -1,8 +1,8 @@
1
- import { t as DevframeDocksUserSettings } from "./settings-DwxCyeI6.mjs";
1
+ import { t as DevframeDocksUserSettings } from "./settings-CWHPzCWS.mjs";
2
2
  //#region src/events.d.ts
3
3
  /**
4
4
  * Centralized registry of every event, broadcast, RPC method, shared-state
5
- * key, and channel name the hub uses — the single source of truth that keeps
5
+ * key, and channel name the hub uses: the single source of truth that keeps
6
6
  * these names out of scattered string literals.
7
7
  *
8
8
  * **Keep this in sync with [`docs/content/8.references/3.events.md`](../../../docs/content/8.references/3.events.md)**
@@ -75,7 +75,7 @@ declare const HUB_EVENTS: {
75
75
  };
76
76
  //#endregion
77
77
  //#region src/constants.d.ts
78
- /** Default mount base for a hub instance — one namespace, one catch-all. */
78
+ /** Default mount base for a hub instance: one namespace, one catch-all. */
79
79
  declare const DEVFRAMES_HUB_BASE = "/__devframes/";
80
80
  /**
81
81
  * Normalize a hub mount base to an absolute path with leading and trailing
@@ -86,7 +86,7 @@ declare const DEVFRAMES_HUB_BASE = "/__devframes/";
86
86
  */
87
87
  declare function normalizeHubBase(base: string): string;
88
88
  /**
89
- * The default ordering weight for each known dock category — lower sorts
89
+ * The default ordering weight for each known dock category, where lower sorts
90
90
  * earlier. Downstream viewers (e.g. `@vitejs/devtools-kit`) import this as the
91
91
  * single source of truth so the hub and its viewers agree on category order.
92
92
  * `framework` sorts first; `~builtin` (the viewer's own built-in views) last.
@@ -97,7 +97,7 @@ declare function normalizeHubBase(base: string): string;
97
97
  */
98
98
  declare const DEFAULT_CATEGORIES_ORDER: Record<string, number>;
99
99
  /**
100
- * Shared-state slot carrying the hub's renderer manifest — one
100
+ * Shared-state slot carrying the hub's renderer manifest: one
101
101
  * {@link import('./client/renderers').DockRendererManifest} entry per dock
102
102
  * `type`, published by `initHub({ renderers })` and consumed by every
103
103
  * hub-aware client (the headless client host and viewers alike).
@@ -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-DJrPlLi0.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-Coo1p3ja.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-D1zicoJ4.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-BUd6oMci.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,6 +1,6 @@
1
1
  import { r as defineHubRpcFunction } from "./define-Ceekw2EO.mjs";
2
- import { n as DEFAULT_STATE_USER_SETTINGS, o as HUB_EVENTS } from "./constants-D1zicoJ4.mjs";
3
- import { i as isBareModuleSpecifier, t as buildRemoteConnectionUrl } from "./remote-url-Bgc7gtsP.mjs";
2
+ import { n as DEFAULT_STATE_USER_SETTINGS, o as HUB_EVENTS } from "./constants-BUd6oMci.mjs";
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
6
  import { createHostContext, createStorage } from "devframe/node";
@@ -13,24 +13,39 @@ import { nanoid } from "devframe/utils/nanoid";
13
13
  import process from "node:process";
14
14
  import { existsSync } from "node:fs";
15
15
  //#region src/node/diagnostics.ts
16
+ /**
17
+ * Hub-side diagnostics for docks, terminals, messages, and commands.
18
+ * Shares the `DF` prefix with devframe core; the hub reserves the
19
+ * `DF8xxx` range so the unified surface stays collision-free.
20
+ * Sub-ranges:
21
+ * DF8000-DF8099: hub context / lifecycle
22
+ * DF8100-DF8199: docks
23
+ * DF8200-DF8299: terminals
24
+ * DF8300-DF8399: messages
25
+ * DF8400-DF8499: commands
26
+ */
16
27
  const diagnostics = defineDiagnostics({
17
28
  docsBase: "https://devfra.me/errors",
18
29
  codes: {
19
30
  DF8000: {
20
- why: (p) => `Devframe id "${p.id}" collides with a reserved hub path — it cannot be mounted directly under the hub base.`,
31
+ why: (p) => `Devframe id "${p.id}" collides with a reserved hub path, so it cannot be mounted directly under the hub base.`,
21
32
  fix: "The filenames directly under the hub base (`__connection.json`, `__ws`, `__index.json`, `__client-imports.js`, `__mcp`, `embedded.js`) are reserved for the hub protocol. Rename the devframe id, or override its mount with a non-colliding `basePath`."
22
33
  },
23
34
  DF8002: {
24
- why: "initHub received both `devframes` and `context` — the two assembly modes are mutually exclusive.",
25
- fix: "Pass `devframes` to let the instance create the hub context and mount each frame itself, or pass a pre-built `context` (your host already mounted the frames) — never both."
35
+ why: "initHub received both `devframes` and `context`; the two assembly modes are mutually exclusive.",
36
+ fix: "Pass `devframes` to let the instance create the hub context and mount each frame itself, or pass a pre-built `context` (your host already mounted the frames), but never both."
26
37
  },
27
38
  DF8003: {
28
39
  why: "connectionMeta() was called before initHub finished initializing.",
29
- fix: "Await `instance.ready` (or any request through `instance.handler`) before reading `connectionMeta()` — the WebSocket binding it describes is only known once initialization completes."
40
+ fix: "Await `instance.ready` (or any request through `instance.handler`) before reading `connectionMeta()`, since the WebSocket binding it describes is only known once initialization completes."
30
41
  },
31
42
  DF8004: {
32
- why: (p) => `Devframe id "${p.id}" is not a mountable URL segment — the hub mounts each frame at \`<base><id>/\`.`,
33
- 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`)."
43
+ why: (p) => `Devframe id "${p.id}" is not a mountable URL segment, and the hub mounts each frame at \`<base><id>/\`.`,
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
+ },
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."
34
49
  },
35
50
  DF8100: {
36
51
  why: (p) => `Dock with id "${p.id}" is already registered`,
@@ -57,12 +72,12 @@ const diagnostics = defineDiagnostics({
57
72
  fix: "Each devframe is deduplicated by id. Set `duplicationStrategy: \"duplicate\"` on the definition to let instances coexist, `\"silent\"` to drop duplicates quietly, or `\"throw\"` to surface them as errors."
58
73
  },
59
74
  DF8106: {
60
- why: (p) => `The host cannot serve the RPC connection meta for devframe "${p.name}" (id "${p.id}") at "${p.base}" — its \`DevframeHost\` does not implement \`mountConnectionMeta\`.`,
75
+ why: (p) => `The host cannot serve the RPC connection meta for devframe "${p.name}" (id "${p.id}") at "${p.base}"; its \`DevframeHost\` does not implement \`mountConnectionMeta\`.`,
61
76
  fix: "Implement `mountConnectionMeta(base)` on your DevframeHost so it serves `__connection.json` at each mounted base. Without it, the devframe SPA connects only when it shares an origin with the hub UI (same-origin window inheritance); cross-origin, sandboxed, or directly-opened iframes stay disconnected. Static-snapshot hosts that bake the meta into the served files can implement it as a no-op to acknowledge this intentionally."
62
77
  },
63
78
  DF8107: {
64
79
  why: (p) => `Dock activation requested for unknown dock id "${p.id}"`,
65
- fix: "Pass a `dockId` that matches a registered dock entry. The activation is still broadcast, but no hub UI provider will switch to it. Ids are case-sensitive — check for typos, and ensure the target dock is registered before activating it."
80
+ fix: "Pass a `dockId` that matches a registered dock entry. The activation is still broadcast, but no hub UI provider will switch to it. Ids are case-sensitive, so check for typos, and ensure the target dock is registered before activating it."
66
81
  },
67
82
  DF8108: {
68
83
  why: (p) => `A renderer module is already registered for dock type "${p.type}"`,
@@ -73,11 +88,11 @@ const diagnostics = defineDiagnostics({
73
88
  fix: "Point the registration's `file` at the prebuilt browser ES module (build the renderer package first, or check the path). Registration helpers like `jsonRenderUiRenderer()` resolve the path for you."
74
89
  },
75
90
  DF8110: {
76
- why: (p) => `Dock type "${p.type}" is not a servable renderer-module name — the hub serves each module at \`<base>__renderers/<type>.mjs\``,
91
+ why: (p) => `Dock type "${p.type}" is not a servable renderer-module name; the hub serves each module at \`<base>__renderers/<type>.mjs\``,
77
92
  fix: "Renderer types become URL segments, so they may only contain letters, digits, `_`, `-`, and `.`. Use a route-safe dock type (e.g. `json-render`)."
78
93
  },
79
94
  DF8111: {
80
- why: (p) => `Dock "${p.id}" declares the bare-specifier client script "${p.specifier}", but this host advertises no client-module resolution — the browser cannot resolve a bare npm specifier natively, so the script will fail to load.`,
95
+ why: (p) => `Dock "${p.id}" declares the bare-specifier client script "${p.specifier}", but this host advertises no client-module resolution, and the browser cannot resolve a bare npm specifier natively, so the script will fail to load.`,
81
96
  fix: "Run under a host framework that declares `initHub({ clientModuleResolution })` (e.g. Vite's `'/@id/{specifier}'`), ship the script as a self-contained bundle served by URL, or resolve it in the hub UI provider via `createDevframeClientRuntime({ resolveClientModule })` (then disregard this warning)."
82
97
  },
83
98
  DF8200: { why: (p) => `Terminal session with id "${p.id}" already registered` },
@@ -89,14 +104,14 @@ const diagnostics = defineDiagnostics({
89
104
  DF8203: { why: (p) => `Failed to spawn PTY session for "${p.command}": ${p.reason}` },
90
105
  DF8204: {
91
106
  why: (p) => `Terminal session "${p.id}" cannot be controlled (no lifecycle handle)`,
92
- fix: "Spawn it via ctx.terminals.startChildProcess() or startPtySession() — sessions added with a bare register() expose no terminate/restart handle."
107
+ fix: "Spawn it via ctx.terminals.startChildProcess() or startPtySession(); sessions added with a bare register() expose no terminate/restart handle."
93
108
  },
94
109
  DF8205: {
95
110
  why: (p) => `Terminal session "${p.id}" is not restartable`,
96
111
  fix: "It was registered with `restartable: false`; restart it through its owner's controls, or spawn it with `restartable: true` (the default) to allow in-place restarts."
97
112
  },
98
113
  DF8206: {
99
- why: (p) => `Terminal session "${p.id}" cannot be restarted — its output stream is already closed`,
114
+ why: (p) => `Terminal session "${p.id}" cannot be restarted because its output stream is already closed`,
100
115
  fix: "The session already exited (or was terminated) and its stream is spent. Drop it with `ctx.terminals.remove(session)`, then spawn a replacement via `ctx.terminals.startChildProcess()` or `ctx.terminals.startPtySession()` with a fresh id."
101
116
  },
102
117
  DF8400: { why: (p) => `Command "${p.id}" is already registered` },
@@ -145,7 +160,7 @@ var DevframeCommandsHost = class {
145
160
  events = createEventEmitter();
146
161
  /**
147
162
  * Lazy agent projection: `ctx.agent` queries this provider at list/invoke
148
- * time, deriving tools from {@link commands} on demand — the commands map
163
+ * time, deriving tools from {@link commands} on demand, so the commands map
149
164
  * stays the single source of truth, nothing is mirrored or kept in sync.
150
165
  */
151
166
  agentProvider;
@@ -223,7 +238,7 @@ var DevframeCommandsHost = class {
223
238
  * agent-flagged, handler-bearing command (children included) becomes a
224
239
  * callable tool. Queried lazily by the provider registered in the
225
240
  * constructor. `when` clauses evaluate client-side only and are not
226
- * enforced here — opting in a `when`-gated command is a deliberate author
241
+ * enforced here; opting in a `when`-gated command is a deliberate author
227
242
  * decision (documented on `DevframeCommandAgentOptions`).
228
243
  */
229
244
  collectAgentTools() {
@@ -236,7 +251,12 @@ var DevframeCommandsHost = class {
236
251
  description: agent.description,
237
252
  safety: agent.safety ?? "action",
238
253
  tags: agent.tags,
254
+ /** The agent host derives the tool's JSON-Schema input from these. */
239
255
  args: agent.args,
256
+ /**
257
+ * A command handler's positional parameters come solely from its
258
+ * declared `agent.args` schemas; undeclared payload is dropped.
259
+ */
240
260
  handler: async (args) => this.execute(command.id, ...coerceAgentPositionalArgs(args, agent.args, "drop"))
241
261
  });
242
262
  for (const child of command.children ?? []) walk(child);
@@ -266,6 +286,7 @@ var DevframeDocksHost = class {
266
286
  }
267
287
  async init() {
268
288
  this.userSettings = await this.context.rpc.sharedState.get(HUB_EVENTS.sharedState.userSettings, { sharedState: createStorage({
289
+ /** Personal dock layout/preferences: per-checkout private state. */
269
290
  filepath: join(this.context.host.getStorageDir("project"), "settings.json"),
270
291
  initialValue: DEFAULT_STATE_USER_SETTINGS()
271
292
  }) });
@@ -326,7 +347,7 @@ var DevframeDocksHost = class {
326
347
  });
327
348
  }
328
349
  /**
329
- * Warn (don't throw — a client-runtime `resolveClientModule` override may still cover
350
+ * Warn (don't throw, since a client-runtime `resolveClientModule` override may still cover
330
351
  * it) when a dock declares a **bare-specifier** client script on a host
331
352
  * that advertises no `staticConfig.dock.clientModuleResolution`: the
332
353
  * browser cannot resolve a bare npm specifier natively, so the script is
@@ -380,7 +401,7 @@ var DevframeMessagesHost = class {
380
401
  _clock = 0;
381
402
  /**
382
403
  * The tick of the newest removal record dropped from the capped
383
- * `removals` log — cursors older than this can't get a reliable delta
404
+ * `removals` log; cursors older than this can't get a reliable delta
384
405
  * and fall back to a full snapshot in {@link listSince}.
385
406
  */
386
407
  _removalsTrimmedAt = 0;
@@ -561,7 +582,7 @@ var DevframeTerminalsHost = class {
561
582
  this.context = context;
562
583
  }
563
584
  /**
564
- * Lazily acquire the streaming channel — `context.rpc` isn't assigned
585
+ * Lazily acquire the streaming channel, since `context.rpc` isn't assigned
565
586
  * until after every host is constructed, so we can't grab it in the
566
587
  * constructor.
567
588
  */
@@ -571,6 +592,41 @@ var DevframeTerminalsHost = class {
571
592
  this._channel = this.context.rpc.streaming.create(TERMINAL_STREAM_CHANNEL, { replayWindow: TERMINAL_REPLAY_WINDOW });
572
593
  return this._channel;
573
594
  }
595
+ /**
596
+ * Per-run stream lifecycle shared by the child-process and PTY paths: guards
597
+ * the single-close/error transition and keeps the session's `status` in step
598
+ * with the process lifecycle (a hub-aware client sees `running` →
599
+ * `stopped`/`error` instead of a value frozen at spawn time).
600
+ */
601
+ createStreamLifecycle() {
602
+ const state = { streamClosed: false };
603
+ const markStatus = (next) => {
604
+ const session = state.session;
605
+ if (session.status === next) return;
606
+ session.status = next;
607
+ this.events.emit(HUB_EVENTS.bus.terminalsSessionUpdated, session);
608
+ };
609
+ const closeStream = () => {
610
+ if (state.streamClosed) return;
611
+ state.streamClosed = true;
612
+ try {
613
+ state.controller?.close();
614
+ } catch {}
615
+ };
616
+ const errorStream = (error) => {
617
+ if (state.streamClosed) return;
618
+ state.streamClosed = true;
619
+ try {
620
+ state.controller?.error(error);
621
+ } catch {}
622
+ };
623
+ return {
624
+ state,
625
+ markStatus,
626
+ closeStream,
627
+ errorStream
628
+ };
629
+ }
574
630
  register(session) {
575
631
  if (this.sessions.has(session.id)) throw diagnostics.DF8200({ id: session.id });
576
632
  this.sessions.set(session.id, session);
@@ -592,6 +648,25 @@ var DevframeTerminalsHost = class {
592
648
  this.events.emit(HUB_EVENTS.bus.terminalsSessionUpdated, session);
593
649
  this._boundStreams.delete(session.id);
594
650
  }
651
+ /** Drain a session's output stream into its scrollback buffer and streaming sink. */
652
+ async pumpStream(reader, sessionBuffer, sink, isDisposed) {
653
+ try {
654
+ while (!isDisposed()) {
655
+ const result = await reader.read();
656
+ if (isDisposed() || result.done) break;
657
+ sessionBuffer.push(result.value);
658
+ if (sessionBuffer.length > TERMINAL_BUFFER_LIMIT) sessionBuffer.splice(0, sessionBuffer.length - TERMINAL_BUFFER_LIMIT);
659
+ sink?.write(result.value);
660
+ }
661
+ if (!isDisposed() && sink && !sink.closed) sink.close();
662
+ } catch (error) {
663
+ if (!isDisposed() && sink && !sink.closed) sink.error(error);
664
+ } finally {
665
+ try {
666
+ reader.releaseLock();
667
+ } catch {}
668
+ }
669
+ }
595
670
  bindStream(session) {
596
671
  if (this._boundStreams.has(session.id) && this._boundStreams.get(session.id)?.stream === session.stream) return;
597
672
  this._boundStreams.get(session.id)?.dispose();
@@ -602,26 +677,7 @@ var DevframeTerminalsHost = class {
602
677
  const sink = this.getStreamingChannel()?.start({ id: session.id });
603
678
  const reader = session.stream.getReader();
604
679
  let disposed = false;
605
- (async () => {
606
- try {
607
- while (true) {
608
- if (disposed) break;
609
- const result = await reader.read();
610
- if (disposed) break;
611
- if (result.done) break;
612
- sessionBuffer.push(result.value);
613
- if (sessionBuffer.length > TERMINAL_BUFFER_LIMIT) sessionBuffer.splice(0, sessionBuffer.length - TERMINAL_BUFFER_LIMIT);
614
- sink?.write(result.value);
615
- }
616
- if (!disposed && sink && !sink.closed) sink.close();
617
- } catch (error) {
618
- if (!disposed && sink && !sink.closed) sink.error(error);
619
- } finally {
620
- try {
621
- reader.releaseLock();
622
- } catch {}
623
- }
624
- })();
680
+ this.pumpStream(reader, sessionBuffer, sink, () => disposed);
625
681
  this._boundStreams.set(session.id, {
626
682
  dispose: () => {
627
683
  disposed = true;
@@ -634,34 +690,13 @@ var DevframeTerminalsHost = class {
634
690
  async startChildProcess(executeOptions, terminal) {
635
691
  if (this.sessions.has(terminal.id)) throw diagnostics.DF8200({ id: terminal.id });
636
692
  const { exec } = await import("tinyexec");
637
- let controller;
693
+ const { state, markStatus, closeStream, errorStream } = this.createStreamLifecycle();
638
694
  let cp;
639
695
  let currentResult;
640
696
  let runId = 0;
641
- let streamClosed = false;
642
- let session;
643
- const markStatus = (next) => {
644
- if (session.status === next) return;
645
- session.status = next;
646
- this.events.emit(HUB_EVENTS.bus.terminalsSessionUpdated, session);
647
- };
648
- const closeStream = () => {
649
- if (streamClosed) return;
650
- streamClosed = true;
651
- try {
652
- controller?.close();
653
- } catch {}
654
- };
655
- const errorStream = (error) => {
656
- if (streamClosed) return;
657
- streamClosed = true;
658
- try {
659
- controller?.error(error);
660
- } catch {}
661
- };
662
697
  const stream = new ReadableStream({
663
698
  start(_controller) {
664
- controller = _controller;
699
+ state.controller = _controller;
665
700
  },
666
701
  cancel() {
667
702
  cp?.kill();
@@ -701,13 +736,13 @@ var DevframeTerminalsHost = class {
701
736
  if (currentRun !== runId) return;
702
737
  const text = chunk.toString();
703
738
  stdoutChunks.push(text);
704
- if (!streamClosed) controller?.enqueue(text);
739
+ if (!state.streamClosed) state.controller?.enqueue(text);
705
740
  });
706
741
  cp.process?.stderr?.on("data", (chunk) => {
707
742
  if (currentRun !== runId) return;
708
743
  const text = chunk.toString();
709
744
  stderrChunks.push(text);
710
- if (!streamClosed) controller?.enqueue(text);
745
+ if (!state.streamClosed) state.controller?.enqueue(text);
711
746
  });
712
747
  cp.process?.once("error", (error) => {
713
748
  if (currentRun !== runId) return;
@@ -739,7 +774,7 @@ var DevframeTerminalsHost = class {
739
774
  }
740
775
  cp = createChildProcess();
741
776
  const restart = async () => {
742
- if (streamClosed) throw diagnostics.DF8206({ id: terminal.id });
777
+ if (state.streamClosed) throw diagnostics.DF8206({ id: terminal.id });
743
778
  cp?.kill();
744
779
  cp = createChildProcess();
745
780
  markStatus("running");
@@ -750,7 +785,7 @@ var DevframeTerminalsHost = class {
750
785
  closeStream();
751
786
  markStatus("stopped");
752
787
  };
753
- session = {
788
+ const session = {
754
789
  ...terminal,
755
790
  status: "running",
756
791
  stream,
@@ -761,6 +796,7 @@ var DevframeTerminalsHost = class {
761
796
  terminate,
762
797
  restart
763
798
  };
799
+ state.session = session;
764
800
  this.register(session);
765
801
  return Promise.resolve(session);
766
802
  }
@@ -769,35 +805,14 @@ var DevframeTerminalsHost = class {
769
805
  const { spawn } = await import("zigpty");
770
806
  const cols = executeOptions.cols ?? 80;
771
807
  const rows = executeOptions.rows ?? 24;
772
- let controller;
808
+ const { state, markStatus, closeStream, errorStream } = this.createStreamLifecycle();
773
809
  let pty;
774
810
  let currentResult;
775
811
  let killCurrentRun;
776
812
  let runId = 0;
777
- let streamClosed = false;
778
- let session;
779
- const markStatus = (next) => {
780
- if (session.status === next) return;
781
- session.status = next;
782
- this.events.emit(HUB_EVENTS.bus.terminalsSessionUpdated, session);
783
- };
784
- const closeStream = () => {
785
- if (streamClosed) return;
786
- streamClosed = true;
787
- try {
788
- controller?.close();
789
- } catch {}
790
- };
791
- const errorStream = (error) => {
792
- if (streamClosed) return;
793
- streamClosed = true;
794
- try {
795
- controller?.error(error);
796
- } catch {}
797
- };
798
813
  const stream = new ReadableStream({
799
814
  start(_controller) {
800
- controller = _controller;
815
+ state.controller = _controller;
801
816
  },
802
817
  cancel() {
803
818
  killCurrentRun?.();
@@ -842,7 +857,7 @@ var DevframeTerminalsHost = class {
842
857
  ptyProcess.onData((data) => {
843
858
  const text = typeof data === "string" ? data : data.toString("utf8");
844
859
  outputChunks.push(text);
845
- if (!streamClosed && currentRun === runId) controller?.enqueue(text);
860
+ if (!state.streamClosed && currentRun === runId) state.controller?.enqueue(text);
846
861
  });
847
862
  ptyProcess.onExit(({ exitCode, signal }) => {
848
863
  settle(exitCode, signal);
@@ -882,7 +897,7 @@ var DevframeTerminalsHost = class {
882
897
  reason: error instanceof Error ? error.message : String(error)
883
898
  });
884
899
  }
885
- session = {
900
+ const session = {
886
901
  ...terminal,
887
902
  status: "running",
888
903
  interactive: true,
@@ -915,7 +930,7 @@ var DevframeTerminalsHost = class {
915
930
  markStatus("stopped");
916
931
  },
917
932
  restart: async () => {
918
- if (streamClosed) throw diagnostics.DF8206({ id: terminal.id });
933
+ if (state.streamClosed) throw diagnostics.DF8206({ id: terminal.id });
919
934
  killCurrentRun?.();
920
935
  killCurrentRun = void 0;
921
936
  pty = void 0;
@@ -931,6 +946,7 @@ var DevframeTerminalsHost = class {
931
946
  markStatus("running");
932
947
  }
933
948
  };
949
+ state.session = session;
934
950
  this.register(session);
935
951
  return session;
936
952
  }
@@ -966,7 +982,7 @@ async function resolvePageScriptClientScript(ctx, clientScript, base) {
966
982
  };
967
983
  }
968
984
  /**
969
- * Framework-neutral primitive backing {@link DevframeHubContext.install} —
985
+ * Framework-neutral primitive backing {@link DevframeHubContext.install} -
970
986
  * installs a {@link DevframeDefinition} as a dock inside a hub-aware context:
971
987
  * serves the devframe's SPA at the resolved base path, synthesizes an iframe
972
988
  * dock entry from the definition's metadata, and runs the definition's
@@ -974,20 +990,35 @@ async function resolvePageScriptClientScript(ctx, clientScript, base) {
974
990
  * calling it directly.
975
991
  *
976
992
  * Framework kits wrap `ctx.install` with their own plugin/middleware
977
- * machinery — e.g. `@vitejs/devtools-kit`'s `createPluginFromDevframe`
993
+ * machinery, e.g. `@vitejs/devtools-kit`'s `createPluginFromDevframe`
978
994
  * returns a Vite `Plugin` whose `devtools.setup` ultimately delegates here.
979
995
  */
980
996
  /**
981
997
  * Phase one of an install: run the duplication guard, serve the SPA + meta,
982
998
  * register the iframe dock, and queue the definition's declarative wire
983
- * services — everything up to (but not including) `setup(ctx)`. Returns a
999
+ * services, everything up to (but not including) `setup(ctx)`. Returns a
984
1000
  * deferred setup thunk, or `null` when the devframe was deduplicated.
985
1001
  *
986
1002
  * The hub's initial batch uses this to collect every devframe's services
987
- * across the whole hub, `ready()` them once, and only then run the setups —
1003
+ * across the whole hub, `ready()` them once, and only then run the setups,
988
1004
  * so services are ready before any setup, and a plugin can consume a service
989
1005
  * another plugin declared regardless of mount order.
990
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
+ }
991
1022
  async function prepareDevframe(ctx, d, options = {}) {
992
1023
  const strategy = d.duplicationStrategy ?? "warn";
993
1024
  const isDuplicate = ctx.docks.views.has(d.id);
@@ -1014,17 +1045,7 @@ async function prepareDevframe(ctx, d, options = {}) {
1014
1045
  };
1015
1046
  const clientScript = await resolvePageScriptClientScript(ctx, dockDefaults.clientScript, base);
1016
1047
  if (clientScript) dockDefaults.clientScript = clientScript;
1017
- const clientAssets = resolveClientAssets(d);
1018
- if (clientAssets) {
1019
- if (ctx.host.mountConnectionMeta) await ctx.host.mountConnectionMeta(base);
1020
- else diagnostics.DF8106({
1021
- id,
1022
- name: d.name,
1023
- base
1024
- });
1025
- const distSource = clientAssets;
1026
- ctx.views.hostStatic(base, typeof distSource === "string" ? resolve(distSource) : distSource, d.importMetaUrl);
1027
- }
1048
+ await serveDevframeAssets(ctx, d, id, base);
1028
1049
  ctx.docks.register({
1029
1050
  id,
1030
1051
  title: d.name,
@@ -1037,7 +1058,7 @@ async function prepareDevframe(ctx, d, options = {}) {
1037
1058
  return () => Promise.resolve(d.setup(ctx));
1038
1059
  }
1039
1060
  /**
1040
- * Install a {@link DevframeDefinition} into a hub in one call — serve its SPA,
1061
+ * Install a {@link DevframeDefinition} into a hub in one call: serve its SPA,
1041
1062
  * register its dock, ready its services, and run `setup(ctx)`. The imperative
1042
1063
  * counterpart to the hub's declarative `devframes` list (which batches the
1043
1064
  * phases via {@link prepareDevframe}); use it from `configure(ctx)` or
@@ -1073,7 +1094,7 @@ function resolveControllableSession(sessions, id) {
1073
1094
  return session;
1074
1095
  }
1075
1096
  /**
1076
- * `hub:commands:execute` — Invoke a registered server command by id. The
1097
+ * `hub:commands:execute`: Invoke a registered server command by id. The
1077
1098
  * arguments after `id` are forwarded to the command's `handler(...)`.
1078
1099
  * Returns whatever the handler returns.
1079
1100
  *
@@ -1088,7 +1109,7 @@ const hubCommandsExecute = defineHubRpcFunction({
1088
1109
  } })
1089
1110
  });
1090
1111
  /**
1091
- * `hub:messages:add` — Add a message from a browser client into the hub's
1112
+ * `hub:messages:add`: Add a message from a browser client into the hub's
1092
1113
  * messages subsystem. Marked `from: 'browser'`. Returns the serializable
1093
1114
  * entry (the mutation handle stays server-side).
1094
1115
  *
@@ -1107,7 +1128,7 @@ const hubMessagesAdd = defineHubRpcFunction({
1107
1128
  })).entry;
1108
1129
  } })
1109
1130
  });
1110
- /** `hub:messages:update` — Patch a message by id; returns the updated entry (or `undefined`). */
1131
+ /** `hub:messages:update`: Patch a message by id; returns the updated entry (or `undefined`). */
1111
1132
  const hubMessagesUpdate = defineHubRpcFunction({
1112
1133
  name: HUB_EVENTS.rpc.messagesUpdate,
1113
1134
  type: "action",
@@ -1116,7 +1137,7 @@ const hubMessagesUpdate = defineHubRpcFunction({
1116
1137
  return context.messages.update(id, patch);
1117
1138
  } })
1118
1139
  });
1119
- /** `hub:messages:remove` — Remove a message by id. */
1140
+ /** `hub:messages:remove`: Remove a message by id. */
1120
1141
  const hubMessagesRemove = defineHubRpcFunction({
1121
1142
  name: HUB_EVENTS.rpc.messagesRemove,
1122
1143
  type: "action",
@@ -1124,7 +1145,7 @@ const hubMessagesRemove = defineHubRpcFunction({
1124
1145
  await context.messages.remove(id);
1125
1146
  } })
1126
1147
  });
1127
- /** `hub:messages:clear` — Remove every message. */
1148
+ /** `hub:messages:clear`: Remove every message. */
1128
1149
  const hubMessagesClear = defineHubRpcFunction({
1129
1150
  name: HUB_EVENTS.rpc.messagesClear,
1130
1151
  type: "action",
@@ -1133,7 +1154,7 @@ const hubMessagesClear = defineHubRpcFunction({
1133
1154
  } })
1134
1155
  });
1135
1156
  /**
1136
- * `hub:terminals:write` — Send input to an interactive PTY session spawned
1157
+ * `hub:terminals:write`: Send input to an interactive PTY session spawned
1137
1158
  * via `ctx.terminals.startPtySession`. Lets a hub-aware terminal UI (e.g. the
1138
1159
  * terminals plugin) drive a session owned by another plugin.
1139
1160
  */
@@ -1144,7 +1165,7 @@ const hubTerminalsWrite = defineHubRpcFunction({
1144
1165
  resolveInteractiveSession(context.terminals.sessions, id).write(data);
1145
1166
  } })
1146
1167
  });
1147
- /** `hub:terminals:resize` — Resize an interactive PTY session by id. */
1168
+ /** `hub:terminals:resize`: Resize an interactive PTY session by id. */
1148
1169
  const hubTerminalsResize = defineHubRpcFunction({
1149
1170
  name: HUB_EVENTS.rpc.terminalsResize,
1150
1171
  type: "action",
@@ -1153,7 +1174,7 @@ const hubTerminalsResize = defineHubRpcFunction({
1153
1174
  } })
1154
1175
  });
1155
1176
  /**
1156
- * `hub:terminals:terminate` — Kill a session's process while keeping the
1177
+ * `hub:terminals:terminate`: Kill a session's process while keeping the
1157
1178
  * session registered (its output/scrollback stays). Works for both read-only
1158
1179
  * child-process and interactive PTY sessions, letting a hub-aware terminal UI
1159
1180
  * force-kill a session owned by another plugin.
@@ -1166,7 +1187,7 @@ const hubTerminalsTerminate = defineHubRpcFunction({
1166
1187
  } })
1167
1188
  });
1168
1189
  /**
1169
- * `hub:terminals:restart` — Re-run a session's command in place. Rejected for
1190
+ * `hub:terminals:restart`: Re-run a session's command in place. Rejected for
1170
1191
  * sessions registered with `restartable: false`, whose lifecycle is owned
1171
1192
  * elsewhere.
1172
1193
  */
@@ -1180,7 +1201,7 @@ const hubTerminalsRestart = defineHubRpcFunction({
1180
1201
  } })
1181
1202
  });
1182
1203
  /**
1183
- * `hub:terminals:remove` — Kill a session's process (when it still owns one)
1204
+ * `hub:terminals:remove`: Kill a session's process (when it still owns one)
1184
1205
  * and drop it from the registry, disposing its output stream. Lets a hub-aware
1185
1206
  * terminal UI discard a stopped aggregated session.
1186
1207
  */
@@ -1196,13 +1217,13 @@ const hubTerminalsRemove = defineHubRpcFunction({
1196
1217
  } })
1197
1218
  });
1198
1219
  /**
1199
- * `hub:docks:activate` — Ask the active viewer to switch its focused dock to
1220
+ * `hub:docks:activate`: Ask the active viewer to switch its focused dock to
1200
1221
  * `dockId`, optionally carrying `params` for the target dock to interpret
1201
1222
  * (e.g. `{ sessionId }` for the terminals dock to focus a session).
1202
1223
  *
1203
1224
  * Any connected client may call it, which is the point: a mounted devframe
1204
1225
  * running in its own iframe (on its own RPC client) can steer the host shell's
1205
- * dock selection — client-local state it otherwise can't reach. The hub
1226
+ * dock selection, client-local state it otherwise can't reach. The hub
1206
1227
  * broadcasts the request live to connected clients (the host shell switches)
1207
1228
  * and mirrors it into the `devframe:docks:active` shared state (a dock that
1208
1229
  * mounts in response still converges on it).