devframe 0.9.8 → 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.
Files changed (121) hide show
  1. package/dist/_shared-CCzf9Hub.d.mts +83 -0
  2. package/dist/_shared-U8D-xzrp.mjs +115 -0
  3. package/dist/adapters/build.d.mts +6 -6
  4. package/dist/adapters/build.mjs +33 -36
  5. package/dist/adapters/cac.d.mts +12 -1
  6. package/dist/adapters/cac.mjs +8 -8
  7. package/dist/adapters/dev.d.mts +17 -17
  8. package/dist/adapters/dev.mjs +2 -2
  9. package/dist/adapters/embedded.d.mts +1 -1
  10. package/dist/adapters/initiate.d.mts +31 -28
  11. package/dist/adapters/initiate.mjs +1 -1
  12. package/dist/adapters/mcp.d.mts +28 -14
  13. package/dist/adapters/mcp.mjs +76 -40
  14. package/dist/{agent-tool-name-EgfoFO8C.mjs → agent-tool-name-DGzeuWcO.mjs} +2 -2
  15. package/dist/cli/main.d.mts +1 -1
  16. package/dist/cli/main.mjs +41 -17
  17. package/dist/client/index.d.mts +36 -36
  18. package/dist/client/index.mjs +90 -52
  19. package/dist/{client-assets-C7SXgTWY.mjs → client-assets-DNdhagGm.mjs} +2 -2
  20. package/dist/constants.d.mts +8 -7
  21. package/dist/constants.mjs +7 -6
  22. package/dist/{context-omlma_1n.mjs → context-B29Oh0oA.mjs} +166 -93
  23. package/dist/{context-DftYyXaR.d.mts → context-BJiKJqyW.d.mts} +5 -5
  24. package/dist/{context-CLF4-8Pp.d.mts → context-CHW4BZOj.d.mts} +203 -134
  25. package/dist/{context-KUqAfawQ.mjs → context-jXO5vzjH.mjs} +3 -3
  26. package/dist/{crypto-token-XCqTSMg9.mjs → crypto-token-TVbGGFuj.mjs} +1 -1
  27. package/dist/{dev-CzkPCJwi.mjs → dev-CRYHx7iG.mjs} +73 -40
  28. package/dist/{diagnostics-BXWW3VK-.mjs → diagnostics-DZA6KQ_Z.mjs} +32 -19
  29. package/dist/{events-4daw0uK9.mjs → events-BV4Dj59a.mjs} +2 -2
  30. package/dist/{services-install-CqqQVls5.mjs → host-agent-CKqSTEX9.mjs} +15 -151
  31. package/dist/{host-h3-fRbF9yor.mjs → host-h3-BROI6SQ4.mjs} +2 -2
  32. package/dist/in-page-channel/index.d.mts +14 -14
  33. package/dist/in-page-channel/index.mjs +63 -54
  34. package/dist/{index-D3k0BIhe.d.mts → index-CNC390b_.d.mts} +4 -4
  35. package/dist/{index-BzyopqxV.d.mts → index-pIVt92jP.d.mts} +9 -9
  36. package/dist/index.d.mts +8 -8
  37. package/dist/index.mjs +2 -2
  38. package/dist/{instance-registry-BjlMQbFB.mjs → instance-registry-DCq762YJ.mjs} +9 -9
  39. package/dist/{instance-shell-Tcp5hGoq.d.mts → instance-shell-DZipaP9k.d.mts} +18 -18
  40. package/dist/{instance-shell-CpVW4zG0.mjs → instance-shell-DvYCwrDy.mjs} +223 -67
  41. package/dist/internal/index.d.mts +45 -26
  42. package/dist/internal/index.mjs +10 -10
  43. package/dist/node/auth.d.mts +2 -2
  44. package/dist/node/auth.mjs +2 -2
  45. package/dist/node/hub-internals.d.mts +2 -2
  46. package/dist/node/hub-internals.mjs +2 -2
  47. package/dist/node/index.d.mts +5 -5
  48. package/dist/node/index.mjs +2 -2
  49. package/dist/{nostics-SPGDgHEP.d.mts → nostics-0sKe5HIj.d.mts} +2 -2
  50. package/dist/{nostics-CzECRXpE.mjs → nostics-D0PvLZsn.mjs} +1 -1
  51. package/dist/origin-DGDH8BI9.d.mts +45 -0
  52. package/dist/origin-DZit9g29.mjs +80 -0
  53. package/dist/recipes/common-rpc-functions.d.mts +5 -5
  54. package/dist/recipes/common-rpc-functions.mjs +3 -3
  55. package/dist/recipes/interactive-auth.d.mts +6 -6
  56. package/dist/recipes/interactive-auth.mjs +6 -6
  57. package/dist/{remote-assets-DezQmPQU.mjs → remote-assets-DMFzLSoH.mjs} +27 -7
  58. package/dist/{remote-assets-B13_vRkJ.d.mts → remote-assets-eBvSKZMS.d.mts} +7 -7
  59. package/dist/{revoke-COLW0cOi.mjs → revoke-cqztiCX4.mjs} +1 -1
  60. package/dist/rpc/dump.d.mts +19 -12
  61. package/dist/rpc/dump.mjs +2 -2
  62. package/dist/rpc/index.d.mts +2 -2
  63. package/dist/rpc/index.mjs +2 -3
  64. package/dist/rpc/transports/sse-client.d.mts +1 -1
  65. package/dist/rpc/transports/sse-client.mjs +30 -14
  66. package/dist/rpc/transports/sse-server.d.mts +4 -4
  67. package/dist/rpc/transports/sse-server.mjs +7 -7
  68. package/dist/rpc/transports/ws-bun.d.mts +5 -5
  69. package/dist/rpc/transports/ws-bun.mjs +8 -2
  70. package/dist/rpc/transports/ws-client.d.mts +1 -1
  71. package/dist/rpc/transports/ws-client.mjs +2 -2
  72. package/dist/rpc/transports/ws-deno.d.mts +4 -4
  73. package/dist/rpc/transports/ws-deno.mjs +8 -2
  74. package/dist/rpc/transports/ws-server.d.mts +2 -1
  75. package/dist/rpc/transports/ws-server.mjs +232 -1
  76. package/dist/{rpc-core-DlHd276T.mjs → rpc-core-CvkU23bQ.mjs} +14 -2
  77. package/dist/{rpc-core-B3W3z9ff.d.mts → rpc-core-DsyDViQy.d.mts} +4 -4
  78. package/dist/serialization-DFg5sUt6.mjs +116 -0
  79. package/dist/services-install-Ben4yh52.mjs +154 -0
  80. package/dist/session-DPfj6zQG.mjs +16 -0
  81. package/dist/{shared-state--sduc6Xi.mjs → shared-state-CaxB1q0h.mjs} +2 -2
  82. package/dist/{simple-schema-DQPZrAaZ.mjs → simple-schema-Bi40l-P6.mjs} +2 -2
  83. package/dist/{sse-client-DwX021ST.d.mts → sse-client-BQ4jS3eO.d.mts} +4 -4
  84. package/dist/{state-CK9LjrnT.mjs → state-Mwt6--el.mjs} +17 -9
  85. package/dist/{storage-BXNVhKyR.mjs → storage-B70AXK64.mjs} +2 -2
  86. package/dist/types/index.d.mts +6 -6
  87. package/dist/{types-CMdPYhOx.d.mts → types-Dq4i156u.d.mts} +17 -17
  88. package/dist/utils/agent-tool-name.d.mts +2 -2
  89. package/dist/utils/agent-tool-name.mjs +2 -2
  90. package/dist/utils/colors.d.mts +1 -1
  91. package/dist/utils/crypto-token.d.mts +1 -1
  92. package/dist/utils/crypto-token.mjs +1 -1
  93. package/dist/utils/events.d.mts +1 -1
  94. package/dist/utils/nostics.d.mts +1 -1
  95. package/dist/utils/nostics.mjs +1 -1
  96. package/dist/utils/open.mjs +1 -1
  97. package/dist/utils/origin.d.mts +2 -0
  98. package/dist/utils/origin.mjs +80 -0
  99. package/dist/utils/remote-assets.d.mts +2 -2
  100. package/dist/utils/remote-assets.mjs +1 -1
  101. package/dist/utils/serve-static.d.mts +4 -4
  102. package/dist/utils/serve-static.mjs +56 -19
  103. package/dist/utils/shared-state.d.mts +1 -1
  104. package/dist/utils/shared-state.mjs +1 -1
  105. package/dist/utils/simple-schema.d.mts +5 -5
  106. package/dist/utils/simple-schema.mjs +2 -2
  107. package/dist/utils/streaming-channel.d.mts +1 -1
  108. package/dist/utils/streaming-channel.mjs +2 -2
  109. package/dist/utils/when.d.mts +5 -5
  110. package/dist/{validation-YrZH6atx.mjs → validation-CfV1s0SF.mjs} +2 -2
  111. package/dist/{wire-codec-0K-o5MYW.mjs → wire-codec-BHGt0sSs.mjs} +3 -3
  112. package/dist/{static-DV_qUSRS.mjs → write-BuLCrn9h.mjs} +153 -137
  113. package/dist/{ws-client-7_lZi-B9.d.mts → ws-client-Xpxt0oPA.d.mts} +3 -3
  114. package/dist/{ws-server-VyHlanzy.d.mts → ws-server-D2nv68Hg.d.mts} +13 -37
  115. package/package.json +3 -6
  116. package/skills/devframe/SKILL.md +5 -3
  117. package/dist/_shared-BM3PdYli.mjs +0 -68
  118. package/dist/_shared-mtXpcJIv.d.mts +0 -41
  119. package/dist/diagnostics-CD8nlgll.mjs +0 -39
  120. package/dist/serialization-BGzEwAdr.mjs +0 -77
  121. package/dist/ws-server-Y_AumUQH.mjs +0 -294
@@ -1,5 +1,6 @@
1
- import { t as DEVFRAME_EVENTS } from "./events-4daw0uK9.mjs";
1
+ import { t as DEVFRAME_EVENTS } from "./events-BV4Dj59a.mjs";
2
2
  //#region src/constants.ts
3
+ /** Devframe runtime routes and static output conventions. */
3
4
  const DEVFRAME_CONNECTION_META_FILENAME = "__connection.json";
4
5
  /**
5
6
  * Global key holding the serializable connection prepared by
@@ -10,7 +11,7 @@ const DEVFRAME_CONNECTION_KEY = "__DEVFRAME_CONNECTION__";
10
11
  /**
11
12
  * Route the WebSocket RPC endpoint is bound to, relative to a devframe's
12
13
  * base path. Sits next to `__connection.json` so the deployed SPA can reach
13
- * it on the same origin it loaded from — the dev server shares one port for
14
+ * it on the same origin it loaded from: the dev server shares one port for
14
15
  * both HTTP and WS, and a host server (Vite, etc.) can mount the WS upgrade
15
16
  * handler here without colliding with its own routes (HMR, asset serving).
16
17
  */
@@ -20,7 +21,7 @@ const DEVFRAME_WS_ROUTE = "__ws";
20
21
  * path. A single method-dispatched route: `GET` opens the event stream
21
22
  * (server→client), `POST` carries RPC frames (client→server). Sits next to
22
23
  * `__connection.json` so the deployed SPA reaches it on the same origin it
23
- * loaded from — the transport for hosts and proxies where the WebSocket
24
+ * loaded from; the transport for hosts and proxies where the WebSocket
24
25
  * upgrade isn't available.
25
26
  */
26
27
  const DEVFRAME_SSE_ROUTE = "__sse";
@@ -34,7 +35,7 @@ const DEVFRAME_SSE_SESSION_HEADER = "x-birpc-session";
34
35
  /**
35
36
  * Route the Streamable-HTTP MCP endpoint is bound to, relative to a
36
37
  * devframe's base path. Sits next to `__connection.json` and the WS route
37
- * so an MCP client reaches it on the same origin the SPA loaded from — the
38
+ * so an MCP client reaches it on the same origin the SPA loaded from; the
38
39
  * dev server shares one port for HTTP, WS, and MCP. Opt-in via `cli.mcp`.
39
40
  */
40
41
  const DEVFRAME_MCP_ROUTE = "__mcp";
@@ -79,7 +80,7 @@ const DEVFRAME_VIEWER_ORIGIN_TOKEN_QUERY_PARAM = "devframe_viewer_origin_token";
79
80
  /**
80
81
  * `postMessage` type the remote-assets fallback page posts to `window.parent`
81
82
  * on load. That page is what a devframe serves (with a 502) when its client
82
- * assets can be reached neither locally nor through their CDN provider — the
83
+ * assets can be reached neither locally nor through their CDN provider; the
83
84
  * message lets an embedding viewer replace the bare page with its own UI
84
85
  * (`@devframes/hub-ui` does, in its iframe view). Payload shape:
85
86
  * `RemoteAssetsErrorMessage` (`devframe/types`).
@@ -87,7 +88,7 @@ const DEVFRAME_VIEWER_ORIGIN_TOKEN_QUERY_PARAM = "devframe_viewer_origin_token";
87
88
  const DEVFRAME_REMOTE_ASSETS_ERROR_MESSAGE_TYPE = DEVFRAME_EVENTS.postMessage.remoteAssetsError;
88
89
  /**
89
90
  * Prefix that marks an RPC method as callable before a connection is
90
- * trusted. This is the *only* rule the pre-trust gate applies — there is no
91
+ * trusted. This is the *only* rule the pre-trust gate applies; there is no
91
92
  * per-method allowlist. Any handshake method a host adapter needs to reach
92
93
  * before authentication must be named `anonymous:<rest>` (e.g.
93
94
  * `anonymous:devframe:auth`).
@@ -1,22 +1,27 @@
1
- import { i as defineDiagnostics } from "./nostics-CzECRXpE.mjs";
2
- import { t as diagnostics } from "./diagnostics-CD8nlgll.mjs";
1
+ import { i as defineDiagnostics } from "./nostics-D0PvLZsn.mjs";
2
+ import { r as diagnostics } from "./serialization-DFg5sUt6.mjs";
3
3
  import { RpcFunctionsCollectorBase } from "./rpc/index.mjs";
4
4
  import { defineRpcFunction } from "./index.mjs";
5
- import { t as DEVFRAME_EVENTS } from "./events-4daw0uK9.mjs";
5
+ import { t as DEVFRAME_EVENTS } from "./events-BV4Dj59a.mjs";
6
6
  import { DEVFRAME_SERVICES_STATE_KEY } from "./constants.mjs";
7
- import { t as diagnostics$1 } from "./diagnostics-BXWW3VK-.mjs";
8
- import { a as DevframeAgentHost, i as satisfiesVersionRange, n as expandResolveFrom, r as importServicePackage, s as createEventEmitter, t as deepMergeOptionSets } from "./services-install-CqqQVls5.mjs";
9
- import { n as createDebug, t as resolveStaticAssetsSource } from "./remote-assets-DezQmPQU.mjs";
10
- import { t as createStorage } from "./storage-BXNVhKyR.mjs";
7
+ import { t as diagnostics$1 } from "./diagnostics-DZA6KQ_Z.mjs";
8
+ import { r as createEventEmitter, t as DevframeAgentHost } from "./host-agent-CKqSTEX9.mjs";
9
+ import { n as createDebug, t as resolveStaticAssetsSource } from "./remote-assets-DMFzLSoH.mjs";
10
+ import { i as satisfiesVersionRange, n as expandResolveFrom, r as importServicePackage, t as deepMergeOptionSets } from "./services-install-Ben4yh52.mjs";
11
+ import { t as createStorage } from "./storage-B70AXK64.mjs";
12
+ import { join } from "pathe";
11
13
  import { createSharedState } from "devframe/utils/shared-state";
12
14
  import process from "node:process";
13
- import { join } from "pathe";
14
15
  import { existsSync } from "node:fs";
15
16
  //#region src/node/host-diagnostics.ts
16
17
  var DevframeDiagnosticsHost = class {
17
18
  context;
18
19
  _registry = {};
19
20
  logger = new Proxy({}, { get: (_, code) => this._registry[code] });
21
+ /**
22
+ * Already pre-wires devframe's ANSI console reporter, so no extra merging
23
+ * needed here, the host's `defineDiagnostics` just is the shared one.
24
+ */
20
25
  defineDiagnostics = defineDiagnostics;
21
26
  constructor(context, initialDefinitions = []) {
22
27
  this.context = context;
@@ -123,7 +128,16 @@ function createRpcSharedStateServerHost(rpc) {
123
128
  if (!sharedState.has(key)) return void 0;
124
129
  return (await host.get(key)).value();
125
130
  },
126
- dump: () => ({ inputs: host.keys().map((key) => [key]) })
131
+ /**
132
+ * Pre-compute snapshots for the build-mode static dump so the SPA
133
+ * can read them without a live server. The `undefined` fallback makes
134
+ * a key the build never created resolve like the live handler does
135
+ * (returning `undefined`) instead of failing the dump lookup.
136
+ */
137
+ dump: () => ({
138
+ inputs: host.keys().map((key) => [key]),
139
+ fallback: void 0
140
+ })
127
141
  });
128
142
  rpc.register({
129
143
  name: "devframe:rpc:server-state:set",
@@ -158,7 +172,7 @@ var StreamClosedError = class extends Error {
158
172
  name = "StreamClosedError";
159
173
  };
160
174
  /**
161
- * Build a server-side stream sink. RPC-agnostic — the RPC host wires
175
+ * Build a server-side stream sink. RPC-agnostic; the RPC host wires
162
176
  * `events.on('chunk' | 'end')` to broadcast, and reads `buffer` to replay
163
177
  * for late or reconnecting subscribers.
164
178
  */
@@ -229,7 +243,7 @@ function createStreamSink(options = {}) {
229
243
  };
230
244
  }
231
245
  /**
232
- * Build a client-side stream reader. RPC-agnostic — the RPC host calls
246
+ * Build a client-side stream reader. RPC-agnostic; the RPC host calls
233
247
  * `_push(seq, chunk)` on each incoming chunk and `_end(error?)` on the
234
248
  * terminal frame. Consumers iterate with `for await` or pipe `readable`.
235
249
  */
@@ -412,7 +426,7 @@ function streamKey(channel, id) {
412
426
  }
413
427
  /**
414
428
  * Build the server-side streaming host. Mirrors the layout of
415
- * `createRpcSharedStateServerHost` — registers a fixed set of internal
429
+ * `createRpcSharedStateServerHost`: registers a fixed set of internal
416
430
  * RPC methods (`subscribe` / `unsubscribe` / `cancel`) once, then per-channel
417
431
  * state lives in a `Map<channelName, ChannelState>`.
418
432
  */
@@ -573,6 +587,10 @@ function createRpcStreamingServerHost(rpc) {
573
587
  name,
574
588
  options: {
575
589
  replayWindow,
590
+ /**
591
+ * Default to a 30-second hold when replay is enabled so late
592
+ * subscribers can join after the producer finishes.
593
+ */
576
594
  closedStreamRetention: opts.closedStreamRetention ?? (replayWindow > 0 ? 3e4 : 0)
577
595
  },
578
596
  streams: /* @__PURE__ */ new Map(),
@@ -669,37 +687,39 @@ function createRpcStreamingServerHost(rpc) {
669
687
  id: key.slice(sepIdx + 1)
670
688
  };
671
689
  }
690
+ function dropOutboundSubscriptions(meta) {
691
+ if (!meta.subscribedStreams) return;
692
+ for (const key of meta.subscribedStreams) {
693
+ const parsed = parseKey(key);
694
+ const state = parsed && channels.get(parsed.channelName);
695
+ const record = state && parsed && state.streams.get(parsed.id);
696
+ if (!parsed || !state || !record) continue;
697
+ record.subscribers.delete(meta);
698
+ if (record.subscribers.size === 0 && !record.sink.closed) record.sink.abort("all subscribers disconnected");
699
+ maybeFreeStream(state, parsed.id);
700
+ }
701
+ meta.subscribedStreams.clear();
702
+ }
703
+ function endInboundUploads(meta) {
704
+ if (!meta.uploadingStreams) return;
705
+ for (const key of meta.uploadingStreams) {
706
+ const parsed = parseKey(key);
707
+ const state = parsed && channels.get(parsed.channelName);
708
+ const record = state && parsed && state.inbound.get(parsed.id);
709
+ if (!parsed || !state || !record) continue;
710
+ record.reader._end({
711
+ name: "UploadDisconnected",
712
+ message: "Uploader disconnected before completing the stream"
713
+ });
714
+ state.inbound.delete(parsed.id);
715
+ }
716
+ meta.uploadingStreams.clear();
717
+ }
672
718
  return {
673
719
  create: createChannel,
674
720
  _onSessionDisconnected(meta) {
675
- if (meta.subscribedStreams) {
676
- for (const key of meta.subscribedStreams) {
677
- const parsed = parseKey(key);
678
- if (!parsed) continue;
679
- const state = channels.get(parsed.channelName);
680
- const record = state?.streams.get(parsed.id);
681
- if (!state || !record) continue;
682
- record.subscribers.delete(meta);
683
- if (record.subscribers.size === 0 && !record.sink.closed) record.sink.abort("all subscribers disconnected");
684
- maybeFreeStream(state, parsed.id);
685
- }
686
- meta.subscribedStreams.clear();
687
- }
688
- if (meta.uploadingStreams) {
689
- for (const key of meta.uploadingStreams) {
690
- const parsed = parseKey(key);
691
- if (!parsed) continue;
692
- const state = channels.get(parsed.channelName);
693
- const record = state?.inbound.get(parsed.id);
694
- if (!state || !record) continue;
695
- record.reader._end({
696
- name: "UploadDisconnected",
697
- message: "Uploader disconnected before completing the stream"
698
- });
699
- state.inbound.delete(parsed.id);
700
- }
701
- meta.uploadingStreams.clear();
702
- }
721
+ dropOutboundSubscriptions(meta);
722
+ endInboundUploads(meta);
703
723
  }
704
724
  };
705
725
  }
@@ -763,6 +783,17 @@ var RpcFunctionsHostImpl = class extends RpcFunctionsCollectorBase {
763
783
  //#endregion
764
784
  //#region src/node/host-services.ts
765
785
  const debug = createDebug("devframe:services");
786
+ /**
787
+ * Per-RPC-host registry of the services already installed against it, keyed
788
+ * by the RPC host object. A single wire service can be declared from more
789
+ * than one devframe context that shares one RPC host (e.g. a kit that mounts
790
+ * a definition alongside another context, so both iterate `def.services`).
791
+ * The per-instance `installed` guard can't see across those sibling hosts, so
792
+ * the second context would re-run the service factory and re-register its RPC,
793
+ * hitting DF0021. Sharing the registry by RPC host makes the first install win
794
+ * across every context on that host.
795
+ */
796
+ const installedByRpcHost = /* @__PURE__ */ new WeakMap();
766
797
  function isServiceDefinition(input) {
767
798
  return typeof input.setup === "function";
768
799
  }
@@ -784,13 +815,24 @@ function validateServiceDefinition(def) {
784
815
  });
785
816
  }
786
817
  /**
818
+ * Merge every installer's option set in declaration order (the default
819
+ * deep-merge unions arrays and lets later scalars win; a service may override
820
+ * with its own `mergeOptions`).
821
+ */
822
+ function mergeInstallerOptions(def, entries) {
823
+ const sets = entries.map((entry) => entry.input.options).filter((options) => options !== void 0);
824
+ if (def.mergeOptions) return def.mergeOptions(sets);
825
+ return sets.length > 0 ? deepMergeOptionSets(sets) : void 0;
826
+ }
827
+ /**
787
828
  * Cross-plugin service registry (see `types/services.ts` for the contract).
788
829
  * Values are held per context instance; `whenAvailable` subscriptions make
789
- * the mechanism robust against setup ordering between provider and consumer.
830
+ * the mechanism work regardless of setup ordering between provider and
831
+ * consumer.
790
832
  *
791
833
  * On top of the in-process `provide`/`get` tier, this host implements the
792
834
  * **wire-service** lifecycle: `install()` queues definitions/descriptors,
793
- * `ready()` fires the collect-then-setup barrier — importing descriptor
835
+ * `ready()` fires the collect-then-setup barrier: importing descriptor
794
836
  * packages, merging option sets per service, constructing each service once,
795
837
  * providing its node API under the package name, and advertising it to
796
838
  * clients through the `devframe:services` shared state.
@@ -800,11 +842,27 @@ var DevframeServicesHostImpl = class {
800
842
  services = /* @__PURE__ */ new Map();
801
843
  listeners = /* @__PURE__ */ new Map();
802
844
  pending = /* @__PURE__ */ new Map();
803
- installed = /* @__PURE__ */ new Map();
845
+ localInstalled = /* @__PURE__ */ new Map();
804
846
  readyPromise;
805
847
  constructor(context) {
806
848
  this.context = context;
807
849
  }
850
+ /**
851
+ * The install-dedup registry, shared across every services host that shares
852
+ * this host's RPC host so the first install of a package wins across sibling
853
+ * contexts. Falls back to a per-instance map when there is no RPC host (a
854
+ * context-less host only exercises the in-process `provide`/`get` tier).
855
+ */
856
+ get installed() {
857
+ const rpc = this.context?.rpc;
858
+ if (!rpc) return this.localInstalled;
859
+ let registry = installedByRpcHost.get(rpc);
860
+ if (!registry) {
861
+ registry = /* @__PURE__ */ new Map();
862
+ installedByRpcHost.set(rpc, registry);
863
+ }
864
+ return registry;
865
+ }
808
866
  provide(id, service) {
809
867
  const key = id;
810
868
  if (this.services.has(key)) throw diagnostics$1.DF0037({ id: key });
@@ -882,47 +940,75 @@ var DevframeServicesHostImpl = class {
882
940
  }
883
941
  async installPackage(pkg, entries) {
884
942
  if (this.installed.has(pkg)) {
885
- diagnostics$1.DF0066({ package: pkg });
886
- return this.installed.get(pkg);
943
+ const api = this.installed.get(pkg);
944
+ if (this.services.has(pkg)) diagnostics$1.DF0066({ package: pkg });
945
+ else this.provide(pkg, api);
946
+ return api;
887
947
  }
888
948
  let def = entries.filter((entry) => isServiceDefinition(entry.input))[0]?.input;
889
949
  if (!def) {
890
- const required = entries.map((entry) => entry.input).some((descriptor) => descriptor.required === true);
891
- const cwd = this.context?.cwd ?? process.cwd();
892
- const resolveFroms = [
893
- ...entries.map((entry) => entry.resolveFrom && expandResolveFrom(entry.resolveFrom, cwd)),
894
- this.context?.workspaceRoot,
895
- cwd
896
- ];
897
- let mod;
898
- try {
899
- mod = await importServicePackage(pkg, resolveFroms);
900
- } catch (error) {
901
- const reason = error instanceof Error ? error.message : String(error);
902
- if (required) throw diagnostics$1.DF0067({
903
- package: pkg,
904
- reason,
905
- cause: error
906
- });
907
- debug("optional service %s not importable, skipping: %s", pkg, reason);
908
- return;
909
- }
910
- const factory = mod.default;
911
- if (typeof factory !== "function") throw diagnostics$1.DF0070({
912
- package: pkg,
913
- reason: "its default export is not a factory function"
914
- });
915
- def = await factory();
916
- if (!def || typeof def.setup !== "function") throw diagnostics$1.DF0070({
950
+ def = await this.importServiceDefinition(pkg, entries);
951
+ if (!def) return void 0;
952
+ }
953
+ this.checkVersionRanges(pkg, def, entries);
954
+ const options = mergeInstallerOptions(def, entries);
955
+ if (!this.context) throw diagnostics$1.DF0070({
956
+ package: pkg,
957
+ reason: "this services host has no node context to install into"
958
+ });
959
+ debug("installing service %s@%s (scope %s)", def.package, def.version, def.scope);
960
+ const scoped = this.context.scope(def.scope);
961
+ const api = await def.setup(scoped, options === void 0 ? {} : { options });
962
+ this.installed.set(pkg, api);
963
+ this.provide(def.package, api);
964
+ await this.advertise(def);
965
+ return api;
966
+ }
967
+ /**
968
+ * Import a descriptor-only package, run its factory, and validate the
969
+ * resulting definition. Returns `undefined` when an optional package can't
970
+ * be imported (the install is skipped); throws for a required one.
971
+ */
972
+ async importServiceDefinition(pkg, entries) {
973
+ const required = entries.map((entry) => entry.input).some((descriptor) => descriptor.required === true);
974
+ const cwd = this.context?.cwd ?? process.cwd();
975
+ const resolveFroms = [
976
+ ...entries.map((entry) => entry.resolveFrom && expandResolveFrom(entry.resolveFrom, cwd)),
977
+ this.context?.workspaceRoot,
978
+ cwd
979
+ ];
980
+ let mod;
981
+ try {
982
+ mod = await importServicePackage(pkg, resolveFroms);
983
+ } catch (error) {
984
+ const reason = error instanceof Error ? error.message : String(error);
985
+ if (required) throw diagnostics$1.DF0067({
917
986
  package: pkg,
918
- reason: "its factory did not return a definition with a `setup` function"
987
+ reason,
988
+ cause: error
919
989
  });
920
- if (typeof def.package !== "string" || def.package.length === 0) def = {
921
- ...def,
922
- package: pkg
923
- };
924
- validateServiceDefinition(def);
990
+ debug("optional service %s not importable, skipping: %s", pkg, reason);
991
+ return;
925
992
  }
993
+ const factory = mod.default;
994
+ if (typeof factory !== "function") throw diagnostics$1.DF0070({
995
+ package: pkg,
996
+ reason: "its default export is not a factory function"
997
+ });
998
+ let def = await factory();
999
+ if (!def || typeof def.setup !== "function") throw diagnostics$1.DF0070({
1000
+ package: pkg,
1001
+ reason: "its factory did not return a definition with a `setup` function"
1002
+ });
1003
+ if (typeof def.package !== "string" || def.package.length === 0) def = {
1004
+ ...def,
1005
+ package: pkg
1006
+ };
1007
+ validateServiceDefinition(def);
1008
+ return def;
1009
+ }
1010
+ /** Version-range checks against the resolved definition's real version. */
1011
+ checkVersionRanges(pkg, def, entries) {
926
1012
  for (const entry of entries) {
927
1013
  const descriptor = entry.input;
928
1014
  if (isServiceDefinition(entry.input) || typeof descriptor.version !== "string") continue;
@@ -938,19 +1024,6 @@ var DevframeServicesHostImpl = class {
938
1024
  installed: def.version
939
1025
  });
940
1026
  }
941
- const sets = entries.map((entry) => entry.input.options).filter((options) => options !== void 0);
942
- const options = def.mergeOptions ? def.mergeOptions(sets) : sets.length > 0 ? deepMergeOptionSets(sets) : void 0;
943
- if (!this.context) throw diagnostics$1.DF0070({
944
- package: pkg,
945
- reason: "this services host has no node context to install into"
946
- });
947
- debug("installing service %s@%s (scope %s)", def.package, def.version, def.scope);
948
- const scoped = this.context.scope(def.scope);
949
- const api = await def.setup(scoped, options === void 0 ? {} : { options });
950
- this.installed.set(def.package, api);
951
- this.provide(def.package, api);
952
- await this.advertise(def);
953
- return api;
954
1027
  }
955
1028
  advertisementState() {
956
1029
  return this.context.rpc.sharedState.get(DEVFRAME_SERVICES_STATE_KEY, { initialValue: {} });
@@ -1019,7 +1092,7 @@ const agentListResources = defineRpcFunction({
1019
1092
  /**
1020
1093
  * Built-in agent introspection RPC functions. Registered automatically
1021
1094
  * by `createHostContext`. Not themselves agent-exposed (no `agent`
1022
- * field) — they power the MCP adapter and any future agent CLI.
1095
+ * field); they power the MCP adapter and any future agent CLI.
1023
1096
  */
1024
1097
  const BUILTIN_AGENT_RPC = [
1025
1098
  defineRpcFunction({
@@ -1169,7 +1242,7 @@ function createScopedNodeContext(context, namespace) {
1169
1242
  * Framework- and build-tool-agnostic core of the Devframe node context.
1170
1243
  * Wires the RPC host, view (HTTP file-serving) host, diagnostics, and
1171
1244
  * agent subsystems. Host adapters can wrap this to augment `ctx` with
1172
- * extra surfaces — for example, `@vitejs/devtools-kit`'s
1245
+ * extra surfaces; for example, `@vitejs/devtools-kit`'s
1173
1246
  * `createKitContext` attaches `docks`, `terminals`, `messages`, and
1174
1247
  * `commands` when mounted into Vite DevTools.
1175
1248
  */
@@ -1,4 +1,4 @@
1
- import { k as SharedState, o as DevframeNodeContext } from "./context-CLF4-8Pp.mjs";
1
+ import { k as SharedState, o as DevframeNodeContext } from "./context-CHW4BZOj.mjs";
2
2
  //#region src/node/hub-internals/context.d.ts
3
3
  interface InternalAnonymousAuthStorage {
4
4
  trusted: Record<string, {
@@ -10,7 +10,7 @@ interface InternalAnonymousAuthStorage {
10
10
  }
11
11
  interface RemoteTokenRecord {
12
12
  dockId: string;
13
- /** Dock URL origin — matched against WS handshake `Origin` header when `originLock` is on. */
13
+ /** Dock URL origin, matched against WS handshake `Origin` header when `originLock` is on. */
14
14
  origin: string;
15
15
  originLock: boolean;
16
16
  }
@@ -24,7 +24,7 @@ interface DevframeInternalContext {
24
24
  */
25
25
  revokeAuthToken: (token: string) => Promise<void>;
26
26
  /**
27
- * Session-only tokens issued to remote-UI iframe docks. Not persisted —
27
+ * Session-only tokens issued to remote-UI iframe docks. Not persisted;
28
28
  * regenerated on every dev-server restart.
29
29
  */
30
30
  remoteTokens: Map<string, RemoteTokenRecord>;
@@ -45,8 +45,8 @@ interface DevframeInternalContext {
45
45
  url: string;
46
46
  };
47
47
  /**
48
- * Set {@link DevframeInternalContext.wsEndpoint} and notify subscribers —
49
- * the WS-binding tiers (side-car, shared-server, and the `unbound` tier's
48
+ * Set {@link DevframeInternalContext.wsEndpoint} and notify subscribers.
49
+ * The WS-binding tiers (side-car, shared-server, and the `unbound` tier's
50
50
  * `attach()`) call this once the socket is bound (or `undefined` once torn
51
51
  * down) instead of assigning the field directly, so anything that already
52
52
  * projected the endpoint (a hub's remote-dock URLs, registered before an