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,158 @@
1
+ export interface AjscOptions {
2
+ enumStyle?: 'union' | 'enum'
3
+ depluralize?: boolean
4
+ inlineTypes?: boolean
5
+ arrayItemNaming?: string | false
6
+ uncountableWords?: string[]
7
+ jsdoc?: boolean
8
+ }
9
+
10
+ let ajscValidated = false
11
+ async function validateAjscFormat() {
12
+ if (ajscValidated) return
13
+ ajscValidated = true
14
+ const { TypescriptConverter } = await import('ajsc')
15
+ const probe = new TypescriptConverter({ type: 'string' }, { inlineTypes: true })
16
+ const probeCode = (probe.code as string).trim()
17
+ if (probeCode !== 'string') {
18
+ console.warn(`[ts-procedures-codegen] ajsc output format may have changed. Expected "string" for { type: "string" }, got: "${probeCode}"`)
19
+ }
20
+ }
21
+
22
+ async function resolveTypeBody(
23
+ schema: Record<string, unknown>,
24
+ options?: AjscOptions,
25
+ ): Promise<string> {
26
+ await validateAjscFormat()
27
+
28
+ const { TypescriptConverter } = await import('ajsc')
29
+
30
+ const converter = new TypescriptConverter(schema, {
31
+ ...options,
32
+ inlineTypes: true,
33
+ })
34
+
35
+ let code = (converter.code as string).trim()
36
+ if (!code) {
37
+ throw new Error(
38
+ `[ts-procedures-codegen] ajsc produced empty output. Schema: ${JSON.stringify(schema)}`
39
+ )
40
+ }
41
+ // Strip any 'export type X = ' or 'type X = ' prefix ajsc might add
42
+ code = code.replace(/^(?:export\s+)?type\s+\w+\s*=\s*/, '')
43
+ // Remove trailing semicolons and newlines
44
+ code = code.replace(/;\s*$/, '').trim()
45
+
46
+ return code
47
+ }
48
+
49
+ /**
50
+ * Converts a JSON Schema to a TypeScript type string using ajsc.
51
+ *
52
+ * Returns `export type ${typeName} = ${code}` where code is the inline type
53
+ * body from TypescriptConverter with inlineTypes: true.
54
+ *
55
+ * Returns undefined for undefined or null schema.
56
+ */
57
+ export async function jsonSchemaToTypeString(
58
+ typeName: string,
59
+ schema: Record<string, unknown> | undefined,
60
+ options?: AjscOptions,
61
+ ): Promise<string | undefined> {
62
+ if (schema == null) return undefined
63
+ const code = await resolveTypeBody(schema, options)
64
+ return `export type ${typeName} = ${code}`
65
+ }
66
+
67
+ /**
68
+ * Converts a JSON Schema to a bare TypeScript type body using ajsc.
69
+ *
70
+ * Returns just the type literal (e.g., `{ id: string; }`) without any
71
+ * `export type Name =` wrapper. Used by flat mode in emit-scope.
72
+ *
73
+ * Returns undefined for undefined or null schema.
74
+ */
75
+ export async function jsonSchemaToTypeBody(
76
+ schema: Record<string, unknown> | undefined,
77
+ options?: AjscOptions,
78
+ ): Promise<string | undefined> {
79
+ if (schema == null) return undefined
80
+ return resolveTypeBody(schema, options)
81
+ }
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // Extracted types (namespace mode — uses inlineTypes: false)
85
+ // ---------------------------------------------------------------------------
86
+
87
+ export interface ExtractedTypeOutput {
88
+ /** Extracted sub-type/enum declarations (e.g., `export type Address = { ... }`, `export enum Status { ... }`). */
89
+ declarations: string[]
90
+ /** The root type body (bare literal, e.g., `{ user: User; }`) without the `export type Root =` wrapper. */
91
+ body: string
92
+ }
93
+
94
+ /**
95
+ * Converts a JSON Schema to extracted TypeScript types using ajsc with
96
+ * `inlineTypes: false`. This produces named sub-types (objects, enums) that
97
+ * are separated from the root type body.
98
+ *
99
+ * Used in namespace mode where sub-types live naturally inside the namespace
100
+ * and ajsc formatting options (enumStyle, depluralize, jsdoc, etc.) take effect.
101
+ *
102
+ * Returns undefined for undefined or null schema.
103
+ */
104
+ export async function jsonSchemaToExtractedTypes(
105
+ schema: Record<string, unknown> | undefined,
106
+ options?: AjscOptions,
107
+ ): Promise<ExtractedTypeOutput | undefined> {
108
+ if (schema == null) return undefined
109
+
110
+ await validateAjscFormat()
111
+
112
+ const { TypescriptConverter } = await import('ajsc')
113
+ const converter = new TypescriptConverter(schema, {
114
+ ...options,
115
+ inlineTypes: false,
116
+ })
117
+
118
+ const code = (converter.code as string).trim()
119
+ if (!code) {
120
+ throw new Error(
121
+ `[ts-procedures-codegen] ajsc produced empty output. Schema: ${JSON.stringify(schema)}`
122
+ )
123
+ }
124
+
125
+ // ajsc with inlineTypes: false produces blocks separated by blank lines:
126
+ // export enum Status { Active = "active" }
127
+ // export type Contact = { name: string; };
128
+ // export type Root = { status: Status; contacts: Array<Contact>; };
129
+ //
130
+ // The Root type is always the last declaration.
131
+ const blocks = code.split(/\n\n+/).map((b) => b.trim()).filter(Boolean)
132
+
133
+ const declarations: string[] = []
134
+ let body = ''
135
+
136
+ for (const block of blocks) {
137
+ if (block.startsWith('export type Root')) {
138
+ // Strip "export type Root = " prefix and trailing ";"
139
+ body = block
140
+ .replace(/^export\s+type\s+Root\s*=\s*/, '')
141
+ .replace(/;\s*$/, '')
142
+ .trim()
143
+ } else {
144
+ // Sub-type or enum declaration — remove trailing ";" for consistency
145
+ declarations.push(block.replace(/;\s*$/, ''))
146
+ }
147
+ }
148
+
149
+ if (!body) {
150
+ // Fallback: treat whole output as inline body
151
+ body = code
152
+ .replace(/^(?:export\s+)?type\s+\w+\s*=\s*/, '')
153
+ .replace(/;\s*$/, '')
154
+ .trim()
155
+ }
156
+
157
+ return { declarations, body }
158
+ }
@@ -0,0 +1,159 @@
1
+ import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'
2
+ import { normalizeScope, scopeToCamelCase, groupRoutesByScope } from './group-routes.js'
3
+ import type { AnyHttpRouteDoc } from '../implementations/types.js'
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Fixtures
7
+ // ---------------------------------------------------------------------------
8
+
9
+ const rpcRouteUsers: AnyHttpRouteDoc = {
10
+ kind: 'rpc',
11
+ name: 'GetUser',
12
+ path: '/users/1',
13
+ method: 'post',
14
+ scope: 'users',
15
+ version: 1,
16
+ jsonSchema: {},
17
+ }
18
+
19
+ const rpcRouteAdmin: AnyHttpRouteDoc = {
20
+ kind: 'rpc',
21
+ name: 'DeleteUser',
22
+ path: '/admin-users/1',
23
+ method: 'post',
24
+ scope: ['admin', 'users'],
25
+ version: 1,
26
+ jsonSchema: {},
27
+ }
28
+
29
+ const rpcRouteUsers2: AnyHttpRouteDoc = {
30
+ kind: 'rpc',
31
+ name: 'ListUsers',
32
+ path: '/users/1',
33
+ method: 'post',
34
+ scope: 'users',
35
+ version: 1,
36
+ jsonSchema: {},
37
+ }
38
+
39
+ const apiRouteNoScope: AnyHttpRouteDoc = {
40
+ kind: 'api',
41
+ name: 'HealthCheck',
42
+ path: '/health',
43
+ method: 'get',
44
+ fullPath: '/api/health',
45
+ jsonSchema: {},
46
+ }
47
+
48
+ const apiRouteWithScope: AnyHttpRouteDoc = {
49
+ kind: 'api',
50
+ name: 'ListPosts',
51
+ path: '/posts',
52
+ method: 'get',
53
+ fullPath: '/api/posts',
54
+ scope: 'posts',
55
+ jsonSchema: {},
56
+ }
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // normalizeScope
60
+ // ---------------------------------------------------------------------------
61
+
62
+ describe('normalizeScope', () => {
63
+ it('returns a string scope as-is', () => {
64
+ expect(normalizeScope('users')).toBe('users')
65
+ })
66
+
67
+ it('joins a string array scope with hyphens', () => {
68
+ expect(normalizeScope(['admin', 'users'])).toBe('admin-users')
69
+ })
70
+
71
+ it('returns "default" for undefined scope', () => {
72
+ expect(normalizeScope(undefined)).toBe('default')
73
+ })
74
+ })
75
+
76
+ // ---------------------------------------------------------------------------
77
+ // scopeToCamelCase
78
+ // ---------------------------------------------------------------------------
79
+
80
+ describe('scopeToCamelCase', () => {
81
+ it('returns a single word as-is', () => {
82
+ expect(scopeToCamelCase('users')).toBe('users')
83
+ })
84
+
85
+ it('converts a hyphenated scope to camelCase', () => {
86
+ expect(scopeToCamelCase('admin-users')).toBe('adminUsers')
87
+ })
88
+
89
+ it('converts multi-segment hyphenated scope', () => {
90
+ expect(scopeToCamelCase('super-admin-users')).toBe('superAdminUsers')
91
+ })
92
+ })
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // groupRoutesByScope
96
+ // ---------------------------------------------------------------------------
97
+
98
+ describe('groupRoutesByScope', () => {
99
+ beforeEach(() => {
100
+ vi.spyOn(console, 'warn').mockImplementation(() => {})
101
+ })
102
+
103
+ afterEach(() => {
104
+ vi.restoreAllMocks()
105
+ })
106
+
107
+ it('groups routes by their scope', () => {
108
+ const routes: AnyHttpRouteDoc[] = [rpcRouteUsers, rpcRouteUsers2]
109
+ const groups = groupRoutesByScope(routes)
110
+
111
+ expect(groups.size).toBe(1)
112
+ expect(groups.has('users')).toBe(true)
113
+
114
+ const usersGroup = groups.get('users')!
115
+ expect(usersGroup.scopeKey).toBe('users')
116
+ expect(usersGroup.camelCase).toBe('users')
117
+ expect(usersGroup.routes).toHaveLength(2)
118
+ })
119
+
120
+ it('handles array scopes by joining with hyphens', () => {
121
+ const routes: AnyHttpRouteDoc[] = [rpcRouteAdmin]
122
+ const groups = groupRoutesByScope(routes)
123
+
124
+ expect(groups.has('admin-users')).toBe(true)
125
+ const group = groups.get('admin-users')!
126
+ expect(group.scopeKey).toBe('admin-users')
127
+ expect(group.camelCase).toBe('adminUsers')
128
+ expect(group.routes).toHaveLength(1)
129
+ })
130
+
131
+ it('groups routes from multiple scopes', () => {
132
+ const routes: AnyHttpRouteDoc[] = [rpcRouteUsers, rpcRouteAdmin, apiRouteWithScope]
133
+ const groups = groupRoutesByScope(routes)
134
+
135
+ expect(groups.size).toBe(3)
136
+ expect(groups.has('users')).toBe(true)
137
+ expect(groups.has('admin-users')).toBe(true)
138
+ expect(groups.has('posts')).toBe(true)
139
+ })
140
+
141
+ it('warns on missing scope and groups into "default"', () => {
142
+ const routes: AnyHttpRouteDoc[] = [apiRouteNoScope]
143
+ const groups = groupRoutesByScope(routes)
144
+
145
+ expect(console.warn).toHaveBeenCalled()
146
+ expect(groups.has('default')).toBe(true)
147
+
148
+ const defaultGroup = groups.get('default')!
149
+ expect(defaultGroup.scopeKey).toBe('default')
150
+ expect(defaultGroup.camelCase).toBe('default')
151
+ expect(defaultGroup.routes).toHaveLength(1)
152
+ })
153
+
154
+ it('does not warn when all routes have scopes', () => {
155
+ const routes: AnyHttpRouteDoc[] = [rpcRouteUsers, apiRouteWithScope]
156
+ groupRoutesByScope(routes)
157
+ expect(console.warn).not.toHaveBeenCalled()
158
+ })
159
+ })
@@ -0,0 +1,61 @@
1
+ import type { AnyHttpRouteDoc } from '../implementations/types.js'
2
+
3
+ export interface ScopeGroup {
4
+ scopeKey: string
5
+ camelCase: string
6
+ routes: AnyHttpRouteDoc[]
7
+ }
8
+
9
+ /**
10
+ * Normalizes a scope value to a string key:
11
+ * - string → returned as-is
12
+ * - string[] → joined with '-'
13
+ * - undefined → 'default'
14
+ */
15
+ export function normalizeScope(scope: string | string[] | undefined): string {
16
+ if (scope === undefined) return 'default'
17
+ if (Array.isArray(scope)) return scope.join('-')
18
+ return scope
19
+ }
20
+
21
+ /**
22
+ * Converts a hyphenated scope key to camelCase.
23
+ * e.g. 'admin-users' → 'adminUsers', 'users' → 'users'
24
+ */
25
+ export function scopeToCamelCase(scope: string): string {
26
+ const parts = scope.split('-')
27
+ return parts
28
+ .map((part, index) =>
29
+ index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)
30
+ )
31
+ .join('')
32
+ }
33
+
34
+ /**
35
+ * Groups an array of route docs into a Map keyed by normalized scope.
36
+ * Routes without a scope emit a console.warn and are grouped under 'default'.
37
+ */
38
+ export function groupRoutesByScope(routes: AnyHttpRouteDoc[]): Map<string, ScopeGroup> {
39
+ const groups = new Map<string, ScopeGroup>()
40
+
41
+ for (const route of routes) {
42
+ const rawScope = 'scope' in route ? (route as { scope?: string | string[] }).scope : undefined
43
+ if (rawScope === undefined) {
44
+ console.warn(
45
+ `[ts-procedures] Route "${route.name}" has no scope — it will be grouped under "default".`
46
+ )
47
+ }
48
+
49
+ const scopeKey = normalizeScope(rawScope)
50
+ const camelCase = scopeToCamelCase(scopeKey)
51
+
52
+ let group = groups.get(scopeKey)
53
+ if (group === undefined) {
54
+ group = { scopeKey, camelCase, routes: [] }
55
+ groups.set(scopeKey, group)
56
+ }
57
+ group.routes.push(route)
58
+ }
59
+
60
+ return groups
61
+ }
@@ -0,0 +1,30 @@
1
+ import { resolveEnvelope, type ResolveInput } from './resolve-envelope.js'
2
+ import { runPipeline, type GeneratedFile } from './pipeline.js'
3
+ import type { AjscOptions } from './emit-types.js'
4
+
5
+ export interface GenerateClientOptions extends ResolveInput {
6
+ outDir: string
7
+ ajsc?: AjscOptions
8
+ clientImportPath?: string
9
+ dryRun?: boolean
10
+ namespaceTypes?: boolean
11
+ selfContained?: boolean
12
+ serviceName?: string
13
+ }
14
+
15
+ export async function generateClient(options: GenerateClientOptions): Promise<GeneratedFile[]> {
16
+ const envelope = await resolveEnvelope(options)
17
+ return runPipeline({
18
+ envelope,
19
+ outDir: options.outDir,
20
+ ajsc: options.ajsc,
21
+ clientImportPath: options.clientImportPath,
22
+ dryRun: options.dryRun,
23
+ namespaceTypes: options.namespaceTypes,
24
+ selfContained: options.selfContained,
25
+ serviceName: options.serviceName,
26
+ })
27
+ }
28
+
29
+ export type { AjscOptions } from './emit-types.js'
30
+ export type { ResolveInput } from './resolve-envelope.js'
@@ -0,0 +1,50 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { toPascalCase, validateServiceName } from './naming.js'
3
+
4
+ describe('toPascalCase', () => {
5
+ it('converts a single word', () => {
6
+ expect(toPascalCase('auth')).toBe('Auth')
7
+ })
8
+
9
+ it('converts hyphen-separated words', () => {
10
+ expect(toPascalCase('auth-service')).toBe('AuthService')
11
+ })
12
+
13
+ it('converts underscore-separated words', () => {
14
+ expect(toPascalCase('users_api')).toBe('UsersApi')
15
+ })
16
+
17
+ it('converts space-separated words', () => {
18
+ expect(toPascalCase('my service')).toBe('MyService')
19
+ })
20
+
21
+ it('preserves already-PascalCased input', () => {
22
+ expect(toPascalCase('UsersApi')).toBe('UsersApi')
23
+ })
24
+ })
25
+
26
+ describe('validateServiceName', () => {
27
+ it('accepts a valid PascalCase name', () => {
28
+ expect(() => validateServiceName('Auth')).not.toThrow()
29
+ })
30
+
31
+ it('accepts a hyphen-separated name', () => {
32
+ expect(() => validateServiceName('auth-service')).not.toThrow()
33
+ })
34
+
35
+ it('accepts an underscore-separated name', () => {
36
+ expect(() => validateServiceName('users_api')).not.toThrow()
37
+ })
38
+
39
+ it('throws for an empty string', () => {
40
+ expect(() => validateServiceName('')).toThrow('empty')
41
+ })
42
+
43
+ it('throws for a string of only separators', () => {
44
+ expect(() => validateServiceName('---')).toThrow('empty')
45
+ })
46
+
47
+ it('throws when the result starts with a digit', () => {
48
+ expect(() => validateServiceName('123service')).toThrow('starts with a digit')
49
+ })
50
+ })
@@ -0,0 +1,25 @@
1
+ /** Converts a string to PascalCase (splits on whitespace, hyphens, underscores). */
2
+ export function toPascalCase(str: string): string {
3
+ return str
4
+ .split(/[\s\-_]+/)
5
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
6
+ .join('')
7
+ }
8
+
9
+ /**
10
+ * Validates that a serviceName option is usable as a TypeScript identifier fragment.
11
+ * Throws a descriptive error if invalid.
12
+ */
13
+ export function validateServiceName(serviceName: string): void {
14
+ const pascal = toPascalCase(serviceName)
15
+ if (pascal.length === 0) {
16
+ throw new Error(
17
+ `[ts-procedures-codegen] --service-name value "${serviceName}" is empty or contains only separator characters.`
18
+ )
19
+ }
20
+ if (/^\d/.test(pascal)) {
21
+ throw new Error(
22
+ `[ts-procedures-codegen] --service-name value "${serviceName}" produces "${pascal}" which starts with a digit — provide a name that starts with a letter.`
23
+ )
24
+ }
25
+ }