devframe 0.5.4 → 0.6.0-beta.2

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 (84) hide show
  1. package/dist/{_shared-CUFqO4kJ.mjs → _shared-bWRzeSa0.mjs} +2 -3
  2. package/dist/adapters/build.d.mts +1 -1
  3. package/dist/adapters/build.mjs +7 -7
  4. package/dist/adapters/cli.d.mts +1 -1
  5. package/dist/adapters/cli.mjs +2 -2
  6. package/dist/adapters/dev.d.mts +8 -2
  7. package/dist/adapters/dev.mjs +1 -1
  8. package/dist/adapters/embedded.d.mts +1 -1
  9. package/dist/adapters/mcp.d.mts +1 -1
  10. package/dist/adapters/mcp.mjs +4 -4
  11. package/dist/client/index.d.mts +148 -6
  12. package/dist/client/index.mjs +257 -44
  13. package/dist/constants.d.mts +17 -1
  14. package/dist/constants.mjs +17 -1
  15. package/dist/context-3x_bgBgZ.mjs +48 -0
  16. package/dist/{context-DTRcO_UH.d.mts → context-BEVByy_Z.d.mts} +1 -1
  17. package/dist/{dev-Cv43GfqM.mjs → dev-BaX8fLH2.mjs} +39 -13
  18. package/dist/{devframe-BuR6n9ZD.d.mts → devframe-D-gr9sBx.d.mts} +365 -15
  19. package/dist/{diagnostics-GitRGKbr.mjs → diagnostics-BXwBQmoN.mjs} +1 -1
  20. package/dist/{dump-9lKIJTLh.mjs → dump-DoXkj4ku.mjs} +1 -1
  21. package/dist/helpers/vite.d.mts +1 -1
  22. package/dist/helpers/vite.mjs +11 -11
  23. package/dist/{host-h3-Dgpgr1Ul.mjs → host-h3-C5SZlk03.mjs} +134 -6
  24. package/dist/{index-C7M1hnvL.d.mts → index-CxaPKDXX.d.mts} +2 -2
  25. package/dist/{index-DH2sBIwd.d.mts → index-DXHWuwJk.d.mts} +1 -1
  26. package/dist/index.d.mts +4 -4
  27. package/dist/index.mjs +1 -1
  28. package/dist/{launch-editor-BbNhtg7b.mjs → launch-editor-DPfltGA4.mjs} +88 -13
  29. package/dist/node/auth.d.mts +43 -23
  30. package/dist/node/auth.mjs +84 -37
  31. package/dist/node/hub-internals.d.mts +2 -2
  32. package/dist/node/hub-internals.mjs +2 -2
  33. package/dist/node/index.d.mts +22 -6
  34. package/dist/node/index.mjs +4 -4
  35. package/dist/recipes/open-helpers.d.mts +1 -1
  36. package/dist/recipes/open-helpers.mjs +3 -3
  37. package/dist/revoke-ENfxWkFK.mjs +79 -0
  38. package/dist/rpc/dump.d.mts +1 -1
  39. package/dist/rpc/dump.mjs +1 -1
  40. package/dist/rpc/index.d.mts +3 -3
  41. package/dist/rpc/index.mjs +4 -4
  42. package/dist/rpc/transports/ws-client.d.mts +1 -1
  43. package/dist/rpc/transports/ws-client.mjs +2 -2
  44. package/dist/rpc/transports/ws-server.d.mts +2 -2
  45. package/dist/rpc/transports/ws-server.mjs +154 -61
  46. package/dist/{serialization-BD_qXGjd.mjs → serialization-DpLXCy13.mjs} +1 -1
  47. package/dist/{server-BBaBJaUL.mjs → server-Dl6TU5LE.mjs} +19 -14
  48. package/dist/server-rX781kz-.d.mts +90 -0
  49. package/dist/{shared-state-u0y123fi.mjs → shared-state-D4PPieA0.mjs} +4 -4
  50. package/dist/{shared-state-BlBNYziY.mjs → storage-l2ezeend.mjs} +128 -6
  51. package/dist/types/index.d.mts +4 -4
  52. package/dist/{types-BkkQ0Txg.d.mts → types-DZEx4ffs.d.mts} +9 -1
  53. package/dist/utils/crypto-token.d.mts +24 -0
  54. package/dist/utils/crypto-token.mjs +45 -0
  55. package/dist/utils/events.d.mts +1 -1
  56. package/dist/utils/hash.mjs +1 -1
  57. package/dist/utils/launch-editor.mjs +1 -1
  58. package/dist/utils/open.mjs +1 -1
  59. package/dist/utils/scope.d.mts +11 -0
  60. package/dist/utils/scope.mjs +15 -0
  61. package/dist/utils/shared-state.d.mts +1 -1
  62. package/dist/utils/shared-state.mjs +1 -1
  63. package/dist/utils/streaming-channel.d.mts +1 -1
  64. package/dist/utils/structured-clone.mjs +1 -1
  65. package/dist/{ws-client-CVYX9niP.d.mts → ws-client-D0z0pOhn.d.mts} +1 -1
  66. package/dist/ws-server-Bc2wBHl-.d.mts +119 -0
  67. package/package.json +12 -7
  68. package/skills/devframe/SKILL.md +123 -23
  69. package/skills/devframe/templates/counter-devframe.ts +14 -4
  70. package/skills/devframe/templates/spa-devframe.ts +13 -3
  71. package/skills/devframe/templates/vite-client.ts +3 -2
  72. package/dist/context-DaKmhhHY.mjs +0 -164
  73. package/dist/revoke-CL0LSAN9.mjs +0 -803
  74. package/dist/server-wHlpcdZ9.d.mts +0 -55
  75. package/dist/utils/human-id.d.mts +0 -8
  76. package/dist/utils/human-id.mjs +0 -769
  77. package/dist/ws-server-C1LjmRnp.d.mts +0 -61
  78. /package/dist/{define-CW9gLnyG.mjs → define-BLWPsH6y.mjs} +0 -0
  79. /package/dist/{diagnostics-reporter-CBBZwoMv.mjs → diagnostics-reporter-CsIG85Q5.mjs} +0 -0
  80. /package/dist/{hash-bwOD8RgU.mjs → hash-DFc5WwhO.mjs} +0 -0
  81. /package/dist/{open-DiQn6zCH.mjs → open-Dusa2Zzd.mjs} +0 -0
  82. /package/dist/{structured-clone-PdCZwt7F.mjs → structured-clone-CeZOHvkd.mjs} +0 -0
  83. /package/dist/{structured-clone-jegjz0hM.mjs → structured-clone-XpHLZ8nr.mjs} +0 -0
  84. /package/dist/{transports-DTFoMUbE.mjs → transports-BmDVn4SF.mjs} +0 -0
@@ -1,5 +1,6 @@
1
- import { S as RpcFunctionsCollectorBase } from "./index-C7M1hnvL.mjs";
2
- import { t as DevframeNodeRpcSessionMeta } from "./ws-server-C1LjmRnp.mjs";
1
+ import { _ as RpcFunctionDefinition } from "./types-DZEx4ffs.mjs";
2
+ import { S as RpcFunctionsCollectorBase } from "./index-CxaPKDXX.mjs";
3
+ import { t as DevframeNodeRpcSessionMeta } from "./ws-server-Bc2wBHl-.mjs";
3
4
  import { CAC } from "cac";
4
5
  import { GenericSchema, InferOutput } from "valibot";
5
6
  import { BirpcReturn } from "birpc";
@@ -368,6 +369,19 @@ interface DevframeHost {
368
369
  * for build snapshots).
369
370
  */
370
371
  mountStatic: (base: string, distDir: string) => void | Promise<void>;
372
+ /**
373
+ * Serve the host's connection meta (`__connection.json`) at the given URL
374
+ * base, so a devframe SPA mounted there can discover the RPC/WS endpoint
375
+ * via `connectDevframe()`'s relative `./__connection.json` fetch.
376
+ *
377
+ * Called by `mountDevframe` for each mounted devframe (alongside
378
+ * `mountStatic`). Without it, an embedded SPA can only discover the
379
+ * endpoint by inheriting it from a same-origin parent window — which fails
380
+ * for cross-origin or sandboxed iframes. Implementations serve the same
381
+ * meta they expose at the hub's own base. Optional: hosts that can't serve
382
+ * a dynamic route (e.g. static-snapshot builds) may omit it.
383
+ */
384
+ mountConnectionMeta?: (base: string) => void | Promise<void>;
371
385
  /**
372
386
  * Return the public origin the host is reachable at, e.g.
373
387
  * `http://localhost:5173`. Used by the dock host to enrich remote
@@ -400,6 +414,14 @@ interface DevframeHost {
400
414
  * To be extended
401
415
  */
402
416
  interface DevframeRpcClientFunctions {
417
+ /**
418
+ * Server→client notification that this connection's auth token has been
419
+ * revoked. The client drops to untrusted on receipt. Broadcast by
420
+ * `revokeActiveConnectionsForToken`.
421
+ *
422
+ * @internal
423
+ */
424
+ 'devframe:auth:revoked': () => Promise<void>;
403
425
  /**
404
426
  * Streaming chunk pushed from server to subscribed clients. Wired by
405
427
  * `RpcStreamingHost`; do not register manually.
@@ -424,11 +446,54 @@ interface DevframeRpcClientFunctions {
424
446
  * @internal
425
447
  */
426
448
  'devframe:streaming:upload-cancel': (channel: string, id: string) => Promise<void>;
449
+ /**
450
+ * Full shared-state snapshot pushed from server to subscribed clients.
451
+ * Wired by `RpcSharedStateHost`; do not register manually.
452
+ *
453
+ * @internal
454
+ */
455
+ 'devframe:rpc:client-state:updated': (key: string, fullState: any, syncId: string) => Promise<void>;
456
+ /**
457
+ * Incremental shared-state patch pushed from server to subscribed clients.
458
+ * Wired by `RpcSharedStateHost`; do not register manually.
459
+ *
460
+ * @internal
461
+ */
462
+ 'devframe:rpc:client-state:patch': (key: string, patches: any[], syncId: string) => Promise<void>;
427
463
  }
428
464
  /**
429
465
  * To be extended
430
466
  */
431
467
  interface DevframeRpcServerFunctions {
468
+ /**
469
+ * Authenticate a connection with a previously-issued bearer token; resolves
470
+ * whether the connection is now trusted. The interactive handler is provided
471
+ * by the host adapter (e.g. Vite DevTools); the standalone server registers
472
+ * an auto-trust noop when `auth: false`.
473
+ *
474
+ * @internal
475
+ */
476
+ 'devframe:anonymous:auth': (params: {
477
+ authToken: string;
478
+ ua: string;
479
+ origin: string;
480
+ }) => Promise<{
481
+ isTrusted: boolean;
482
+ }>;
483
+ /**
484
+ * Exchange a one-time authentication code (shown by the dev server) for a fresh,
485
+ * node-issued bearer token, returning the token on success or `null`. The
486
+ * handler is provided by the host adapter on top of `exchangeTempAuthCode`.
487
+ *
488
+ * @internal
489
+ */
490
+ 'devframe:auth:exchange': (params: {
491
+ code: string;
492
+ ua: string;
493
+ origin: string;
494
+ }) => Promise<{
495
+ authToken: string | null;
496
+ }>;
432
497
  /**
433
498
  * Subscribe a client to a shared-state key. Wired by
434
499
  * `RpcSharedStateHost`; do not register manually.
@@ -503,15 +568,6 @@ interface DevframeRpcServerFunctions {
503
568
  */
504
569
  interface DevframeRpcSharedStates {}
505
570
  //#endregion
506
- //#region src/types/utils.d.ts
507
- type Thenable<T> = T | Promise<T>;
508
- type EntriesToObject<T extends readonly [string, any][]> = { [K in T[number] as K[0]]: K[1] };
509
- type PartialWithoutId<T extends {
510
- id: string;
511
- }> = Partial<Omit<T, 'id'>> & {
512
- id: string;
513
- };
514
- //#endregion
515
571
  //#region src/types/views.d.ts
516
572
  interface DevframeViewHost {
517
573
  /**
@@ -529,6 +585,188 @@ interface DevframeViewHost {
529
585
  hostStatic: (baseUrl: string, distDir: string) => void;
530
586
  }
531
587
  //#endregion
588
+ //#region src/types/scope.d.ts
589
+ type AnyRpcFn = (...args: any[]) => any;
590
+ /**
591
+ * Augmentable registry mapping a scope namespace to the shape of its
592
+ * persisted settings. Tools augment it so `ctx.scope('my-plugin')`
593
+ * gets a fully-typed `settings.global` / `settings.project`:
594
+ *
595
+ * ```ts
596
+ * declare module 'devframe' {
597
+ * interface DevframeSettingsRegistry {
598
+ * 'my-plugin': { theme: 'light' | 'dark', recentFiles: string[] }
599
+ * }
600
+ * }
601
+ * ```
602
+ */
603
+ interface DevframeSettingsRegistry {}
604
+ /**
605
+ * Resolve the settings shape for a namespace from
606
+ * {@link DevframeSettingsRegistry}, falling back to an open record when
607
+ * the namespace hasn't been augmented.
608
+ */
609
+ type SettingsForNamespace<NS extends string> = NS extends keyof DevframeSettingsRegistry ? DevframeSettingsRegistry[NS] extends Record<string, any> ? DevframeSettingsRegistry[NS] : Record<string, any> : Record<string, any>;
610
+ /**
611
+ * A persisted key-value settings store for one scope (`global` or
612
+ * `project`). Backed by a file on the node side and by the shared-state
613
+ * sync protocol on the client, so a `set` on either side propagates to
614
+ * every connected peer and survives restarts.
615
+ *
616
+ * Every method is async because the underlying shared state is resolved
617
+ * lazily on first access.
618
+ */
619
+ interface DevframeSettingsStore<T extends Record<string, any> = Record<string, any>> {
620
+ /** Read a single setting. Resolves to `undefined` when unset. */
621
+ get: <K extends keyof T>(key: K) => Promise<T[K] | undefined>;
622
+ /** Write a single setting. */
623
+ set: <K extends keyof T>(key: K, value: T[K]) => Promise<void>;
624
+ /** Remove a single setting. */
625
+ delete: <K extends keyof T>(key: K) => Promise<void>;
626
+ /** Read the whole settings object (immutable snapshot). */
627
+ all: () => Promise<Readonly<T>>;
628
+ /**
629
+ * Subscribe to settings changes. Resolves to an unsubscribe function.
630
+ * Fires on every local or remote mutation with the full snapshot.
631
+ */
632
+ onChange: (fn: (value: Readonly<T>) => void) => Promise<() => void>;
633
+ }
634
+ /**
635
+ * The two settings scopes available on a scoped context.
636
+ *
637
+ * - `project` — per-workspace state, persisted under the host's
638
+ * `workspace` storage dir. Project-local settings.
639
+ * - `global` — per-user state, persisted under the host's `global`
640
+ * storage dir. Machine-wide preferences.
641
+ */
642
+ interface DevframeSettings<T extends Record<string, any> = Record<string, any>> {
643
+ global: DevframeSettingsStore<T>;
644
+ project: DevframeSettingsStore<T>;
645
+ }
646
+ /**
647
+ * Map the server-side RPC registry down to the bare names owned by a
648
+ * namespace, so a scoped `call('get-cwd')` is typed without the
649
+ * `my-plugin:` prefix.
650
+ */
651
+ type ScopedServerFunctions<NS extends string> = { [K in keyof DevframeRpcServerFunctions as K extends `${NS}:${infer R}` ? R : never]: DevframeRpcServerFunctions[K] };
652
+ /**
653
+ * Map the client-side RPC registry down to the bare names owned by a
654
+ * namespace, used by scoped `broadcast`.
655
+ */
656
+ type ScopedClientFunctions<NS extends string> = { [K in keyof DevframeRpcClientFunctions as K extends `${NS}:${infer R}` ? R : never]: DevframeRpcClientFunctions[K] };
657
+ /**
658
+ * Map the shared-state registry down to the bare keys owned by a
659
+ * namespace, so scoped `sharedState('messages')` is typed without the
660
+ * `my-plugin:` prefix.
661
+ */
662
+ type ScopedSharedStates<NS extends string> = { [K in keyof DevframeRpcSharedStates as K extends `${NS}:${infer R}` ? R : never]: DevframeRpcSharedStates[K] };
663
+ /**
664
+ * Broadcast options for a scoped client method (bare name).
665
+ */
666
+ interface ScopedBroadcastOptions<METHOD, Args extends any[]> {
667
+ method: METHOD;
668
+ args: Args;
669
+ optional?: boolean;
670
+ event?: boolean;
671
+ }
672
+ /**
673
+ * Node-side streaming host scoped to a namespace. Channel names are
674
+ * auto-prefixed with `<namespace>:`.
675
+ */
676
+ interface DevframeScopedStreamingHost {
677
+ /**
678
+ * Register a streaming channel. The bare `name` is auto-prefixed with
679
+ * the scope namespace (`<namespace>:<name>`); pass an already-qualified
680
+ * name (containing `:`) to opt out.
681
+ */
682
+ create: <T = unknown>(name: string, opts?: RpcStreamingChannelOptions) => RpcStreamingChannel<T>;
683
+ }
684
+ /**
685
+ * The scoped node RPC surface exposed on `ctx.scope('my-plugin').rpc`.
686
+ * IDs and keys you pass are auto-namespaced with `my-plugin:`.
687
+ */
688
+ interface DevframeScopedNodeRpc<NS extends string = string> {
689
+ /** The namespace this surface is scoped to. */
690
+ readonly namespace: NS;
691
+ /**
692
+ * Register a server RPC function. The definition's `name` must be bare
693
+ * (no `:`); it is stored as `<namespace>:<name>`. Throws `DF0034` if an
694
+ * already-namespaced name is passed.
695
+ */
696
+ register: (fn: RpcFunctionDefinition<string, any, any, any, any, any, DevframeNodeContext>, force?: boolean) => void;
697
+ /**
698
+ * Update a previously registered server RPC function. Same naming rules
699
+ * as {@link DevframeScopedNodeRpc.register}.
700
+ */
701
+ update: (fn: RpcFunctionDefinition<string, any, any, any, any, any, DevframeNodeContext>, force?: boolean) => void;
702
+ /**
703
+ * Invoke a locally registered server RPC function. Bare names are
704
+ * resolved within this namespace; pass a fully-qualified name
705
+ * (containing `:`) to call another scope's function.
706
+ */
707
+ call: {
708
+ <T extends keyof ScopedServerFunctions<NS> & string>(method: T, ...args: Parameters<Extract<ScopedServerFunctions<NS>[T], AnyRpcFn>>): Promise<Awaited<ReturnType<Extract<ScopedServerFunctions<NS>[T], AnyRpcFn>>>>;
709
+ <T extends keyof DevframeRpcServerFunctions & string>(method: T, ...args: Parameters<Extract<DevframeRpcServerFunctions[T], AnyRpcFn>>): Promise<Awaited<ReturnType<Extract<DevframeRpcServerFunctions[T], AnyRpcFn>>>>;
710
+ (method: string, ...args: any[]): Promise<any>;
711
+ };
712
+ /**
713
+ * Broadcast a client RPC event to every connected client. Bare method
714
+ * names are resolved within this namespace.
715
+ */
716
+ broadcast: {
717
+ <T extends keyof ScopedClientFunctions<NS> & string>(options: ScopedBroadcastOptions<T, Parameters<Extract<ScopedClientFunctions<NS>[T], AnyRpcFn>>>): Promise<void>;
718
+ (options: ScopedBroadcastOptions<string, any[]>): Promise<void>;
719
+ };
720
+ /**
721
+ * Resolve a namespaced shared state. Bare keys are resolved within this
722
+ * namespace; pass a fully-qualified key (containing `:`) to opt out.
723
+ */
724
+ sharedState: {
725
+ <T extends keyof ScopedSharedStates<NS> & string>(key: T, options?: RpcSharedStateGetOptions<ScopedSharedStates<NS>[T]>): Promise<SharedState<ScopedSharedStates<NS>[T]>>;
726
+ <T extends Record<string, any> = Record<string, any>>(key: string, options?: RpcSharedStateGetOptions<T>): Promise<SharedState<T>>;
727
+ };
728
+ /** Streaming host scoped to this namespace. */
729
+ streaming: DevframeScopedStreamingHost;
730
+ /** Pass-through to the current RPC session, when called inside a handler. */
731
+ getCurrentRpcSession: () => DevframeNodeRpcSession | undefined;
732
+ }
733
+ /**
734
+ * A namespace-scoped view of {@link DevframeNodeContext}. Returned by
735
+ * `ctx.scope('my-plugin')`. Re-exposes the unscoped surfaces (`views`,
736
+ * `diagnostics`, `agent`, …), replaces `rpc` with the auto-namespaced
737
+ * {@link DevframeScopedNodeRpc}, and adds a top-level `settings` store.
738
+ */
739
+ interface DevframeScopedNodeContext<NS extends string = string, Settings extends Record<string, any> = Record<string, any>> {
740
+ /** The namespace this context is scoped to. */
741
+ readonly namespace: NS;
742
+ /** The underlying unscoped context. */
743
+ readonly base: DevframeNodeContext;
744
+ readonly workspaceRoot: string;
745
+ readonly cwd: string;
746
+ readonly mode: 'dev' | 'build';
747
+ host: DevframeHost;
748
+ rpc: DevframeScopedNodeRpc<NS>;
749
+ /** Persisted settings for this namespace (`global` + `project`). */
750
+ settings: DevframeSettings<Settings>;
751
+ views: DevframeViewHost;
752
+ diagnostics: DevframeDiagnosticsHost;
753
+ agent: DevframeAgentHost;
754
+ /**
755
+ * Return a new scoped context, replacing the current scope. Pass `null`
756
+ * or `''` to un-scope and get the base context.
757
+ */
758
+ scope: DevframeNodeContext['scope'];
759
+ }
760
+ //#endregion
761
+ //#region src/types/utils.d.ts
762
+ type Thenable<T> = T | Promise<T>;
763
+ type EntriesToObject<T extends readonly [string, any][]> = { [K in T[number] as K[0]]: K[1] };
764
+ type PartialWithoutId<T extends {
765
+ id: string;
766
+ }> = Partial<Omit<T, 'id'>> & {
767
+ id: string;
768
+ };
769
+ //#endregion
532
770
  //#region src/utils/shared-state.d.ts
533
771
  type ImmutablePrimitive = undefined | null | boolean | string | number | Function;
534
772
  type Immutable<T> = T extends ImmutablePrimitive ? T : T extends Array<infer U> ? ImmutableArray<U> : T extends Map<infer K, infer V> ? ImmutableMap<K, V> : T extends Set<infer M> ? ImmutableSet<M> : ImmutableObject<T>;
@@ -755,7 +993,7 @@ interface RpcSharedStateGetOptions<T> {
755
993
  initialValue?: T;
756
994
  }
757
995
  interface RpcSharedStateHost {
758
- get: <T extends keyof DevframeRpcSharedStates>(key: T, options?: RpcSharedStateGetOptions<DevframeRpcSharedStates[T]>) => Promise<SharedState<DevframeRpcSharedStates[T]>>;
996
+ get: <T extends object = any>(key: string, options?: RpcSharedStateGetOptions<T>) => Promise<SharedState<T>>;
759
997
  keys: () => string[];
760
998
  /**
761
999
  * Subscribe to new shared-state keys becoming available. Fires when
@@ -917,10 +1155,55 @@ interface DevframeNodeContext {
917
1155
  * @experimental
918
1156
  */
919
1157
  agent: DevframeAgentHost;
1158
+ /**
1159
+ * Create a namespace-scoped view of this context. The returned
1160
+ * `ctx.scope('my-plugin')` auto-namespaces every RPC id, shared-state
1161
+ * key, and streaming channel with `my-plugin:`, and exposes a typed
1162
+ * top-level `settings` store. This is the preferred way to consume the
1163
+ * context from a single tool's setup code.
1164
+ *
1165
+ * Pass `null` or `''` to un-scope and get the base context.
1166
+ */
1167
+ scope: {
1168
+ <NS extends string>(namespace: NS): DevframeScopedNodeContext<NS, SettingsForNamespace<NS>>;
1169
+ (namespace?: null | ''): DevframeNodeContext;
1170
+ };
1171
+ }
1172
+ /**
1173
+ * Describes where the browser client should open its RPC WebSocket. The
1174
+ * object form is the proxy-flexible default: `path` is resolved relative to
1175
+ * where `__connection.json` was loaded, and the connection is made to the
1176
+ * page's own origin (only the `http`→`ws` / `https`→`wss` protocol swap is
1177
+ * applied). This survives reverse proxies that change the host/port, because
1178
+ * the client never trusts a server-baked hostname — it reuses its own.
1179
+ *
1180
+ * Set `port` (and/or `host`) only when the WS endpoint genuinely lives on a
1181
+ * different origin than the page, e.g. a side-car server on its own port.
1182
+ */
1183
+ interface ConnectionMetaWebsocket {
1184
+ /**
1185
+ * Path to the WS endpoint. Relative paths (the default, e.g. `__devframe_ws`) are
1186
+ * resolved against `__connection.json`'s location; absolute paths (`/__devframe_ws`)
1187
+ * resolve against the page origin.
1188
+ */
1189
+ path?: string;
1190
+ /** Override the port. Combined with the page hostname unless `host` is set. */
1191
+ port?: number;
1192
+ /** Override the host (`hostname[:port]`). Use for a fully cross-origin endpoint. */
1193
+ host?: string;
920
1194
  }
921
1195
  interface ConnectionMeta {
922
1196
  backend: 'websocket' | 'static';
923
- websocket?: number | string;
1197
+ /**
1198
+ * WebSocket endpoint, resolved by the client into a `ws(s)://` URL:
1199
+ *
1200
+ * - {@link ConnectionMetaWebsocket} — the proxy-flexible default; a
1201
+ * same-origin path relative to `__connection.json`.
1202
+ * - `number` — a port on the page's hostname (`ws(s)://<host>:<port>`).
1203
+ * - `string` — a full `ws://`/`wss://` URL used verbatim, an `http(s)://`
1204
+ * URL with its protocol swapped, or a path resolved same-origin.
1205
+ */
1206
+ websocket?: number | string | ConnectionMetaWebsocket;
924
1207
  /**
925
1208
  * Names of RPC functions that have declared `jsonSerializable: true`.
926
1209
  * Used by the WS / static client to dispatch the per-call wire
@@ -941,6 +1224,51 @@ type DevframeRuntime = 'cli' | 'build' | 'spa' | 'vite' | 'embedded';
941
1224
  * (`cli`, `spa`, `build`) own the origin and default to `/`.
942
1225
  */
943
1226
  type DevframeDeploymentKind = 'standalone' | 'hosted';
1227
+ /**
1228
+ * How a hub deduplicates devframes that share an `id` when more than one
1229
+ * is mounted onto the same hub. See {@link DevframeDefinition.duplicationStrategy}.
1230
+ *
1231
+ * - `'warn'` (default) — keep the first registration, drop later
1232
+ * duplicates, and emit a warning diagnostic (`DF8105`).
1233
+ * - `'silent'` — drop later duplicates without warning.
1234
+ * - `'throw'` — throw when a duplicate is mounted.
1235
+ * - `'duplicate'` — let every instance coexist under a disambiguated
1236
+ * dock id.
1237
+ */
1238
+ type DevframeDuplicationStrategy = 'warn' | 'silent' | 'throw' | 'duplicate';
1239
+ /**
1240
+ * Controls where the browser opens the RPC WebSocket — advertised in
1241
+ * `__connection.json` and used to bind the dev server. The three shapes map
1242
+ * to the three connection scenarios; precedence is `url` > `port` > `route`:
1243
+ *
1244
+ * 1. **Same server, different route** (default) — leave `port`/`url` unset.
1245
+ * The socket shares the HTTP server's port and binds to `route`
1246
+ * (`__devframe_ws`). The client connects to its own origin, so the link
1247
+ * survives a reverse proxy that rewrites the host/port/subpath.
1248
+ *
1249
+ * 2. **Different port** — set `port`. The socket binds on its own port on the
1250
+ * same host; the client targets `ws(s)://<page-host>:<port>/<route>`.
1251
+ *
1252
+ * 3. **Remote, different origin** — set `url` to a full `ws://`/`wss://`
1253
+ * endpoint (e.g. a public tunnel or relay). The client uses it verbatim.
1254
+ */
1255
+ interface DevframeWsOptions {
1256
+ /**
1257
+ * Upgrade route segment the socket binds to and is advertised at, relative
1258
+ * to the SPA base. Default: `__devframe_ws`.
1259
+ */
1260
+ route?: string;
1261
+ /**
1262
+ * Bind the socket on its own port instead of sharing the HTTP port. The
1263
+ * browser connects to this port on the page's hostname.
1264
+ */
1265
+ port?: number;
1266
+ /**
1267
+ * Advertise a fixed, fully-qualified endpoint on another origin (a full
1268
+ * `ws://`/`wss://` URL). Takes precedence over `port`/`route` in the meta.
1269
+ */
1270
+ url?: string;
1271
+ }
944
1272
  interface DevframeCliOptions {
945
1273
  /** Binary name; default: the devframe's `id`. */
946
1274
  command?: string;
@@ -969,6 +1297,12 @@ interface DevframeCliOptions {
969
1297
  auth?: boolean;
970
1298
  /** Author's SPA dist directory (served as the devframe's UI). */
971
1299
  distDir?: string;
1300
+ /**
1301
+ * How the browser reaches the RPC WebSocket. Defaults to sharing the HTTP
1302
+ * port on the `__devframe_ws` route. See {@link DevframeWsOptions} for the
1303
+ * different-port and remote-origin variants.
1304
+ */
1305
+ ws?: DevframeWsOptions;
972
1306
  /**
973
1307
  * Capability-side CAC hook. Called with the CAC instance after the
974
1308
  * adapter registers its built-in commands (`build` / `spa` / `mcp`)
@@ -1031,17 +1365,33 @@ interface DevframeSetupInfo {
1031
1365
  interface DevframeDefinition {
1032
1366
  id: string;
1033
1367
  name: string;
1368
+ /** Semver of the tool, surfaced in hub UIs and diagnostics. */
1369
+ version: string;
1370
+ /** npm package name the devframe ships in (e.g. `@scope/my-tool`). */
1371
+ packageName: string;
1372
+ /** Project homepage or documentation URL. */
1373
+ homepage: string;
1374
+ /** One-line summary of what the tool does. */
1375
+ description: string;
1034
1376
  icon?: string | {
1035
1377
  light: string;
1036
1378
  dark: string;
1037
1379
  };
1038
- version?: string;
1039
1380
  /**
1040
1381
  * Mount path override. Defaults depend on the adapter:
1041
1382
  * `/` for standalone (`cli` / `spa` / `build`), `/__<id>/` for hosted
1042
1383
  * (`vite` / `embedded`).
1043
1384
  */
1044
1385
  basePath?: string;
1386
+ /**
1387
+ * How a hub reacts when another devframe sharing this one's `id` is
1388
+ * mounted onto the same hub. Consulted only by hub adapters
1389
+ * (`mountDevframe`); standalone adapters (`cli` / `spa` / `build`)
1390
+ * ignore it.
1391
+ *
1392
+ * @default 'warn'
1393
+ */
1394
+ duplicationStrategy?: DevframeDuplicationStrategy;
1045
1395
  capabilities?: {
1046
1396
  dev?: boolean | Record<string, boolean>;
1047
1397
  build?: boolean | Record<string, boolean>;
@@ -1056,4 +1406,4 @@ interface DevframeDefinition {
1056
1406
  }
1057
1407
  declare function defineDevframe(d: DevframeDefinition): DevframeDefinition;
1058
1408
  //#endregion
1059
- export { AgentResource as $, ImmutableArray as A, EntriesToObject as B, StreamErrorPayload as C, createStreamReader as D, StreamSinkEvents as E, SharedStateEvents as F, DevframeRpcSharedStates as G, Thenable as H, SharedStateOptions as I, DevframeDiagnosticsDefinition as J, DevframeHost as K, SharedStatePatch as L, ImmutableObject as M, ImmutableSet as N, createStreamSink as O, SharedState as P, AgentManifest as Q, createSharedState as R, CreateStreamSinkOptions as S, StreamSink as T, DevframeRpcClientFunctions as U, PartialWithoutId as V, DevframeRpcServerFunctions as W, DevframeDiagnosticsLogger as X, DevframeDiagnosticsHost as Y, AgentHandle as Z, RpcStreamingChannel as _, DevframeRuntime as a, DevframeAgentHostEvents as at, BufferedChunk as b, defineDevframe as c, EventsMap as ct, DevframeNodeContext as d, defineCliFlags as dt, AgentResourceContent as et, DevframeNodeRpcSession as f, parseCliFlags as ft, RpcSharedStateHost as g, RpcSharedStateGetOptions as h, DevframeDeploymentKind as i, DevframeAgentHost as it, ImmutableMap as j, Immutable as k, ConnectionMeta as l, CliFlagsSchema as lt, RpcFunctionsHost as m, DevframeCliOptions as n, AgentTool as nt, DevframeSetupInfo as o, EventEmitter as ot, RpcBroadcastOptions as p, DevframeDefineDiagnosticsOptions as q, DevframeDefinition as r, AgentToolInput as rt, DevframeSpaOptions as s, EventUnsubscribe as st, DevframeBrowserContext as t, AgentResourceInput as tt, DevframeCapabilities as u, InferCliFlags as ut, RpcStreamingChannelOptions as v, StreamReader as w, CreateStreamReaderOptions as x, RpcStreamingHost as y, DevframeViewHost as z };
1409
+ export { ScopedServerFunctions as $, createStreamReader as A, SharedStatePatch as B, BufferedChunk as C, CliFlagsSchema as Ct, StreamReader as D, StreamErrorPayload as E, parseCliFlags as Et, ImmutableObject as F, DevframeScopedNodeContext as G, EntriesToObject as H, ImmutableSet as I, DevframeSettings as J, DevframeScopedNodeRpc as K, SharedState as L, Immutable as M, ImmutableArray as N, StreamSink as O, ImmutableMap as P, ScopedClientFunctions as Q, SharedStateEvents as R, RpcStreamingHost as S, EventsMap as St, CreateStreamSinkOptions as T, defineCliFlags as Tt, PartialWithoutId as U, createSharedState as V, Thenable as W, DevframeSettingsStore as X, DevframeSettingsRegistry as Y, ScopedBroadcastOptions as Z, RpcFunctionsHost as _, AgentToolInput as _t, DevframeDuplicationStrategy as a, DevframeRpcSharedStates as at, RpcStreamingChannel as b, EventEmitter as bt, DevframeSpaOptions as c, DevframeDiagnosticsDefinition as ct, ConnectionMeta as d, AgentHandle as dt, ScopedSharedStates as et, ConnectionMetaWebsocket as f, AgentManifest as ft, RpcBroadcastOptions as g, AgentTool as gt, DevframeNodeRpcSession as h, AgentResourceInput as ht, DevframeDeploymentKind as i, DevframeRpcServerFunctions as it, createStreamSink as j, StreamSinkEvents as k, DevframeWsOptions as l, DevframeDiagnosticsHost as lt, DevframeNodeContext as m, AgentResourceContent as mt, DevframeCliOptions as n, DevframeViewHost as nt, DevframeRuntime as o, DevframeHost as ot, DevframeCapabilities as p, AgentResource as pt, DevframeScopedStreamingHost as q, DevframeDefinition as r, DevframeRpcClientFunctions as rt, DevframeSetupInfo as s, DevframeDefineDiagnosticsOptions as st, DevframeBrowserContext as t, SettingsForNamespace as tt, defineDevframe as u, DevframeDiagnosticsLogger as ut, RpcSharedStateGetOptions as v, DevframeAgentHost as vt, CreateStreamReaderOptions as w, InferCliFlags as wt, RpcStreamingChannelOptions as x, EventUnsubscribe as xt, RpcSharedStateHost as y, DevframeAgentHostEvents as yt, SharedStateOptions as z };
@@ -1,4 +1,4 @@
1
- import { t as devframeReporter } from "./diagnostics-reporter-CBBZwoMv.mjs";
1
+ import { t as devframeReporter } from "./diagnostics-reporter-CsIG85Q5.mjs";
2
2
  import { defineDiagnostics } from "nostics";
3
3
  //#region src/rpc/diagnostics.ts
4
4
  const diagnostics = defineDiagnostics({
@@ -1,4 +1,4 @@
1
- import { t as diagnostics } from "./diagnostics-GitRGKbr.mjs";
1
+ import { t as diagnostics } from "./diagnostics-BXwBQmoN.mjs";
2
2
  import { DEVFRAME_RPC_DUMP_DIRNAME } from "./constants.mjs";
3
3
  import { createHash } from "node:crypto";
4
4
  //#region ../../node_modules/.pnpm/ohash@2.0.11/node_modules/ohash/dist/shared/ohash.D__AXeF1.mjs
@@ -1,4 +1,4 @@
1
- import { r as DevframeDefinition } from "../devframe-BuR6n9ZD.mjs";
1
+ import { r as DevframeDefinition } from "../devframe-D-gr9sBx.mjs";
2
2
 
3
3
  //#region src/helpers/vite.d.ts
4
4
  interface ViteDevBridgeOptions {
@@ -1,8 +1,8 @@
1
- import { DEVFRAME_CONNECTION_META_FILENAME } from "../constants.mjs";
2
- import { i as diagnostics } from "../shared-state-BlBNYziY.mjs";
3
- import { n as resolveBasePath } from "../_shared-CUFqO4kJ.mjs";
1
+ import { DEVFRAME_CONNECTION_META_FILENAME, DEVFRAME_WS_ROUTE } from "../constants.mjs";
2
+ import { a as diagnostics } from "../storage-l2ezeend.mjs";
3
+ import { n as resolveBasePath, t as normalizeBasePath } from "../_shared-bWRzeSa0.mjs";
4
4
  import { serveStaticNodeMiddleware } from "../utils/serve-static.mjs";
5
- import { n as resolveDevServerPort, t as createDevServer } from "../dev-Cv43GfqM.mjs";
5
+ import { n as resolveDevServerPort, t as createDevServer } from "../dev-BaX8fLH2.mjs";
6
6
  import { resolve } from "pathe";
7
7
  //#region src/helpers/vite.ts
8
8
  /**
@@ -66,7 +66,10 @@ function viteDevBridge(d, options = {}) {
66
66
  res.setHeader("Content-Type", "application/json");
67
67
  res.end(JSON.stringify({
68
68
  backend: "websocket",
69
- websocket: port
69
+ websocket: {
70
+ port,
71
+ path: `/${DEVFRAME_WS_ROUTE}`
72
+ }
70
73
  }));
71
74
  });
72
75
  server.httpServer?.once("close", () => {
@@ -82,14 +85,11 @@ function viteDevBridge(d, options = {}) {
82
85
  /**
83
86
  * Make `base` safe for `server.middlewares.use(path, …)`. Vite's connect
84
87
  * router matches by absolute URL prefix, so relative spellings like
85
- * `'./'` (commonly used for base-agnostic Nuxt builds) need to be
86
- * converted to `/` first.
88
+ * `'./'` (commonly used for base-agnostic Nuxt builds) collapse to the
89
+ * origin root before the shared leading/trailing-slash normalization.
87
90
  */
88
91
  function normalizeMountBase(base) {
89
- let out = base.replace(/^\.\/?/, "/");
90
- if (!out.startsWith("/")) out = `/${out}`;
91
- if (!out.endsWith("/")) out = `${out}/`;
92
- return out.replace(/\/+/g, "/");
92
+ return normalizeBasePath(base.replace(/^\.\/?/, "/"));
93
93
  }
94
94
  //#endregion
95
95
  export { viteDevBridge };