ts-procedures 5.7.0 → 5.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +185 -29
  2. package/agent_config/claude-code/skills/guide/SKILL.md +1 -1
  3. package/agent_config/claude-code/skills/guide/api-reference.md +203 -3
  4. package/agent_config/claude-code/skills/guide/patterns.md +108 -0
  5. package/agent_config/copilot/copilot-instructions.md +87 -0
  6. package/agent_config/cursor/cursorrules +87 -0
  7. package/build/implementations/http/doc-registry.test.js +27 -1
  8. package/build/implementations/http/doc-registry.test.js.map +1 -1
  9. package/build/implementations/http/express-rpc/index.js +1 -0
  10. package/build/implementations/http/express-rpc/index.js.map +1 -1
  11. package/build/implementations/http/express-rpc/index.test.js +1 -1
  12. package/build/implementations/http/express-rpc/index.test.js.map +1 -1
  13. package/build/implementations/http/hono-api/index.js +2 -0
  14. package/build/implementations/http/hono-api/index.js.map +1 -1
  15. package/build/implementations/http/hono-api/index.test.js +9 -0
  16. package/build/implementations/http/hono-api/index.test.js.map +1 -1
  17. package/build/implementations/http/hono-rpc/index.js +1 -0
  18. package/build/implementations/http/hono-rpc/index.js.map +1 -1
  19. package/build/implementations/http/hono-rpc/index.test.js +1 -1
  20. package/build/implementations/http/hono-rpc/index.test.js.map +1 -1
  21. package/build/implementations/http/hono-stream/index.js +17 -1
  22. package/build/implementations/http/hono-stream/index.js.map +1 -1
  23. package/build/implementations/http/hono-stream/index.test.js +75 -6
  24. package/build/implementations/http/hono-stream/index.test.js.map +1 -1
  25. package/build/implementations/http/hono-stream/types.d.ts +4 -13
  26. package/build/implementations/types.d.ts +5 -0
  27. package/build/index.d.ts +2 -6
  28. package/build/index.js +8 -1
  29. package/build/index.js.map +1 -1
  30. package/build/index.test.js +4 -10
  31. package/build/index.test.js.map +1 -1
  32. package/package.json +21 -3
  33. package/src/client/call.ts +74 -0
  34. package/src/client/errors.ts +43 -0
  35. package/src/client/fetch-adapter.ts +191 -0
  36. package/src/client/hooks.ts +65 -0
  37. package/src/client/index.ts +121 -0
  38. package/src/client/request-builder.ts +73 -0
  39. package/src/client/stream.ts +164 -0
  40. package/src/client/types.ts +103 -0
package/README.md CHANGED
@@ -302,35 +302,6 @@ AJV is configured with:
302
302
 
303
303
  **Note:** `schema.params` is validated at runtime. `schema.returnType` is for documentation/introspection only.
304
304
 
305
- ### Skipping Validation with isPrevalidated
306
-
307
- When building framework integrations that validate params before calling procedure handlers, you can pass `isPrevalidated: true` in the context to skip duplicate validation:
308
-
309
- ```typescript
310
- // Framework integration example
311
- app.post('/rpc/:name', async (req, res) => {
312
- const procedure = getProcedure(req.params.name)
313
-
314
- // Validate params at the framework level
315
- const { errors } = procedure.config.validation?.params?.(req.body)
316
- if (errors) {
317
- return res.status(400).json({ errors })
318
- }
319
-
320
- // Call handler with isPrevalidated to skip redundant validation
321
- const result = await procedure.handler(
322
- { ...context, isPrevalidated: true },
323
- req.body
324
- )
325
- res.json(result)
326
- })
327
- ```
328
-
329
- This is useful for:
330
- - Framework integrations (like `HonoStreamAppBuilder`) that validate before starting streams
331
- - Custom middleware that performs early validation for better error responses
332
- - Performance optimization when validation has already occurred
333
-
334
305
  ## Streaming Procedures
335
306
 
336
307
  Streaming procedures use async generators to yield values over time, enabling SSE (Server-Sent Events), HTTP streaming, and real-time data feeds.
@@ -880,6 +851,191 @@ import type { RPCConfig, RPCHttpRouteDoc, StreamHttpRouteDoc, StreamMode, APICon
880
851
  // Hono API (REST-style)
881
852
  import { HonoAPIAppBuilder } from 'ts-procedures/hono-api'
882
853
  import type { APIConfig, APIHttpRouteDoc, APIInput, HttpMethod, QueryParser } from 'ts-procedures/hono-api'
854
+
855
+ // Client Runtime
856
+ import { createClient, createFetchAdapter } from 'ts-procedures/client'
857
+ import type { ClientAdapter, ClientHooks, TypedStream, ClientInstance } from 'ts-procedures/client'
858
+
859
+ // Code Generation
860
+ import { generateClient } from 'ts-procedures/codegen'
861
+ ```
862
+
863
+ ## Client Code Generation
864
+
865
+ 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.
866
+
867
+ ### Quick Start
868
+
869
+ **Step 1 — Serve your docs endpoint:**
870
+
871
+ ```typescript
872
+ app.get('/docs', (c) => c.json(docs.toJSON()))
873
+ ```
874
+
875
+ **Step 2 — Generate the client:**
876
+
877
+ ```bash
878
+ npx ts-procedures-codegen --url http://localhost:3000/docs --out ./src/generated/api
879
+ ```
880
+
881
+ **Step 3 — Use the client:**
882
+
883
+ ```typescript
884
+ import { createClient, createFetchAdapter } from 'ts-procedures/client'
885
+ import { createScopeBindings } from './generated/api'
886
+
887
+ const client = createClient({
888
+ adapter: createFetchAdapter(),
889
+ basePath: 'http://localhost:3000',
890
+ scopes: createScopeBindings,
891
+ hooks: {
892
+ onBeforeRequest(ctx) {
893
+ ctx.request.headers = { ...ctx.request.headers, Authorization: `Bearer ${getToken()}` }
894
+ return ctx
895
+ },
896
+ },
897
+ })
898
+
899
+ // Fully typed — params and response inferred from server schemas
900
+ const user = await client.users.GetUser({ pathParams: { id: '123' } })
901
+ ```
902
+
903
+ ### Generated File Structure
904
+
905
+ Running the codegen command produces one file per scope, plus shared error types and a barrel export:
906
+
907
+ ```
908
+ generated/
909
+ users.ts # Types + callables for "users" scope
910
+ billing.ts # Types + callables for "billing" scope
911
+ notifications.ts # Types + callables for stream procedures
912
+ _errors.ts # Typed error classes + ProcedureErrorUnion
913
+ index.ts # Barrel exports + createScopeBindings
914
+ ```
915
+
916
+ ### CLI Reference
917
+
918
+ | Flag | Description | Required |
919
+ |------|-------------|----------|
920
+ | `--url <url>` | Fetch DocEnvelope from URL | One of `--url` or `--file` |
921
+ | `--file <path>` | Read DocEnvelope from JSON file | One of `--url` or `--file` |
922
+ | `--out <dir>` | Output directory | Yes |
923
+ | `--watch` | Poll for changes and regenerate | No |
924
+ | `--interval <ms>` | Watch poll interval (default: 3000) | No |
925
+ | `--dry-run` | Preview without writing files | No |
926
+ | `--client-import-path <path>` | Override import path (default: `ts-procedures/client`) | No |
927
+ | `--namespace-types` | Wrap types in nested TypeScript namespaces per scope/route | No |
928
+ | `--config <path>` | Path to config file (default: `ts-procedures-codegen.config.json`) | No |
929
+ | `--enum-style <union\|enum>` | TypeScript enum style (requires `--namespace-types`) | No |
930
+ | `--depluralize` | Singularize array item type names (requires `--namespace-types`) | No |
931
+ | `--array-item-naming <value>` | Postfix for array item type names (requires `--namespace-types`) | No |
932
+ | `--uncountable-words <list>` | Comma-separated words to skip singularization (requires `--namespace-types`) | No |
933
+ | `--jsdoc` | Emit JSDoc comments from JSON Schema descriptions (requires `--namespace-types`) | No |
934
+ | `--self-contained` | Emit `_types.ts` and `_client.ts` — no runtime dependency on `ts-procedures` | No |
935
+
936
+ > **Note:** ajsc formatting options (`--enum-style enum`, `--depluralize`, `--array-item-naming`, `--uncountable-words`, `--jsdoc`) only take effect with `--namespace-types`. In flat mode, all types are inlined and these options have no effect.
937
+ >
938
+ > You can also use a `ts-procedures-codegen.config.json` file in your project root instead of CLI flags. CLI flags override config values.
939
+
940
+ ### Adapter Interface
941
+
942
+ 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:
943
+
944
+ ```typescript
945
+ import { createFetchAdapter } from 'ts-procedures/client'
946
+
947
+ // Use the built-in fetch adapter
948
+ const adapter = createFetchAdapter({ headers: { 'X-API-Key': 'my-key' } })
949
+
950
+ // Or implement your own (e.g., for axios)
951
+ const axiosAdapter: ClientAdapter = {
952
+ async request({ url, method, headers, body }) {
953
+ const res = await axios({ url, method, headers, data: body })
954
+ return { status: res.status, headers: res.headers, body: res.data }
955
+ },
956
+ async stream({ url, method, headers, body }) {
957
+ // Return AsyncIterable of SSE events
958
+ },
959
+ }
960
+ ```
961
+
962
+ ### Hooks
963
+
964
+ 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.
965
+
966
+ ```typescript
967
+ // Global hooks (apply to all calls)
968
+ const client = createClient({
969
+ adapter,
970
+ basePath: 'http://localhost:3000',
971
+ scopes: createScopeBindings,
972
+ hooks: {
973
+ onBeforeRequest(ctx) { /* add auth headers */ return ctx },
974
+ onAfterResponse(ctx) { /* handle errors, logging */ },
975
+ onError(ctx) { /* error reporting */ },
976
+ },
977
+ })
978
+
979
+ // Per-procedure hook override
980
+ await client.users.GetUser({ pathParams: { id: '123' } }, {
981
+ onAfterResponse(ctx) {
982
+ const rateLimit = ctx.response.headers['x-rate-limit-remaining']
983
+ },
984
+ })
985
+ ```
986
+
987
+ ### Streaming
988
+
989
+ Stream procedures return a `TypedStream` — an async iterable for yield values, with a `.result` promise for the final return value:
990
+
991
+ ```typescript
992
+ const stream = client.events.WatchNotifications({ filter: 'all' })
993
+
994
+ for await (const event of stream) {
995
+ console.log(event) // typed as WatchNotificationsYield
996
+ }
997
+
998
+ const result = await stream.result // typed as WatchNotificationsReturn
999
+ ```
1000
+
1001
+ ### Programmatic API
1002
+
1003
+ For build pipelines or custom tooling, `generateClient` can be called directly without the CLI:
1004
+
1005
+ ```typescript
1006
+ import { generateClient } from 'ts-procedures/codegen'
1007
+
1008
+ await generateClient({
1009
+ url: 'http://localhost:3000/docs',
1010
+ outDir: './src/generated/api',
1011
+ clientImportPath: '@my-app/procedures-client', // optional
1012
+ namespaceTypes: true, // optional — wrap types in nested namespaces
1013
+ selfContained: true, // optional — emit _types.ts + _client.ts (no ts-procedures runtime dep)
1014
+ dryRun: false, // optional
1015
+ ajsc: { // optional — ajsc TypescriptConverter options
1016
+ enumStyle: 'union',
1017
+ depluralize: true,
1018
+ arrayItemNaming: 'Item',
1019
+ uncountableWords: ['criteria'],
1020
+ },
1021
+ })
1022
+ ```
1023
+
1024
+ ### Self-Contained Mode
1025
+
1026
+ With `--self-contained`, the generated output includes two additional files in the output directory:
1027
+
1028
+ - **`_types.ts`** — All client type definitions (`ClientInstance`, `TypedStream`, `ProcedureCallOptions`, hooks, adapters, descriptors)
1029
+ - **`_client.ts`** — Full client runtime: `createClient`, `createFetchAdapter`, hook pipeline, and error classes (`ClientRequestError`, `ClientPathParamError`, `ClientStreamError`)
1030
+
1031
+ All generated scope files and `index.ts` import from `./_types` instead of `ts-procedures/client`, so app consumers can import everything from the generated directory without needing `ts-procedures` as a runtime dependency. `ts-procedures` becomes a devDependency only.
1032
+
1033
+ ```typescript
1034
+ // Without --self-contained (default)
1035
+ import { createClient, createFetchAdapter } from 'ts-procedures/client'
1036
+
1037
+ // With --self-contained
1038
+ import { createClient, createFetchAdapter } from './generated/_client'
883
1039
  ```
884
1040
 
885
1041
  ## AI Agent Setup
@@ -47,7 +47,7 @@ Handlers receive `(ctx, params)` where ctx includes:
47
47
  | Base context fields | `TContext` | Always |
48
48
  | `error(message, meta?)` | `(string, object?) => ProcedureError` | Always |
49
49
  | `signal` | `AbortSignal` | **Guaranteed** in CreateStream; optional in Create (present when HTTP impl provides it) |
50
- | `isPrevalidated` | `boolean` | Set by HTTP impls to skip redundant validation |
50
+
51
51
 
52
52
  ## Schema System
53
53
 
@@ -110,7 +110,7 @@ function Create<TName extends string, TParams, TReturnType>(
110
110
  }
111
111
  } & TExtendedConfig,
112
112
  handler: (
113
- ctx: TContext & TLocalContext & { isPrevalidated?: boolean },
113
+ ctx: TContext & TLocalContext,
114
114
  params: TSchemaLib<TParams>
115
115
  ) => Promise<TSchemaLib<TReturnType>>
116
116
  ): {
@@ -140,7 +140,7 @@ function Create<TName extends string, TParams, TReturnType>(
140
140
  | `...TContext` | varies | All base context fields |
141
141
  | `error(message, meta?)` | `Function` | Creates `ProcedureError` — throw this for business logic errors |
142
142
  | `signal?` | `AbortSignal` | Present when HTTP implementation injects it (Express/Hono do this automatically) |
143
- | `isPrevalidated?` | `boolean` | `true` when HTTP impl already validated params — skips AJV validation |
143
+
144
144
 
145
145
  ### Return Value
146
146
 
@@ -198,7 +198,7 @@ function CreateStream<TName extends string, TParams, TYieldType, TReturnType = v
198
198
  validateYields?: boolean // Default: false
199
199
  } & TExtendedConfig,
200
200
  handler: (
201
- ctx: TContext & TStreamContext & { isPrevalidated?: boolean },
201
+ ctx: TContext & TStreamContext,
202
202
  params: TSchemaLib<TParams>
203
203
  ) => AsyncGenerator<TSchemaLib<TYieldType>, TSchemaLib<TReturnType> | void, unknown>
204
204
  ): {
@@ -751,6 +751,206 @@ type AnyHttpRouteDoc = RPCHttpRouteDoc | APIHttpRouteDoc | StreamHttpRouteDoc
751
751
 
752
752
  ---
753
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> [--namespace-types] [--dry-run]
849
+
850
+ // Programmatic
851
+ async function generateClient(options: {
852
+ url?: string
853
+ file?: string
854
+ envelope?: DocEnvelope
855
+ outDir: string
856
+ ajsc?: AjscOptions
857
+ clientImportPath?: string
858
+ dryRun?: boolean
859
+ namespaceTypes?: boolean
860
+ selfContained?: boolean
861
+ }): Promise<GeneratedFile[]>
862
+ ```
863
+
864
+ ### CLI Options
865
+
866
+ | Flag | Description |
867
+ |------|-------------|
868
+ | `--url <url>` | URL to fetch the `DocEnvelope` JSON from (e.g., `http://localhost:3000/docs`) |
869
+ | `--file <path>` | Read DocEnvelope from a local JSON file |
870
+ | `--out <dir>` | Output directory for generated files |
871
+ | `--watch` | Poll for changes and regenerate |
872
+ | `--interval <ms>` | Watch poll interval (default: 3000) |
873
+ | `--dry-run` | Preview without writing files |
874
+ | `--namespace-types` | Wrap types in nested TS namespaces (`Scope.Route.Params`) |
875
+ | `--config <path>` | Path to config file (default: `ts-procedures-codegen.config.json`) |
876
+ | `--client-import-path <path>` | Override import path (default: `ts-procedures/client`) |
877
+ | `--enum-style <union\|enum>` | TypeScript enum style (requires `--namespace-types`) |
878
+ | `--depluralize` | Singularize array item type names (requires `--namespace-types`) |
879
+ | `--array-item-naming <value>` | Postfix for array item type names (requires `--namespace-types`) |
880
+ | `--uncountable-words <list>` | Comma-separated words to skip singularization (requires `--namespace-types`) |
881
+ | `--jsdoc` | Emit JSDoc comments from schema descriptions (requires `--namespace-types`) |
882
+ | `--self-contained` | Emit `_types.ts` and `_client.ts` in output dir — no runtime dependency on `ts-procedures` |
883
+
884
+ ### Generated Output
885
+
886
+ - One `.ts` file per scope (e.g., `users.ts`, `events.ts`)
887
+ - A root `index.ts` exporting `createScopeBindings`
888
+ - Each scope file exports fully-typed callable functions and TypeScript types
889
+ - With `--namespace-types`: types are wrapped in `export namespace Scope { export namespace Route { ... } }`
890
+
891
+ ### Example
892
+
893
+ ```typescript
894
+ // CLI
895
+ // npx ts-procedures-codegen --url http://localhost:3000/docs --out ./src/generated/api --namespace-types
896
+
897
+ // Programmatic
898
+ import { generateClient } from 'ts-procedures/codegen'
899
+
900
+ await generateClient({
901
+ url: 'http://localhost:3000/docs',
902
+ outDir: './src/generated/api',
903
+ namespaceTypes: true,
904
+ selfContained: true, // emit _types.ts + _client.ts (no runtime dep)
905
+ ajsc: { depluralize: true, arrayItemNaming: 'Item' },
906
+ })
907
+ ```
908
+
909
+ ---
910
+
911
+ ## TypedStream\<TYield, TReturn\>
912
+
913
+ 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.
914
+
915
+ ```typescript
916
+ interface TypedStream<TYield, TReturn> extends AsyncIterable<TYield> {
917
+ result: Promise<TReturn>
918
+ [Symbol.asyncIterator](): AsyncIterator<TYield>
919
+ }
920
+ ```
921
+
922
+ ### Type Parameters
923
+
924
+ - `TYield` — Type of each yielded value (from `schema.yieldType`)
925
+ - `TReturn` — Type of the stream's return value (from `schema.returnType`). Sent as `event: 'return'` SSE message by `HonoStreamAppBuilder`.
926
+
927
+ ### Key Behavior
928
+
929
+ - Iterating with `for await...of` yields each SSE data event as `TYield`
930
+ - `stream.result` resolves when the stream closes with the final return value
931
+ - If the stream has no return value, `TReturn` is `void` and `result` resolves to `undefined`
932
+
933
+ ### Example
934
+
935
+ ```typescript
936
+ import { createClient, createFetchAdapter } from 'ts-procedures/client'
937
+ import { createScopeBindings } from './generated/api'
938
+
939
+ const client = createClient({ adapter: createFetchAdapter(), basePath: '...', scopes: createScopeBindings })
940
+
941
+ const stream = client.events.WatchNotifications({ filter: 'all' })
942
+
943
+ // Consume yielded values
944
+ for await (const event of stream) {
945
+ console.log(event) // Typed as WatchNotificationsYield
946
+ }
947
+
948
+ // Access return value after iteration completes
949
+ const summary = await stream.result // Typed as WatchNotificationsReturn
950
+ ```
951
+
952
+ ---
953
+
754
954
  ## Stack Utilities
755
955
 
756
956
  ### captureDefinitionInfo()
@@ -758,3 +758,111 @@ onRequestStart → factoryContext() → params validation
758
758
  → onStreamStart → handler yields → onStreamEnd → onRequestEnd
759
759
  → onMidStreamError (if throw) → onStreamEnd → onRequestEnd
760
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
+ // Optional: --namespace-types --self-contained --depluralize --enum-style union
784
+ ```
785
+
786
+ ---
787
+
788
+ ## Type-Safe Client Usage
789
+
790
+ ```typescript
791
+ import { createClient, createFetchAdapter } from 'ts-procedures/client'
792
+ import { createScopeBindings } from './generated/api'
793
+
794
+ const client = createClient({
795
+ adapter: createFetchAdapter(),
796
+ basePath: 'http://localhost:3000',
797
+ scopes: createScopeBindings,
798
+ hooks: {
799
+ onBeforeRequest(ctx) {
800
+ ctx.request.headers = {
801
+ ...ctx.request.headers,
802
+ Authorization: `Bearer ${getToken()}`,
803
+ }
804
+ return ctx
805
+ },
806
+ onAfterResponse(ctx) {
807
+ if (ctx.response.status === 401) {
808
+ redirect('/login')
809
+ }
810
+ },
811
+ },
812
+ })
813
+
814
+ // Fully typed — params and response inferred from server schemas
815
+ const user = await client.users.GetUser({ pathParams: { id: '123' } })
816
+ ```
817
+
818
+ ---
819
+
820
+ ## Streaming Client Usage
821
+
822
+ ```typescript
823
+ const stream = client.events.WatchNotifications({ filter: 'all' })
824
+
825
+ for await (const event of stream) {
826
+ console.log(event) // Typed as WatchNotificationsYield
827
+ }
828
+
829
+ // Access the stream's return value
830
+ const result = await stream.result // Typed as WatchNotificationsReturn
831
+ ```
832
+
833
+ ---
834
+
835
+ ## Per-Procedure Hook Override
836
+
837
+ ```typescript
838
+ // Override hooks for a specific call
839
+ await client.users.GetUser({ pathParams: { id: '123' } }, {
840
+ onAfterResponse(ctx) {
841
+ const rateLimit = ctx.response.headers['x-rate-limit-remaining']
842
+ console.log(`Rate limit remaining: ${rateLimit}`)
843
+ },
844
+ })
845
+ ```
846
+
847
+ ---
848
+
849
+ ## Custom Client Adapter (Axios)
850
+
851
+ ```typescript
852
+ import type { ClientAdapter } from 'ts-procedures/client'
853
+ import axios from 'axios'
854
+
855
+ const axiosAdapter: ClientAdapter = {
856
+ async request({ url, method, headers, body, signal }) {
857
+ const res = await axios({ url, method, headers, data: body, signal })
858
+ return {
859
+ status: res.status,
860
+ headers: Object.fromEntries(Object.entries(res.headers).filter(([, v]) => typeof v === 'string')) as Record<string, string>,
861
+ body: res.data,
862
+ }
863
+ },
864
+ async stream() {
865
+ throw new Error('Streaming not supported with axios adapter')
866
+ },
867
+ }
868
+ ```
@@ -292,3 +292,90 @@ describe('GetUser', () => {
292
292
  - `scope` can be string or string[] (joined as path segments)
293
293
  - Procedure name auto-converted to kebab-case
294
294
  - Stream routes support both GET (query params) and POST (JSON body)
295
+
296
+ ## Client Code Generation
297
+
298
+ Generate type-safe client SDKs from a running DocRegistry endpoint.
299
+
300
+ ### Imports
301
+
302
+ ```typescript
303
+ // Client Runtime
304
+ import { createClient, createFetchAdapter } from 'ts-procedures/client'
305
+ import type { ClientAdapter, ClientHooks, TypedStream, ClientInstance } from 'ts-procedures/client'
306
+
307
+ // Code Generation (build-time only)
308
+ import { generateClient } from 'ts-procedures/codegen'
309
+ ```
310
+
311
+ ### CLI
312
+
313
+ ```bash
314
+ npx ts-procedures-codegen --url http://localhost:3000/docs --out ./src/generated/api
315
+ # Optional flags: --namespace-types --jsdoc --depluralize --enum-style union
316
+ # --array-item-naming Item --uncountable-words criteria,alumni
317
+ # --dry-run --watch --client-import-path @my-app/client
318
+ # --config ./codegen.config.json --self-contained
319
+ ```
320
+
321
+ Generates one `.ts` file per scope plus a root `index.ts` exporting `createScopeBindings`.
322
+ Use `--namespace-types` to wrap types in nested TS namespaces (`Scope.Route.Params` instead of flat `RouteParams`).
323
+ Note: ajsc formatting options (`--enum-style enum`, `--depluralize`, `--jsdoc`, etc.) only take effect with `--namespace-types`.
324
+ Supports config file: `ts-procedures-codegen.config.json` (auto-loaded from CWD) or `--config <path>`.
325
+
326
+ ### createClient Example
327
+
328
+ ```typescript
329
+ import { createClient, createFetchAdapter } from 'ts-procedures/client'
330
+ import { createScopeBindings } from './generated/api'
331
+
332
+ const client = createClient({
333
+ adapter: createFetchAdapter(),
334
+ basePath: 'http://localhost:3000',
335
+ scopes: createScopeBindings,
336
+ hooks: {
337
+ onBeforeRequest(ctx) {
338
+ ctx.request.headers = { ...ctx.request.headers, Authorization: `Bearer ${getToken()}` }
339
+ return ctx
340
+ },
341
+ onAfterResponse(ctx) {
342
+ if (ctx.response.status === 401) redirect('/login')
343
+ },
344
+ },
345
+ })
346
+
347
+ // Fully typed — params and response inferred from server schemas
348
+ const user = await client.users.GetUser({ pathParams: { id: '123' } })
349
+
350
+ // Per-call hook override
351
+ await client.users.GetUser({ pathParams: { id: '123' } }, {
352
+ onAfterResponse(ctx) {
353
+ console.log(ctx.response.headers['x-rate-limit-remaining'])
354
+ },
355
+ })
356
+ ```
357
+
358
+ ### Hook Types
359
+
360
+ ```typescript
361
+ interface ClientHooks {
362
+ onBeforeRequest?: (ctx: { request: ClientRequest }) => { request: ClientRequest } | void
363
+ onAfterResponse?: (ctx: { request: ClientRequest; response: ClientResponse }) => void
364
+ }
365
+ ```
366
+
367
+ ### TypedStream Usage
368
+
369
+ ```typescript
370
+ const stream = client.events.WatchNotifications({ filter: 'all' })
371
+
372
+ for await (const event of stream) {
373
+ console.log(event) // Typed as WatchNotificationsYield
374
+ }
375
+
376
+ const result = await stream.result // Typed as WatchNotificationsReturn
377
+ ```
378
+
379
+ - `TypedStream<TYield, TReturn>` extends `AsyncIterable<TYield>`
380
+ - `stream.result` resolves with the final return value sent as `event: 'return'` SSE message
381
+ - Stream procedures that return `void` have `result` resolve to `undefined`