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.
Files changed (49) hide show
  1. package/README.md +150 -0
  2. package/agent_config/bin/postinstall.mjs +105 -0
  3. package/agent_config/bin/setup.mjs +286 -0
  4. package/agent_config/claude-code/.claude-plugin/plugin.json +5 -0
  5. package/agent_config/claude-code/agents/ts-procedures-architect.md +188 -0
  6. package/agent_config/claude-code/skills/guide/SKILL.md +142 -0
  7. package/agent_config/claude-code/skills/guide/anti-patterns.md +608 -0
  8. package/agent_config/claude-code/skills/guide/api-reference.md +696 -0
  9. package/agent_config/claude-code/skills/guide/patterns.md +727 -0
  10. package/agent_config/claude-code/skills/review/SKILL.md +53 -0
  11. package/agent_config/claude-code/skills/review/checklist.md +163 -0
  12. package/agent_config/claude-code/skills/scaffold/SKILL.md +56 -0
  13. package/agent_config/claude-code/skills/scaffold/templates/express-rpc.md +134 -0
  14. package/agent_config/claude-code/skills/scaffold/templates/hono-api.md +169 -0
  15. package/agent_config/claude-code/skills/scaffold/templates/hono-rpc.md +139 -0
  16. package/agent_config/claude-code/skills/scaffold/templates/hono-stream.md +134 -0
  17. package/agent_config/claude-code/skills/scaffold/templates/procedure.md +77 -0
  18. package/agent_config/claude-code/skills/scaffold/templates/stream-procedure.md +113 -0
  19. package/agent_config/copilot/copilot-instructions.md +290 -0
  20. package/agent_config/cursor/cursorrules +290 -0
  21. package/agent_config/lib/install-claude.mjs +109 -0
  22. package/build/implementations/http/hono-api/index.d.ts +102 -0
  23. package/build/implementations/http/hono-api/index.js +339 -0
  24. package/build/implementations/http/hono-api/index.js.map +1 -0
  25. package/build/implementations/http/hono-api/index.test.d.ts +1 -0
  26. package/build/implementations/http/hono-api/index.test.js +983 -0
  27. package/build/implementations/http/hono-api/index.test.js.map +1 -0
  28. package/build/implementations/http/hono-api/types.d.ts +13 -0
  29. package/build/implementations/http/hono-api/types.js +2 -0
  30. package/build/implementations/http/hono-api/types.js.map +1 -0
  31. package/build/implementations/types.d.ts +44 -0
  32. package/build/index.d.ts +28 -6
  33. package/build/index.js +28 -0
  34. package/build/index.js.map +1 -1
  35. package/build/schema/compute-schema.d.ts +5 -0
  36. package/build/schema/compute-schema.js +8 -1
  37. package/build/schema/compute-schema.js.map +1 -1
  38. package/build/schema/parser.d.ts +6 -5
  39. package/build/schema/parser.js +54 -0
  40. package/build/schema/parser.js.map +1 -1
  41. package/package.json +14 -4
  42. package/src/implementations/http/README.md +45 -2
  43. package/src/implementations/http/hono-api/index.test.ts +1328 -0
  44. package/src/implementations/http/hono-api/index.ts +461 -0
  45. package/src/implementations/http/hono-api/types.ts +16 -0
  46. package/src/implementations/types.ts +52 -0
  47. package/src/index.ts +87 -10
  48. package/src/schema/compute-schema.ts +23 -2
  49. package/src/schema/parser.ts +70 -3
@@ -0,0 +1,139 @@
1
+ # Hono RPC Template: {{Name}}
2
+
3
+ ## Implementation — `{{Name}}.rpc.ts`
4
+
5
+ ```typescript
6
+ import { Procedures, ProcedureError, ProcedureValidationError } from 'ts-procedures'
7
+ import { HonoRPCAppBuilder } from 'ts-procedures/hono-rpc'
8
+ import type { RPCConfig } from 'ts-procedures/http'
9
+ import { Type } from 'typebox'
10
+
11
+ // ─── Context ──────────────────────────────────────────────
12
+
13
+ type {{Name}}Context = {
14
+ userId: string
15
+ requestId: string
16
+ }
17
+
18
+ // ─── Procedures ───────────────────────────────────────────
19
+
20
+ const RPC = Procedures<{{Name}}Context, RPCConfig>()
21
+
22
+ export const { GetItem } = RPC.Create(
23
+ 'GetItem',
24
+ {
25
+ scope: '{{name}}', // TODO: set URL scope
26
+ version: 1,
27
+ description: 'Fetch item by ID',
28
+ schema: {
29
+ params: Type.Object({
30
+ id: Type.String(),
31
+ }),
32
+ returnType: Type.Object({
33
+ id: Type.String(),
34
+ name: Type.String(),
35
+ }),
36
+ },
37
+ },
38
+ async (ctx, params) => {
39
+ // TODO: implement
40
+ return { id: params.id, name: 'Example' }
41
+ }
42
+ )
43
+
44
+ export const { ListItems } = RPC.Create(
45
+ 'ListItems',
46
+ {
47
+ scope: '{{name}}',
48
+ version: 1,
49
+ description: 'List all items',
50
+ schema: {
51
+ params: Type.Object({
52
+ page: Type.Optional(Type.Number()),
53
+ limit: Type.Optional(Type.Number()),
54
+ }),
55
+ },
56
+ },
57
+ async (ctx, params) => {
58
+ // TODO: implement
59
+ return { items: [], total: 0 }
60
+ }
61
+ )
62
+
63
+ // ─── Hono App Builder ─────────────────────────────────────
64
+
65
+ export const {{name}}App = new HonoRPCAppBuilder({
66
+ pathPrefix: '/api',
67
+ onError: (procedure, c, error) => {
68
+ if (error instanceof ProcedureValidationError) {
69
+ return c.json({
70
+ error: error.message,
71
+ details: error.errors,
72
+ procedure: error.procedureName,
73
+ }, 400)
74
+ } else if (error instanceof ProcedureError) {
75
+ return c.json({
76
+ error: error.message,
77
+ meta: error.meta,
78
+ procedure: error.procedureName,
79
+ }, 422)
80
+ }
81
+ return c.json({ error: 'Internal server error' }, 500)
82
+ },
83
+ })
84
+ .register(RPC, (c) => ({
85
+ userId: c.req.header('x-user-id') || 'anonymous',
86
+ requestId: c.req.header('x-request-id') || crypto.randomUUID(),
87
+ }))
88
+ .build()
89
+
90
+ // Route map:
91
+ // POST /api/{{name}}/get-item/1
92
+ // POST /api/{{name}}/list-items/1
93
+
94
+ // Documentation: {{name}}App.docs
95
+ ```
96
+
97
+ ## Test — `{{Name}}.rpc.test.ts`
98
+
99
+ ```typescript
100
+ import { describe, test, expect } from 'vitest'
101
+ import { {{name}}App } from './{{Name}}.rpc'
102
+
103
+ describe('{{Name}} RPC', () => {
104
+ test('GET item returns expected result', async () => {
105
+ const res = await {{name}}App.request('/api/{{name}}/get-item/1', {
106
+ method: 'POST',
107
+ headers: { 'Content-Type': 'application/json' },
108
+ body: JSON.stringify({ id: 'item-1' }),
109
+ })
110
+
111
+ expect(res.status).toBe(200)
112
+ const body = await res.json()
113
+ expect(body).toEqual({ id: 'item-1', name: 'Example' })
114
+ })
115
+
116
+ test('returns 400 for invalid params', async () => {
117
+ const res = await {{name}}App.request('/api/{{name}}/get-item/1', {
118
+ method: 'POST',
119
+ headers: { 'Content-Type': 'application/json' },
120
+ body: JSON.stringify({}),
121
+ })
122
+
123
+ expect(res.status).toBe(400)
124
+ })
125
+
126
+ test('LIST items returns array', async () => {
127
+ const res = await {{name}}App.request('/api/{{name}}/list-items/1', {
128
+ method: 'POST',
129
+ headers: { 'Content-Type': 'application/json' },
130
+ body: JSON.stringify({ page: 1, limit: 10 }),
131
+ })
132
+
133
+ expect(res.status).toBe(200)
134
+ const body = await res.json()
135
+ expect(body).toHaveProperty('items')
136
+ expect(body).toHaveProperty('total')
137
+ })
138
+ })
139
+ ```
@@ -0,0 +1,134 @@
1
+ # Hono Stream Template: {{Name}}
2
+
3
+ ## Implementation — `{{Name}}.stream-rpc.ts`
4
+
5
+ ```typescript
6
+ import { Procedures } from 'ts-procedures'
7
+ import { HonoStreamAppBuilder, sse } from 'ts-procedures/hono-stream'
8
+ import type { RPCConfig } from 'ts-procedures/http'
9
+ import { Type } from 'typebox'
10
+
11
+ // ─── Context ──────────────────────────────────────────────
12
+
13
+ type {{Name}}Context = {
14
+ userId: string
15
+ }
16
+
17
+ // ─── Stream Procedures ────────────────────────────────────
18
+
19
+ const StreamRPC = Procedures<{{Name}}Context, RPCConfig>()
20
+
21
+ export const { {{Name}}Stream } = StreamRPC.CreateStream(
22
+ '{{Name}}Stream',
23
+ {
24
+ scope: '{{name}}', // TODO: set URL scope
25
+ version: 1,
26
+ description: 'TODO: describe what this streams',
27
+ schema: {
28
+ params: Type.Object({
29
+ // TODO: define input parameters
30
+ channel: Type.String(),
31
+ }),
32
+ yieldType: Type.Object({
33
+ // TODO: define shape of each yielded value
34
+ type: Type.String(),
35
+ payload: Type.Any(),
36
+ timestamp: Type.Number(),
37
+ }),
38
+ },
39
+ },
40
+ async function* (ctx, params) {
41
+ // ctx.signal is ALWAYS present in stream handlers
42
+
43
+ // TODO: implement streaming logic
44
+ let counter = 0
45
+ while (!ctx.signal.aborted) {
46
+ const event = await pollForEvents(params.channel, { signal: ctx.signal })
47
+
48
+ // Use sse() to attach SSE metadata (event type, id, retry)
49
+ yield sse(
50
+ { type: event.type, payload: event.data, timestamp: Date.now() },
51
+ { event: event.type, id: String(counter++) }
52
+ )
53
+ }
54
+ }
55
+ )
56
+
57
+ // Placeholder — replace with real event source
58
+ async function pollForEvents(channel: string, opts?: { signal?: AbortSignal }) {
59
+ await new Promise(r => setTimeout(r, 1000))
60
+ return { type: 'update', data: { value: Math.random() } }
61
+ }
62
+
63
+ // ─── Hono Stream App Builder ──────────────────────────────
64
+
65
+ export const {{name}}StreamApp = new HonoStreamAppBuilder({
66
+ pathPrefix: '/api',
67
+ defaultStreamMode: 'sse', // or 'text' for newline-delimited JSON
68
+ onPreStreamError: (procedure, c, error) => {
69
+ return c.json({ error: error.message }, 400)
70
+ },
71
+ onMidStreamError: (procedure, c, error) => ({
72
+ data: { type: 'error', payload: { message: error.message }, timestamp: Date.now() },
73
+ closeStream: true,
74
+ }),
75
+ })
76
+ .register(StreamRPC, (c) => ({
77
+ userId: c.req.header('x-user-id') || 'anonymous',
78
+ }))
79
+ .build()
80
+
81
+ // Routes:
82
+ // GET /api/{{name}}/{{name}}-stream/1?channel=test
83
+ // POST /api/{{name}}/{{name}}-stream/1 (body: { channel: "test" })
84
+
85
+ // SSE output per yield:
86
+ // event: {{Name}}Stream
87
+ // id: 0
88
+ // data: {"type":"update","payload":{...},"timestamp":1234567890}
89
+ ```
90
+
91
+ ## Test — `{{Name}}.stream-rpc.test.ts`
92
+
93
+ ```typescript
94
+ import { describe, test, expect } from 'vitest'
95
+ import { {{name}}StreamApp } from './{{Name}}.stream-rpc'
96
+
97
+ describe('{{Name}} Stream RPC', () => {
98
+ test('GET returns SSE stream', async () => {
99
+ const res = await {{name}}StreamApp.request(
100
+ '/api/{{name}}/{{name}}-stream/1?channel=test'
101
+ )
102
+
103
+ expect(res.status).toBe(200)
104
+ expect(res.headers.get('content-type')).toContain('text/event-stream')
105
+ })
106
+
107
+ test('POST returns SSE stream', async () => {
108
+ const res = await {{name}}StreamApp.request(
109
+ '/api/{{name}}/{{name}}-stream/1',
110
+ {
111
+ method: 'POST',
112
+ headers: { 'Content-Type': 'application/json' },
113
+ body: JSON.stringify({ channel: 'test' }),
114
+ }
115
+ )
116
+
117
+ expect(res.status).toBe(200)
118
+ expect(res.headers.get('content-type')).toContain('text/event-stream')
119
+ })
120
+
121
+ test('returns 400 for invalid params', async () => {
122
+ const res = await {{name}}StreamApp.request(
123
+ '/api/{{name}}/{{name}}-stream/1',
124
+ {
125
+ method: 'POST',
126
+ headers: { 'Content-Type': 'application/json' },
127
+ body: JSON.stringify({}),
128
+ }
129
+ )
130
+
131
+ expect(res.status).toBe(400)
132
+ })
133
+ })
134
+ ```
@@ -0,0 +1,77 @@
1
+ # Procedure Template: {{Name}}
2
+
3
+ ## Implementation — `{{Name}}.procedure.ts`
4
+
5
+ ```typescript
6
+ import { Procedures } from 'ts-procedures'
7
+ import { Type } from 'typebox'
8
+
9
+ // Define context type (adjust to match your app's context)
10
+ type AppContext = {
11
+ userId: string
12
+ signal?: AbortSignal
13
+ }
14
+
15
+ const { Create } = Procedures<AppContext>()
16
+
17
+ export const { {{Name}}, procedure: {{Name}}Procedure, info: {{Name}}Info } = Create(
18
+ '{{Name}}',
19
+ {
20
+ description: 'TODO: describe what {{Name}} does',
21
+ schema: {
22
+ params: Type.Object({
23
+ // TODO: define input parameters
24
+ id: Type.String(),
25
+ }),
26
+ returnType: Type.Object({
27
+ // TODO: define return shape (documentation only, not validated at runtime)
28
+ id: Type.String(),
29
+ // ...fields
30
+ }),
31
+ },
32
+ },
33
+ async (ctx, params) => {
34
+ // params.id is guaranteed to be a string (validated by AJV)
35
+
36
+ // Use ctx.error() for business logic errors
37
+ // throw ctx.error('Not found', { code: 'NOT_FOUND', id: params.id })
38
+
39
+ // Pass ctx.signal to downstream async calls
40
+ // const data = await fetch(url, { signal: ctx.signal })
41
+
42
+ // TODO: implement handler logic
43
+ return { id: params.id }
44
+ }
45
+ )
46
+ ```
47
+
48
+ ## Test — `{{Name}}.procedure.test.ts`
49
+
50
+ ```typescript
51
+ import { describe, test, expect } from 'vitest'
52
+ import { ProcedureError, ProcedureValidationError } from 'ts-procedures'
53
+ import { {{Name}} } from './{{Name}}.procedure'
54
+
55
+ const mockContext = { userId: 'test-user' }
56
+
57
+ describe('{{Name}}', () => {
58
+ test('returns expected result for valid params', async () => {
59
+ const result = await {{Name}}(mockContext, { id: 'test-id' })
60
+ expect(result).toBeDefined()
61
+ expect(result.id).toBe('test-id')
62
+ })
63
+
64
+ test('throws ProcedureValidationError for invalid params', async () => {
65
+ await expect(
66
+ {{Name}}(mockContext, {} as any)
67
+ ).rejects.toThrow(ProcedureValidationError)
68
+ })
69
+
70
+ test('throws ProcedureError for business logic failures', async () => {
71
+ // TODO: test ctx.error() scenarios
72
+ // await expect(
73
+ // {{Name}}(mockContext, { id: 'invalid' })
74
+ // ).rejects.toThrow(ProcedureError)
75
+ })
76
+ })
77
+ ```
@@ -0,0 +1,113 @@
1
+ # Stream Procedure Template: {{Name}}
2
+
3
+ ## Implementation — `{{Name}}.stream.ts`
4
+
5
+ ```typescript
6
+ import { Procedures } from 'ts-procedures'
7
+ import { Type } from 'typebox'
8
+
9
+ type AppContext = {
10
+ userId: string
11
+ signal?: AbortSignal
12
+ }
13
+
14
+ const { CreateStream } = Procedures<AppContext>()
15
+
16
+ export const { {{Name}}, procedure: {{Name}}Procedure, info: {{Name}}Info } = CreateStream(
17
+ '{{Name}}',
18
+ {
19
+ description: 'TODO: describe what {{Name}} streams',
20
+ schema: {
21
+ params: Type.Object({
22
+ // TODO: define input parameters
23
+ channel: Type.String(),
24
+ }),
25
+ yieldType: Type.Object({
26
+ // TODO: define shape of each yielded value
27
+ id: Type.String(),
28
+ data: Type.Any(),
29
+ timestamp: Type.Number(),
30
+ }),
31
+ },
32
+ // Set to true to validate each yielded value against yieldType schema
33
+ // validateYields: false,
34
+ },
35
+ async function* (ctx, params) {
36
+ // ctx.signal is ALWAYS present in stream handlers (guaranteed AbortSignal)
37
+
38
+ // TODO: implement streaming logic
39
+ // Example: poll-based streaming
40
+ let counter = 0
41
+ while (!ctx.signal.aborted) {
42
+ const data = await fetchLatestData(params.channel, { signal: ctx.signal })
43
+
44
+ yield {
45
+ id: String(counter++),
46
+ data,
47
+ timestamp: Date.now(),
48
+ }
49
+
50
+ // Wait between polls (pass signal for cancellation)
51
+ await new Promise((resolve, reject) => {
52
+ const timeout = setTimeout(resolve, 1000)
53
+ ctx.signal.addEventListener('abort', () => {
54
+ clearTimeout(timeout)
55
+ resolve(undefined)
56
+ }, { once: true })
57
+ })
58
+ }
59
+
60
+ // After loop exits, signal.reason tells you why:
61
+ // - 'stream-completed': consumer finished reading (normal)
62
+ // - other: client disconnected or external abort
63
+ }
64
+ )
65
+
66
+ // Placeholder — replace with real data fetching
67
+ async function fetchLatestData(channel: string, opts?: { signal?: AbortSignal }) {
68
+ return { value: Math.random() }
69
+ }
70
+ ```
71
+
72
+ ## Test — `{{Name}}.stream.test.ts`
73
+
74
+ ```typescript
75
+ import { describe, test, expect } from 'vitest'
76
+ import { ProcedureValidationError } from 'ts-procedures'
77
+ import { {{Name}} } from './{{Name}}.stream'
78
+
79
+ const mockContext = { userId: 'test-user' }
80
+
81
+ describe('{{Name}}', () => {
82
+ test('yields expected values', async () => {
83
+ const values = []
84
+ for await (const val of {{Name}}(mockContext, { channel: 'test' })) {
85
+ values.push(val)
86
+ if (values.length >= 3) break // Don't consume indefinitely
87
+ }
88
+
89
+ expect(values).toHaveLength(3)
90
+ expect(values[0]).toHaveProperty('id')
91
+ expect(values[0]).toHaveProperty('data')
92
+ expect(values[0]).toHaveProperty('timestamp')
93
+ })
94
+
95
+ test('throws ProcedureValidationError for invalid params', async () => {
96
+ const gen = {{Name}}(mockContext, {} as any)
97
+ await expect(gen.next()).rejects.toThrow(ProcedureValidationError)
98
+ })
99
+
100
+ test('respects signal abort', async () => {
101
+ const ac = new AbortController()
102
+ const values = []
103
+
104
+ setTimeout(() => ac.abort(), 100)
105
+
106
+ for await (const val of {{Name}}({ ...mockContext, signal: ac.signal }, { channel: 'test' })) {
107
+ values.push(val)
108
+ }
109
+
110
+ expect(values.length).toBeGreaterThanOrEqual(0)
111
+ })
112
+ })
113
+ ```