ts-procedures 5.2.0 → 5.4.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.
- package/README.md +150 -0
- package/agent_config/bin/postinstall.mjs +105 -0
- package/agent_config/bin/setup.mjs +286 -0
- package/agent_config/claude-code/.claude-plugin/plugin.json +5 -0
- package/agent_config/claude-code/agents/ts-procedures-architect.md +188 -0
- package/agent_config/claude-code/skills/guide/SKILL.md +142 -0
- package/agent_config/claude-code/skills/guide/anti-patterns.md +608 -0
- package/agent_config/claude-code/skills/guide/api-reference.md +696 -0
- package/agent_config/claude-code/skills/guide/patterns.md +727 -0
- package/agent_config/claude-code/skills/review/SKILL.md +53 -0
- package/agent_config/claude-code/skills/review/checklist.md +163 -0
- package/agent_config/claude-code/skills/scaffold/SKILL.md +56 -0
- package/agent_config/claude-code/skills/scaffold/templates/express-rpc.md +134 -0
- package/agent_config/claude-code/skills/scaffold/templates/hono-api.md +169 -0
- package/agent_config/claude-code/skills/scaffold/templates/hono-rpc.md +139 -0
- package/agent_config/claude-code/skills/scaffold/templates/hono-stream.md +134 -0
- package/agent_config/claude-code/skills/scaffold/templates/procedure.md +77 -0
- package/agent_config/claude-code/skills/scaffold/templates/stream-procedure.md +113 -0
- package/agent_config/copilot/copilot-instructions.md +290 -0
- package/agent_config/cursor/cursorrules +290 -0
- package/agent_config/lib/install-claude.mjs +109 -0
- package/build/implementations/http/hono-api/index.d.ts +102 -0
- package/build/implementations/http/hono-api/index.js +339 -0
- package/build/implementations/http/hono-api/index.js.map +1 -0
- package/build/implementations/http/hono-api/index.test.d.ts +1 -0
- package/build/implementations/http/hono-api/index.test.js +983 -0
- package/build/implementations/http/hono-api/index.test.js.map +1 -0
- package/build/implementations/http/hono-api/types.d.ts +13 -0
- package/build/implementations/http/hono-api/types.js +2 -0
- package/build/implementations/http/hono-api/types.js.map +1 -0
- package/build/implementations/types.d.ts +44 -0
- package/build/index.d.ts +28 -6
- package/build/index.js +28 -0
- package/build/index.js.map +1 -1
- package/build/schema/compute-schema.d.ts +5 -0
- package/build/schema/compute-schema.js +8 -1
- package/build/schema/compute-schema.js.map +1 -1
- package/build/schema/parser.d.ts +6 -5
- package/build/schema/parser.js +54 -0
- package/build/schema/parser.js.map +1 -1
- package/package.json +14 -4
- package/src/implementations/http/README.md +45 -2
- package/src/implementations/http/hono-api/index.test.ts +1328 -0
- package/src/implementations/http/hono-api/index.ts +461 -0
- package/src/implementations/http/hono-api/types.ts +16 -0
- package/src/implementations/types.ts +52 -0
- package/src/index.ts +87 -10
- package/src/schema/compute-schema.ts +23 -2
- package/src/schema/parser.ts +70 -3
|
@@ -0,0 +1,290 @@
|
|
|
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>(builder?)
|
|
9
|
+
↓
|
|
10
|
+
Create(name, config, handler) → Standard async procedure
|
|
11
|
+
CreateStream(name, config, handler) → Streaming async generator
|
|
12
|
+
↓
|
|
13
|
+
Returns: { [name]: handler, procedure: handler, info: metadata }
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Imports
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
// Core
|
|
20
|
+
import { Procedures } from 'ts-procedures'
|
|
21
|
+
import type { TLocalContext, TStreamContext, TProcedureRegistration, TStreamProcedureRegistration } from 'ts-procedures'
|
|
22
|
+
|
|
23
|
+
// Errors
|
|
24
|
+
import { ProcedureError, ProcedureValidationError, ProcedureYieldValidationError, ProcedureRegistrationError } from 'ts-procedures'
|
|
25
|
+
|
|
26
|
+
// HTTP types
|
|
27
|
+
import type { RPCConfig, RPCHttpRouteDoc, StreamHttpRouteDoc, StreamMode } from 'ts-procedures/http'
|
|
28
|
+
|
|
29
|
+
// Express RPC
|
|
30
|
+
import { ExpressRPCAppBuilder } from 'ts-procedures/express-rpc'
|
|
31
|
+
|
|
32
|
+
// Hono RPC
|
|
33
|
+
import { HonoRPCAppBuilder } from 'ts-procedures/hono-rpc'
|
|
34
|
+
|
|
35
|
+
// Hono Streaming
|
|
36
|
+
import { HonoStreamAppBuilder, sse } from 'ts-procedures/hono-stream'
|
|
37
|
+
|
|
38
|
+
// Hono API (REST-style)
|
|
39
|
+
import { HonoAPIAppBuilder } from 'ts-procedures/hono-api'
|
|
40
|
+
import type { APIConfig, APIInput } from 'ts-procedures/hono-api'
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Architecture Rules
|
|
44
|
+
|
|
45
|
+
1. **schema.params is validated at runtime; schema.returnType is documentation only.** Never expect returnType to be validated. Use TypeScript for return type safety.
|
|
46
|
+
|
|
47
|
+
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.
|
|
48
|
+
|
|
49
|
+
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.
|
|
50
|
+
|
|
51
|
+
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.
|
|
52
|
+
|
|
53
|
+
5. **Use TypeBox for schemas** (`import { Type } from 'typebox'`). Plain JSON Schema objects are not recognized. AJV config: `allErrors: true`, `coerceTypes: true`, `removeAdditional: true`.
|
|
54
|
+
|
|
55
|
+
6. **One factory per access level.** Separate `Procedures()` factories for public, authenticated, admin contexts.
|
|
56
|
+
|
|
57
|
+
7. **onCreate callback enables framework integration.** Use for route registration, OpenAPI generation, logging.
|
|
58
|
+
|
|
59
|
+
8. **getProcedures() for introspection.** Returns all registered procedures with metadata.
|
|
60
|
+
|
|
61
|
+
## Procedure Pattern
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import { Procedures } from 'ts-procedures'
|
|
65
|
+
import { Type } from 'typebox'
|
|
66
|
+
|
|
67
|
+
type AppContext = { userId: string; signal?: AbortSignal }
|
|
68
|
+
|
|
69
|
+
const { Create, CreateStream } = Procedures<AppContext>()
|
|
70
|
+
|
|
71
|
+
// Standard procedure
|
|
72
|
+
const { GetUser } = Create(
|
|
73
|
+
'GetUser',
|
|
74
|
+
{
|
|
75
|
+
description: 'Fetch user by ID',
|
|
76
|
+
schema: {
|
|
77
|
+
params: Type.Object({ id: Type.String() }),
|
|
78
|
+
returnType: Type.Object({ id: Type.String(), name: Type.String() }),
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
async (ctx, params) => {
|
|
82
|
+
// params.id guaranteed string (AJV validated)
|
|
83
|
+
const user = await fetchUser(params.id, { signal: ctx.signal })
|
|
84
|
+
if (!user) throw ctx.error('Not found', { code: 'NOT_FOUND' })
|
|
85
|
+
return user
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Stream Procedure Pattern
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
const { StreamEvents } = CreateStream(
|
|
94
|
+
'StreamEvents',
|
|
95
|
+
{
|
|
96
|
+
schema: {
|
|
97
|
+
params: Type.Object({ channel: Type.String() }),
|
|
98
|
+
yieldType: Type.Object({ type: Type.String(), data: Type.Any() }),
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
async function* (ctx, params) {
|
|
102
|
+
// ctx.signal always present in streams
|
|
103
|
+
while (!ctx.signal.aborted) {
|
|
104
|
+
const event = await pollEvents(params.channel, { signal: ctx.signal })
|
|
105
|
+
yield event
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Express RPC Pattern
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
import { ExpressRPCAppBuilder } from 'ts-procedures/express-rpc'
|
|
115
|
+
import type { RPCConfig } from 'ts-procedures/http'
|
|
116
|
+
|
|
117
|
+
const RPC = Procedures<AppContext, RPCConfig>()
|
|
118
|
+
|
|
119
|
+
RPC.Create('GetUser', {
|
|
120
|
+
scope: 'users', version: 1,
|
|
121
|
+
schema: { params: Type.Object({ id: Type.String() }) },
|
|
122
|
+
}, async (ctx, params) => fetchUser(params.id))
|
|
123
|
+
|
|
124
|
+
const app = new ExpressRPCAppBuilder({ pathPrefix: '/api' })
|
|
125
|
+
.register(RPC, async (req) => ({ userId: await authenticate(req) }))
|
|
126
|
+
.build()
|
|
127
|
+
// POST /api/users/get-user/1
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Hono RPC Pattern
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
import { HonoRPCAppBuilder } from 'ts-procedures/hono-rpc'
|
|
134
|
+
|
|
135
|
+
const app = new HonoRPCAppBuilder({ pathPrefix: '/api' })
|
|
136
|
+
.register(RPC, (c) => ({ userId: c.req.header('x-user-id') }))
|
|
137
|
+
.build()
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Hono Streaming Pattern
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
import { HonoStreamAppBuilder, sse } from 'ts-procedures/hono-stream'
|
|
144
|
+
|
|
145
|
+
const StreamRPC = Procedures<AppContext, RPCConfig>()
|
|
146
|
+
|
|
147
|
+
StreamRPC.CreateStream('Feed', {
|
|
148
|
+
scope: 'events', version: 1,
|
|
149
|
+
schema: { params: Type.Object({ channel: Type.String() }) },
|
|
150
|
+
}, async function* (ctx, params) {
|
|
151
|
+
while (!ctx.signal.aborted) {
|
|
152
|
+
const event = await poll({ signal: ctx.signal })
|
|
153
|
+
yield sse(event, { event: 'update' })
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
const app = new HonoStreamAppBuilder({ defaultStreamMode: 'sse' })
|
|
158
|
+
.register(StreamRPC, (c) => ({ userId: c.req.header('x-user-id') }))
|
|
159
|
+
.build()
|
|
160
|
+
// GET|POST /events/feed/1
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Hono API Pattern (REST-style)
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
import { HonoAPIAppBuilder } from 'ts-procedures/hono-api'
|
|
167
|
+
import type { APIConfig } from 'ts-procedures/http'
|
|
168
|
+
|
|
169
|
+
const API = Procedures<AppContext, APIConfig>()
|
|
170
|
+
|
|
171
|
+
API.Create('GetUser', {
|
|
172
|
+
path: '/users/:id', method: 'get',
|
|
173
|
+
schema: {
|
|
174
|
+
input: { pathParams: Type.Object({ id: Type.String() }) },
|
|
175
|
+
},
|
|
176
|
+
}, async (ctx, { pathParams }) => fetchUser(pathParams.id))
|
|
177
|
+
|
|
178
|
+
API.Create('CreateUser', {
|
|
179
|
+
path: '/users', method: 'post',
|
|
180
|
+
schema: {
|
|
181
|
+
input: { body: Type.Object({ name: Type.String() }) },
|
|
182
|
+
},
|
|
183
|
+
}, async (ctx, { body }) => createUser(body))
|
|
184
|
+
|
|
185
|
+
const app = await new HonoAPIAppBuilder({ pathPrefix: '/api' })
|
|
186
|
+
.register(API, (c) => ({ userId: c.req.header('x-user-id') }))
|
|
187
|
+
.build()
|
|
188
|
+
// GET /api/users/:id → 200, POST /api/users → 201
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Error Handling
|
|
192
|
+
|
|
193
|
+
| Error Class | Trigger | HTTP Status |
|
|
194
|
+
|-------------|---------|-------------|
|
|
195
|
+
| `ProcedureValidationError` | Schema params validation failure | 400 |
|
|
196
|
+
| `ProcedureError` | `ctx.error()` or unhandled handler exception | 422/500 |
|
|
197
|
+
| `ProcedureYieldValidationError` | Yield validation failure (validateYields: true) | N/A (mid-stream) |
|
|
198
|
+
| `ProcedureRegistrationError` | Invalid schema or duplicate name at registration | N/A (startup) |
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
// In HTTP builder
|
|
202
|
+
onError: (procedure, req, res, error) => {
|
|
203
|
+
if (error instanceof ProcedureValidationError) {
|
|
204
|
+
res.status(400).json({ error: error.message, details: error.errors })
|
|
205
|
+
} else if (error instanceof ProcedureError) {
|
|
206
|
+
res.status(422).json({ error: error.message, meta: error.meta })
|
|
207
|
+
} else {
|
|
208
|
+
res.status(500).json({ error: 'Internal server error' })
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Lifecycle Hook Order
|
|
214
|
+
|
|
215
|
+
### Standard RPC
|
|
216
|
+
```
|
|
217
|
+
onRequestStart → factoryContext() → handler() → onSuccess → onRequestEnd
|
|
218
|
+
→ onError → onRequestEnd
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Streaming
|
|
222
|
+
```
|
|
223
|
+
onRequestStart → factoryContext() → validation → onStreamStart → handler yields → onStreamEnd → onRequestEnd
|
|
224
|
+
→ onPreStreamError → onRequestEnd
|
|
225
|
+
→ onMidStreamError → onStreamEnd → onRequestEnd
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Decision Framework
|
|
229
|
+
|
|
230
|
+
**Which procedure type?**
|
|
231
|
+
- Single response → `Create`
|
|
232
|
+
- Multiple values over time → `CreateStream`
|
|
233
|
+
|
|
234
|
+
**Which schema library?**
|
|
235
|
+
- **TypeBox** (`import { Type } from 'typebox'`)
|
|
236
|
+
|
|
237
|
+
**Which HTTP implementation?**
|
|
238
|
+
- Express → `ExpressRPCAppBuilder`
|
|
239
|
+
- Hono (standard) → `HonoRPCAppBuilder`
|
|
240
|
+
- Hono (streaming) → `HonoStreamAppBuilder`
|
|
241
|
+
- Hono (REST-style, per-channel input) → `HonoAPIAppBuilder`
|
|
242
|
+
|
|
243
|
+
**Stream mode?**
|
|
244
|
+
- Browser EventSource → `'sse'` (default)
|
|
245
|
+
- Simple HTTP client → `'text'` (newline-delimited JSON)
|
|
246
|
+
|
|
247
|
+
## Anti-Patterns — NEVER Do These
|
|
248
|
+
|
|
249
|
+
1. **Never throw raw Error** — use `ctx.error(message, meta?)`
|
|
250
|
+
2. **Never expect returnType runtime validation** — it's docs only
|
|
251
|
+
3. **Never put validation logic in handler** — use `schema.params`
|
|
252
|
+
4. **Never ignore ctx.signal** — pass to all async calls
|
|
253
|
+
5. **Never skip signal.aborted check in stream loops** — causes resource leaks
|
|
254
|
+
6. **Never register Create procedures with HonoStreamAppBuilder** — they're silently ignored
|
|
255
|
+
7. **Never use plain JSON Schema objects** — use TypeBox builders
|
|
256
|
+
8. **Never swallow errors without re-throwing** — hides failures
|
|
257
|
+
9. **Never assume extra params fields survive** — `removeAdditional: true` strips them
|
|
258
|
+
10. **Never manually parse types AJV coerces** — `coerceTypes: true` handles it
|
|
259
|
+
11. **Never define both schema.params and schema.input** — mutually exclusive, throws ProcedureRegistrationError
|
|
260
|
+
12. **Never forget to await HonoAPIAppBuilder.build()** — it's async (resolves query parser)
|
|
261
|
+
|
|
262
|
+
## Testing
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
import { describe, test, expect } from 'vitest'
|
|
266
|
+
import { ProcedureError, ProcedureValidationError } from 'ts-procedures'
|
|
267
|
+
|
|
268
|
+
describe('GetUser', () => {
|
|
269
|
+
test('valid params', async () => {
|
|
270
|
+
const result = await GetUser(mockCtx, { id: 'user-1' })
|
|
271
|
+
expect(result.id).toBe('user-1')
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
test('invalid params', async () => {
|
|
275
|
+
await expect(GetUser(mockCtx, {})).rejects.toThrow(ProcedureValidationError)
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
test('business error', async () => {
|
|
279
|
+
await expect(GetUser(mockCtx, { id: 'missing' })).rejects.toThrow(ProcedureError)
|
|
280
|
+
})
|
|
281
|
+
})
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## HTTP Route Path Format
|
|
285
|
+
|
|
286
|
+
`{pathPrefix}/{scope}/{kebab-case-name}/{version}`
|
|
287
|
+
|
|
288
|
+
- `scope` can be string or string[] (joined as path segments)
|
|
289
|
+
- Procedure name auto-converted to kebab-case
|
|
290
|
+
- Stream routes support both GET (query params) and POST (JSON body)
|
|
@@ -0,0 +1,290 @@
|
|
|
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>(builder?)
|
|
9
|
+
↓
|
|
10
|
+
Create(name, config, handler) → Standard async procedure
|
|
11
|
+
CreateStream(name, config, handler) → Streaming async generator
|
|
12
|
+
↓
|
|
13
|
+
Returns: { [name]: handler, procedure: handler, info: metadata }
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Imports
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
// Core
|
|
20
|
+
import { Procedures } from 'ts-procedures'
|
|
21
|
+
import type { TLocalContext, TStreamContext, TProcedureRegistration, TStreamProcedureRegistration } from 'ts-procedures'
|
|
22
|
+
|
|
23
|
+
// Errors
|
|
24
|
+
import { ProcedureError, ProcedureValidationError, ProcedureYieldValidationError, ProcedureRegistrationError } from 'ts-procedures'
|
|
25
|
+
|
|
26
|
+
// HTTP types
|
|
27
|
+
import type { RPCConfig, RPCHttpRouteDoc, StreamHttpRouteDoc, StreamMode } from 'ts-procedures/http'
|
|
28
|
+
|
|
29
|
+
// Express RPC
|
|
30
|
+
import { ExpressRPCAppBuilder } from 'ts-procedures/express-rpc'
|
|
31
|
+
|
|
32
|
+
// Hono RPC
|
|
33
|
+
import { HonoRPCAppBuilder } from 'ts-procedures/hono-rpc'
|
|
34
|
+
|
|
35
|
+
// Hono Streaming
|
|
36
|
+
import { HonoStreamAppBuilder, sse } from 'ts-procedures/hono-stream'
|
|
37
|
+
|
|
38
|
+
// Hono API (REST-style)
|
|
39
|
+
import { HonoAPIAppBuilder } from 'ts-procedures/hono-api'
|
|
40
|
+
import type { APIConfig, APIInput } from 'ts-procedures/hono-api'
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Architecture Rules
|
|
44
|
+
|
|
45
|
+
1. **schema.params is validated at runtime; schema.returnType is documentation only.** Never expect returnType to be validated. Use TypeScript for return type safety.
|
|
46
|
+
|
|
47
|
+
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.
|
|
48
|
+
|
|
49
|
+
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.
|
|
50
|
+
|
|
51
|
+
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.
|
|
52
|
+
|
|
53
|
+
5. **Use TypeBox for schemas** (`import { Type } from 'typebox'`). Plain JSON Schema objects are not recognized. AJV config: `allErrors: true`, `coerceTypes: true`, `removeAdditional: true`.
|
|
54
|
+
|
|
55
|
+
6. **One factory per access level.** Separate `Procedures()` factories for public, authenticated, admin contexts.
|
|
56
|
+
|
|
57
|
+
7. **onCreate callback enables framework integration.** Use for route registration, OpenAPI generation, logging.
|
|
58
|
+
|
|
59
|
+
8. **getProcedures() for introspection.** Returns all registered procedures with metadata.
|
|
60
|
+
|
|
61
|
+
## Procedure Pattern
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import { Procedures } from 'ts-procedures'
|
|
65
|
+
import { Type } from 'typebox'
|
|
66
|
+
|
|
67
|
+
type AppContext = { userId: string; signal?: AbortSignal }
|
|
68
|
+
|
|
69
|
+
const { Create, CreateStream } = Procedures<AppContext>()
|
|
70
|
+
|
|
71
|
+
// Standard procedure
|
|
72
|
+
const { GetUser } = Create(
|
|
73
|
+
'GetUser',
|
|
74
|
+
{
|
|
75
|
+
description: 'Fetch user by ID',
|
|
76
|
+
schema: {
|
|
77
|
+
params: Type.Object({ id: Type.String() }),
|
|
78
|
+
returnType: Type.Object({ id: Type.String(), name: Type.String() }),
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
async (ctx, params) => {
|
|
82
|
+
// params.id guaranteed string (AJV validated)
|
|
83
|
+
const user = await fetchUser(params.id, { signal: ctx.signal })
|
|
84
|
+
if (!user) throw ctx.error('Not found', { code: 'NOT_FOUND' })
|
|
85
|
+
return user
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Stream Procedure Pattern
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
const { StreamEvents } = CreateStream(
|
|
94
|
+
'StreamEvents',
|
|
95
|
+
{
|
|
96
|
+
schema: {
|
|
97
|
+
params: Type.Object({ channel: Type.String() }),
|
|
98
|
+
yieldType: Type.Object({ type: Type.String(), data: Type.Any() }),
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
async function* (ctx, params) {
|
|
102
|
+
// ctx.signal always present in streams
|
|
103
|
+
while (!ctx.signal.aborted) {
|
|
104
|
+
const event = await pollEvents(params.channel, { signal: ctx.signal })
|
|
105
|
+
yield event
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Express RPC Pattern
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
import { ExpressRPCAppBuilder } from 'ts-procedures/express-rpc'
|
|
115
|
+
import type { RPCConfig } from 'ts-procedures/http'
|
|
116
|
+
|
|
117
|
+
const RPC = Procedures<AppContext, RPCConfig>()
|
|
118
|
+
|
|
119
|
+
RPC.Create('GetUser', {
|
|
120
|
+
scope: 'users', version: 1,
|
|
121
|
+
schema: { params: Type.Object({ id: Type.String() }) },
|
|
122
|
+
}, async (ctx, params) => fetchUser(params.id))
|
|
123
|
+
|
|
124
|
+
const app = new ExpressRPCAppBuilder({ pathPrefix: '/api' })
|
|
125
|
+
.register(RPC, async (req) => ({ userId: await authenticate(req) }))
|
|
126
|
+
.build()
|
|
127
|
+
// POST /api/users/get-user/1
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Hono RPC Pattern
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
import { HonoRPCAppBuilder } from 'ts-procedures/hono-rpc'
|
|
134
|
+
|
|
135
|
+
const app = new HonoRPCAppBuilder({ pathPrefix: '/api' })
|
|
136
|
+
.register(RPC, (c) => ({ userId: c.req.header('x-user-id') }))
|
|
137
|
+
.build()
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Hono Streaming Pattern
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
import { HonoStreamAppBuilder, sse } from 'ts-procedures/hono-stream'
|
|
144
|
+
|
|
145
|
+
const StreamRPC = Procedures<AppContext, RPCConfig>()
|
|
146
|
+
|
|
147
|
+
StreamRPC.CreateStream('Feed', {
|
|
148
|
+
scope: 'events', version: 1,
|
|
149
|
+
schema: { params: Type.Object({ channel: Type.String() }) },
|
|
150
|
+
}, async function* (ctx, params) {
|
|
151
|
+
while (!ctx.signal.aborted) {
|
|
152
|
+
const event = await poll({ signal: ctx.signal })
|
|
153
|
+
yield sse(event, { event: 'update' })
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
const app = new HonoStreamAppBuilder({ defaultStreamMode: 'sse' })
|
|
158
|
+
.register(StreamRPC, (c) => ({ userId: c.req.header('x-user-id') }))
|
|
159
|
+
.build()
|
|
160
|
+
// GET|POST /events/feed/1
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Hono API Pattern (REST-style)
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
import { HonoAPIAppBuilder } from 'ts-procedures/hono-api'
|
|
167
|
+
import type { APIConfig } from 'ts-procedures/http'
|
|
168
|
+
|
|
169
|
+
const API = Procedures<AppContext, APIConfig>()
|
|
170
|
+
|
|
171
|
+
API.Create('GetUser', {
|
|
172
|
+
path: '/users/:id', method: 'get',
|
|
173
|
+
schema: {
|
|
174
|
+
input: { pathParams: Type.Object({ id: Type.String() }) },
|
|
175
|
+
},
|
|
176
|
+
}, async (ctx, { pathParams }) => fetchUser(pathParams.id))
|
|
177
|
+
|
|
178
|
+
API.Create('CreateUser', {
|
|
179
|
+
path: '/users', method: 'post',
|
|
180
|
+
schema: {
|
|
181
|
+
input: { body: Type.Object({ name: Type.String() }) },
|
|
182
|
+
},
|
|
183
|
+
}, async (ctx, { body }) => createUser(body))
|
|
184
|
+
|
|
185
|
+
const app = await new HonoAPIAppBuilder({ pathPrefix: '/api' })
|
|
186
|
+
.register(API, (c) => ({ userId: c.req.header('x-user-id') }))
|
|
187
|
+
.build()
|
|
188
|
+
// GET /api/users/:id → 200, POST /api/users → 201
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Error Handling
|
|
192
|
+
|
|
193
|
+
| Error Class | Trigger | HTTP Status |
|
|
194
|
+
|-------------|---------|-------------|
|
|
195
|
+
| `ProcedureValidationError` | Schema params validation failure | 400 |
|
|
196
|
+
| `ProcedureError` | `ctx.error()` or unhandled handler exception | 422/500 |
|
|
197
|
+
| `ProcedureYieldValidationError` | Yield validation failure (validateYields: true) | N/A (mid-stream) |
|
|
198
|
+
| `ProcedureRegistrationError` | Invalid schema or duplicate name at registration | N/A (startup) |
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
// In HTTP builder
|
|
202
|
+
onError: (procedure, req, res, error) => {
|
|
203
|
+
if (error instanceof ProcedureValidationError) {
|
|
204
|
+
res.status(400).json({ error: error.message, details: error.errors })
|
|
205
|
+
} else if (error instanceof ProcedureError) {
|
|
206
|
+
res.status(422).json({ error: error.message, meta: error.meta })
|
|
207
|
+
} else {
|
|
208
|
+
res.status(500).json({ error: 'Internal server error' })
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Lifecycle Hook Order
|
|
214
|
+
|
|
215
|
+
### Standard RPC
|
|
216
|
+
```
|
|
217
|
+
onRequestStart → factoryContext() → handler() → onSuccess → onRequestEnd
|
|
218
|
+
→ onError → onRequestEnd
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Streaming
|
|
222
|
+
```
|
|
223
|
+
onRequestStart → factoryContext() → validation → onStreamStart → handler yields → onStreamEnd → onRequestEnd
|
|
224
|
+
→ onPreStreamError → onRequestEnd
|
|
225
|
+
→ onMidStreamError → onStreamEnd → onRequestEnd
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Decision Framework
|
|
229
|
+
|
|
230
|
+
**Which procedure type?**
|
|
231
|
+
- Single response → `Create`
|
|
232
|
+
- Multiple values over time → `CreateStream`
|
|
233
|
+
|
|
234
|
+
**Which schema library?**
|
|
235
|
+
- **TypeBox** (`import { Type } from 'typebox'`)
|
|
236
|
+
|
|
237
|
+
**Which HTTP implementation?**
|
|
238
|
+
- Express → `ExpressRPCAppBuilder`
|
|
239
|
+
- Hono (standard) → `HonoRPCAppBuilder`
|
|
240
|
+
- Hono (streaming) → `HonoStreamAppBuilder`
|
|
241
|
+
- Hono (REST-style, per-channel input) → `HonoAPIAppBuilder`
|
|
242
|
+
|
|
243
|
+
**Stream mode?**
|
|
244
|
+
- Browser EventSource → `'sse'` (default)
|
|
245
|
+
- Simple HTTP client → `'text'` (newline-delimited JSON)
|
|
246
|
+
|
|
247
|
+
## Anti-Patterns — NEVER Do These
|
|
248
|
+
|
|
249
|
+
1. **Never throw raw Error** — use `ctx.error(message, meta?)`
|
|
250
|
+
2. **Never expect returnType runtime validation** — it's docs only
|
|
251
|
+
3. **Never put validation logic in handler** — use `schema.params`
|
|
252
|
+
4. **Never ignore ctx.signal** — pass to all async calls
|
|
253
|
+
5. **Never skip signal.aborted check in stream loops** — causes resource leaks
|
|
254
|
+
6. **Never register Create procedures with HonoStreamAppBuilder** — they're silently ignored
|
|
255
|
+
7. **Never use plain JSON Schema objects** — use TypeBox builders
|
|
256
|
+
8. **Never swallow errors without re-throwing** — hides failures
|
|
257
|
+
9. **Never assume extra params fields survive** — `removeAdditional: true` strips them
|
|
258
|
+
10. **Never manually parse types AJV coerces** — `coerceTypes: true` handles it
|
|
259
|
+
11. **Never define both schema.params and schema.input** — mutually exclusive, throws ProcedureRegistrationError
|
|
260
|
+
12. **Never forget to await HonoAPIAppBuilder.build()** — it's async (resolves query parser)
|
|
261
|
+
|
|
262
|
+
## Testing
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
import { describe, test, expect } from 'vitest'
|
|
266
|
+
import { ProcedureError, ProcedureValidationError } from 'ts-procedures'
|
|
267
|
+
|
|
268
|
+
describe('GetUser', () => {
|
|
269
|
+
test('valid params', async () => {
|
|
270
|
+
const result = await GetUser(mockCtx, { id: 'user-1' })
|
|
271
|
+
expect(result.id).toBe('user-1')
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
test('invalid params', async () => {
|
|
275
|
+
await expect(GetUser(mockCtx, {})).rejects.toThrow(ProcedureValidationError)
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
test('business error', async () => {
|
|
279
|
+
await expect(GetUser(mockCtx, { id: 'missing' })).rejects.toThrow(ProcedureError)
|
|
280
|
+
})
|
|
281
|
+
})
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## HTTP Route Path Format
|
|
285
|
+
|
|
286
|
+
`{pathPrefix}/{scope}/{kebab-case-name}/{version}`
|
|
287
|
+
|
|
288
|
+
- `scope` can be string or string[] (joined as path segments)
|
|
289
|
+
- Procedure name auto-converted to kebab-case
|
|
290
|
+
- Stream routes support both GET (query params) and POST (JSON body)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = dirname(__filename);
|
|
7
|
+
const SKILLS_DIR = join(__dirname, '..', 'claude-code', 'skills');
|
|
8
|
+
const AGENTS_DIR = join(__dirname, '..', 'claude-code', 'agents');
|
|
9
|
+
|
|
10
|
+
function getPackageVersion() {
|
|
11
|
+
try {
|
|
12
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf-8'));
|
|
13
|
+
return pkg.version || 'unknown';
|
|
14
|
+
} catch {
|
|
15
|
+
return 'unknown';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function makeAutoHeader() {
|
|
20
|
+
const version = getPackageVersion();
|
|
21
|
+
return `<!-- Auto-generated by ts-procedures@${version}. Updated on npm install/update. Do not edit. -->\n\n`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function stripFrontmatter(content) {
|
|
25
|
+
const match = content.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
|
|
26
|
+
return match ? match[1].trim() : content.trim();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function ensureDir(dir) {
|
|
30
|
+
if (!existsSync(dir)) {
|
|
31
|
+
mkdirSync(dir, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Install Claude Code integration files into a project's .claude/ directory.
|
|
37
|
+
*
|
|
38
|
+
* Creates:
|
|
39
|
+
* .claude/rules/ts-procedures.md — Framework reference (always loaded in context)
|
|
40
|
+
* .claude/commands/ts-procedures-scaffold.md — Scaffold command (/project:ts-procedures-scaffold)
|
|
41
|
+
* .claude/commands/ts-procedures-review.md — Review command (/project:ts-procedures-review)
|
|
42
|
+
* .claude/agents/ts-procedures-architect.md — Architecture planning agent
|
|
43
|
+
*
|
|
44
|
+
* @param {string} projectRoot — Absolute path to the consuming project's root
|
|
45
|
+
* @returns {{ files: string[] }} — List of created/updated file paths (relative to projectRoot)
|
|
46
|
+
*/
|
|
47
|
+
export function installClaude(projectRoot) {
|
|
48
|
+
const claudeDir = join(projectRoot, '.claude');
|
|
49
|
+
const rulesDir = join(claudeDir, 'rules');
|
|
50
|
+
const commandsDir = join(claudeDir, 'commands');
|
|
51
|
+
const agentsDir = join(claudeDir, 'agents');
|
|
52
|
+
|
|
53
|
+
ensureDir(rulesDir);
|
|
54
|
+
ensureDir(commandsDir);
|
|
55
|
+
ensureDir(agentsDir);
|
|
56
|
+
|
|
57
|
+
const autoHeader = makeAutoHeader();
|
|
58
|
+
const files = [];
|
|
59
|
+
|
|
60
|
+
// 1. Rules file — framework reference (always loaded in context)
|
|
61
|
+
const guideSkill = readFileSync(join(SKILLS_DIR, 'guide', 'SKILL.md'), 'utf-8');
|
|
62
|
+
const guideBody = stripFrontmatter(guideSkill)
|
|
63
|
+
// Remove the "Supporting Files" section — replaced by "Detailed Reference" below
|
|
64
|
+
.replace(/\n## Supporting Files[\s\S]*$/, '');
|
|
65
|
+
|
|
66
|
+
const rulesContent = autoHeader + guideBody + `
|
|
67
|
+
|
|
68
|
+
## Detailed Reference
|
|
69
|
+
|
|
70
|
+
For complete API details, patterns, and anti-patterns, read the files in:
|
|
71
|
+
\`node_modules/ts-procedures/agent_config/claude-code/skills/guide/\`
|
|
72
|
+
|
|
73
|
+
- \`api-reference.md\` — Full API reference for Procedures, Create, CreateStream, errors, schema, HTTP implementations
|
|
74
|
+
- \`patterns.md\` — Prescribed patterns with code examples
|
|
75
|
+
- \`anti-patterns.md\` — Common mistakes to avoid with fixes
|
|
76
|
+
`;
|
|
77
|
+
|
|
78
|
+
writeFileSync(join(rulesDir, 'ts-procedures.md'), rulesContent, 'utf-8');
|
|
79
|
+
files.push('.claude/rules/ts-procedures.md');
|
|
80
|
+
|
|
81
|
+
// 2. Scaffold command
|
|
82
|
+
const scaffoldSkill = readFileSync(join(SKILLS_DIR, 'scaffold', 'SKILL.md'), 'utf-8');
|
|
83
|
+
const scaffoldBody = stripFrontmatter(scaffoldSkill)
|
|
84
|
+
.replace(
|
|
85
|
+
'Read the template file from `templates/<type>.md` in this skill directory.',
|
|
86
|
+
'Read the template file from `node_modules/ts-procedures/agent_config/claude-code/skills/scaffold/templates/<type>.md`.'
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
writeFileSync(join(commandsDir, 'ts-procedures-scaffold.md'), autoHeader + scaffoldBody, 'utf-8');
|
|
90
|
+
files.push('.claude/commands/ts-procedures-scaffold.md');
|
|
91
|
+
|
|
92
|
+
// 3. Review command
|
|
93
|
+
const reviewSkill = readFileSync(join(SKILLS_DIR, 'review', 'SKILL.md'), 'utf-8');
|
|
94
|
+
const reviewBody = stripFrontmatter(reviewSkill)
|
|
95
|
+
.replaceAll(
|
|
96
|
+
'`checklist.md`',
|
|
97
|
+
'`node_modules/ts-procedures/agent_config/claude-code/skills/review/checklist.md`'
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
writeFileSync(join(commandsDir, 'ts-procedures-review.md'), autoHeader + reviewBody, 'utf-8');
|
|
101
|
+
files.push('.claude/commands/ts-procedures-review.md');
|
|
102
|
+
|
|
103
|
+
// 4. Architect agent
|
|
104
|
+
const architectAgent = readFileSync(join(AGENTS_DIR, 'ts-procedures-architect.md'), 'utf-8');
|
|
105
|
+
writeFileSync(join(agentsDir, 'ts-procedures-architect.md'), autoHeader + architectAgent, 'utf-8');
|
|
106
|
+
files.push('.claude/agents/ts-procedures-architect.md');
|
|
107
|
+
|
|
108
|
+
return { files };
|
|
109
|
+
}
|