ts-procedures 5.9.1 → 5.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (305) hide show
  1. package/README.md +1 -1
  2. package/agent_config/bin/postinstall.mjs +3 -3
  3. package/agent_config/bin/setup.mjs +22 -11
  4. package/agent_config/claude-code/agents/ts-procedures-architect.md +46 -101
  5. package/agent_config/claude-code/skills/{guide → ts-procedures}/SKILL.md +50 -35
  6. package/agent_config/claude-code/skills/{guide → ts-procedures}/anti-patterns.md +6 -5
  7. package/agent_config/claude-code/skills/{guide → ts-procedures}/api-reference.md +60 -49
  8. package/agent_config/claude-code/skills/ts-procedures-review/SKILL.md +48 -0
  9. package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/SKILL.md +19 -24
  10. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/client.md +115 -0
  11. package/agent_config/lib/install-claude.mjs +35 -87
  12. package/build/src/client/call.d.ts +14 -0
  13. package/build/src/client/call.js +47 -0
  14. package/build/src/client/call.js.map +1 -0
  15. package/build/src/client/call.test.d.ts +1 -0
  16. package/build/src/client/call.test.js +124 -0
  17. package/build/src/client/call.test.js.map +1 -0
  18. package/build/src/client/errors.d.ts +25 -0
  19. package/build/src/client/errors.js +33 -0
  20. package/build/src/client/errors.js.map +1 -0
  21. package/build/src/client/errors.test.d.ts +1 -0
  22. package/build/src/client/errors.test.js +41 -0
  23. package/build/src/client/errors.test.js.map +1 -0
  24. package/build/src/client/fetch-adapter.d.ts +12 -0
  25. package/build/src/client/fetch-adapter.js +156 -0
  26. package/build/src/client/fetch-adapter.js.map +1 -0
  27. package/build/src/client/fetch-adapter.test.d.ts +1 -0
  28. package/build/src/client/fetch-adapter.test.js +271 -0
  29. package/build/src/client/fetch-adapter.test.js.map +1 -0
  30. package/build/src/client/hooks.d.ts +17 -0
  31. package/build/src/client/hooks.js +40 -0
  32. package/build/src/client/hooks.js.map +1 -0
  33. package/build/src/client/hooks.test.d.ts +1 -0
  34. package/build/src/client/hooks.test.js +163 -0
  35. package/build/src/client/hooks.test.js.map +1 -0
  36. package/build/src/client/index.d.ts +22 -0
  37. package/build/src/client/index.js +67 -0
  38. package/build/src/client/index.js.map +1 -0
  39. package/build/src/client/index.test.d.ts +1 -0
  40. package/build/src/client/index.test.js +231 -0
  41. package/build/src/client/index.test.js.map +1 -0
  42. package/build/src/client/request-builder.d.ts +13 -0
  43. package/build/src/client/request-builder.js +53 -0
  44. package/build/src/client/request-builder.js.map +1 -0
  45. package/build/src/client/request-builder.test.d.ts +1 -0
  46. package/build/src/client/request-builder.test.js +160 -0
  47. package/build/src/client/request-builder.test.js.map +1 -0
  48. package/build/src/client/stream.d.ts +27 -0
  49. package/build/src/client/stream.js +118 -0
  50. package/build/src/client/stream.js.map +1 -0
  51. package/build/src/client/stream.test.d.ts +1 -0
  52. package/build/src/client/stream.test.js +228 -0
  53. package/build/src/client/stream.test.js.map +1 -0
  54. package/build/src/client/types.d.ts +78 -0
  55. package/build/src/client/types.js +3 -0
  56. package/build/src/client/types.js.map +1 -0
  57. package/build/src/codegen/bin/cli.d.ts +45 -0
  58. package/build/src/codegen/bin/cli.js +246 -0
  59. package/build/src/codegen/bin/cli.js.map +1 -0
  60. package/build/src/codegen/bin/cli.test.d.ts +1 -0
  61. package/build/src/codegen/bin/cli.test.js +220 -0
  62. package/build/src/codegen/bin/cli.test.js.map +1 -0
  63. package/build/src/codegen/constants.d.ts +1 -0
  64. package/build/src/codegen/constants.js +2 -0
  65. package/build/src/codegen/constants.js.map +1 -0
  66. package/build/src/codegen/e2e.test.d.ts +1 -0
  67. package/build/src/codegen/e2e.test.js +464 -0
  68. package/build/src/codegen/e2e.test.js.map +1 -0
  69. package/build/src/codegen/emit-client-runtime.d.ts +9 -0
  70. package/build/src/codegen/emit-client-runtime.js +99 -0
  71. package/build/src/codegen/emit-client-runtime.js.map +1 -0
  72. package/build/src/codegen/emit-client-runtime.test.d.ts +1 -0
  73. package/build/src/codegen/emit-client-runtime.test.js +78 -0
  74. package/build/src/codegen/emit-client-runtime.test.js.map +1 -0
  75. package/build/src/codegen/emit-client-types.d.ts +8 -0
  76. package/build/src/codegen/emit-client-types.js +25 -0
  77. package/build/src/codegen/emit-client-types.js.map +1 -0
  78. package/build/src/codegen/emit-client-types.test.d.ts +1 -0
  79. package/build/src/codegen/emit-client-types.test.js +33 -0
  80. package/build/src/codegen/emit-client-types.test.js.map +1 -0
  81. package/build/src/codegen/emit-errors.d.ts +19 -0
  82. package/build/src/codegen/emit-errors.js +59 -0
  83. package/build/src/codegen/emit-errors.js.map +1 -0
  84. package/build/src/codegen/emit-errors.test.d.ts +1 -0
  85. package/build/src/codegen/emit-errors.test.js +175 -0
  86. package/build/src/codegen/emit-errors.test.js.map +1 -0
  87. package/build/src/codegen/emit-index.d.ts +12 -0
  88. package/build/src/codegen/emit-index.js +41 -0
  89. package/build/src/codegen/emit-index.js.map +1 -0
  90. package/build/src/codegen/emit-index.test.d.ts +1 -0
  91. package/build/src/codegen/emit-index.test.js +106 -0
  92. package/build/src/codegen/emit-index.test.js.map +1 -0
  93. package/build/src/codegen/emit-scope.d.ts +15 -0
  94. package/build/src/codegen/emit-scope.js +299 -0
  95. package/build/src/codegen/emit-scope.js.map +1 -0
  96. package/build/src/codegen/emit-scope.test.d.ts +1 -0
  97. package/build/src/codegen/emit-scope.test.js +559 -0
  98. package/build/src/codegen/emit-scope.test.js.map +1 -0
  99. package/build/src/codegen/emit-types.d.ts +43 -0
  100. package/build/src/codegen/emit-types.js +111 -0
  101. package/build/src/codegen/emit-types.js.map +1 -0
  102. package/build/src/codegen/emit-types.test.d.ts +1 -0
  103. package/build/src/codegen/emit-types.test.js +184 -0
  104. package/build/src/codegen/emit-types.test.js.map +1 -0
  105. package/build/src/codegen/group-routes.d.ts +23 -0
  106. package/build/src/codegen/group-routes.js +46 -0
  107. package/build/src/codegen/group-routes.js.map +1 -0
  108. package/build/src/codegen/group-routes.test.d.ts +1 -0
  109. package/build/src/codegen/group-routes.test.js +131 -0
  110. package/build/src/codegen/group-routes.test.js.map +1 -0
  111. package/build/src/codegen/index.d.ts +15 -0
  112. package/build/src/codegen/index.js +16 -0
  113. package/build/src/codegen/index.js.map +1 -0
  114. package/build/src/codegen/naming.d.ts +7 -0
  115. package/build/src/codegen/naming.js +21 -0
  116. package/build/src/codegen/naming.js.map +1 -0
  117. package/build/src/codegen/naming.test.d.ts +1 -0
  118. package/build/src/codegen/naming.test.js +40 -0
  119. package/build/src/codegen/naming.test.js.map +1 -0
  120. package/build/src/codegen/pipeline.d.ts +17 -0
  121. package/build/src/codegen/pipeline.js +78 -0
  122. package/build/src/codegen/pipeline.js.map +1 -0
  123. package/build/src/codegen/pipeline.test.d.ts +1 -0
  124. package/build/src/codegen/pipeline.test.js +269 -0
  125. package/build/src/codegen/pipeline.test.js.map +1 -0
  126. package/build/src/codegen/resolve-envelope.d.ts +7 -0
  127. package/build/src/codegen/resolve-envelope.js +46 -0
  128. package/build/src/codegen/resolve-envelope.js.map +1 -0
  129. package/build/src/codegen/resolve-envelope.test.d.ts +1 -0
  130. package/build/src/codegen/resolve-envelope.test.js +69 -0
  131. package/build/src/codegen/resolve-envelope.test.js.map +1 -0
  132. package/build/src/errors.d.ts +33 -0
  133. package/build/src/errors.js +91 -0
  134. package/build/src/errors.js.map +1 -0
  135. package/build/src/errors.test.d.ts +1 -0
  136. package/build/src/errors.test.js +122 -0
  137. package/build/src/errors.test.js.map +1 -0
  138. package/build/src/exports.d.ts +7 -0
  139. package/build/src/exports.js +8 -0
  140. package/build/src/exports.js.map +1 -0
  141. package/build/src/implementations/http/doc-registry.d.ts +12 -0
  142. package/build/src/implementations/http/doc-registry.js +114 -0
  143. package/build/src/implementations/http/doc-registry.js.map +1 -0
  144. package/build/src/implementations/http/doc-registry.test.d.ts +1 -0
  145. package/build/src/implementations/http/doc-registry.test.js +347 -0
  146. package/build/src/implementations/http/doc-registry.test.js.map +1 -0
  147. package/build/src/implementations/http/express-rpc/index.d.ts +94 -0
  148. package/build/src/implementations/http/express-rpc/index.js +185 -0
  149. package/build/src/implementations/http/express-rpc/index.js.map +1 -0
  150. package/build/src/implementations/http/express-rpc/index.test.d.ts +1 -0
  151. package/build/src/implementations/http/express-rpc/index.test.js +684 -0
  152. package/build/src/implementations/http/express-rpc/index.test.js.map +1 -0
  153. package/build/src/implementations/http/express-rpc/types.d.ts +11 -0
  154. package/build/src/implementations/http/express-rpc/types.js +2 -0
  155. package/build/src/implementations/http/express-rpc/types.js.map +1 -0
  156. package/build/src/implementations/http/hono-api/index.d.ts +102 -0
  157. package/build/src/implementations/http/hono-api/index.js +341 -0
  158. package/build/src/implementations/http/hono-api/index.js.map +1 -0
  159. package/build/src/implementations/http/hono-api/index.test.d.ts +1 -0
  160. package/build/src/implementations/http/hono-api/index.test.js +992 -0
  161. package/build/src/implementations/http/hono-api/index.test.js.map +1 -0
  162. package/build/src/implementations/http/hono-api/types.d.ts +13 -0
  163. package/build/src/implementations/http/hono-api/types.js +2 -0
  164. package/build/src/implementations/http/hono-api/types.js.map +1 -0
  165. package/build/src/implementations/http/hono-rpc/index.d.ts +92 -0
  166. package/build/src/implementations/http/hono-rpc/index.js +161 -0
  167. package/build/src/implementations/http/hono-rpc/index.js.map +1 -0
  168. package/build/src/implementations/http/hono-rpc/index.test.d.ts +1 -0
  169. package/build/src/implementations/http/hono-rpc/index.test.js +803 -0
  170. package/build/src/implementations/http/hono-rpc/index.test.js.map +1 -0
  171. package/build/src/implementations/http/hono-rpc/types.d.ts +11 -0
  172. package/build/src/implementations/http/hono-rpc/types.js +2 -0
  173. package/build/src/implementations/http/hono-rpc/types.js.map +1 -0
  174. package/build/src/implementations/http/hono-stream/index.d.ts +120 -0
  175. package/build/src/implementations/http/hono-stream/index.js +309 -0
  176. package/build/src/implementations/http/hono-stream/index.js.map +1 -0
  177. package/build/src/implementations/http/hono-stream/index.test.d.ts +1 -0
  178. package/build/src/implementations/http/hono-stream/index.test.js +1356 -0
  179. package/build/src/implementations/http/hono-stream/index.test.js.map +1 -0
  180. package/build/src/implementations/http/hono-stream/types.d.ts +15 -0
  181. package/build/src/implementations/http/hono-stream/types.js +2 -0
  182. package/build/src/implementations/http/hono-stream/types.js.map +1 -0
  183. package/build/src/implementations/types.d.ts +142 -0
  184. package/build/src/implementations/types.js +2 -0
  185. package/build/src/implementations/types.js.map +1 -0
  186. package/build/src/index.d.ts +165 -0
  187. package/build/src/index.js +253 -0
  188. package/build/src/index.js.map +1 -0
  189. package/build/src/index.test.d.ts +1 -0
  190. package/build/src/index.test.js +890 -0
  191. package/build/src/index.test.js.map +1 -0
  192. package/build/src/schema/compute-schema.d.ts +35 -0
  193. package/build/src/schema/compute-schema.js +41 -0
  194. package/build/src/schema/compute-schema.js.map +1 -0
  195. package/build/src/schema/compute-schema.test.d.ts +1 -0
  196. package/build/src/schema/compute-schema.test.js +107 -0
  197. package/build/src/schema/compute-schema.test.js.map +1 -0
  198. package/build/src/schema/extract-json-schema.d.ts +2 -0
  199. package/build/src/schema/extract-json-schema.js +12 -0
  200. package/build/src/schema/extract-json-schema.js.map +1 -0
  201. package/build/src/schema/extract-json-schema.test.d.ts +1 -0
  202. package/build/src/schema/extract-json-schema.test.js +23 -0
  203. package/build/src/schema/extract-json-schema.test.js.map +1 -0
  204. package/build/src/schema/parser.d.ts +28 -0
  205. package/build/src/schema/parser.js +170 -0
  206. package/build/src/schema/parser.js.map +1 -0
  207. package/build/src/schema/parser.test.d.ts +1 -0
  208. package/build/src/schema/parser.test.js +120 -0
  209. package/build/src/schema/parser.test.js.map +1 -0
  210. package/build/src/schema/resolve-schema-lib.d.ts +12 -0
  211. package/build/src/schema/resolve-schema-lib.js +11 -0
  212. package/build/src/schema/resolve-schema-lib.js.map +1 -0
  213. package/build/src/schema/resolve-schema-lib.test.d.ts +1 -0
  214. package/build/src/schema/resolve-schema-lib.test.js +17 -0
  215. package/build/src/schema/resolve-schema-lib.test.js.map +1 -0
  216. package/build/src/schema/types.d.ts +8 -0
  217. package/build/src/schema/types.js +2 -0
  218. package/build/src/schema/types.js.map +1 -0
  219. package/build/src/stack-utils.d.ts +25 -0
  220. package/build/src/stack-utils.js +95 -0
  221. package/build/src/stack-utils.js.map +1 -0
  222. package/build/src/stack-utils.test.d.ts +1 -0
  223. package/build/src/stack-utils.test.js +80 -0
  224. package/build/src/stack-utils.test.js.map +1 -0
  225. package/docs/ai-agent-setup.md +7 -6
  226. package/docs/core.md +5 -9
  227. package/docs/streaming.md +9 -9
  228. package/package.json +2 -13
  229. package/src/client/call.test.ts +162 -0
  230. package/src/client/errors.test.ts +43 -0
  231. package/src/client/fetch-adapter.test.ts +340 -0
  232. package/src/client/hooks.test.ts +191 -0
  233. package/src/client/index.test.ts +290 -0
  234. package/src/client/request-builder.test.ts +184 -0
  235. package/src/client/stream.test.ts +331 -0
  236. package/src/codegen/bin/cli.test.ts +260 -0
  237. package/src/codegen/bin/cli.ts +282 -0
  238. package/src/codegen/constants.ts +1 -0
  239. package/src/codegen/e2e.test.ts +565 -0
  240. package/src/codegen/emit-client-runtime.test.ts +93 -0
  241. package/src/codegen/emit-client-runtime.ts +114 -0
  242. package/src/codegen/emit-client-types.test.ts +39 -0
  243. package/src/codegen/emit-client-types.ts +27 -0
  244. package/src/codegen/emit-errors.test.ts +202 -0
  245. package/src/codegen/emit-errors.ts +80 -0
  246. package/src/codegen/emit-index.test.ts +127 -0
  247. package/src/codegen/emit-index.ts +58 -0
  248. package/src/codegen/emit-scope.test.ts +624 -0
  249. package/src/codegen/emit-scope.ts +389 -0
  250. package/src/codegen/emit-types.test.ts +205 -0
  251. package/src/codegen/emit-types.ts +158 -0
  252. package/src/codegen/group-routes.test.ts +159 -0
  253. package/src/codegen/group-routes.ts +61 -0
  254. package/src/codegen/index.ts +30 -0
  255. package/src/codegen/naming.test.ts +50 -0
  256. package/src/codegen/naming.ts +25 -0
  257. package/src/codegen/pipeline.test.ts +316 -0
  258. package/src/codegen/pipeline.ts +108 -0
  259. package/src/codegen/resolve-envelope.test.ts +76 -0
  260. package/src/codegen/resolve-envelope.ts +61 -0
  261. package/src/errors.test.ts +163 -0
  262. package/src/errors.ts +107 -0
  263. package/src/exports.ts +7 -0
  264. package/src/implementations/http/doc-registry.test.ts +415 -0
  265. package/src/implementations/http/doc-registry.ts +143 -0
  266. package/src/implementations/http/express-rpc/README.md +6 -6
  267. package/src/implementations/http/express-rpc/index.test.ts +957 -0
  268. package/src/implementations/http/express-rpc/index.ts +266 -0
  269. package/src/implementations/http/express-rpc/types.ts +16 -0
  270. package/src/implementations/http/hono-api/index.test.ts +1341 -0
  271. package/src/implementations/http/hono-api/index.ts +463 -0
  272. package/src/implementations/http/hono-api/types.ts +16 -0
  273. package/src/implementations/http/hono-rpc/README.md +6 -6
  274. package/src/implementations/http/hono-rpc/index.test.ts +1075 -0
  275. package/src/implementations/http/hono-rpc/index.ts +238 -0
  276. package/src/implementations/http/hono-rpc/types.ts +16 -0
  277. package/src/implementations/http/hono-stream/README.md +12 -12
  278. package/src/implementations/http/hono-stream/index.test.ts +1768 -0
  279. package/src/implementations/http/hono-stream/index.ts +456 -0
  280. package/src/implementations/http/hono-stream/types.ts +20 -0
  281. package/src/implementations/types.ts +174 -0
  282. package/src/index.test.ts +1185 -0
  283. package/src/index.ts +522 -0
  284. package/src/schema/compute-schema.test.ts +128 -0
  285. package/src/schema/compute-schema.ts +88 -0
  286. package/src/schema/extract-json-schema.test.ts +25 -0
  287. package/src/schema/extract-json-schema.ts +15 -0
  288. package/src/schema/parser.test.ts +182 -0
  289. package/src/schema/parser.ts +215 -0
  290. package/src/schema/resolve-schema-lib.test.ts +19 -0
  291. package/src/schema/resolve-schema-lib.ts +29 -0
  292. package/src/schema/types.ts +20 -0
  293. package/src/stack-utils.test.ts +94 -0
  294. package/src/stack-utils.ts +129 -0
  295. package/agent_config/claude-code/skills/review/SKILL.md +0 -53
  296. package/docs/superpowers/plans/2026-03-30-client-codegen.md +0 -2833
  297. package/docs/superpowers/specs/2026-03-30-client-codegen-design.md +0 -632
  298. /package/agent_config/claude-code/skills/{guide → ts-procedures}/patterns.md +0 -0
  299. /package/agent_config/claude-code/skills/{review → ts-procedures-review}/checklist.md +0 -0
  300. /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/express-rpc.md +0 -0
  301. /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/hono-api.md +0 -0
  302. /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/hono-rpc.md +0 -0
  303. /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/hono-stream.md +0 -0
  304. /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/procedure.md +0 -0
  305. /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/stream-procedure.md +0 -0
@@ -0,0 +1,316 @@
1
+ import { describe, it, expect, afterEach, vi } from 'vitest'
2
+ import { generateClient } from './index.js'
3
+ import { runPipeline } from './pipeline.js'
4
+ import { mkdirSync, rmSync, readFileSync, existsSync } from 'node:fs'
5
+ import { join } from 'node:path'
6
+ import { tmpdir } from 'node:os'
7
+ import type { DocEnvelope } from '../implementations/types.js'
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Fixtures
11
+ // ---------------------------------------------------------------------------
12
+
13
+ const envelope: DocEnvelope = {
14
+ basePath: '/api',
15
+ headers: [],
16
+ errors: [],
17
+ routes: [
18
+ {
19
+ kind: 'rpc',
20
+ name: 'GetUser',
21
+ path: '/users/1',
22
+ method: 'post',
23
+ scope: 'users',
24
+ version: 1,
25
+ jsonSchema: {
26
+ body: {
27
+ type: 'object',
28
+ properties: { id: { type: 'string' } },
29
+ required: ['id'],
30
+ },
31
+ response: {
32
+ type: 'object',
33
+ properties: { name: { type: 'string' } },
34
+ required: ['name'],
35
+ },
36
+ },
37
+ },
38
+ {
39
+ kind: 'rpc',
40
+ name: 'CreateInvoice',
41
+ path: '/billing/1',
42
+ method: 'post',
43
+ scope: 'billing',
44
+ version: 1,
45
+ jsonSchema: {
46
+ body: {
47
+ type: 'object',
48
+ properties: { amount: { type: 'number' } },
49
+ required: ['amount'],
50
+ },
51
+ response: {
52
+ type: 'object',
53
+ properties: { invoiceId: { type: 'string' } },
54
+ required: ['invoiceId'],
55
+ },
56
+ },
57
+ },
58
+ ],
59
+ }
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Helpers
63
+ // ---------------------------------------------------------------------------
64
+
65
+ function makeTmpDir(): string {
66
+ const dir = join(tmpdir(), `ts-proc-pipeline-test-${Date.now()}-${Math.random().toString(36).slice(2)}`)
67
+ mkdirSync(dir, { recursive: true })
68
+ return dir
69
+ }
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Tests
73
+ // ---------------------------------------------------------------------------
74
+
75
+ describe('generateClient pipeline', () => {
76
+ let tmpDir: string
77
+
78
+ afterEach(() => {
79
+ if (tmpDir && existsSync(tmpDir)) {
80
+ rmSync(tmpDir, { recursive: true, force: true })
81
+ }
82
+ })
83
+
84
+ it('creates scope file for each unique scope (users.ts, billing.ts)', async () => {
85
+ tmpDir = makeTmpDir()
86
+ await generateClient({ envelope, outDir: tmpDir })
87
+
88
+ expect(existsSync(join(tmpDir, 'users.ts'))).toBe(true)
89
+ expect(existsSync(join(tmpDir, 'billing.ts'))).toBe(true)
90
+ })
91
+
92
+ it('creates index.ts', async () => {
93
+ tmpDir = makeTmpDir()
94
+ await generateClient({ envelope, outDir: tmpDir })
95
+
96
+ expect(existsSync(join(tmpDir, 'index.ts'))).toBe(true)
97
+ })
98
+
99
+ it('users.ts contains bindUsersScope function', async () => {
100
+ tmpDir = makeTmpDir()
101
+ await generateClient({ envelope, outDir: tmpDir })
102
+
103
+ const content = readFileSync(join(tmpDir, 'users.ts'), 'utf-8')
104
+ expect(content).toContain('bindUsersScope')
105
+ expect(content).toContain('GetUser')
106
+ })
107
+
108
+ it('billing.ts contains bindBillingScope function', async () => {
109
+ tmpDir = makeTmpDir()
110
+ await generateClient({ envelope, outDir: tmpDir })
111
+
112
+ const content = readFileSync(join(tmpDir, 'billing.ts'), 'utf-8')
113
+ expect(content).toContain('bindBillingScope')
114
+ expect(content).toContain('CreateInvoice')
115
+ })
116
+
117
+ it('index.ts exports from both scope files and has createScopeBindings', async () => {
118
+ tmpDir = makeTmpDir()
119
+ await generateClient({ envelope, outDir: tmpDir })
120
+
121
+ const content = readFileSync(join(tmpDir, 'index.ts'), 'utf-8')
122
+ expect(content).toContain("from './users'")
123
+ expect(content).toContain("from './billing'")
124
+ expect(content).toContain('createScopeBindings')
125
+ })
126
+
127
+ it('creates outDir if it does not exist', async () => {
128
+ tmpDir = makeTmpDir()
129
+ const nested = join(tmpDir, 'nested', 'output')
130
+ await generateClient({ envelope, outDir: nested })
131
+
132
+ expect(existsSync(join(nested, 'index.ts'))).toBe(true)
133
+ })
134
+
135
+ it('all generated files have the auto-generated header comment', async () => {
136
+ tmpDir = makeTmpDir()
137
+ await generateClient({ envelope, outDir: tmpDir })
138
+
139
+ for (const file of ['users.ts', 'billing.ts', 'index.ts']) {
140
+ const content = readFileSync(join(tmpDir, file), 'utf-8')
141
+ expect(content).toContain('// Auto-generated by ts-procedures-codegen — do not edit')
142
+ }
143
+ })
144
+
145
+ it('generated files include source hash comment', async () => {
146
+ tmpDir = makeTmpDir()
147
+ await generateClient({ envelope, outDir: tmpDir })
148
+
149
+ for (const file of ['users.ts', 'billing.ts', 'index.ts']) {
150
+ const content = readFileSync(join(tmpDir, file), 'utf-8')
151
+ const lines = content.split('\n')
152
+ expect(lines[1]).toMatch(/^\/\/ Source hash: [a-f0-9]{32}$/)
153
+ }
154
+ })
155
+
156
+ it('dry-run mode does not create files on disk', async () => {
157
+ tmpDir = makeTmpDir()
158
+ const dryDir = join(tmpDir, 'dry-output')
159
+ await runPipeline({ envelope, outDir: dryDir, dryRun: true })
160
+ expect(existsSync(dryDir)).toBe(false)
161
+ })
162
+
163
+ it('dry-run mode returns generated file entries', async () => {
164
+ tmpDir = makeTmpDir()
165
+ const dryDir = join(tmpDir, 'dry-output')
166
+ const files = await runPipeline({ envelope, outDir: dryDir, dryRun: true })
167
+ expect(files.length).toBe(3) // users.ts, billing.ts, index.ts
168
+ const paths = files.map((f) => f.path)
169
+ expect(paths).toContain(join(dryDir, 'users.ts'))
170
+ expect(paths).toContain(join(dryDir, 'billing.ts'))
171
+ expect(paths).toContain(join(dryDir, 'index.ts'))
172
+ for (const file of files) {
173
+ expect(file.code).toContain('// Auto-generated by ts-procedures-codegen — do not edit')
174
+ }
175
+ })
176
+
177
+ it('selfContained: true produces _types.ts and _client.ts', async () => {
178
+ tmpDir = makeTmpDir()
179
+ const files = await runPipeline({ envelope, outDir: tmpDir, selfContained: true })
180
+ const paths = files.map((f) => f.path)
181
+ expect(paths).toContain(join(tmpDir, '_types.ts'))
182
+ expect(paths).toContain(join(tmpDir, '_client.ts'))
183
+
184
+ // Verify both files have auto-generated header and source hash
185
+ const typesFile = files.find((f) => f.path.endsWith('_types.ts'))!
186
+ const clientFile = files.find((f) => f.path.endsWith('_client.ts'))!
187
+ expect(typesFile.code).toContain('// Auto-generated by ts-procedures-codegen — do not edit')
188
+ expect(clientFile.code).toContain('// Auto-generated by ts-procedures-codegen — do not edit')
189
+ const typesLines = typesFile.code.split('\n')
190
+ expect(typesLines[1]).toMatch(/^\/\/ Source hash: [a-f0-9]{32}$/)
191
+ const clientLines = clientFile.code.split('\n')
192
+ expect(clientLines[1]).toMatch(/^\/\/ Source hash: [a-f0-9]{32}$/)
193
+ })
194
+
195
+ it('selfContained: true makes scope files import from ./_types', async () => {
196
+ tmpDir = makeTmpDir()
197
+ const files = await runPipeline({ envelope, outDir: tmpDir, selfContained: true })
198
+ const scopeFiles = files.filter((f) => !f.path.endsWith('index.ts') && !f.path.startsWith(join(tmpDir, '_')))
199
+ expect(scopeFiles.length).toBeGreaterThan(0)
200
+ for (const file of scopeFiles) {
201
+ expect(file.code).toContain("from './_types'")
202
+ expect(file.code).not.toContain("from 'ts-procedures/client'")
203
+ }
204
+ })
205
+
206
+ it('selfContained + clientImportPath emits a console.warn', async () => {
207
+ tmpDir = makeTmpDir()
208
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
209
+ try {
210
+ await runPipeline({ envelope, outDir: tmpDir, selfContained: true, clientImportPath: './my-client' })
211
+ expect(warnSpy).toHaveBeenCalledWith(
212
+ '[ts-procedures-codegen] --self-contained overrides --client-import-path; using ./_types'
213
+ )
214
+ } finally {
215
+ warnSpy.mockRestore()
216
+ }
217
+ })
218
+
219
+ it('serviceName + namespaceTypes produces prefixed error namespace in _errors.ts', async () => {
220
+ tmpDir = makeTmpDir()
221
+ const envelopeWithErrors: DocEnvelope = {
222
+ ...envelope,
223
+ errors: [
224
+ {
225
+ name: 'ProcedureError',
226
+ statusCode: 500,
227
+ description: 'Handler error',
228
+ schema: {
229
+ type: 'object',
230
+ properties: { name: { type: 'string' }, message: { type: 'string' } },
231
+ required: ['name', 'message'],
232
+ },
233
+ },
234
+ ],
235
+ }
236
+ await generateClient({ envelope: envelopeWithErrors, outDir: tmpDir, serviceName: 'Auth', namespaceTypes: true })
237
+
238
+ const errorsContent = readFileSync(join(tmpDir, '_errors.ts'), 'utf-8')
239
+ expect(errorsContent).toContain('export namespace AuthErrors {')
240
+ expect(errorsContent).not.toContain('export namespace Errors {')
241
+ expect(errorsContent).toContain('AuthProcedureErrorUnion')
242
+ })
243
+
244
+ it('serviceName prefixes ProcedureErrorUnion in flat mode _errors.ts', async () => {
245
+ tmpDir = makeTmpDir()
246
+ const envelopeWithErrors: DocEnvelope = {
247
+ ...envelope,
248
+ errors: [
249
+ {
250
+ name: 'ProcedureError',
251
+ statusCode: 500,
252
+ description: 'Handler error',
253
+ schema: {
254
+ type: 'object',
255
+ properties: { name: { type: 'string' }, message: { type: 'string' } },
256
+ required: ['name', 'message'],
257
+ },
258
+ },
259
+ ],
260
+ }
261
+ await generateClient({ envelope: envelopeWithErrors, outDir: tmpDir, serviceName: 'Auth' })
262
+
263
+ const errorsContent = readFileSync(join(tmpDir, '_errors.ts'), 'utf-8')
264
+ expect(errorsContent).toContain('export type AuthProcedureErrorUnion =')
265
+ expect(errorsContent).not.toContain('export type ProcedureErrorUnion =')
266
+ })
267
+
268
+ it('rejects invalid serviceName', async () => {
269
+ tmpDir = makeTmpDir()
270
+ await expect(
271
+ generateClient({ envelope, outDir: tmpDir, serviceName: '---' })
272
+ ).rejects.toThrow('empty')
273
+ })
274
+
275
+ it('rejects serviceName starting with a digit', async () => {
276
+ tmpDir = makeTmpDir()
277
+ await expect(
278
+ generateClient({ envelope, outDir: tmpDir, serviceName: '123svc' })
279
+ ).rejects.toThrow('starts with a digit')
280
+ })
281
+
282
+ it('serviceName generates custom factory function name in index.ts', async () => {
283
+ tmpDir = makeTmpDir()
284
+ await generateClient({ envelope, outDir: tmpDir, serviceName: 'Auth' })
285
+
286
+ const content = readFileSync(join(tmpDir, 'index.ts'), 'utf-8')
287
+ expect(content).toContain('createAuthBindings')
288
+ expect(content).not.toContain('createScopeBindings')
289
+ })
290
+
291
+ it('throws when a scope key collides with _types in selfContained mode', async () => {
292
+ tmpDir = makeTmpDir()
293
+ const collisionEnvelope: DocEnvelope = {
294
+ ...envelope,
295
+ routes: [
296
+ {
297
+ kind: 'rpc',
298
+ name: 'GetUser',
299
+ path: '/users/1',
300
+ method: 'post',
301
+ scope: '_types',
302
+ version: 1,
303
+ jsonSchema: {
304
+ body: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },
305
+ response: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] },
306
+ },
307
+ },
308
+ ],
309
+ }
310
+ await expect(
311
+ runPipeline({ envelope: collisionEnvelope, outDir: tmpDir, selfContained: true })
312
+ ).rejects.toThrow(
313
+ '[ts-procedures-codegen] Scope "_types" conflicts with self-contained mode reserved filename "_types.ts". Rename the scope to avoid collision.'
314
+ )
315
+ })
316
+ })
@@ -0,0 +1,108 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises'
2
+ import { join } from 'node:path'
3
+ import { createHash } from 'node:crypto'
4
+ import type { DocEnvelope } from '../implementations/types.js'
5
+ import type { AjscOptions } from './emit-types.js'
6
+ import { groupRoutesByScope } from './group-routes.js'
7
+ import { emitScopeFile } from './emit-scope.js'
8
+ import { emitIndexFile } from './emit-index.js'
9
+ import { emitErrorsFile } from './emit-errors.js'
10
+ import { emitClientTypesFile } from './emit-client-types.js'
11
+ import { emitClientRuntimeFile } from './emit-client-runtime.js'
12
+ import { validateServiceName } from './naming.js'
13
+
14
+ export interface PipelineOptions {
15
+ envelope: DocEnvelope
16
+ outDir: string
17
+ ajsc?: AjscOptions
18
+ clientImportPath?: string
19
+ dryRun?: boolean
20
+ namespaceTypes?: boolean
21
+ selfContained?: boolean
22
+ serviceName?: string
23
+ }
24
+
25
+ export interface GeneratedFile {
26
+ path: string
27
+ code: string
28
+ }
29
+
30
+ export async function runPipeline(options: PipelineOptions): Promise<GeneratedFile[]> {
31
+ const { envelope, outDir, ajsc: ajscOpts, dryRun = false, namespaceTypes = false, selfContained = false, serviceName } = options
32
+ if (serviceName != null) {
33
+ validateServiceName(serviceName)
34
+ }
35
+ const clientImportPath = selfContained ? './_types' : options.clientImportPath
36
+ if (selfContained && options.clientImportPath != null) {
37
+ console.warn('[ts-procedures-codegen] --self-contained overrides --client-import-path; using ./_types')
38
+ }
39
+
40
+ const hash = createHash('md5').update(JSON.stringify(envelope)).digest('hex')
41
+ const hashComment = `// Source hash: ${hash}`
42
+
43
+ const groups = groupRoutesByScope(envelope.routes)
44
+ const groupArray = Array.from(groups.values())
45
+
46
+ if (selfContained) {
47
+ for (const group of groupArray) {
48
+ if (group.scopeKey === '_types' || group.scopeKey === '_client') {
49
+ throw new Error(
50
+ `[ts-procedures-codegen] Scope "${group.scopeKey}" conflicts with self-contained mode reserved filename "${group.scopeKey}.ts". Rename the scope to avoid collision.`
51
+ )
52
+ }
53
+ }
54
+ }
55
+
56
+ const files: GeneratedFile[] = []
57
+
58
+ for (const group of groupArray) {
59
+ const rawCode = await emitScopeFile(group, { ajsc: ajscOpts, clientImportPath, namespaceTypes })
60
+ const lines = rawCode.split('\n')
61
+ lines.splice(1, 0, hashComment)
62
+ const code = lines.join('\n')
63
+ files.push({ path: join(outDir, `${group.scopeKey}.ts`), code })
64
+ }
65
+
66
+ const errorsCode = await emitErrorsFile(envelope.errors, { ajsc: ajscOpts, clientImportPath, namespaceTypes, serviceName })
67
+ const hasErrors = errorsCode != null
68
+ if (errorsCode != null) {
69
+ const errorsLines = errorsCode.split('\n')
70
+ errorsLines.splice(1, 0, hashComment)
71
+ const errorsWithHash = errorsLines.join('\n')
72
+ files.push({ path: join(outDir, '_errors.ts'), code: errorsWithHash })
73
+ }
74
+
75
+ const rawIndexCode = emitIndexFile(groupArray, { clientImportPath, hasErrors, serviceName })
76
+ const indexLines = rawIndexCode.split('\n')
77
+ indexLines.splice(1, 0, hashComment)
78
+ const indexCode = indexLines.join('\n')
79
+ files.push({ path: join(outDir, 'index.ts'), code: indexCode })
80
+
81
+ if (selfContained) {
82
+ const rawTypesCode = await emitClientTypesFile()
83
+ const typesLines = rawTypesCode.split('\n')
84
+ typesLines.splice(1, 0, hashComment)
85
+ const typesCode = typesLines.join('\n')
86
+ files.push({ path: join(outDir, '_types.ts'), code: typesCode })
87
+
88
+ const rawClientCode = await emitClientRuntimeFile()
89
+ const clientLines = rawClientCode.split('\n')
90
+ clientLines.splice(1, 0, hashComment)
91
+ const clientCode = clientLines.join('\n')
92
+ files.push({ path: join(outDir, '_client.ts'), code: clientCode })
93
+ }
94
+
95
+ if (dryRun) {
96
+ for (const file of files) {
97
+ const bytes = Buffer.byteLength(file.code, 'utf-8')
98
+ console.log(`[dry-run] Would write: ${file.path} (${bytes} bytes)`)
99
+ }
100
+ } else {
101
+ await mkdir(outDir, { recursive: true })
102
+ for (const file of files) {
103
+ await writeFile(file.path, file.code, 'utf-8')
104
+ }
105
+ }
106
+
107
+ return files
108
+ }
@@ -0,0 +1,76 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { writeFile, mkdtemp, rm } from 'node:fs/promises'
3
+ import { tmpdir } from 'node:os'
4
+ import { join } from 'node:path'
5
+ import { resolveEnvelope } from './resolve-envelope.js'
6
+ import type { DocEnvelope } from '../implementations/types.js'
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Fixtures
10
+ // ---------------------------------------------------------------------------
11
+
12
+ const minimalEnvelope: DocEnvelope = {
13
+ basePath: '/api',
14
+ headers: [],
15
+ errors: [],
16
+ routes: [
17
+ {
18
+ kind: 'rpc',
19
+ name: 'Ping',
20
+ path: '/ping/1',
21
+ method: 'post',
22
+ scope: 'ping',
23
+ version: 1,
24
+ jsonSchema: {},
25
+ },
26
+ ],
27
+ }
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Tests
31
+ // ---------------------------------------------------------------------------
32
+
33
+ describe('resolveEnvelope', () => {
34
+ it('accepts a DocEnvelope object directly', async () => {
35
+ const result = await resolveEnvelope({ envelope: minimalEnvelope })
36
+ expect(result).toEqual(minimalEnvelope)
37
+ })
38
+
39
+ it('reads a DocEnvelope from a JSON file', async () => {
40
+ const dir = await mkdtemp(join(tmpdir(), 'ts-proc-test-'))
41
+ const filePath = join(dir, 'envelope.json')
42
+ try {
43
+ await writeFile(filePath, JSON.stringify(minimalEnvelope), 'utf8')
44
+ const result = await resolveEnvelope({ file: filePath })
45
+ expect(result).toEqual(minimalEnvelope)
46
+ } finally {
47
+ await rm(dir, { recursive: true, force: true })
48
+ }
49
+ })
50
+
51
+ it('throws when routes array is empty (envelope input)', async () => {
52
+ const empty: DocEnvelope = { basePath: '', headers: [], errors: [], routes: [] }
53
+ await expect(resolveEnvelope({ envelope: empty })).rejects.toThrow(/routes/)
54
+ })
55
+
56
+ it('throws when routes array is empty (file input)', async () => {
57
+ const dir = await mkdtemp(join(tmpdir(), 'ts-proc-test-'))
58
+ const filePath = join(dir, 'empty.json')
59
+ const empty: DocEnvelope = { basePath: '', headers: [], errors: [], routes: [] }
60
+ try {
61
+ await writeFile(filePath, JSON.stringify(empty), 'utf8')
62
+ await expect(resolveEnvelope({ file: filePath })).rejects.toThrow(/routes/)
63
+ } finally {
64
+ await rm(dir, { recursive: true, force: true })
65
+ }
66
+ })
67
+
68
+ it('throws when no input source is provided', async () => {
69
+ await expect(resolveEnvelope({})).rejects.toThrow(/input/)
70
+ })
71
+
72
+ // URL test skipped — requires network mock
73
+ it.skip('fetches a DocEnvelope from a URL', async () => {
74
+ // Would require mocking fetch
75
+ })
76
+ })
@@ -0,0 +1,61 @@
1
+ import { readFile } from 'node:fs/promises'
2
+ import type { DocEnvelope } from '../implementations/types.js'
3
+
4
+ export interface ResolveInput {
5
+ url?: string
6
+ file?: string
7
+ envelope?: DocEnvelope
8
+ }
9
+
10
+ export async function resolveEnvelope(input: ResolveInput): Promise<DocEnvelope> {
11
+ let envelope: DocEnvelope
12
+
13
+ if (input.envelope !== undefined) {
14
+ envelope = input.envelope
15
+ } else if (input.file !== undefined) {
16
+ const raw = await readFile(input.file, 'utf8')
17
+ envelope = JSON.parse(raw) as DocEnvelope
18
+ } else if (input.url !== undefined) {
19
+ let response: Response
20
+ try {
21
+ response = await fetch(input.url)
22
+ } catch (err) {
23
+ const msg = err instanceof Error ? err.message : String(err)
24
+ throw new Error(`[ts-procedures-codegen] Failed to connect to ${input.url}: ${msg}`)
25
+ }
26
+ if (!response.ok) {
27
+ throw new Error(`[ts-procedures-codegen] ${input.url} returned HTTP ${response.status} ${response.statusText}`)
28
+ }
29
+ try {
30
+ envelope = (await response.json()) as DocEnvelope
31
+ } catch {
32
+ throw new Error(`[ts-procedures-codegen] ${input.url} returned non-JSON response. Expected a DocEnvelope JSON object.`)
33
+ }
34
+ } else {
35
+ throw new Error(
36
+ 'resolveEnvelope: no input source provided. Pass one of: envelope, file, or url.'
37
+ )
38
+ }
39
+
40
+ if (!envelope || typeof envelope !== 'object') {
41
+ throw new Error(
42
+ `[ts-procedures-codegen] Invalid DocEnvelope: expected an object with a "routes" array, got ${typeof envelope}`
43
+ )
44
+ }
45
+
46
+ if (!Array.isArray(envelope.routes)) {
47
+ const keys = Object.keys(envelope).join(', ')
48
+ throw new Error(
49
+ `[ts-procedures-codegen] DocEnvelope is missing "routes" array. Found keys: [${keys}]. ` +
50
+ `Make sure your server uses DocRegistry.toJSON() to produce the envelope.`
51
+ )
52
+ }
53
+
54
+ if (envelope.routes.length === 0) {
55
+ throw new Error(
56
+ '[ts-procedures-codegen] DocEnvelope has an empty "routes" array. Register at least one procedure before generating.'
57
+ )
58
+ }
59
+
60
+ return envelope
61
+ }