devframe 0.9.2 → 0.9.4

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 (63) hide show
  1. package/dist/{_shared-BAGBgTUO.d.mts → _shared-BPovvJW8.d.mts} +1 -1
  2. package/dist/adapters/build.d.mts +11 -2
  3. package/dist/adapters/build.mjs +40 -4
  4. package/dist/adapters/cac.d.mts +1 -1
  5. package/dist/adapters/cac.mjs +1 -1
  6. package/dist/adapters/dev.d.mts +4 -4
  7. package/dist/adapters/dev.mjs +1 -1
  8. package/dist/adapters/embedded.d.mts +1 -1
  9. package/dist/adapters/initiate.d.mts +3 -3
  10. package/dist/adapters/initiate.mjs +1 -1
  11. package/dist/adapters/mcp.d.mts +1 -1
  12. package/dist/adapters/mcp.mjs +1 -1
  13. package/dist/cli/main.mjs +2 -2
  14. package/dist/client/index.d.mts +5 -5
  15. package/dist/{context-af2w_F0_.mjs → context-BUJGvlje.mjs} +1 -1
  16. package/dist/{context-al99DDJ8.d.mts → context-CO7eZHJy.d.mts} +1 -1
  17. package/dist/{context-riPPHGiu.mjs → context-CQW0lSw8.mjs} +10 -5
  18. package/dist/{devframe-mbfgpQQC.d.mts → context-_i51nYOs.d.mts} +1199 -1170
  19. package/dist/{dev-BKm_HisC.mjs → dev-lpIvwagf.mjs} +5 -5
  20. package/dist/{diagnostics-DI1HGj2I.mjs → diagnostics-BXWW3VK-.mjs} +4 -0
  21. package/dist/{host-agent-DVDLqjvj.mjs → host-agent-B84av916.mjs} +1 -1
  22. package/dist/{http-POepPnaQ.mjs → http-BKiqHcXX.mjs} +2 -2
  23. package/dist/{index-Dipgo9ji.d.mts → index-CEvd-YXD.d.mts} +2 -2
  24. package/dist/{index-tdrk1bzV.d.mts → index-DOg4S5wk.d.mts} +1 -1
  25. package/dist/index.d.mts +6 -6
  26. package/dist/{instance-registry-Dy28drtm.mjs → instance-registry-BjlMQbFB.mjs} +1 -1
  27. package/dist/{instance-shell-GqTY93gC.mjs → instance-shell-B-uevnK-.mjs} +4 -4
  28. package/dist/{instance-shell-mU5j5xWn.d.mts → instance-shell-BHvBIGP4.d.mts} +3 -3
  29. package/dist/internal/index.d.mts +11 -5
  30. package/dist/internal/index.mjs +5 -5
  31. package/dist/node/auth.d.mts +2 -2
  32. package/dist/node/hub-internals.d.mts +2 -2
  33. package/dist/node/hub-internals.mjs +1 -1
  34. package/dist/node/index.d.mts +3 -3
  35. package/dist/node/index.mjs +2 -2
  36. package/dist/recipes/common-rpc-functions.d.mts +3 -3
  37. package/dist/recipes/interactive-auth.d.mts +2 -2
  38. package/dist/recipes/interactive-auth.mjs +1 -1
  39. package/dist/{remote-assets-CqxiyltC.mjs → remote-assets-DezQmPQU.mjs} +1 -1
  40. package/dist/rpc/dump.d.mts +1 -1
  41. package/dist/rpc/index.d.mts +2 -2
  42. package/dist/rpc/transports/sse-client.d.mts +1 -1
  43. package/dist/rpc/transports/sse-server.d.mts +2 -2
  44. package/dist/rpc/transports/ws-bun.d.mts +2 -2
  45. package/dist/rpc/transports/ws-client.d.mts +1 -1
  46. package/dist/rpc/transports/ws-deno.d.mts +2 -2
  47. package/dist/rpc/transports/ws-server.d.mts +1 -1
  48. package/dist/{rpc-core-dV69u4lY.d.mts → rpc-core-CDZoGYiD.d.mts} +3 -3
  49. package/dist/{rpc-core-DRYnbxdD.mjs → rpc-core-DlHd276T.mjs} +1 -1
  50. package/dist/{sse-client-CFBOIaUi.d.mts → sse-client-CdGD_B8_.d.mts} +1 -1
  51. package/dist/{storage-BNqSAOxA.mjs → storage-BXNVhKyR.mjs} +1 -1
  52. package/dist/types/index.d.mts +5 -5
  53. package/dist/{types-Djq7CBXh.d.mts → types-BBLeGfpt.d.mts} +1 -1
  54. package/dist/utils/events.d.mts +1 -1
  55. package/dist/utils/remote-assets.mjs +1 -1
  56. package/dist/utils/shared-state.d.mts +1 -1
  57. package/dist/utils/streaming-channel.d.mts +1 -1
  58. package/dist/{ws-client-DhjNyQo3.d.mts → ws-client-Bo2t6hES.d.mts} +1 -1
  59. package/dist/{ws-server-D1d3QM9f.d.mts → ws-server-DLtEoLdR.d.mts} +1 -1
  60. package/package.json +1 -1
  61. package/skills/devframe/SKILL.md +313 -292
  62. package/skills/devframe/templates/counter-devframe.ts +8 -3
  63. package/skills/devframe/templates/hub.ts +50 -0
@@ -1,50 +1,11 @@
1
- import { d as RpcFunctionsCollectorBase } from "./index-tdrk1bzV.mjs";
2
- import { _ as RpcFunctionDefinition, v as RpcFunctionDefinitionAny } from "./types-Djq7CBXh.mjs";
3
- import { d as DevframeRpcConnection, u as DevframeNodeRpcSessionMeta } from "./ws-server-D1d3QM9f.mjs";
4
- import { m as defineDiagnostics } from "./nostics-SPGDgHEP.mjs";
1
+ import { d as RpcFunctionsCollectorBase } from "./index-DOg4S5wk.mjs";
2
+ import { _ as RpcFunctionDefinition, v as RpcFunctionDefinitionAny } from "./types-BBLeGfpt.mjs";
3
+ import { d as DevframeRpcConnection, u as DevframeNodeRpcSessionMeta } from "./ws-server-DLtEoLdR.mjs";
5
4
  import { a as RemoteAssetsStore, o as StaticAssetsSource } from "./remote-assets-Bg4gCUZ_.mjs";
6
- import { CAC } from "cac";
7
- import { StandardSchemaV1 } from "@standard-schema/spec";
5
+ import { m as defineDiagnostics } from "./nostics-SPGDgHEP.mjs";
8
6
  import { BirpcReturn } from "birpc";
9
- //#region src/adapters/flags.d.ts
10
- /**
11
- * Schema map for typed CLI flags. Keys are flag names in camelCase —
12
- * this matches CAC's parsed-flag output ( `--no-open` → `noOpen` ). Each
13
- * value is any [Standard Schema](https://standardschema.dev/) validator
14
- * (valibot, zod, arktype, devframe's built-in `s`, …) used to both (a)
15
- * derive the CAC option type when the flag is registered and (b) validate
16
- * the parsed value before it's forwarded to `setup(ctx, { flags })`.
17
- */
18
- type CliFlagsSchema = Record<string, StandardSchemaV1>;
19
- /**
20
- * Identity helper that preserves the literal schema-map type — use this
21
- * so `InferCliFlags<typeof myFlags>` resolves to the right object shape.
22
- *
23
- * ```ts
24
- * const appFlags = defineCliFlags({
25
- * depth: v.pipe(v.number(), v.integer()),
26
- * config: v.optional(v.string()),
27
- * })
28
- *
29
- * defineDevframe({
30
- * cli: { flags: appFlags },
31
- * setup(ctx, info) {
32
- * const flags = info.flags as InferCliFlags<typeof appFlags>
33
- * flags.depth // number
34
- * flags.config // string | undefined
35
- * },
36
- * })
37
- * ```
38
- */
39
- declare function defineCliFlags<T extends CliFlagsSchema>(flags: T): T;
40
- /** Extract the parsed-output type from a {@link CliFlagsSchema}. */
41
- type InferCliFlags<T extends CliFlagsSchema> = { [K in keyof T]: StandardSchemaV1.InferOutput<T[K]>; };
42
- /** Validate the raw cac-parsed bag against a {@link CliFlagsSchema}. */
43
- declare function parseCliFlags(schema: CliFlagsSchema, raw: Record<string, unknown>): {
44
- flags: Record<string, unknown>;
45
- issues?: string[];
46
- };
47
- //#endregion
7
+ import { StandardSchemaV1 } from "@standard-schema/spec";
8
+ import { CAC } from "cac";
48
9
  //#region src/types/events.d.ts
49
10
  interface EventsMap {
50
11
  [event: string]: any;
@@ -313,235 +274,144 @@ interface DevframeAgentHost {
313
274
  getResource: (id: string) => AgentResource | undefined;
314
275
  }
315
276
  //#endregion
316
- //#region src/utils/shared-state.d.ts
317
- type ImmutablePrimitive = undefined | null | boolean | string | number | Function;
318
- type Immutable<T> = T extends ImmutablePrimitive ? T : T extends Array<infer U> ? ImmutableArray<U> : T extends Map<infer K, infer V> ? ImmutableMap<K, V> : T extends Set<infer M> ? ImmutableSet<M> : ImmutableObject<T>;
319
- type ImmutableArray<T> = ReadonlyArray<Immutable<T>>;
320
- type ImmutableMap<K, V> = ReadonlyMap<Immutable<K>, Immutable<V>>;
321
- type ImmutableSet<T> = ReadonlySet<Immutable<T>>;
322
- type ImmutableObject<T> = { readonly [K in keyof T]: Immutable<T[K]>; };
277
+ //#region src/adapters/flags.d.ts
323
278
  /**
324
- * Serializable patch describing a single mutation to a `SharedState`.
325
- * Structurally compatible with JSON-Patch and is safe to send over RPC.
279
+ * Schema map for typed CLI flags. Keys are flag names in camelCase —
280
+ * this matches CAC's parsed-flag output ( `--no-open` → `noOpen` ). Each
281
+ * value is any [Standard Schema](https://standardschema.dev/) validator
282
+ * (valibot, zod, arktype, devframe's built-in `s`, …) used to both (a)
283
+ * derive the CAC option type when the flag is registered and (b) validate
284
+ * the parsed value before it's forwarded to `setup(ctx, { flags })`.
326
285
  */
327
- interface SharedStatePatch {
328
- op: 'add' | 'remove' | 'replace';
329
- path: readonly (string | number)[];
330
- value?: unknown;
331
- }
286
+ type CliFlagsSchema = Record<string, StandardSchemaV1>;
332
287
  /**
333
- * State host that is immutable by default with explicit mutate.
288
+ * Identity helper that preserves the literal schema-map type — use this
289
+ * so `InferCliFlags<typeof myFlags>` resolves to the right object shape.
290
+ *
291
+ * ```ts
292
+ * const appFlags = defineCliFlags({
293
+ * depth: v.pipe(v.number(), v.integer()),
294
+ * config: v.optional(v.string()),
295
+ * })
296
+ *
297
+ * defineDevframe({
298
+ * cli: { flags: appFlags },
299
+ * setup(ctx, info) {
300
+ * const flags = info.flags as InferCliFlags<typeof appFlags>
301
+ * flags.depth // number
302
+ * flags.config // string | undefined
303
+ * },
304
+ * })
305
+ * ```
334
306
  */
335
- interface SharedState<T> {
336
- /**
337
- * Get the current state. Immutable.
338
- */
339
- value: () => Immutable<T>;
340
- /**
341
- * Subscribe to state changes.
342
- */
343
- on: EventEmitter<SharedStateEvents<T>>['on'];
307
+ declare function defineCliFlags<T extends CliFlagsSchema>(flags: T): T;
308
+ /** Extract the parsed-output type from a {@link CliFlagsSchema}. */
309
+ type InferCliFlags<T extends CliFlagsSchema> = { [K in keyof T]: StandardSchemaV1.InferOutput<T[K]>; };
310
+ /** Validate the raw cac-parsed bag against a {@link CliFlagsSchema}. */
311
+ declare function parseCliFlags(schema: CliFlagsSchema, raw: Record<string, unknown>): {
312
+ flags: Record<string, unknown>;
313
+ issues?: string[];
314
+ };
315
+ //#endregion
316
+ //#region src/node/auth/handler.d.ts
317
+ /**
318
+ * A ready-made pre-auth RPC handler, as produced by
319
+ * `devframe/recipes/interactive-auth`'s `createInteractiveAuth`. Bundles
320
+ * everything a host adapter needs to wire an authenticated server:
321
+ * the handshake RPC functions, the resolver gate, the connect-time trust
322
+ * hook, and the startup banner.
323
+ *
324
+ * `initDevframe` / `initHub` accept one of these directly via their `auth` option —
325
+ * see {@link https://devfra.me | devframe}'s server docs — or a host can
326
+ * wire the four pieces itself against a lower-level transport.
327
+ */
328
+ interface DevframeAuthHandler {
344
329
  /**
345
- * Mutate the state.
330
+ * `anonymous:devframe:auth` + `anonymous:devframe:auth:exchange` (the
331
+ * handshake) and `devframe:auth:revoke` (self-revoke) — register these on
332
+ * the RPC host (e.g. `rpcHost.register(fn)` for each).
346
333
  */
347
- mutate: (fn: (state: T) => void, syncId?: string) => void;
334
+ rpcFunctions: RpcFunctionDefinitionAny[];
348
335
  /**
349
- * Apply patches to the state.
336
+ * Resolver gate: whether `methodName` is callable given `session`'s
337
+ * current trust state. Defaults to allowing any `anonymous:`-prefixed
338
+ * method (see `isAnonymousRpcMethod`) plus anything once the session is
339
+ * trusted.
350
340
  */
351
- patch: (patches: SharedStatePatch[], syncId?: string) => void;
341
+ authorize: (methodName: string, session: DevframeNodeRpcSession) => boolean;
352
342
  /**
353
- * Sync IDs that have been applied to the state.
343
+ * Connect-time trust: reads a bearer token off the connection's initial
344
+ * request (a static/pre-shared token from `clientAuthTokens`, or a token
345
+ * minted by the code exchange) and, when valid, marks the session
346
+ * trusted immediately — before the client's own handshake call.
354
347
  */
355
- syncIds: Set<string>;
356
- }
357
- interface SharedStateEvents<T> {
358
- updated: (fullState: T, patches: SharedStatePatch[] | undefined, syncId: string) => void;
359
- }
360
- interface SharedStateOptions<T> {
348
+ onConnect: (connection: DevframeRpcConnection, session: DevframeNodeRpcSession) => void;
361
349
  /**
362
- * Initial state.
350
+ * Print the current one-time code and its magic-link URL. Devframe stays
351
+ * headless — call this yourself once the server is listening. Safe to
352
+ * call repeatedly; it only prints once per code.
363
353
  */
364
- initialValue: T;
354
+ printBanner: () => void;
365
355
  /**
366
- * Enable patches.
367
- *
368
- * @default false
356
+ * Rewrite a URL the CLI is about to open in the browser (`--open`) so the
357
+ * tab lands already authenticated — e.g. appending the current OTP as a
358
+ * query param. Called with the fully-resolved target URL; return it
359
+ * unchanged to opt out. Optional: a handler that doesn't need this (e.g.
360
+ * one gated by a pre-shared bearer token instead of an interactive code)
361
+ * can omit it and the URL opens as-is.
369
362
  */
370
- enablePatches?: boolean;
363
+ buildOpenUrl?: (url: string) => string;
371
364
  }
372
- declare function createSharedState<T extends object>(options: SharedStateOptions<T>): SharedState<T>;
373
365
  //#endregion
374
- //#region src/utils/streaming-channel.d.ts
366
+ //#region src/types/diagnostics.d.ts
375
367
  /**
376
- * Serialized error shape sent over the wire when a stream ends with a failure.
377
- * Stays JSON-safe so the strict-JSON encoder can carry it without coercion.
368
+ * The shared diagnostics lookup exposed by the host. A `Proxy` that resolves
369
+ * any registered code name to its `nostics` handle (a callable that builds
370
+ * a diagnostic and routes it through registered reporters). Typed loosely
371
+ * because it spans heterogeneous definitions registered by different
372
+ * integrations.
378
373
  */
379
- interface StreamErrorPayload {
380
- name: string;
381
- message: string;
382
- }
374
+ type DevframeDiagnosticsLogger = Record<string, any>;
383
375
  /**
384
- * Single buffered chunk in the server-side ring buffer.
376
+ * Host for structured diagnostics — a thin layer over `nostics` that lets
377
+ * integrations register their own coded errors/warnings into a shared
378
+ * registry without taking a direct dependency on `nostics`.
385
379
  *
386
- * Sequence numbers start at 1 and increment per write. Subscribers track
387
- * `lastSeenSeq` and ask for `afterSeq` on resubscribe so the server can
388
- * replay any chunks the client missed during a brief disconnect.
380
+ * Typical usage from a plugin's `setup(ctx)`:
381
+ *
382
+ * ```ts
383
+ * const myDiagnostics = ctx.diagnostics.defineDiagnostics({
384
+ * docsBase: 'https://example.com/errors',
385
+ * codes: {
386
+ * MYP0001: { why: 'Something went wrong' },
387
+ * },
388
+ * })
389
+ * ctx.diagnostics.register(myDiagnostics)
390
+ *
391
+ * // Through the shared lookup (loose typing):
392
+ * throw ctx.diagnostics.logger.MYP0001()
393
+ *
394
+ * // Or directly on the typed handle returned from `defineDiagnostics`:
395
+ * throw myDiagnostics.MYP0001()
396
+ * ```
389
397
  */
390
- interface BufferedChunk<T> {
391
- seq: number;
392
- chunk: T;
393
- }
394
- interface StreamSinkEvents<T> {
395
- /** Fired for each `write()`. The RPC layer subscribes and broadcasts. */
396
- chunk: (seq: number, chunk: T) => void;
397
- /** Terminal — fired exactly once per sink lifetime. */
398
- end: (error?: StreamErrorPayload) => void;
399
- }
400
- interface CreateStreamSinkOptions {
401
- id?: string;
398
+ interface DevframeDiagnosticsHost {
402
399
  /**
403
- * Size of the per-stream ring buffer kept for replay-on-resubscribe.
404
- * `0` (default) disables replay.
400
+ * Proxy-backed lookup of every registered diagnostic handle by code name.
401
+ * Resolves to a `nostics` `DiagnosticHandle` — a callable that builds a
402
+ * diagnostic and routes it through registered reporters; prefix with
403
+ * `throw` to raise. Loosely typed — for autocompletion, keep a reference
404
+ * to the typed result of `defineDiagnostics()` instead.
405
405
  */
406
- replayWindow?: number;
407
- }
408
- /**
409
- * Server-side producer handle. Two equivalent surfaces share one piece of
410
- * state: the imperative `write/error/close` triple, and a `WritableStream<T>`
411
- * for `pipeTo`-style consumption.
412
- */
413
- interface StreamSink<T> {
414
- /** Stable id used by clients to subscribe. */
415
- readonly id: string;
406
+ readonly logger: DevframeDiagnosticsLogger;
416
407
  /**
417
- * Aborts when the consumer cancels (server-side) or when the transport
418
- * loses every subscriber. Producers should poll `signal.aborted` and exit
419
- * cleanly.
408
+ * Register additional diagnostic definitions with this host. After
409
+ * registration, codes from the new definition are reachable via
410
+ * `host.logger.CODE`. Plugins that want shared output formatting should
411
+ * build their diagnostics via `host.defineDiagnostics()` first — that
412
+ * factory pre-wires the host's ANSI console reporter.
420
413
  */
421
- readonly signal: AbortSignal;
422
- /** `true` after `close()` / `error()`. Further writes throw. */
423
- readonly closed: boolean;
424
- /** Last allocated sequence number. `0` until the first write. */
425
- readonly lastSeq: number;
426
- write: (chunk: T) => void;
427
- error: (reason: unknown) => void;
428
- close: () => void;
429
- /** External-cancel path. Aborts the signal so handlers can short-circuit. */
430
- abort: (reason?: unknown) => void;
431
- /** `WritableStream<T>` adapter — same in-memory state as the imperative API. */
432
- readonly writable: WritableStream<T>;
433
- /**
434
- * Internal — RPC layer subscribes to receive chunk/end notifications.
435
- * Not part of the public contract; do not call directly.
436
- *
437
- * @internal
438
- */
439
- readonly events: EventEmitter<StreamSinkEvents<T>>;
440
- /**
441
- * Internal — replay buffer. RPC layer reads on (re)subscribe to feed
442
- * missed chunks before going live.
443
- *
444
- * @internal
445
- */
446
- readonly buffer: ReadonlyArray<BufferedChunk<T>>;
447
- }
448
- interface CreateStreamReaderOptions {
449
- id?: string;
450
- /**
451
- * Maximum number of buffered chunks held client-side while the consumer
452
- * isn't draining. On overflow, the oldest chunk is dropped.
453
- */
454
- highWaterMark?: number;
455
- /**
456
- * Called when the chunk queue overflows the high-water mark. The RPC
457
- * layer wires this to a coded warning; the primitive itself is
458
- * RPC-agnostic.
459
- */
460
- onOverflow?: (dropped: number) => void;
461
- /** Called when the consumer cancels — the RPC layer sends `:cancel` upstream. */
462
- onCancel?: () => void;
463
- }
464
- /**
465
- * Client-side consumer handle. Both an `AsyncIterable<T>` (for `for await`)
466
- * and exposes `readable: ReadableStream<T>` (for `pipeTo`). Pick one — they
467
- * share a single internal queue, so concurrent draining will race.
468
- */
469
- interface StreamReader<T> extends AsyncIterable<T> {
470
- readonly id: string;
471
- readonly cancelled: boolean;
472
- readonly done: boolean;
473
- /** Highest `seq` observed. Used for replay on reconnect. */
474
- readonly lastSeenSeq: number;
475
- /** `ReadableStream<T>` adapter for `pipeTo`-style consumption. */
476
- readonly readable: ReadableStream<T>;
477
- cancel: () => void;
478
- /** @internal */
479
- _push: (seq: number, chunk: T) => void;
480
- /** @internal */
481
- _end: (error?: StreamErrorPayload) => void;
482
- }
483
- /**
484
- * Build a server-side stream sink. RPC-agnostic — the RPC host wires
485
- * `events.on('chunk' | 'end')` to broadcast, and reads `buffer` to replay
486
- * for late or reconnecting subscribers.
487
- */
488
- declare function createStreamSink<T>(options?: CreateStreamSinkOptions): StreamSink<T>;
489
- /**
490
- * Build a client-side stream reader. RPC-agnostic — the RPC host calls
491
- * `_push(seq, chunk)` on each incoming chunk and `_end(error?)` on the
492
- * terminal frame. Consumers iterate with `for await` or pipe `readable`.
493
- */
494
- declare function createStreamReader<T>(options?: CreateStreamReaderOptions): StreamReader<T>;
495
- //#endregion
496
- //#region src/types/diagnostics.d.ts
497
- /**
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.
503
- */
504
- type DevframeDiagnosticsLogger = Record<string, any>;
505
- /**
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
- * ```
527
- */
528
- interface DevframeDiagnosticsHost {
529
- /**
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.
535
- */
536
- readonly logger: DevframeDiagnosticsLogger;
537
- /**
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.
543
- */
544
- register: (definitions: Record<string, unknown>) => void;
414
+ register: (definitions: Record<string, unknown>) => void;
545
415
  /**
546
416
  * Build a typed diagnostics object with the host's ANSI console reporter
547
417
  * pre-wired. The same `devframe/utils/nostics` `defineDiagnostics` every
@@ -620,125 +490,309 @@ interface DevframeHost {
620
490
  */
621
491
  type DevframeStorageScope = 'workspace' | 'project' | 'global';
622
492
  //#endregion
623
- //#region src/types/views.d.ts
624
- interface DevframeViewHost {
493
+ //#region src/types/rpc-augments.d.ts
494
+ /**
495
+ * To be extended
496
+ */
497
+ interface DevframeRpcClientFunctions {
625
498
  /**
499
+ * Server→client notification that this connection's auth token has been
500
+ * revoked. The client drops to untrusted on receipt. Broadcast by
501
+ * `revokeActiveConnectionsForToken`.
502
+ *
626
503
  * @internal
627
504
  */
628
- buildStaticDirs: {
629
- baseUrl: string;
630
- source: StaticAssetsSource;
631
- }[];
505
+ 'devframe:auth:revoked': () => Promise<void>;
632
506
  /**
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
507
+ * Streaming chunk pushed from server to subscribed clients. Wired by
508
+ * `RpcStreamingHost`; do not register manually.
636
509
  *
637
- * Accepts a local dist directory, or a {@link StaticAssetsSource} remote
638
- * declaration served through devframe's caching CDN back-proxy.
510
+ * @internal
511
+ */
512
+ 'devframe:streaming:chunk': (channel: string, id: string, seq: number, chunk: any) => Promise<void>;
513
+ /**
514
+ * Streaming terminator pushed from server to subscribed clients. Wired by
515
+ * `RpcStreamingHost`; do not register manually.
639
516
  *
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.
517
+ * @internal
646
518
  */
647
- hostStatic: (baseUrl: string, source: StaticAssetsSource, defaultResolveFrom?: string | null) => void;
648
- }
649
- //#endregion
650
- //#region src/types/scope.d.ts
651
- type AnyRpcFn = (...args: any[]) => any;
652
- /**
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.
658
- */
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>>;
519
+ 'devframe:streaming:end': (channel: string, id: string, error?: {
520
+ name: string;
521
+ message: string;
522
+ }) => Promise<void>;
698
523
  /**
699
- * Subscribe to settings changes. Resolves to an unsubscribe function.
700
- * Fires on every local or remote mutation with the full snapshot.
524
+ * Server→client cancel for an in-flight upload. Wired by
525
+ * `RpcStreamingHost`; do not register manually.
526
+ *
527
+ * @internal
701
528
  */
702
- onChange: (fn: (value: Readonly<T>) => void) => Promise<() => void>;
703
- }
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>;
529
+ 'devframe:streaming:upload-cancel': (channel: string, id: string) => Promise<void>;
530
+ /**
531
+ * Full shared-state snapshot pushed from server to subscribed clients.
532
+ * Wired by `RpcSharedStateHost`; do not register manually.
533
+ *
534
+ * @internal
535
+ */
536
+ 'devframe:rpc:client-state:updated': (key: string, fullState: any, syncId: string) => Promise<void>;
537
+ /**
538
+ * Incremental shared-state patch pushed from server to subscribed clients.
539
+ * Wired by `RpcSharedStateHost`; do not register manually.
540
+ *
541
+ * @internal
542
+ */
543
+ 'devframe:rpc:client-state:patch': (key: string, patches: any[], syncId: string) => Promise<void>;
715
544
  }
716
545
  /**
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).
546
+ * To be extended
735
547
  */
736
- interface ScopedBroadcastOptions<METHOD, Args extends any[]> {
737
- method: METHOD;
738
- args: Args;
739
- optional?: boolean;
740
- event?: boolean;
741
- }
548
+ interface DevframeRpcServerFunctions {
549
+ /**
550
+ * Authenticate a connection with a previously-issued bearer token; resolves
551
+ * whether the connection is now trusted. The interactive handler is provided
552
+ * by the host adapter (e.g. Vite DevTools); the standalone server registers
553
+ * an auto-trust noop when `auth: false`.
554
+ *
555
+ * Named with the `anonymous:` prefix (see `isAnonymousRpcMethod`) so it is
556
+ * reachable before the connection is trusted — this is the *only* rule the
557
+ * pre-trust gate applies, no per-method allowlist.
558
+ *
559
+ * @internal
560
+ */
561
+ 'anonymous:devframe:auth': (params: {
562
+ authToken: string;
563
+ ua: string;
564
+ origin: string;
565
+ }) => Promise<{
566
+ isTrusted: boolean;
567
+ }>;
568
+ /**
569
+ * Exchange a one-time authentication code (shown by the dev server) for a fresh,
570
+ * node-issued bearer token, returning the token on success or `null`. The
571
+ * handler is provided by the host adapter on top of `exchangeTempAuthCode`.
572
+ *
573
+ * Named with the `anonymous:` prefix (see `isAnonymousRpcMethod`) so it is
574
+ * reachable before the connection is trusted.
575
+ *
576
+ * @internal
577
+ */
578
+ 'anonymous:devframe:auth:exchange': (params: {
579
+ code: string;
580
+ ua: string;
581
+ origin: string;
582
+ }) => Promise<{
583
+ authToken: string | null;
584
+ }>;
585
+ /**
586
+ * Self-revoke: the caller asks the server to revoke its own bearer token
587
+ * (if any) and drop to untrusted. Requires an already-trusted caller, so
588
+ * unlike the two handshake methods above it does **not** carry the
589
+ * `anonymous:` prefix. Registered by `recipes/interactive-auth`.
590
+ *
591
+ * @internal
592
+ */
593
+ 'devframe:auth:revoke': () => Promise<void>;
594
+ /**
595
+ * Subscribe a client to a shared-state key. Wired by
596
+ * `RpcSharedStateHost`; do not register manually.
597
+ *
598
+ * @internal
599
+ */
600
+ 'devframe:rpc:server-state:subscribe': (key: string) => Promise<void>;
601
+ /**
602
+ * Read the current value for a shared-state key. Wired by
603
+ * `RpcSharedStateHost`; do not register manually.
604
+ *
605
+ * @internal
606
+ */
607
+ 'devframe:rpc:server-state:get': (key: string) => Promise<any>;
608
+ /**
609
+ * Replace a shared-state value (from the client). Wired by
610
+ * `RpcSharedStateHost`; do not register manually.
611
+ *
612
+ * @internal
613
+ */
614
+ 'devframe:rpc:server-state:set': (key: string, value: any, syncId: string) => Promise<void>;
615
+ /**
616
+ * Apply a patch to a shared-state value (from the client). Wired by
617
+ * `RpcSharedStateHost`; do not register manually.
618
+ *
619
+ * @internal
620
+ */
621
+ 'devframe:rpc:server-state:patch': (key: string, patches: any[], syncId: string) => Promise<void>;
622
+ /**
623
+ * Client→server streaming subscription with optional replay cursor.
624
+ * Wired by `RpcStreamingHost`; do not register manually.
625
+ *
626
+ * @internal
627
+ */
628
+ 'devframe:streaming:subscribe': (channel: string, id: string, opts?: {
629
+ afterSeq?: number;
630
+ }) => Promise<void>;
631
+ /**
632
+ * Client→server streaming unsubscribe. Wired by `RpcStreamingHost`;
633
+ * do not register manually.
634
+ *
635
+ * @internal
636
+ */
637
+ 'devframe:streaming:unsubscribe': (channel: string, id: string) => Promise<void>;
638
+ /**
639
+ * Client→server streaming cancellation request. Wired by
640
+ * `RpcStreamingHost`; do not register manually.
641
+ *
642
+ * @internal
643
+ */
644
+ 'devframe:streaming:cancel': (channel: string, id: string) => Promise<void>;
645
+ /**
646
+ * Client→server upload chunk. Wired by `RpcStreamingHost`; do not
647
+ * register manually.
648
+ *
649
+ * @internal
650
+ */
651
+ 'devframe:streaming:upload-chunk': (channel: string, id: string, seq: number, chunk: any) => Promise<void>;
652
+ /**
653
+ * Client→server upload terminator. Wired by `RpcStreamingHost`; do not
654
+ * register manually.
655
+ *
656
+ * @internal
657
+ */
658
+ 'devframe:streaming:upload-end': (channel: string, id: string, error?: {
659
+ name: string;
660
+ message: string;
661
+ }) => Promise<void>;
662
+ }
663
+ /**
664
+ * To be extended
665
+ */
666
+ interface DevframeRpcSharedStates {
667
+ /**
668
+ * Wire-service advertisements — package name → `{ package, version,
669
+ * scope, meta }` for every installed {@link import('./services').DevframeServiceDefinition}.
670
+ * Written by the node services host at the `ready()` barrier; clients read
671
+ * it through `client.services` (or subscribe to it directly for
672
+ * reactivity). Read-only from the browser.
673
+ */
674
+ 'devframe:services': DevframeServicesState;
675
+ }
676
+ //#endregion
677
+ //#region src/types/views.d.ts
678
+ interface DevframeViewHost {
679
+ /**
680
+ * @internal
681
+ */
682
+ buildStaticDirs: {
683
+ baseUrl: string;
684
+ source: StaticAssetsSource;
685
+ }[];
686
+ /**
687
+ * Helper to host static files
688
+ * - In `dev` mode, it will register middleware to `viteServer.middlewares` to host the static files
689
+ * - In `build` mode, it will copy the static files to the dist directory
690
+ *
691
+ * Accepts a local dist directory, or a {@link StaticAssetsSource} remote
692
+ * declaration served through devframe's caching CDN back-proxy.
693
+ *
694
+ * `defaultResolveFrom` overrides, for this call only, the context's own
695
+ * `importMetaUrl` as the default `resolveFrom` for a remote source that
696
+ * doesn't set one. A shared host that mounts assets on behalf of another
697
+ * devframe (a hub installing a plugin) passes that plugin's `importMetaUrl`
698
+ * so the assets resolve against the plugin's dependency graph rather than
699
+ * the host's.
700
+ */
701
+ hostStatic: (baseUrl: string, source: StaticAssetsSource, defaultResolveFrom?: string | null) => void;
702
+ }
703
+ //#endregion
704
+ //#region src/types/scope.d.ts
705
+ type AnyRpcFn = (...args: any[]) => any;
706
+ /**
707
+ * Resolve a scoped bare name `T` back to its fully-qualified entry in
708
+ * `Registry` (`<NS>:<T>`). Indexing the plain registry behind a `keyof`
709
+ * guard keeps TypeScript from rejecting a generic index into the
710
+ * key-remapped `Scoped*Functions<NS>` maps (TS2536) once a plugin augments
711
+ * the registry, while preserving each name's argument/return typing.
712
+ */
713
+ type ScopedRpcFn<Registry, NS extends string, T extends string> = `${NS}:${T}` extends keyof Registry ? Extract<Registry[`${NS}:${T}`], AnyRpcFn> : AnyRpcFn;
714
+ /**
715
+ * Augmentable registry mapping a scope namespace to the shape of its
716
+ * persisted settings. Tools augment it so `ctx.scope('my-plugin')`
717
+ * gets a fully-typed `settings.global` / `settings.project`:
718
+ *
719
+ * ```ts
720
+ * declare module 'devframe' {
721
+ * interface DevframeSettingsRegistry {
722
+ * 'my-plugin': { theme: 'light' | 'dark', recentFiles: string[] }
723
+ * }
724
+ * }
725
+ * ```
726
+ */
727
+ interface DevframeSettingsRegistry {}
728
+ /**
729
+ * Resolve the settings shape for a namespace from
730
+ * {@link DevframeSettingsRegistry}, falling back to an open record when
731
+ * the namespace hasn't been augmented.
732
+ */
733
+ type SettingsForNamespace<NS extends string> = NS extends keyof DevframeSettingsRegistry ? DevframeSettingsRegistry[NS] extends Record<string, any> ? DevframeSettingsRegistry[NS] : Record<string, any> : Record<string, any>;
734
+ /**
735
+ * A persisted key-value settings store for one scope (`global` or
736
+ * `project`). Backed by a file on the node side and by the shared-state
737
+ * sync protocol on the client, so a `set` on either side propagates to
738
+ * every connected peer and survives restarts.
739
+ *
740
+ * Every method is async because the underlying shared state is resolved
741
+ * lazily on first access.
742
+ */
743
+ interface DevframeSettingsStore<T extends Record<string, any> = Record<string, any>> {
744
+ /** Read a single setting. Resolves to `undefined` when unset. */
745
+ get: <K extends keyof T>(key: K) => Promise<T[K] | undefined>;
746
+ /** Write a single setting. */
747
+ set: <K extends keyof T>(key: K, value: T[K]) => Promise<void>;
748
+ /** Remove a single setting. */
749
+ delete: <K extends keyof T>(key: K) => Promise<void>;
750
+ /** Read the whole settings object (immutable snapshot). */
751
+ all: () => Promise<Readonly<T>>;
752
+ /**
753
+ * Subscribe to settings changes. Resolves to an unsubscribe function.
754
+ * Fires on every local or remote mutation with the full snapshot.
755
+ */
756
+ onChange: (fn: (value: Readonly<T>) => void) => Promise<() => void>;
757
+ }
758
+ /**
759
+ * The two settings scopes available on a scoped context.
760
+ *
761
+ * - `project` — per-workspace state, persisted under the host's
762
+ * `workspace` storage dir. Project-local settings.
763
+ * - `global` — per-user state, persisted under the host's `global`
764
+ * storage dir. Machine-wide preferences.
765
+ */
766
+ interface DevframeSettings<T extends Record<string, any> = Record<string, any>> {
767
+ global: DevframeSettingsStore<T>;
768
+ project: DevframeSettingsStore<T>;
769
+ }
770
+ /**
771
+ * Map the server-side RPC registry down to the bare names owned by a
772
+ * namespace, so a scoped `call('get-cwd')` is typed without the
773
+ * `my-plugin:` prefix.
774
+ */
775
+ type ScopedServerFunctions<NS extends string> = { [K in keyof DevframeRpcServerFunctions as K extends `${NS}:${infer R}` ? R : never]: DevframeRpcServerFunctions[K]; };
776
+ /**
777
+ * Map the client-side RPC registry down to the bare names owned by a
778
+ * namespace, used by scoped `broadcast`.
779
+ */
780
+ type ScopedClientFunctions<NS extends string> = { [K in keyof DevframeRpcClientFunctions as K extends `${NS}:${infer R}` ? R : never]: DevframeRpcClientFunctions[K]; };
781
+ /**
782
+ * Map the shared-state registry down to the bare keys owned by a
783
+ * namespace, so scoped `sharedState('messages')` is typed without the
784
+ * `my-plugin:` prefix.
785
+ */
786
+ type ScopedSharedStates<NS extends string> = { [K in keyof DevframeRpcSharedStates as K extends `${NS}:${infer R}` ? R : never]: DevframeRpcSharedStates[K]; };
787
+ /**
788
+ * Broadcast options for a scoped client method (bare name).
789
+ */
790
+ interface ScopedBroadcastOptions<METHOD, Args extends any[]> {
791
+ method: METHOD;
792
+ args: Args;
793
+ optional?: boolean;
794
+ event?: boolean;
795
+ }
742
796
  /**
743
797
  * Node-side streaming host scoped to a namespace. Channel names are
744
798
  * auto-prefixed with `<namespace>:`.
@@ -1023,1014 +1077,989 @@ interface DevframeServicesHost {
1023
1077
  ready: () => Promise<void>;
1024
1078
  }
1025
1079
  //#endregion
1026
- //#region src/types/rpc-augments.d.ts
1080
+ //#region src/types/devframe.d.ts
1027
1081
  /**
1028
- * To be extended
1082
+ * Classification of how a devframe is being deployed. Hosted adapters
1083
+ * (`vite`, `embedded`) share their origin with a host app and must
1084
+ * namespace their mount path under `/__<id>/`. Standalone adapters
1085
+ * (`cli`, `build`) own the origin and default to `/`.
1029
1086
  */
1030
- interface DevframeRpcClientFunctions {
1031
- /**
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
1037
- */
1038
- 'devframe:auth:revoked': () => Promise<void>;
1087
+ type DevframeDeploymentKind = 'standalone' | 'hosted';
1088
+ /**
1089
+ * How a hub deduplicates devframes that share an `id` when more than one
1090
+ * is mounted onto the same hub. See {@link DevframeDefinition.duplicationStrategy}.
1091
+ *
1092
+ * - `'warn'` (default) — keep the first registration, drop later
1093
+ * duplicates, and emit a warning diagnostic (`DF8105`).
1094
+ * - `'silent'` — drop later duplicates without warning.
1095
+ * - `'throw'` — throw when a duplicate is mounted.
1096
+ * - `'duplicate'` — let every instance coexist under a disambiguated
1097
+ * dock id.
1098
+ */
1099
+ type DevframeDuplicationStrategy = 'warn' | 'silent' | 'throw' | 'duplicate';
1100
+ /**
1101
+ * Controls where the browser opens the RPC WebSocket — advertised in
1102
+ * `__connection.json` and used to bind the dev server. The three shapes map
1103
+ * to the three connection scenarios; precedence is `url` > `port` > `route`:
1104
+ *
1105
+ * 1. **Same server, different route** (default) — leave `port`/`url` unset.
1106
+ * The socket shares the HTTP server's port and binds to `route`
1107
+ * (`__ws`). The client connects to its own origin, so the link
1108
+ * survives a reverse proxy that rewrites the host/port/subpath.
1109
+ *
1110
+ * 2. **Different port** — set `port`. The socket binds on its own port on the
1111
+ * same host; the client targets `ws(s)://<page-host>:<port>/<route>`.
1112
+ *
1113
+ * 3. **Remote, different origin** — set `url` to a full `ws://`/`wss://`
1114
+ * endpoint (e.g. a public tunnel or relay). The client uses it verbatim.
1115
+ */
1116
+ interface DevframeWsOptions {
1039
1117
  /**
1040
- * Streaming chunk pushed from server to subscribed clients. Wired by
1041
- * `RpcStreamingHost`; do not register manually.
1042
- *
1043
- * @internal
1118
+ * Upgrade route segment the socket binds to and is advertised at, relative
1119
+ * to the SPA base. Default: `__ws`.
1044
1120
  */
1045
- 'devframe:streaming:chunk': (channel: string, id: string, seq: number, chunk: any) => Promise<void>;
1121
+ route?: string;
1046
1122
  /**
1047
- * Streaming terminator pushed from server to subscribed clients. Wired by
1048
- * `RpcStreamingHost`; do not register manually.
1049
- *
1050
- * @internal
1123
+ * Bind the socket on its own port instead of sharing the HTTP port. The
1124
+ * browser connects to this port on the page's hostname. Implies
1125
+ * {@link DevframeWsOptions.sidecar}.
1051
1126
  */
1052
- 'devframe:streaming:end': (channel: string, id: string, error?: {
1053
- name: string;
1054
- message: string;
1055
- }) => Promise<void>;
1127
+ port?: number;
1056
1128
  /**
1057
- * Server→client cancel for an in-flight upload. Wired by
1058
- * `RpcStreamingHost`; do not register manually.
1059
- *
1060
- * @internal
1129
+ * Start a side-car WebSocket server on a free port, for hosts whose request
1130
+ * handlers can't accept upgrades (Next.js route handlers, Nitro, Rsbuild).
1131
+ * The resolved port is advertised in `__connection.json`, so the browser
1132
+ * finds it without any further wiring. Set `port` instead to pin it.
1061
1133
  */
1062
- 'devframe:streaming:upload-cancel': (channel: string, id: string) => Promise<void>;
1134
+ sidecar?: boolean;
1063
1135
  /**
1064
- * Full shared-state snapshot pushed from server to subscribed clients.
1065
- * Wired by `RpcSharedStateHost`; do not register manually.
1066
- *
1067
- * @internal
1136
+ * Advertise a fixed, fully-qualified endpoint on another origin (a full
1137
+ * `ws://`/`wss://` URL). Takes precedence over `port`/`route` in the meta.
1068
1138
  */
1069
- 'devframe:rpc:client-state:updated': (key: string, fullState: any, syncId: string) => Promise<void>;
1139
+ url?: string;
1140
+ }
1141
+ /**
1142
+ * Controls the SSE RPC endpoint — the WebSocket-free transport for hosts
1143
+ * and proxies where the upgrade isn't available. It rides whatever HTTP
1144
+ * surface serves the instance (the same one serving `__connection.json`),
1145
+ * so a relative `route` always resolves; there is no port plumbing of its
1146
+ * own. Enabled by default alongside the WebSocket; pass `sse: false` to
1147
+ * disable, or `ws: false` to run SSE-only (`backend: 'sse'`).
1148
+ */
1149
+ interface DevframeSseOptions {
1070
1150
  /**
1071
- * Incremental shared-state patch pushed from server to subscribed clients.
1072
- * Wired by `RpcSharedStateHost`; do not register manually.
1073
- *
1074
- * @internal
1151
+ * Route segment the SSE endpoint binds to and is advertised at, relative
1152
+ * to the SPA base. Default: `__sse`.
1075
1153
  */
1076
- 'devframe:rpc:client-state:patch': (key: string, patches: any[], syncId: string) => Promise<void>;
1154
+ route?: string;
1077
1155
  }
1078
1156
  /**
1079
- * To be extended
1157
+ * Configuration for the route-based MCP server mounted alongside the dev
1158
+ * server (opt-in via {@link DevframeCliOptions.mcp}). The endpoint speaks
1159
+ * the MCP Streamable-HTTP transport over the same origin as the SPA,
1160
+ * exposing the definition's `ctx.agent` tools + shared-state resources to
1161
+ * external MCP clients connected to the *running* server.
1080
1162
  */
1081
- interface DevframeRpcServerFunctions {
1163
+ interface McpRouteOptions {
1082
1164
  /**
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
- *
1092
- * @internal
1165
+ * Route segment the MCP endpoint binds to, relative to the SPA base.
1166
+ * Default: `__mcp` (i.e. `/__mcp` standalone, `/__<id>/__mcp` hosted).
1093
1167
  */
1094
- 'anonymous:devframe:auth': (params: {
1095
- authToken: string;
1096
- ua: string;
1097
- origin: string;
1098
- }) => Promise<{
1099
- isTrusted: boolean;
1100
- }>;
1168
+ path?: string;
1101
1169
  /**
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`.
1105
- *
1106
- * Named with the `anonymous:` prefix (see `isAnonymousRpcMethod`) so it is
1107
- * reachable before the connection is trusted.
1170
+ * Extra `Origin` header values to accept beyond the loopback default
1171
+ * (`localhost`/`127.0.0.1`/`::1` and any `Origin`-less native client).
1172
+ * Add your LAN/tunnel origin here when reaching the endpoint from another
1173
+ * host, mirroring the WS transport's origin gate. Pass `false` to disable
1174
+ * origin checking entirely (not recommended). Default: loopback-only.
1108
1175
  *
1109
- * @internal
1176
+ * This is the endpoint's DNS-rebinding protection — the shared
1177
+ * `isAllowedOrigin` gate the WS upgrade already uses, applied as external
1178
+ * middleware (the approach the MCP SDK now recommends over its own
1179
+ * deprecated `allowedHosts`/`allowedOrigins` transport flags).
1110
1180
  */
1111
- 'anonymous:devframe:auth:exchange': (params: {
1112
- code: string;
1113
- ua: string;
1114
- origin: string;
1115
- }) => Promise<{
1116
- authToken: string | null;
1117
- }>;
1181
+ allowedOrigins?: readonly string[] | false;
1182
+ }
1183
+ interface DevframeCliOptions {
1184
+ /** Binary name; default: the devframe's `id`. */
1185
+ command?: string;
1186
+ /** Preferred port for the dev server (default 9999). */
1187
+ port?: number;
1188
+ /** Port scan range, forwarded to `get-port-please`. */
1189
+ portRange?: [number, number];
1190
+ /** Prefer a random open port. */
1191
+ random?: boolean;
1192
+ /** Default host to bind to; `--host` overrides. */
1193
+ host?: string;
1118
1194
  /**
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
1195
+ * Auto-open the browser when the dev server starts.
1196
+ * `true` opens the resolved origin; a string opens that relative path.
1197
+ * The `--open` / `--no-open` flags override this.
1125
1198
  */
1126
- 'devframe:auth:revoke': () => Promise<void>;
1199
+ open?: boolean | string;
1127
1200
  /**
1128
- * Subscribe a client to a shared-state key. Wired by
1129
- * `RpcSharedStateHost`; do not register manually.
1201
+ * Authentication for the standalone dev server.
1130
1202
  *
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.
1203
+ * - `undefined` / `true` — the standalone adapters (`cli` /
1204
+ * served `build`) auto-wire devframe's interactive OTP auth
1205
+ * (`createInteractiveAuth`): an untrusted client can only reach
1206
+ * `anonymous:` methods until it exchanges the printed one-time code.
1207
+ * The adapter prints the code + magic-link banner once the server is
1208
+ * listening.
1209
+ * - `false` — no gate, for trusted single-user localhost tools where an
1210
+ * auth round-trip only gets in the way (the built-in plugins set this).
1211
+ * The `--no-auth` CLI flag maps here for one-off runs.
1212
+ * - A {@link DevframeAuthHandler} — a custom handler (e.g. a tuned
1213
+ * `createInteractiveAuth`, or an entirely different scheme) passed
1214
+ * straight through to the RPC transport binding.
1137
1215
  *
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.
1216
+ * Hosted adapters (`vite`, `embedded`) ignore this and defer to the host's
1217
+ * auth; `@vitejs/devtools` honors the equivalent `devtools.clientAuth`.
1144
1218
  *
1145
- * @internal
1219
+ * @default true
1146
1220
  */
1147
- 'devframe:rpc:server-state:set': (key: string, value: any, syncId: string) => Promise<void>;
1221
+ auth?: boolean | DevframeAuthHandler;
1148
1222
  /**
1149
- * Apply a patch to a shared-state value (from the client). Wired by
1150
- * `RpcSharedStateHost`; do not register manually.
1223
+ * Expose a route-based MCP server alongside the dev server, speaking the
1224
+ * MCP Streamable-HTTP transport at `/__mcp` (relative to the base path).
1225
+ * It surfaces the same `ctx.agent` tools + shared-state resources as the
1226
+ * stdio `mcp` command, but against the live, running server.
1151
1227
  *
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.
1228
+ * - `false` / omitted (default) — no MCP route is mounted.
1229
+ * - `true` — mount at the default `__mcp` route with the loopback-only
1230
+ * origin gate.
1231
+ * - {@link McpRouteOptions} — customise the route path / allowed origins.
1158
1232
  *
1159
- * @internal
1233
+ * The `--mcp` / `--no-mcp` CLI flags override this per run.
1160
1234
  */
1161
- 'devframe:streaming:subscribe': (channel: string, id: string, opts?: {
1162
- afterSeq?: number;
1163
- }) => Promise<void>;
1235
+ mcp?: boolean | McpRouteOptions;
1164
1236
  /**
1165
- * Client→server streaming unsubscribe. Wired by `RpcStreamingHost`;
1166
- * do not register manually.
1167
- *
1168
- * @internal
1237
+ * Author's SPA dist — served as the devframe's UI. A local directory, or
1238
+ * a {@link StaticAssetsSource} remote declaration (`{ package, version }`)
1239
+ * served through devframe's caching CDN back-proxy so the assets need not
1240
+ * ship inside the node package.
1169
1241
  */
1170
- 'devframe:streaming:unsubscribe': (channel: string, id: string) => Promise<void>;
1242
+ distDir?: StaticAssetsSource;
1171
1243
  /**
1172
- * Client→server streaming cancellation request. Wired by
1173
- * `RpcStreamingHost`; do not register manually.
1174
- *
1175
- * @internal
1244
+ * How the browser reaches the RPC WebSocket. Defaults to sharing the HTTP
1245
+ * port on the `__ws` route. See {@link DevframeWsOptions} for the
1246
+ * different-port and remote-origin variants. Pass `false` to serve no
1247
+ * WebSocket at all — clients connect over SSE instead (`backend: 'sse'`).
1176
1248
  */
1177
- 'devframe:streaming:cancel': (channel: string, id: string) => Promise<void>;
1249
+ ws?: DevframeWsOptions | false;
1178
1250
  /**
1179
- * Client→server upload chunk. Wired by `RpcStreamingHost`; do not
1180
- * register manually.
1181
- *
1182
- * @internal
1251
+ * How the browser reaches the SSE RPC endpoint — enabled by default
1252
+ * alongside the WebSocket as the more portable transport. Pass `false`
1253
+ * to disable, or a {@link DevframeSseOptions} to change its route.
1183
1254
  */
1184
- 'devframe:streaming:upload-chunk': (channel: string, id: string, seq: number, chunk: any) => Promise<void>;
1255
+ sse?: boolean | DevframeSseOptions;
1185
1256
  /**
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
- }
1196
- /**
1197
- * To be extended
1198
- */
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.
1257
+ * Capability-side CAC hook. Called with the CAC instance after the
1258
+ * adapter registers its built-in commands (`build` / `mcp`)
1259
+ * but before `createCac`'s own `configureCli` caller. Use this to
1260
+ * contribute tool-specific flags and subcommands from the definition
1261
+ * itself.
1206
1262
  */
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[]> {
1216
- method: METHOD;
1217
- args: Args;
1218
- optional?: boolean;
1219
- event?: boolean;
1220
- filter?: (client: BirpcReturn<DevframeRpcClientFunctions, DevframeRpcServerFunctions, false>) => boolean | void;
1221
- }
1222
- type RpcFunctionsHost = RpcFunctionsCollectorBase<DevframeRpcServerFunctions, DevframeNodeContext> & {
1263
+ configure?: (cli: CAC) => void;
1223
1264
  /**
1224
- * Invoke a locally registered server RPC function directly.
1265
+ * Typed CLI flags for the default `dev` command, backed by any
1266
+ * [Standard Schema](https://standardschema.dev/) validator (valibot,
1267
+ * zod, arktype, or devframe's built-in `s`). The adapter registers
1268
+ * matching `--kebab-key` options on CAC, validates the parsed values,
1269
+ * and forwards the typed bag to `setup(ctx, { flags })`.
1225
1270
  *
1226
- * This bypasses transport and is useful for server-side cross-function calls.
1227
- */
1228
- invokeLocal: <T extends keyof DevframeRpcServerFunctions, Args extends Parameters<DevframeRpcServerFunctions[T]>>(method: T, ...args: Args) => Promise<Awaited<ReturnType<DevframeRpcServerFunctions[T]>>>;
1229
- /**
1230
- * Broadcast a message to all connected clients
1231
- */
1232
- broadcast: <T extends keyof DevframeRpcClientFunctions, Args extends Parameters<DevframeRpcClientFunctions[T]>>(options: RpcBroadcastOptions<T, Args>) => Promise<void>;
1233
- /**
1234
- * Get the current RPC client
1271
+ * Use {@link defineCliFlags} to preserve the literal schema-map
1272
+ * shape, and {@link InferCliFlags} to recover the typed output at the
1273
+ * call site:
1235
1274
  *
1236
- * Available in RPC functions to get the current RPC client
1275
+ * ```ts
1276
+ * const appFlags = defineCliFlags({
1277
+ * depth: v.pipe(v.number(), v.integer()),
1278
+ * config: v.optional(v.string()),
1279
+ * })
1280
+ *
1281
+ * defineDevframe({
1282
+ * cli: { flags: appFlags },
1283
+ * setup(ctx, info) {
1284
+ * const flags = info.flags as InferCliFlags<typeof appFlags>
1285
+ * },
1286
+ * })
1287
+ * ```
1237
1288
  */
1238
- getCurrentRpcSession: () => DevframeNodeRpcSession | undefined;
1289
+ flags?: CliFlagsSchema;
1290
+ }
1291
+ /**
1292
+ * Default dock attributes for the iframe entry a hub synthesizes when it
1293
+ * mounts this devframe. Framework-neutral metadata only — the hub layer
1294
+ * (`ctx.install`) merges these beneath its per-mount `dock` overrides,
1295
+ * which in turn sit beneath the locked, derived `id` / `type` / `url`.
1296
+ *
1297
+ * Every field is optional. `title` / `icon` default to the definition's
1298
+ * `name` / `icon` when omitted here; the rest are unset by default.
1299
+ * Standalone adapters (`cli` / `build`) ignore this entirely.
1300
+ */
1301
+ interface DevframeDockDefaults {
1302
+ /** Dock entry title. Defaults to the definition's `name`. */
1303
+ title?: string;
1304
+ /** Dock entry icon. Defaults to the definition's `icon`. */
1305
+ icon?: string | {
1306
+ light: string;
1307
+ dark: string;
1308
+ };
1239
1309
  /**
1240
- * The shared state host
1310
+ * Sort weight within the dock; higher sorts earlier.
1311
+ * @default 0
1241
1312
  */
1242
- sharedState: RpcSharedStateHost;
1313
+ defaultOrder?: number;
1243
1314
  /**
1244
- * The streaming channel host. Provides per-channel `start()` /
1245
- * `pipeFrom()` producers; clients consume via `rpc.streaming.subscribe()`.
1246
- *
1247
- * @see RpcStreamingHost
1315
+ * Category the entry groups under in the dock.
1316
+ * @default 'default'
1248
1317
  */
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[];
1318
+ category?: string;
1258
1319
  /**
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.
1320
+ * Conditional-visibility expression (same syntax as command `when`
1321
+ * clauses). Set to `'false'` to hide the entry unconditionally.
1263
1322
  */
1264
- onKeyAdded: (fn: (key: string) => void) => () => void;
1323
+ when?: string;
1265
1324
  /**
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.
1325
+ * Render-only visibility expression, same syntax as {@link when}. Hides the
1326
+ * entry's own dock-bar button when it evaluates to `false` while leaving it
1327
+ * registered and reachable (activation, RPC lookups, etc.) — unlike `when`,
1328
+ * which is the general relevance switch for the entry as a whole.
1270
1329
  */
1271
- delete: (key: string) => boolean;
1330
+ visibility?: string;
1331
+ /** Badge text rendered on the dock icon (e.g. an unread count). */
1332
+ badge?: string;
1333
+ /** Id of the dock group this entry collapses under, if any. */
1334
+ groupId?: string;
1272
1335
  }
1273
1336
  /**
1274
- * Options for `RpcStreamingHost.create()`.
1337
+ * Runtime information threaded into `setup(ctx, info)`. Adapters
1338
+ * populate the fields that make sense for their deployment. In
1339
+ * particular, `createCac` fills `flags` with the parsed CAC bag.
1275
1340
  */
1276
- interface RpcStreamingChannelOptions {
1341
+ interface DevframeSetupInfo {
1342
+ /** Parsed CLI flags, populated by the CLI adapter. */
1343
+ flags?: Record<string, unknown>;
1344
+ }
1345
+ interface DevframeDefinition {
1346
+ id: string;
1347
+ name: string;
1348
+ /** Semver of the tool, surfaced in hub UIs and diagnostics. */
1349
+ version: string;
1350
+ /** npm package name the devframe ships in (e.g. `@scope/my-tool`). */
1351
+ packageName: string;
1277
1352
  /**
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.
1353
+ * `import.meta.url` of the module that defines this devframe. **Always
1354
+ * provide it** (`importMetaUrl: import.meta.url`): it is the resolution base
1355
+ * for the tool's own dependency graph, which lets the host resolve
1356
+ * everything against the plugin's own installed packages rather than the
1357
+ * consuming app's.
1281
1358
  *
1282
- * The buffer is per stream id, not per channel — each `channel.start()`
1283
- * gets its own.
1359
+ * - **Remote assets** — becomes the default `resolveFrom` for any remote
1360
+ * {@link StaticAssetsSource} the devframe hosts (its `cli.distDir`, and
1361
+ * every `ctx.views.hostStatic` call) that doesn't set one explicitly, so a
1362
+ * locally installed copy of the assets package is served with zero
1363
+ * network. A per-asset `resolveFrom` still wins, and an explicit
1364
+ * `resolveFrom: null` still opts out.
1365
+ * - **Service dependencies** — becomes the base the host resolves declared
1366
+ * {@link DevframeServiceInput | services} from, so a plugin can ship a
1367
+ * service package as its own dependency instead of asking users to install
1368
+ * it.
1369
+ *
1370
+ * Optional for backward compatibility; omitting it falls back to
1371
+ * runtime-directory resolution and disables the zero-network installed-copy
1372
+ * fast paths above.
1284
1373
  */
1285
- replayWindow?: number;
1374
+ importMetaUrl?: string;
1375
+ /** Project homepage or documentation URL. */
1376
+ homepage: string;
1377
+ /** One-line summary of what the tool does. */
1378
+ description: string;
1379
+ icon?: string | {
1380
+ light: string;
1381
+ dark: string;
1382
+ };
1286
1383
  /**
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.
1384
+ * Default dock attributes applied when a hub mounts this devframe as an
1385
+ * iframe dock entry. Consulted only by the hub install path (`ctx.install`),
1386
+ * which merge these beneath the per-mount `dock` overrides; standalone
1387
+ * adapters (`cli` / `build`) ignore it.
1291
1388
  *
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.
1389
+ * @see {@link DevframeDockDefaults}
1295
1390
  */
1296
- closedStreamRetention?: number;
1297
- }
1298
- /**
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.
1302
- *
1303
- * @see {@link https://devfra.me/guide/streaming Streaming guide}
1304
- */
1305
- interface RpcStreamingChannel<T = unknown> {
1306
- /** Channel name as registered with `ctx.rpc.streaming.create()`. */
1307
- readonly name: string;
1391
+ dock?: DevframeDockDefaults;
1308
1392
  /**
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.
1393
+ * Mount path override. Defaults depend on the adapter:
1394
+ * `/` for standalone (`cli` / `build`), `/__<id>/` for hosted
1395
+ * (`vite` / `embedded`).
1313
1396
  */
1314
- start: (opts?: {
1315
- id?: string;
1316
- }) => StreamSink<T>;
1397
+ basePath?: string;
1317
1398
  /**
1318
- * Convenience: start a stream and pipe a `ReadableStream<T>` into it.
1319
- * The pipe uses `sink.signal` so cancellation propagates upstream.
1399
+ * How a hub reacts when another devframe sharing this one's `id` is
1400
+ * mounted onto the same hub. Consulted only by hub adapters
1401
+ * (`ctx.install`); standalone adapters (`cli` / `build`)
1402
+ * ignore it.
1320
1403
  *
1321
- * Node-stream interop: convert a `Readable` with `Readable.toWeb(node)`
1322
- * before passing it here.
1404
+ * @default 'warn'
1323
1405
  */
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[];
1406
+ duplicationStrategy?: DevframeDuplicationStrategy;
1331
1407
  /**
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
- * ```
1408
+ * Declares which runtimes meaningfully support this devframe. Adapters
1409
+ * act on a `false` before doing any work:
1347
1410
  *
1348
- * Calling `reader.cancel()` on the server sends an `upload-cancel` to
1349
- * the uploading client, which aborts its sink.
1411
+ * - `build: false` — `createCac` skips registering the `build` subcommand,
1412
+ * and `createBuild` refuses (throws `DF0042`) unless `{ force: true }`.
1413
+ * Useful for a devframe whose value is inherently live (e.g. it manages
1414
+ * real files on disk), so a static export would only ever produce a
1415
+ * broken, write-less shell of the tool.
1416
+ * - `dev: false` — `createDevServer` refuses (throws `DF0058`) unless
1417
+ * `{ force: true }`. Useful for a devframe that only makes sense as a
1418
+ * static export (e.g. a report generator with nothing to serve live).
1350
1419
  */
1351
- openInbound: (opts?: {
1352
- id?: string;
1353
- }) => StreamReader<T>;
1420
+ capabilities?: {
1421
+ dev?: boolean;
1422
+ build?: boolean;
1423
+ };
1424
+ /**
1425
+ * Wire services this devframe consumes (see `DevframeServiceDefinition`).
1426
+ * Each entry is either a declarative descriptor
1427
+ * (`{ package, version?, required?, options? }` — the host imports the
1428
+ * package's default-export factory, resolving it against **this plugin's
1429
+ * own dependencies**) or a ready `DevframeServiceDefinition` (the factory
1430
+ * was already called). The adapter queues these before `setup(ctx)` runs
1431
+ * and constructs each service once at the `ctx.services.ready()` barrier,
1432
+ * merging option sets across every declarer. Missing services are skipped
1433
+ * unless marked `required` — clients feature-detect via
1434
+ * `client.services.has(pkg)` and degrade.
1435
+ */
1436
+ services?: DevframeServiceInput[];
1437
+ /** RPC-level configuration for this devframe (see {@link DevframeRpcOptions}). */
1438
+ rpc?: DevframeRpcOptions;
1439
+ /** Server-side setup — the primary entrypoint. Runs in every runtime. */
1440
+ setup: (ctx: DevframeNodeContext, info?: DevframeSetupInfo) => void | Promise<void>;
1441
+ cli?: DevframeCliOptions;
1354
1442
  }
1443
+ interface DevframeRpcOptions {
1444
+ /**
1445
+ * Opt an RPC function into the static-build snapshot **without owning its
1446
+ * definition** — the mechanism a devframe uses to bake a wire service's
1447
+ * RPC (e.g. `@devframes/service-git`'s `status`/`log`/`show`) into its
1448
+ * `build` export, since the service itself defines no `dump`/`snapshot`.
1449
+ *
1450
+ * Each entry is either a bare method id (bakes the no-argument call, like
1451
+ * `snapshot: true`) or `{ method, inputs }` where `inputs` is the list of
1452
+ * argument-tuples to bake — or an async provider given the node context
1453
+ * (so it can enumerate at build time, e.g. read commit hashes via the
1454
+ * service's node API). `createBuild` resolves these after setup and
1455
+ * executes the target's own handler per tuple; the first tuple's result
1456
+ * becomes the fallback so any call variant resolves to a baked value.
1457
+ */
1458
+ snapshot?: DevframeSnapshotRpcEntry[];
1459
+ }
1460
+ /** Argument-tuples to bake for a {@link DevframeSnapshotRpcEntry}, or a provider that computes them at build time. */
1461
+ type DevframeSnapshotRpcInputs = readonly (readonly unknown[])[] | ((ctx: DevframeNodeContext) => readonly (readonly unknown[])[] | Promise<readonly (readonly unknown[])[]>);
1355
1462
  /**
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.
1463
+ * One {@link DevframeRpcOptions.snapshot} entry: a bare method id (bakes
1464
+ * the no-argument call) or a method plus the argument-tuples to bake.
1359
1465
  */
1360
- interface RpcStreamingHost {
1361
- /**
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.
1365
- */
1366
- create: <T = unknown>(name: string, opts?: RpcStreamingChannelOptions) => RpcStreamingChannel<T>;
1367
- /**
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
1373
- */
1374
- _onSessionDisconnected: (meta: DevframeNodeRpcSessionMeta) => void;
1375
- }
1466
+ type DevframeSnapshotRpcEntry = string | {
1467
+ method: string;
1468
+ inputs: DevframeSnapshotRpcInputs;
1469
+ };
1376
1470
  //#endregion
1377
- //#region src/types/context.d.ts
1378
- interface DevframeCapabilities {
1379
- rpc?: boolean;
1380
- views?: boolean;
1471
+ //#region src/utils/shared-state.d.ts
1472
+ type ImmutablePrimitive = undefined | null | boolean | string | number | Function;
1473
+ type Immutable<T> = T extends ImmutablePrimitive ? T : T extends Array<infer U> ? ImmutableArray<U> : T extends Map<infer K, infer V> ? ImmutableMap<K, V> : T extends Set<infer M> ? ImmutableSet<M> : ImmutableObject<T>;
1474
+ type ImmutableArray<T> = ReadonlyArray<Immutable<T>>;
1475
+ type ImmutableMap<K, V> = ReadonlyMap<Immutable<K>, Immutable<V>>;
1476
+ type ImmutableSet<T> = ReadonlySet<Immutable<T>>;
1477
+ type ImmutableObject<T> = { readonly [K in keyof T]: Immutable<T[K]>; };
1478
+ /**
1479
+ * Serializable patch describing a single mutation to a `SharedState`.
1480
+ * Structurally compatible with JSON-Patch and is safe to send over RPC.
1481
+ */
1482
+ interface SharedStatePatch {
1483
+ op: 'add' | 'remove' | 'replace';
1484
+ path: readonly (string | number)[];
1485
+ value?: unknown;
1381
1486
  }
1382
1487
  /**
1383
- * Framework- and build-tool-agnostic node context — RPC + diagnostics +
1384
- * agent + the view-host (HTTP file-serving). Host adapters can wrap this
1385
- * to add their own surfaces; for example, `@vitejs/devtools-kit`'s
1386
- * `createKitContext` adds `docks`, `terminals`, `messages`, and
1387
- * `commands` when mounted into Vite DevTools. JSON rendering is an opt-in
1388
- * integration (`@devframes/json-render`) layered on top, not part of this
1389
- * core surface.
1488
+ * State host that is immutable by default with explicit mutate.
1390
1489
  */
1391
- interface DevframeNodeContext {
1392
- readonly workspaceRoot: string;
1393
- readonly cwd: string;
1490
+ interface SharedState<T> {
1394
1491
  /**
1395
- * Lifecycle distinction surfaced to plugin authors:
1396
- *
1397
- * - `'dev'` — long-running, interactive session. Connections come and
1398
- * go; broadcasts and shared-state mutations are debounced
1399
- * to keep the UI responsive.
1400
- * - `'build'` — one-shot batch run. The context is set up, the devtool
1401
- * collects what it needs, and a snapshot is written. No
1402
- * live UI, no WS server.
1403
- *
1404
- * Names are inherited from Vite's serve/build dichotomy but the meaning
1405
- * is general: the same distinction applies to any tool that runs in
1406
- * either an interactive or a static-output mode.
1492
+ * Get the current state. Immutable.
1407
1493
  */
1408
- readonly mode: 'dev' | 'build';
1494
+ value: () => Immutable<T>;
1409
1495
  /**
1410
- * Host runtime abstraction — exposes `mountStatic` / `resolveOrigin` /
1411
- * `getStorageDir`.
1496
+ * Subscribe to state changes.
1412
1497
  */
1413
- host: DevframeHost;
1414
- rpc: RpcFunctionsHost;
1415
- views: DevframeViewHost;
1498
+ on: EventEmitter<SharedStateEvents<T>>['on'];
1416
1499
  /**
1417
- * Structured diagnostics host — wraps `nostics` and lets integrations
1418
- * register their own coded errors/warnings into the shared lookup.
1500
+ * Mutate the state.
1419
1501
  */
1420
- diagnostics: DevframeDiagnosticsHost;
1502
+ mutate: (fn: (state: T) => void, syncId?: string) => void;
1421
1503
  /**
1422
- * Agent host — aggregates the agent-exposed surface of this devtool.
1504
+ * Apply patches to the state.
1423
1505
  */
1424
- agent: DevframeAgentHost;
1506
+ patch: (patches: SharedStatePatch[], syncId?: string) => void;
1425
1507
  /**
1426
- * Cross-plugin services — a typed, namespaced registry through which one
1427
- * integration exposes a capability (e.g. a data-source registry) and
1428
- * others consume it without a hard package dependency. Ids follow the RPC
1429
- * namespacing rule (`<plugin-id>:<service>`); types come from augmenting
1430
- * the `DevframeServicesRegistry` interface. `whenAvailable` subscriptions
1431
- * absorb setup-order differences between provider and consumer.
1508
+ * Sync IDs that have been applied to the state.
1432
1509
  */
1433
- services: DevframeServicesHost;
1510
+ syncIds: Set<string>;
1511
+ }
1512
+ interface SharedStateEvents<T> {
1513
+ updated: (fullState: T, patches: SharedStatePatch[] | undefined, syncId: string) => void;
1514
+ }
1515
+ interface SharedStateOptions<T> {
1434
1516
  /**
1435
- * This context's own {@link ConnectionMeta.configs} — static, boot-time
1436
- * config a host publishes once through the connection handshake and every
1437
- * client reads read-only. A plain, **non-reactive** object: mutate it
1438
- * during `setup(ctx)` (a plugin sets its keys, a hub aggregates across
1439
- * every installed devframe), never during the session — it's serialized
1440
- * once, after setup, and changing it afterwards reaches no client.
1441
- *
1442
- * ```ts
1443
- * ctx.staticConfig.dock = {
1444
- * ...ctx.staticConfig.dock,
1445
- * categoryOrder: { ...ctx.staticConfig.dock?.categoryOrder, ...myOrder },
1446
- * }
1447
- * ```
1517
+ * Initial state.
1448
1518
  */
1449
- staticConfig: Partial<DevframeConnectionConfigsRegistry>;
1519
+ initialValue: T;
1450
1520
  /**
1451
- * Create a namespace-scoped view of this context. The returned
1452
- * `ctx.scope('my-plugin')` auto-namespaces every RPC id, shared-state
1453
- * key, and streaming channel with `my-plugin:`, and exposes a typed
1454
- * top-level `settings` store. This is the preferred way to consume the
1455
- * context from a single tool's setup code.
1521
+ * Enable patches.
1456
1522
  *
1457
- * Pass `null` or `''` to un-scope and get the base context.
1523
+ * @default false
1458
1524
  */
1459
- scope: {
1460
- <NS extends string>(namespace: NS): DevframeScopedNodeContext<NS, SettingsForNamespace<NS>>;
1461
- (namespace?: null | ''): DevframeNodeContext;
1462
- };
1525
+ enablePatches?: boolean;
1526
+ }
1527
+ declare function createSharedState<T extends object>(options: SharedStateOptions<T>): SharedState<T>;
1528
+ //#endregion
1529
+ //#region src/utils/streaming-channel.d.ts
1530
+ /**
1531
+ * Serialized error shape sent over the wire when a stream ends with a failure.
1532
+ * Stays JSON-safe so the strict-JSON encoder can carry it without coercion.
1533
+ */
1534
+ interface StreamErrorPayload {
1535
+ name: string;
1536
+ message: string;
1463
1537
  }
1464
1538
  /**
1465
- * Describes where the browser client should open its RPC WebSocket. The
1466
- * object form is the proxy-flexible default: `path` is resolved relative to
1467
- * where `__connection.json` was loaded, and the connection is made to the
1468
- * page's own origin (only the `http`→`ws` / `https`→`wss` protocol swap is
1469
- * applied). This survives reverse proxies that change the host/port, because
1470
- * the client never trusts a server-baked hostname — it reuses its own.
1539
+ * Single buffered chunk in the server-side ring buffer.
1471
1540
  *
1472
- * Set `port` (and/or `host`) only when the WS endpoint genuinely lives on a
1473
- * different origin than the page, e.g. a side-car server on its own port.
1541
+ * Sequence numbers start at 1 and increment per write. Subscribers track
1542
+ * `lastSeenSeq` and ask for `afterSeq` on resubscribe so the server can
1543
+ * replay any chunks the client missed during a brief disconnect.
1474
1544
  */
1475
- interface ConnectionMetaWebsocket {
1545
+ interface BufferedChunk<T> {
1546
+ seq: number;
1547
+ chunk: T;
1548
+ }
1549
+ interface StreamSinkEvents<T> {
1550
+ /** Fired for each `write()`. The RPC layer subscribes and broadcasts. */
1551
+ chunk: (seq: number, chunk: T) => void;
1552
+ /** Terminal — fired exactly once per sink lifetime. */
1553
+ end: (error?: StreamErrorPayload) => void;
1554
+ }
1555
+ interface CreateStreamSinkOptions {
1556
+ id?: string;
1476
1557
  /**
1477
- * Path to the WS endpoint. Relative paths (the default, e.g. `__ws`) are
1478
- * resolved against `__connection.json`'s location; absolute paths (`/__ws`)
1479
- * resolve against the page origin.
1558
+ * Size of the per-stream ring buffer kept for replay-on-resubscribe.
1559
+ * `0` (default) disables replay.
1480
1560
  */
1481
- path?: string;
1482
- /** Override the port. Combined with the page hostname unless `host` is set. */
1483
- port?: number;
1484
- /** Override the host (`hostname[:port]`). Use for a fully cross-origin endpoint. */
1485
- host?: string;
1561
+ replayWindow?: number;
1486
1562
  }
1487
1563
  /**
1488
- * Object form of {@link ConnectionMeta.sse} — the same proxy-safe shape and
1489
- * resolution rules as {@link ConnectionMetaWebsocket}, producing an
1490
- * `http(s)://` endpoint instead of a `ws(s)://` one. Set `port` (and/or
1491
- * `host`) only when the SSE endpoint genuinely lives on a different origin
1492
- * than the page, e.g. a side-car server on its own port.
1564
+ * Server-side producer handle. Two equivalent surfaces share one piece of
1565
+ * state: the imperative `write/error/close` triple, and a `WritableStream<T>`
1566
+ * for `pipeTo`-style consumption.
1493
1567
  */
1494
- interface ConnectionMetaSse {
1495
- /**
1496
- * Path to the SSE endpoint. Relative paths (the default, e.g. `__sse`)
1497
- * are resolved against `__connection.json`'s location; absolute paths
1498
- * (`/__sse`) resolve against the page origin.
1499
- */
1500
- path?: string;
1501
- /** Override the port. Combined with the page hostname unless `host` is set. */
1502
- port?: number;
1503
- /** Override the host (`hostname[:port]`). Use for a fully cross-origin endpoint. */
1504
- host?: string;
1505
- }
1506
- interface ConnectionMeta {
1507
- /**
1508
- * The server's primary live-RPC transport (`websocket` / `sse`), `static`
1509
- * for a pre-computed RPC dump with no live server, or `none` for a server
1510
- * exposing no RPC transport at all (e.g. an MCP-only deployment).
1511
- */
1512
- backend: 'websocket' | 'sse' | 'static' | 'none';
1513
- /**
1514
- * WebSocket endpoint, resolved by the client into a `ws(s)://` URL:
1515
- *
1516
- * - {@link ConnectionMetaWebsocket} — the proxy-flexible default; a
1517
- * same-origin path relative to `__connection.json`.
1518
- * - `number` — a port on the page's hostname (`ws(s)://<host>:<port>`).
1519
- * - `string` — a full `ws://`/`wss://` URL used verbatim, an `http(s)://`
1520
- * URL with its protocol swapped, or a path resolved same-origin.
1521
- */
1522
- websocket?: number | string | ConnectionMetaWebsocket;
1523
- /**
1524
- * SSE endpoint (`GET` opens the event stream, `POST` carries RPC calls),
1525
- * resolved by the client into an `http(s)://` URL with the same
1526
- * proxy-safe rules as {@link ConnectionMeta.websocket}:
1527
- *
1528
- * - {@link ConnectionMetaSse} — the proxy-flexible default; a
1529
- * same-origin path relative to `__connection.json`.
1530
- * - `string` — a full `http(s)://` URL used verbatim, or a path
1531
- * resolved same-origin.
1532
- */
1533
- sse?: string | ConnectionMetaSse;
1534
- /**
1535
- * Present when the dev server exposes a route-based MCP endpoint
1536
- * (`cli.mcp`). Advertises the MCP Streamable-HTTP route so in-browser
1537
- * tooling (e.g. an MCP inspector) can discover it without guessing the
1538
- * path. `path` is relative to `__connection.json`'s location, like the
1539
- * WebSocket `path`. `port` is set when the endpoint lives on a side-car
1540
- * server on its own port (bridge mode — `devframeViteBridge`,
1541
- * `@devframes/next`): the client combines the page hostname with `port`
1542
- * and resolves `path` against that origin, mirroring
1543
- * {@link ConnectionMetaWebsocket.port}.
1544
- */
1545
- mcp?: {
1546
- path: string;
1547
- port?: number;
1548
- };
1568
+ interface StreamSink<T> {
1569
+ /** Stable id used by clients to subscribe. */
1570
+ readonly id: string;
1549
1571
  /**
1550
- * Names of RPC functions that have declared `jsonSerializable: true`.
1551
- * Used by the WS / static client to dispatch the per-call wire
1552
- * serializer (strict JSON for these methods, structured-clone for
1553
- * the rest). Populated by the server / build adapter; absent on
1554
- * legacy clients, in which case all outgoing messages fall back to
1555
- * structured-clone.
1572
+ * Aborts when the consumer cancels (server-side) or when the transport
1573
+ * loses every subscriber. Producers should poll `signal.aborted` and exit
1574
+ * cleanly.
1556
1575
  */
1557
- jsonSerializableMethods?: string[];
1576
+ readonly signal: AbortSignal;
1577
+ /** `true` after `close()` / `error()`. Further writes throw. */
1578
+ readonly closed: boolean;
1579
+ /** Last allocated sequence number. `0` until the first write. */
1580
+ readonly lastSeq: number;
1581
+ write: (chunk: T) => void;
1582
+ error: (reason: unknown) => void;
1583
+ close: () => void;
1584
+ /** External-cancel path. Aborts the signal so handlers can short-circuit. */
1585
+ abort: (reason?: unknown) => void;
1586
+ /** `WritableStream<T>` adapter — same in-memory state as the imperative API. */
1587
+ readonly writable: WritableStream<T>;
1558
1588
  /**
1559
- * Absolute URL of the `__connection.json` this meta was loaded from.
1560
- * Annotated by the client (not served) when it publishes the meta on a
1561
- * shared window for same-origin inheritance: a relative `websocket.path`
1562
- * resolves against this, so a child SPA mounted at another base (e.g. a hub
1563
- * mounting several devframes at `/__foo/`, `/__bar/`, …) inherits a dialable
1564
- * endpoint rather than resolving the path against its own mount.
1589
+ * Internal — RPC layer subscribes to receive chunk/end notifications.
1590
+ * Not part of the public contract; do not call directly.
1591
+ *
1592
+ * @internal
1565
1593
  */
1566
- baseUrl?: string;
1594
+ readonly events: EventEmitter<StreamSinkEvents<T>>;
1567
1595
  /**
1568
- * A pre-issued bearer token embedded in the meta so a client trusts the
1569
- * server on connect without any prompt or `localStorage` lookup. Only a
1570
- * **hub** serving a **per-frame** connection meta populates this — it
1571
- * authenticates once at the top level and bakes the resulting token into
1572
- * the meta each plugin iframe fetches, so a cross-origin frame (which
1573
- * cannot read the hub's `localStorage`) is still pre-authorized. The
1574
- * standalone `__connection.json` never carries a token.
1596
+ * Internal — replay buffer. RPC layer reads on (re)subscribe to feed
1597
+ * missed chunks before going live.
1575
1598
  *
1576
- * > [!WARNING]
1577
- * > A token in a fetchable JSON is only as protected as the URL serving
1578
- * > it. Emit it exclusively from hub-controlled, per-frame meta — never
1579
- * > from a publicly reachable static `__connection.json`.
1599
+ * @internal
1580
1600
  */
1581
- authToken?: string;
1601
+ readonly buffer: ReadonlyArray<BufferedChunk<T>>;
1602
+ }
1603
+ interface CreateStreamReaderOptions {
1604
+ id?: string;
1582
1605
  /**
1583
- * Session-scoped token that lets a trusted external viewer register its
1584
- * browser origin before opening the WebSocket. The host includes it in the
1585
- * connection metadata, which the viewer obtains through the host page.
1586
- *
1587
- * Treat this as a bearer credential. Keep connection metadata containing the
1588
- * token same-origin until the requesting origin has been verified.
1606
+ * Maximum number of buffered chunks held client-side while the consumer
1607
+ * isn't draining. On overflow, the oldest chunk is dropped.
1589
1608
  */
1590
- viewerOriginToken?: string;
1609
+ highWaterMark?: number;
1591
1610
  /**
1592
- * Static, host-declared configuration — baked in once at connect time and
1593
- * fixed for the life of the server (e.g. a hub's UI rebrand, or its
1594
- * aggregated dock-bar layout preferences). Read-only from the browser: a
1595
- * client only ever reads `rpc.connectionMeta.configs`, never writes to it.
1596
- *
1597
- * Contrast this with {@link DevframeSettingsRegistry} (`ctx.scope(ns).settings`)
1598
- * and a hub's `devframe:user-settings` shared-state key — both are
1599
- * mutable, user-editable, and synced bidirectionally over RPC for the
1600
- * life of the session. `configs` is the opposite: one-way, immutable,
1601
- * decided by whoever assembled the server.
1602
- *
1603
- * Each key is owned by one package, contributed via declaration merging:
1604
- *
1605
- * ```ts
1606
- * declare module 'devframe/types' {
1607
- * interface DevframeConnectionConfigsRegistry {
1608
- * 'my-key': { some: 'shape' }
1609
- * }
1610
- * }
1611
- * ```
1611
+ * Called when the chunk queue overflows the high-water mark. The RPC
1612
+ * layer wires this to a coded warning; the primitive itself is
1613
+ * RPC-agnostic.
1612
1614
  */
1613
- configs?: Partial<DevframeConnectionConfigsRegistry>;
1615
+ onOverflow?: (dropped: number) => void;
1616
+ /** Called when the consumer cancels — the RPC layer sends `:cancel` upstream. */
1617
+ onCancel?: () => void;
1614
1618
  }
1615
1619
  /**
1616
- * Augmentation point for {@link ConnectionMeta.configs}. Empty by default —
1617
- * a package that wants to publish static, boot-time config through the
1618
- * connection handshake augments this interface with its own key (see
1619
- * {@link ConnectionMeta.configs} for the pattern). `@devframes/hub`
1620
- * augments it with `dock`; `@devframes/hub-ui` augments it with `ui`.
1620
+ * Client-side consumer handle. Both an `AsyncIterable<T>` (for `for await`)
1621
+ * and exposes `readable: ReadableStream<T>` (for `pipeTo`). Pick one — they
1622
+ * share a single internal queue, so concurrent draining will race.
1621
1623
  */
1622
- interface DevframeConnectionConfigsRegistry {}
1623
- //#endregion
1624
- //#region src/node/auth/handler.d.ts
1624
+ interface StreamReader<T> extends AsyncIterable<T> {
1625
+ readonly id: string;
1626
+ readonly cancelled: boolean;
1627
+ readonly done: boolean;
1628
+ /** Highest `seq` observed. Used for replay on reconnect. */
1629
+ readonly lastSeenSeq: number;
1630
+ /** `ReadableStream<T>` adapter for `pipeTo`-style consumption. */
1631
+ readonly readable: ReadableStream<T>;
1632
+ cancel: () => void;
1633
+ /** @internal */
1634
+ _push: (seq: number, chunk: T) => void;
1635
+ /** @internal */
1636
+ _end: (error?: StreamErrorPayload) => void;
1637
+ }
1625
1638
  /**
1626
- * A ready-made pre-auth RPC handler, as produced by
1627
- * `devframe/recipes/interactive-auth`'s `createInteractiveAuth`. Bundles
1628
- * everything a host adapter needs to wire an authenticated server:
1629
- * the handshake RPC functions, the resolver gate, the connect-time trust
1630
- * hook, and the startup banner.
1631
- *
1632
- * `initDevframe` / `initHub` accept one of these directly via their `auth` option —
1633
- * see {@link https://devfra.me | devframe}'s server docs — or a host can
1634
- * wire the four pieces itself against a lower-level transport.
1639
+ * Build a server-side stream sink. RPC-agnostic — the RPC host wires
1640
+ * `events.on('chunk' | 'end')` to broadcast, and reads `buffer` to replay
1641
+ * for late or reconnecting subscribers.
1635
1642
  */
1636
- interface DevframeAuthHandler {
1643
+ declare function createStreamSink<T>(options?: CreateStreamSinkOptions): StreamSink<T>;
1644
+ /**
1645
+ * Build a client-side stream reader. RPC-agnostic — the RPC host calls
1646
+ * `_push(seq, chunk)` on each incoming chunk and `_end(error?)` on the
1647
+ * terminal frame. Consumers iterate with `for await` or pipe `readable`.
1648
+ */
1649
+ declare function createStreamReader<T>(options?: CreateStreamReaderOptions): StreamReader<T>;
1650
+ //#endregion
1651
+ //#region src/types/rpc.d.ts
1652
+ interface DevframeNodeRpcSession {
1653
+ meta: DevframeNodeRpcSessionMeta;
1654
+ rpc: BirpcReturn<DevframeRpcClientFunctions, DevframeRpcServerFunctions, false>;
1655
+ }
1656
+ interface RpcBroadcastOptions<METHOD, Args extends any[]> {
1657
+ method: METHOD;
1658
+ args: Args;
1659
+ optional?: boolean;
1660
+ event?: boolean;
1661
+ filter?: (client: BirpcReturn<DevframeRpcClientFunctions, DevframeRpcServerFunctions, false>) => boolean | void;
1662
+ }
1663
+ type RpcFunctionsHost = RpcFunctionsCollectorBase<DevframeRpcServerFunctions, DevframeNodeContext> & {
1637
1664
  /**
1638
- * `anonymous:devframe:auth` + `anonymous:devframe:auth:exchange` (the
1639
- * handshake) and `devframe:auth:revoke` (self-revoke) — register these on
1640
- * the RPC host (e.g. `rpcHost.register(fn)` for each).
1665
+ * Invoke a locally registered server RPC function directly.
1666
+ *
1667
+ * This bypasses transport and is useful for server-side cross-function calls.
1641
1668
  */
1642
- rpcFunctions: RpcFunctionDefinitionAny[];
1669
+ invokeLocal: <T extends keyof DevframeRpcServerFunctions, Args extends Parameters<DevframeRpcServerFunctions[T]>>(method: T, ...args: Args) => Promise<Awaited<ReturnType<DevframeRpcServerFunctions[T]>>>;
1643
1670
  /**
1644
- * Resolver gate: whether `methodName` is callable given `session`'s
1645
- * current trust state. Defaults to allowing any `anonymous:`-prefixed
1646
- * method (see `isAnonymousRpcMethod`) plus anything once the session is
1647
- * trusted.
1671
+ * Broadcast a message to all connected clients
1648
1672
  */
1649
- authorize: (methodName: string, session: DevframeNodeRpcSession) => boolean;
1673
+ broadcast: <T extends keyof DevframeRpcClientFunctions, Args extends Parameters<DevframeRpcClientFunctions[T]>>(options: RpcBroadcastOptions<T, Args>) => Promise<void>;
1650
1674
  /**
1651
- * Connect-time trust: reads a bearer token off the connection's initial
1652
- * request (a static/pre-shared token from `clientAuthTokens`, or a token
1653
- * minted by the code exchange) and, when valid, marks the session
1654
- * trusted immediately — before the client's own handshake call.
1675
+ * Get the current RPC client
1676
+ *
1677
+ * Available in RPC functions to get the current RPC client
1655
1678
  */
1656
- onConnect: (connection: DevframeRpcConnection, session: DevframeNodeRpcSession) => void;
1679
+ getCurrentRpcSession: () => DevframeNodeRpcSession | undefined;
1657
1680
  /**
1658
- * Print the current one-time code and its magic-link URL. Devframe stays
1659
- * headless — call this yourself once the server is listening. Safe to
1660
- * call repeatedly; it only prints once per code.
1681
+ * The shared state host
1661
1682
  */
1662
- printBanner: () => void;
1683
+ sharedState: RpcSharedStateHost;
1663
1684
  /**
1664
- * Rewrite a URL the CLI is about to open in the browser (`--open`) so the
1665
- * tab lands already authenticated — e.g. appending the current OTP as a
1666
- * query param. Called with the fully-resolved target URL; return it
1667
- * unchanged to opt out. Optional: a handler that doesn't need this (e.g.
1668
- * one gated by a pre-shared bearer token instead of an interactive code)
1669
- * can omit it and the URL opens as-is.
1685
+ * The streaming channel host. Provides per-channel `start()` /
1686
+ * `pipeFrom()` producers; clients consume via `rpc.streaming.subscribe()`.
1687
+ *
1688
+ * @see RpcStreamingHost
1670
1689
  */
1671
- buildOpenUrl?: (url: string) => string;
1690
+ streaming: RpcStreamingHost;
1691
+ };
1692
+ interface RpcSharedStateGetOptions<T> {
1693
+ sharedState?: SharedState<T>;
1694
+ initialValue?: T;
1672
1695
  }
1673
- //#endregion
1674
- //#region src/types/devframe.d.ts
1675
- /**
1676
- * Classification of how a devframe is being deployed. Hosted adapters
1677
- * (`vite`, `embedded`) share their origin with a host app and must
1678
- * namespace their mount path under `/__<id>/`. Standalone adapters
1679
- * (`cli`, `build`) own the origin and default to `/`.
1680
- */
1681
- type DevframeDeploymentKind = 'standalone' | 'hosted';
1682
- /**
1683
- * How a hub deduplicates devframes that share an `id` when more than one
1684
- * is mounted onto the same hub. See {@link DevframeDefinition.duplicationStrategy}.
1685
- *
1686
- * - `'warn'` (default) — keep the first registration, drop later
1687
- * duplicates, and emit a warning diagnostic (`DF8105`).
1688
- * - `'silent'` — drop later duplicates without warning.
1689
- * - `'throw'` — throw when a duplicate is mounted.
1690
- * - `'duplicate'` — let every instance coexist under a disambiguated
1691
- * dock id.
1692
- */
1693
- type DevframeDuplicationStrategy = 'warn' | 'silent' | 'throw' | 'duplicate';
1694
- /**
1695
- * Controls where the browser opens the RPC WebSocket — advertised in
1696
- * `__connection.json` and used to bind the dev server. The three shapes map
1697
- * to the three connection scenarios; precedence is `url` > `port` > `route`:
1698
- *
1699
- * 1. **Same server, different route** (default) — leave `port`/`url` unset.
1700
- * The socket shares the HTTP server's port and binds to `route`
1701
- * (`__ws`). The client connects to its own origin, so the link
1702
- * survives a reverse proxy that rewrites the host/port/subpath.
1703
- *
1704
- * 2. **Different port** — set `port`. The socket binds on its own port on the
1705
- * same host; the client targets `ws(s)://<page-host>:<port>/<route>`.
1706
- *
1707
- * 3. **Remote, different origin** — set `url` to a full `ws://`/`wss://`
1708
- * endpoint (e.g. a public tunnel or relay). The client uses it verbatim.
1709
- */
1710
- interface DevframeWsOptions {
1696
+ interface RpcSharedStateHost {
1697
+ get: <T extends object = any>(key: string, options?: RpcSharedStateGetOptions<T>) => Promise<SharedState<T>>;
1698
+ keys: () => string[];
1711
1699
  /**
1712
- * Upgrade route segment the socket binds to and is advertised at, relative
1713
- * to the SPA base. Default: `__ws`.
1700
+ * Subscribe to new shared-state keys becoming available. Fires when
1701
+ * `get(key, ...)` creates a fresh entry (not on subsequent gets).
1702
+ * Useful for protocol adapters (e.g. MCP) that surface shared state
1703
+ * as dynamic resources.
1714
1704
  */
1715
- route?: string;
1705
+ onKeyAdded: (fn: (key: string) => void) => () => void;
1716
1706
  /**
1717
- * Bind the socket on its own port instead of sharing the HTTP port. The
1718
- * browser connects to this port on the page's hostname. Implies
1719
- * {@link DevframeWsOptions.sidecar}.
1707
+ * Unregister a shared state and drop its broadcast listeners. Returns
1708
+ * `true` when a state was removed, `false` when the key was unknown.
1709
+ * Used by short-lived states (e.g. a disposed JSON-render view) to avoid
1710
+ * leaking listeners and lingering entries for the context lifetime.
1720
1711
  */
1721
- port?: number;
1712
+ delete: (key: string) => boolean;
1713
+ }
1714
+ /**
1715
+ * Options for `RpcStreamingHost.create()`.
1716
+ */
1717
+ interface RpcStreamingChannelOptions {
1722
1718
  /**
1723
- * Start a side-car WebSocket server on a free port, for hosts whose request
1724
- * handlers can't accept upgrades (Next.js route handlers, Nitro, Rsbuild).
1725
- * The resolved port is advertised in `__connection.json`, so the browser
1726
- * finds it without any further wiring. Set `port` instead to pin it.
1719
+ * Size of the per-stream ring buffer kept on the server for
1720
+ * replay-on-resubscribe. `0` (default) disables replay; on reconnect
1721
+ * the consumer only sees chunks that arrive after subscribing.
1722
+ *
1723
+ * The buffer is per stream id, not per channel — each `channel.start()`
1724
+ * gets its own.
1727
1725
  */
1728
- sidecar?: boolean;
1726
+ replayWindow?: number;
1729
1727
  /**
1730
- * Advertise a fixed, fully-qualified endpoint on another origin (a full
1731
- * `ws://`/`wss://` URL). Takes precedence over `port`/`route` in the meta.
1728
+ * Milliseconds a closed stream is retained on the server after its
1729
+ * last subscriber leaves (or if no subscriber ever arrived). During
1730
+ * this window, late subscribers can still join and replay the buffer
1731
+ * + receive the `end` frame.
1732
+ *
1733
+ * Defaults to `30_000` (30 s) when `replayWindow > 0`, else `0`
1734
+ * (immediate free). Set to `0` to opt out, or higher for longer
1735
+ * post-mortem replay.
1732
1736
  */
1733
- url?: string;
1737
+ closedStreamRetention?: number;
1734
1738
  }
1735
1739
  /**
1736
- * Controls the SSE RPC endpoint — the WebSocket-free transport for hosts
1737
- * and proxies where the upgrade isn't available. It rides whatever HTTP
1738
- * surface serves the instance (the same one serving `__connection.json`),
1739
- * so a relative `route` always resolves; there is no port plumbing of its
1740
- * own. Enabled by default alongside the WebSocket; pass `sse: false` to
1741
- * disable, or `ws: false` to run SSE-only (`backend: 'sse'`).
1740
+ * Channel handle returned by `ctx.rpc.streaming.create(name, opts)`. A
1741
+ * channel owns a wire namespace; calling `start()` produces individual
1742
+ * streams keyed by id.
1743
+ *
1744
+ * @see {@link https://devfra.me/guide/streaming Streaming guide}
1742
1745
  */
1743
- interface DevframeSseOptions {
1746
+ interface RpcStreamingChannel<T = unknown> {
1747
+ /** Channel name as registered with `ctx.rpc.streaming.create()`. */
1748
+ readonly name: string;
1744
1749
  /**
1745
- * Route segment the SSE endpoint binds to and is advertised at, relative
1746
- * to the SPA base. Default: `__sse`.
1750
+ * Start a new stream. Returns a server-side sink with both an imperative
1751
+ * (`write` / `close` / `error`) surface and a `WritableStream<T>` for
1752
+ * `pipeTo` consumption. The sink's `signal` aborts when every subscriber
1753
+ * disconnects or cancels.
1747
1754
  */
1748
- route?: string;
1749
- }
1750
- /**
1751
- * Configuration for the route-based MCP server mounted alongside the dev
1752
- * server (opt-in via {@link DevframeCliOptions.mcp}). The endpoint speaks
1753
- * the MCP Streamable-HTTP transport over the same origin as the SPA,
1754
- * exposing the definition's `ctx.agent` tools + shared-state resources to
1755
- * external MCP clients connected to the *running* server.
1756
- */
1757
- interface McpRouteOptions {
1755
+ start: (opts?: {
1756
+ id?: string;
1757
+ }) => StreamSink<T>;
1758
1758
  /**
1759
- * Route segment the MCP endpoint binds to, relative to the SPA base.
1760
- * Default: `__mcp` (i.e. `/__mcp` standalone, `/__<id>/__mcp` hosted).
1759
+ * Convenience: start a stream and pipe a `ReadableStream<T>` into it.
1760
+ * The pipe uses `sink.signal` so cancellation propagates upstream.
1761
+ *
1762
+ * Node-stream interop: convert a `Readable` with `Readable.toWeb(node)`
1763
+ * before passing it here.
1761
1764
  */
1762
- path?: string;
1765
+ pipeFrom: (readable: ReadableStream<T>, opts?: {
1766
+ id?: string;
1767
+ }) => Promise<StreamSink<T>>;
1768
+ /** Look up an active stream by id. Returns `undefined` if none. */
1769
+ get: (id: string) => StreamSink<T> | undefined;
1770
+ /** All active outbound stream ids on this channel. */
1771
+ ids: () => string[];
1763
1772
  /**
1764
- * Extra `Origin` header values to accept beyond the loopback default
1765
- * (`localhost`/`127.0.0.1`/`::1` and any `Origin`-less native client).
1766
- * Add your LAN/tunnel origin here when reaching the endpoint from another
1767
- * host, mirroring the WS transport's origin gate. Pass `false` to disable
1768
- * origin checking entirely (not recommended). Default: loopback-only.
1773
+ * Open an inbound stream — the server side of a client-to-server
1774
+ * upload. Allocates an id, returns a `StreamReader<T>` that fills as
1775
+ * the client writes chunks. Typical pattern is to call this from an
1776
+ * action handler, kick off background processing, and return the id
1777
+ * so the caller can start uploading:
1769
1778
  *
1770
- * This is the endpoint's DNS-rebinding protection — the shared
1771
- * `isAllowedOrigin` gate the WS upgrade already uses, applied as external
1772
- * middleware (the approach the MCP SDK now recommends over its own
1773
- * deprecated `allowedHosts`/`allowedOrigins` transport flags).
1779
+ * ```ts
1780
+ * handler: async () => {
1781
+ * const reader = channel.openInbound()
1782
+ * ;(async () => {
1783
+ * for await (const chunk of reader) processChunk(chunk)
1784
+ * })()
1785
+ * return { uploadId: reader.id }
1786
+ * }
1787
+ * ```
1788
+ *
1789
+ * Calling `reader.cancel()` on the server sends an `upload-cancel` to
1790
+ * the uploading client, which aborts its sink.
1774
1791
  */
1775
- allowedOrigins?: readonly string[] | false;
1792
+ openInbound: (opts?: {
1793
+ id?: string;
1794
+ }) => StreamReader<T>;
1776
1795
  }
1777
- interface DevframeCliOptions {
1778
- /** Binary name; default: the devframe's `id`. */
1779
- command?: string;
1780
- /** Preferred port for the dev server (default 9999). */
1781
- port?: number;
1782
- /** Port scan range, forwarded to `get-port-please`. */
1783
- portRange?: [number, number];
1784
- /** Prefer a random open port. */
1785
- random?: boolean;
1786
- /** Default host to bind to; `--host` overrides. */
1787
- host?: string;
1796
+ /**
1797
+ * Server-side streaming host. Lives on `ctx.rpc.streaming` alongside
1798
+ * `ctx.rpc.sharedState`. Each named channel owns its own stream registry
1799
+ * and wire namespace.
1800
+ */
1801
+ interface RpcStreamingHost {
1788
1802
  /**
1789
- * Auto-open the browser when the dev server starts.
1790
- * `true` opens the resolved origin; a string opens that relative path.
1791
- * The `--open` / `--no-open` flags override this.
1803
+ * Register a streaming channel. Names follow the `<plugin-id>:<channel>`
1804
+ * convention (e.g. `'my-devtool:chat-stream'`). Throws `DF0032` if the
1805
+ * name is already taken.
1792
1806
  */
1793
- open?: boolean | string;
1807
+ create: <T = unknown>(name: string, opts?: RpcStreamingChannelOptions) => RpcStreamingChannel<T>;
1794
1808
  /**
1795
- * Authentication for the standalone dev server.
1796
- *
1797
- * - `undefined` / `true` — the standalone adapters (`cli` /
1798
- * served `build`) auto-wire devframe's interactive OTP auth
1799
- * (`createInteractiveAuth`): an untrusted client can only reach
1800
- * `anonymous:` methods until it exchanges the printed one-time code.
1801
- * The adapter prints the code + magic-link banner once the server is
1802
- * listening.
1803
- * - `false` — no gate, for trusted single-user localhost tools where an
1804
- * auth round-trip only gets in the way (the built-in plugins set this).
1805
- * The `--no-auth` CLI flag maps here for one-off runs.
1806
- * - A {@link DevframeAuthHandler} — a custom handler (e.g. a tuned
1807
- * `createInteractiveAuth`, or an entirely different scheme) passed
1808
- * straight through to the RPC transport binding.
1809
- *
1810
- * Hosted adapters (`vite`, `embedded`) ignore this and defer to the host's
1811
- * auth; `@vitejs/devtools` honors the equivalent `devtools.clientAuth`.
1809
+ * Adapters call this when a session disconnects so the host can drop
1810
+ * subscribers and abort orphaned streams. Most users do not need this;
1811
+ * it's wired by the RPC server binding automatically.
1812
1812
  *
1813
- * @default true
1813
+ * @internal
1814
1814
  */
1815
- auth?: boolean | DevframeAuthHandler;
1815
+ _onSessionDisconnected: (meta: DevframeNodeRpcSessionMeta) => void;
1816
+ }
1817
+ //#endregion
1818
+ //#region src/types/context.d.ts
1819
+ interface DevframeCapabilities {
1820
+ rpc?: boolean;
1821
+ views?: boolean;
1822
+ }
1823
+ /**
1824
+ * Framework- and build-tool-agnostic node context — RPC + diagnostics +
1825
+ * agent + the view-host (HTTP file-serving). Host adapters can wrap this
1826
+ * to add their own surfaces; for example, `@vitejs/devtools-kit`'s
1827
+ * `createKitContext` adds `docks`, `terminals`, `messages`, and
1828
+ * `commands` when mounted into Vite DevTools. JSON rendering is an opt-in
1829
+ * integration (`@devframes/json-render`) layered on top, not part of this
1830
+ * core surface.
1831
+ */
1832
+ interface DevframeNodeContext {
1833
+ readonly workspaceRoot: string;
1834
+ readonly cwd: string;
1816
1835
  /**
1817
- * Expose a route-based MCP server alongside the dev server, speaking the
1818
- * MCP Streamable-HTTP transport at `/__mcp` (relative to the base path).
1819
- * It surfaces the same `ctx.agent` tools + shared-state resources as the
1820
- * stdio `mcp` command, but against the live, running server.
1836
+ * Lifecycle distinction surfaced to plugin authors:
1821
1837
  *
1822
- * - `false` / omitted (default) — no MCP route is mounted.
1823
- * - `true` — mount at the default `__mcp` route with the loopback-only
1824
- * origin gate.
1825
- * - {@link McpRouteOptions} — customise the route path / allowed origins.
1838
+ * - `'dev'` — long-running, interactive session. Connections come and
1839
+ * go; broadcasts and shared-state mutations are debounced
1840
+ * to keep the UI responsive.
1841
+ * - `'build'` — one-shot batch run. The context is set up, the devtool
1842
+ * collects what it needs, and a snapshot is written. No
1843
+ * live UI, no WS server.
1826
1844
  *
1827
- * The `--mcp` / `--no-mcp` CLI flags override this per run.
1845
+ * Names are inherited from Vite's serve/build dichotomy but the meaning
1846
+ * is general: the same distinction applies to any tool that runs in
1847
+ * either an interactive or a static-output mode.
1828
1848
  */
1829
- mcp?: boolean | McpRouteOptions;
1849
+ readonly mode: 'dev' | 'build';
1830
1850
  /**
1831
- * Author's SPA dist — served as the devframe's UI. A local directory, or
1832
- * a {@link StaticAssetsSource} remote declaration (`{ package, version }`)
1833
- * served through devframe's caching CDN back-proxy so the assets need not
1834
- * ship inside the node package.
1851
+ * Host runtime abstraction — exposes `mountStatic` / `resolveOrigin` /
1852
+ * `getStorageDir`.
1835
1853
  */
1836
- distDir?: StaticAssetsSource;
1854
+ host: DevframeHost;
1855
+ rpc: RpcFunctionsHost;
1856
+ views: DevframeViewHost;
1837
1857
  /**
1838
- * How the browser reaches the RPC WebSocket. Defaults to sharing the HTTP
1839
- * port on the `__ws` route. See {@link DevframeWsOptions} for the
1840
- * different-port and remote-origin variants. Pass `false` to serve no
1841
- * WebSocket at all — clients connect over SSE instead (`backend: 'sse'`).
1858
+ * Structured diagnostics host — wraps `nostics` and lets integrations
1859
+ * register their own coded errors/warnings into the shared lookup.
1842
1860
  */
1843
- ws?: DevframeWsOptions | false;
1861
+ diagnostics: DevframeDiagnosticsHost;
1844
1862
  /**
1845
- * How the browser reaches the SSE RPC endpoint — enabled by default
1846
- * alongside the WebSocket as the more portable transport. Pass `false`
1847
- * to disable, or a {@link DevframeSseOptions} to change its route.
1863
+ * Agent host — aggregates the agent-exposed surface of this devtool.
1848
1864
  */
1849
- sse?: boolean | DevframeSseOptions;
1865
+ agent: DevframeAgentHost;
1850
1866
  /**
1851
- * Capability-side CAC hook. Called with the CAC instance after the
1852
- * adapter registers its built-in commands (`build` / `mcp`)
1853
- * but before `createCac`'s own `configureCli` caller. Use this to
1854
- * contribute tool-specific flags and subcommands from the definition
1855
- * itself.
1867
+ * Cross-plugin services — a typed, namespaced registry through which one
1868
+ * integration exposes a capability (e.g. a data-source registry) and
1869
+ * others consume it without a hard package dependency. Ids follow the RPC
1870
+ * namespacing rule (`<plugin-id>:<service>`); types come from augmenting
1871
+ * the `DevframeServicesRegistry` interface. `whenAvailable` subscriptions
1872
+ * absorb setup-order differences between provider and consumer.
1856
1873
  */
1857
- configure?: (cli: CAC) => void;
1874
+ services: DevframeServicesHost;
1858
1875
  /**
1859
- * Typed CLI flags for the default `dev` command, backed by any
1860
- * [Standard Schema](https://standardschema.dev/) validator (valibot,
1861
- * zod, arktype, or devframe's built-in `s`). The adapter registers
1862
- * matching `--kebab-key` options on CAC, validates the parsed values,
1863
- * and forwards the typed bag to `setup(ctx, { flags })`.
1864
- *
1865
- * Use {@link defineCliFlags} to preserve the literal schema-map
1866
- * shape, and {@link InferCliFlags} to recover the typed output at the
1867
- * call site:
1876
+ * This context's own {@link ConnectionMeta.configs} — static, boot-time
1877
+ * config a host publishes once through the connection handshake and every
1878
+ * client reads read-only. A plain, **non-reactive** object: mutate it
1879
+ * during `setup(ctx)` (a plugin sets its keys, a hub aggregates across
1880
+ * every installed devframe), never during the session — it's serialized
1881
+ * once, after setup, and changing it afterwards reaches no client.
1868
1882
  *
1869
1883
  * ```ts
1870
- * const appFlags = defineCliFlags({
1871
- * depth: v.pipe(v.number(), v.integer()),
1872
- * config: v.optional(v.string()),
1873
- * })
1874
- *
1875
- * defineDevframe({
1876
- * cli: { flags: appFlags },
1877
- * setup(ctx, info) {
1878
- * const flags = info.flags as InferCliFlags<typeof appFlags>
1879
- * },
1880
- * })
1884
+ * ctx.staticConfig.dock = {
1885
+ * ...ctx.staticConfig.dock,
1886
+ * categoryOrder: { ...ctx.staticConfig.dock?.categoryOrder, ...myOrder },
1887
+ * }
1881
1888
  * ```
1882
1889
  */
1883
- flags?: CliFlagsSchema;
1890
+ staticConfig: Partial<DevframeConnectionConfigsRegistry>;
1891
+ /**
1892
+ * Create a namespace-scoped view of this context. The returned
1893
+ * `ctx.scope('my-plugin')` auto-namespaces every RPC id, shared-state
1894
+ * key, and streaming channel with `my-plugin:`, and exposes a typed
1895
+ * top-level `settings` store. This is the preferred way to consume the
1896
+ * context from a single tool's setup code.
1897
+ *
1898
+ * Pass `null` or `''` to un-scope and get the base context.
1899
+ */
1900
+ scope: {
1901
+ <NS extends string>(namespace: NS): DevframeScopedNodeContext<NS, SettingsForNamespace<NS>>;
1902
+ (namespace?: null | ''): DevframeNodeContext;
1903
+ };
1884
1904
  }
1885
1905
  /**
1886
- * Default dock attributes for the iframe entry a hub synthesizes when it
1887
- * mounts this devframe. Framework-neutral metadata only — the hub layer
1888
- * (`ctx.install`) merges these beneath its per-mount `dock` overrides,
1889
- * which in turn sit beneath the locked, derived `id` / `type` / `url`.
1906
+ * Describes where the browser client should open its RPC WebSocket. The
1907
+ * object form is the proxy-flexible default: `path` is resolved relative to
1908
+ * where `__connection.json` was loaded, and the connection is made to the
1909
+ * page's own origin (only the `http`→`ws` / `https`→`wss` protocol swap is
1910
+ * applied). This survives reverse proxies that change the host/port, because
1911
+ * the client never trusts a server-baked hostname — it reuses its own.
1890
1912
  *
1891
- * Every field is optional. `title` / `icon` default to the definition's
1892
- * `name` / `icon` when omitted here; the rest are unset by default.
1893
- * Standalone adapters (`cli` / `build`) ignore this entirely.
1913
+ * Set `port` (and/or `host`) only when the WS endpoint genuinely lives on a
1914
+ * different origin than the page, e.g. a side-car server on its own port.
1894
1915
  */
1895
- interface DevframeDockDefaults {
1896
- /** Dock entry title. Defaults to the definition's `name`. */
1897
- title?: string;
1898
- /** Dock entry icon. Defaults to the definition's `icon`. */
1899
- icon?: string | {
1900
- light: string;
1901
- dark: string;
1902
- };
1916
+ interface ConnectionMetaWebsocket {
1903
1917
  /**
1904
- * Sort weight within the dock; higher sorts earlier.
1905
- * @default 0
1918
+ * Path to the WS endpoint. Relative paths (the default, e.g. `__ws`) are
1919
+ * resolved against `__connection.json`'s location; absolute paths (`/__ws`)
1920
+ * resolve against the page origin.
1906
1921
  */
1907
- defaultOrder?: number;
1922
+ path?: string;
1923
+ /** Override the port. Combined with the page hostname unless `host` is set. */
1924
+ port?: number;
1925
+ /** Override the host (`hostname[:port]`). Use for a fully cross-origin endpoint. */
1926
+ host?: string;
1927
+ }
1928
+ /**
1929
+ * Object form of {@link ConnectionMeta.sse} — the same proxy-safe shape and
1930
+ * resolution rules as {@link ConnectionMetaWebsocket}, producing an
1931
+ * `http(s)://` endpoint instead of a `ws(s)://` one. Set `port` (and/or
1932
+ * `host`) only when the SSE endpoint genuinely lives on a different origin
1933
+ * than the page, e.g. a side-car server on its own port.
1934
+ */
1935
+ interface ConnectionMetaSse {
1908
1936
  /**
1909
- * Category the entry groups under in the dock.
1910
- * @default 'default'
1937
+ * Path to the SSE endpoint. Relative paths (the default, e.g. `__sse`)
1938
+ * are resolved against `__connection.json`'s location; absolute paths
1939
+ * (`/__sse`) resolve against the page origin.
1911
1940
  */
1912
- category?: string;
1941
+ path?: string;
1942
+ /** Override the port. Combined with the page hostname unless `host` is set. */
1943
+ port?: number;
1944
+ /** Override the host (`hostname[:port]`). Use for a fully cross-origin endpoint. */
1945
+ host?: string;
1946
+ }
1947
+ interface ConnectionMeta {
1913
1948
  /**
1914
- * Conditional-visibility expression (same syntax as command `when`
1915
- * clauses). Set to `'false'` to hide the entry unconditionally.
1949
+ * The server's primary live-RPC transport (`websocket` / `sse`), `static`
1950
+ * for a pre-computed RPC dump with no live server, or `none` for a server
1951
+ * exposing no RPC transport at all (e.g. an MCP-only deployment).
1916
1952
  */
1917
- when?: string;
1953
+ backend: 'websocket' | 'sse' | 'static' | 'none';
1918
1954
  /**
1919
- * Render-only visibility expression, same syntax as {@link when}. Hides the
1920
- * entry's own dock-bar button when it evaluates to `false` while leaving it
1921
- * registered and reachable (activation, RPC lookups, etc.) — unlike `when`,
1922
- * which is the general relevance switch for the entry as a whole.
1955
+ * WebSocket endpoint, resolved by the client into a `ws(s)://` URL:
1956
+ *
1957
+ * - {@link ConnectionMetaWebsocket} — the proxy-flexible default; a
1958
+ * same-origin path relative to `__connection.json`.
1959
+ * - `number` — a port on the page's hostname (`ws(s)://<host>:<port>`).
1960
+ * - `string` — a full `ws://`/`wss://` URL used verbatim, an `http(s)://`
1961
+ * URL with its protocol swapped, or a path resolved same-origin.
1923
1962
  */
1924
- visibility?: string;
1925
- /** Badge text rendered on the dock icon (e.g. an unread count). */
1926
- badge?: string;
1927
- /** Id of the dock group this entry collapses under, if any. */
1928
- groupId?: string;
1929
- }
1930
- /**
1931
- * Runtime information threaded into `setup(ctx, info)`. Adapters
1932
- * populate the fields that make sense for their deployment. In
1933
- * particular, `createCac` fills `flags` with the parsed CAC bag.
1934
- */
1935
- interface DevframeSetupInfo {
1936
- /** Parsed CLI flags, populated by the CLI adapter. */
1937
- flags?: Record<string, unknown>;
1938
- }
1939
- interface DevframeDefinition {
1940
- id: string;
1941
- name: string;
1942
- /** Semver of the tool, surfaced in hub UIs and diagnostics. */
1943
- version: string;
1944
- /** npm package name the devframe ships in (e.g. `@scope/my-tool`). */
1945
- packageName: string;
1963
+ websocket?: number | string | ConnectionMetaWebsocket;
1946
1964
  /**
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.
1965
+ * SSE endpoint (`GET` opens the event stream, `POST` carries RPC calls),
1966
+ * resolved by the client into an `http(s)://` URL with the same
1967
+ * proxy-safe rules as {@link ConnectionMeta.websocket}:
1963
1968
  *
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.
1969
+ * - {@link ConnectionMetaSse} — the proxy-flexible default; a
1970
+ * same-origin path relative to `__connection.json`.
1971
+ * - `string` — a full `http(s)://` URL used verbatim, or a path
1972
+ * resolved same-origin.
1967
1973
  */
1968
- importMetaUrl?: string;
1969
- /** Project homepage or documentation URL. */
1970
- homepage: string;
1971
- /** One-line summary of what the tool does. */
1972
- description: string;
1973
- icon?: string | {
1974
- light: string;
1975
- dark: string;
1974
+ sse?: string | ConnectionMetaSse;
1975
+ /**
1976
+ * Present when the dev server exposes a route-based MCP endpoint
1977
+ * (`cli.mcp`). Advertises the MCP Streamable-HTTP route so in-browser
1978
+ * tooling (e.g. an MCP inspector) can discover it without guessing the
1979
+ * path. `path` is relative to `__connection.json`'s location, like the
1980
+ * WebSocket `path`. `port` is set when the endpoint lives on a side-car
1981
+ * server on its own port (bridge mode — `devframeViteBridge`,
1982
+ * `@devframes/next`): the client combines the page hostname with `port`
1983
+ * and resolves `path` against that origin, mirroring
1984
+ * {@link ConnectionMetaWebsocket.port}.
1985
+ */
1986
+ mcp?: {
1987
+ path: string;
1988
+ port?: number;
1976
1989
  };
1977
1990
  /**
1978
- * Default dock attributes applied when a hub mounts this devframe as an
1979
- * iframe dock entry. Consulted only by the hub install path (`ctx.install`),
1980
- * which merge these beneath the per-mount `dock` overrides; standalone
1981
- * adapters (`cli` / `build`) ignore it.
1982
- *
1983
- * @see {@link DevframeDockDefaults}
1991
+ * Names of RPC functions that have declared `jsonSerializable: true`.
1992
+ * Used by the WS / static client to dispatch the per-call wire
1993
+ * serializer (strict JSON for these methods, structured-clone for
1994
+ * the rest). Populated by the server / build adapter; absent on
1995
+ * legacy clients, in which case all outgoing messages fall back to
1996
+ * structured-clone.
1984
1997
  */
1985
- dock?: DevframeDockDefaults;
1998
+ jsonSerializableMethods?: string[];
1986
1999
  /**
1987
- * Mount path override. Defaults depend on the adapter:
1988
- * `/` for standalone (`cli` / `build`), `/__<id>/` for hosted
1989
- * (`vite` / `embedded`).
2000
+ * Absolute URL of the `__connection.json` this meta was loaded from.
2001
+ * Annotated by the client (not served) when it publishes the meta on a
2002
+ * shared window for same-origin inheritance: a relative `websocket.path`
2003
+ * resolves against this, so a child SPA mounted at another base (e.g. a hub
2004
+ * mounting several devframes at `/__foo/`, `/__bar/`, …) inherits a dialable
2005
+ * endpoint rather than resolving the path against its own mount.
1990
2006
  */
1991
- basePath?: string;
2007
+ baseUrl?: string;
1992
2008
  /**
1993
- * How a hub reacts when another devframe sharing this one's `id` is
1994
- * mounted onto the same hub. Consulted only by hub adapters
1995
- * (`ctx.install`); standalone adapters (`cli` / `build`)
1996
- * ignore it.
2009
+ * A pre-issued bearer token embedded in the meta so a client trusts the
2010
+ * server on connect without any prompt or `localStorage` lookup. Only a
2011
+ * **hub** serving a **per-frame** connection meta populates this — it
2012
+ * authenticates once at the top level and bakes the resulting token into
2013
+ * the meta each plugin iframe fetches, so a cross-origin frame (which
2014
+ * cannot read the hub's `localStorage`) is still pre-authorized. The
2015
+ * standalone `__connection.json` never carries a token.
1997
2016
  *
1998
- * @default 'warn'
2017
+ * > [!WARNING]
2018
+ * > A token in a fetchable JSON is only as protected as the URL serving
2019
+ * > it. Emit it exclusively from hub-controlled, per-frame meta — never
2020
+ * > from a publicly reachable static `__connection.json`.
1999
2021
  */
2000
- duplicationStrategy?: DevframeDuplicationStrategy;
2022
+ authToken?: string;
2001
2023
  /**
2002
- * Declares which runtimes meaningfully support this devframe. Adapters
2003
- * act on a `false` before doing any work:
2024
+ * Session-scoped token that lets a trusted external viewer register its
2025
+ * browser origin before opening the WebSocket. The host includes it in the
2026
+ * connection metadata, which the viewer obtains through the host page.
2004
2027
  *
2005
- * - `build: false` — `createCac` skips registering the `build` subcommand,
2006
- * and `createBuild` refuses (throws `DF0042`) unless `{ force: true }`.
2007
- * Useful for a devframe whose value is inherently live (e.g. it manages
2008
- * real files on disk), so a static export would only ever produce a
2009
- * broken, write-less shell of the tool.
2010
- * - `dev: false` — `createDevServer` refuses (throws `DF0058`) unless
2011
- * `{ force: true }`. Useful for a devframe that only makes sense as a
2012
- * static export (e.g. a report generator with nothing to serve live).
2028
+ * Treat this as a bearer credential. Keep connection metadata containing the
2029
+ * token same-origin until the requesting origin has been verified.
2013
2030
  */
2014
- capabilities?: {
2015
- dev?: boolean;
2016
- build?: boolean;
2017
- };
2031
+ viewerOriginToken?: string;
2018
2032
  /**
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.
2033
+ * Static, host-declared configuration — baked in once at connect time and
2034
+ * fixed for the life of the server (e.g. a hub's UI rebrand, or its
2035
+ * aggregated dock-bar layout preferences). Read-only from the browser: a
2036
+ * client only ever reads `rpc.connectionMeta.configs`, never writes to it.
2037
+ *
2038
+ * Contrast this with {@link DevframeSettingsRegistry} (`ctx.scope(ns).settings`)
2039
+ * and a hub's `devframe:user-settings` shared-state key — both are
2040
+ * mutable, user-editable, and synced bidirectionally over RPC for the
2041
+ * life of the session. `configs` is the opposite: one-way, immutable,
2042
+ * decided by whoever assembled the server.
2043
+ *
2044
+ * Each key is owned by one package, contributed via declaration merging:
2045
+ *
2046
+ * ```ts
2047
+ * declare module 'devframe/types' {
2048
+ * interface DevframeConnectionConfigsRegistry {
2049
+ * 'my-key': { some: 'shape' }
2050
+ * }
2051
+ * }
2052
+ * ```
2029
2053
  */
2030
- services?: DevframeServiceInput[];
2031
- /** Server-side setup — the primary entrypoint. Runs in every runtime. */
2032
- setup: (ctx: DevframeNodeContext, info?: DevframeSetupInfo) => void | Promise<void>;
2033
- cli?: DevframeCliOptions;
2054
+ configs?: Partial<DevframeConnectionConfigsRegistry>;
2034
2055
  }
2056
+ /**
2057
+ * Augmentation point for {@link ConnectionMeta.configs}. Empty by default —
2058
+ * a package that wants to publish static, boot-time config through the
2059
+ * connection handshake augments this interface with its own key (see
2060
+ * {@link ConnectionMeta.configs} for the pattern). `@devframes/hub`
2061
+ * augments it with `dock`; `@devframes/hub-ui` augments it with `ui`.
2062
+ */
2063
+ interface DevframeConnectionConfigsRegistry {}
2035
2064
  //#endregion
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 };
2065
+ export { DevframeServicesHost as $, SharedStateEvents as A, AgentResource as At, DevframeSetupInfo as B, EventUnsubscribe as Bt, createStreamSink as C, DevframeAuthHandler as Ct, ImmutableObject as D, parseCliFlags as Dt, ImmutableMap as E, defineCliFlags as Et, DevframeDefinition as F, AgentToolProvider as Ft, McpRouteOptions as G, DevframeSnapshotRpcInputs as H, DevframeDeploymentKind as I, AgentToolProviderHandle as It, DevframeServiceId as J, DevframeServiceDefinition as K, DevframeDockDefaults as L, DevframeAgentHost as Lt, SharedStatePatch as M, AgentResourceInput as Mt, createSharedState as N, AgentTool as Nt, ImmutableSet as O, AgentHandle as Ot, DevframeCliOptions as P, AgentToolInput as Pt, DevframeServiceScopeOf as Q, DevframeDuplicationStrategy as R, DevframeAgentHostEvents as Rt, createStreamReader as S, DevframeDiagnosticsLogger as St, ImmutableArray as T, InferCliFlags as Tt, DevframeSseOptions as U, DevframeSnapshotRpcEntry as V, EventsMap as Vt, DevframeWsOptions as W, DevframeServiceMeta as X, DevframeServiceInput as Y, DevframeServiceOf as Z, CreateStreamSinkOptions as _, DevframeRpcServerFunctions as _t, DevframeConnectionConfigsRegistry as a, DevframeScopedStreamingHost as at, StreamSink as b, DevframeStorageScope as bt, RpcBroadcastOptions as c, DevframeSettingsStore as ct, RpcSharedStateHost as d, ScopedRpcFn as dt, DevframeServicesRegistry as et, RpcStreamingChannel as f, ScopedServerFunctions as ft, CreateStreamReaderOptions as g, DevframeRpcClientFunctions as gt, BufferedChunk as h, DevframeViewHost as ht, DevframeCapabilities as i, DevframeScopedNodeRpc as it, SharedStateOptions as j, AgentResourceContent as jt, SharedState as k, AgentManifest as kt, RpcFunctionsHost as l, ScopedBroadcastOptions as lt, RpcStreamingHost as m, SettingsForNamespace as mt, ConnectionMetaSse as n, DevframeServicesState as nt, DevframeNodeContext as o, DevframeSettings as ot, RpcStreamingChannelOptions as p, ScopedSharedStates as pt, DevframeServiceDescriptor as q, ConnectionMetaWebsocket as r, DevframeScopedNodeContext as rt, DevframeNodeRpcSession as s, DevframeSettingsRegistry as st, ConnectionMeta as t, DevframeServicesScopeRegistry as tt, RpcSharedStateGetOptions as u, ScopedClientFunctions as ut, StreamErrorPayload as v, DevframeRpcSharedStates as vt, Immutable as w, CliFlagsSchema as wt, StreamSinkEvents as x, DevframeDiagnosticsHost as xt, StreamReader as y, DevframeHost as yt, DevframeRpcOptions as z, EventEmitter as zt };