ts-procedures 5.4.1 → 5.6.0-beta.0

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 (135) hide show
  1. package/README.md +171 -0
  2. package/agent_config/claude-code/skills/guide/SKILL.md +1 -0
  3. package/agent_config/claude-code/skills/guide/anti-patterns.md +37 -4
  4. package/agent_config/claude-code/skills/guide/api-reference.md +263 -0
  5. package/agent_config/claude-code/skills/guide/patterns.md +140 -0
  6. package/agent_config/claude-code/skills/review/checklist.md +2 -0
  7. package/agent_config/claude-code/skills/scaffold/templates/hono-api.md +1 -1
  8. package/agent_config/copilot/copilot-instructions.md +84 -0
  9. package/agent_config/cursor/cursorrules +84 -0
  10. package/build/client/call.d.ts +14 -0
  11. package/build/client/call.js +47 -0
  12. package/build/client/call.js.map +1 -0
  13. package/build/client/call.test.d.ts +1 -0
  14. package/build/client/call.test.js +124 -0
  15. package/build/client/call.test.js.map +1 -0
  16. package/build/client/errors.d.ts +25 -0
  17. package/build/client/errors.js +33 -0
  18. package/build/client/errors.js.map +1 -0
  19. package/build/client/errors.test.d.ts +1 -0
  20. package/build/client/errors.test.js +41 -0
  21. package/build/client/errors.test.js.map +1 -0
  22. package/build/client/fetch-adapter.d.ts +12 -0
  23. package/build/client/fetch-adapter.js +156 -0
  24. package/build/client/fetch-adapter.js.map +1 -0
  25. package/build/client/fetch-adapter.test.d.ts +1 -0
  26. package/build/client/fetch-adapter.test.js +271 -0
  27. package/build/client/fetch-adapter.test.js.map +1 -0
  28. package/build/client/hooks.d.ts +17 -0
  29. package/build/client/hooks.js +40 -0
  30. package/build/client/hooks.js.map +1 -0
  31. package/build/client/hooks.test.d.ts +1 -0
  32. package/build/client/hooks.test.js +163 -0
  33. package/build/client/hooks.test.js.map +1 -0
  34. package/build/client/index.d.ts +22 -0
  35. package/build/client/index.js +67 -0
  36. package/build/client/index.js.map +1 -0
  37. package/build/client/index.test.d.ts +1 -0
  38. package/build/client/index.test.js +231 -0
  39. package/build/client/index.test.js.map +1 -0
  40. package/build/client/request-builder.d.ts +13 -0
  41. package/build/client/request-builder.js +53 -0
  42. package/build/client/request-builder.js.map +1 -0
  43. package/build/client/request-builder.test.d.ts +1 -0
  44. package/build/client/request-builder.test.js +160 -0
  45. package/build/client/request-builder.test.js.map +1 -0
  46. package/build/client/stream.d.ts +27 -0
  47. package/build/client/stream.js +118 -0
  48. package/build/client/stream.js.map +1 -0
  49. package/build/client/stream.test.d.ts +1 -0
  50. package/build/client/stream.test.js +228 -0
  51. package/build/client/stream.test.js.map +1 -0
  52. package/build/client/types.d.ts +78 -0
  53. package/build/client/types.js +3 -0
  54. package/build/client/types.js.map +1 -0
  55. package/build/codegen/bin/cli.d.ts +17 -0
  56. package/build/codegen/bin/cli.js +148 -0
  57. package/build/codegen/bin/cli.js.map +1 -0
  58. package/build/codegen/bin/cli.test.d.ts +1 -0
  59. package/build/codegen/bin/cli.test.js +83 -0
  60. package/build/codegen/bin/cli.test.js.map +1 -0
  61. package/build/codegen/e2e.test.d.ts +1 -0
  62. package/build/codegen/e2e.test.js +321 -0
  63. package/build/codegen/e2e.test.js.map +1 -0
  64. package/build/codegen/emit-errors.d.ts +9 -0
  65. package/build/codegen/emit-errors.js +30 -0
  66. package/build/codegen/emit-errors.js.map +1 -0
  67. package/build/codegen/emit-errors.test.d.ts +1 -0
  68. package/build/codegen/emit-errors.test.js +110 -0
  69. package/build/codegen/emit-errors.test.js.map +1 -0
  70. package/build/codegen/emit-index.d.ts +6 -0
  71. package/build/codegen/emit-index.js +49 -0
  72. package/build/codegen/emit-index.js.map +1 -0
  73. package/build/codegen/emit-index.test.d.ts +1 -0
  74. package/build/codegen/emit-index.test.js +83 -0
  75. package/build/codegen/emit-index.test.js.map +1 -0
  76. package/build/codegen/emit-scope.d.ts +6 -0
  77. package/build/codegen/emit-scope.js +194 -0
  78. package/build/codegen/emit-scope.js.map +1 -0
  79. package/build/codegen/emit-scope.test.d.ts +1 -0
  80. package/build/codegen/emit-scope.test.js +276 -0
  81. package/build/codegen/emit-scope.test.js.map +1 -0
  82. package/build/codegen/emit-types.d.ts +14 -0
  83. package/build/codegen/emit-types.js +40 -0
  84. package/build/codegen/emit-types.js.map +1 -0
  85. package/build/codegen/emit-types.test.d.ts +1 -0
  86. package/build/codegen/emit-types.test.js +82 -0
  87. package/build/codegen/emit-types.test.js.map +1 -0
  88. package/build/codegen/group-routes.d.ts +23 -0
  89. package/build/codegen/group-routes.js +46 -0
  90. package/build/codegen/group-routes.js.map +1 -0
  91. package/build/codegen/group-routes.test.d.ts +1 -0
  92. package/build/codegen/group-routes.test.js +131 -0
  93. package/build/codegen/group-routes.test.js.map +1 -0
  94. package/build/codegen/index.d.ts +11 -0
  95. package/build/codegen/index.js +13 -0
  96. package/build/codegen/index.js.map +1 -0
  97. package/build/codegen/pipeline.d.ts +14 -0
  98. package/build/codegen/pipeline.js +49 -0
  99. package/build/codegen/pipeline.js.map +1 -0
  100. package/build/codegen/pipeline.test.d.ts +1 -0
  101. package/build/codegen/pipeline.test.js +151 -0
  102. package/build/codegen/pipeline.test.js.map +1 -0
  103. package/build/codegen/resolve-envelope.d.ts +7 -0
  104. package/build/codegen/resolve-envelope.js +26 -0
  105. package/build/codegen/resolve-envelope.js.map +1 -0
  106. package/build/codegen/resolve-envelope.test.d.ts +1 -0
  107. package/build/codegen/resolve-envelope.test.js +69 -0
  108. package/build/codegen/resolve-envelope.test.js.map +1 -0
  109. package/build/implementations/http/doc-registry.d.ts +12 -0
  110. package/build/implementations/http/doc-registry.js +114 -0
  111. package/build/implementations/http/doc-registry.js.map +1 -0
  112. package/build/implementations/http/doc-registry.test.d.ts +1 -0
  113. package/build/implementations/http/doc-registry.test.js +347 -0
  114. package/build/implementations/http/doc-registry.test.js.map +1 -0
  115. package/build/implementations/http/express-rpc/index.js +1 -0
  116. package/build/implementations/http/express-rpc/index.js.map +1 -1
  117. package/build/implementations/http/express-rpc/index.test.js +1 -1
  118. package/build/implementations/http/express-rpc/index.test.js.map +1 -1
  119. package/build/implementations/http/hono-api/index.js +2 -0
  120. package/build/implementations/http/hono-api/index.js.map +1 -1
  121. package/build/implementations/http/hono-api/index.test.js +9 -0
  122. package/build/implementations/http/hono-api/index.test.js.map +1 -1
  123. package/build/implementations/http/hono-rpc/index.js +1 -0
  124. package/build/implementations/http/hono-rpc/index.js.map +1 -1
  125. package/build/implementations/http/hono-rpc/index.test.js +1 -1
  126. package/build/implementations/http/hono-rpc/index.test.js.map +1 -1
  127. package/build/implementations/http/hono-stream/index.js +17 -1
  128. package/build/implementations/http/hono-stream/index.js.map +1 -1
  129. package/build/implementations/http/hono-stream/index.test.js +61 -0
  130. package/build/implementations/http/hono-stream/index.test.js.map +1 -1
  131. package/build/implementations/http/hono-stream/types.d.ts +4 -13
  132. package/build/implementations/types.d.ts +36 -0
  133. package/build/index.js +8 -1
  134. package/build/index.js.map +1 -1
  135. package/package.json +16 -2
package/README.md CHANGED
@@ -727,6 +727,27 @@ for (const config of procedures) {
727
727
  }
728
728
  ```
729
729
 
730
+ ### DocRegistry — Composing Docs from Multiple Builders
731
+
732
+ Use `DocRegistry` to compose route documentation from any combination of HTTP builders into a typed envelope:
733
+
734
+ ```typescript
735
+ import { DocRegistry } from 'ts-procedures/http-docs'
736
+
737
+ const docs = new DocRegistry({
738
+ basePath: '/api',
739
+ headers: [{ name: 'Authorization', description: 'Bearer token', required: false }],
740
+ errors: DocRegistry.defaultErrors(),
741
+ })
742
+ .from(rpcBuilder)
743
+ .from(apiBuilder)
744
+ .from(streamBuilder)
745
+
746
+ app.get('/docs', (c) => c.json(docs.toJSON()))
747
+ ```
748
+
749
+ `from()` stores a reference — routes are read lazily at `toJSON()` time, so builders can be registered before or after `.build()`. Supports optional `filter` and `transform` options for customizing output.
750
+
730
751
  ## Testing
731
752
 
732
753
  Procedures return handlers that can be called directly in tests:
@@ -859,6 +880,156 @@ import type { RPCConfig, RPCHttpRouteDoc, StreamHttpRouteDoc, StreamMode, APICon
859
880
  // Hono API (REST-style)
860
881
  import { HonoAPIAppBuilder } from 'ts-procedures/hono-api'
861
882
  import type { APIConfig, APIHttpRouteDoc, APIInput, HttpMethod, QueryParser } from 'ts-procedures/hono-api'
883
+
884
+ // Client Runtime
885
+ import { createClient, createFetchAdapter } from 'ts-procedures/client'
886
+ import type { ClientAdapter, ClientHooks, TypedStream, ClientInstance } from 'ts-procedures/client'
887
+
888
+ // Code Generation
889
+ import { generateClient } from 'ts-procedures/codegen'
890
+ ```
891
+
892
+ ## Client Code Generation
893
+
894
+ ts-procedures can generate type-safe client SDKs directly from your server's `DocRegistry` output. Generated files include TypeScript types and callable functions for every registered procedure, organized by scope — no manual type duplication required.
895
+
896
+ ### Quick Start
897
+
898
+ **Step 1 — Serve your docs endpoint:**
899
+
900
+ ```typescript
901
+ app.get('/docs', (c) => c.json(docs.toJSON()))
902
+ ```
903
+
904
+ **Step 2 — Generate the client:**
905
+
906
+ ```bash
907
+ npx ts-procedures-codegen --url http://localhost:3000/docs --out ./src/generated/api
908
+ ```
909
+
910
+ **Step 3 — Use the client:**
911
+
912
+ ```typescript
913
+ import { createClient, createFetchAdapter } from 'ts-procedures/client'
914
+ import { createScopeBindings } from './generated/api'
915
+
916
+ const client = createClient({
917
+ adapter: createFetchAdapter(),
918
+ basePath: 'http://localhost:3000',
919
+ scopes: createScopeBindings,
920
+ hooks: {
921
+ onBeforeRequest(ctx) {
922
+ ctx.request.headers = { ...ctx.request.headers, Authorization: `Bearer ${getToken()}` }
923
+ return ctx
924
+ },
925
+ },
926
+ })
927
+
928
+ // Fully typed — params and response inferred from server schemas
929
+ const user = await client.users.GetUser({ pathParams: { id: '123' } })
930
+ ```
931
+
932
+ ### Generated File Structure
933
+
934
+ Running the codegen command produces one file per scope, plus shared error types and a barrel export:
935
+
936
+ ```
937
+ generated/
938
+ users.ts # Types + callables for "users" scope
939
+ billing.ts # Types + callables for "billing" scope
940
+ notifications.ts # Types + callables for stream procedures
941
+ _errors.ts # Typed error classes + ProcedureErrorUnion
942
+ index.ts # Barrel exports + createScopeBindings
943
+ ```
944
+
945
+ ### CLI Reference
946
+
947
+ | Flag | Description | Required |
948
+ |------|-------------|----------|
949
+ | `--url <url>` | Fetch DocEnvelope from URL | One of `--url` or `--file` |
950
+ | `--file <path>` | Read DocEnvelope from JSON file | One of `--url` or `--file` |
951
+ | `--out <dir>` | Output directory | Yes |
952
+ | `--watch` | Poll for changes and regenerate | No |
953
+ | `--interval <ms>` | Watch poll interval (default: 3000) | No |
954
+ | `--dry-run` | Preview without writing files | No |
955
+ | `--client-import-path <path>` | Override import path (default: `ts-procedures/client`) | No |
956
+ | `--enum-style <union\|enum>` | TypeScript enum style | No |
957
+ | `--depluralize` | Singularize array item type names | No |
958
+
959
+ ### Adapter Interface
960
+
961
+ The client requires an adapter that handles the actual HTTP transport. A built-in fetch adapter is included, and you can implement your own for any HTTP library:
962
+
963
+ ```typescript
964
+ import { createFetchAdapter } from 'ts-procedures/client'
965
+
966
+ // Use the built-in fetch adapter
967
+ const adapter = createFetchAdapter({ headers: { 'X-API-Key': 'my-key' } })
968
+
969
+ // Or implement your own (e.g., for axios)
970
+ const axiosAdapter: ClientAdapter = {
971
+ async request({ url, method, headers, body }) {
972
+ const res = await axios({ url, method, headers, data: body })
973
+ return { status: res.status, headers: res.headers, body: res.data }
974
+ },
975
+ async stream({ url, method, headers, body }) {
976
+ // Return AsyncIterable of SSE events
977
+ },
978
+ }
979
+ ```
980
+
981
+ ### Hooks
982
+
983
+ Hooks let you intercept requests and responses globally or per-procedure call. Global hooks apply to every call made through the client instance; per-procedure hooks override or extend them for a single invocation.
984
+
985
+ ```typescript
986
+ // Global hooks (apply to all calls)
987
+ const client = createClient({
988
+ adapter,
989
+ basePath: 'http://localhost:3000',
990
+ scopes: createScopeBindings,
991
+ hooks: {
992
+ onBeforeRequest(ctx) { /* add auth headers */ return ctx },
993
+ onAfterResponse(ctx) { /* handle errors, logging */ },
994
+ onError(ctx) { /* error reporting */ },
995
+ },
996
+ })
997
+
998
+ // Per-procedure hook override
999
+ await client.users.GetUser({ pathParams: { id: '123' } }, {
1000
+ onAfterResponse(ctx) {
1001
+ const rateLimit = ctx.response.headers['x-rate-limit-remaining']
1002
+ },
1003
+ })
1004
+ ```
1005
+
1006
+ ### Streaming
1007
+
1008
+ Stream procedures return a `TypedStream` — an async iterable for yield values, with a `.result` promise for the final return value:
1009
+
1010
+ ```typescript
1011
+ const stream = client.events.WatchNotifications({ filter: 'all' })
1012
+
1013
+ for await (const event of stream) {
1014
+ console.log(event) // typed as WatchNotificationsYield
1015
+ }
1016
+
1017
+ const result = await stream.result // typed as WatchNotificationsReturn
1018
+ ```
1019
+
1020
+ ### Programmatic API
1021
+
1022
+ For build pipelines or custom tooling, `generateClient` can be called directly without the CLI:
1023
+
1024
+ ```typescript
1025
+ import { generateClient } from 'ts-procedures/codegen'
1026
+
1027
+ await generateClient({
1028
+ url: 'http://localhost:3000/docs',
1029
+ outDir: './src/generated/api',
1030
+ clientImportPath: '@my-app/procedures-client', // optional
1031
+ dryRun: false, // optional
1032
+ })
862
1033
  ```
863
1034
 
864
1035
  ## AI Agent Setup
@@ -82,6 +82,7 @@ All errors include `definedAt` (file:line:column) and enhanced stack traces poin
82
82
  | `ExpressRPCAppBuilder` | `ts-procedures/express-rpc` | POST JSON |
83
83
  | `HonoRPCAppBuilder` | `ts-procedures/hono-rpc` | POST JSON |
84
84
  | `HonoStreamAppBuilder` | `ts-procedures/hono-stream` | SSE or newline-delimited JSON |
85
+ | `DocRegistry` | `ts-procedures/http-docs` | Compose route docs from multiple builders |
85
86
 
86
87
  ### Route Path Format
87
88
 
@@ -451,7 +451,40 @@ console.log(builder.docs)
451
451
 
452
452
  ---
453
453
 
454
- ## 16. Using Async Context Factory Without Error Handling
454
+ ## 16. Manually Wiring a /docs Endpoint Instead of Using DocRegistry
455
+
456
+ **Problem:** Building the docs JSON envelope by hand in every app — duplicating basePath, headers, errors, and route assembly.
457
+
458
+ ```typescript
459
+ // BAD — repeated boilerplate in every app
460
+ app.get('/docs', (c) => c.json({
461
+ basePath: '/api',
462
+ headers: [{ name: 'Authorization', description: 'Bearer token' }],
463
+ errors: [],
464
+ routes: [...rpcBuilder.docs, ...apiBuilder.docs, ...streamBuilder.docs],
465
+ }))
466
+ ```
467
+
468
+ **Fix:** Use `DocRegistry` to compose docs from builders.
469
+
470
+ ```typescript
471
+ // GOOD — declarative, composable, typed
472
+ import { DocRegistry } from 'ts-procedures/http-docs'
473
+
474
+ const docs = new DocRegistry({
475
+ basePath: '/api',
476
+ headers: [{ name: 'Authorization', description: 'Bearer token' }],
477
+ errors: DocRegistry.defaultErrors(),
478
+ }).from(rpcBuilder).from(apiBuilder).from(streamBuilder)
479
+
480
+ app.get('/docs', (c) => c.json(docs.toJSON()))
481
+ ```
482
+
483
+ **Why:** `DocRegistry` provides a typed `DocEnvelope`, includes `defaultErrors()` with JSON Schemas for all 4 procedure error types, reads docs lazily (order-independent), and supports filtering and transformation via `toJSON()` options.
484
+
485
+ ---
486
+
487
+ ## 17. Using Async Context Factory Without Error Handling
455
488
 
456
489
  **Problem:** Async context factories that throw unhandled errors, crashing the request.
457
490
 
@@ -480,7 +513,7 @@ new ExpressRPCAppBuilder({
480
513
 
481
514
  ---
482
515
 
483
- ## 17. Using Both schema.params and schema.input
516
+ ## 18. Using Both schema.params and schema.input
484
517
 
485
518
  **Problem:** Defining both `schema.params` and `schema.input` on the same procedure.
486
519
 
@@ -523,7 +556,7 @@ Create('GetUser', {
523
556
 
524
557
  ---
525
558
 
526
- ## 18. Mismatched Path Param Names in schema.input
559
+ ## 19. Mismatched Path Param Names in schema.input
527
560
 
528
561
  **Problem:** Path template param names don't match `schema.input.pathParams` property names.
529
562
 
@@ -559,7 +592,7 @@ Create('GetUser', {
559
592
 
560
593
  ---
561
594
 
562
- ## 19. Forgetting build() is Async on HonoAPIAppBuilder
595
+ ## 20. Forgetting build() is Async on HonoAPIAppBuilder
563
596
 
564
597
  **Problem:** Not awaiting `build()` on `HonoAPIAppBuilder`.
565
598
 
@@ -665,6 +665,269 @@ type HttpMethod = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'head'
665
665
 
666
666
  ---
667
667
 
668
+ ## DocRegistry
669
+
670
+ Composes route documentation from any number of HTTP builders into a typed envelope. Eliminates boilerplate `/docs` endpoint wiring.
671
+
672
+ ```typescript
673
+ import { DocRegistry } from 'ts-procedures/http-docs'
674
+ import type { DocEnvelope, DocRegistryConfig, DocRegistryOutputOptions, HeaderDoc, ErrorDoc, DocSource } from 'ts-procedures/http-docs'
675
+ ```
676
+
677
+ ```typescript
678
+ class DocRegistry {
679
+ constructor(config?: {
680
+ basePath?: string
681
+ headers?: HeaderDoc[]
682
+ errors?: ErrorDoc[]
683
+ })
684
+
685
+ from(source: DocSource<AnyHttpRouteDoc>): this
686
+
687
+ toJSON<T = DocEnvelope>(options?: {
688
+ filter?: (route: AnyHttpRouteDoc) => boolean
689
+ transform?: (envelope: DocEnvelope) => T
690
+ }): T
691
+
692
+ static defaultErrors(): ErrorDoc[]
693
+ }
694
+ ```
695
+
696
+ ### DocSource
697
+
698
+ Any object with a `readonly docs` array. All four HTTP builders (`HonoRPCAppBuilder`, `HonoAPIAppBuilder`, `HonoStreamAppBuilder`, `ExpressRPCAppBuilder`) satisfy this interface.
699
+
700
+ ```typescript
701
+ interface DocSource<T = AnyHttpRouteDoc> {
702
+ readonly docs: T[]
703
+ }
704
+ ```
705
+
706
+ ### HeaderDoc
707
+
708
+ ```typescript
709
+ interface HeaderDoc {
710
+ name: string
711
+ description?: string
712
+ required?: boolean
713
+ example?: string
714
+ }
715
+ ```
716
+
717
+ ### ErrorDoc
718
+
719
+ ```typescript
720
+ interface ErrorDoc {
721
+ name: string
722
+ statusCode: number
723
+ description: string
724
+ schema?: Record<string, unknown>
725
+ }
726
+ ```
727
+
728
+ ### DocEnvelope
729
+
730
+ ```typescript
731
+ interface DocEnvelope {
732
+ basePath: string
733
+ headers: HeaderDoc[]
734
+ errors: ErrorDoc[]
735
+ routes: AnyHttpRouteDoc[]
736
+ }
737
+ ```
738
+
739
+ ### AnyHttpRouteDoc
740
+
741
+ ```typescript
742
+ type AnyHttpRouteDoc = RPCHttpRouteDoc | APIHttpRouteDoc | StreamHttpRouteDoc
743
+ ```
744
+
745
+ ### Key Behaviors
746
+
747
+ - `from()` stores a **reference** to the builder — routes are read lazily at `toJSON()` time, so builders can be registered before or after `.build()`
748
+ - `toJSON()` collects routes via `flatMap(source => source.docs)`, applies optional `filter`, builds envelope, then applies optional `transform`
749
+ - `defaultErrors()` returns 4 `ErrorDoc` entries describing `ProcedureError` (500), `ProcedureValidationError` (400), `ProcedureYieldValidationError` (500), `ProcedureRegistrationError` (500) — each with a JSON Schema for the error response body shape
750
+ - `JSON.stringify(registry)` works directly (calls `toJSON()` implicitly)
751
+
752
+ ---
753
+
754
+ ## createClient(config)
755
+
756
+ Creates a typed client instance bound to generated scope bindings.
757
+
758
+ ```typescript
759
+ function createClient<TScopes>(config: {
760
+ adapter: ClientAdapter
761
+ basePath: string
762
+ scopes: (client: ClientInstance) => TScopes
763
+ hooks?: ClientHooks
764
+ }): TScopes
765
+ ```
766
+
767
+ ### Parameters
768
+
769
+ - `config.adapter` — Transport adapter implementing `ClientAdapter`. Use `createFetchAdapter()` for fetch-based transport.
770
+ - `config.basePath` — Base URL prepended to all request paths (e.g., `'http://localhost:3000'`).
771
+ - `config.scopes` — Factory function that receives a raw `ClientInstance` and returns the typed scope object. Typically the `createScopeBindings` export from generated code.
772
+ - `config.hooks` — Optional global hooks applied to every call. Can be overridden per call.
773
+
774
+ ### Return Value
775
+
776
+ Returns the result of `config.scopes(clientInstance)` — a typed object where each key is a scope and each value is a record of callable procedure functions.
777
+
778
+ ### ClientHooks
779
+
780
+ ```typescript
781
+ interface ClientHooks {
782
+ onBeforeRequest?: (ctx: { request: ClientRequest }) => { request: ClientRequest } | void
783
+ onAfterResponse?: (ctx: { request: ClientRequest; response: ClientResponse }) => void
784
+ }
785
+ ```
786
+
787
+ ### Example
788
+
789
+ ```typescript
790
+ import { createClient, createFetchAdapter } from 'ts-procedures/client'
791
+ import { createScopeBindings } from './generated/api'
792
+
793
+ const client = createClient({
794
+ adapter: createFetchAdapter(),
795
+ basePath: 'http://localhost:3000',
796
+ scopes: createScopeBindings,
797
+ hooks: {
798
+ onBeforeRequest(ctx) {
799
+ ctx.request.headers = { ...ctx.request.headers, Authorization: `Bearer ${getToken()}` }
800
+ return ctx
801
+ },
802
+ },
803
+ })
804
+
805
+ const user = await client.users.GetUser({ pathParams: { id: '123' } })
806
+ ```
807
+
808
+ ---
809
+
810
+ ## createFetchAdapter()
811
+
812
+ Creates a `ClientAdapter` using the global `fetch` API.
813
+
814
+ ```typescript
815
+ function createFetchAdapter(options?: {
816
+ fetch?: typeof globalThis.fetch
817
+ }): ClientAdapter
818
+ ```
819
+
820
+ ### Parameters
821
+
822
+ - `options.fetch` — Optional custom fetch implementation. Defaults to `globalThis.fetch`.
823
+
824
+ ### Return Value
825
+
826
+ A `ClientAdapter` that supports both request/response and SSE streaming.
827
+
828
+ ### Example
829
+
830
+ ```typescript
831
+ import { createFetchAdapter } from 'ts-procedures/client'
832
+
833
+ // Default (uses globalThis.fetch)
834
+ const adapter = createFetchAdapter()
835
+
836
+ // Custom fetch (e.g., node-fetch, undici)
837
+ const adapter = createFetchAdapter({ fetch: customFetch })
838
+ ```
839
+
840
+ ---
841
+
842
+ ## generateClient(options)
843
+
844
+ Build-time CLI and programmatic API for generating typed client files from a `DocEnvelope`.
845
+
846
+ ```typescript
847
+ // CLI usage (preferred)
848
+ // npx ts-procedures-codegen --url <url> --out <dir> [--fetch]
849
+
850
+ // Programmatic
851
+ async function generateClient(options: {
852
+ envelope: DocEnvelope
853
+ outDir: string
854
+ }): Promise<void>
855
+ ```
856
+
857
+ ### CLI Options
858
+
859
+ | Flag | Description |
860
+ |------|-------------|
861
+ | `--url <url>` | URL to fetch the `DocEnvelope` JSON from (e.g., `http://localhost:3000/docs`) |
862
+ | `--out <dir>` | Output directory for generated files |
863
+ | `--fetch` | Fetch the envelope at runtime rather than from a local file |
864
+
865
+ ### Generated Output
866
+
867
+ - One `.ts` file per scope (e.g., `users.ts`, `events.ts`)
868
+ - A root `index.ts` exporting `createScopeBindings`
869
+ - Each scope file exports fully-typed callable functions and TypeScript types
870
+
871
+ ### Example
872
+
873
+ ```typescript
874
+ // CLI
875
+ // npx ts-procedures-codegen --url http://localhost:3000/docs --out ./src/generated/api
876
+
877
+ // Programmatic
878
+ import { generateClient } from 'ts-procedures/codegen'
879
+
880
+ await generateClient({
881
+ envelope: await fetch('http://localhost:3000/docs').then(r => r.json()),
882
+ outDir: './src/generated/api',
883
+ })
884
+ ```
885
+
886
+ ---
887
+
888
+ ## TypedStream\<TYield, TReturn\>
889
+
890
+ Async iterable returned by stream procedure calls on the generated client. Yields typed values and exposes a `result` promise for the stream's final return value.
891
+
892
+ ```typescript
893
+ interface TypedStream<TYield, TReturn> extends AsyncIterable<TYield> {
894
+ result: Promise<TReturn>
895
+ [Symbol.asyncIterator](): AsyncIterator<TYield>
896
+ }
897
+ ```
898
+
899
+ ### Type Parameters
900
+
901
+ - `TYield` — Type of each yielded value (from `schema.yieldType`)
902
+ - `TReturn` — Type of the stream's return value (from `schema.returnType`). Sent as `event: 'return'` SSE message by `HonoStreamAppBuilder`.
903
+
904
+ ### Key Behavior
905
+
906
+ - Iterating with `for await...of` yields each SSE data event as `TYield`
907
+ - `stream.result` resolves when the stream closes with the final return value
908
+ - If the stream has no return value, `TReturn` is `void` and `result` resolves to `undefined`
909
+
910
+ ### Example
911
+
912
+ ```typescript
913
+ import { createClient, createFetchAdapter } from 'ts-procedures/client'
914
+ import { createScopeBindings } from './generated/api'
915
+
916
+ const client = createClient({ adapter: createFetchAdapter(), basePath: '...', scopes: createScopeBindings })
917
+
918
+ const stream = client.events.WatchNotifications({ filter: 'all' })
919
+
920
+ // Consume yielded values
921
+ for await (const event of stream) {
922
+ console.log(event) // Typed as WatchNotificationsYield
923
+ }
924
+
925
+ // Access return value after iteration completes
926
+ const summary = await stream.result // Typed as WatchNotificationsReturn
927
+ ```
928
+
929
+ ---
930
+
668
931
  ## Stack Utilities
669
932
 
670
933
  ### captureDefinitionInfo()
@@ -640,6 +640,39 @@ const openApiPaths = app.docs.map(doc => ({
640
640
 
641
641
  ---
642
642
 
643
+ ## DocRegistry — Composing Docs from Multiple Builders
644
+
645
+ ```typescript
646
+ import { DocRegistry } from 'ts-procedures/http-docs'
647
+
648
+ // Compose docs from any combination of builders
649
+ const docs = new DocRegistry({
650
+ basePath: '/api',
651
+ headers: [{ name: 'Authorization', description: 'Bearer token', required: false }],
652
+ errors: DocRegistry.defaultErrors(),
653
+ })
654
+ .from(rpcBuilder)
655
+ .from(apiBuilder)
656
+ .from(streamBuilder)
657
+
658
+ // Serve as a /docs endpoint
659
+ app.get('/docs', (c) => c.json(docs.toJSON()))
660
+
661
+ // With filtering and transformation
662
+ app.get('/docs', (c) => c.json(docs.toJSON({
663
+ filter: (route) => route.name !== 'InternalHealthCheck',
664
+ transform: (envelope) => ({ ...envelope, generatedAt: new Date().toISOString() }),
665
+ })))
666
+ ```
667
+
668
+ **Key points:**
669
+ - `from()` stores a reference — register builders before or after `.build()`
670
+ - `toJSON()` reads docs lazily, so late-registered procedures are included
671
+ - `DocRegistry.defaultErrors()` provides error schemas for all 4 procedure error types
672
+ - Accepts any object satisfying `{ readonly docs: AnyHttpRouteDoc[] }` — not limited to built-in builders
673
+
674
+ ---
675
+
643
676
  ## Testing Procedures
644
677
 
645
678
  ```typescript
@@ -725,3 +758,110 @@ onRequestStart → factoryContext() → params validation
725
758
  → onStreamStart → handler yields → onStreamEnd → onRequestEnd
726
759
  → onMidStreamError (if throw) → onStreamEnd → onRequestEnd
727
760
  ```
761
+
762
+ ---
763
+
764
+ ## Client Code Generation Setup
765
+
766
+ ```typescript
767
+ import { DocRegistry } from 'ts-procedures/http-docs'
768
+
769
+ // Server: serve docs endpoint
770
+ const docs = new DocRegistry({
771
+ basePath: '/api',
772
+ headers: [{ name: 'Authorization', description: 'Bearer token' }],
773
+ errors: DocRegistry.defaultErrors(),
774
+ })
775
+ .from(rpcBuilder)
776
+ .from(apiBuilder)
777
+ .from(streamBuilder)
778
+
779
+ app.get('/docs', (c) => c.json(docs.toJSON()))
780
+
781
+ // Then generate client:
782
+ // npx ts-procedures-codegen --url http://localhost:3000/docs --out ./src/generated/api
783
+ ```
784
+
785
+ ---
786
+
787
+ ## Type-Safe Client Usage
788
+
789
+ ```typescript
790
+ import { createClient, createFetchAdapter } from 'ts-procedures/client'
791
+ import { createScopeBindings } from './generated/api'
792
+
793
+ const client = createClient({
794
+ adapter: createFetchAdapter(),
795
+ basePath: 'http://localhost:3000',
796
+ scopes: createScopeBindings,
797
+ hooks: {
798
+ onBeforeRequest(ctx) {
799
+ ctx.request.headers = {
800
+ ...ctx.request.headers,
801
+ Authorization: `Bearer ${getToken()}`,
802
+ }
803
+ return ctx
804
+ },
805
+ onAfterResponse(ctx) {
806
+ if (ctx.response.status === 401) {
807
+ redirect('/login')
808
+ }
809
+ },
810
+ },
811
+ })
812
+
813
+ // Fully typed — params and response inferred from server schemas
814
+ const user = await client.users.GetUser({ pathParams: { id: '123' } })
815
+ ```
816
+
817
+ ---
818
+
819
+ ## Streaming Client Usage
820
+
821
+ ```typescript
822
+ const stream = client.events.WatchNotifications({ filter: 'all' })
823
+
824
+ for await (const event of stream) {
825
+ console.log(event) // Typed as WatchNotificationsYield
826
+ }
827
+
828
+ // Access the stream's return value
829
+ const result = await stream.result // Typed as WatchNotificationsReturn
830
+ ```
831
+
832
+ ---
833
+
834
+ ## Per-Procedure Hook Override
835
+
836
+ ```typescript
837
+ // Override hooks for a specific call
838
+ await client.users.GetUser({ pathParams: { id: '123' } }, {
839
+ onAfterResponse(ctx) {
840
+ const rateLimit = ctx.response.headers['x-rate-limit-remaining']
841
+ console.log(`Rate limit remaining: ${rateLimit}`)
842
+ },
843
+ })
844
+ ```
845
+
846
+ ---
847
+
848
+ ## Custom Client Adapter (Axios)
849
+
850
+ ```typescript
851
+ import type { ClientAdapter } from 'ts-procedures/client'
852
+ import axios from 'axios'
853
+
854
+ const axiosAdapter: ClientAdapter = {
855
+ async request({ url, method, headers, body, signal }) {
856
+ const res = await axios({ url, method, headers, data: body, signal })
857
+ return {
858
+ status: res.status,
859
+ headers: Object.fromEntries(Object.entries(res.headers).filter(([, v]) => typeof v === 'string')) as Record<string, string>,
860
+ body: res.data,
861
+ }
862
+ },
863
+ async stream() {
864
+ throw new Error('Streaming not supported with axios adapter')
865
+ },
866
+ }
867
+ ```
@@ -72,6 +72,7 @@
72
72
 
73
73
  ### SUGGESTION
74
74
  - [ ] Route documentation accessed via `builder.docs` for OpenAPI generation
75
+ - [ ] Uses `DocRegistry` to compose docs from multiple builders instead of manual envelope assembly
75
76
  - [ ] Lifecycle hooks used for observability (logging, metrics)
76
77
 
77
78
  ---
@@ -110,6 +111,7 @@
110
111
 
111
112
  ### SUGGESTION
112
113
  - [ ] Route documentation accessed via `builder.docs` for OpenAPI generation
114
+ - [ ] Uses `DocRegistry` to compose docs across builders instead of manual assembly
113
115
  - [ ] Custom `queryParser` provided if complex query string formats needed
114
116
  - [ ] Lifecycle hooks used for observability (logging, metrics)
115
117
 
@@ -113,7 +113,7 @@ export const {{name}}App = await new HonoAPIAppBuilder({
113
113
  // POST /api/{{name}} → 201
114
114
  // DELETE /api/{{name}}/:id → 204
115
115
 
116
- // Documentation: builder.docs (access before .build() resolves)
116
+ // Documentation: builder.docs or compose with DocRegistry from 'ts-procedures/http-docs'
117
117
  ```
118
118
 
119
119
  ## Test — `{{Name}}.api.test.ts`