devframe 0.9.0 → 0.9.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 (79) hide show
  1. package/dist/{_shared-BPmX9fqK.d.mts → _shared-BAGBgTUO.d.mts} +1 -1
  2. package/dist/{_shared-CbZ_yhbz.mjs → _shared-BM3PdYli.mjs} +1 -1
  3. package/dist/adapters/build.d.mts +1 -1
  4. package/dist/adapters/build.mjs +12 -9
  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 +2 -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 +133 -23
  18. package/dist/constants.d.mts +74 -2
  19. package/dist/constants.mjs +9 -2
  20. package/dist/{context-xdynGSLP.mjs → context-af2w_F0_.mjs} +2 -2
  21. package/dist/{context-B3YUu_95.d.mts → context-al99DDJ8.d.mts} +1 -1
  22. package/dist/{context-CEe4PBz1.mjs → context-riPPHGiu.mjs} +339 -26
  23. package/dist/{dev--rAPzbvO.mjs → dev-BKm_HisC.mjs} +15 -11
  24. package/dist/{devframe-DJMg0WLe.d.mts → devframe-mbfgpQQC.d.mts} +774 -587
  25. package/dist/{diagnostics-DOQPnQmX.mjs → diagnostics-DI1HGj2I.mjs} +20 -0
  26. package/dist/{dist-eNI9qM_I.mjs → dist-CZXfGEkd.mjs} +1 -1
  27. package/dist/events-DKfSmoTj.mjs +65 -0
  28. package/dist/hash-DdPnc4k3.mjs +360 -0
  29. package/dist/{host-agent-xLCSe65D.mjs → host-agent-DVDLqjvj.mjs} +14 -13
  30. package/dist/{http-DYpK0cUi.mjs → http-POepPnaQ.mjs} +10 -6
  31. package/dist/{index-CB-fSUyn.d.mts → index-Dipgo9ji.d.mts} +2 -2
  32. package/dist/index.d.mts +2 -2
  33. package/dist/{instance-registry-WvQkt42E.mjs → instance-registry-Dy28drtm.mjs} +4 -4
  34. package/dist/{instance-shell-CMFyszWJ.mjs → instance-shell-GqTY93gC.mjs} +5 -5
  35. package/dist/{instance-shell-CljiKq1c.d.mts → instance-shell-mU5j5xWn.d.mts} +2 -2
  36. package/dist/internal/index.d.mts +40 -4
  37. package/dist/internal/index.mjs +7 -7
  38. package/dist/node/auth.d.mts +2 -2
  39. package/dist/node/auth.mjs +2 -2
  40. package/dist/node/hub-internals.d.mts +2 -2
  41. package/dist/node/hub-internals.mjs +2 -2
  42. package/dist/node/index.d.mts +10 -1
  43. package/dist/node/index.mjs +2 -2
  44. package/dist/recipes/common-rpc-functions.d.mts +11 -1
  45. package/dist/recipes/common-rpc-functions.mjs +10 -0
  46. package/dist/recipes/interactive-auth.d.mts +2 -2
  47. package/dist/recipes/interactive-auth.mjs +2 -2
  48. package/dist/{remote-assets-nTTxY2MU.mjs → remote-assets-CqxiyltC.mjs} +18 -7
  49. package/dist/{revoke-DZbJ7Cl0.mjs → revoke-3Q6aDKWk.mjs} +2 -1
  50. package/dist/{rolldown-runtime-JspESFgx.mjs → rolldown-runtime-B4iAMlE-.mjs} +1 -1
  51. package/dist/rpc/dump.mjs +1 -1
  52. package/dist/rpc/index.mjs +1 -1
  53. package/dist/rpc/transports/ws-bun.d.mts +1 -1
  54. package/dist/rpc/transports/ws-deno.d.mts +26 -0
  55. package/dist/rpc/transports/ws-deno.mjs +30 -0
  56. package/dist/{rpc-core-DMTe-hLo.mjs → rpc-core-DRYnbxdD.mjs} +2 -2
  57. package/dist/{rpc-core-Cc4lucI0.d.mts → rpc-core-dV69u4lY.d.mts} +2 -2
  58. package/dist/{shared-state-fRRbRUtD.mjs → shared-state-DoXutg_U.mjs} +1 -1
  59. package/dist/{state-Cc1IeSmQ.mjs → state-CK9LjrnT.mjs} +1 -1
  60. package/dist/{static-BPZ1_OPA.mjs → static-DV_qUSRS.mjs} +1 -1
  61. package/dist/{storage-CYye7P7L.mjs → storage-BNqSAOxA.mjs} +3 -3
  62. package/dist/types/index.d.mts +2 -2
  63. package/dist/utils/events.d.mts +1 -1
  64. package/dist/utils/get-port.mjs +1 -1
  65. package/dist/utils/hash.mjs +1 -1
  66. package/dist/utils/launch-editor.mjs +12 -11
  67. package/dist/utils/open.mjs +1 -1
  68. package/dist/utils/remote-assets.d.mts +7 -1
  69. package/dist/utils/remote-assets.mjs +1 -1
  70. package/dist/utils/serve-static.mjs +6 -5
  71. package/dist/utils/shared-state.d.mts +1 -1
  72. package/dist/utils/shared-state.mjs +1 -1
  73. package/dist/utils/streaming-channel.d.mts +1 -1
  74. package/dist/validation-YrZH6atx.mjs +311 -0
  75. package/package.json +4 -3
  76. package/skills/devframe/SKILL.md +1 -1
  77. package/dist/hash-KtDZYXDN.mjs +0 -266
  78. package/dist/validation-CpXFB6Dz.mjs +0 -266
  79. package/dist/{host-h3-Crkzm42q.mjs → host-h3-fRbF9yor.mjs} +1 -1
@@ -493,734 +493,885 @@ 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 {
528
+ interface DevframeDiagnosticsHost {
552
529
  /**
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
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.
563
535
  */
564
- 'anonymous:devframe:auth': (params: {
565
- authToken: string;
566
- ua: string;
567
- origin: string;
568
- }) => Promise<{
569
- isTrusted: boolean;
570
- }>;
536
+ readonly logger: DevframeDiagnosticsLogger;
571
537
  /**
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
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.
580
543
  */
581
- 'anonymous:devframe:auth:exchange': (params: {
582
- code: string;
583
- ua: string;
584
- origin: string;
585
- }) => Promise<{
586
- authToken: string | null;
587
- }>;
544
+ register: (definitions: Record<string, unknown>) => void;
588
545
  /**
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
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`.
595
550
  */
596
- 'devframe:auth:revoke': () => Promise<void>;
551
+ defineDiagnostics: typeof defineDiagnostics;
552
+ }
553
+ //#endregion
554
+ //#region src/types/host.d.ts
555
+ interface DevframeHost {
597
556
  /**
598
- * Subscribe a client to a shared-state key. Wired by
599
- * `RpcSharedStateHost`; do not register manually.
600
- *
601
- * @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.
602
564
  */
603
- 'devframe:rpc:server-state:subscribe': (key: string) => Promise<void>;
565
+ mountStatic: (base: string, source: string | RemoteAssetsStore) => void | Promise<void>;
604
566
  /**
605
- * Read the current value for a shared-state key. Wired by
606
- * `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.
607
570
  *
608
- * @internal
609
- */
610
- 'devframe:rpc:server-state:get': (key: string) => Promise<any>;
611
- /**
612
- * Replace a shared-state value (from the client). Wired by
613
- * `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.
614
576
  *
615
- * @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.
616
582
  */
617
- 'devframe:rpc:server-state:set': (key: string, value: any, syncId: string) => Promise<void>;
583
+ mountConnectionMeta?: (base: string) => void | Promise<void>;
618
584
  /**
619
- * Apply a patch to a shared-state value (from the client). Wired by
620
- * `RpcSharedStateHost`; do not register manually.
621
- *
622
- * @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.
623
590
  */
624
- 'devframe:rpc:server-state:patch': (key: string, patches: any[], syncId: string) => Promise<void>;
591
+ resolveOrigin: () => string;
625
592
  /**
626
- * Client→server streaming subscription with optional replay cursor.
627
- * Wired by `RpcStreamingHost`; 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`).
628
597
  *
629
- * @internal
630
- */
631
- 'devframe:streaming:subscribe': (channel: string, id: string, opts?: {
632
- afterSeq?: number;
633
- }) => Promise<void>;
634
- /**
635
- * Client→server streaming unsubscribe. Wired by `RpcStreamingHost`;
636
- * 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/`.
637
609
  *
638
- * @internal
610
+ * Implementations should ensure the directory exists or be safe to
611
+ * pass to a downstream `createStorage(...)` call that creates it
612
+ * lazily.
639
613
  */
640
- 'devframe:streaming:unsubscribe': (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 {
641
625
  /**
642
- * Client→server streaming cancellation request. Wired by
643
- * `RpcStreamingHost`; do not register manually.
644
- *
645
626
  * @internal
646
627
  */
647
- 'devframe:streaming:cancel': (channel: string, id: string) => Promise<void>;
628
+ buildStaticDirs: {
629
+ baseUrl: string;
630
+ source: StaticAssetsSource;
631
+ }[];
648
632
  /**
649
- * Client→server upload chunk. Wired by `RpcStreamingHost`; do not
650
- * 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
651
636
  *
652
- * @internal
653
- */
654
- 'devframe:streaming:upload-chunk': (channel: string, id: string, seq: number, chunk: any) => Promise<void>;
655
- /**
656
- * Client→server upload terminator. Wired by `RpcStreamingHost`; do not
657
- * register manually.
637
+ * Accepts a local dist directory, or a {@link StaticAssetsSource} remote
638
+ * declaration served through devframe's caching CDN back-proxy.
658
639
  *
659
- * @internal
640
+ * `defaultResolveFrom` overrides, for this call only, the context's own
641
+ * `importMetaUrl` as the default `resolveFrom` for a remote source that
642
+ * doesn't set one. A shared host that mounts assets on behalf of another
643
+ * devframe (a hub installing a plugin) passes that plugin's `importMetaUrl`
644
+ * so the assets resolve against the plugin's dependency graph rather than
645
+ * the host's.
660
646
  */
661
- 'devframe:streaming:upload-end': (channel: string, id: string, error?: {
662
- name: string;
663
- message: string;
664
- }) => Promise<void>;
647
+ hostStatic: (baseUrl: string, source: StaticAssetsSource, defaultResolveFrom?: string | null) => void;
665
648
  }
649
+ //#endregion
650
+ //#region src/types/scope.d.ts
651
+ type AnyRpcFn = (...args: any[]) => any;
666
652
  /**
667
- * To be extended
653
+ * Resolve a scoped bare name `T` back to its fully-qualified entry in
654
+ * `Registry` (`<NS>:<T>`). Indexing the plain registry behind a `keyof`
655
+ * guard keeps TypeScript from rejecting a generic index into the
656
+ * key-remapped `Scoped*Functions<NS>` maps (TS2536) once a plugin augments
657
+ * the registry, while preserving each name's argument/return typing.
668
658
  */
669
- interface DevframeRpcSharedStates {}
670
- //#endregion
671
- //#region src/types/rpc.d.ts
672
- interface DevframeNodeRpcSession {
673
- meta: DevframeNodeRpcSessionMeta;
674
- rpc: BirpcReturn<DevframeRpcClientFunctions, DevframeRpcServerFunctions, false>;
659
+ type ScopedRpcFn<Registry, NS extends string, T extends string> = `${NS}:${T}` extends keyof Registry ? Extract<Registry[`${NS}:${T}`], AnyRpcFn> : AnyRpcFn;
660
+ /**
661
+ * Augmentable registry mapping a scope namespace to the shape of its
662
+ * persisted settings. Tools augment it so `ctx.scope('my-plugin')`
663
+ * gets a fully-typed `settings.global` / `settings.project`:
664
+ *
665
+ * ```ts
666
+ * declare module 'devframe' {
667
+ * interface DevframeSettingsRegistry {
668
+ * 'my-plugin': { theme: 'light' | 'dark', recentFiles: string[] }
669
+ * }
670
+ * }
671
+ * ```
672
+ */
673
+ interface DevframeSettingsRegistry {}
674
+ /**
675
+ * Resolve the settings shape for a namespace from
676
+ * {@link DevframeSettingsRegistry}, falling back to an open record when
677
+ * the namespace hasn't been augmented.
678
+ */
679
+ type SettingsForNamespace<NS extends string> = NS extends keyof DevframeSettingsRegistry ? DevframeSettingsRegistry[NS] extends Record<string, any> ? DevframeSettingsRegistry[NS] : Record<string, any> : Record<string, any>;
680
+ /**
681
+ * A persisted key-value settings store for one scope (`global` or
682
+ * `project`). Backed by a file on the node side and by the shared-state
683
+ * sync protocol on the client, so a `set` on either side propagates to
684
+ * every connected peer and survives restarts.
685
+ *
686
+ * Every method is async because the underlying shared state is resolved
687
+ * lazily on first access.
688
+ */
689
+ interface DevframeSettingsStore<T extends Record<string, any> = Record<string, any>> {
690
+ /** Read a single setting. Resolves to `undefined` when unset. */
691
+ get: <K extends keyof T>(key: K) => Promise<T[K] | undefined>;
692
+ /** Write a single setting. */
693
+ set: <K extends keyof T>(key: K, value: T[K]) => Promise<void>;
694
+ /** Remove a single setting. */
695
+ delete: <K extends keyof T>(key: K) => Promise<void>;
696
+ /** Read the whole settings object (immutable snapshot). */
697
+ all: () => Promise<Readonly<T>>;
698
+ /**
699
+ * Subscribe to settings changes. Resolves to an unsubscribe function.
700
+ * Fires on every local or remote mutation with the full snapshot.
701
+ */
702
+ onChange: (fn: (value: Readonly<T>) => void) => Promise<() => void>;
675
703
  }
676
- interface RpcBroadcastOptions<METHOD, Args extends any[]> {
704
+ /**
705
+ * The two settings scopes available on a scoped context.
706
+ *
707
+ * - `project` — per-workspace state, persisted under the host's
708
+ * `workspace` storage dir. Project-local settings.
709
+ * - `global` — per-user state, persisted under the host's `global`
710
+ * storage dir. Machine-wide preferences.
711
+ */
712
+ interface DevframeSettings<T extends Record<string, any> = Record<string, any>> {
713
+ global: DevframeSettingsStore<T>;
714
+ project: DevframeSettingsStore<T>;
715
+ }
716
+ /**
717
+ * Map the server-side RPC registry down to the bare names owned by a
718
+ * namespace, so a scoped `call('get-cwd')` is typed without the
719
+ * `my-plugin:` prefix.
720
+ */
721
+ type ScopedServerFunctions<NS extends string> = { [K in keyof DevframeRpcServerFunctions as K extends `${NS}:${infer R}` ? R : never]: DevframeRpcServerFunctions[K]; };
722
+ /**
723
+ * Map the client-side RPC registry down to the bare names owned by a
724
+ * namespace, used by scoped `broadcast`.
725
+ */
726
+ type ScopedClientFunctions<NS extends string> = { [K in keyof DevframeRpcClientFunctions as K extends `${NS}:${infer R}` ? R : never]: DevframeRpcClientFunctions[K]; };
727
+ /**
728
+ * Map the shared-state registry down to the bare keys owned by a
729
+ * namespace, so scoped `sharedState('messages')` is typed without the
730
+ * `my-plugin:` prefix.
731
+ */
732
+ type ScopedSharedStates<NS extends string> = { [K in keyof DevframeRpcSharedStates as K extends `${NS}:${infer R}` ? R : never]: DevframeRpcSharedStates[K]; };
733
+ /**
734
+ * Broadcast options for a scoped client method (bare name).
735
+ */
736
+ interface ScopedBroadcastOptions<METHOD, Args extends any[]> {
677
737
  method: METHOD;
678
738
  args: Args;
679
739
  optional?: boolean;
680
740
  event?: boolean;
681
- filter?: (client: BirpcReturn<DevframeRpcClientFunctions, DevframeRpcServerFunctions, false>) => boolean | void;
682
741
  }
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]>>>;
742
+ /**
743
+ * Node-side streaming host scoped to a namespace. Channel names are
744
+ * auto-prefixed with `<namespace>:`.
745
+ */
746
+ interface DevframeScopedStreamingHost {
690
747
  /**
691
- * Broadcast a message to all connected clients
748
+ * Register a streaming channel. The bare `name` is auto-prefixed with
749
+ * the scope namespace (`<namespace>:<name>`); pass an already-qualified
750
+ * name (containing `:`) to opt out.
692
751
  */
693
- broadcast: <T extends keyof DevframeRpcClientFunctions, Args extends Parameters<DevframeRpcClientFunctions[T]>>(options: RpcBroadcastOptions<T, Args>) => Promise<void>;
752
+ create: <T = unknown>(name: string, opts?: RpcStreamingChannelOptions) => RpcStreamingChannel<T>;
753
+ }
754
+ /**
755
+ * The scoped node RPC surface exposed on `ctx.scope('my-plugin').rpc`.
756
+ * IDs and keys you pass are auto-namespaced with `my-plugin:`.
757
+ */
758
+ interface DevframeScopedNodeRpc<NS extends string = string> {
759
+ /** The namespace this surface is scoped to. */
760
+ readonly namespace: NS;
694
761
  /**
695
- * Get the current RPC client
696
- *
697
- * Available in RPC functions to get the current RPC client
762
+ * Register a server RPC function. The definition's `name` must be bare
763
+ * (no `:`); it is stored as `<namespace>:<name>`. Throws `DF0034` if an
764
+ * already-namespaced name is passed.
698
765
  */
699
- getCurrentRpcSession: () => DevframeNodeRpcSession | undefined;
766
+ register: (fn: RpcFunctionDefinition<string, any, any, any, any, any, DevframeNodeContext>, force?: boolean) => void;
700
767
  /**
701
- * The shared state host
768
+ * Update a previously registered server RPC function. Same naming rules
769
+ * as {@link DevframeScopedNodeRpc.register}.
702
770
  */
703
- sharedState: RpcSharedStateHost;
771
+ update: (fn: RpcFunctionDefinition<string, any, any, any, any, any, DevframeNodeContext>, force?: boolean) => void;
704
772
  /**
705
- * The streaming channel host. Provides per-channel `start()` /
706
- * `pipeFrom()` producers; clients consume via `rpc.streaming.subscribe()`.
707
- *
708
- * @see RpcStreamingHost
773
+ * Invoke a locally registered server RPC function. Bare names are
774
+ * resolved within this namespace; pass a fully-qualified name
775
+ * (containing `:`) to call another scope's function.
709
776
  */
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[];
777
+ call: {
778
+ <T extends keyof ScopedServerFunctions<NS> & string>(method: T, ...args: Parameters<ScopedRpcFn<DevframeRpcServerFunctions, NS, T>>): Promise<Awaited<ReturnType<ScopedRpcFn<DevframeRpcServerFunctions, NS, T>>>>;
779
+ <T extends keyof DevframeRpcServerFunctions & string>(method: T, ...args: Parameters<Extract<DevframeRpcServerFunctions[T], AnyRpcFn>>): Promise<Awaited<ReturnType<Extract<DevframeRpcServerFunctions[T], AnyRpcFn>>>>;
780
+ (method: string, ...args: any[]): Promise<any>;
781
+ };
719
782
  /**
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.
783
+ * Broadcast a client RPC event to every connected client. Bare method
784
+ * names are resolved within this namespace.
724
785
  */
725
- onKeyAdded: (fn: (key: string) => void) => () => void;
786
+ broadcast: {
787
+ <T extends keyof ScopedClientFunctions<NS> & string>(options: ScopedBroadcastOptions<T, Parameters<ScopedRpcFn<DevframeRpcClientFunctions, NS, T>>>): Promise<void>;
788
+ (options: ScopedBroadcastOptions<string, any[]>): Promise<void>;
789
+ };
726
790
  /**
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.
791
+ * Resolve a namespaced shared state. Bare keys are resolved within this
792
+ * namespace; pass a fully-qualified key (containing `:`) to opt out.
731
793
  */
732
- delete: (key: string) => boolean;
794
+ sharedState: {
795
+ <T extends keyof ScopedSharedStates<NS> & string>(key: T, options?: RpcSharedStateGetOptions<ScopedSharedStates<NS>[T]>): Promise<SharedState<ScopedSharedStates<NS>[T]>>;
796
+ <T extends Record<string, any> = Record<string, any>>(key: string, options?: RpcSharedStateGetOptions<T>): Promise<SharedState<T>>;
797
+ };
798
+ /** Streaming host scoped to this namespace. */
799
+ streaming: DevframeScopedStreamingHost;
800
+ /** Pass-through to the current RPC session, when called inside a handler. */
801
+ getCurrentRpcSession: () => DevframeNodeRpcSession | undefined;
733
802
  }
734
803
  /**
735
- * Options for `RpcStreamingHost.create()`.
804
+ * A namespace-scoped view of {@link DevframeNodeContext}. Returned by
805
+ * `ctx.scope('my-plugin')`. Re-exposes the unscoped surfaces (`views`,
806
+ * `diagnostics`, `agent`, …), replaces `rpc` with the auto-namespaced
807
+ * {@link DevframeScopedNodeRpc}, and adds a top-level `settings` store.
736
808
  */
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;
809
+ interface DevframeScopedNodeContext<NS extends string = string, Settings extends Record<string, any> = Record<string, any>> {
810
+ /** The namespace this context is scoped to. */
811
+ readonly namespace: NS;
812
+ /** The underlying unscoped context. */
813
+ readonly base: DevframeNodeContext;
814
+ readonly workspaceRoot: string;
815
+ readonly cwd: string;
816
+ readonly mode: 'dev' | 'build';
817
+ host: DevframeHost;
818
+ rpc: DevframeScopedNodeRpc<NS>;
819
+ /** Persisted settings for this namespace (`global` + `project`). */
820
+ settings: DevframeSettings<Settings>;
821
+ views: DevframeViewHost;
822
+ diagnostics: DevframeDiagnosticsHost;
823
+ agent: DevframeAgentHost;
747
824
  /**
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.
825
+ * Return a new scoped context, replacing the current scope. Pass `null`
826
+ * or `''` to un-scope and get the base context.
756
827
  */
757
- closedStreamRetention?: number;
828
+ scope: DevframeNodeContext['scope'];
758
829
  }
830
+ //#endregion
831
+ //#region src/types/services.d.ts
759
832
  /**
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.
833
+ * Augmentation point mapping service ids to their implementation types.
834
+ * Providers extend this via `declare module 'devframe'`.
835
+ */
836
+ interface DevframeServicesRegistry {}
837
+ /**
838
+ * A known (augmented) service id, or any namespaced string for services
839
+ * without a published type augmentation.
840
+ */
841
+ type DevframeServiceId = keyof DevframeServicesRegistry | (string & {});
842
+ /** Resolved service type for an id: augmented type, or `unknown`. */
843
+ type DevframeServiceOf<ID> = ID extends keyof DevframeServicesRegistry ? DevframeServicesRegistry[ID] : unknown;
844
+ /**
845
+ * Augmentation point mapping a service's npm package name to the RPC scope
846
+ * namespace it registers its functions under, so a client's
847
+ * `services.get('@devframes/service-x')` returns a scoped RPC surface typed
848
+ * against that namespace. Service packages contribute their entry via
849
+ * declaration merging:
763
850
  *
764
- * @see {@link https://devfra.me/guide/streaming Streaming guide}
851
+ * ```ts
852
+ * declare module 'devframe' {
853
+ * interface DevframeServicesScopeRegistry {
854
+ * '@devframes/service-open': 'devframes:service:open'
855
+ * }
856
+ * }
857
+ * ```
765
858
  */
766
- interface RpcStreamingChannel<T = unknown> {
767
- /** Channel name as registered with `ctx.rpc.streaming.create()`. */
768
- readonly name: string;
859
+ interface DevframeServicesScopeRegistry {}
860
+ /** Resolved RPC scope namespace for a service package name, or `string`. */
861
+ type DevframeServiceScopeOf<PKG> = PKG extends keyof DevframeServicesScopeRegistry ? DevframeServicesScopeRegistry[PKG] & string : string;
862
+ /**
863
+ * A **wire service** — a shared server-side capability (e.g. open-in-editor,
864
+ * syntax highlighting) packaged so any devframe host can install it once and
865
+ * every plugin/client can consume it without re-implementing or re-bundling
866
+ * it. Contrast with plain {@link DevframeServicesHost.provide}, which shares
867
+ * an in-process object between plugins on the node side only: a
868
+ * `DevframeServiceDefinition` additionally registers RPC functions under its
869
+ * {@link DevframeServiceDefinition.scope} and is **advertised to clients**
870
+ * through the reactive `devframe:services` shared state, so browser UIs can
871
+ * feature-detect it (`ctx.services.has(pkg)`) and degrade gracefully.
872
+ *
873
+ * Ship one per npm package (`@devframes/service-<slug>` for first-party),
874
+ * with the package's default export being the `create<X>Service` factory —
875
+ * never a pre-built instance.
876
+ */
877
+ interface DevframeServiceDefinition<API = unknown, Options = any> {
769
878
  /**
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.
879
+ * The npm package name this service ships in — also its registry key
880
+ * (`ctx.services.has('@devframes/service-x')` on both node and client).
774
881
  */
775
- start: (opts?: {
776
- id?: string;
777
- }) => StreamSink<T>;
882
+ package: string;
883
+ /** Semver of the service — advertised to clients, checked against descriptor ranges. */
884
+ version: string;
778
885
  /**
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.
886
+ * RPC namespace the service's functions register under, following the
887
+ * plugin id grammar (e.g. `devframes:service:open`). `setup` receives a
888
+ * context pre-scoped to it, so functions register with bare names.
784
889
  */
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[];
890
+ scope: string;
792
891
  /**
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.
892
+ * Extra advertised metadata (feature flags, defaults, …). Must be
893
+ * JSON-serializable — it is mirrored to every client.
811
894
  */
812
- openInbound: (opts?: {
813
- id?: string;
814
- }) => StreamReader<T>;
895
+ meta?: Record<string, unknown>;
896
+ /**
897
+ * This instance's own option set (usually baked in by the factory that
898
+ * created the definition). Joins the merge alongside every declarative
899
+ * descriptor's `options`.
900
+ */
901
+ options?: Options;
902
+ /**
903
+ * Merge the option sets contributed by multiple installers (in declaration
904
+ * order) into the one bag passed to `setup`. Defaults to a shallow merge
905
+ * where later sets win.
906
+ */
907
+ mergeOptions?: (sets: Options[]) => Options;
908
+ /**
909
+ * Construct the service: register its RPC functions on the pre-scoped
910
+ * context and return its **node API** — the in-process surface other
911
+ * plugins get from `ctx.services.get(package)` (no RPC hop server-side).
912
+ * `info.options` carries every installer's option sets merged at the
913
+ * `ready()` barrier (via {@link mergeOptions} when present, otherwise a
914
+ * shallow merge in declaration order — later sets win).
915
+ */
916
+ setup: (ctx: DevframeScopedNodeContext, info: {
917
+ options?: Options;
918
+ }) => API | Promise<API>;
815
919
  }
816
920
  /**
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.
921
+ * Declarative reference to a service package — the form a
922
+ * {@link DevframeServiceInput} takes when the installer doesn't hold the
923
+ * factory itself (e.g. `DevframeDefinition.services`). The host imports the
924
+ * package's default-export factory and installs the resulting definition at
925
+ * the `ready()` barrier.
820
926
  */
821
- interface RpcStreamingHost {
927
+ interface DevframeServiceDescriptor<Options = any> {
928
+ /** npm package name of the service (its default export is the factory). */
929
+ package: string;
822
930
  /**
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.
931
+ * Accepted semver range for the installed service. An unsatisfied range
932
+ * warns (`DF0069`) — or throws (`DF0068`) when {@link required} — while the
933
+ * service still installs; the advertised meta carries the real version.
826
934
  */
827
- create: <T = unknown>(name: string, opts?: RpcStreamingChannelOptions) => RpcStreamingChannel<T>;
935
+ version?: string;
828
936
  /**
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.
937
+ * Fail hard when the service can't be imported or its version range isn't
938
+ * satisfied. By default a missing service is skipped silently — clients see
939
+ * `has() === false` and degrade.
832
940
  *
833
- * @internal
941
+ * @default false
834
942
  */
835
- _onSessionDisconnected: (meta: DevframeNodeRpcSessionMeta) => void;
943
+ required?: boolean;
944
+ /** Option set this installer contributes to the merge. */
945
+ options?: Options;
836
946
  }
837
- //#endregion
838
- //#region src/types/diagnostics.d.ts
839
947
  /**
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.
948
+ * What can be passed to `ctx.services.install()` (and listed in
949
+ * `DevframeDefinition.services`): a declarative {@link DevframeServiceDescriptor}
950
+ * (the host imports the factory) or a ready {@link DevframeServiceDefinition}
951
+ * (the installer already called the factory — its `options` join the merge).
845
952
  */
846
- type DevframeDiagnosticsLogger = Record<string, any>;
953
+ type DevframeServiceInput<API = unknown, Options = any> = DevframeServiceDescriptor<Options> | DevframeServiceDefinition<API, Options>;
847
954
  /**
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
- * ```
955
+ * One service's advertisement entry, mirrored to clients through the
956
+ * `devframe:services` shared state.
869
957
  */
870
- interface DevframeDiagnosticsHost {
958
+ interface DevframeServiceMeta {
959
+ /** npm package name — the registry key. */
960
+ package: string;
961
+ /** Installed version of the service. */
962
+ version: string;
963
+ /** RPC namespace its functions live under. */
964
+ scope: string;
965
+ /** Extra service-declared metadata. */
966
+ meta?: Record<string, unknown>;
967
+ }
968
+ /**
969
+ * Shape of the `devframe:services` shared state: package name → advertisement.
970
+ */
971
+ type DevframeServicesState = Record<string, DevframeServiceMeta>;
972
+ interface DevframeServicesHost {
871
973
  /**
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.
974
+ * Publish a service under a namespaced id. Throws `DF0037` when the id is
975
+ * already provided (revoke first to replace). Returns a revoke function.
877
976
  */
878
- readonly logger: DevframeDiagnosticsLogger;
977
+ provide: <ID extends DevframeServiceId>(id: ID, service: DevframeServiceOf<ID>) => () => void;
978
+ /** The service currently provided under `id`, or `undefined`. */
979
+ get: <ID extends DevframeServiceId>(id: ID) => DevframeServiceOf<ID> | undefined;
980
+ has: (id: DevframeServiceId) => boolean;
879
981
  /**
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.
982
+ * Run `callback` with the service as soon as it is available — immediately
983
+ * when already provided, otherwise on `provide`. Survives provider/consumer
984
+ * setup-order differences. The callback also re-fires if the service is
985
+ * revoked and provided again. Returns an unsubscribe function.
885
986
  */
886
- register: (definitions: Record<string, unknown>) => void;
987
+ whenAvailable: <ID extends DevframeServiceId>(id: ID, callback: (service: DevframeServiceOf<ID>) => void) => () => void;
988
+ /** Ids of every currently-provided service. */
989
+ keys: () => string[];
887
990
  /**
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;
991
+ * Install a **wire service** (see {@link DevframeServiceDefinition}). The
992
+ * common path is declarative — list services on `DevframeDefinition.services`
993
+ * (or `initHub({ services })`) and the adapter installs them for you before
994
+ * `setup` runs. Call `install()` directly only for the dynamic escape hatch:
995
+ * a service configured at runtime from data unknown until then.
996
+ *
997
+ * Before the pre-setup ready fires, installs are queued and their option
998
+ * sets deep-merged, constructing each service **once**. After it, an install
999
+ * constructs immediately; installing an already-installed package returns
1000
+ * the existing API (a warning — `DF0066` — when the late install carried
1001
+ * options, since they're ignored). The returned promise resolves with the
1002
+ * node API (or `undefined` when an optional descriptor's package can't be
1003
+ * imported).
1004
+ *
1005
+ * `resolveFrom` is where a descriptor's package resolves **from**: a path
1006
+ * or file URL (e.g. the declaring devframe's `importMetaUrl`), or an npm
1007
+ * package name, so its declared services resolve against the declarer's own
1008
+ * dependencies. Falls back to the context's `workspaceRoot` then `cwd`.
1009
+ */
1010
+ install: <API = unknown, Options = any>(input: DevframeServiceInput<API, Options>, options?: {
1011
+ resolveFrom?: string | null;
1012
+ }) => Promise<API | undefined>;
1013
+ /**
1014
+ * Construct every queued service — importing descriptor packages, merging
1015
+ * option sets, `provide()`-ing each node API under its package name, and
1016
+ * advertising it to clients via the `devframe:services` shared state.
1017
+ * Idempotent. **Internal**: the adapters call it once, before running any
1018
+ * `setup`, so services are ready for `setup` to consume; application code
1019
+ * uses declarative `services` (or `install()` for the dynamic case) and
1020
+ * never calls this. Rejects when a `required` service fails to import or
1021
+ * misses its version range.
1022
+ */
1023
+ ready: () => Promise<void>;
894
1024
  }
895
1025
  //#endregion
896
- //#region src/types/host.d.ts
897
- interface DevframeHost {
1026
+ //#region src/types/rpc-augments.d.ts
1027
+ /**
1028
+ * To be extended
1029
+ */
1030
+ interface DevframeRpcClientFunctions {
898
1031
  /**
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.
1032
+ * Server→client notification that this connection's auth token has been
1033
+ * revoked. The client drops to untrusted on receipt. Broadcast by
1034
+ * `revokeActiveConnectionsForToken`.
1035
+ *
1036
+ * @internal
906
1037
  */
907
- mountStatic: (base: string, source: string | RemoteAssetsStore) => void | Promise<void>;
1038
+ 'devframe:auth:revoked': () => Promise<void>;
908
1039
  /**
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.
1040
+ * Streaming chunk pushed from server to subscribed clients. Wired by
1041
+ * `RpcStreamingHost`; do not register manually.
912
1042
  *
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.
1043
+ * @internal
1044
+ */
1045
+ 'devframe:streaming:chunk': (channel: string, id: string, seq: number, chunk: any) => Promise<void>;
1046
+ /**
1047
+ * Streaming terminator pushed from server to subscribed clients. Wired by
1048
+ * `RpcStreamingHost`; do not register manually.
918
1049
  *
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.
1050
+ * @internal
924
1051
  */
925
- mountConnectionMeta?: (base: string) => void | Promise<void>;
1052
+ 'devframe:streaming:end': (channel: string, id: string, error?: {
1053
+ name: string;
1054
+ message: string;
1055
+ }) => Promise<void>;
926
1056
  /**
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.
1057
+ * Server→client cancel for an in-flight upload. Wired by
1058
+ * `RpcStreamingHost`; do not register manually.
1059
+ *
1060
+ * @internal
932
1061
  */
933
- resolveOrigin: () => string;
1062
+ 'devframe:streaming:upload-cancel': (channel: string, id: string) => Promise<void>;
934
1063
  /**
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`).
1064
+ * Full shared-state snapshot pushed from server to subscribed clients.
1065
+ * Wired by `RpcSharedStateHost`; do not register manually.
939
1066
  *
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/`.
1067
+ * @internal
1068
+ */
1069
+ 'devframe:rpc:client-state:updated': (key: string, fullState: any, syncId: string) => Promise<void>;
1070
+ /**
1071
+ * Incremental shared-state patch pushed from server to subscribed clients.
1072
+ * Wired by `RpcSharedStateHost`; do not register manually.
951
1073
  *
952
- * Implementations should ensure the directory exists or be safe to
953
- * pass to a downstream `createStorage(...)` call that creates it
954
- * lazily.
1074
+ * @internal
955
1075
  */
956
- getStorageDir: (scope: DevframeStorageScope) => string;
1076
+ 'devframe:rpc:client-state:patch': (key: string, patches: any[], syncId: string) => Promise<void>;
957
1077
  }
958
1078
  /**
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.
1079
+ * To be extended
962
1080
  */
963
- type DevframeStorageScope = 'workspace' | 'project' | 'global';
964
- //#endregion
965
- //#region src/types/views.d.ts
966
- interface DevframeViewHost {
1081
+ interface DevframeRpcServerFunctions {
967
1082
  /**
1083
+ * Authenticate a connection with a previously-issued bearer token; resolves
1084
+ * whether the connection is now trusted. The interactive handler is provided
1085
+ * by the host adapter (e.g. Vite DevTools); the standalone server registers
1086
+ * an auto-trust noop when `auth: false`.
1087
+ *
1088
+ * Named with the `anonymous:` prefix (see `isAnonymousRpcMethod`) so it is
1089
+ * reachable before the connection is trusted — this is the *only* rule the
1090
+ * pre-trust gate applies, no per-method allowlist.
1091
+ *
968
1092
  * @internal
969
1093
  */
970
- buildStaticDirs: {
971
- baseUrl: string;
972
- source: StaticAssetsSource;
973
- }[];
1094
+ 'anonymous:devframe:auth': (params: {
1095
+ authToken: string;
1096
+ ua: string;
1097
+ origin: string;
1098
+ }) => Promise<{
1099
+ isTrusted: boolean;
1100
+ }>;
974
1101
  /**
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
1102
+ * Exchange a one-time authentication code (shown by the dev server) for a fresh,
1103
+ * node-issued bearer token, returning the token on success or `null`. The
1104
+ * handler is provided by the host adapter on top of `exchangeTempAuthCode`.
978
1105
  *
979
- * Accepts a local dist directory, or a {@link StaticAssetsSource} remote
980
- * declaration served through devframe's caching CDN back-proxy.
1106
+ * Named with the `anonymous:` prefix (see `isAnonymousRpcMethod`) so it is
1107
+ * reachable before the connection is trusted.
1108
+ *
1109
+ * @internal
981
1110
  */
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>>;
1111
+ 'anonymous:devframe:auth:exchange': (params: {
1112
+ code: string;
1113
+ ua: string;
1114
+ origin: string;
1115
+ }) => Promise<{
1116
+ authToken: string | null;
1117
+ }>;
1033
1118
  /**
1034
- * Subscribe to settings changes. Resolves to an unsubscribe function.
1035
- * Fires on every local or remote mutation with the full snapshot.
1119
+ * Self-revoke: the caller asks the server to revoke its own bearer token
1120
+ * (if any) and drop to untrusted. Requires an already-trusted caller, so
1121
+ * unlike the two handshake methods above it does **not** carry the
1122
+ * `anonymous:` prefix. Registered by `recipes/interactive-auth`.
1123
+ *
1124
+ * @internal
1036
1125
  */
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]; };
1126
+ 'devframe:auth:revoke': () => Promise<void>;
1127
+ /**
1128
+ * Subscribe a client to a shared-state key. Wired by
1129
+ * `RpcSharedStateHost`; do not register manually.
1130
+ *
1131
+ * @internal
1132
+ */
1133
+ 'devframe:rpc:server-state:subscribe': (key: string) => Promise<void>;
1134
+ /**
1135
+ * Read the current value for a shared-state key. Wired by
1136
+ * `RpcSharedStateHost`; do not register manually.
1137
+ *
1138
+ * @internal
1139
+ */
1140
+ 'devframe:rpc:server-state:get': (key: string) => Promise<any>;
1141
+ /**
1142
+ * Replace a shared-state value (from the client). Wired by
1143
+ * `RpcSharedStateHost`; do not register manually.
1144
+ *
1145
+ * @internal
1146
+ */
1147
+ 'devframe:rpc:server-state:set': (key: string, value: any, syncId: string) => Promise<void>;
1148
+ /**
1149
+ * Apply a patch to a shared-state value (from the client). Wired by
1150
+ * `RpcSharedStateHost`; do not register manually.
1151
+ *
1152
+ * @internal
1153
+ */
1154
+ 'devframe:rpc:server-state:patch': (key: string, patches: any[], syncId: string) => Promise<void>;
1155
+ /**
1156
+ * Client→server streaming subscription with optional replay cursor.
1157
+ * Wired by `RpcStreamingHost`; do not register manually.
1158
+ *
1159
+ * @internal
1160
+ */
1161
+ 'devframe:streaming:subscribe': (channel: string, id: string, opts?: {
1162
+ afterSeq?: number;
1163
+ }) => Promise<void>;
1164
+ /**
1165
+ * Client→server streaming unsubscribe. Wired by `RpcStreamingHost`;
1166
+ * do not register manually.
1167
+ *
1168
+ * @internal
1169
+ */
1170
+ 'devframe:streaming:unsubscribe': (channel: string, id: string) => Promise<void>;
1171
+ /**
1172
+ * Client→server streaming cancellation request. Wired by
1173
+ * `RpcStreamingHost`; do not register manually.
1174
+ *
1175
+ * @internal
1176
+ */
1177
+ 'devframe:streaming:cancel': (channel: string, id: string) => Promise<void>;
1178
+ /**
1179
+ * Client→server upload chunk. Wired by `RpcStreamingHost`; do not
1180
+ * register manually.
1181
+ *
1182
+ * @internal
1183
+ */
1184
+ 'devframe:streaming:upload-chunk': (channel: string, id: string, seq: number, chunk: any) => Promise<void>;
1185
+ /**
1186
+ * Client→server upload terminator. Wired by `RpcStreamingHost`; do not
1187
+ * register manually.
1188
+ *
1189
+ * @internal
1190
+ */
1191
+ 'devframe:streaming:upload-end': (channel: string, id: string, error?: {
1192
+ name: string;
1193
+ message: string;
1194
+ }) => Promise<void>;
1195
+ }
1068
1196
  /**
1069
- * Broadcast options for a scoped client method (bare name).
1197
+ * To be extended
1070
1198
  */
1071
- interface ScopedBroadcastOptions<METHOD, Args extends any[]> {
1199
+ interface DevframeRpcSharedStates {
1200
+ /**
1201
+ * Wire-service advertisements — package name → `{ package, version,
1202
+ * scope, meta }` for every installed {@link import('./services').DevframeServiceDefinition}.
1203
+ * Written by the node services host at the `ready()` barrier; clients read
1204
+ * it through `client.services` (or subscribe to it directly for
1205
+ * reactivity). Read-only from the browser.
1206
+ */
1207
+ 'devframe:services': DevframeServicesState;
1208
+ }
1209
+ //#endregion
1210
+ //#region src/types/rpc.d.ts
1211
+ interface DevframeNodeRpcSession {
1212
+ meta: DevframeNodeRpcSessionMeta;
1213
+ rpc: BirpcReturn<DevframeRpcClientFunctions, DevframeRpcServerFunctions, false>;
1214
+ }
1215
+ interface RpcBroadcastOptions<METHOD, Args extends any[]> {
1072
1216
  method: METHOD;
1073
1217
  args: Args;
1074
1218
  optional?: boolean;
1075
1219
  event?: boolean;
1220
+ filter?: (client: BirpcReturn<DevframeRpcClientFunctions, DevframeRpcServerFunctions, false>) => boolean | void;
1076
1221
  }
1077
- /**
1078
- * Node-side streaming host scoped to a namespace. Channel names are
1079
- * auto-prefixed with `<namespace>:`.
1080
- */
1081
- interface DevframeScopedStreamingHost {
1222
+ type RpcFunctionsHost = RpcFunctionsCollectorBase<DevframeRpcServerFunctions, DevframeNodeContext> & {
1082
1223
  /**
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.
1224
+ * Invoke a locally registered server RPC function directly.
1225
+ *
1226
+ * This bypasses transport and is useful for server-side cross-function calls.
1086
1227
  */
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;
1228
+ invokeLocal: <T extends keyof DevframeRpcServerFunctions, Args extends Parameters<DevframeRpcServerFunctions[T]>>(method: T, ...args: Args) => Promise<Awaited<ReturnType<DevframeRpcServerFunctions[T]>>>;
1096
1229
  /**
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.
1230
+ * Broadcast a message to all connected clients
1100
1231
  */
1101
- register: (fn: RpcFunctionDefinition<string, any, any, any, any, any, DevframeNodeContext>, force?: boolean) => void;
1232
+ broadcast: <T extends keyof DevframeRpcClientFunctions, Args extends Parameters<DevframeRpcClientFunctions[T]>>(options: RpcBroadcastOptions<T, Args>) => Promise<void>;
1102
1233
  /**
1103
- * Update a previously registered server RPC function. Same naming rules
1104
- * as {@link DevframeScopedNodeRpc.register}.
1234
+ * Get the current RPC client
1235
+ *
1236
+ * Available in RPC functions to get the current RPC client
1105
1237
  */
1106
- update: (fn: RpcFunctionDefinition<string, any, any, any, any, any, DevframeNodeContext>, force?: boolean) => void;
1238
+ getCurrentRpcSession: () => DevframeNodeRpcSession | undefined;
1107
1239
  /**
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.
1240
+ * The shared state host
1111
1241
  */
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
- };
1242
+ sharedState: RpcSharedStateHost;
1117
1243
  /**
1118
- * Broadcast a client RPC event to every connected client. Bare method
1119
- * names are resolved within this namespace.
1244
+ * The streaming channel host. Provides per-channel `start()` /
1245
+ * `pipeFrom()` producers; clients consume via `rpc.streaming.subscribe()`.
1246
+ *
1247
+ * @see RpcStreamingHost
1120
1248
  */
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
- };
1249
+ streaming: RpcStreamingHost;
1250
+ };
1251
+ interface RpcSharedStateGetOptions<T> {
1252
+ sharedState?: SharedState<T>;
1253
+ initialValue?: T;
1254
+ }
1255
+ interface RpcSharedStateHost {
1256
+ get: <T extends object = any>(key: string, options?: RpcSharedStateGetOptions<T>) => Promise<SharedState<T>>;
1257
+ keys: () => string[];
1125
1258
  /**
1126
- * Resolve a namespaced shared state. Bare keys are resolved within this
1127
- * namespace; pass a fully-qualified key (containing `:`) to opt out.
1259
+ * Subscribe to new shared-state keys becoming available. Fires when
1260
+ * `get(key, ...)` creates a fresh entry (not on subsequent gets).
1261
+ * Useful for protocol adapters (e.g. MCP) that surface shared state
1262
+ * as dynamic resources.
1128
1263
  */
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;
1264
+ onKeyAdded: (fn: (key: string) => void) => () => void;
1265
+ /**
1266
+ * Unregister a shared state and drop its broadcast listeners. Returns
1267
+ * `true` when a state was removed, `false` when the key was unknown.
1268
+ * Used by short-lived states (e.g. a disposed JSON-render view) to avoid
1269
+ * leaking listeners and lingering entries for the context lifetime.
1270
+ */
1271
+ delete: (key: string) => boolean;
1137
1272
  }
1138
1273
  /**
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.
1274
+ * Options for `RpcStreamingHost.create()`.
1143
1275
  */
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;
1276
+ interface RpcStreamingChannelOptions {
1159
1277
  /**
1160
- * Return a new scoped context, replacing the current scope. Pass `null`
1161
- * or `''` to un-scope and get the base context.
1278
+ * Size of the per-stream ring buffer kept on the server for
1279
+ * replay-on-resubscribe. `0` (default) disables replay; on reconnect
1280
+ * the consumer only sees chunks that arrive after subscribing.
1281
+ *
1282
+ * The buffer is per stream id, not per channel — each `channel.start()`
1283
+ * gets its own.
1162
1284
  */
1163
- scope: DevframeNodeContext['scope'];
1285
+ replayWindow?: number;
1286
+ /**
1287
+ * Milliseconds a closed stream is retained on the server after its
1288
+ * last subscriber leaves (or if no subscriber ever arrived). During
1289
+ * this window, late subscribers can still join and replay the buffer
1290
+ * + receive the `end` frame.
1291
+ *
1292
+ * Defaults to `30_000` (30 s) when `replayWindow > 0`, else `0`
1293
+ * (immediate free). Set to `0` to opt out, or higher for longer
1294
+ * post-mortem replay.
1295
+ */
1296
+ closedStreamRetention?: number;
1164
1297
  }
1165
- //#endregion
1166
- //#region src/types/services.d.ts
1167
1298
  /**
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
- * ```
1299
+ * Channel handle returned by `ctx.rpc.streaming.create(name, opts)`. A
1300
+ * channel owns a wire namespace; calling `start()` produces individual
1301
+ * streams keyed by id.
1190
1302
  *
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'`.
1303
+ * @see {@link https://devfra.me/guide/streaming Streaming guide}
1197
1304
  */
1198
- interface DevframeServicesRegistry {}
1305
+ interface RpcStreamingChannel<T = unknown> {
1306
+ /** Channel name as registered with `ctx.rpc.streaming.create()`. */
1307
+ readonly name: string;
1308
+ /**
1309
+ * Start a new stream. Returns a server-side sink with both an imperative
1310
+ * (`write` / `close` / `error`) surface and a `WritableStream<T>` for
1311
+ * `pipeTo` consumption. The sink's `signal` aborts when every subscriber
1312
+ * disconnects or cancels.
1313
+ */
1314
+ start: (opts?: {
1315
+ id?: string;
1316
+ }) => StreamSink<T>;
1317
+ /**
1318
+ * Convenience: start a stream and pipe a `ReadableStream<T>` into it.
1319
+ * The pipe uses `sink.signal` so cancellation propagates upstream.
1320
+ *
1321
+ * Node-stream interop: convert a `Readable` with `Readable.toWeb(node)`
1322
+ * before passing it here.
1323
+ */
1324
+ pipeFrom: (readable: ReadableStream<T>, opts?: {
1325
+ id?: string;
1326
+ }) => Promise<StreamSink<T>>;
1327
+ /** Look up an active stream by id. Returns `undefined` if none. */
1328
+ get: (id: string) => StreamSink<T> | undefined;
1329
+ /** All active outbound stream ids on this channel. */
1330
+ ids: () => string[];
1331
+ /**
1332
+ * Open an inbound stream — the server side of a client-to-server
1333
+ * upload. Allocates an id, returns a `StreamReader<T>` that fills as
1334
+ * the client writes chunks. Typical pattern is to call this from an
1335
+ * action handler, kick off background processing, and return the id
1336
+ * so the caller can start uploading:
1337
+ *
1338
+ * ```ts
1339
+ * handler: async () => {
1340
+ * const reader = channel.openInbound()
1341
+ * ;(async () => {
1342
+ * for await (const chunk of reader) processChunk(chunk)
1343
+ * })()
1344
+ * return { uploadId: reader.id }
1345
+ * }
1346
+ * ```
1347
+ *
1348
+ * Calling `reader.cancel()` on the server sends an `upload-cancel` to
1349
+ * the uploading client, which aborts its sink.
1350
+ */
1351
+ openInbound: (opts?: {
1352
+ id?: string;
1353
+ }) => StreamReader<T>;
1354
+ }
1199
1355
  /**
1200
- * A known (augmented) service id, or any namespaced string for services
1201
- * without a published type augmentation.
1356
+ * Server-side streaming host. Lives on `ctx.rpc.streaming` alongside
1357
+ * `ctx.rpc.sharedState`. Each named channel owns its own stream registry
1358
+ * and wire namespace.
1202
1359
  */
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 {
1360
+ interface RpcStreamingHost {
1207
1361
  /**
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.
1362
+ * Register a streaming channel. Names follow the `<plugin-id>:<channel>`
1363
+ * convention (e.g. `'my-devtool:chat-stream'`). Throws `DF0032` if the
1364
+ * name is already taken.
1210
1365
  */
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;
1366
+ create: <T = unknown>(name: string, opts?: RpcStreamingChannelOptions) => RpcStreamingChannel<T>;
1215
1367
  /**
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.
1368
+ * Adapters call this when a session disconnects so the host can drop
1369
+ * subscribers and abort orphaned streams. Most users do not need this;
1370
+ * it's wired by the RPC server binding automatically.
1371
+ *
1372
+ * @internal
1220
1373
  */
1221
- whenAvailable: <ID extends DevframeServiceId>(id: ID, callback: (service: DevframeServiceOf<ID>) => void) => () => void;
1222
- /** Ids of every currently-provided service. */
1223
- keys: () => string[];
1374
+ _onSessionDisconnected: (meta: DevframeNodeRpcSessionMeta) => void;
1224
1375
  }
1225
1376
  //#endregion
1226
1377
  //#region src/types/context.d.ts
@@ -1792,6 +1943,29 @@ interface DevframeDefinition {
1792
1943
  version: string;
1793
1944
  /** npm package name the devframe ships in (e.g. `@scope/my-tool`). */
1794
1945
  packageName: string;
1946
+ /**
1947
+ * `import.meta.url` of the module that defines this devframe. **Always
1948
+ * provide it** (`importMetaUrl: import.meta.url`): it is the resolution base
1949
+ * for the tool's own dependency graph, which lets the host resolve
1950
+ * everything against the plugin's own installed packages rather than the
1951
+ * consuming app's.
1952
+ *
1953
+ * - **Remote assets** — becomes the default `resolveFrom` for any remote
1954
+ * {@link StaticAssetsSource} the devframe hosts (its `cli.distDir`, and
1955
+ * every `ctx.views.hostStatic` call) that doesn't set one explicitly, so a
1956
+ * locally installed copy of the assets package is served with zero
1957
+ * network. A per-asset `resolveFrom` still wins, and an explicit
1958
+ * `resolveFrom: null` still opts out.
1959
+ * - **Service dependencies** — becomes the base the host resolves declared
1960
+ * {@link DevframeServiceInput | services} from, so a plugin can ship a
1961
+ * service package as its own dependency instead of asking users to install
1962
+ * it.
1963
+ *
1964
+ * Optional for backward compatibility; omitting it falls back to
1965
+ * runtime-directory resolution and disables the zero-network installed-copy
1966
+ * fast paths above.
1967
+ */
1968
+ importMetaUrl?: string;
1795
1969
  /** Project homepage or documentation URL. */
1796
1970
  homepage: string;
1797
1971
  /** One-line summary of what the tool does. */
@@ -1841,9 +2015,22 @@ interface DevframeDefinition {
1841
2015
  dev?: boolean;
1842
2016
  build?: boolean;
1843
2017
  };
2018
+ /**
2019
+ * Wire services this devframe consumes (see `DevframeServiceDefinition`).
2020
+ * Each entry is either a declarative descriptor
2021
+ * (`{ package, version?, required?, options? }` — the host imports the
2022
+ * package's default-export factory, resolving it against **this plugin's
2023
+ * own dependencies**) or a ready `DevframeServiceDefinition` (the factory
2024
+ * was already called). The adapter queues these before `setup(ctx)` runs
2025
+ * and constructs each service once at the `ctx.services.ready()` barrier,
2026
+ * merging option sets across every declarer. Missing services are skipped
2027
+ * unless marked `required` — clients feature-detect via
2028
+ * `client.services.has(pkg)` and degrade.
2029
+ */
2030
+ services?: DevframeServiceInput[];
1844
2031
  /** Server-side setup — the primary entrypoint. Runs in every runtime. */
1845
2032
  setup: (ctx: DevframeNodeContext, info?: DevframeSetupInfo) => void | Promise<void>;
1846
2033
  cli?: DevframeCliOptions;
1847
2034
  }
1848
2035
  //#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 };
2036
+ 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 };