ts-procedures 10.2.0 → 10.2.1

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 (24) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +9 -6
  3. package/agent_config/bin/postinstall.mjs +20 -89
  4. package/agent_config/bin/setup.mjs +51 -264
  5. package/agent_config/lib/install-skills.mjs +108 -0
  6. package/docs/ai-agent-setup.md +19 -27
  7. package/docs/client-error-handling.md +1 -1
  8. package/docs/http-integrations.md +1 -1
  9. package/package.json +1 -1
  10. package/agent_config/claude-code/.claude-plugin/plugin.json +0 -5
  11. package/agent_config/claude-code/agents/ts-procedures-architect.md +0 -138
  12. package/agent_config/copilot/copilot-instructions.md +0 -521
  13. package/agent_config/cursor/cursorrules +0 -521
  14. package/agent_config/lib/install-claude.mjs +0 -57
  15. /package/agent_config/{claude-code/skills → skills}/ts-procedures/SKILL.md +0 -0
  16. /package/agent_config/{claude-code/skills → skills}/ts-procedures/anti-patterns.md +0 -0
  17. /package/agent_config/{claude-code/skills → skills}/ts-procedures/api-reference.md +0 -0
  18. /package/agent_config/{claude-code/skills → skills}/ts-procedures/checklist.md +0 -0
  19. /package/agent_config/{claude-code/skills → skills}/ts-procedures/patterns.md +0 -0
  20. /package/agent_config/{claude-code/skills → skills}/ts-procedures/templates/astro-catchall.md +0 -0
  21. /package/agent_config/{claude-code/skills → skills}/ts-procedures/templates/client.md +0 -0
  22. /package/agent_config/{claude-code/skills → skills}/ts-procedures/templates/hono.md +0 -0
  23. /package/agent_config/{claude-code/skills → skills}/ts-procedures/templates/procedure.md +0 -0
  24. /package/agent_config/{claude-code/skills → skills}/ts-procedures/templates/stream-procedure.md +0 -0
@@ -1,521 +0,0 @@
1
- # ts-procedures Framework Reference
2
-
3
- You are assisting a developer using **ts-procedures**, a TypeScript RPC framework that creates type-safe, schema-validated procedure calls with a single function definition.
4
-
5
- ## Core Flow
6
-
7
- ```
8
- Procedures<TContext, TExtendedConfig>(options?)
9
-
10
- Create(name, config, handler) → Standard async procedure (kind: 'rpc')
11
- CreateStream(name, config, handler) → Streaming async generator (kind: 'rpc-stream')
12
- CreateHttp(name, config, handler) → REST-style HTTP route (kind: 'http')
13
- CreateHttpStream(name, config, handler) → REST-style streaming route (kind: 'http-stream')
14
-
15
- Returns: { [name]: handler, procedure: handler, info: metadata }
16
- ```
17
-
18
- Every registration and every creator's `info` carries the `kind` discriminant — branch on it (there is no `isStream` flag).
19
-
20
- ## Imports
21
-
22
- ```typescript
23
- // Core
24
- import { Procedures } from 'ts-procedures'
25
- import type { Infer, SchemaAdapter, TLocalContext, TStreamContext, TProcedureRegistration, TStreamProcedureRegistration } from 'ts-procedures'
26
-
27
- // Errors
28
- import { ProcedureError, ProcedureValidationError, ProcedureYieldValidationError, ProcedureRegistrationError } from 'ts-procedures'
29
-
30
- // HTTP types
31
- import type { RPCConfig, APIConfig, APIInput, RPCHttpRouteDoc, StreamHttpRouteDoc, StreamMode } from 'ts-procedures/http'
32
-
33
- // Hono (RPC, streaming, REST — one unified builder)
34
- import { HonoAppBuilder, sse, defineErrorTaxonomy } from 'ts-procedures/hono'
35
-
36
- // Doc Registry — compose route docs from multiple builders
37
- import { DocRegistry } from 'ts-procedures/http-docs'
38
- import type { DocEnvelope, HeaderDoc, ErrorDoc } from 'ts-procedures/http-docs'
39
-
40
- // Server toolkit — transport-agnostic building blocks for writing custom adapters (Fastify, Express, ...)
41
- // Also re-exports DocRegistry, writeDocEnvelope, defineErrorTaxonomy, sse, and the doc builders
42
- import { dispatchPreStreamError, dispatchMidStreamError, extractReqChannels, buildRpcRouteDoc, SseEventSequencer } from 'ts-procedures/server'
43
- import type { RequestSource } from 'ts-procedures/server'
44
- ```
45
-
46
- ## Architecture Rules
47
-
48
- 1. **schema.params is validated at runtime; schema.returnType is documentation only.** Never expect returnType to be validated. Use TypeScript for return type safety.
49
-
50
- 2. **Use ctx.error() for business logic errors.** Never throw raw `Error` instances. `ctx.error(message, meta?)` creates `ProcedureError` with procedure name, metadata, and enhanced stack trace.
51
-
52
- 3. **Pass ctx.signal to all downstream async calls.** HTTP implementations inject AbortSignal automatically. Pass it to fetch, database queries, and other async operations for cancellation support.
53
-
54
- 4. **Stream handlers always have ctx.signal (guaranteed AbortSignal).** Standard handlers get it when HTTP implementations provide it. Check `signal.reason === 'stream-completed'` to distinguish normal completion from client disconnect.
55
-
56
- 5. **Use TypeBox for schemas** (`import { Type } from 'typebox'`). Plain JSON Schema objects are not recognized. TypeBox is the built-in schema library; other libraries plug in via the `SchemaAdapter` interface (`Procedures({ schema: { adapters: [myAdapter] } })` — `{ name, detect, toJsonSchema }`). Custom adapters get runtime validation + docs only; compile-time `Infer<T>` inference is TypeBox-only. Default AJV config: `allErrors: true`, `coerceTypes: true`, `removeAdditional: true` — customize per factory via `Procedures({ validation: { ajv } })` (options merged over defaults, or a configured Ajv instance). The bundled TypeBox does **not** export `Type.Composite` — compose schemas with a flat property spread (`Type.Object({ ...Base.properties, name: Type.String() })`), which also keeps the generated JSON Schema a single `object` instead of an `allOf`.
57
-
58
- 6. **Prefer middleware + layered context for access levels.** Use `HonoAppBuilder` `middleware`, `.use()`, or per-`register({ middleware, context })` for tenant/auth. Separate factories are still valid for truly disjoint `TContext` shapes.
59
-
60
- 7. **onCreate callback enables framework integration.** Use for route registration, OpenAPI generation, logging.
61
-
62
- 8. **getProcedures() for introspection.** Returns all registered procedures with metadata.
63
-
64
- 9. **`Procedures({ validation: false })` skips per-call AJV validation** for every procedure on the factory (all four creators, both `schema.params` and `schema.req`). JSON Schema and validators are still computed at registration time (bad schemas still fail fast), so codegen and `info.schema` are unaffected; only the per-call validator runs are bypassed. **Never enable this on a factory whose handlers are reachable from public/untrusted callers** — coerceTypes and removeAdditional also stop running. Reserve for trusted internal factories whose callers are already type-checked at build time. Independent of the per-call `ctx.isPrevalidated` escape hatch used by HTTP builders.
65
-
66
- 10. **`Procedures({ http: { pathPrefix, scope } })` sets factory-level defaults for `CreateHttp` / `CreateHttpStream`.** `pathPrefix` is prepended to every route's `path` at registration time — it becomes part of the route's identity (shows up in `config.path` and route docs), unlike the builder-level `pathPrefix`, which is a deployment mount point. `scope` is the default codegen scope for routes that don't set one; per-route values win.
67
-
68
- ## Procedure Pattern
69
-
70
- ```typescript
71
- import { Procedures } from 'ts-procedures'
72
- import { Type } from 'typebox'
73
-
74
- type AppContext = { userId: string; signal?: AbortSignal }
75
-
76
- const { Create, CreateStream } = Procedures<AppContext>()
77
-
78
- // Standard procedure
79
- const { GetUser } = Create(
80
- 'GetUser',
81
- {
82
- description: 'Fetch user by ID',
83
- schema: {
84
- params: Type.Object({ id: Type.String() }),
85
- returnType: Type.Object({ id: Type.String(), name: Type.String() }),
86
- },
87
- },
88
- async (ctx, params) => {
89
- // params.id guaranteed string (AJV validated)
90
- const user = await fetchUser(params.id, { signal: ctx.signal })
91
- if (!user) throw ctx.error('Not found', { code: 'NOT_FOUND' })
92
- return user
93
- }
94
- )
95
- ```
96
-
97
- ## Stream Procedure Pattern
98
-
99
- ```typescript
100
- const { StreamEvents } = CreateStream(
101
- 'StreamEvents',
102
- {
103
- schema: {
104
- params: Type.Object({ channel: Type.String() }),
105
- yieldType: Type.Object({ type: Type.String(), data: Type.Any() }),
106
- },
107
- },
108
- async function* (ctx, params) {
109
- // ctx.signal always present in streams
110
- while (!ctx.signal.aborted) {
111
- const event = await pollEvents(params.channel, { signal: ctx.signal })
112
- yield event
113
- }
114
- }
115
- )
116
- ```
117
-
118
- ## Hono RPC Pattern
119
-
120
- ```typescript
121
- import { HonoAppBuilder } from 'ts-procedures/hono'
122
-
123
- const app = new HonoAppBuilder({ pathPrefix: '/api' })
124
- .register(RPC, (c) => ({ userId: c.req.header('x-user-id') }))
125
- .build()
126
- ```
127
-
128
- ## Hono Streaming Pattern
129
-
130
- ```typescript
131
- import { HonoAppBuilder, sse } from 'ts-procedures/hono'
132
-
133
- const StreamRPC = Procedures<AppContext, RPCConfig>()
134
-
135
- StreamRPC.CreateStream('Feed', {
136
- scope: 'events', version: 1,
137
- schema: { params: Type.Object({ channel: Type.String() }) },
138
- }, async function* (ctx, params) {
139
- while (!ctx.signal.aborted) {
140
- const event = await poll({ signal: ctx.signal })
141
- yield sse(event, { event: 'update' })
142
- }
143
- })
144
-
145
- const app = new HonoAppBuilder({ stream: { defaultStreamMode: 'sse' } })
146
- .register(StreamRPC, (c) => ({ userId: c.req.header('x-user-id') }))
147
- .build()
148
- // GET|POST /events/feed/1
149
- ```
150
-
151
- SSE wire behavior is identical for `rpc-stream` and `http-stream` routes (one shared `SseEventSequencer`): `sse(data, { event, id, retry })` metadata is honored on yields and error events of both kinds, and a `null` yield serializes as empty SSE data on both.
152
-
153
- ## Hono API Pattern (REST-style) — `CreateHttp`
154
-
155
- HTTP routes use `CreateHttp` (not `Create + APIConfig`). Request channels live under `schema.req` (`pathParams`, `query`, `body`, `headers`); response docs under `schema.res`. The factory needs no `APIConfig` type parameter.
156
-
157
- ```typescript
158
- import { HonoAppBuilder } from 'ts-procedures/hono'
159
-
160
- const API = Procedures<AppContext>()
161
-
162
- API.CreateHttp('GetUser', {
163
- path: '/users/:id', method: 'get',
164
- schema: {
165
- req: { pathParams: Type.Object({ id: Type.String() }) },
166
- res: { body: Type.Object({ id: Type.String(), name: Type.String() }) },
167
- },
168
- }, async (ctx, { pathParams }) => fetchUser(pathParams.id))
169
-
170
- API.CreateHttp('CreateUser', {
171
- path: '/users', method: 'post',
172
- schema: {
173
- req: { body: Type.Object({ name: Type.String() }) },
174
- },
175
- }, async (ctx, { body }) => createUser(body))
176
-
177
- const app = new HonoAppBuilder({ pathPrefix: '/api' })
178
- .register(API, (c) => ({ userId: c.req.header('x-user-id') }))
179
- .build()
180
- // GET /api/users/:id → 200, POST /api/users → 201
181
- ```
182
-
183
- Streaming HTTP routes use `CreateHttpStream` with `schema.yield` (and optional `schema.res.headers`) — both schemas flow into route docs and generated client types.
184
-
185
- **v8 → v9 migration:**
186
-
187
- 1. `Procedures({ config: { noRuntimeValidation: true } })` → `Procedures({ validation: false })`
188
- 2. Suretype schemas → TypeBox (or a custom `SchemaAdapter`); `isSuretypeSchema` removed
189
- 3. `registration.isStream` → branch on the `kind` discriminant (`'rpc' | 'rpc-stream' | 'http' | 'http-stream'`)
190
- 4. Duplicate-name registration now throws `ProcedureRegistrationError` instead of a bare `Error` (same message)
191
- 5. `schemaParser` removed — use `computeSchema(name, schema, { adapters, compile })`; `extractJsonSchema(schema, adapters)` takes the adapter list explicitly
192
- 6. Codegen CLI/options/output unchanged — regenerated clients are byte-identical to v8.6.0
193
-
194
- ## Astro adapter
195
-
196
- Catch-all endpoint pattern. Build Hono apps once with `HonoAppBuilder`, mount via `createAstroHandler` in `src/pages/api/[...rest].ts` with `pathPrefix: '/api'`. Read Astro context inside factory closures with `getAstroContext(c)`. Multi-app dispatch is first-non-404-wins.
197
-
198
- ## Error Handling
199
-
200
- | Error Class | Trigger | HTTP Status |
201
- |-------------|---------|-------------|
202
- | `ProcedureValidationError` | Schema params validation failure | 400 (auto — default taxonomy) |
203
- | `ProcedureError` | `ctx.error()` or unhandled handler exception | 500 (auto — default taxonomy) |
204
- | `ProcedureYieldValidationError` | Yield validation failure (validateYields: true) | N/A (mid-stream) |
205
- | `ProcedureRegistrationError` | Invalid schema or duplicate name at registration | N/A (startup) |
206
-
207
- Every framework error instance carries a `kind` field (`'procedure' | 'validation' | 'yield-validation' | 'registration'`) as an alternative to `instanceof`.
208
-
209
- ### Error Taxonomy (required for custom errors)
210
-
211
- `defineErrorTaxonomy` maps error classes to HTTP responses declaratively. Works with `HonoAppBuilder` for `rpc`, `http`, and pre-stream paths (`rpc-stream` / `http-stream`).
212
-
213
- Do NOT write `onError` `instanceof` ladders — that's anti-pattern #20. Use the taxonomy instead:
214
-
215
- ```typescript
216
- import { defineErrorTaxonomy } from 'ts-procedures/http-errors'
217
-
218
- class UseCaseError extends Error {
219
- constructor(readonly externalMsg: string, readonly internalMsg: string) {
220
- super(externalMsg); this.name = 'UseCaseError'
221
- Object.setPrototypeOf(this, UseCaseError.prototype)
222
- }
223
- }
224
-
225
- const appErrors = defineErrorTaxonomy({
226
- // class: entries are topologically sorted (subclasses checked first) automatically.
227
- // Predicate (match:) entries keep declared order — put narrower predicates first.
228
- UseCaseError: {
229
- class: UseCaseError,
230
- statusCode: 422,
231
- toResponse: (err) => ({ name: 'UseCaseError', message: err.externalMsg }),
232
- onCatch: (err) => logger.error(err.internalMsg), // internal logs only
233
- },
234
- MongoDuplicateKey: {
235
- match: (err): err is Error => err instanceof Error && (err as any).code === 11000,
236
- statusCode: 409,
237
- toResponse: () => ({ name: 'Conflict', message: 'Resource already exists' }),
238
- },
239
- })
240
-
241
- new HonoAppBuilder({
242
- errors: appErrors,
243
- unknownError: { statusCode: 500, toResponse: () => ({ name: 'InternalServerError' }) },
244
- })
245
- ```
246
-
247
- Handlers throw the error classes directly — the builder auto-serializes. `onError` is the first-class imperative peer when you want to handle errors in one callback instead (both modes coexist). In v6 `HonoAppBuilder.onPreStreamError` was renamed to `onError`. Cross-cutting `onRequestError` observer fires for every caught error.
248
-
249
- Custom (non-Hono) adapters consume the same taxonomy via `ts-procedures/server`'s `dispatchPreStreamError` / `dispatchMidStreamError`, which return plain data (`{ type: 'body', statusCode, body } | { type: 'response', response }` / `{ data, sseEvent?, runOnCatch? }`) — your adapter translates that into its framework's response.
250
-
251
- ### Per-route errors (typed)
252
-
253
- Declare a route's errors via the `errors` field on the `CreateHttp` config. For compile-time typo protection, attach a `satisfies` clause to the array — do NOT pass an explicit generic. `CreateHttp` is generic over `<TName, TReq, TRes, TErrorKey>`, so a single type argument binds `TName` and breaks `schema.req` / `schema.res` inference:
254
-
255
- ```typescript
256
- import { DocRegistry } from 'ts-procedures/http-docs'
257
-
258
- const API = Procedures<Ctx>()
259
-
260
- API.CreateHttp('GetUser', {
261
- path: '/users/:id', method: 'get',
262
- errors: ['UseCaseError'] satisfies (keyof typeof appErrors & string)[],
263
- schema: { req: { pathParams: Type.Object({ id: Type.String() }) } },
264
- }, async (ctx, { pathParams }) => fetchUser(pathParams.id))
265
-
266
- // Seed envelope errors from the taxonomy + framework defaults in one call
267
- const envelope = new DocRegistry({ errors: appErrors, basePath: '/api' }).from(apiApp).toJSON()
268
- ```
269
-
270
- ### Client-side typed catch blocks (via codegen)
271
-
272
- The generated `_errors.ts` emits real runtime classes extending a shared `${Service}ProcedureError` base. The generated `createApiClient` wires an error registry so non-2xx responses arrive as typed class instances:
273
-
274
- ```typescript
275
- import { createApiClient, ApiErrors, createFetchAdapter } from './generated'
276
-
277
- const api = createApiClient({ adapter: createFetchAdapter({ /* ... */ }), basePath: '...' })
278
-
279
- try {
280
- await api.users.getUser({ pathParams: { id: 'x' } })
281
- } catch (err) {
282
- if (err instanceof ApiErrors.UseCaseError) {
283
- // err.body typed; err.status, err.procedureName, err.scope available
284
- } else if (err instanceof ApiErrors.ApiProcedureError) {
285
- // Catch-all for any service error
286
- }
287
- }
288
- ```
289
-
290
- Typed classes are generated automatically for taxonomy entries declared with just `{ class, statusCode }` (their default `{ name, message }` body is self-describing). Add an explicit `schema` only when you give an entry a custom `toResponse` (shape can't be inferred) or document a raw `ErrorDoc` (via `documentError(...)` / a `config.errors` array) — without a schema those dispatch as the untyped `ClientHttpError`.
291
-
292
- Per-route error unions: routes with `errors: [...]` get an `Errors` type in their namespace (e.g. `Users.GetUser.Errors = ApiErrors.UseCaseError | ApiErrors.AuthError`).
293
-
294
- - Generated client error handling: catch the framework classes (`ClientHttpError`, `ClientNetworkError`, `ClientTimeoutError`, `ClientAbortError`), not raw `DOMException`/`TypeError` — the framework normalizes platform errors at the `executeCall` boundary, so raw platform errors no longer reach `catch` blocks after 7.0.0 (original is on `error.cause`). Use the `.safe()` sibling on RPC/API callables for an exhaustive `Result<T, E>` switch (`ok`, `typed`, `http`, `network`, `timeout`, `aborted`, `parse`, `usage`, `unknown`). Streams keep the throwing form — no `.safe()`.
295
-
296
- ## Lifecycle Hook Order
297
-
298
- ### Standard RPC
299
- ```
300
- onRequestStart → factoryContext() → handler() → onSuccess → onRequestEnd
301
- → onError → onRequestEnd
302
- ```
303
-
304
- ### Streaming
305
- ```
306
- onRequestStart → factoryContext() → validation → onStreamStart → handler yields → onStreamEnd → onRequestEnd
307
- → onError (or taxonomy) → onRequestEnd
308
- → onMidStreamError → onStreamEnd → onRequestEnd
309
- ```
310
-
311
- ## Decision Framework
312
-
313
- **Which procedure type?**
314
- - Single response (RPC) → `Create`
315
- - Multiple values over time (RPC) → `CreateStream`
316
- - REST-style route (`path` + `method`) → `CreateHttp`
317
- - REST-style streaming route → `CreateHttpStream`
318
-
319
- **Which schema library?**
320
- - **TypeBox** (`import { Type } from 'typebox'`) — built in, with `Infer<T>` type inference
321
- - Other libraries → implement `SchemaAdapter` and pass `Procedures({ schema: { adapters: [...] } })` (runtime validation + docs only)
322
-
323
- **Which HTTP implementation?**
324
- - Hono (RPC, streams, REST, REST streams) → `HonoAppBuilder`
325
- - Other frameworks (Fastify, Express, ...) → write a thin adapter on `ts-procedures/server` (doc builders, taxonomy dispatch returning data, `extractReqChannels`/`RequestSource`, `SseEventSequencer`); use the Hono adapter as the blueprint
326
-
327
- **Stream mode?**
328
- - Browser EventSource → `'sse'` (default)
329
- - Simple HTTP client → `'text'` (newline-delimited JSON)
330
-
331
- ## Anti-Patterns — NEVER Do These
332
-
333
- 1. **Never throw raw Error** — use `ctx.error(message, meta?)`
334
- 2. **Never expect returnType runtime validation** — it's docs only
335
- 3. **Never put validation logic in handler** — use `schema.params`
336
- 4. **Never ignore ctx.signal** — pass to all async calls
337
- 5. **Never skip signal.aborted check in stream loops** — causes resource leaks
338
- 6. **Never branch on `isStream`** — removed in v9; use the `kind` discriminant (on registrations and every creator's `info`)
339
- 7. **Never use plain JSON Schema objects** — use TypeBox builders (or register a `SchemaAdapter` for another library)
340
- 8. **Never swallow errors without re-throwing** — hides failures
341
- 9. **Never assume extra params fields survive** — `removeAdditional: true` strips them
342
- 10. **Never manually parse types AJV coerces** — `coerceTypes: true` handles it
343
- 11. **Never put `schema.req` on `Create`/`CreateStream` or `schema.params` on `CreateHttp`/`CreateHttpStream`** — RPC creators take `schema.params`; HTTP creators take `schema.req` channels
344
- 12. **Never catch raw DOMException/TypeError from generated callables** — catch `ClientTimeoutError`, `ClientAbortError`, `ClientNetworkError` instead; or use `.safe()` for Result-based narrowing
345
- 13. **Never use `Create` for HTTP routes (v8+)** — use `CreateHttp` (unary) or `CreateHttpStream` (streaming); request channels go under `schema.req`; `Procedures<Ctx, APIConfig>()` → `Procedures<Ctx>()`
346
-
347
- ## Testing
348
-
349
- ```typescript
350
- import { describe, test, expect } from 'vitest'
351
- import { ProcedureError, ProcedureValidationError } from 'ts-procedures'
352
-
353
- describe('GetUser', () => {
354
- test('valid params', async () => {
355
- const result = await GetUser(mockCtx, { id: 'user-1' })
356
- expect(result.id).toBe('user-1')
357
- })
358
-
359
- test('invalid params', async () => {
360
- await expect(GetUser(mockCtx, {})).rejects.toThrow(ProcedureValidationError)
361
- })
362
-
363
- test('business error', async () => {
364
- await expect(GetUser(mockCtx, { id: 'missing' })).rejects.toThrow(ProcedureError)
365
- })
366
- })
367
- ```
368
-
369
- ## HTTP Route Path Format
370
-
371
- `{pathPrefix}/{scope}/{kebab-case-name}/{version}`
372
-
373
- - `scope` can be string or string[] (joined as path segments)
374
- - Procedure name auto-converted to kebab-case
375
- - Stream routes support both GET (query params) and POST (JSON body)
376
-
377
- ## Client Code Generation
378
-
379
- Generate type-safe client SDKs from a running DocRegistry endpoint.
380
-
381
- ### Imports
382
-
383
- ```typescript
384
- // Client Runtime
385
- import { createClient, createFetchAdapter } from 'ts-procedures/client'
386
- import type { ClientAdapter, ClientHooks, TypedStream, ClientInstance } from 'ts-procedures/client'
387
-
388
- // Code Generation (build-time only)
389
- import { generateClient } from 'ts-procedures/codegen'
390
- ```
391
-
392
- ### CLI
393
-
394
- ```bash
395
- npx ts-procedures-codegen --url http://localhost:3000/docs --out ./src/generated/api
396
- # Defaults (on): --self-contained --namespace-types --jsdoc --clean-out-dir
397
- # (--clean-out-dir prunes orphaned generated files signed by ts-procedures-codegen; never deletes hand-written files)
398
- # Opt-out flags: --no-self-contained --no-namespace-types --no-jsdoc --no-clean-out-dir
399
- # Optional flags: --depluralize --enum-style union
400
- # --array-item-naming Item --uncountable-words criteria,alumni
401
- # --dry-run --watch --client-import-path @my-app/client
402
- # --config ./codegen.config.json --service-name Auth
403
- # --check [--check-mode scan|tsc] (self-validate output; exit non-zero if it won't compile)
404
- ```
405
-
406
- For Kotlin codegen (Android/JVM consumers), see `docs/codegen-kotlin.md` in the ts-procedures repo. The Kotlin target is types-only; consumer apps own HTTP/error handling.
407
- For Swift codegen (iOS/macOS/Apple-platform consumers), see `docs/codegen-swift.md` in the ts-procedures repo. The Swift target is types-only; consumer apps own HTTP/error handling.
408
-
409
- Generates one `.ts` file per scope plus a root `index.ts` that imports each scope as a namespace and exports a `create${ServiceName}Bindings(client)` factory AND a `create${ServiceName}Client(config)` convenience factory that pre-wires the error registry (defaults to `createApiBindings` / `createApiClient`; pass `--service-name <Name>` to rename). When namespace mode is on (the default), `index.ts` also wraps every scope namespace in an outer `export namespace ${ServiceName} { ... }` block so types are reachable as `Api.Users.GetUser.Params`, `Api.Errors.UseCaseError`, etc. The errors file (`_errors.ts`) emits runtime error classes extending a shared `${ServiceName}ProcedureError` base, each with `static fromResponse(body, meta)`, plus `${ServiceName}ErrorRegistry` (runtime dispatch map) and `${ServiceName}ProcedureErrorUnion` (type union). Defaults: `ApiErrors`, `ApiProcedureError`, `ApiErrorRegistry`, `ApiProcedureErrorUnion`.
410
- By default, types are wrapped in nested TS namespaces (`Scope.Route.Params`), JSDoc comments are emitted, and output is self-contained (no runtime dependency on `ts-procedures`). Use `--no-namespace-types` to revert to flat type names (`RouteParams`); this also disables the outer service namespace in `index.ts` and skips importing `_errors` from there.
411
- The generated client is **compatible with `tsconfig` `verbatimModuleSyntax: true`** (the strictest mode astro/tsconfigs and many monorepos enable). In namespace mode the scope's `bindScope(client)` function is emitted as a member of `export namespace ${Pascal}` so the namespace is value+type — required for the index's `export import` re-exports to compile under verbatim. Most consumers stay on `create${Name}Bindings(client)` / `create${Name}Client(config)` and never touch `bindScope` directly.
412
- **Strict CLI**: any unknown `--…` flag throws immediately with a `Did you mean …?` suggestion when the typo is close to a known flag (e.g. `--targt` → `--target`, `--service` → `--service-name`). Typos no longer fall through silently.
413
- **Generation-time guards** (fail fast, non-zero): two routes in a scope whose names PascalCase to the same identifier (e.g. `get-task` + `getTask`), or a scope name colliding with `--service-name` (e.g. scope `godmode` + `--service-name Godmode`), error at generation with the fix to make. Opt into `--check` (or `generateClient({ validate: true })`) to validate the whole emitted client and fail loud if it won't compile — recommended in CI.
414
- Note: ajsc formatting options (`--enum-style enum`, `--depluralize`, etc.) only take effect in namespace mode (the default). They are ignored with `--no-namespace-types`.
415
- Supports config file: `ts-procedures-codegen.config.json` (auto-loaded from CWD) or `--config <path>`.
416
-
417
- ### createClient Example
418
-
419
- ```typescript
420
- import { createClient, createFetchAdapter } from 'ts-procedures/client'
421
- import { createApiBindings, Api } from './generated/api'
422
- // With --service-name <Name>: import { create<Name>Bindings, <Name> } from './generated/api'
423
-
424
- const client = createClient({
425
- adapter: createFetchAdapter(),
426
- basePath: 'http://localhost:3000',
427
- scopes: createApiBindings,
428
- defaults: { timeout: 30_000, headers: { 'X-Client-Version': '1.0.0' } },
429
- hooks: {
430
- onBeforeRequest(ctx) {
431
- ctx.request.headers = { ...ctx.request.headers, Authorization: `Bearer ${getToken()}` }
432
- return ctx
433
- },
434
- onAfterResponse(ctx) {
435
- if (ctx.response.status === 401) redirect('/login')
436
- },
437
- },
438
- })
439
-
440
- // Fully typed — params and response inferred from server schemas; type aliases live under Api.<Scope>.<Route>.
441
- const user = await client.users.GetUser({ pathParams: { id: '123' } })
442
-
443
- // Per-call options — timeout, signal, headers, basePath, and hooks all share one bag
444
- await client.users.GetUser(
445
- { pathParams: { id: '123' } },
446
- {
447
- timeout: 5000,
448
- headers: { 'X-Request-Id': crypto.randomUUID() },
449
- onAfterResponse(ctx) {
450
- console.log(ctx.response.headers['x-rate-limit-remaining'])
451
- },
452
- },
453
- )
454
- ```
455
-
456
- ### Per-Call Options & Defaults
457
-
458
- ```typescript
459
- interface ProcedureCallDefaults {
460
- signal?: AbortSignal // cancellation (combined with per-call via AbortSignal.any)
461
- timeout?: number // ms — per-call timeout:0 disables inherited default
462
- headers?: Record<string, string> // merged, per-call keys win
463
- basePath?: string // per-call > default > config.basePath
464
- meta?: RequestMeta // typed per-request metadata (declaration-mergeable)
465
- }
466
-
467
- interface ProcedureCallOptions extends ProcedureCallDefaults, ClientHooks {}
468
- ```
469
-
470
- - Defaults are set via `config.defaults`; per-call options are passed as the 2nd argument to any generated callable.
471
- - Header precedence (low → high): adapter config < `defaults.headers` < per-call `headers` < route-declared `schema.req.headers` < `onBeforeRequest` mutations.
472
-
473
- ### Typed RequestMeta (Declaration Merging)
474
-
475
- Augment `RequestMeta` to type `meta` end-to-end (options, hooks, adapter):
476
-
477
- ```typescript
478
- // For code-generated self-contained clients
479
- declare module './generated/_types' {
480
- interface RequestMeta {
481
- traceId: string
482
- priority?: 'high' | 'low'
483
- }
484
- }
485
-
486
- // Or for direct ts-procedures/client usage
487
- declare module 'ts-procedures/client' {
488
- interface RequestMeta { traceId: string }
489
- }
490
-
491
- await client.users.GetUser(
492
- { pathParams: { id: '1' } },
493
- { meta: { traceId: 'req-abc', priority: 'high' } }, // fully typed
494
- )
495
- ```
496
-
497
- ### Hook Types
498
-
499
- ```typescript
500
- interface ClientHooks {
501
- onBeforeRequest?: (ctx: BeforeRequestContext) => BeforeRequestContext | Promise<BeforeRequestContext>
502
- onAfterResponse?: (ctx: AfterResponseContext) => void | Promise<void>
503
- onError?: (ctx: ErrorContext) => void | Promise<void>
504
- }
505
- ```
506
-
507
- ### TypedStream Usage
508
-
509
- ```typescript
510
- const stream = client.events.WatchNotifications({ filter: 'all' })
511
-
512
- for await (const event of stream) {
513
- console.log(event) // Typed as WatchNotificationsYield
514
- }
515
-
516
- const result = await stream.result // Typed as WatchNotificationsReturn
517
- ```
518
-
519
- - `TypedStream<TYield, TReturn>` extends `AsyncIterable<TYield>`
520
- - `stream.result` resolves with the final return value sent as `event: 'return'` SSE message
521
- - Stream procedures that return `void` have `result` resolve to `undefined`