ts-procedures 5.7.2 → 5.9.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 (50) hide show
  1. package/README.md +7 -1051
  2. package/agent_config/claude-code/skills/guide/api-reference.md +21 -16
  3. package/agent_config/claude-code/skills/guide/patterns.md +3 -1
  4. package/agent_config/copilot/copilot-instructions.md +7 -5
  5. package/agent_config/cursor/cursorrules +7 -5
  6. package/build/codegen/bin/cli.d.ts +2 -0
  7. package/build/codegen/bin/cli.js +21 -10
  8. package/build/codegen/bin/cli.js.map +1 -1
  9. package/build/codegen/bin/cli.test.js +44 -2
  10. package/build/codegen/bin/cli.test.js.map +1 -1
  11. package/build/codegen/emit-errors.d.ts +4 -1
  12. package/build/codegen/emit-errors.js +11 -5
  13. package/build/codegen/emit-errors.js.map +1 -1
  14. package/build/codegen/emit-errors.test.js +37 -0
  15. package/build/codegen/emit-errors.test.js.map +1 -1
  16. package/build/codegen/emit-index.d.ts +3 -1
  17. package/build/codegen/emit-index.js +6 -13
  18. package/build/codegen/emit-index.js.map +1 -1
  19. package/build/codegen/emit-index.test.js +23 -0
  20. package/build/codegen/emit-index.test.js.map +1 -1
  21. package/build/codegen/emit-scope.js +17 -13
  22. package/build/codegen/emit-scope.js.map +1 -1
  23. package/build/codegen/emit-scope.test.js +166 -0
  24. package/build/codegen/emit-scope.test.js.map +1 -1
  25. package/build/codegen/index.d.ts +1 -0
  26. package/build/codegen/index.js +1 -0
  27. package/build/codegen/index.js.map +1 -1
  28. package/build/codegen/naming.d.ts +7 -0
  29. package/build/codegen/naming.js +21 -0
  30. package/build/codegen/naming.js.map +1 -0
  31. package/build/codegen/naming.test.d.ts +1 -0
  32. package/build/codegen/naming.test.js +40 -0
  33. package/build/codegen/naming.test.js.map +1 -0
  34. package/build/codegen/pipeline.d.ts +1 -0
  35. package/build/codegen/pipeline.js +7 -3
  36. package/build/codegen/pipeline.js.map +1 -1
  37. package/build/codegen/pipeline.test.js +60 -0
  38. package/build/codegen/pipeline.test.js.map +1 -1
  39. package/docs/ai-agent-setup.md +61 -0
  40. package/docs/client-and-codegen.md +193 -0
  41. package/docs/core.md +473 -0
  42. package/docs/http-integrations.md +183 -0
  43. package/docs/streaming.md +199 -0
  44. package/docs/superpowers/plans/2026-03-30-client-codegen.md +2833 -0
  45. package/docs/superpowers/specs/2026-03-30-client-codegen-design.md +632 -0
  46. package/package.json +6 -1
  47. package/src/implementations/http/README.md +324 -0
  48. package/src/implementations/http/express-rpc/README.md +281 -0
  49. package/src/implementations/http/hono-rpc/README.md +358 -0
  50. package/src/implementations/http/hono-stream/README.md +525 -0
package/README.md CHANGED
@@ -40,1063 +40,19 @@ const user = await GetUser({}, { userId: '123' })
40
40
  const user2 = await procedure({}, { userId: '456' })
41
41
  ```
42
42
 
43
- ## Core Concepts
43
+ ## Features
44
44
 
45
- ### Procedures Factory
45
+ - **[Core Procedures](docs/core.md)** — Type-safe procedure definitions with `Procedures()`, `Create`, and `CreateStream`. Includes schema validation (Suretype / TypeBox), error handling, generics, testing patterns, and the full API reference.
46
46
 
47
- The `Procedures()` function creates a factory for defining procedures. It accepts two generic type parameters:
47
+ - **[Streaming](docs/streaming.md)** Async generator procedures with yield validation, abort signal integration, SSE examples, and stream error handling.
48
48
 
49
- ```typescript
50
- Procedures<TContext, TExtendedConfig>(builder?: {
51
- onCreate?: (procedure: TProcedureRegistration<TContext, TExtendedConfig>) => void
52
- })
53
- ```
54
-
55
- | Parameter | Description |
56
- |-----------|----------------------------------------------------------------------------|
57
- | `TContext` | The base context type passed to all handlers as the first parameter |
58
- | `TExtendedConfig` | Additional configuration properties for all procedures `config` properties |
59
- | `builder.onCreate` | Optional callback invoked when each procedure is registered (runtime) |
60
-
61
- ### Create Function
62
-
63
- The `Create` function defines individual procedures:
64
-
65
- ```typescript
66
- Create(name, config, handler)
67
- ```
68
-
69
- **Returns:**
70
- - `{ [name]: handler }` - Named export for the handler
71
- - `procedure` - Generic reference to the handler
72
- - `info` - Procedure meta (name, description, schema, `TExtendedConfig` properties, etc.)
73
-
74
- ### Structured Input with schema.input
75
-
76
- For HTTP APIs and other multi-channel transports, `schema.input` provides per-channel type safety. Each key is an independently validated input channel:
77
-
78
- ```typescript
79
- const { Create } = Procedures<AppContext, APIConfig>()
80
-
81
- const { UpdateUser } = Create(
82
- 'UpdateUser',
83
- {
84
- path: '/users/:id',
85
- method: 'put',
86
- schema: {
87
- input: {
88
- pathParams: Type.Object({ id: Type.String() }),
89
- query: Type.Object({ notify: Type.Optional(Type.Boolean()) }),
90
- body: Type.Object({ name: Type.String(), email: Type.String() }),
91
- },
92
- returnType: Type.Object({ ok: Type.Boolean() }),
93
- },
94
- },
95
- async (ctx, { pathParams, query, body }) => {
96
- // Each channel is independently typed and validated
97
- await updateUser(pathParams.id, body)
98
- if (query.notify) await sendNotification(pathParams.id)
99
- return { ok: true }
100
- }
101
- )
102
- ```
103
-
104
- **Rules:**
105
- - `schema.input` and `schema.params` are **mutually exclusive** — defining both throws `ProcedureRegistrationError`
106
- - Each channel is validated independently with per-channel error messages
107
- - Works with both `Create` and `CreateStream`
108
-
109
- ### CreateStream Function
110
-
111
- The `CreateStream` function defines streaming procedures that yield values over time using async generators:
112
-
113
- ```typescript
114
- CreateStream(name, config, handler)
115
- ```
116
-
117
- **Config Options:**
118
- - `schema.params` - Input parameter schema (validated at runtime)
119
- - `schema.yieldType` - Schema for each yielded value (validated if `validateYields: true`)
120
- - `schema.returnType` - Schema for final return value (documentation only)
121
- - `validateYields` - Enable runtime validation of yielded values (default: `false`)
122
-
123
- **Handler Signature:**
124
- ```typescript
125
- async function* (ctx, params) => AsyncGenerator<TYield, TReturn | void>
126
- ```
127
-
128
- **Context Extensions (all handlers):**
129
- - `ctx.error(message, meta?)` - Create a ProcedureError
130
- - `ctx.signal?` - AbortSignal for cancellation support (optional for `Create`, always present for `CreateStream`)
131
-
132
- When using the built-in HTTP implementations (Hono, Express), `ctx.signal` is automatically injected from the HTTP request, so handlers can detect client disconnection. For direct usage without a server, `signal` is `undefined` unless you pass one in context.
133
-
134
- **Returns:**
135
- - `{ [name]: handler }` - Named generator export
136
- - `procedure` - Generic reference to the generator
137
- - `info` - Procedure meta with `isStream: true`
138
-
139
- ## Using Generics
140
-
141
- ### Base Context
142
-
143
- Define a shared context type for all procedures in your application:
144
-
145
- ```typescript
146
- interface AppContext {
147
- authToken: string
148
- requestId: string
149
- logger: Logger
150
- }
151
-
152
- const { Create } = Procedures<AppContext>()
153
-
154
- const { SecureEndpoint } = Create(
155
- 'SecureEndpoint',
156
- {},
157
- async (ctx, params) => {
158
- // ctx.authToken is typed as string
159
- // ctx.requestId is typed as string
160
- // ctx.logger is typed as Logger
161
- return { token: ctx.authToken }
162
- },
163
- )
164
-
165
- // When calling, you must provide the context
166
- await SecureEndpoint({ authToken: 'abc', requestId: '123', logger: myLogger }, {})
167
- ```
168
-
169
- ### Extended Configuration
170
-
171
- Add custom properties to all procedure configs:
172
-
173
- ```typescript
174
- interface ExtendedConfig {
175
- permissions: string[]
176
- rateLimit?: number
177
- cacheTTL?: number
178
- }
179
-
180
- const { Create } = Procedures<AppContext, ExtendedConfig>()
181
-
182
- const { AdminOnly } = Create(
183
- 'AdminOnly',
184
- {
185
- permissions: ['admin'], // Required by ExtendedConfig
186
- rateLimit: 100, // Optional
187
- description: 'Admin-only endpoint',
188
- },
189
- async (ctx, params) => {
190
- return { admin: true }
191
- },
192
- )
193
-
194
- // Access extended config via info
195
- console.log(AdminOnly.info.permissions) // ['admin']
196
- ```
197
-
198
- ### Combined Example
199
-
200
- ```typescript
201
- interface CustomContext {
202
- authToken: string
203
- tenantId: string
204
- }
205
-
206
- interface ExtendedConfig {
207
- requiresAuth: boolean
208
- auditLog?: boolean
209
- }
210
-
211
- const { Create, getProcedures } = Procedures<CustomContext, ExtendedConfig>({
212
- onCreate: (procedure) => {
213
- // Register with your framework
214
- console.log(`Registered: ${procedure.name}`)
215
- console.log(`Requires Auth: ${procedure.config.requiresAuth}`)
216
- },
217
- })
218
-
219
- const { CreateUser } = Create(
220
- 'CreateUser',
221
- {
222
- requiresAuth: true,
223
- auditLog: true,
224
- description: 'Creates a new user',
225
- schema: {
226
- params: Type.Object({
227
- email: Type.String(),
228
- name: Type.String(),
229
- }),
230
- returnType: Type.Object({ id: Type.String() }),
231
- },
232
- },
233
- async (ctx, params) => {
234
- // Both context and params are fully typed
235
- return { id: 'user-123' }
236
- },
237
- )
238
- ```
239
-
240
- ## Schema Validation
241
-
242
- ### Suretype
243
-
244
- ```typescript
245
- import { v } from 'suretype'
246
-
247
- Create(
248
- 'CreatePost',
249
- {
250
- schema: {
251
- params: Type.Object({
252
- title: Type.String(),
253
- content: Type.String(),
254
- tags: Type.array(Type.String()),
255
- }),
256
- returnType: Type.Object({
257
- id: Type.String(),
258
- createdAt: Type.String(),
259
- }),
260
- },
261
- },
262
- async (ctx, params) => {
263
- // params typed as { title: string, content: string, tags?: string[] }
264
- return { id: '1', createdAt: new Date().toISOString() }
265
- },
266
- )
267
- ```
268
-
269
- ### TypeBox
270
-
271
- ```typescript
272
- import { Type } from 'typebox'
273
-
274
- Create(
275
- 'CreatePost',
276
- {
277
- schema: {
278
- params: Type.Object({
279
- title: Type.String(),
280
- content: Type.String(),
281
- tags: Type.Optional(Type.Array(Type.String())),
282
- }),
283
- returnType: Type.Object({
284
- id: Type.String(),
285
- createdAt: Type.String(),
286
- }),
287
- },
288
- },
289
- async (ctx, params) => {
290
- // params typed as { title: string, content: string, tags?: string[] }
291
- return { id: '1', createdAt: new Date().toISOString() }
292
- },
293
- )
294
- ```
295
-
296
- ### Validation Behavior
297
-
298
- AJV is configured with:
299
- - `allErrors: true` - Report all validation errors
300
- - `coerceTypes: true` - Automatically coerce types when possible
301
- - `removeAdditional: true` - Strip properties not in schema
302
-
303
- **Note:** `schema.params` is validated at runtime. `schema.returnType` is for documentation/introspection only.
304
-
305
- ## Streaming Procedures
306
-
307
- Streaming procedures use async generators to yield values over time, enabling SSE (Server-Sent Events), HTTP streaming, and real-time data feeds.
308
-
309
- ### Basic Streaming
310
-
311
- ```typescript
312
- import { Procedures } from 'ts-procedures'
313
- import { v } from 'suretype'
314
-
315
- const { CreateStream } = Procedures<{ userId: string }>()
316
-
317
- const { StreamUpdates } = CreateStream(
318
- 'StreamUpdates',
319
- {
320
- description: 'Stream real-time updates',
321
- schema: {
322
- params: v.object({ topic: v.string().required() }),
323
- yieldType: v.object({
324
- id: v.string().required(),
325
- message: v.string().required(),
326
- timestamp: v.number().required(),
327
- }),
328
- },
329
- },
330
- async function* (ctx, params) {
331
- // Types are inferred from schema:
332
- // - params.topic: string
333
- // - yield value must match { id, message, timestamp }
334
- // - ctx.signal: AbortSignal for cancellation
335
-
336
- let counter = 0
337
- while (!ctx.signal.aborted) {
338
- yield {
339
- id: `${counter++}`,
340
- message: `Update for ${params.topic}`,
341
- timestamp: Date.now(),
342
- }
343
- await new Promise(r => setTimeout(r, 1000))
344
- }
345
- },
346
- )
347
-
348
- // Consume the stream
349
- for await (const update of StreamUpdates({ userId: 'user-123' }, { topic: 'news' })) {
350
- console.log(update.message)
351
- }
352
- ```
353
-
354
- ### Yield Validation
355
-
356
- By default, yielded values are not validated for performance. Enable validation with `validateYields: true`:
357
-
358
- ```typescript
359
- const { ValidatedStream } = CreateStream(
360
- 'ValidatedStream',
361
- {
362
- schema: {
363
- yieldType: v.object({ count: v.number().required() }),
364
- },
365
- validateYields: true, // Enable runtime validation of each yield
366
- },
367
- async function* () {
368
- yield { count: 1 } // Valid
369
- yield { count: 2 } // Valid
370
- // yield { count: 'invalid' } // Would throw ProcedureYieldValidationError
371
- },
372
- )
373
- ```
374
-
375
- ### Abort Signal Integration
376
-
377
- #### Streaming Procedures
378
-
379
- The `ctx.signal` allows stream handlers to detect when consumers stop iterating. After completion, `signal.reason` indicates why the stream ended:
380
-
381
- ```typescript
382
- const { CancellableStream } = CreateStream(
383
- 'CancellableStream',
384
- {},
385
- async function* (ctx) {
386
- try {
387
- while (!ctx.signal.aborted) {
388
- yield await fetchNextItem()
389
- }
390
- } finally {
391
- // Distinguish normal completion from client disconnect
392
- if (ctx.signal.reason === 'stream-completed') {
393
- // Stream finished normally
394
- } else {
395
- // Client disconnected or external abort
396
- }
397
- await cleanup()
398
- }
399
- },
400
- )
401
-
402
- // Consumer can break early - signal.aborted becomes true
403
- for await (const item of CancellableStream({}, {})) {
404
- if (shouldStop) break // Triggers abort
405
- }
406
- ```
407
-
408
- #### Regular Procedures
409
-
410
- For regular procedures, `ctx.signal` is available when the server implementation provides it. The built-in HTTP integrations (Hono RPC, Express RPC) inject the request's abort signal automatically:
411
-
412
- ```typescript
413
- const { Create } = Procedures<{ signal: AbortSignal }>()
414
-
415
- const { LongQuery } = Create(
416
- 'LongQuery',
417
- {},
418
- async (ctx, params) => {
419
- // Pass signal to downstream operations
420
- const result = await fetch('https://api.example.com/data', {
421
- signal: ctx.signal,
422
- })
423
- return result.json()
424
- },
425
- )
426
- ```
427
-
428
- When using the Hono or Express implementations, `ctx.signal` aborts when the client disconnects, automatically cancelling in-flight `fetch()` calls, database queries, or any other signal-aware operation.
429
-
430
- ### SSE Integration Example
431
-
432
- ```typescript
433
- import express from 'express'
434
- import { Procedures } from 'ts-procedures'
435
-
436
- const app = express()
437
-
438
- const { CreateStream, getProcedures } = Procedures<{ req: express.Request }>({
439
- onCreate: (proc) => {
440
- if (proc.isStream) {
441
- // Register streaming procedures as SSE endpoints
442
- app.get(`/stream/${proc.name}`, async (req, res) => {
443
- res.writeHead(200, {
444
- 'Content-Type': 'text/event-stream',
445
- 'Cache-Control': 'no-cache',
446
- 'Connection': 'keep-alive',
447
- })
448
-
449
- const generator = proc.handler({ req }, req.query)
450
-
451
- req.on('close', async () => {
452
- // Client disconnected - stop the generator
453
- await generator.return(undefined)
454
- })
455
-
456
- try {
457
- for await (const data of generator) {
458
- res.write(`data: ${JSON.stringify(data)}\n\n`)
459
- }
460
- } finally {
461
- res.end()
462
- }
463
- })
464
- }
465
- },
466
- })
467
-
468
- // Define a streaming procedure
469
- CreateStream(
470
- 'LiveFeed',
471
- {
472
- schema: {
473
- params: v.object({ channel: v.string() }),
474
- yieldType: v.object({ event: v.string(), data: v.any() }),
475
- },
476
- },
477
- async function* (ctx, params) {
478
- while (!ctx.signal.aborted) {
479
- const event = await pollForEvent(params.channel)
480
- yield event
481
- }
482
- },
483
- )
484
-
485
- app.listen(3000)
486
- // SSE endpoint: GET /stream/LiveFeed?channel=updates
487
- ```
488
-
489
- ### Stream Errors
490
-
491
- Streaming procedures support the same error handling as regular procedures:
492
-
493
- ```typescript
494
- const { StreamWithErrors } = CreateStream(
495
- 'StreamWithErrors',
496
- {},
497
- async function* (ctx) {
498
- yield { status: 'starting' }
499
-
500
- const data = await fetchData()
501
- if (!data) {
502
- throw ctx.error('No data available', { code: 'NO_DATA' })
503
- }
504
-
505
- yield { status: 'complete', data }
506
- },
507
- )
508
-
509
- try {
510
- for await (const item of StreamWithErrors({}, {})) {
511
- console.log(item)
512
- }
513
- } catch (e) {
514
- if (e instanceof ProcedureError) {
515
- console.log(e.message) // 'No data available'
516
- console.log(e.meta) // { code: 'NO_DATA' }
517
- }
518
- }
519
- ```
520
-
521
- ## Error Handling
522
-
523
- ### Using ctx.error()
524
-
525
- The `error()` function is injected into both hooks and handlers:
526
-
527
- ```typescript
528
- Create(
529
- 'GetResource',
530
- {},
531
- async (ctx, params) => {
532
- const resource = await db.find(params.id)
533
- if (!resource) {
534
- throw ctx.error(404, 'Resource not found', { id: params.id })
535
- }
536
- return resource
537
- },
538
- )
539
- ```
540
-
541
- ### Error Handling
542
-
543
- | Error Class | Trigger |
544
- |-------------|---------|
545
- | ProcedureError | `ctx.error()` in handlers |
546
- | ProcedureValidationError | Schema validation failure (params) |
547
- | ProcedureYieldValidationError | Yield validation failure (streaming with `validateYields: true`) |
548
- | ProcedureRegistrationError | Invalid schema at registration |
549
-
550
- ### Error Properties
551
-
552
- ```typescript
553
- try {
554
- await MyProcedure(ctx, params)
555
- } catch (e) {
556
- if (e instanceof ProcedureError) {
557
- console.log(e.procedureName) // 'MyProcedure'
558
- console.log(e.message) // 'Resource not found'
559
- console.log(e.meta) // { id: '123' }
560
- }
561
- }
562
- ```
563
-
564
- ## Framework Integration
565
-
566
- ### onCreate Callback
567
-
568
- Register procedures with your framework (Express, Fastify, etc.):
49
+ - **[HTTP Integrations](docs/http-integrations.md)** — Express and Hono builders with lifecycle hooks, route documentation, `DocRegistry` for composing docs, and per-channel input validation.
569
50
 
570
- ```typescript
571
- import express from 'express'
572
-
573
- const app = express()
574
- const routes: Map<string, Function> = new Map()
575
-
576
- const { Create } = Procedures<{ req: Request; res: Response }>({
577
- onCreate: ({ name, handler, config }) => {
578
- // Register as Express route
579
- app.post(`/rpc/${name}`, async (req, res) => {
580
- try {
581
- const result = await handler({ req, res }, req.body)
582
- res.json(result)
583
- } catch (e) {
584
- if (e instanceof ProcedureError) {
585
- res.status(500).json({ error: e.message })
586
- } else {
587
- res.status(500).json({ error: 'Internal error' })
588
- }
589
- }
590
- })
591
- },
592
- })
593
-
594
- // Procedures are automatically registered as /rpc/GetUser, /rpc/CreateUser, etc.
595
- ```
596
-
597
- ### Express RPC Integration
598
-
599
- `ts-procedures` includes an RPC-style HTTP integration for Express that creates POST routes at `/rpc/{name}/{version}` paths with automatic JSON schema documentation.
600
-
601
- ```typescript
602
- import { ExpressRPCAppBuilder, RPCConfig } from 'ts-procedures/express-rpc'
603
-
604
- // Create procedure factory with RPC config
605
- const RPC = Procedures<AppContext, RPCConfig>()
606
-
607
- // Define procedures with name and version
608
- RPC.Create(
609
- 'GetUser',
610
- {
611
- name: ['users', 'get'],
612
- version: 1,
613
- schema: {
614
- params: Type.Object({ id: Type.String() }),
615
- returnType: Type.Object({ id: Type.String(), name: Type.String() }),
616
- },
617
- },
618
- async (ctx, params) => {
619
- return { id: params.id, name: 'John Doe' }
620
- }
621
- )
622
-
623
- // Build Express app with registered procedures
624
- const app = new ExpressRPCAppBuilder()
625
- .register(RPC, (req) => ({ userId: req.headers['x-user-id'] as string }))
626
- .build()
627
-
628
- app.listen(3000)
629
- // Route created: POST /rpc/users/get/1
630
- ```
631
-
632
- See [Express RPC Integration Guide](src/implementations/http/express-rpc/README.md) for complete setup instructions including lifecycle hooks, error handling, and route documentation.
633
-
634
- ### Hono API Integration
635
-
636
- `ts-procedures` includes a REST-style HTTP integration for Hono that routes by HTTP method with per-channel input validation via `schema.input`.
637
-
638
- ```typescript
639
- import { Procedures } from 'ts-procedures'
640
- import { HonoAPIAppBuilder } from 'ts-procedures/hono-api'
641
- import type { APIConfig } from 'ts-procedures/http'
642
- import { Type } from 'typebox'
643
-
644
- const API = Procedures<{ userId: string }, APIConfig>()
645
-
646
- API.Create('GetUser', {
647
- path: '/users/:id',
648
- method: 'get',
649
- schema: {
650
- input: {
651
- pathParams: Type.Object({ id: Type.String() }),
652
- },
653
- returnType: Type.Object({ id: Type.String(), name: Type.String() }),
654
- },
655
- }, async (ctx, { pathParams }) => {
656
- return await fetchUser(pathParams.id)
657
- })
658
-
659
- API.Create('CreateUser', {
660
- path: '/users',
661
- method: 'post',
662
- schema: {
663
- input: {
664
- body: Type.Object({ name: Type.String(), email: Type.String() }),
665
- },
666
- },
667
- }, async (ctx, { body }) => {
668
- return await createUser(body)
669
- })
670
-
671
- const app = await new HonoAPIAppBuilder({ pathPrefix: '/api' })
672
- .register(API, (c) => ({ userId: c.req.header('x-user-id') || 'anonymous' }))
673
- .build()
674
-
675
- // Routes:
676
- // GET /api/users/:id → 200
677
- // POST /api/users → 201
678
- ```
679
-
680
- See [Hono API Integration Guide](src/implementations/http/hono-api/) for complete setup.
681
-
682
- ### Introspection with getProcedures()
683
-
684
- Access all registered procedures for documentation or routing:
685
-
686
- ```typescript
687
- const { Create, getProcedures } = Procedures()
688
-
689
- Create('GetUser', { schema: { params: Type.Object({ id: Type.String() }) } }, async () => {})
690
- Create('ListUsers', { schema: { params: Type.Object({}) } }, async () => {})
691
-
692
- // Get all registered procedures
693
- const procedures = getProcedures()
694
-
695
- // Generate OpenAPI spec
696
- for (const config of procedures) {
697
- console.log(`${config.name}:`, config.schema)
698
- }
699
- ```
700
-
701
- ### DocRegistry — Composing Docs from Multiple Builders
702
-
703
- Use `DocRegistry` to compose route documentation from any combination of HTTP builders into a typed envelope:
704
-
705
- ```typescript
706
- import { DocRegistry } from 'ts-procedures/http-docs'
707
-
708
- const docs = new DocRegistry({
709
- basePath: '/api',
710
- headers: [{ name: 'Authorization', description: 'Bearer token', required: false }],
711
- errors: DocRegistry.defaultErrors(),
712
- })
713
- .from(rpcBuilder)
714
- .from(apiBuilder)
715
- .from(streamBuilder)
716
-
717
- app.get('/docs', (c) => c.json(docs.toJSON()))
718
- ```
719
-
720
- `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.
721
-
722
- ## Testing
723
-
724
- Procedures return handlers that can be called directly in tests:
725
-
726
- ```typescript
727
- import { describe, test, expect } from 'vitest'
728
- import { Procedures } from 'ts-procedures'
729
- import { Type } from 'typebox'
730
-
731
- interface MyCustomContext {
732
- userId?: string
733
- userName?: string
734
- }
735
-
736
- const { Create } = Procedures<MyCustomContext>()
737
-
738
- const { GetUser, info } = Create(
739
- 'GetUser',
740
- {
741
- schema: {
742
- params: Type.Object({ hideName: Type.Optional(Type.Boolean()) }),
743
- returnType: Type.Object({ id: Type.String(), name: Type.String() }),
744
- },
745
- },
746
- async (ctx, params) => {
747
- if (!params.userName || !ctx.userId) {
748
- throw ctx.error('User is not authenticated')
749
- }
750
-
751
- return {
752
- id: params.userId,
753
- name: params?.hideName ? '*******' : params.userName
754
- }
755
- },
756
- )
757
-
758
- describe('GetUser', () => {
759
- test('returns user', async () => {
760
- const result = await GetUser({userId:'123',userName:'Ray'}, { hideName: false })
761
- expect(result).toEqual({ id: '123', name: 'Ray' })
762
- })
763
-
764
- test('hides user name', async () => {
765
- const result = await GetUser({userId:'123',userName:'Ray'}, { hideName: true })
766
- expect(result).toEqual({ id: '123', name: '*******' })
767
- })
768
-
769
- test('validates params', async () => {
770
- await expect(GetUser({}, {})).rejects.toThrow(ProcedureValidationError)
771
- })
772
-
773
- test('has correct schema', () => {
774
- expect(info.schema.params).toEqual({
775
- type: 'object',
776
- properties: { id: { type: 'string' } },
777
- required: ['id'],
778
- })
779
- })
780
- })
781
- ```
782
-
783
- ## API Reference
784
-
785
- ### Procedures(builder?)
786
-
787
- Creates a procedure factory.
788
-
789
- **Parameters:**
790
- - `builder.onCreate` - Callback invoked when each procedure is registered
791
-
792
- **Returns:**
793
- - `Create` - Function to define procedures
794
- - `getProcedures()` - Returns `Array` of all registered procedures
795
-
796
- ### Create(name, config, handler)
797
-
798
- Defines a procedure.
799
-
800
- **Parameters:**
801
- - `name` - Unique procedure name (becomes named export)
802
- - `config.description` - Optional description
803
- - `config.schema.params` - Suretype or TypeBox schema for params (validated at runtime)
804
- - `config.schema.returnType` - Suretype or TypeBox schema for return returnType (documentation only)
805
- - Additional properties from `TExtendedConfig`
806
- - `handler` - Async function `(ctx, params) => Promise<returnType>`
807
-
808
- **Returns:**
809
- - `{ [name]: handler }` - Named handler export
810
- - `procedure` - Generic handler reference
811
- - `info` - Procedure metareturnType
812
-
813
- ### Type Exports
814
-
815
- ```typescript
816
- import {
817
- // Core
818
- Procedures,
819
-
820
- // Errors
821
- ProcedureError,
822
- ProcedureValidationError,
823
- ProcedureRegistrationError,
824
- ProcedureYieldValidationError, // For streaming yield validation
825
-
826
- // Types
827
- TLocalContext,
828
- TStreamContext, // Streaming context (AbortSignal always present)
829
- TProcedureRegistration,
830
- TStreamProcedureRegistration, // Streaming procedure registration
831
- TNoContextProvided,
832
-
833
- // Schema utilities
834
- extractJsonSchema,
835
- schemaParser,
836
- isTypeboxSchema,
837
- isSuretypeSchema,
838
-
839
- // Schema types
840
- TJSONSchema,
841
- TSchemaLib,
842
- TSchemaLibGenerator, // AsyncGenerator type utility
843
- TSchemaParsed,
844
- TSchemaValidationError,
845
- Prettify,
846
- } from 'ts-procedures'
847
-
848
- // HTTP types
849
- import type { RPCConfig, RPCHttpRouteDoc, StreamHttpRouteDoc, StreamMode, APIConfig, APIHttpRouteDoc, APIInput, HttpMethod } from 'ts-procedures/http'
51
+ - **[Client Code Generation](docs/client-and-codegen.md)** — Generate type-safe client SDKs from your server's `DocRegistry`. CLI and programmatic API, adapters, hooks, streaming support, and self-contained mode.
850
52
 
851
- // Hono API (REST-style)
852
- import { HonoAPIAppBuilder } from 'ts-procedures/hono-api'
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'
1039
- ```
1040
-
1041
- ## AI Agent Setup
1042
-
1043
- ts-procedures ships with built-in AI assistant configuration for **Claude Code**, **Cursor**, and **GitHub Copilot**. This gives AI tools framework-aware context when writing ts-procedures code in your project.
1044
-
1045
- ### Quick Setup
1046
-
1047
- ```bash
1048
- npx ts-procedures-setup
1049
- ```
1050
-
1051
- This installs rules for all supported AI tools. You can also target specific tools:
1052
-
1053
- ```bash
1054
- npx ts-procedures-setup claude # Claude Code only
1055
- npx ts-procedures-setup cursor # Cursor only
1056
- npx ts-procedures-setup copilot # GitHub Copilot only
1057
- ```
1058
-
1059
- ### What Gets Installed
1060
-
1061
- | Tool | Files | Auto-updates? |
1062
- |------|-------|---------------|
1063
- | **Claude Code** | `.claude/rules/ts-procedures.md`, `.claude/commands/ts-procedures-scaffold.md`, `.claude/commands/ts-procedures-review.md`, `.claude/agents/ts-procedures-architect.md` | Yes |
1064
- | **Cursor** | `.cursorrules` (marker-based section) | Yes |
1065
- | **GitHub Copilot** | `.github/copilot-instructions.md` (marker-based section) | Yes |
1066
-
1067
- ### Auto-Updates
1068
-
1069
- After initial setup, rules are automatically refreshed on every `npm install` or `npm update`. When ts-procedures publishes a new version, your AI tools get the latest framework guidance without any manual steps.
1070
-
1071
- ### Claude Code Features
1072
-
1073
- Once installed, Claude Code gets:
1074
-
1075
- - **Framework reference** — auto-loaded rules with core API, schema system, error handling, and decision framework
1076
- - **Scaffold command** — `/project:ts-procedures-scaffold <type> <Name>` generates procedures, streams, and HTTP setups with correct patterns
1077
- - **Review command** — `/project:ts-procedures-review <path>` checks code against a 60+ item checklist
1078
- - **Architecture agent** — `ts-procedures-architect` helps plan procedure structure, schema design, and HTTP implementation choices
1079
-
1080
- ### CLI Options
1081
-
1082
- ```bash
1083
- npx ts-procedures-setup --force # Overwrite without prompting
1084
- npx ts-procedures-setup --dry-run # Preview what would be created/updated
1085
- npx ts-procedures-setup --check # Exit with code 1 if files are outdated (for CI)
1086
- ```
1087
-
1088
- ### Gitignore
1089
-
1090
- The `.claude/` files are auto-generated and regenerated on `npm install`. You can add them to `.gitignore`:
1091
-
1092
- ```gitignore
1093
- # Auto-generated AI agent rules (regenerated on npm install)
1094
- .claude/rules/ts-procedures.md
1095
- .claude/commands/ts-procedures-*.md
1096
- .claude/agents/ts-procedures-*.md
1097
- ```
53
+ - **[AI Agent Setup](docs/ai-agent-setup.md)** — Built-in configuration for Claude Code, Cursor, and GitHub Copilot. Auto-updates on `npm install`.
1098
54
 
1099
- Cursor and Copilot files use marker-based sections that coexist with your own rules, so they should typically be committed.
55
+ Full documentation is available on [GitHub](https://github.com/thermsio/ts-procedures).
1100
56
 
1101
57
  ## License
1102
58