ts-procedures 5.9.0 → 5.10.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 (80) hide show
  1. package/README.md +1 -1
  2. package/agent_config/claude-code/agents/ts-procedures-architect.md +46 -101
  3. package/agent_config/claude-code/skills/guide/SKILL.md +49 -34
  4. package/agent_config/claude-code/skills/guide/anti-patterns.md +6 -5
  5. package/agent_config/claude-code/skills/guide/api-reference.md +60 -49
  6. package/agent_config/claude-code/skills/review/SKILL.md +12 -17
  7. package/agent_config/claude-code/skills/scaffold/SKILL.md +18 -23
  8. package/agent_config/claude-code/skills/scaffold/templates/client.md +115 -0
  9. package/agent_config/lib/install-claude.mjs +22 -22
  10. package/docs/core.md +5 -9
  11. package/docs/streaming.md +9 -9
  12. package/package.json +3 -14
  13. package/src/client/call.test.ts +162 -0
  14. package/src/client/errors.test.ts +43 -0
  15. package/src/client/fetch-adapter.test.ts +340 -0
  16. package/src/client/hooks.test.ts +191 -0
  17. package/src/client/index.test.ts +290 -0
  18. package/src/client/request-builder.test.ts +184 -0
  19. package/src/client/stream.test.ts +331 -0
  20. package/src/codegen/bin/cli.test.ts +260 -0
  21. package/src/codegen/bin/cli.ts +282 -0
  22. package/src/codegen/constants.ts +1 -0
  23. package/src/codegen/e2e.test.ts +565 -0
  24. package/src/codegen/emit-client-runtime.test.ts +93 -0
  25. package/src/codegen/emit-client-runtime.ts +114 -0
  26. package/src/codegen/emit-client-types.test.ts +39 -0
  27. package/src/codegen/emit-client-types.ts +27 -0
  28. package/src/codegen/emit-errors.test.ts +202 -0
  29. package/src/codegen/emit-errors.ts +80 -0
  30. package/src/codegen/emit-index.test.ts +127 -0
  31. package/src/codegen/emit-index.ts +58 -0
  32. package/src/codegen/emit-scope.test.ts +624 -0
  33. package/src/codegen/emit-scope.ts +389 -0
  34. package/src/codegen/emit-types.test.ts +205 -0
  35. package/src/codegen/emit-types.ts +158 -0
  36. package/src/codegen/group-routes.test.ts +159 -0
  37. package/src/codegen/group-routes.ts +61 -0
  38. package/src/codegen/index.ts +30 -0
  39. package/src/codegen/naming.test.ts +50 -0
  40. package/src/codegen/naming.ts +25 -0
  41. package/src/codegen/pipeline.test.ts +316 -0
  42. package/src/codegen/pipeline.ts +108 -0
  43. package/src/codegen/resolve-envelope.test.ts +76 -0
  44. package/src/codegen/resolve-envelope.ts +61 -0
  45. package/src/errors.test.ts +163 -0
  46. package/src/errors.ts +107 -0
  47. package/src/exports.ts +7 -0
  48. package/src/implementations/http/doc-registry.test.ts +415 -0
  49. package/src/implementations/http/doc-registry.ts +143 -0
  50. package/src/implementations/http/express-rpc/README.md +6 -6
  51. package/src/implementations/http/express-rpc/index.test.ts +957 -0
  52. package/src/implementations/http/express-rpc/index.ts +266 -0
  53. package/src/implementations/http/express-rpc/types.ts +16 -0
  54. package/src/implementations/http/hono-api/index.test.ts +1341 -0
  55. package/src/implementations/http/hono-api/index.ts +463 -0
  56. package/src/implementations/http/hono-api/types.ts +16 -0
  57. package/src/implementations/http/hono-rpc/README.md +6 -6
  58. package/src/implementations/http/hono-rpc/index.test.ts +1075 -0
  59. package/src/implementations/http/hono-rpc/index.ts +238 -0
  60. package/src/implementations/http/hono-rpc/types.ts +16 -0
  61. package/src/implementations/http/hono-stream/README.md +12 -12
  62. package/src/implementations/http/hono-stream/index.test.ts +1768 -0
  63. package/src/implementations/http/hono-stream/index.ts +456 -0
  64. package/src/implementations/http/hono-stream/types.ts +20 -0
  65. package/src/implementations/types.ts +174 -0
  66. package/src/index.test.ts +1185 -0
  67. package/src/index.ts +522 -0
  68. package/src/schema/compute-schema.test.ts +128 -0
  69. package/src/schema/compute-schema.ts +88 -0
  70. package/src/schema/extract-json-schema.test.ts +25 -0
  71. package/src/schema/extract-json-schema.ts +15 -0
  72. package/src/schema/parser.test.ts +182 -0
  73. package/src/schema/parser.ts +215 -0
  74. package/src/schema/resolve-schema-lib.test.ts +19 -0
  75. package/src/schema/resolve-schema-lib.ts +29 -0
  76. package/src/schema/types.ts +20 -0
  77. package/src/stack-utils.test.ts +94 -0
  78. package/src/stack-utils.ts +129 -0
  79. package/docs/superpowers/plans/2026-03-30-client-codegen.md +0 -2833
  80. package/docs/superpowers/specs/2026-03-30-client-codegen-design.md +0 -632
@@ -0,0 +1,282 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from 'node:fs/promises'
3
+ import { resolve } from 'node:path'
4
+ import { createHash } from 'node:crypto'
5
+ import { generateClient, type GenerateClientOptions } from '../index.js'
6
+ import type { AjscOptions } from '../emit-types.js'
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Types
10
+ // ---------------------------------------------------------------------------
11
+
12
+ export interface CodegenConfig {
13
+ url?: string
14
+ file?: string
15
+ outDir?: string
16
+ watch?: boolean
17
+ interval?: number
18
+ ajsc?: AjscOptions
19
+ clientImportPath?: string
20
+ dryRun?: boolean
21
+ namespaceTypes?: boolean
22
+ selfContained?: boolean
23
+ serviceName?: string
24
+ }
25
+
26
+ export interface ParsedArgs {
27
+ url?: string
28
+ file?: string
29
+ outDir: string
30
+ watch: boolean
31
+ interval: number
32
+ ajsc?: AjscOptions
33
+ clientImportPath?: string
34
+ dryRun: boolean
35
+ namespaceTypes: boolean
36
+ selfContained: boolean
37
+ serviceName?: string
38
+ }
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Config file loading
42
+ // ---------------------------------------------------------------------------
43
+
44
+ const DEFAULT_CONFIG_NAME = 'ts-procedures-codegen.config.json'
45
+
46
+ /**
47
+ * Loads a JSON config file. Returns undefined if the file doesn't exist.
48
+ * Throws on parse errors.
49
+ */
50
+ export async function loadConfigFile(configPath?: string): Promise<CodegenConfig | undefined> {
51
+ const filePath = resolve(configPath ?? DEFAULT_CONFIG_NAME)
52
+ try {
53
+ const raw = await readFile(filePath, 'utf8')
54
+ return JSON.parse(raw) as CodegenConfig
55
+ } catch (err) {
56
+ if (configPath !== undefined) {
57
+ // Explicit path — always throw
58
+ throw new Error(`[ts-procedures-codegen] Failed to load config from ${filePath}: ${err instanceof Error ? err.message : err}`)
59
+ }
60
+ // Default path — silently ignore if not found
61
+ return undefined
62
+ }
63
+ }
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // parseArgs
67
+ // ---------------------------------------------------------------------------
68
+
69
+ /**
70
+ * Parses CLI argv (pass process.argv.slice(2)).
71
+ * Throws with a descriptive message on validation errors.
72
+ *
73
+ * When a config object is provided, its values are used as defaults
74
+ * and CLI flags override them.
75
+ */
76
+ export function parseArgs(argv: string[], config?: CodegenConfig): ParsedArgs {
77
+ let url: string | undefined = config?.url
78
+ let file: string | undefined = config?.file
79
+ let outDir: string | undefined = config?.outDir
80
+ let watch = config?.watch ?? false
81
+ let interval = config?.interval ?? 3000
82
+ const ajsc: AjscOptions = { jsdoc: true, ...config?.ajsc }
83
+ let clientImportPath: string | undefined = config?.clientImportPath
84
+ let dryRun = config?.dryRun ?? false
85
+ let namespaceTypes = config?.namespaceTypes ?? true
86
+ let selfContained = config?.selfContained ?? true
87
+ let serviceName: string | undefined = config?.serviceName
88
+ let configPath: string | undefined
89
+
90
+ for (let i = 0; i < argv.length; i++) {
91
+ const arg = argv[i]
92
+
93
+ if (arg === '--url') {
94
+ url = argv[++i]
95
+ } else if (arg === '--file') {
96
+ file = argv[++i]
97
+ } else if (arg === '--out') {
98
+ outDir = argv[++i]
99
+ } else if (arg === '--watch') {
100
+ watch = true
101
+ } else if (arg === '--interval') {
102
+ interval = parseInt(argv[++i] ?? '3000', 10)
103
+ } else if (arg === '--enum-style') {
104
+ const val = argv[++i]
105
+ if (val === 'union' || val === 'enum') {
106
+ ajsc.enumStyle = val
107
+ }
108
+ } else if (arg === '--depluralize') {
109
+ ajsc.depluralize = true
110
+ } else if (arg === '--array-item-naming') {
111
+ const val = argv[++i]
112
+ ajsc.arrayItemNaming = val === 'false' ? false : val
113
+ } else if (arg === '--uncountable-words') {
114
+ ajsc.uncountableWords = (argv[++i] ?? '').split(',').map((w) => w.trim()).filter(Boolean)
115
+ } else if (arg === '--jsdoc') {
116
+ ajsc.jsdoc = true
117
+ } else if (arg === '--no-jsdoc') {
118
+ ajsc.jsdoc = false
119
+ } else if (arg === '--client-import-path') {
120
+ clientImportPath = argv[++i]
121
+ } else if (arg === '--dry-run') {
122
+ dryRun = true
123
+ } else if (arg === '--namespace-types') {
124
+ namespaceTypes = true
125
+ } else if (arg === '--no-namespace-types') {
126
+ namespaceTypes = false
127
+ } else if (arg === '--self-contained') {
128
+ selfContained = true
129
+ } else if (arg === '--no-self-contained') {
130
+ selfContained = false
131
+ } else if (arg === '--service-name') {
132
+ serviceName = argv[++i]
133
+ } else if (arg === '--config') {
134
+ configPath = argv[++i]
135
+ }
136
+ }
137
+
138
+ // configPath is consumed by the caller (main) before parseArgs is called with the loaded config.
139
+ // When called from main, config is already loaded. When called directly (tests), configPath is ignored.
140
+
141
+ if (outDir === undefined) {
142
+ throw new Error('Missing required argument: --out <dir>')
143
+ }
144
+
145
+ if (url === undefined && file === undefined) {
146
+ throw new Error('Missing required input source: provide --url <url> or --file <path>')
147
+ }
148
+
149
+ return {
150
+ url,
151
+ file,
152
+ outDir,
153
+ watch,
154
+ interval,
155
+ ajsc,
156
+ ...(clientImportPath !== undefined ? { clientImportPath } : {}),
157
+ dryRun,
158
+ namespaceTypes,
159
+ selfContained,
160
+ ...(serviceName !== undefined ? { serviceName } : {}),
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Extracts the --config value from argv without full parsing.
166
+ */
167
+ export function extractConfigPath(argv: string[]): string | undefined {
168
+ for (let i = 0; i < argv.length; i++) {
169
+ if (argv[i] === '--config') return argv[i + 1]
170
+ }
171
+ return undefined
172
+ }
173
+
174
+ // ---------------------------------------------------------------------------
175
+ // Watch mode
176
+ // ---------------------------------------------------------------------------
177
+
178
+ function hashEnvelope(envelope: unknown): string {
179
+ return createHash('md5').update(JSON.stringify(envelope)).digest('hex')
180
+ }
181
+
182
+ async function runWithWatch(parsed: ParsedArgs): Promise<void> {
183
+ const opts: GenerateClientOptions = {
184
+ url: parsed.url,
185
+ file: parsed.file,
186
+ outDir: parsed.outDir,
187
+ ajsc: parsed.ajsc,
188
+ clientImportPath: parsed.clientImportPath,
189
+ dryRun: parsed.dryRun,
190
+ namespaceTypes: parsed.namespaceTypes,
191
+ selfContained: parsed.selfContained,
192
+ serviceName: parsed.serviceName,
193
+ }
194
+
195
+ let lastHash: string | undefined
196
+
197
+ const run = async (): Promise<void> => {
198
+ try {
199
+ // Resolve envelope separately so we can hash it
200
+ const { resolveEnvelope } = await import('../resolve-envelope.js')
201
+ const envelope = await resolveEnvelope(opts)
202
+ const hash = hashEnvelope(envelope)
203
+
204
+ if (hash === lastHash) {
205
+ return
206
+ }
207
+
208
+ lastHash = hash
209
+ const { runPipeline } = await import('../pipeline.js')
210
+ await runPipeline({
211
+ envelope,
212
+ outDir: parsed.outDir,
213
+ ajsc: parsed.ajsc,
214
+ clientImportPath: parsed.clientImportPath,
215
+ dryRun: parsed.dryRun,
216
+ namespaceTypes: parsed.namespaceTypes,
217
+ selfContained: parsed.selfContained,
218
+ serviceName: parsed.serviceName,
219
+ })
220
+ console.log(`[ts-procedures-codegen] Generated client files → ${parsed.outDir}`)
221
+ } catch (err) {
222
+ console.error('[ts-procedures-codegen] Error:', err instanceof Error ? err.message : err)
223
+ }
224
+ }
225
+
226
+ await run()
227
+ setInterval(() => {
228
+ void run()
229
+ }, parsed.interval)
230
+ }
231
+
232
+ // ---------------------------------------------------------------------------
233
+ // Main
234
+ // ---------------------------------------------------------------------------
235
+
236
+ async function main(): Promise<void> {
237
+ const argv = process.argv.slice(2)
238
+ const configPath = extractConfigPath(argv)
239
+ const config = await loadConfigFile(configPath)
240
+ if (config != null) {
241
+ console.log(`[ts-procedures-codegen] Loaded config from ${configPath ?? DEFAULT_CONFIG_NAME}`)
242
+ }
243
+ const parsed = parseArgs(argv, config)
244
+
245
+ const source = parsed.url ?? parsed.file!
246
+ console.log(`[ts-procedures-codegen] Reading docs from ${source}...`)
247
+
248
+ if (parsed.watch) {
249
+ await runWithWatch(parsed)
250
+ } else {
251
+ const result = await generateClient({
252
+ url: parsed.url,
253
+ file: parsed.file,
254
+ outDir: parsed.outDir,
255
+ ajsc: parsed.ajsc,
256
+ clientImportPath: parsed.clientImportPath,
257
+ dryRun: parsed.dryRun,
258
+ namespaceTypes: parsed.namespaceTypes,
259
+ selfContained: parsed.selfContained,
260
+ serviceName: parsed.serviceName,
261
+ })
262
+ if (parsed.dryRun) {
263
+ console.log(`[ts-procedures-codegen] Dry run complete — ${result.length} files would be generated`)
264
+ } else {
265
+ console.log(`[ts-procedures-codegen] Generated ${result.length} files → ${parsed.outDir}`)
266
+ }
267
+ }
268
+ }
269
+
270
+ // Run when this file is the entry point (direct execution or via npx bin symlink)
271
+ const isMain =
272
+ typeof process !== 'undefined' &&
273
+ process.argv[1] !== undefined &&
274
+ !process.argv[1].includes('vitest') &&
275
+ !process.argv[1].includes('jest')
276
+
277
+ if (isMain) {
278
+ main().catch((err: unknown) => {
279
+ console.error('[ts-procedures-codegen] Fatal error:', err instanceof Error ? err.message : err)
280
+ process.exit(1)
281
+ })
282
+ }
@@ -0,0 +1 @@
1
+ export const CODEGEN_HEADER = '// Auto-generated by ts-procedures-codegen — do not edit'