devframe 0.9.0-beta.9 → 0.9.1

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 (72) hide show
  1. package/dist/_shared-BM3PdYli.mjs +68 -0
  2. package/dist/{_shared-BPmX9fqK.d.mts → _shared-DqbKCAew.d.mts} +1 -1
  3. package/dist/adapters/build.d.mts +1 -1
  4. package/dist/adapters/build.mjs +8 -6
  5. package/dist/adapters/cac.d.mts +1 -1
  6. package/dist/adapters/cac.mjs +4 -3
  7. package/dist/adapters/dev.d.mts +3 -3
  8. package/dist/adapters/dev.mjs +2 -2
  9. package/dist/adapters/embedded.d.mts +1 -1
  10. package/dist/adapters/embedded.mjs +1 -0
  11. package/dist/adapters/initiate.d.mts +2 -2
  12. package/dist/adapters/initiate.mjs +1 -1
  13. package/dist/adapters/mcp.d.mts +1 -1
  14. package/dist/adapters/mcp.mjs +1 -1
  15. package/dist/cli/main.mjs +2 -2
  16. package/dist/client/index.d.mts +48 -2
  17. package/dist/client/index.mjs +131 -22
  18. package/dist/constants.d.mts +74 -2
  19. package/dist/constants.mjs +9 -2
  20. package/dist/{context-CEe4PBz1.mjs → context-Bdkakk8S.mjs} +319 -21
  21. package/dist/{context-xdynGSLP.mjs → context-CXhjygJU.mjs} +2 -2
  22. package/dist/{context-B3YUu_95.d.mts → context-dqyXS6o3.d.mts} +1 -1
  23. package/dist/{dev-Chvlqs7I.mjs → dev-C8VHtuPP.mjs} +12 -9
  24. package/dist/{devframe-DJMg0WLe.d.mts → devframe-BlLEZR-x.d.mts} +741 -588
  25. package/dist/{diagnostics-DOQPnQmX.mjs → diagnostics-Di8ytitn.mjs} +24 -0
  26. package/dist/{_shared-CKZPvSrN.mjs → dist-CZXfGEkd.mjs} +2 -68
  27. package/dist/events-DKfSmoTj.mjs +65 -0
  28. package/dist/{host-agent-xLCSe65D.mjs → host-agent-BDdcOXhG.mjs} +14 -13
  29. package/dist/{http-8Kpkap4_.mjs → http-BbHCU5Zs.mjs} +9 -6
  30. package/dist/{index-CB-fSUyn.d.mts → index-BKFT9-jA.d.mts} +2 -2
  31. package/dist/index.d.mts +2 -2
  32. package/dist/{instance-registry-WvQkt42E.mjs → instance-registry-CHunhMi5.mjs} +4 -4
  33. package/dist/{instance-shell-CljiKq1c.d.mts → instance-shell-0iw-cQ5V.d.mts} +2 -2
  34. package/dist/{instance-shell-XvN5USg4.mjs → instance-shell-DbDoozNM.mjs} +5 -5
  35. package/dist/internal/index.d.mts +46 -7
  36. package/dist/internal/index.mjs +8 -8
  37. package/dist/node/auth.d.mts +2 -2
  38. package/dist/node/auth.mjs +2 -2
  39. package/dist/node/hub-internals.d.mts +2 -2
  40. package/dist/node/hub-internals.mjs +2 -2
  41. package/dist/node/index.d.mts +1 -1
  42. package/dist/node/index.mjs +2 -2
  43. package/dist/recipes/common-rpc-functions.d.mts +1 -1
  44. package/dist/recipes/interactive-auth.d.mts +2 -2
  45. package/dist/recipes/interactive-auth.mjs +2 -2
  46. package/dist/{remote-assets-nTTxY2MU.mjs → remote-assets-DRfY2RjX.mjs} +6 -5
  47. package/dist/{revoke-DZbJ7Cl0.mjs → revoke-3Q6aDKWk.mjs} +2 -1
  48. package/dist/{rolldown-runtime-JspESFgx.mjs → rolldown-runtime-B4iAMlE-.mjs} +1 -1
  49. package/dist/rpc/transports/sse-client.mjs +1 -1
  50. package/dist/rpc/transports/sse-server.mjs +2 -2
  51. package/dist/rpc/transports/ws-bun.d.mts +1 -1
  52. package/dist/rpc/transports/ws-bun.mjs +1 -1
  53. package/dist/rpc/transports/ws-client.mjs +1 -1
  54. package/dist/rpc/transports/ws-server.mjs +1 -1
  55. package/dist/{rpc-core-Cc4lucI0.d.mts → rpc-core-CR8o34O9.d.mts} +2 -2
  56. package/dist/{rpc-core-DMTe-hLo.mjs → rpc-core-Dru9uoM0.mjs} +11 -4
  57. package/dist/{state-Cc1IeSmQ.mjs → state-CK9LjrnT.mjs} +1 -1
  58. package/dist/{storage-CYye7P7L.mjs → storage-D4a99Ung.mjs} +3 -3
  59. package/dist/types/index.d.mts +2 -2
  60. package/dist/utils/events.d.mts +1 -1
  61. package/dist/utils/get-port.d.mts +17 -0
  62. package/dist/utils/get-port.mjs +2 -0
  63. package/dist/utils/launch-editor.mjs +12 -11
  64. package/dist/utils/open.mjs +1 -1
  65. package/dist/utils/remote-assets.mjs +1 -1
  66. package/dist/utils/serve-static.mjs +6 -5
  67. package/dist/utils/shared-state.d.mts +1 -1
  68. package/dist/utils/streaming-channel.d.mts +1 -1
  69. package/dist/{wire-codec-GsoRxIsD.mjs → wire-codec-0K-o5MYW.mjs} +0 -2
  70. package/dist/{ws-server-BiUne4K7.mjs → ws-server-BdSLrhxE.mjs} +1 -1
  71. package/package.json +2 -1
  72. package/dist/{host-h3-Crkzm42q.mjs → host-h3-fRbF9yor.mjs} +1 -1
@@ -493,734 +493,874 @@ declare function createStreamSink<T>(options?: CreateStreamSinkOptions): StreamS
493
493
  */
494
494
  declare function createStreamReader<T>(options?: CreateStreamReaderOptions): StreamReader<T>;
495
495
  //#endregion
496
- //#region src/types/rpc-augments.d.ts
496
+ //#region src/types/diagnostics.d.ts
497
497
  /**
498
- * To be extended
498
+ * The shared diagnostics lookup exposed by the host. A `Proxy` that resolves
499
+ * any registered code name to its `nostics` handle (a callable that builds
500
+ * a diagnostic and routes it through registered reporters). Typed loosely
501
+ * because it spans heterogeneous definitions registered by different
502
+ * integrations.
499
503
  */
500
- interface DevframeRpcClientFunctions {
501
- /**
502
- * Server→client notification that this connection's auth token has been
503
- * revoked. The client drops to untrusted on receipt. Broadcast by
504
- * `revokeActiveConnectionsForToken`.
505
- *
506
- * @internal
507
- */
508
- 'devframe:auth:revoked': () => Promise<void>;
509
- /**
510
- * Streaming chunk pushed from server to subscribed clients. Wired by
511
- * `RpcStreamingHost`; do not register manually.
512
- *
513
- * @internal
514
- */
515
- 'devframe:streaming:chunk': (channel: string, id: string, seq: number, chunk: any) => Promise<void>;
516
- /**
517
- * Streaming terminator pushed from server to subscribed clients. Wired by
518
- * `RpcStreamingHost`; do not register manually.
519
- *
520
- * @internal
521
- */
522
- 'devframe:streaming:end': (channel: string, id: string, error?: {
523
- name: string;
524
- message: string;
525
- }) => Promise<void>;
526
- /**
527
- * Server→client cancel for an in-flight upload. Wired by
528
- * `RpcStreamingHost`; do not register manually.
529
- *
530
- * @internal
531
- */
532
- 'devframe:streaming:upload-cancel': (channel: string, id: string) => Promise<void>;
533
- /**
534
- * Full shared-state snapshot pushed from server to subscribed clients.
535
- * Wired by `RpcSharedStateHost`; do not register manually.
536
- *
537
- * @internal
538
- */
539
- 'devframe:rpc:client-state:updated': (key: string, fullState: any, syncId: string) => Promise<void>;
540
- /**
541
- * Incremental shared-state patch pushed from server to subscribed clients.
542
- * Wired by `RpcSharedStateHost`; do not register manually.
543
- *
544
- * @internal
545
- */
546
- 'devframe:rpc:client-state:patch': (key: string, patches: any[], syncId: string) => Promise<void>;
547
- }
504
+ type DevframeDiagnosticsLogger = Record<string, any>;
548
505
  /**
549
- * To be extended
506
+ * Host for structured diagnostics — a thin layer over `nostics` that lets
507
+ * integrations register their own coded errors/warnings into a shared
508
+ * registry without taking a direct dependency on `nostics`.
509
+ *
510
+ * Typical usage from a plugin's `setup(ctx)`:
511
+ *
512
+ * ```ts
513
+ * const myDiagnostics = ctx.diagnostics.defineDiagnostics({
514
+ * docsBase: 'https://example.com/errors',
515
+ * codes: {
516
+ * MYP0001: { why: 'Something went wrong' },
517
+ * },
518
+ * })
519
+ * ctx.diagnostics.register(myDiagnostics)
520
+ *
521
+ * // Through the shared lookup (loose typing):
522
+ * throw ctx.diagnostics.logger.MYP0001()
523
+ *
524
+ * // Or directly on the typed handle returned from `defineDiagnostics`:
525
+ * throw myDiagnostics.MYP0001()
526
+ * ```
550
527
  */
551
- interface DevframeRpcServerFunctions {
552
- /**
553
- * Authenticate a connection with a previously-issued bearer token; resolves
554
- * whether the connection is now trusted. The interactive handler is provided
555
- * by the host adapter (e.g. Vite DevTools); the standalone server registers
556
- * an auto-trust noop when `auth: false`.
557
- *
558
- * Named with the `anonymous:` prefix (see `isAnonymousRpcMethod`) so it is
559
- * reachable before the connection is trusted — this is the *only* rule the
560
- * pre-trust gate applies, no per-method allowlist.
561
- *
562
- * @internal
563
- */
564
- 'anonymous:devframe:auth': (params: {
565
- authToken: string;
566
- ua: string;
567
- origin: string;
568
- }) => Promise<{
569
- isTrusted: boolean;
570
- }>;
528
+ interface DevframeDiagnosticsHost {
571
529
  /**
572
- * Exchange a one-time authentication code (shown by the dev server) for a fresh,
573
- * node-issued bearer token, returning the token on success or `null`. The
574
- * handler is provided by the host adapter on top of `exchangeTempAuthCode`.
575
- *
576
- * Named with the `anonymous:` prefix (see `isAnonymousRpcMethod`) so it is
577
- * reachable before the connection is trusted.
578
- *
579
- * @internal
530
+ * Proxy-backed lookup of every registered diagnostic handle by code name.
531
+ * Resolves to a `nostics` `DiagnosticHandle` — a callable that builds a
532
+ * diagnostic and routes it through registered reporters; prefix with
533
+ * `throw` to raise. Loosely typed — for autocompletion, keep a reference
534
+ * to the typed result of `defineDiagnostics()` instead.
580
535
  */
581
- 'anonymous:devframe:auth:exchange': (params: {
582
- code: string;
583
- ua: string;
584
- origin: string;
585
- }) => Promise<{
586
- authToken: string | null;
587
- }>;
536
+ readonly logger: DevframeDiagnosticsLogger;
588
537
  /**
589
- * Self-revoke: the caller asks the server to revoke its own bearer token
590
- * (if any) and drop to untrusted. Requires an already-trusted caller, so
591
- * unlike the two handshake methods above it does **not** carry the
592
- * `anonymous:` prefix. Registered by `recipes/interactive-auth`.
593
- *
594
- * @internal
538
+ * Register additional diagnostic definitions with this host. After
539
+ * registration, codes from the new definition are reachable via
540
+ * `host.logger.CODE`. Plugins that want shared output formatting should
541
+ * build their diagnostics via `host.defineDiagnostics()` first — that
542
+ * factory pre-wires the host's ANSI console reporter.
595
543
  */
596
- 'devframe:auth:revoke': () => Promise<void>;
544
+ register: (definitions: Record<string, unknown>) => void;
597
545
  /**
598
- * Subscribe a client to a shared-state key. Wired by
599
- * `RpcSharedStateHost`; do not register manually.
600
- *
601
- * @internal
546
+ * Build a typed diagnostics object with the host's ANSI console reporter
547
+ * pre-wired. The same `devframe/utils/nostics` `defineDiagnostics` every
548
+ * built-in plugin's module-level `diagnostics.ts` uses, so integrations
549
+ * don't need to take a direct dependency on `nostics`.
602
550
  */
603
- 'devframe:rpc:server-state:subscribe': (key: string) => Promise<void>;
551
+ defineDiagnostics: typeof defineDiagnostics;
552
+ }
553
+ //#endregion
554
+ //#region src/types/host.d.ts
555
+ interface DevframeHost {
604
556
  /**
605
- * Read the current value for a shared-state key. Wired by
606
- * `RpcSharedStateHost`; do not register manually.
607
- *
608
- * @internal
557
+ * Serve static assets at the given URL base — a local directory, or a
558
+ * resolved {@link RemoteAssetsStore} back-proxy. Called by
559
+ * `DevframeViewHost.hostStatic` (which normalizes `RemoteAssets`
560
+ * declarations into stores first). Implementations map this to whatever
561
+ * the underlying runtime expects (Vite middleware, h3 handler, no-op
562
+ * for build snapshots) — the shared engine in
563
+ * `devframe/utils/serve-static` accepts either shape.
609
564
  */
610
- 'devframe:rpc:server-state:get': (key: string) => Promise<any>;
565
+ mountStatic: (base: string, source: string | RemoteAssetsStore) => void | Promise<void>;
611
566
  /**
612
- * Replace a shared-state value (from the client). Wired by
613
- * `RpcSharedStateHost`; do not register manually.
567
+ * Serve the host's connection meta (`__connection.json`) at the given URL
568
+ * base, so a devframe SPA mounted there can discover the RPC/WS endpoint
569
+ * via `connectDevframe()`'s relative `./__connection.json` fetch.
614
570
  *
615
- * @internal
616
- */
617
- 'devframe:rpc:server-state:set': (key: string, value: any, syncId: string) => Promise<void>;
618
- /**
619
- * Apply a patch to a shared-state value (from the client). Wired by
620
- * `RpcSharedStateHost`; do not register manually.
571
+ * Called by `ctx.install` for each mounted devframe (alongside
572
+ * `mountStatic`). Without it, an embedded SPA can only discover the
573
+ * endpoint by inheriting it from a same-origin parent window — which fails
574
+ * for cross-origin or sandboxed iframes. Implementations serve the same
575
+ * meta they expose at the hub's own base.
621
576
  *
622
- * @internal
577
+ * Optional in the type, but a host that mounts a devframe with a servable
578
+ * `distDir` yet omits this hook triggers a `DF8106` diagnostic, since the
579
+ * SPA's `./__connection.json` fetch would otherwise fall through and break
580
+ * silently. A static-snapshot host that bakes the meta into its served files
581
+ * can implement it as a no-op to acknowledge this intentionally.
623
582
  */
624
- 'devframe:rpc:server-state:patch': (key: string, patches: any[], syncId: string) => Promise<void>;
583
+ mountConnectionMeta?: (base: string) => void | Promise<void>;
625
584
  /**
626
- * Client→server streaming subscription with optional replay cursor.
627
- * Wired by `RpcStreamingHost`; do not register manually.
628
- *
629
- * @internal
585
+ * Return the public origin the host is reachable at, e.g.
586
+ * `http://localhost:5173`. Used by the dock host to enrich remote
587
+ * iframe URLs with a full `origin`. Called only when a dock needs an
588
+ * absolute URL; hosts that never serve remote docks can return any
589
+ * reasonable value.
630
590
  */
631
- 'devframe:streaming:subscribe': (channel: string, id: string, opts?: {
632
- afterSeq?: number;
633
- }) => Promise<void>;
591
+ resolveOrigin: () => string;
634
592
  /**
635
- * Client→server streaming unsubscribe. Wired by `RpcStreamingHost`;
636
- * do not register manually.
593
+ * Resolve a directory the host owns for persisted devframe state.
594
+ * Each host picks its own app-name namespace so storage doesn't
595
+ * collide between, say, the Vite host (`.vite/devframe`) and a
596
+ * standalone CLI host (`.<appName>/devframe`).
637
597
  *
638
- * @internal
639
- */
640
- 'devframe:streaming:unsubscribe': (channel: string, id: string) => Promise<void>;
641
- /**
642
- * Client→server streaming cancellation request. Wired by
643
- * `RpcStreamingHost`; do not register manually.
598
+ * - `workspace` — state shared with the whole team through version
599
+ * control (saved queries, shared presets). Conventionally
600
+ * `${workspaceRoot}/.devframe/`; hosts must place it somewhere
601
+ * committable.
602
+ * - `project` — per-checkout private state (caches, personal
603
+ * settings). Typically under
604
+ * `${cwd}/node_modules/.<appName>/devframe/`, which version
605
+ * control ignores.
606
+ * - `global` — per-user state (auth tokens, machine-wide
607
+ * preferences). Typically under
608
+ * `${homedir()}/.<appName>/devframe/`.
644
609
  *
645
- * @internal
610
+ * Implementations should ensure the directory exists or be safe to
611
+ * pass to a downstream `createStorage(...)` call that creates it
612
+ * lazily.
646
613
  */
647
- 'devframe:streaming:cancel': (channel: string, id: string) => Promise<void>;
614
+ getStorageDir: (scope: DevframeStorageScope) => string;
615
+ }
616
+ /**
617
+ * Storage placement classes for {@link DevframeHost.getStorageDir}:
618
+ * `workspace` is committable and team-shared, `project` is per-checkout
619
+ * private, `global` is per-user.
620
+ */
621
+ type DevframeStorageScope = 'workspace' | 'project' | 'global';
622
+ //#endregion
623
+ //#region src/types/views.d.ts
624
+ interface DevframeViewHost {
648
625
  /**
649
- * Client→server upload chunk. Wired by `RpcStreamingHost`; do not
650
- * register manually.
651
- *
652
626
  * @internal
653
627
  */
654
- 'devframe:streaming:upload-chunk': (channel: string, id: string, seq: number, chunk: any) => Promise<void>;
628
+ buildStaticDirs: {
629
+ baseUrl: string;
630
+ source: StaticAssetsSource;
631
+ }[];
655
632
  /**
656
- * Client→server upload terminator. Wired by `RpcStreamingHost`; do not
657
- * register manually.
633
+ * Helper to host static files
634
+ * - In `dev` mode, it will register middleware to `viteServer.middlewares` to host the static files
635
+ * - In `build` mode, it will copy the static files to the dist directory
658
636
  *
659
- * @internal
637
+ * Accepts a local dist directory, or a {@link StaticAssetsSource} remote
638
+ * declaration served through devframe's caching CDN back-proxy.
660
639
  */
661
- 'devframe:streaming:upload-end': (channel: string, id: string, error?: {
662
- name: string;
663
- message: string;
664
- }) => Promise<void>;
640
+ hostStatic: (baseUrl: string, source: StaticAssetsSource) => void;
665
641
  }
642
+ //#endregion
643
+ //#region src/types/scope.d.ts
644
+ type AnyRpcFn = (...args: any[]) => any;
666
645
  /**
667
- * To be extended
646
+ * Resolve a scoped bare name `T` back to its fully-qualified entry in
647
+ * `Registry` (`<NS>:<T>`). Indexing the plain registry behind a `keyof`
648
+ * guard keeps TypeScript from rejecting a generic index into the
649
+ * key-remapped `Scoped*Functions<NS>` maps (TS2536) once a plugin augments
650
+ * the registry, while preserving each name's argument/return typing.
668
651
  */
669
- interface DevframeRpcSharedStates {}
670
- //#endregion
671
- //#region src/types/rpc.d.ts
672
- interface DevframeNodeRpcSession {
673
- meta: DevframeNodeRpcSessionMeta;
674
- rpc: BirpcReturn<DevframeRpcClientFunctions, DevframeRpcServerFunctions, false>;
652
+ type ScopedRpcFn<Registry, NS extends string, T extends string> = `${NS}:${T}` extends keyof Registry ? Extract<Registry[`${NS}:${T}`], AnyRpcFn> : AnyRpcFn;
653
+ /**
654
+ * Augmentable registry mapping a scope namespace to the shape of its
655
+ * persisted settings. Tools augment it so `ctx.scope('my-plugin')`
656
+ * gets a fully-typed `settings.global` / `settings.project`:
657
+ *
658
+ * ```ts
659
+ * declare module 'devframe' {
660
+ * interface DevframeSettingsRegistry {
661
+ * 'my-plugin': { theme: 'light' | 'dark', recentFiles: string[] }
662
+ * }
663
+ * }
664
+ * ```
665
+ */
666
+ interface DevframeSettingsRegistry {}
667
+ /**
668
+ * Resolve the settings shape for a namespace from
669
+ * {@link DevframeSettingsRegistry}, falling back to an open record when
670
+ * the namespace hasn't been augmented.
671
+ */
672
+ type SettingsForNamespace<NS extends string> = NS extends keyof DevframeSettingsRegistry ? DevframeSettingsRegistry[NS] extends Record<string, any> ? DevframeSettingsRegistry[NS] : Record<string, any> : Record<string, any>;
673
+ /**
674
+ * A persisted key-value settings store for one scope (`global` or
675
+ * `project`). Backed by a file on the node side and by the shared-state
676
+ * sync protocol on the client, so a `set` on either side propagates to
677
+ * every connected peer and survives restarts.
678
+ *
679
+ * Every method is async because the underlying shared state is resolved
680
+ * lazily on first access.
681
+ */
682
+ interface DevframeSettingsStore<T extends Record<string, any> = Record<string, any>> {
683
+ /** Read a single setting. Resolves to `undefined` when unset. */
684
+ get: <K extends keyof T>(key: K) => Promise<T[K] | undefined>;
685
+ /** Write a single setting. */
686
+ set: <K extends keyof T>(key: K, value: T[K]) => Promise<void>;
687
+ /** Remove a single setting. */
688
+ delete: <K extends keyof T>(key: K) => Promise<void>;
689
+ /** Read the whole settings object (immutable snapshot). */
690
+ all: () => Promise<Readonly<T>>;
691
+ /**
692
+ * Subscribe to settings changes. Resolves to an unsubscribe function.
693
+ * Fires on every local or remote mutation with the full snapshot.
694
+ */
695
+ onChange: (fn: (value: Readonly<T>) => void) => Promise<() => void>;
675
696
  }
676
- interface RpcBroadcastOptions<METHOD, Args extends any[]> {
697
+ /**
698
+ * The two settings scopes available on a scoped context.
699
+ *
700
+ * - `project` — per-workspace state, persisted under the host's
701
+ * `workspace` storage dir. Project-local settings.
702
+ * - `global` — per-user state, persisted under the host's `global`
703
+ * storage dir. Machine-wide preferences.
704
+ */
705
+ interface DevframeSettings<T extends Record<string, any> = Record<string, any>> {
706
+ global: DevframeSettingsStore<T>;
707
+ project: DevframeSettingsStore<T>;
708
+ }
709
+ /**
710
+ * Map the server-side RPC registry down to the bare names owned by a
711
+ * namespace, so a scoped `call('get-cwd')` is typed without the
712
+ * `my-plugin:` prefix.
713
+ */
714
+ type ScopedServerFunctions<NS extends string> = { [K in keyof DevframeRpcServerFunctions as K extends `${NS}:${infer R}` ? R : never]: DevframeRpcServerFunctions[K]; };
715
+ /**
716
+ * Map the client-side RPC registry down to the bare names owned by a
717
+ * namespace, used by scoped `broadcast`.
718
+ */
719
+ type ScopedClientFunctions<NS extends string> = { [K in keyof DevframeRpcClientFunctions as K extends `${NS}:${infer R}` ? R : never]: DevframeRpcClientFunctions[K]; };
720
+ /**
721
+ * Map the shared-state registry down to the bare keys owned by a
722
+ * namespace, so scoped `sharedState('messages')` is typed without the
723
+ * `my-plugin:` prefix.
724
+ */
725
+ type ScopedSharedStates<NS extends string> = { [K in keyof DevframeRpcSharedStates as K extends `${NS}:${infer R}` ? R : never]: DevframeRpcSharedStates[K]; };
726
+ /**
727
+ * Broadcast options for a scoped client method (bare name).
728
+ */
729
+ interface ScopedBroadcastOptions<METHOD, Args extends any[]> {
677
730
  method: METHOD;
678
731
  args: Args;
679
732
  optional?: boolean;
680
733
  event?: boolean;
681
- filter?: (client: BirpcReturn<DevframeRpcClientFunctions, DevframeRpcServerFunctions, false>) => boolean | void;
682
734
  }
683
- type RpcFunctionsHost = RpcFunctionsCollectorBase<DevframeRpcServerFunctions, DevframeNodeContext> & {
684
- /**
685
- * Invoke a locally registered server RPC function directly.
686
- *
687
- * This bypasses transport and is useful for server-side cross-function calls.
688
- */
689
- invokeLocal: <T extends keyof DevframeRpcServerFunctions, Args extends Parameters<DevframeRpcServerFunctions[T]>>(method: T, ...args: Args) => Promise<Awaited<ReturnType<DevframeRpcServerFunctions[T]>>>;
735
+ /**
736
+ * Node-side streaming host scoped to a namespace. Channel names are
737
+ * auto-prefixed with `<namespace>:`.
738
+ */
739
+ interface DevframeScopedStreamingHost {
690
740
  /**
691
- * Broadcast a message to all connected clients
741
+ * Register a streaming channel. The bare `name` is auto-prefixed with
742
+ * the scope namespace (`<namespace>:<name>`); pass an already-qualified
743
+ * name (containing `:`) to opt out.
692
744
  */
693
- broadcast: <T extends keyof DevframeRpcClientFunctions, Args extends Parameters<DevframeRpcClientFunctions[T]>>(options: RpcBroadcastOptions<T, Args>) => Promise<void>;
745
+ create: <T = unknown>(name: string, opts?: RpcStreamingChannelOptions) => RpcStreamingChannel<T>;
746
+ }
747
+ /**
748
+ * The scoped node RPC surface exposed on `ctx.scope('my-plugin').rpc`.
749
+ * IDs and keys you pass are auto-namespaced with `my-plugin:`.
750
+ */
751
+ interface DevframeScopedNodeRpc<NS extends string = string> {
752
+ /** The namespace this surface is scoped to. */
753
+ readonly namespace: NS;
694
754
  /**
695
- * Get the current RPC client
696
- *
697
- * Available in RPC functions to get the current RPC client
755
+ * Register a server RPC function. The definition's `name` must be bare
756
+ * (no `:`); it is stored as `<namespace>:<name>`. Throws `DF0034` if an
757
+ * already-namespaced name is passed.
698
758
  */
699
- getCurrentRpcSession: () => DevframeNodeRpcSession | undefined;
759
+ register: (fn: RpcFunctionDefinition<string, any, any, any, any, any, DevframeNodeContext>, force?: boolean) => void;
700
760
  /**
701
- * The shared state host
761
+ * Update a previously registered server RPC function. Same naming rules
762
+ * as {@link DevframeScopedNodeRpc.register}.
702
763
  */
703
- sharedState: RpcSharedStateHost;
764
+ update: (fn: RpcFunctionDefinition<string, any, any, any, any, any, DevframeNodeContext>, force?: boolean) => void;
704
765
  /**
705
- * The streaming channel host. Provides per-channel `start()` /
706
- * `pipeFrom()` producers; clients consume via `rpc.streaming.subscribe()`.
707
- *
708
- * @see RpcStreamingHost
766
+ * Invoke a locally registered server RPC function. Bare names are
767
+ * resolved within this namespace; pass a fully-qualified name
768
+ * (containing `:`) to call another scope's function.
709
769
  */
710
- streaming: RpcStreamingHost;
711
- };
712
- interface RpcSharedStateGetOptions<T> {
713
- sharedState?: SharedState<T>;
714
- initialValue?: T;
715
- }
716
- interface RpcSharedStateHost {
717
- get: <T extends object = any>(key: string, options?: RpcSharedStateGetOptions<T>) => Promise<SharedState<T>>;
718
- keys: () => string[];
770
+ call: {
771
+ <T extends keyof ScopedServerFunctions<NS> & string>(method: T, ...args: Parameters<ScopedRpcFn<DevframeRpcServerFunctions, NS, T>>): Promise<Awaited<ReturnType<ScopedRpcFn<DevframeRpcServerFunctions, NS, T>>>>;
772
+ <T extends keyof DevframeRpcServerFunctions & string>(method: T, ...args: Parameters<Extract<DevframeRpcServerFunctions[T], AnyRpcFn>>): Promise<Awaited<ReturnType<Extract<DevframeRpcServerFunctions[T], AnyRpcFn>>>>;
773
+ (method: string, ...args: any[]): Promise<any>;
774
+ };
719
775
  /**
720
- * Subscribe to new shared-state keys becoming available. Fires when
721
- * `get(key, ...)` creates a fresh entry (not on subsequent gets).
722
- * Useful for protocol adapters (e.g. MCP) that surface shared state
723
- * as dynamic resources.
776
+ * Broadcast a client RPC event to every connected client. Bare method
777
+ * names are resolved within this namespace.
724
778
  */
725
- onKeyAdded: (fn: (key: string) => void) => () => void;
779
+ broadcast: {
780
+ <T extends keyof ScopedClientFunctions<NS> & string>(options: ScopedBroadcastOptions<T, Parameters<ScopedRpcFn<DevframeRpcClientFunctions, NS, T>>>): Promise<void>;
781
+ (options: ScopedBroadcastOptions<string, any[]>): Promise<void>;
782
+ };
726
783
  /**
727
- * Unregister a shared state and drop its broadcast listeners. Returns
728
- * `true` when a state was removed, `false` when the key was unknown.
729
- * Used by short-lived states (e.g. a disposed JSON-render view) to avoid
730
- * leaking listeners and lingering entries for the context lifetime.
784
+ * Resolve a namespaced shared state. Bare keys are resolved within this
785
+ * namespace; pass a fully-qualified key (containing `:`) to opt out.
731
786
  */
732
- delete: (key: string) => boolean;
787
+ sharedState: {
788
+ <T extends keyof ScopedSharedStates<NS> & string>(key: T, options?: RpcSharedStateGetOptions<ScopedSharedStates<NS>[T]>): Promise<SharedState<ScopedSharedStates<NS>[T]>>;
789
+ <T extends Record<string, any> = Record<string, any>>(key: string, options?: RpcSharedStateGetOptions<T>): Promise<SharedState<T>>;
790
+ };
791
+ /** Streaming host scoped to this namespace. */
792
+ streaming: DevframeScopedStreamingHost;
793
+ /** Pass-through to the current RPC session, when called inside a handler. */
794
+ getCurrentRpcSession: () => DevframeNodeRpcSession | undefined;
733
795
  }
734
796
  /**
735
- * Options for `RpcStreamingHost.create()`.
797
+ * A namespace-scoped view of {@link DevframeNodeContext}. Returned by
798
+ * `ctx.scope('my-plugin')`. Re-exposes the unscoped surfaces (`views`,
799
+ * `diagnostics`, `agent`, …), replaces `rpc` with the auto-namespaced
800
+ * {@link DevframeScopedNodeRpc}, and adds a top-level `settings` store.
736
801
  */
737
- interface RpcStreamingChannelOptions {
738
- /**
739
- * Size of the per-stream ring buffer kept on the server for
740
- * replay-on-resubscribe. `0` (default) disables replay; on reconnect
741
- * the consumer only sees chunks that arrive after subscribing.
742
- *
743
- * The buffer is per stream id, not per channel — each `channel.start()`
744
- * gets its own.
745
- */
746
- replayWindow?: number;
802
+ interface DevframeScopedNodeContext<NS extends string = string, Settings extends Record<string, any> = Record<string, any>> {
803
+ /** The namespace this context is scoped to. */
804
+ readonly namespace: NS;
805
+ /** The underlying unscoped context. */
806
+ readonly base: DevframeNodeContext;
807
+ readonly workspaceRoot: string;
808
+ readonly cwd: string;
809
+ readonly mode: 'dev' | 'build';
810
+ host: DevframeHost;
811
+ rpc: DevframeScopedNodeRpc<NS>;
812
+ /** Persisted settings for this namespace (`global` + `project`). */
813
+ settings: DevframeSettings<Settings>;
814
+ views: DevframeViewHost;
815
+ diagnostics: DevframeDiagnosticsHost;
816
+ agent: DevframeAgentHost;
747
817
  /**
748
- * Milliseconds a closed stream is retained on the server after its
749
- * last subscriber leaves (or if no subscriber ever arrived). During
750
- * this window, late subscribers can still join and replay the buffer
751
- * + receive the `end` frame.
752
- *
753
- * Defaults to `30_000` (30 s) when `replayWindow > 0`, else `0`
754
- * (immediate free). Set to `0` to opt out, or higher for longer
755
- * post-mortem replay.
818
+ * Return a new scoped context, replacing the current scope. Pass `null`
819
+ * or `''` to un-scope and get the base context.
756
820
  */
757
- closedStreamRetention?: number;
821
+ scope: DevframeNodeContext['scope'];
758
822
  }
823
+ //#endregion
824
+ //#region src/types/services.d.ts
759
825
  /**
760
- * Channel handle returned by `ctx.rpc.streaming.create(name, opts)`. A
761
- * channel owns a wire namespace; calling `start()` produces individual
762
- * streams keyed by id.
826
+ * Augmentation point mapping service ids to their implementation types.
827
+ * Providers extend this via `declare module 'devframe'`.
828
+ */
829
+ interface DevframeServicesRegistry {}
830
+ /**
831
+ * A known (augmented) service id, or any namespaced string for services
832
+ * without a published type augmentation.
833
+ */
834
+ type DevframeServiceId = keyof DevframeServicesRegistry | (string & {});
835
+ /** Resolved service type for an id: augmented type, or `unknown`. */
836
+ type DevframeServiceOf<ID> = ID extends keyof DevframeServicesRegistry ? DevframeServicesRegistry[ID] : unknown;
837
+ /**
838
+ * Augmentation point mapping a service's npm package name to the RPC scope
839
+ * namespace it registers its functions under, so a client's
840
+ * `services.get('@devframes/service-x')` returns a scoped RPC surface typed
841
+ * against that namespace. Service packages contribute their entry via
842
+ * declaration merging:
763
843
  *
764
- * @see {@link https://devfra.me/guide/streaming Streaming guide}
844
+ * ```ts
845
+ * declare module 'devframe' {
846
+ * interface DevframeServicesScopeRegistry {
847
+ * '@devframes/service-open': 'devframes:service:open'
848
+ * }
849
+ * }
850
+ * ```
765
851
  */
766
- interface RpcStreamingChannel<T = unknown> {
767
- /** Channel name as registered with `ctx.rpc.streaming.create()`. */
768
- readonly name: string;
852
+ interface DevframeServicesScopeRegistry {}
853
+ /** Resolved RPC scope namespace for a service package name, or `string`. */
854
+ type DevframeServiceScopeOf<PKG> = PKG extends keyof DevframeServicesScopeRegistry ? DevframeServicesScopeRegistry[PKG] & string : string;
855
+ /**
856
+ * A **wire service** — a shared server-side capability (e.g. open-in-editor,
857
+ * syntax highlighting) packaged so any devframe host can install it once and
858
+ * every plugin/client can consume it without re-implementing or re-bundling
859
+ * it. Contrast with plain {@link DevframeServicesHost.provide}, which shares
860
+ * an in-process object between plugins on the node side only: a
861
+ * `DevframeServiceDefinition` additionally registers RPC functions under its
862
+ * {@link DevframeServiceDefinition.scope} and is **advertised to clients**
863
+ * through the reactive `devframe:services` shared state, so browser UIs can
864
+ * feature-detect it (`ctx.services.has(pkg)`) and degrade gracefully.
865
+ *
866
+ * Ship one per npm package (`@devframes/service-<slug>` for first-party),
867
+ * with the package's default export being the `create<X>Service` factory —
868
+ * never a pre-built instance.
869
+ */
870
+ interface DevframeServiceDefinition<API = unknown, Options = any> {
769
871
  /**
770
- * Start a new stream. Returns a server-side sink with both an imperative
771
- * (`write` / `close` / `error`) surface and a `WritableStream<T>` for
772
- * `pipeTo` consumption. The sink's `signal` aborts when every subscriber
773
- * disconnects or cancels.
872
+ * The npm package name this service ships in — also its registry key
873
+ * (`ctx.services.has('@devframes/service-x')` on both node and client).
774
874
  */
775
- start: (opts?: {
776
- id?: string;
777
- }) => StreamSink<T>;
875
+ package: string;
876
+ /** Semver of the service — advertised to clients, checked against descriptor ranges. */
877
+ version: string;
778
878
  /**
779
- * Convenience: start a stream and pipe a `ReadableStream<T>` into it.
780
- * The pipe uses `sink.signal` so cancellation propagates upstream.
781
- *
782
- * Node-stream interop: convert a `Readable` with `Readable.toWeb(node)`
783
- * before passing it here.
879
+ * RPC namespace the service's functions register under, following the
880
+ * plugin id grammar (e.g. `devframes:service:open`). `setup` receives a
881
+ * context pre-scoped to it, so functions register with bare names.
784
882
  */
785
- pipeFrom: (readable: ReadableStream<T>, opts?: {
786
- id?: string;
787
- }) => Promise<StreamSink<T>>;
788
- /** Look up an active stream by id. Returns `undefined` if none. */
789
- get: (id: string) => StreamSink<T> | undefined;
790
- /** All active outbound stream ids on this channel. */
791
- ids: () => string[];
883
+ scope: string;
792
884
  /**
793
- * Open an inbound stream — the server side of a client-to-server
794
- * upload. Allocates an id, returns a `StreamReader<T>` that fills as
795
- * the client writes chunks. Typical pattern is to call this from an
796
- * action handler, kick off background processing, and return the id
797
- * so the caller can start uploading:
798
- *
799
- * ```ts
800
- * handler: async () => {
801
- * const reader = channel.openInbound()
802
- * ;(async () => {
803
- * for await (const chunk of reader) processChunk(chunk)
804
- * })()
805
- * return { uploadId: reader.id }
806
- * }
807
- * ```
808
- *
809
- * Calling `reader.cancel()` on the server sends an `upload-cancel` to
810
- * the uploading client, which aborts its sink.
885
+ * Extra advertised metadata (feature flags, defaults, …). Must be
886
+ * JSON-serializable — it is mirrored to every client.
811
887
  */
812
- openInbound: (opts?: {
813
- id?: string;
814
- }) => StreamReader<T>;
888
+ meta?: Record<string, unknown>;
889
+ /**
890
+ * This instance's own option set (usually baked in by the factory that
891
+ * created the definition). Joins the merge alongside every declarative
892
+ * descriptor's `options`.
893
+ */
894
+ options?: Options;
895
+ /**
896
+ * Merge the option sets contributed by multiple installers (in declaration
897
+ * order) into the one bag passed to `setup`. Defaults to a shallow merge
898
+ * where later sets win.
899
+ */
900
+ mergeOptions?: (sets: Options[]) => Options;
901
+ /**
902
+ * Construct the service: register its RPC functions on the pre-scoped
903
+ * context and return its **node API** — the in-process surface other
904
+ * plugins get from `ctx.services.get(package)` (no RPC hop server-side).
905
+ * `info.options` carries every installer's option sets merged at the
906
+ * `ready()` barrier (via {@link mergeOptions} when present, otherwise a
907
+ * shallow merge in declaration order — later sets win).
908
+ */
909
+ setup: (ctx: DevframeScopedNodeContext, info: {
910
+ options?: Options;
911
+ }) => API | Promise<API>;
815
912
  }
816
913
  /**
817
- * Server-side streaming host. Lives on `ctx.rpc.streaming` alongside
818
- * `ctx.rpc.sharedState`. Each named channel owns its own stream registry
819
- * and wire namespace.
914
+ * Declarative reference to a service package — the form a
915
+ * {@link DevframeServiceInput} takes when the installer doesn't hold the
916
+ * factory itself (e.g. `DevframeDefinition.services`). The host imports the
917
+ * package's default-export factory and installs the resulting definition at
918
+ * the `ready()` barrier.
820
919
  */
821
- interface RpcStreamingHost {
920
+ interface DevframeServiceDescriptor<Options = any> {
921
+ /** npm package name of the service (its default export is the factory). */
922
+ package: string;
822
923
  /**
823
- * Register a streaming channel. Names follow the `<plugin-id>:<channel>`
824
- * convention (e.g. `'my-devtool:chat-stream'`). Throws `DF0032` if the
825
- * name is already taken.
924
+ * Accepted semver range for the installed service. An unsatisfied range
925
+ * warns (`DF0069`) — or throws (`DF0068`) when {@link required} — while the
926
+ * service still installs; the advertised meta carries the real version.
826
927
  */
827
- create: <T = unknown>(name: string, opts?: RpcStreamingChannelOptions) => RpcStreamingChannel<T>;
928
+ version?: string;
828
929
  /**
829
- * Adapters call this when a session disconnects so the host can drop
830
- * subscribers and abort orphaned streams. Most users do not need this;
831
- * it's wired by the RPC server binding automatically.
930
+ * Fail hard when the service can't be imported or its version range isn't
931
+ * satisfied. By default a missing service is skipped silently — clients see
932
+ * `has() === false` and degrade.
832
933
  *
833
- * @internal
934
+ * @default false
834
935
  */
835
- _onSessionDisconnected: (meta: DevframeNodeRpcSessionMeta) => void;
936
+ required?: boolean;
937
+ /** Option set this installer contributes to the merge. */
938
+ options?: Options;
836
939
  }
837
- //#endregion
838
- //#region src/types/diagnostics.d.ts
839
940
  /**
840
- * The shared diagnostics lookup exposed by the host. A `Proxy` that resolves
841
- * any registered code name to its `nostics` handle (a callable that builds
842
- * a diagnostic and routes it through registered reporters). Typed loosely
843
- * because it spans heterogeneous definitions registered by different
844
- * integrations.
941
+ * What can be passed to `ctx.services.install()` (and listed in
942
+ * `DevframeDefinition.services`): a declarative {@link DevframeServiceDescriptor}
943
+ * (the host imports the factory) or a ready {@link DevframeServiceDefinition}
944
+ * (the installer already called the factory — its `options` join the merge).
845
945
  */
846
- type DevframeDiagnosticsLogger = Record<string, any>;
946
+ type DevframeServiceInput<API = unknown, Options = any> = DevframeServiceDescriptor<Options> | DevframeServiceDefinition<API, Options>;
847
947
  /**
848
- * Host for structured diagnostics — a thin layer over `nostics` that lets
849
- * integrations register their own coded errors/warnings into a shared
850
- * registry without taking a direct dependency on `nostics`.
851
- *
852
- * Typical usage from a plugin's `setup(ctx)`:
853
- *
854
- * ```ts
855
- * const myDiagnostics = ctx.diagnostics.defineDiagnostics({
856
- * docsBase: 'https://example.com/errors',
857
- * codes: {
858
- * MYP0001: { why: 'Something went wrong' },
859
- * },
860
- * })
861
- * ctx.diagnostics.register(myDiagnostics)
862
- *
863
- * // Through the shared lookup (loose typing):
864
- * throw ctx.diagnostics.logger.MYP0001()
865
- *
866
- * // Or directly on the typed handle returned from `defineDiagnostics`:
867
- * throw myDiagnostics.MYP0001()
868
- * ```
948
+ * One service's advertisement entry, mirrored to clients through the
949
+ * `devframe:services` shared state.
869
950
  */
870
- interface DevframeDiagnosticsHost {
951
+ interface DevframeServiceMeta {
952
+ /** npm package name — the registry key. */
953
+ package: string;
954
+ /** Installed version of the service. */
955
+ version: string;
956
+ /** RPC namespace its functions live under. */
957
+ scope: string;
958
+ /** Extra service-declared metadata. */
959
+ meta?: Record<string, unknown>;
960
+ }
961
+ /**
962
+ * Shape of the `devframe:services` shared state: package name → advertisement.
963
+ */
964
+ type DevframeServicesState = Record<string, DevframeServiceMeta>;
965
+ interface DevframeServicesHost {
871
966
  /**
872
- * Proxy-backed lookup of every registered diagnostic handle by code name.
873
- * Resolves to a `nostics` `DiagnosticHandle` — a callable that builds a
874
- * diagnostic and routes it through registered reporters; prefix with
875
- * `throw` to raise. Loosely typed — for autocompletion, keep a reference
876
- * to the typed result of `defineDiagnostics()` instead.
967
+ * Publish a service under a namespaced id. Throws `DF0037` when the id is
968
+ * already provided (revoke first to replace). Returns a revoke function.
877
969
  */
878
- readonly logger: DevframeDiagnosticsLogger;
970
+ provide: <ID extends DevframeServiceId>(id: ID, service: DevframeServiceOf<ID>) => () => void;
971
+ /** The service currently provided under `id`, or `undefined`. */
972
+ get: <ID extends DevframeServiceId>(id: ID) => DevframeServiceOf<ID> | undefined;
973
+ has: (id: DevframeServiceId) => boolean;
879
974
  /**
880
- * Register additional diagnostic definitions with this host. After
881
- * registration, codes from the new definition are reachable via
882
- * `host.logger.CODE`. Plugins that want shared output formatting should
883
- * build their diagnostics via `host.defineDiagnostics()` first — that
884
- * factory pre-wires the host's ANSI console reporter.
975
+ * Run `callback` with the service as soon as it is available — immediately
976
+ * when already provided, otherwise on `provide`. Survives provider/consumer
977
+ * setup-order differences. The callback also re-fires if the service is
978
+ * revoked and provided again. Returns an unsubscribe function.
885
979
  */
886
- register: (definitions: Record<string, unknown>) => void;
980
+ whenAvailable: <ID extends DevframeServiceId>(id: ID, callback: (service: DevframeServiceOf<ID>) => void) => () => void;
981
+ /** Ids of every currently-provided service. */
982
+ keys: () => string[];
887
983
  /**
888
- * Build a typed diagnostics object with the host's ANSI console reporter
889
- * pre-wired. The same `devframe/utils/nostics` `defineDiagnostics` every
890
- * built-in plugin's module-level `diagnostics.ts` uses, so integrations
891
- * don't need to take a direct dependency on `nostics`.
892
- */
893
- defineDiagnostics: typeof defineDiagnostics;
984
+ * Install a **wire service** (see {@link DevframeServiceDefinition}).
985
+ * Before {@link DevframeServicesHost.ready} fires, installs are queued —
986
+ * option sets from every installer accumulate and each service is
987
+ * constructed **once** at the barrier with the merged options; the
988
+ * returned promise resolves with the service's node API then (or
989
+ * `undefined` when an optional descriptor's package can't be imported).
990
+ * After the barrier, installs construct immediately; installing an
991
+ * already-installed package returns the existing API (a warning is
992
+ * emitted when the late install carried options, since they're ignored).
993
+ *
994
+ * `resolveFrom` is where a descriptor's package resolves **from**: a path
995
+ * or file URL (e.g. `import.meta.url`), or an npm package name — typically
996
+ * the declaring plugin's `packageName`, so its declared services resolve
997
+ * against the plugin's own dependencies. Falls back to the context's
998
+ * `workspaceRoot`.
999
+ */
1000
+ install: <API = unknown, Options = any>(input: DevframeServiceInput<API, Options>, options?: {
1001
+ resolveFrom?: string | null;
1002
+ }) => Promise<API | undefined>;
1003
+ /**
1004
+ * Fire the collect-then-setup barrier: resolve every queued descriptor
1005
+ * (importing its package), merge option sets per service, construct each
1006
+ * service once, `provide()` its node API under the package name, and
1007
+ * advertise it to clients via the `devframe:services` shared state.
1008
+ * Idempotent — adapters call it once after every devframe's `setup` has
1009
+ * run; a repeat returns the same promise. Rejects when a `required`
1010
+ * service fails to import or misses its version range.
1011
+ */
1012
+ ready: () => Promise<void>;
894
1013
  }
895
1014
  //#endregion
896
- //#region src/types/host.d.ts
897
- interface DevframeHost {
1015
+ //#region src/types/rpc-augments.d.ts
1016
+ /**
1017
+ * To be extended
1018
+ */
1019
+ interface DevframeRpcClientFunctions {
898
1020
  /**
899
- * Serve static assets at the given URL base — a local directory, or a
900
- * resolved {@link RemoteAssetsStore} back-proxy. Called by
901
- * `DevframeViewHost.hostStatic` (which normalizes `RemoteAssets`
902
- * declarations into stores first). Implementations map this to whatever
903
- * the underlying runtime expects (Vite middleware, h3 handler, no-op
904
- * for build snapshots) — the shared engine in
905
- * `devframe/utils/serve-static` accepts either shape.
1021
+ * Server→client notification that this connection's auth token has been
1022
+ * revoked. The client drops to untrusted on receipt. Broadcast by
1023
+ * `revokeActiveConnectionsForToken`.
1024
+ *
1025
+ * @internal
906
1026
  */
907
- mountStatic: (base: string, source: string | RemoteAssetsStore) => void | Promise<void>;
1027
+ 'devframe:auth:revoked': () => Promise<void>;
908
1028
  /**
909
- * Serve the host's connection meta (`__connection.json`) at the given URL
910
- * base, so a devframe SPA mounted there can discover the RPC/WS endpoint
911
- * via `connectDevframe()`'s relative `./__connection.json` fetch.
1029
+ * Streaming chunk pushed from server to subscribed clients. Wired by
1030
+ * `RpcStreamingHost`; do not register manually.
912
1031
  *
913
- * Called by `ctx.install` for each mounted devframe (alongside
914
- * `mountStatic`). Without it, an embedded SPA can only discover the
915
- * endpoint by inheriting it from a same-origin parent window — which fails
916
- * for cross-origin or sandboxed iframes. Implementations serve the same
917
- * meta they expose at the hub's own base.
1032
+ * @internal
1033
+ */
1034
+ 'devframe:streaming:chunk': (channel: string, id: string, seq: number, chunk: any) => Promise<void>;
1035
+ /**
1036
+ * Streaming terminator pushed from server to subscribed clients. Wired by
1037
+ * `RpcStreamingHost`; do not register manually.
918
1038
  *
919
- * Optional in the type, but a host that mounts a devframe with a servable
920
- * `distDir` yet omits this hook triggers a `DF8106` diagnostic, since the
921
- * SPA's `./__connection.json` fetch would otherwise fall through and break
922
- * silently. A static-snapshot host that bakes the meta into its served files
923
- * can implement it as a no-op to acknowledge this intentionally.
1039
+ * @internal
924
1040
  */
925
- mountConnectionMeta?: (base: string) => void | Promise<void>;
1041
+ 'devframe:streaming:end': (channel: string, id: string, error?: {
1042
+ name: string;
1043
+ message: string;
1044
+ }) => Promise<void>;
926
1045
  /**
927
- * Return the public origin the host is reachable at, e.g.
928
- * `http://localhost:5173`. Used by the dock host to enrich remote
929
- * iframe URLs with a full `origin`. Called only when a dock needs an
930
- * absolute URL; hosts that never serve remote docks can return any
931
- * reasonable value.
1046
+ * Server→client cancel for an in-flight upload. Wired by
1047
+ * `RpcStreamingHost`; do not register manually.
1048
+ *
1049
+ * @internal
932
1050
  */
933
- resolveOrigin: () => string;
1051
+ 'devframe:streaming:upload-cancel': (channel: string, id: string) => Promise<void>;
934
1052
  /**
935
- * Resolve a directory the host owns for persisted devframe state.
936
- * Each host picks its own app-name namespace so storage doesn't
937
- * collide between, say, the Vite host (`.vite/devframe`) and a
938
- * standalone CLI host (`.<appName>/devframe`).
1053
+ * Full shared-state snapshot pushed from server to subscribed clients.
1054
+ * Wired by `RpcSharedStateHost`; do not register manually.
939
1055
  *
940
- * - `workspace` — state shared with the whole team through version
941
- * control (saved queries, shared presets). Conventionally
942
- * `${workspaceRoot}/.devframe/`; hosts must place it somewhere
943
- * committable.
944
- * - `project` — per-checkout private state (caches, personal
945
- * settings). Typically under
946
- * `${cwd}/node_modules/.<appName>/devframe/`, which version
947
- * control ignores.
948
- * - `global` — per-user state (auth tokens, machine-wide
949
- * preferences). Typically under
950
- * `${homedir()}/.<appName>/devframe/`.
1056
+ * @internal
1057
+ */
1058
+ 'devframe:rpc:client-state:updated': (key: string, fullState: any, syncId: string) => Promise<void>;
1059
+ /**
1060
+ * Incremental shared-state patch pushed from server to subscribed clients.
1061
+ * Wired by `RpcSharedStateHost`; do not register manually.
951
1062
  *
952
- * Implementations should ensure the directory exists or be safe to
953
- * pass to a downstream `createStorage(...)` call that creates it
954
- * lazily.
1063
+ * @internal
955
1064
  */
956
- getStorageDir: (scope: DevframeStorageScope) => string;
1065
+ 'devframe:rpc:client-state:patch': (key: string, patches: any[], syncId: string) => Promise<void>;
957
1066
  }
958
1067
  /**
959
- * Storage placement classes for {@link DevframeHost.getStorageDir}:
960
- * `workspace` is committable and team-shared, `project` is per-checkout
961
- * private, `global` is per-user.
1068
+ * To be extended
962
1069
  */
963
- type DevframeStorageScope = 'workspace' | 'project' | 'global';
964
- //#endregion
965
- //#region src/types/views.d.ts
966
- interface DevframeViewHost {
1070
+ interface DevframeRpcServerFunctions {
967
1071
  /**
1072
+ * Authenticate a connection with a previously-issued bearer token; resolves
1073
+ * whether the connection is now trusted. The interactive handler is provided
1074
+ * by the host adapter (e.g. Vite DevTools); the standalone server registers
1075
+ * an auto-trust noop when `auth: false`.
1076
+ *
1077
+ * Named with the `anonymous:` prefix (see `isAnonymousRpcMethod`) so it is
1078
+ * reachable before the connection is trusted — this is the *only* rule the
1079
+ * pre-trust gate applies, no per-method allowlist.
1080
+ *
968
1081
  * @internal
969
1082
  */
970
- buildStaticDirs: {
971
- baseUrl: string;
972
- source: StaticAssetsSource;
973
- }[];
1083
+ 'anonymous:devframe:auth': (params: {
1084
+ authToken: string;
1085
+ ua: string;
1086
+ origin: string;
1087
+ }) => Promise<{
1088
+ isTrusted: boolean;
1089
+ }>;
974
1090
  /**
975
- * Helper to host static files
976
- * - In `dev` mode, it will register middleware to `viteServer.middlewares` to host the static files
977
- * - In `build` mode, it will copy the static files to the dist directory
1091
+ * Exchange a one-time authentication code (shown by the dev server) for a fresh,
1092
+ * node-issued bearer token, returning the token on success or `null`. The
1093
+ * handler is provided by the host adapter on top of `exchangeTempAuthCode`.
978
1094
  *
979
- * Accepts a local dist directory, or a {@link StaticAssetsSource} remote
980
- * declaration served through devframe's caching CDN back-proxy.
1095
+ * Named with the `anonymous:` prefix (see `isAnonymousRpcMethod`) so it is
1096
+ * reachable before the connection is trusted.
1097
+ *
1098
+ * @internal
981
1099
  */
982
- hostStatic: (baseUrl: string, source: StaticAssetsSource) => void;
983
- }
984
- //#endregion
985
- //#region src/types/scope.d.ts
986
- type AnyRpcFn = (...args: any[]) => any;
987
- /**
988
- * Resolve a scoped bare name `T` back to its fully-qualified entry in
989
- * `Registry` (`<NS>:<T>`). Indexing the plain registry behind a `keyof`
990
- * guard keeps TypeScript from rejecting a generic index into the
991
- * key-remapped `Scoped*Functions<NS>` maps (TS2536) once a plugin augments
992
- * the registry, while preserving each name's argument/return typing.
993
- */
994
- type ScopedRpcFn<Registry, NS extends string, T extends string> = `${NS}:${T}` extends keyof Registry ? Extract<Registry[`${NS}:${T}`], AnyRpcFn> : AnyRpcFn;
995
- /**
996
- * Augmentable registry mapping a scope namespace to the shape of its
997
- * persisted settings. Tools augment it so `ctx.scope('my-plugin')`
998
- * gets a fully-typed `settings.global` / `settings.project`:
999
- *
1000
- * ```ts
1001
- * declare module 'devframe' {
1002
- * interface DevframeSettingsRegistry {
1003
- * 'my-plugin': { theme: 'light' | 'dark', recentFiles: string[] }
1004
- * }
1005
- * }
1006
- * ```
1007
- */
1008
- interface DevframeSettingsRegistry {}
1009
- /**
1010
- * Resolve the settings shape for a namespace from
1011
- * {@link DevframeSettingsRegistry}, falling back to an open record when
1012
- * the namespace hasn't been augmented.
1013
- */
1014
- type SettingsForNamespace<NS extends string> = NS extends keyof DevframeSettingsRegistry ? DevframeSettingsRegistry[NS] extends Record<string, any> ? DevframeSettingsRegistry[NS] : Record<string, any> : Record<string, any>;
1015
- /**
1016
- * A persisted key-value settings store for one scope (`global` or
1017
- * `project`). Backed by a file on the node side and by the shared-state
1018
- * sync protocol on the client, so a `set` on either side propagates to
1019
- * every connected peer and survives restarts.
1020
- *
1021
- * Every method is async because the underlying shared state is resolved
1022
- * lazily on first access.
1023
- */
1024
- interface DevframeSettingsStore<T extends Record<string, any> = Record<string, any>> {
1025
- /** Read a single setting. Resolves to `undefined` when unset. */
1026
- get: <K extends keyof T>(key: K) => Promise<T[K] | undefined>;
1027
- /** Write a single setting. */
1028
- set: <K extends keyof T>(key: K, value: T[K]) => Promise<void>;
1029
- /** Remove a single setting. */
1030
- delete: <K extends keyof T>(key: K) => Promise<void>;
1031
- /** Read the whole settings object (immutable snapshot). */
1032
- all: () => Promise<Readonly<T>>;
1100
+ 'anonymous:devframe:auth:exchange': (params: {
1101
+ code: string;
1102
+ ua: string;
1103
+ origin: string;
1104
+ }) => Promise<{
1105
+ authToken: string | null;
1106
+ }>;
1033
1107
  /**
1034
- * Subscribe to settings changes. Resolves to an unsubscribe function.
1035
- * Fires on every local or remote mutation with the full snapshot.
1108
+ * Self-revoke: the caller asks the server to revoke its own bearer token
1109
+ * (if any) and drop to untrusted. Requires an already-trusted caller, so
1110
+ * unlike the two handshake methods above it does **not** carry the
1111
+ * `anonymous:` prefix. Registered by `recipes/interactive-auth`.
1112
+ *
1113
+ * @internal
1036
1114
  */
1037
- onChange: (fn: (value: Readonly<T>) => void) => Promise<() => void>;
1038
- }
1039
- /**
1040
- * The two settings scopes available on a scoped context.
1041
- *
1042
- * - `project` — per-workspace state, persisted under the host's
1043
- * `workspace` storage dir. Project-local settings.
1044
- * - `global` — per-user state, persisted under the host's `global`
1045
- * storage dir. Machine-wide preferences.
1046
- */
1047
- interface DevframeSettings<T extends Record<string, any> = Record<string, any>> {
1048
- global: DevframeSettingsStore<T>;
1049
- project: DevframeSettingsStore<T>;
1050
- }
1051
- /**
1052
- * Map the server-side RPC registry down to the bare names owned by a
1053
- * namespace, so a scoped `call('get-cwd')` is typed without the
1054
- * `my-plugin:` prefix.
1055
- */
1056
- type ScopedServerFunctions<NS extends string> = { [K in keyof DevframeRpcServerFunctions as K extends `${NS}:${infer R}` ? R : never]: DevframeRpcServerFunctions[K]; };
1057
- /**
1058
- * Map the client-side RPC registry down to the bare names owned by a
1059
- * namespace, used by scoped `broadcast`.
1060
- */
1061
- type ScopedClientFunctions<NS extends string> = { [K in keyof DevframeRpcClientFunctions as K extends `${NS}:${infer R}` ? R : never]: DevframeRpcClientFunctions[K]; };
1062
- /**
1063
- * Map the shared-state registry down to the bare keys owned by a
1064
- * namespace, so scoped `sharedState('messages')` is typed without the
1065
- * `my-plugin:` prefix.
1066
- */
1067
- type ScopedSharedStates<NS extends string> = { [K in keyof DevframeRpcSharedStates as K extends `${NS}:${infer R}` ? R : never]: DevframeRpcSharedStates[K]; };
1115
+ 'devframe:auth:revoke': () => Promise<void>;
1116
+ /**
1117
+ * Subscribe a client to a shared-state key. Wired by
1118
+ * `RpcSharedStateHost`; do not register manually.
1119
+ *
1120
+ * @internal
1121
+ */
1122
+ 'devframe:rpc:server-state:subscribe': (key: string) => Promise<void>;
1123
+ /**
1124
+ * Read the current value for a shared-state key. Wired by
1125
+ * `RpcSharedStateHost`; do not register manually.
1126
+ *
1127
+ * @internal
1128
+ */
1129
+ 'devframe:rpc:server-state:get': (key: string) => Promise<any>;
1130
+ /**
1131
+ * Replace a shared-state value (from the client). Wired by
1132
+ * `RpcSharedStateHost`; do not register manually.
1133
+ *
1134
+ * @internal
1135
+ */
1136
+ 'devframe:rpc:server-state:set': (key: string, value: any, syncId: string) => Promise<void>;
1137
+ /**
1138
+ * Apply a patch to a shared-state value (from the client). Wired by
1139
+ * `RpcSharedStateHost`; do not register manually.
1140
+ *
1141
+ * @internal
1142
+ */
1143
+ 'devframe:rpc:server-state:patch': (key: string, patches: any[], syncId: string) => Promise<void>;
1144
+ /**
1145
+ * Client→server streaming subscription with optional replay cursor.
1146
+ * Wired by `RpcStreamingHost`; do not register manually.
1147
+ *
1148
+ * @internal
1149
+ */
1150
+ 'devframe:streaming:subscribe': (channel: string, id: string, opts?: {
1151
+ afterSeq?: number;
1152
+ }) => Promise<void>;
1153
+ /**
1154
+ * Client→server streaming unsubscribe. Wired by `RpcStreamingHost`;
1155
+ * do not register manually.
1156
+ *
1157
+ * @internal
1158
+ */
1159
+ 'devframe:streaming:unsubscribe': (channel: string, id: string) => Promise<void>;
1160
+ /**
1161
+ * Client→server streaming cancellation request. Wired by
1162
+ * `RpcStreamingHost`; do not register manually.
1163
+ *
1164
+ * @internal
1165
+ */
1166
+ 'devframe:streaming:cancel': (channel: string, id: string) => Promise<void>;
1167
+ /**
1168
+ * Client→server upload chunk. Wired by `RpcStreamingHost`; do not
1169
+ * register manually.
1170
+ *
1171
+ * @internal
1172
+ */
1173
+ 'devframe:streaming:upload-chunk': (channel: string, id: string, seq: number, chunk: any) => Promise<void>;
1174
+ /**
1175
+ * Client→server upload terminator. Wired by `RpcStreamingHost`; do not
1176
+ * register manually.
1177
+ *
1178
+ * @internal
1179
+ */
1180
+ 'devframe:streaming:upload-end': (channel: string, id: string, error?: {
1181
+ name: string;
1182
+ message: string;
1183
+ }) => Promise<void>;
1184
+ }
1068
1185
  /**
1069
- * Broadcast options for a scoped client method (bare name).
1186
+ * To be extended
1070
1187
  */
1071
- interface ScopedBroadcastOptions<METHOD, Args extends any[]> {
1188
+ interface DevframeRpcSharedStates {
1189
+ /**
1190
+ * Wire-service advertisements — package name → `{ package, version,
1191
+ * scope, meta }` for every installed {@link import('./services').DevframeServiceDefinition}.
1192
+ * Written by the node services host at the `ready()` barrier; clients read
1193
+ * it through `client.services` (or subscribe to it directly for
1194
+ * reactivity). Read-only from the browser.
1195
+ */
1196
+ 'devframe:services': DevframeServicesState;
1197
+ }
1198
+ //#endregion
1199
+ //#region src/types/rpc.d.ts
1200
+ interface DevframeNodeRpcSession {
1201
+ meta: DevframeNodeRpcSessionMeta;
1202
+ rpc: BirpcReturn<DevframeRpcClientFunctions, DevframeRpcServerFunctions, false>;
1203
+ }
1204
+ interface RpcBroadcastOptions<METHOD, Args extends any[]> {
1072
1205
  method: METHOD;
1073
1206
  args: Args;
1074
1207
  optional?: boolean;
1075
1208
  event?: boolean;
1209
+ filter?: (client: BirpcReturn<DevframeRpcClientFunctions, DevframeRpcServerFunctions, false>) => boolean | void;
1076
1210
  }
1077
- /**
1078
- * Node-side streaming host scoped to a namespace. Channel names are
1079
- * auto-prefixed with `<namespace>:`.
1080
- */
1081
- interface DevframeScopedStreamingHost {
1211
+ type RpcFunctionsHost = RpcFunctionsCollectorBase<DevframeRpcServerFunctions, DevframeNodeContext> & {
1082
1212
  /**
1083
- * Register a streaming channel. The bare `name` is auto-prefixed with
1084
- * the scope namespace (`<namespace>:<name>`); pass an already-qualified
1085
- * name (containing `:`) to opt out.
1213
+ * Invoke a locally registered server RPC function directly.
1214
+ *
1215
+ * This bypasses transport and is useful for server-side cross-function calls.
1086
1216
  */
1087
- create: <T = unknown>(name: string, opts?: RpcStreamingChannelOptions) => RpcStreamingChannel<T>;
1088
- }
1089
- /**
1090
- * The scoped node RPC surface exposed on `ctx.scope('my-plugin').rpc`.
1091
- * IDs and keys you pass are auto-namespaced with `my-plugin:`.
1092
- */
1093
- interface DevframeScopedNodeRpc<NS extends string = string> {
1094
- /** The namespace this surface is scoped to. */
1095
- readonly namespace: NS;
1217
+ invokeLocal: <T extends keyof DevframeRpcServerFunctions, Args extends Parameters<DevframeRpcServerFunctions[T]>>(method: T, ...args: Args) => Promise<Awaited<ReturnType<DevframeRpcServerFunctions[T]>>>;
1096
1218
  /**
1097
- * Register a server RPC function. The definition's `name` must be bare
1098
- * (no `:`); it is stored as `<namespace>:<name>`. Throws `DF0034` if an
1099
- * already-namespaced name is passed.
1219
+ * Broadcast a message to all connected clients
1100
1220
  */
1101
- register: (fn: RpcFunctionDefinition<string, any, any, any, any, any, DevframeNodeContext>, force?: boolean) => void;
1221
+ broadcast: <T extends keyof DevframeRpcClientFunctions, Args extends Parameters<DevframeRpcClientFunctions[T]>>(options: RpcBroadcastOptions<T, Args>) => Promise<void>;
1102
1222
  /**
1103
- * Update a previously registered server RPC function. Same naming rules
1104
- * as {@link DevframeScopedNodeRpc.register}.
1223
+ * Get the current RPC client
1224
+ *
1225
+ * Available in RPC functions to get the current RPC client
1105
1226
  */
1106
- update: (fn: RpcFunctionDefinition<string, any, any, any, any, any, DevframeNodeContext>, force?: boolean) => void;
1227
+ getCurrentRpcSession: () => DevframeNodeRpcSession | undefined;
1107
1228
  /**
1108
- * Invoke a locally registered server RPC function. Bare names are
1109
- * resolved within this namespace; pass a fully-qualified name
1110
- * (containing `:`) to call another scope's function.
1229
+ * The shared state host
1111
1230
  */
1112
- call: {
1113
- <T extends keyof ScopedServerFunctions<NS> & string>(method: T, ...args: Parameters<ScopedRpcFn<DevframeRpcServerFunctions, NS, T>>): Promise<Awaited<ReturnType<ScopedRpcFn<DevframeRpcServerFunctions, NS, T>>>>;
1114
- <T extends keyof DevframeRpcServerFunctions & string>(method: T, ...args: Parameters<Extract<DevframeRpcServerFunctions[T], AnyRpcFn>>): Promise<Awaited<ReturnType<Extract<DevframeRpcServerFunctions[T], AnyRpcFn>>>>;
1115
- (method: string, ...args: any[]): Promise<any>;
1116
- };
1231
+ sharedState: RpcSharedStateHost;
1117
1232
  /**
1118
- * Broadcast a client RPC event to every connected client. Bare method
1119
- * names are resolved within this namespace.
1233
+ * The streaming channel host. Provides per-channel `start()` /
1234
+ * `pipeFrom()` producers; clients consume via `rpc.streaming.subscribe()`.
1235
+ *
1236
+ * @see RpcStreamingHost
1120
1237
  */
1121
- broadcast: {
1122
- <T extends keyof ScopedClientFunctions<NS> & string>(options: ScopedBroadcastOptions<T, Parameters<ScopedRpcFn<DevframeRpcClientFunctions, NS, T>>>): Promise<void>;
1123
- (options: ScopedBroadcastOptions<string, any[]>): Promise<void>;
1124
- };
1238
+ streaming: RpcStreamingHost;
1239
+ };
1240
+ interface RpcSharedStateGetOptions<T> {
1241
+ sharedState?: SharedState<T>;
1242
+ initialValue?: T;
1243
+ }
1244
+ interface RpcSharedStateHost {
1245
+ get: <T extends object = any>(key: string, options?: RpcSharedStateGetOptions<T>) => Promise<SharedState<T>>;
1246
+ keys: () => string[];
1125
1247
  /**
1126
- * Resolve a namespaced shared state. Bare keys are resolved within this
1127
- * namespace; pass a fully-qualified key (containing `:`) to opt out.
1248
+ * Subscribe to new shared-state keys becoming available. Fires when
1249
+ * `get(key, ...)` creates a fresh entry (not on subsequent gets).
1250
+ * Useful for protocol adapters (e.g. MCP) that surface shared state
1251
+ * as dynamic resources.
1128
1252
  */
1129
- sharedState: {
1130
- <T extends keyof ScopedSharedStates<NS> & string>(key: T, options?: RpcSharedStateGetOptions<ScopedSharedStates<NS>[T]>): Promise<SharedState<ScopedSharedStates<NS>[T]>>;
1131
- <T extends Record<string, any> = Record<string, any>>(key: string, options?: RpcSharedStateGetOptions<T>): Promise<SharedState<T>>;
1132
- };
1133
- /** Streaming host scoped to this namespace. */
1134
- streaming: DevframeScopedStreamingHost;
1135
- /** Pass-through to the current RPC session, when called inside a handler. */
1136
- getCurrentRpcSession: () => DevframeNodeRpcSession | undefined;
1253
+ onKeyAdded: (fn: (key: string) => void) => () => void;
1254
+ /**
1255
+ * Unregister a shared state and drop its broadcast listeners. Returns
1256
+ * `true` when a state was removed, `false` when the key was unknown.
1257
+ * Used by short-lived states (e.g. a disposed JSON-render view) to avoid
1258
+ * leaking listeners and lingering entries for the context lifetime.
1259
+ */
1260
+ delete: (key: string) => boolean;
1137
1261
  }
1138
1262
  /**
1139
- * A namespace-scoped view of {@link DevframeNodeContext}. Returned by
1140
- * `ctx.scope('my-plugin')`. Re-exposes the unscoped surfaces (`views`,
1141
- * `diagnostics`, `agent`, …), replaces `rpc` with the auto-namespaced
1142
- * {@link DevframeScopedNodeRpc}, and adds a top-level `settings` store.
1263
+ * Options for `RpcStreamingHost.create()`.
1143
1264
  */
1144
- interface DevframeScopedNodeContext<NS extends string = string, Settings extends Record<string, any> = Record<string, any>> {
1145
- /** The namespace this context is scoped to. */
1146
- readonly namespace: NS;
1147
- /** The underlying unscoped context. */
1148
- readonly base: DevframeNodeContext;
1149
- readonly workspaceRoot: string;
1150
- readonly cwd: string;
1151
- readonly mode: 'dev' | 'build';
1152
- host: DevframeHost;
1153
- rpc: DevframeScopedNodeRpc<NS>;
1154
- /** Persisted settings for this namespace (`global` + `project`). */
1155
- settings: DevframeSettings<Settings>;
1156
- views: DevframeViewHost;
1157
- diagnostics: DevframeDiagnosticsHost;
1158
- agent: DevframeAgentHost;
1265
+ interface RpcStreamingChannelOptions {
1159
1266
  /**
1160
- * Return a new scoped context, replacing the current scope. Pass `null`
1161
- * or `''` to un-scope and get the base context.
1267
+ * Size of the per-stream ring buffer kept on the server for
1268
+ * replay-on-resubscribe. `0` (default) disables replay; on reconnect
1269
+ * the consumer only sees chunks that arrive after subscribing.
1270
+ *
1271
+ * The buffer is per stream id, not per channel — each `channel.start()`
1272
+ * gets its own.
1162
1273
  */
1163
- scope: DevframeNodeContext['scope'];
1274
+ replayWindow?: number;
1275
+ /**
1276
+ * Milliseconds a closed stream is retained on the server after its
1277
+ * last subscriber leaves (or if no subscriber ever arrived). During
1278
+ * this window, late subscribers can still join and replay the buffer
1279
+ * + receive the `end` frame.
1280
+ *
1281
+ * Defaults to `30_000` (30 s) when `replayWindow > 0`, else `0`
1282
+ * (immediate free). Set to `0` to opt out, or higher for longer
1283
+ * post-mortem replay.
1284
+ */
1285
+ closedStreamRetention?: number;
1164
1286
  }
1165
- //#endregion
1166
- //#region src/types/services.d.ts
1167
1287
  /**
1168
- * Cross-plugin services — a typed, namespaced registry on the shared node
1169
- * context through which one integration exposes a capability and others
1170
- * consume it without a hard package dependency.
1171
- *
1172
- * A provider ships a *types-only* augmentation of {@link DevframeServicesRegistry}
1173
- * (so consumers get full typing from `import type`), then provides the
1174
- * implementation at setup time:
1175
- *
1176
- * ```ts
1177
- * // provider (e.g. the data-inspector plugin)
1178
- * declare module 'devframe' {
1179
- * interface DevframeServicesRegistry {
1180
- * 'devframes:plugin:data-inspector:sources': DataSourcesService
1181
- * }
1182
- * }
1183
- * ctx.services.provide('devframes:plugin:data-inspector:sources', host)
1184
- *
1185
- * // consumer (another plugin) — no runtime dependency on the provider
1186
- * ctx.services.whenAvailable('devframes:plugin:data-inspector:sources', (sources) => {
1187
- * sources.register({ id: 'my-plugin:state', title: 'My state', data: () => state })
1188
- * })
1189
- * ```
1288
+ * Channel handle returned by `ctx.rpc.streaming.create(name, opts)`. A
1289
+ * channel owns a wire namespace; calling `start()` produces individual
1290
+ * streams keyed by id.
1190
1291
  *
1191
- * Service ids follow the same namespacing rule as RPC functions: prefix with
1192
- * the providing plugin's id.
1193
- */
1194
- /**
1195
- * Augmentation point mapping service ids to their implementation types.
1196
- * Providers extend this via `declare module 'devframe'`.
1292
+ * @see {@link https://devfra.me/guide/streaming Streaming guide}
1197
1293
  */
1198
- interface DevframeServicesRegistry {}
1294
+ interface RpcStreamingChannel<T = unknown> {
1295
+ /** Channel name as registered with `ctx.rpc.streaming.create()`. */
1296
+ readonly name: string;
1297
+ /**
1298
+ * Start a new stream. Returns a server-side sink with both an imperative
1299
+ * (`write` / `close` / `error`) surface and a `WritableStream<T>` for
1300
+ * `pipeTo` consumption. The sink's `signal` aborts when every subscriber
1301
+ * disconnects or cancels.
1302
+ */
1303
+ start: (opts?: {
1304
+ id?: string;
1305
+ }) => StreamSink<T>;
1306
+ /**
1307
+ * Convenience: start a stream and pipe a `ReadableStream<T>` into it.
1308
+ * The pipe uses `sink.signal` so cancellation propagates upstream.
1309
+ *
1310
+ * Node-stream interop: convert a `Readable` with `Readable.toWeb(node)`
1311
+ * before passing it here.
1312
+ */
1313
+ pipeFrom: (readable: ReadableStream<T>, opts?: {
1314
+ id?: string;
1315
+ }) => Promise<StreamSink<T>>;
1316
+ /** Look up an active stream by id. Returns `undefined` if none. */
1317
+ get: (id: string) => StreamSink<T> | undefined;
1318
+ /** All active outbound stream ids on this channel. */
1319
+ ids: () => string[];
1320
+ /**
1321
+ * Open an inbound stream — the server side of a client-to-server
1322
+ * upload. Allocates an id, returns a `StreamReader<T>` that fills as
1323
+ * the client writes chunks. Typical pattern is to call this from an
1324
+ * action handler, kick off background processing, and return the id
1325
+ * so the caller can start uploading:
1326
+ *
1327
+ * ```ts
1328
+ * handler: async () => {
1329
+ * const reader = channel.openInbound()
1330
+ * ;(async () => {
1331
+ * for await (const chunk of reader) processChunk(chunk)
1332
+ * })()
1333
+ * return { uploadId: reader.id }
1334
+ * }
1335
+ * ```
1336
+ *
1337
+ * Calling `reader.cancel()` on the server sends an `upload-cancel` to
1338
+ * the uploading client, which aborts its sink.
1339
+ */
1340
+ openInbound: (opts?: {
1341
+ id?: string;
1342
+ }) => StreamReader<T>;
1343
+ }
1199
1344
  /**
1200
- * A known (augmented) service id, or any namespaced string for services
1201
- * without a published type augmentation.
1345
+ * Server-side streaming host. Lives on `ctx.rpc.streaming` alongside
1346
+ * `ctx.rpc.sharedState`. Each named channel owns its own stream registry
1347
+ * and wire namespace.
1202
1348
  */
1203
- type DevframeServiceId = keyof DevframeServicesRegistry | (string & {});
1204
- /** Resolved service type for an id: augmented type, or `unknown`. */
1205
- type DevframeServiceOf<ID> = ID extends keyof DevframeServicesRegistry ? DevframeServicesRegistry[ID] : unknown;
1206
- interface DevframeServicesHost {
1349
+ interface RpcStreamingHost {
1207
1350
  /**
1208
- * Publish a service under a namespaced id. Throws `DF0037` when the id is
1209
- * already provided (revoke first to replace). Returns a revoke function.
1351
+ * Register a streaming channel. Names follow the `<plugin-id>:<channel>`
1352
+ * convention (e.g. `'my-devtool:chat-stream'`). Throws `DF0032` if the
1353
+ * name is already taken.
1210
1354
  */
1211
- provide: <ID extends DevframeServiceId>(id: ID, service: DevframeServiceOf<ID>) => () => void;
1212
- /** The service currently provided under `id`, or `undefined`. */
1213
- get: <ID extends DevframeServiceId>(id: ID) => DevframeServiceOf<ID> | undefined;
1214
- has: (id: DevframeServiceId) => boolean;
1355
+ create: <T = unknown>(name: string, opts?: RpcStreamingChannelOptions) => RpcStreamingChannel<T>;
1215
1356
  /**
1216
- * Run `callback` with the service as soon as it is available — immediately
1217
- * when already provided, otherwise on `provide`. Survives provider/consumer
1218
- * setup-order differences. The callback also re-fires if the service is
1219
- * revoked and provided again. Returns an unsubscribe function.
1357
+ * Adapters call this when a session disconnects so the host can drop
1358
+ * subscribers and abort orphaned streams. Most users do not need this;
1359
+ * it's wired by the RPC server binding automatically.
1360
+ *
1361
+ * @internal
1220
1362
  */
1221
- whenAvailable: <ID extends DevframeServiceId>(id: ID, callback: (service: DevframeServiceOf<ID>) => void) => () => void;
1222
- /** Ids of every currently-provided service. */
1223
- keys: () => string[];
1363
+ _onSessionDisconnected: (meta: DevframeNodeRpcSessionMeta) => void;
1224
1364
  }
1225
1365
  //#endregion
1226
1366
  //#region src/types/context.d.ts
@@ -1841,9 +1981,22 @@ interface DevframeDefinition {
1841
1981
  dev?: boolean;
1842
1982
  build?: boolean;
1843
1983
  };
1984
+ /**
1985
+ * Wire services this devframe consumes (see `DevframeServiceDefinition`).
1986
+ * Each entry is either a declarative descriptor
1987
+ * (`{ package, version?, required?, options? }` — the host imports the
1988
+ * package's default-export factory, resolving it against **this plugin's
1989
+ * own dependencies**) or a ready `DevframeServiceDefinition` (the factory
1990
+ * was already called). The adapter queues these before `setup(ctx)` runs
1991
+ * and constructs each service once at the `ctx.services.ready()` barrier,
1992
+ * merging option sets across every declarer. Missing services are skipped
1993
+ * unless marked `required` — clients feature-detect via
1994
+ * `client.services.has(pkg)` and degrade.
1995
+ */
1996
+ services?: DevframeServiceInput[];
1844
1997
  /** Server-side setup — the primary entrypoint. Runs in every runtime. */
1845
1998
  setup: (ctx: DevframeNodeContext, info?: DevframeSetupInfo) => void | Promise<void>;
1846
1999
  cli?: DevframeCliOptions;
1847
2000
  }
1848
2001
  //#endregion
1849
- export { StreamReader as $, ScopedServerFunctions as A, defineCliFlags as At, RpcFunctionsHost as B, DevframeScopedStreamingHost as C, DevframeAgentHost as Ct, ScopedBroadcastOptions as D, EventsMap as Dt, DevframeSettingsStore as E, EventUnsubscribe as Et, DevframeStorageScope as F, RpcStreamingHost as G, RpcSharedStateHost as H, DevframeDiagnosticsHost as I, DevframeRpcSharedStates as J, DevframeRpcClientFunctions as K, DevframeDiagnosticsLogger as L, SettingsForNamespace as M, DevframeViewHost as N, ScopedClientFunctions as O, CliFlagsSchema as Ot, DevframeHost as P, StreamErrorPayload as Q, DevframeNodeRpcSession as R, DevframeScopedNodeRpc as S, AgentToolProviderHandle as St, DevframeSettingsRegistry as T, EventEmitter as Tt, RpcStreamingChannel as U, RpcSharedStateGetOptions as V, RpcStreamingChannelOptions as W, CreateStreamReaderOptions as X, BufferedChunk as Y, CreateStreamSinkOptions as Z, DevframeServiceId as _, AgentResourceContent as _t, DevframeDuplicationStrategy as a, ImmutableArray as at, DevframeServicesRegistry as b, AgentToolInput as bt, DevframeWsOptions as c, ImmutableSet as ct, ConnectionMeta as d, SharedStateOptions as dt, StreamSink as et, ConnectionMetaSse as f, SharedStatePatch as ft, DevframeNodeContext as g, AgentResource as gt, DevframeConnectionConfigsRegistry as h, AgentManifest as ht, DevframeDockDefaults as i, Immutable as it, ScopedSharedStates as j, parseCliFlags as jt, ScopedRpcFn as k, InferCliFlags as kt, McpRouteOptions as l, SharedState as lt, DevframeCapabilities as m, AgentHandle as mt, DevframeDefinition as n, createStreamReader as nt, DevframeSetupInfo as o, ImmutableMap as ot, ConnectionMetaWebsocket as p, createSharedState as pt, DevframeRpcServerFunctions as q, DevframeDeploymentKind as r, createStreamSink as rt, DevframeSseOptions as s, ImmutableObject as st, DevframeCliOptions as t, StreamSinkEvents as tt, DevframeAuthHandler as u, SharedStateEvents as ut, DevframeServiceOf as v, AgentResourceInput as vt, DevframeSettings as w, DevframeAgentHostEvents as wt, DevframeScopedNodeContext as x, AgentToolProvider as xt, DevframeServicesHost as y, AgentTool as yt, RpcBroadcastOptions as z };
2002
+ export { DevframeStorageScope as $, DevframeServiceId as A, DevframeAgentHost as At, DevframeScopedNodeRpc as B, RpcStreamingChannelOptions as C, AgentResource as Ct, DevframeRpcSharedStates as D, AgentToolInput as Dt, DevframeRpcServerFunctions as E, AgentTool as Et, DevframeServicesHost as F, CliFlagsSchema as Ft, ScopedBroadcastOptions as G, DevframeSettings as H, DevframeServicesRegistry as I, InferCliFlags as It, ScopedServerFunctions as J, ScopedClientFunctions as K, DevframeServicesScopeRegistry as L, defineCliFlags as Lt, DevframeServiceMeta as M, EventEmitter as Mt, DevframeServiceOf as N, EventUnsubscribe as Nt, DevframeServiceDefinition as O, AgentToolProvider as Ot, DevframeServiceScopeOf as P, EventsMap as Pt, DevframeHost as Q, DevframeServicesState as R, parseCliFlags as Rt, RpcStreamingChannel as S, AgentManifest as St, DevframeRpcClientFunctions as T, AgentResourceInput as Tt, DevframeSettingsRegistry as U, DevframeScopedStreamingHost as V, DevframeSettingsStore as W, SettingsForNamespace as X, ScopedSharedStates as Y, DevframeViewHost as Z, DevframeNodeRpcSession as _, SharedStateEvents as _t, DevframeDuplicationStrategy as a, StreamErrorPayload as at, RpcSharedStateGetOptions as b, createSharedState as bt, DevframeWsOptions as c, StreamSinkEvents as ct, ConnectionMeta as d, Immutable as dt, DevframeDiagnosticsHost as et, ConnectionMetaSse as f, ImmutableArray as ft, DevframeNodeContext as g, SharedState as gt, DevframeConnectionConfigsRegistry as h, ImmutableSet as ht, DevframeDockDefaults as i, CreateStreamSinkOptions as it, DevframeServiceInput as j, DevframeAgentHostEvents as jt, DevframeServiceDescriptor as k, AgentToolProviderHandle as kt, McpRouteOptions as l, createStreamReader as lt, DevframeCapabilities as m, ImmutableObject as mt, DevframeDefinition as n, BufferedChunk as nt, DevframeSetupInfo as o, StreamReader as ot, ConnectionMetaWebsocket as p, ImmutableMap as pt, ScopedRpcFn as q, DevframeDeploymentKind as r, CreateStreamReaderOptions as rt, DevframeSseOptions as s, StreamSink as st, DevframeCliOptions as t, DevframeDiagnosticsLogger as tt, DevframeAuthHandler as u, createStreamSink as ut, RpcBroadcastOptions as v, SharedStateOptions as vt, RpcStreamingHost as w, AgentResourceContent as wt, RpcSharedStateHost as x, AgentHandle as xt, RpcFunctionsHost as y, SharedStatePatch as yt, DevframeScopedNodeContext as z };