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.
- package/README.md +1 -1
- package/agent_config/claude-code/agents/ts-procedures-architect.md +46 -101
- package/agent_config/claude-code/skills/guide/SKILL.md +49 -34
- package/agent_config/claude-code/skills/guide/anti-patterns.md +6 -5
- package/agent_config/claude-code/skills/guide/api-reference.md +60 -49
- package/agent_config/claude-code/skills/review/SKILL.md +12 -17
- package/agent_config/claude-code/skills/scaffold/SKILL.md +18 -23
- package/agent_config/claude-code/skills/scaffold/templates/client.md +115 -0
- package/agent_config/lib/install-claude.mjs +22 -22
- package/docs/core.md +5 -9
- package/docs/streaming.md +9 -9
- package/package.json +3 -14
- package/src/client/call.test.ts +162 -0
- package/src/client/errors.test.ts +43 -0
- package/src/client/fetch-adapter.test.ts +340 -0
- package/src/client/hooks.test.ts +191 -0
- package/src/client/index.test.ts +290 -0
- package/src/client/request-builder.test.ts +184 -0
- package/src/client/stream.test.ts +331 -0
- package/src/codegen/bin/cli.test.ts +260 -0
- package/src/codegen/bin/cli.ts +282 -0
- package/src/codegen/constants.ts +1 -0
- package/src/codegen/e2e.test.ts +565 -0
- package/src/codegen/emit-client-runtime.test.ts +93 -0
- package/src/codegen/emit-client-runtime.ts +114 -0
- package/src/codegen/emit-client-types.test.ts +39 -0
- package/src/codegen/emit-client-types.ts +27 -0
- package/src/codegen/emit-errors.test.ts +202 -0
- package/src/codegen/emit-errors.ts +80 -0
- package/src/codegen/emit-index.test.ts +127 -0
- package/src/codegen/emit-index.ts +58 -0
- package/src/codegen/emit-scope.test.ts +624 -0
- package/src/codegen/emit-scope.ts +389 -0
- package/src/codegen/emit-types.test.ts +205 -0
- package/src/codegen/emit-types.ts +158 -0
- package/src/codegen/group-routes.test.ts +159 -0
- package/src/codegen/group-routes.ts +61 -0
- package/src/codegen/index.ts +30 -0
- package/src/codegen/naming.test.ts +50 -0
- package/src/codegen/naming.ts +25 -0
- package/src/codegen/pipeline.test.ts +316 -0
- package/src/codegen/pipeline.ts +108 -0
- package/src/codegen/resolve-envelope.test.ts +76 -0
- package/src/codegen/resolve-envelope.ts +61 -0
- package/src/errors.test.ts +163 -0
- package/src/errors.ts +107 -0
- package/src/exports.ts +7 -0
- package/src/implementations/http/doc-registry.test.ts +415 -0
- package/src/implementations/http/doc-registry.ts +143 -0
- package/src/implementations/http/express-rpc/README.md +6 -6
- package/src/implementations/http/express-rpc/index.test.ts +957 -0
- package/src/implementations/http/express-rpc/index.ts +266 -0
- package/src/implementations/http/express-rpc/types.ts +16 -0
- package/src/implementations/http/hono-api/index.test.ts +1341 -0
- package/src/implementations/http/hono-api/index.ts +463 -0
- package/src/implementations/http/hono-api/types.ts +16 -0
- package/src/implementations/http/hono-rpc/README.md +6 -6
- package/src/implementations/http/hono-rpc/index.test.ts +1075 -0
- package/src/implementations/http/hono-rpc/index.ts +238 -0
- package/src/implementations/http/hono-rpc/types.ts +16 -0
- package/src/implementations/http/hono-stream/README.md +12 -12
- package/src/implementations/http/hono-stream/index.test.ts +1768 -0
- package/src/implementations/http/hono-stream/index.ts +456 -0
- package/src/implementations/http/hono-stream/types.ts +20 -0
- package/src/implementations/types.ts +174 -0
- package/src/index.test.ts +1185 -0
- package/src/index.ts +522 -0
- package/src/schema/compute-schema.test.ts +128 -0
- package/src/schema/compute-schema.ts +88 -0
- package/src/schema/extract-json-schema.test.ts +25 -0
- package/src/schema/extract-json-schema.ts +15 -0
- package/src/schema/parser.test.ts +182 -0
- package/src/schema/parser.ts +215 -0
- package/src/schema/resolve-schema-lib.test.ts +19 -0
- package/src/schema/resolve-schema-lib.ts +29 -0
- package/src/schema/types.ts +20 -0
- package/src/stack-utils.test.ts +94 -0
- package/src/stack-utils.ts +129 -0
- package/docs/superpowers/plans/2026-03-30-client-codegen.md +0 -2833
- 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'
|