@lovrozagar/honey 0.6.5 → 0.7.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 (674) hide show
  1. package/README.md +514 -192
  2. package/bin/honey.js +25 -0
  3. package/dist/accepts.d.ts +7 -1
  4. package/dist/accepts.d.ts.map +1 -1
  5. package/dist/accepts.js +126 -0
  6. package/dist/accepts.js.map +1 -0
  7. package/dist/body-kind.d.ts +26 -0
  8. package/dist/body-kind.d.ts.map +1 -0
  9. package/dist/body-kind.js +70 -0
  10. package/dist/body-kind.js.map +1 -0
  11. package/dist/body-limit.d.ts +5 -1
  12. package/dist/body-limit.d.ts.map +1 -1
  13. package/dist/body-limit.js +126 -0
  14. package/dist/body-limit.js.map +1 -0
  15. package/dist/build/index.d.ts +19 -3
  16. package/dist/build/index.d.ts.map +1 -1
  17. package/dist/build/index.js +173 -0
  18. package/dist/build/index.js.map +1 -0
  19. package/dist/cli-args.d.ts +33 -0
  20. package/dist/cli-args.d.ts.map +1 -0
  21. package/dist/cli-args.js +102 -0
  22. package/dist/cli-args.js.map +1 -0
  23. package/dist/cli-go/auth.go +26 -0
  24. package/dist/cli-go/config.go +192 -0
  25. package/dist/cli-go/errors.go +75 -0
  26. package/dist/cli-go/multipart.go +79 -0
  27. package/dist/cli-go/output.go +163 -0
  28. package/dist/cli-go/root.go +30 -0
  29. package/dist/cli-go/stream.go +88 -0
  30. package/dist/cli-go/version.go +5 -0
  31. package/dist/cli.d.ts +1 -1
  32. package/dist/cli.js +209 -0
  33. package/dist/cli.js.map +1 -0
  34. package/dist/client/defaults.js +48 -0
  35. package/dist/client/defaults.js.map +1 -0
  36. package/dist/client/error.d.ts +35 -0
  37. package/dist/client/error.d.ts.map +1 -1
  38. package/dist/client/error.js +71 -0
  39. package/dist/client/error.js.map +1 -0
  40. package/dist/client/http.d.ts +67 -8
  41. package/dist/client/http.d.ts.map +1 -1
  42. package/dist/client/http.js +571 -0
  43. package/dist/client/http.js.map +1 -0
  44. package/dist/client/index.d.ts +3 -2
  45. package/dist/client/index.d.ts.map +1 -1
  46. package/dist/client/index.js +95 -0
  47. package/dist/client/index.js.map +1 -0
  48. package/dist/client/path.d.ts +26 -0
  49. package/dist/client/path.d.ts.map +1 -0
  50. package/dist/client/path.js +163 -0
  51. package/dist/client/path.js.map +1 -0
  52. package/dist/client/redirect-policy.d.ts +12 -0
  53. package/dist/client/redirect-policy.d.ts.map +1 -0
  54. package/dist/client/redirect-policy.js +20 -0
  55. package/dist/client/redirect-policy.js.map +1 -0
  56. package/dist/client/sdk.d.ts +36 -0
  57. package/dist/client/sdk.d.ts.map +1 -1
  58. package/dist/client/sdk.js +292 -0
  59. package/dist/client/sdk.js.map +1 -0
  60. package/dist/client/sse.d.ts +13 -0
  61. package/dist/client/sse.d.ts.map +1 -1
  62. package/dist/client/sse.js +129 -0
  63. package/dist/client/sse.js.map +1 -0
  64. package/dist/client/types.js +2 -0
  65. package/dist/client/types.js.map +1 -0
  66. package/dist/client/ws.d.ts +9 -2
  67. package/dist/client/ws.d.ts.map +1 -1
  68. package/dist/client/ws.js +112 -0
  69. package/dist/client/ws.js.map +1 -0
  70. package/dist/client-go/errors.go +236 -0
  71. package/dist/client-go/export.go +44 -0
  72. package/dist/client-go/invalidation.go +371 -0
  73. package/dist/client-go/nullable.go +62 -0
  74. package/dist/client-go/realtime.go +354 -0
  75. package/dist/client-go/result.go +20 -0
  76. package/dist/client-go/runtime.go +975 -0
  77. package/dist/client-go/sse.go +159 -0
  78. package/dist/client-go/transport.go +495 -0
  79. package/dist/client-go/ws.go +128 -0
  80. package/dist/client-info.d.ts +36 -0
  81. package/dist/client-info.d.ts.map +1 -0
  82. package/dist/client-info.js +53 -0
  83. package/dist/client-info.js.map +1 -0
  84. package/dist/client-mcp/README.md +10 -0
  85. package/dist/client-mcp/server.ts +221 -0
  86. package/dist/client-mcp/types.ts +19 -0
  87. package/dist/client-python/_errors.py +115 -0
  88. package/dist/client-python/_invalidation.py +494 -0
  89. package/dist/client-python/_realtime.py +179 -0
  90. package/dist/client-python/_runtime.py +860 -0
  91. package/dist/client-python/_sse.py +155 -0
  92. package/dist/client-python/_transport.py +366 -0
  93. package/dist/client-python/_ws.py +159 -0
  94. package/dist/client-python/py.typed +0 -0
  95. package/dist/client-rust/errors.rs +220 -0
  96. package/dist/client-rust/invalidation.rs +412 -0
  97. package/dist/client-rust/invalidation_sync.rs +93 -0
  98. package/dist/client-rust/nullable.rs +17 -0
  99. package/dist/client-rust/realtime.rs +702 -0
  100. package/dist/client-rust/result.rs +22 -0
  101. package/dist/client-rust/runtime.rs +980 -0
  102. package/dist/client-rust/runtime_sync.rs +484 -0
  103. package/dist/client-rust/sse.rs +188 -0
  104. package/dist/client-rust/ws.rs +167 -0
  105. package/dist/codegen-go-cli.d.ts.map +1 -1
  106. package/dist/codegen-go-cli.js +964 -0
  107. package/dist/codegen-go-cli.js.map +1 -0
  108. package/dist/codegen-go.d.ts +15 -3
  109. package/dist/codegen-go.d.ts.map +1 -1
  110. package/dist/codegen-go.js +872 -0
  111. package/dist/codegen-go.js.map +1 -0
  112. package/dist/codegen-hash.js +9 -0
  113. package/dist/codegen-hash.js.map +1 -0
  114. package/dist/codegen-ir.d.ts +69 -2
  115. package/dist/codegen-ir.d.ts.map +1 -1
  116. package/dist/codegen-ir.js +661 -0
  117. package/dist/codegen-ir.js.map +1 -0
  118. package/dist/codegen-lang.d.ts +65 -0
  119. package/dist/codegen-lang.d.ts.map +1 -0
  120. package/dist/codegen-lang.js +526 -0
  121. package/dist/codegen-lang.js.map +1 -0
  122. package/dist/codegen-load.d.ts +31 -0
  123. package/dist/codegen-load.d.ts.map +1 -0
  124. package/dist/codegen-load.js +138 -0
  125. package/dist/codegen-load.js.map +1 -0
  126. package/dist/codegen-loaders.js +55 -0
  127. package/dist/codegen-loaders.js.map +1 -0
  128. package/dist/codegen-mcp.d.ts.map +1 -1
  129. package/dist/codegen-mcp.js +368 -0
  130. package/dist/codegen-mcp.js.map +1 -0
  131. package/dist/codegen-python.d.ts +13 -1
  132. package/dist/codegen-python.d.ts.map +1 -1
  133. package/dist/codegen-python.js +828 -0
  134. package/dist/codegen-python.js.map +1 -0
  135. package/dist/codegen-route-tree-intern.d.ts +15 -5
  136. package/dist/codegen-route-tree-intern.d.ts.map +1 -1
  137. package/dist/codegen-route-tree-intern.js +221 -0
  138. package/dist/codegen-route-tree-intern.js.map +1 -0
  139. package/dist/codegen-rust.d.ts.map +1 -1
  140. package/dist/codegen-rust.js +1293 -0
  141. package/dist/codegen-rust.js.map +1 -0
  142. package/dist/codegen-sanitize.d.ts.map +1 -1
  143. package/dist/codegen-sanitize.js +56 -0
  144. package/dist/codegen-sanitize.js.map +1 -0
  145. package/dist/codegen-schema-naming.d.ts +5 -0
  146. package/dist/codegen-schema-naming.d.ts.map +1 -1
  147. package/dist/codegen-schema-naming.js +150 -0
  148. package/dist/codegen-schema-naming.js.map +1 -0
  149. package/dist/codegen-sdk-model.d.ts +71 -0
  150. package/dist/codegen-sdk-model.d.ts.map +1 -0
  151. package/dist/codegen-sdk-model.js +179 -0
  152. package/dist/codegen-sdk-model.js.map +1 -0
  153. package/dist/codegen.d.ts +41 -23
  154. package/dist/codegen.d.ts.map +1 -1
  155. package/dist/codegen.js +4610 -0
  156. package/dist/codegen.js.map +1 -0
  157. package/dist/context.d.ts +36 -6
  158. package/dist/context.d.ts.map +1 -1
  159. package/dist/context.js +242 -0
  160. package/dist/context.js.map +1 -0
  161. package/dist/cookie-sign.d.ts +16 -2
  162. package/dist/cookie-sign.d.ts.map +1 -1
  163. package/dist/cookie-sign.js +104 -0
  164. package/dist/cookie-sign.js.map +1 -0
  165. package/dist/cookie.d.ts +12 -0
  166. package/dist/cookie.d.ts.map +1 -1
  167. package/dist/cookie.js +138 -0
  168. package/dist/cookie.js.map +1 -0
  169. package/dist/cors.d.ts +1 -0
  170. package/dist/cors.d.ts.map +1 -1
  171. package/dist/cors.js +87 -0
  172. package/dist/cors.js.map +1 -0
  173. package/dist/crypto.d.ts +6 -1
  174. package/dist/crypto.d.ts.map +1 -1
  175. package/dist/crypto.js +38 -0
  176. package/dist/crypto.js.map +1 -0
  177. package/dist/csrf.d.ts +19 -0
  178. package/dist/csrf.d.ts.map +1 -1
  179. package/dist/csrf.js +79 -0
  180. package/dist/csrf.js.map +1 -0
  181. package/dist/curl-logger.d.ts +14 -3
  182. package/dist/curl-logger.d.ts.map +1 -1
  183. package/dist/curl-logger.js +199 -0
  184. package/dist/curl-logger.js.map +1 -0
  185. package/dist/detect-runtime.js +13 -0
  186. package/dist/detect-runtime.js.map +1 -0
  187. package/dist/dict.d.ts +11 -0
  188. package/dist/dict.d.ts.map +1 -0
  189. package/dist/dict.js +13 -0
  190. package/dist/dict.js.map +1 -0
  191. package/dist/error.d.ts.map +1 -1
  192. package/dist/error.js +44 -0
  193. package/dist/error.js.map +1 -0
  194. package/dist/errors.js +39 -0
  195. package/dist/errors.js.map +1 -0
  196. package/dist/etag.d.ts +2 -0
  197. package/dist/etag.d.ts.map +1 -1
  198. package/dist/etag.js +82 -0
  199. package/dist/etag.js.map +1 -0
  200. package/dist/feature-detect.d.ts +25 -0
  201. package/dist/feature-detect.d.ts.map +1 -0
  202. package/dist/feature-detect.js +144 -0
  203. package/dist/feature-detect.js.map +1 -0
  204. package/dist/feature-load.js +36 -0
  205. package/dist/feature-load.js.map +1 -0
  206. package/dist/feature-slots.d.ts +44 -0
  207. package/dist/feature-slots.d.ts.map +1 -0
  208. package/dist/feature-slots.js +48 -0
  209. package/dist/feature-slots.js.map +1 -0
  210. package/dist/fetch-request.d.ts +10 -0
  211. package/dist/fetch-request.d.ts.map +1 -0
  212. package/dist/fetch-request.js +13 -0
  213. package/dist/fetch-request.js.map +1 -0
  214. package/dist/gen-process.d.ts +15 -0
  215. package/dist/gen-process.d.ts.map +1 -0
  216. package/dist/gen-process.js +43 -0
  217. package/dist/gen-process.js.map +1 -0
  218. package/dist/gen-write.d.ts +19 -0
  219. package/dist/gen-write.d.ts.map +1 -0
  220. package/dist/gen-write.js +120 -0
  221. package/dist/gen-write.js.map +1 -0
  222. package/dist/glob.d.ts +21 -0
  223. package/dist/glob.d.ts.map +1 -0
  224. package/dist/glob.js +115 -0
  225. package/dist/glob.js.map +1 -0
  226. package/dist/go-type-emitter.d.ts +18 -4
  227. package/dist/go-type-emitter.d.ts.map +1 -1
  228. package/dist/go-type-emitter.js +594 -0
  229. package/dist/go-type-emitter.js.map +1 -0
  230. package/dist/honey-response.d.ts.map +1 -1
  231. package/dist/honey-response.js +338 -0
  232. package/dist/honey-response.js.map +1 -0
  233. package/dist/i18n-slot.d.ts +8 -0
  234. package/dist/i18n-slot.d.ts.map +1 -1
  235. package/dist/i18n-slot.js +14 -0
  236. package/dist/i18n-slot.js.map +1 -0
  237. package/dist/i18n.d.ts +3 -0
  238. package/dist/i18n.d.ts.map +1 -1
  239. package/dist/i18n.js +250 -0
  240. package/dist/i18n.js.map +1 -0
  241. package/dist/index.d.ts +410 -149
  242. package/dist/index.d.ts.map +1 -1
  243. package/dist/index.js +2324 -0
  244. package/dist/index.js.map +1 -0
  245. package/dist/init.d.ts +3 -5
  246. package/dist/init.d.ts.map +1 -1
  247. package/dist/init.js +140 -0
  248. package/dist/init.js.map +1 -0
  249. package/dist/input.js +4 -0
  250. package/dist/input.js.map +1 -0
  251. package/dist/invalidate-check.d.ts +11 -0
  252. package/dist/invalidate-check.d.ts.map +1 -1
  253. package/dist/invalidate-check.js +163 -0
  254. package/dist/invalidate-check.js.map +1 -0
  255. package/dist/invoke-user.d.ts +35 -0
  256. package/dist/invoke-user.d.ts.map +1 -0
  257. package/dist/invoke-user.js +73 -0
  258. package/dist/invoke-user.js.map +1 -0
  259. package/dist/ip-restrict.d.ts +13 -1
  260. package/dist/ip-restrict.d.ts.map +1 -1
  261. package/dist/ip-restrict.js +60 -0
  262. package/dist/ip-restrict.js.map +1 -0
  263. package/dist/ip.d.ts +39 -0
  264. package/dist/ip.d.ts.map +1 -0
  265. package/dist/ip.js +209 -0
  266. package/dist/ip.js.map +1 -0
  267. package/dist/logger.d.ts.map +1 -1
  268. package/dist/logger.js +113 -0
  269. package/dist/logger.js.map +1 -0
  270. package/dist/media-type.d.ts +30 -0
  271. package/dist/media-type.d.ts.map +1 -0
  272. package/dist/media-type.js +97 -0
  273. package/dist/media-type.js.map +1 -0
  274. package/dist/meta-spec-merge.d.ts +32 -0
  275. package/dist/meta-spec-merge.d.ts.map +1 -0
  276. package/dist/meta-spec-merge.js +68 -0
  277. package/dist/meta-spec-merge.js.map +1 -0
  278. package/dist/meta-spec.d.ts +16 -3
  279. package/dist/meta-spec.d.ts.map +1 -1
  280. package/dist/meta-spec.js +535 -0
  281. package/dist/meta-spec.js.map +1 -0
  282. package/dist/middleware.d.ts +23 -6
  283. package/dist/middleware.d.ts.map +1 -1
  284. package/dist/middleware.js +253 -0
  285. package/dist/middleware.js.map +1 -0
  286. package/dist/node-request.d.ts +57 -12
  287. package/dist/node-request.d.ts.map +1 -1
  288. package/dist/node-request.js +407 -0
  289. package/dist/node-request.js.map +1 -0
  290. package/dist/node.d.ts +20 -3
  291. package/dist/node.d.ts.map +1 -1
  292. package/dist/node.js +396 -0
  293. package/dist/node.js.map +1 -0
  294. package/dist/openapi/collect.d.ts +6 -3
  295. package/dist/openapi/collect.d.ts.map +1 -1
  296. package/dist/openapi/collect.js +30 -0
  297. package/dist/openapi/collect.js.map +1 -0
  298. package/dist/openapi/docs-page.d.ts +42 -0
  299. package/dist/openapi/docs-page.d.ts.map +1 -0
  300. package/dist/openapi/docs-page.js +80 -0
  301. package/dist/openapi/docs-page.js.map +1 -0
  302. package/dist/openapi/document.d.ts +4 -0
  303. package/dist/openapi/document.d.ts.map +1 -1
  304. package/dist/openapi/document.js +785 -0
  305. package/dist/openapi/document.js.map +1 -0
  306. package/dist/openapi/json-schema-slot.d.ts +7 -0
  307. package/dist/openapi/json-schema-slot.d.ts.map +1 -1
  308. package/dist/openapi/json-schema-slot.js +38 -0
  309. package/dist/openapi/json-schema-slot.js.map +1 -0
  310. package/dist/openapi/register.d.ts.map +1 -1
  311. package/dist/openapi/register.js +20 -0
  312. package/dist/openapi/register.js.map +1 -0
  313. package/dist/openapi/scalar.d.ts +3 -1
  314. package/dist/openapi/scalar.d.ts.map +1 -1
  315. package/dist/openapi/scalar.js +21 -0
  316. package/dist/openapi/scalar.js.map +1 -0
  317. package/dist/openapi/serve.d.ts +9 -0
  318. package/dist/openapi/serve.d.ts.map +1 -0
  319. package/dist/openapi/serve.js +36 -0
  320. package/dist/openapi/serve.js.map +1 -0
  321. package/dist/openapi/spec-factory.d.ts +30 -1
  322. package/dist/openapi/spec-factory.d.ts.map +1 -1
  323. package/dist/openapi/spec-factory.js +52 -0
  324. package/dist/openapi/spec-factory.js.map +1 -0
  325. package/dist/openapi/spec.d.ts +14 -5
  326. package/dist/openapi/spec.d.ts.map +1 -1
  327. package/dist/openapi/spec.js +50 -0
  328. package/dist/openapi/spec.js.map +1 -0
  329. package/dist/openapi/swagger.d.ts +3 -1
  330. package/dist/openapi/swagger.d.ts.map +1 -1
  331. package/dist/openapi/swagger.js +22 -0
  332. package/dist/openapi/swagger.js.map +1 -0
  333. package/dist/pattern.d.ts +71 -0
  334. package/dist/pattern.d.ts.map +1 -0
  335. package/dist/pattern.js +239 -0
  336. package/dist/pattern.js.map +1 -0
  337. package/dist/peer.d.ts +17 -0
  338. package/dist/peer.d.ts.map +1 -0
  339. package/dist/peer.js +50 -0
  340. package/dist/peer.js.map +1 -0
  341. package/dist/plugin.d.ts +67 -16
  342. package/dist/plugin.d.ts.map +1 -1
  343. package/dist/plugin.js +649 -0
  344. package/dist/plugin.js.map +1 -0
  345. package/dist/powered-by.d.ts.map +1 -1
  346. package/dist/powered-by.js +11 -0
  347. package/dist/powered-by.js.map +1 -0
  348. package/dist/pretty-json.d.ts.map +1 -1
  349. package/dist/pretty-json.js +47 -0
  350. package/dist/pretty-json.js.map +1 -0
  351. package/dist/producer-stream.d.ts +47 -0
  352. package/dist/producer-stream.d.ts.map +1 -0
  353. package/dist/producer-stream.js +267 -0
  354. package/dist/producer-stream.js.map +1 -0
  355. package/dist/proxy.d.ts +47 -8
  356. package/dist/proxy.d.ts.map +1 -1
  357. package/dist/proxy.js +274 -0
  358. package/dist/proxy.js.map +1 -0
  359. package/dist/python-type-emitter.d.ts +14 -1
  360. package/dist/python-type-emitter.d.ts.map +1 -1
  361. package/dist/python-type-emitter.js +157 -0
  362. package/dist/python-type-emitter.js.map +1 -0
  363. package/dist/realtime/bus.d.ts +17 -1
  364. package/dist/realtime/bus.d.ts.map +1 -1
  365. package/dist/realtime/bus.js +112 -0
  366. package/dist/realtime/bus.js.map +1 -0
  367. package/dist/realtime/register.d.ts +3 -0
  368. package/dist/realtime/register.d.ts.map +1 -0
  369. package/dist/realtime/register.js +10 -0
  370. package/dist/realtime/register.js.map +1 -0
  371. package/dist/realtime/route.d.ts +65 -26
  372. package/dist/realtime/route.d.ts.map +1 -1
  373. package/dist/realtime/route.js +72 -0
  374. package/dist/realtime/route.js.map +1 -0
  375. package/dist/realtime/server.d.ts +43 -0
  376. package/dist/realtime/server.d.ts.map +1 -0
  377. package/dist/realtime/server.js +255 -0
  378. package/dist/realtime/server.js.map +1 -0
  379. package/dist/request-id.d.ts +7 -0
  380. package/dist/request-id.d.ts.map +1 -1
  381. package/dist/request-id.js +16 -0
  382. package/dist/request-id.js.map +1 -0
  383. package/dist/request-limits.d.ts +3 -0
  384. package/dist/request-limits.d.ts.map +1 -0
  385. package/dist/request-limits.js +3 -0
  386. package/dist/request-limits.js.map +1 -0
  387. package/dist/request-path.d.ts +33 -0
  388. package/dist/request-path.d.ts.map +1 -0
  389. package/dist/request-path.js +178 -0
  390. package/dist/request-path.js.map +1 -0
  391. package/dist/request-to-curl.d.ts +7 -1
  392. package/dist/request-to-curl.d.ts.map +1 -1
  393. package/dist/request-to-curl.js +48 -0
  394. package/dist/request-to-curl.js.map +1 -0
  395. package/dist/response.d.ts +25 -2
  396. package/dist/response.d.ts.map +1 -1
  397. package/dist/response.js +313 -0
  398. package/dist/response.js.map +1 -0
  399. package/dist/rust-type-emitter.d.ts +14 -2
  400. package/dist/rust-type-emitter.d.ts.map +1 -1
  401. package/dist/rust-type-emitter.js +463 -0
  402. package/dist/rust-type-emitter.js.map +1 -0
  403. package/dist/secure-headers.d.ts.map +1 -1
  404. package/dist/secure-headers.js +53 -0
  405. package/dist/secure-headers.js.map +1 -0
  406. package/dist/serve-register.d.ts +4 -0
  407. package/dist/serve-register.d.ts.map +1 -1
  408. package/dist/serve-register.js +9 -0
  409. package/dist/serve-register.js.map +1 -0
  410. package/dist/serve-slot.d.ts +3 -0
  411. package/dist/serve-slot.d.ts.map +1 -1
  412. package/dist/serve-slot.js +52 -0
  413. package/dist/serve-slot.js.map +1 -0
  414. package/dist/serve.d.ts +18 -1
  415. package/dist/serve.d.ts.map +1 -1
  416. package/dist/serve.js +157 -0
  417. package/dist/serve.js.map +1 -0
  418. package/dist/server-timing.d.ts.map +1 -1
  419. package/dist/server-timing.js +51 -0
  420. package/dist/server-timing.js.map +1 -0
  421. package/dist/static.d.ts +9 -0
  422. package/dist/static.d.ts.map +1 -1
  423. package/dist/static.js +102 -0
  424. package/dist/static.js.map +1 -0
  425. package/dist/telemetry/otel.d.ts +2 -0
  426. package/dist/telemetry/otel.d.ts.map +1 -1
  427. package/dist/telemetry/otel.js +135 -0
  428. package/dist/telemetry/otel.js.map +1 -0
  429. package/dist/testing.d.ts +12 -0
  430. package/dist/testing.d.ts.map +1 -1
  431. package/dist/testing.js +124 -0
  432. package/dist/testing.js.map +1 -0
  433. package/dist/timeout.d.ts +6 -0
  434. package/dist/timeout.d.ts.map +1 -1
  435. package/dist/timeout.js +36 -0
  436. package/dist/timeout.js.map +1 -0
  437. package/dist/tree.d.ts +133 -36
  438. package/dist/tree.d.ts.map +1 -1
  439. package/dist/tree.js +437 -0
  440. package/dist/tree.js.map +1 -0
  441. package/dist/trust.d.ts +21 -0
  442. package/dist/trust.d.ts.map +1 -0
  443. package/dist/trust.js +100 -0
  444. package/dist/trust.js.map +1 -0
  445. package/dist/ts-type-emitter.d.ts.map +1 -1
  446. package/dist/ts-type-emitter.js +150 -0
  447. package/dist/ts-type-emitter.js.map +1 -0
  448. package/dist/type-emitter.d.ts +16 -2
  449. package/dist/type-emitter.d.ts.map +1 -1
  450. package/dist/type-emitter.js +729 -0
  451. package/dist/type-emitter.js.map +1 -0
  452. package/dist/type-extractor.d.ts +8 -4
  453. package/dist/type-extractor.d.ts.map +1 -1
  454. package/dist/type-extractor.js +776 -0
  455. package/dist/type-extractor.js.map +1 -0
  456. package/dist/types.d.ts +7 -0
  457. package/dist/types.d.ts.map +1 -1
  458. package/dist/types.js +96 -0
  459. package/dist/types.js.map +1 -0
  460. package/dist/validation.d.ts +14 -2
  461. package/dist/validation.d.ts.map +1 -1
  462. package/dist/validation.js +335 -0
  463. package/dist/validation.js.map +1 -0
  464. package/dist/with-headers.d.ts +15 -0
  465. package/dist/with-headers.d.ts.map +1 -0
  466. package/dist/with-headers.js +45 -0
  467. package/dist/with-headers.js.map +1 -0
  468. package/dist/ws/bun.d.ts +11 -5
  469. package/dist/ws/bun.d.ts.map +1 -1
  470. package/dist/ws/bun.js +82 -0
  471. package/dist/ws/bun.js.map +1 -0
  472. package/dist/ws/cloudflare.d.ts +25 -3
  473. package/dist/ws/cloudflare.d.ts.map +1 -1
  474. package/dist/ws/cloudflare.js +117 -0
  475. package/dist/ws/cloudflare.js.map +1 -0
  476. package/dist/ws/deno.d.ts +4 -4
  477. package/dist/ws/deno.d.ts.map +1 -1
  478. package/dist/ws/deno.js +133 -0
  479. package/dist/ws/deno.js.map +1 -0
  480. package/dist/ws/node.d.ts +20 -3
  481. package/dist/ws/node.d.ts.map +1 -1
  482. package/dist/ws/node.js +173 -0
  483. package/dist/ws/node.js.map +1 -0
  484. package/dist/ws/session.d.ts +11 -0
  485. package/dist/ws/session.d.ts.map +1 -0
  486. package/dist/ws/session.js +60 -0
  487. package/dist/ws/session.js.map +1 -0
  488. package/dist/ws/shared.d.ts +65 -0
  489. package/dist/ws/shared.d.ts.map +1 -0
  490. package/dist/ws/shared.js +103 -0
  491. package/dist/ws/shared.js.map +1 -0
  492. package/dist/ws-origin.d.ts +20 -0
  493. package/dist/ws-origin.d.ts.map +1 -0
  494. package/dist/ws-origin.js +41 -0
  495. package/dist/ws-origin.js.map +1 -0
  496. package/dist/yaml.d.ts +15 -1
  497. package/dist/yaml.d.ts.map +1 -1
  498. package/dist/yaml.js +101 -0
  499. package/dist/yaml.js.map +1 -0
  500. package/docs/invalidate-spec.md +13 -1
  501. package/docs/meta-spec.md +36 -12
  502. package/docs/packaging.md +43 -24
  503. package/docs/regression-matrix/README.md +32 -0
  504. package/docs/regression-matrix/review.md +44 -0
  505. package/docs/regression-matrix/ws1-3.md +97 -0
  506. package/docs/regression-matrix/ws4-8.md +107 -0
  507. package/docs/regression-matrix/ws9-12.md +266 -0
  508. package/docs/sdk.md +102 -52
  509. package/package.json +183 -52
  510. package/src/accepts.ts +91 -33
  511. package/src/body-kind.ts +67 -0
  512. package/src/body-limit.ts +102 -87
  513. package/src/build/index.ts +104 -54
  514. package/src/cli-args.ts +123 -0
  515. package/src/cli-go/auth.go +12 -1
  516. package/src/cli-go/config.go +29 -2
  517. package/src/cli-go/errors.go +28 -10
  518. package/src/cli-go/multipart.go +51 -27
  519. package/src/cli-go/output.go +58 -4
  520. package/src/cli-go/root.go +1 -1
  521. package/src/cli-go/version.go +2 -2
  522. package/src/cli.ts +154 -148
  523. package/src/client/error.ts +56 -1
  524. package/src/client/http.ts +479 -256
  525. package/src/client/index.ts +55 -17
  526. package/src/client/path.ts +169 -0
  527. package/src/client/redirect-policy.ts +19 -0
  528. package/src/client/sdk.ts +180 -101
  529. package/src/client/sse.ts +92 -66
  530. package/src/client/ws.ts +67 -31
  531. package/src/client-go/errors.go +41 -12
  532. package/src/client-go/export.go +44 -0
  533. package/src/client-go/invalidation.go +52 -54
  534. package/src/client-go/nullable.go +62 -0
  535. package/src/client-go/realtime.go +147 -110
  536. package/src/client-go/runtime.go +646 -88
  537. package/src/client-go/sse.go +104 -94
  538. package/src/client-go/transport.go +67 -27
  539. package/src/client-go/ws.go +49 -25
  540. package/src/client-info.ts +79 -0
  541. package/src/client-mcp/server.ts +133 -3
  542. package/src/client-python/_invalidation.py +207 -291
  543. package/src/client-python/_realtime.py +63 -40
  544. package/src/client-python/_runtime.py +501 -119
  545. package/src/client-python/_sse.py +130 -74
  546. package/src/client-python/_transport.py +37 -9
  547. package/src/client-python/_ws.py +50 -22
  548. package/src/client-rust/errors.rs +21 -1
  549. package/src/client-rust/invalidation.rs +202 -231
  550. package/src/client-rust/invalidation_sync.rs +16 -229
  551. package/src/client-rust/nullable.rs +17 -0
  552. package/src/client-rust/realtime.rs +217 -217
  553. package/src/client-rust/runtime.rs +602 -177
  554. package/src/client-rust/runtime_sync.rs +169 -145
  555. package/src/client-rust/sse.rs +142 -121
  556. package/src/client-rust/ws.rs +61 -37
  557. package/src/codegen-go-cli.ts +607 -728
  558. package/src/codegen-go.ts +710 -770
  559. package/src/codegen-ir.ts +381 -75
  560. package/src/codegen-lang.ts +517 -0
  561. package/src/codegen-load.ts +150 -0
  562. package/src/codegen-mcp.ts +207 -108
  563. package/src/codegen-python.ts +684 -816
  564. package/src/codegen-route-tree-intern.ts +51 -25
  565. package/src/codegen-rust.ts +946 -1240
  566. package/src/codegen-sanitize.ts +0 -4
  567. package/src/codegen-schema-naming.ts +19 -6
  568. package/src/codegen-sdk-model.ts +228 -0
  569. package/src/codegen.ts +1488 -1140
  570. package/src/context.ts +130 -11
  571. package/src/cookie-sign.ts +85 -16
  572. package/src/cookie.ts +66 -11
  573. package/src/cors.ts +55 -54
  574. package/src/crypto.ts +27 -5
  575. package/src/csrf.ts +57 -25
  576. package/src/curl-logger.ts +155 -114
  577. package/src/dict.ts +12 -0
  578. package/src/error.ts +4 -2
  579. package/src/etag.ts +58 -34
  580. package/src/feature-detect.ts +147 -0
  581. package/src/feature-slots.ts +70 -0
  582. package/src/fetch-request.ts +13 -0
  583. package/src/gen-process.ts +42 -0
  584. package/src/gen-write.ts +127 -0
  585. package/src/glob.ts +109 -0
  586. package/src/go-type-emitter.ts +406 -155
  587. package/src/honey-response.ts +36 -9
  588. package/src/i18n-slot.ts +10 -0
  589. package/src/i18n.ts +98 -23
  590. package/src/index.ts +1893 -1761
  591. package/src/init.ts +40 -28
  592. package/src/invalidate-check.ts +42 -4
  593. package/src/invoke-user.ts +95 -0
  594. package/src/ip-restrict.ts +47 -161
  595. package/src/ip.ts +194 -0
  596. package/src/logger.ts +65 -36
  597. package/src/media-type.ts +104 -0
  598. package/src/meta-spec-merge.ts +80 -0
  599. package/src/meta-spec.ts +0 -0
  600. package/src/middleware.ts +112 -12
  601. package/src/node-request.ts +242 -52
  602. package/src/node.ts +318 -75
  603. package/src/openapi/collect.ts +19 -44
  604. package/src/openapi/docs-page.ts +89 -0
  605. package/src/openapi/document.ts +503 -224
  606. package/src/openapi/json-schema-slot.ts +40 -0
  607. package/src/openapi/register.ts +7 -2
  608. package/src/openapi/scalar.ts +11 -10
  609. package/src/openapi/serve.ts +39 -0
  610. package/src/openapi/spec-factory.ts +50 -1
  611. package/src/openapi/spec.ts +53 -30
  612. package/src/openapi/swagger.ts +12 -11
  613. package/src/pattern.ts +245 -0
  614. package/src/peer.ts +53 -0
  615. package/src/plugin.ts +270 -185
  616. package/src/powered-by.ts +2 -2
  617. package/src/pretty-json.ts +25 -9
  618. package/src/producer-stream.ts +296 -0
  619. package/src/proxy.ts +271 -55
  620. package/src/python-type-emitter.ts +68 -56
  621. package/src/realtime/bus.ts +56 -9
  622. package/src/realtime/register.ts +13 -0
  623. package/src/realtime/route.ts +129 -80
  624. package/src/realtime/server.ts +298 -0
  625. package/src/request-id.ts +13 -3
  626. package/src/request-limits.ts +2 -0
  627. package/src/request-path.ts +165 -0
  628. package/src/request-to-curl.ts +28 -7
  629. package/src/response.ts +143 -103
  630. package/src/rust-type-emitter.ts +265 -170
  631. package/src/secure-headers.ts +8 -4
  632. package/src/serve-register.ts +4 -0
  633. package/src/serve-slot.ts +38 -0
  634. package/src/serve.ts +113 -25
  635. package/src/server-timing.ts +11 -7
  636. package/src/static.ts +85 -32
  637. package/src/telemetry/otel.ts +29 -7
  638. package/src/testing.ts +76 -14
  639. package/src/timeout.ts +15 -7
  640. package/src/tree.ts +405 -333
  641. package/src/trust.ts +117 -0
  642. package/src/ts-type-emitter.ts +18 -12
  643. package/src/type-emitter.ts +227 -89
  644. package/src/type-extractor.ts +562 -424
  645. package/src/types.ts +8 -0
  646. package/src/validation.ts +221 -96
  647. package/src/with-headers.ts +45 -0
  648. package/src/ws/bun.ts +53 -15
  649. package/src/ws/cloudflare.ts +81 -19
  650. package/src/ws/deno.ts +124 -36
  651. package/src/ws/node.ts +173 -79
  652. package/src/ws/session.ts +73 -0
  653. package/src/ws/shared.ts +140 -0
  654. package/src/ws-origin.ts +53 -0
  655. package/src/yaml.ts +45 -22
  656. package/dist/client-mcp/server.d.ts +0 -10
  657. package/dist/client-mcp/server.d.ts.map +0 -1
  658. package/dist/client-mcp/types.d.ts +0 -16
  659. package/dist/client-mcp/types.d.ts.map +0 -1
  660. package/dist/cloudflare/realtime.d.ts +0 -30
  661. package/dist/cloudflare/realtime.d.ts.map +0 -1
  662. package/dist/realtime/buffer.d.ts +0 -18
  663. package/dist/realtime/buffer.d.ts.map +0 -1
  664. package/dist/realtime/longpoll.d.ts +0 -23
  665. package/dist/realtime/longpoll.d.ts.map +0 -1
  666. package/dist/realtime/protocol.d.ts +0 -50
  667. package/dist/realtime/protocol.d.ts.map +0 -1
  668. package/dist/realtime/runtime.d.ts +0 -74
  669. package/dist/realtime/runtime.d.ts.map +0 -1
  670. package/src/cloudflare/realtime.ts +0 -234
  671. package/src/realtime/buffer.ts +0 -101
  672. package/src/realtime/longpoll.ts +0 -148
  673. package/src/realtime/protocol.ts +0 -128
  674. package/src/realtime/runtime.ts +0 -427
@@ -1,11 +1,11 @@
1
1
  /* Go CLI emitter — generates a complete cobra-based Go CLI module from an OpenAPI 3.1 spec.
2
2
  *
3
- * Mirrors the pattern used by codegen-go.ts:
3
+ * - Input is the SDK request model (codegen-sdk-model.ts), shared with the Go SDK emitter.
4
4
  * - Static runtime files (config.go, auth.go, output.go, errors.go, stream.go, multipart.go,
5
- * version.go, root.go) are read from ./cli-go/ at module load time and copied verbatim
6
- * into the output file map under "cli-go/<name>".
5
+ * version.go, root.go) are read from ./cli-go/ and copied verbatim under "internal/cli/".
7
6
  * - Per-resource cobra command files are emitted as cmd/<resource>.go.
8
- * - main.go and cmd/root.go wire cobra + auth + exit-code mapping.
7
+ * - URLs, query encoding, SSE parsing and status errors go through the SDK's exported
8
+ * Honey* helpers, so the CLI sends exactly what the SDK sends.
9
9
  *
10
10
  * Two SDK wiring modes:
11
11
  * - sdkModulePath set → go.mod emits `require` + `replace`; no internal/sdk/ files.
@@ -15,10 +15,13 @@
15
15
 
16
16
  import { readFileSync } from "node:fs"
17
17
  import { fileURLToPath } from "node:url"
18
- import { generateGoSDK, safeResourceName } from "./codegen-go.ts"
19
- import { collectSDKMethods, extractOpenApiPathParams, isSSEOperation, resolveRefs } from "./codegen.ts"
20
- import { toIR } from "./codegen-ir.ts"
21
- import { goPascal } from "./go-type-emitter.ts"
18
+ import type { OpenApiSpecInput } from "./codegen.ts"
19
+ import { serviceMapOf } from "./codegen.ts"
20
+ import { GO_KEYWORDS, GO_PREDECLARED, NameScope, cmpCodeUnit, goExported, goLocal, goString } from "./codegen-lang.ts"
21
+ import type { IRInfo, IRParam, IRSchema } from "./codegen-ir.ts"
22
+ import { buildSdkModel } from "./codegen-sdk-model.ts"
23
+ import type { SdkModel, SdkOp } from "./codegen-sdk-model.ts"
24
+ import { generateGoSDK } from "./codegen-go.ts"
22
25
 
23
26
  const DEFAULT_MODULE_PATH = "example.com/cli"
24
27
 
@@ -26,14 +29,6 @@ function resolveModulePath(options: GoCLIOptions): string {
26
29
  return options.modulePath ?? DEFAULT_MODULE_PATH
27
30
  }
28
31
 
29
- function enumStrings(schema: Record<string, unknown>): string[] {
30
- return Array.isArray(schema.enum) ? (schema.enum as unknown[]).map((v) => String(v)) : []
31
- }
32
-
33
- function isLastEventIdName(name: string): boolean {
34
- return name === "last-event-id" || name === "last_event_id" || name === "lastEventId"
35
- }
36
-
37
32
  /* ── types ── */
38
33
 
39
34
  export type GoCLIOptions = {
@@ -52,66 +47,47 @@ export type GeneratedGoCLI = {
52
47
  skippedOperations: string[]
53
48
  }
54
49
 
55
- type OpenApiSpec = {
56
- components?: { schemas?: Record<string, Record<string, unknown>> }
57
- info: { description?: string; title: string; version: string }
58
- openapi: string
59
- paths: Record<string, Record<string, Record<string, unknown>>>
60
- }
50
+ type FlagKind = "string" | "int" | "float" | "bool" | "strings"
61
51
 
62
- type ParamInfo = {
52
+ type Flag = {
53
+ /** Wire name of the param / property. */
63
54
  name: string
64
55
  flagName: string
65
56
  varName: string
57
+ kind: FlagKind
66
58
  required: boolean
67
- schema: Record<string, unknown>
68
- defaultValue: unknown
69
- enumValues: string[]
70
- }
71
-
72
- type BodyScalarInfo = {
73
- propName: string
74
- flagName: string
75
- varName: string
76
- schemaType: string
77
59
  enumValues: string[]
60
+ defaultValue: unknown
61
+ where: "path" | "query" | "header" | "body" | "file"
78
62
  }
79
63
 
80
64
  type BodyShape =
81
65
  | { kind: "none" }
82
- | { kind: "json-flat"; scalars: BodyScalarInfo[]; required: boolean }
83
- | { kind: "json-complex"; required: boolean }
84
- | {
85
- kind: "multipart"
86
- binaryFields: { name: string; required: boolean }[]
87
- scalarFields: BodyScalarInfo[]
88
- useGenericFileFlag: boolean
89
- }
66
+ | { kind: "json-flat"; scalars: Flag[]; required: boolean; dataVar: string }
67
+ | { kind: "json-complex"; required: boolean; dataVar: string }
68
+ | { kind: "form"; scalars: Flag[]; required: boolean }
69
+ | { kind: "multipart"; scalars: Flag[]; files: Flag[] }
70
+ | { kind: "raw"; contentType: string; required: boolean; dataVar: string }
90
71
 
91
72
  type CommandInfo = {
92
- opId: string
93
- resource: string
94
- action: string /* joined-with-dash deeper-than-2 dot segments */
95
- cmdVarName: string /* e.g. "usersGetCmd" */
96
- cmdUse: string /* e.g. "get" */
97
- method: string /* GET, POST, ... */
98
- path: string
99
- summary: string
100
- description: string
101
- pathParams: ParamInfo[]
102
- queryParams: ParamInfo[]
73
+ op: SdkOp
74
+ cmdVarName: string
75
+ cmdUse: string
76
+ pathFlags: Flag[]
77
+ queryFlags: Flag[]
78
+ headerFlags: Flag[]
103
79
  body: BodyShape
104
- isSSE: boolean
105
- hasLastEventId: boolean
106
- has204: boolean
107
- responseType: "json" | "none"
80
+ lastEventIdVar?: string
108
81
  }
109
82
 
110
83
  type ResourceGroup = {
111
- resource: string /* raw resource e.g. "apiKey" */
112
- cmdUse: string /* kebab e.g. "api-key" */
113
- cmdVarBase: string /* Go var prefix e.g. "apiKey" or "type_" */
114
- fileName: string /* e.g. "apiKey.go" (matches raw resource) */
84
+ resource: string
85
+ cmdUse: string
86
+ /** prefix for per-command var names */
87
+ cmdVarBase: string
88
+ /** the resource's own cobra.Command var */
89
+ groupVar: string
90
+ fileName: string
115
91
  commands: CommandInfo[]
116
92
  }
117
93
 
@@ -123,13 +99,13 @@ export function toKebab(s: string): string {
123
99
  return s
124
100
  .replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2")
125
101
  .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
126
- .replace(/[\s_]+/g, "-")
127
- .replace(/-+/g, "-")
102
+ .replace(/[^A-Za-z0-9]+/g, "-")
103
+ .replace(/^-+|-+$/g, "")
128
104
  .toLowerCase()
129
105
  }
130
106
 
131
107
  export function toFlagName(paramName: string): string {
132
- return toKebab(paramName)
108
+ return toKebab(paramName) || "value"
133
109
  }
134
110
 
135
111
  /** Upper snake case for env var: toSnakeUpper("project-id") → "PROJECT_ID". */
@@ -141,21 +117,6 @@ export function envVarFromFlag(flagName: string, envPrefix: string): string {
141
117
  return `${envPrefix}_${toSnakeUpper(flagName)}`
142
118
  }
143
119
 
144
- /** Go var name from a param name: snake_case or kebab-case → camelCase with preserved acronyms.
145
- * e.g. "project_id" → "projectID", "user-name" → "userName", "type" → "type_". */
146
- function goCamelVar(name: string): string {
147
- const pascal = goPascal(name)
148
- if (!pascal) return pascal
149
- /* preserve acronym tail: "Id" → "ID", "Url" → "URL" */
150
- const fixed = pascal
151
- .replace(/\bId\b/g, "ID")
152
- .replace(/\bUrl\b/g, "URL")
153
- .replace(/Id$/, "ID")
154
- .replace(/Url$/, "URL")
155
- const camel = fixed.charAt(0).toLowerCase() + fixed.slice(1)
156
- return safeResourceName(camel)
157
- }
158
-
159
120
  /* ── runtime template loader ── */
160
121
 
161
122
  let runtimeCache: Map<string, string> | null = null
@@ -179,260 +140,228 @@ export function loadGoCliRuntimeTemplates(): Map<string, string> {
179
140
  return cache
180
141
  }
181
142
 
182
- /* ── param + body helpers ── */
183
-
184
- function getPathParams(op: Record<string, unknown>, rawPath: string): ParamInfo[] {
185
- const declared = extractOpenApiPathParams(rawPath)
186
- const byName = new Map<string, Record<string, unknown>>()
187
- const parameters = (op.parameters as Array<Record<string, unknown>> | undefined) ?? []
188
- for (const p of parameters) {
189
- if (p.in !== "path") continue
190
- byName.set(String(p.name), p)
191
- }
192
-
193
- const out: ParamInfo[] = []
194
- for (const name of declared) {
195
- const param = byName.get(name)
196
- const schema = (param?.schema as Record<string, unknown>) ?? { type: "string" }
197
- out.push({
198
- defaultValue: schema.default,
199
- enumValues: enumStrings(schema),
200
- flagName: toFlagName(name),
201
- name,
202
- required: true,
203
- schema,
204
- varName: goCamelVar(name),
205
- })
206
- }
207
- return out
143
+ /* ── naming ── */
144
+
145
+ /** Persistent flags on the root command, plus cobra's own. A command flag must not shadow them. */
146
+ const GLOBAL_FLAGS = ["api-key", "base-url", "output", "verbose", "config", "timeout", "help", "version"]
147
+
148
+ /** Identifiers every cmd/*.go file shares: imports and the package-level names root.go declares. */
149
+ const CMD_PACKAGE_NAMES = [
150
+ ...GO_KEYWORDS,
151
+ ...GO_PREDECLARED,
152
+ "rootCmd",
153
+ "Execute",
154
+ "configFromCtx",
155
+ "configKey",
156
+ "runtimeConfig",
157
+ "EnvPrefix",
158
+ "ConfigName",
159
+ "DefaultBaseURL",
160
+ "AuthHeaderName",
161
+ "AuthHeaderPrefix",
162
+ "newHTTPClient",
163
+ "doJSON",
164
+ "init",
165
+ ]
166
+
167
+ /** File stems Go treats specially, or that the generator writes itself. */
168
+ const GOOS_GOARCH =
169
+ /_(aix|android|darwin|dragonfly|freebsd|hurd|illumos|ios|js|linux|nacl|netbsd|openbsd|plan9|solaris|wasip1|windows|zos|386|amd64|arm|arm64|loong64|mips|mipsle|mips64|mips64le|ppc64|ppc64le|riscv64|s390x|wasm|test)$/
170
+
171
+ function resourceFileName(resource: string, taken: NameScope): string {
172
+ let stem = resource.replace(/[^A-Za-z0-9_-]/g, "_").replace(/^[-_.]+/, "")
173
+ if (stem === "" || /^(root|main|doc)$/i.test(stem) || GOOS_GOARCH.test(stem.toLowerCase())) stem = `res_${stem}`
174
+ return `${taken.claim(stem)}.go`
208
175
  }
209
176
 
210
- function getQueryParams(op: Record<string, unknown>): ParamInfo[] {
211
- const parameters = (op.parameters as Array<Record<string, unknown>> | undefined) ?? []
212
- const out: ParamInfo[] = []
213
- for (const p of parameters) {
214
- if (p.in !== "query") continue
215
- const name = String(p.name)
216
- const schema = (p.schema as Record<string, unknown>) ?? { type: "string" }
217
- out.push({
218
- defaultValue: schema.default,
219
- enumValues: enumStrings(schema),
220
- flagName: toFlagName(name),
221
- name,
222
- required: p.required === true,
223
- schema,
224
- varName: goCamelVar(name),
225
- })
177
+ function flagKind(schema: IRSchema, resolve: (s: IRSchema) => IRSchema): FlagKind {
178
+ let s = resolve(schema)
179
+ if (s.kind === "nullable") s = resolve(s.inner)
180
+ if (s.kind === "array") {
181
+ return "strings"
226
182
  }
227
- return out
228
- }
229
-
230
- function isScalarType(t: unknown): t is "string" | "integer" | "number" | "boolean" {
231
- return t === "string" || t === "integer" || t === "number" || t === "boolean"
232
- }
233
-
234
- function detectBodyShape(op: Record<string, unknown>): BodyShape {
235
- const requestBody = op.requestBody as Record<string, unknown> | undefined
236
- if (!requestBody) return { kind: "none" }
237
- const required = requestBody.required === true
238
- const content = requestBody.content as Record<string, Record<string, unknown>> | undefined
239
- if (!content) return { kind: "none" }
240
-
241
- /* multipart wins over json */
242
- const multipart = content["multipart/form-data"]
243
- if (multipart) {
244
- const schema = (multipart.schema as Record<string, unknown>) ?? {}
245
- const props = (schema.properties as Record<string, Record<string, unknown>>) ?? {}
246
- const reqList = new Set(Array.isArray(schema.required) ? (schema.required as string[]) : [])
247
- const binaryFields: { name: string; required: boolean }[] = []
248
- const scalarFields: BodyScalarInfo[] = []
249
- for (const [propName, propSchema] of Object.entries(props)) {
250
- const type = propSchema.type
251
- const format = propSchema.format
252
- if (type === "string" && format === "binary") {
253
- binaryFields.push({ name: propName, required: reqList.has(propName) })
254
- } else if (isScalarType(type)) {
255
- scalarFields.push({
256
- enumValues: enumStrings(propSchema),
257
- flagName: toFlagName(propName),
258
- propName,
259
- schemaType: String(type),
260
- varName: goCamelVar(propName),
261
- })
262
- }
263
- }
264
- return {
265
- binaryFields,
266
- kind: "multipart",
267
- scalarFields,
268
- useGenericFileFlag: binaryFields.length === 1,
269
- }
270
- }
271
-
272
- const jsonContent = content["application/json"]
273
- if (!jsonContent) return { kind: "none" }
274
- const schema = (jsonContent.schema as Record<string, unknown>) ?? {}
275
-
276
- /* oneOf/anyOf/allOf, array-root, or non-object → complex */
277
- if (schema.oneOf || schema.anyOf || schema.allOf) {
278
- return { kind: "json-complex", required }
279
- }
280
- if (schema.type !== "object") {
281
- return { kind: "json-complex", required }
183
+ if (s.kind === "scalar") {
184
+ if (s.type === "integer") return "int"
185
+ if (s.type === "number") return "float"
186
+ if (s.type === "boolean") return "bool"
282
187
  }
188
+ if (s.kind === "const" && typeof s.value === "boolean") return "bool"
189
+ return "string"
190
+ }
283
191
 
284
- const props = (schema.properties as Record<string, Record<string, unknown>>) ?? {}
285
- const scalars: BodyScalarInfo[] = []
286
- let allScalar = true
287
- for (const [propName, propSchema] of Object.entries(props)) {
288
- if (!isScalarType(propSchema.type)) {
289
- allScalar = false
290
- break
291
- }
292
- scalars.push({
293
- enumValues: enumStrings(propSchema),
294
- flagName: toFlagName(propName),
295
- propName,
296
- schemaType: String(propSchema.type),
297
- varName: goCamelVar(propName),
298
- })
299
- }
300
- if (!allScalar) return { kind: "json-complex", required }
301
- return { kind: "json-flat", required, scalars }
192
+ function enumOf(schema: IRSchema, resolve: (s: IRSchema) => IRSchema): string[] {
193
+ let s = resolve(schema)
194
+ if (s.kind === "nullable") s = resolve(s.inner)
195
+ if (s.kind === "scalar" && s.enum) return s.enum.map(String)
196
+ return []
302
197
  }
303
198
 
304
- function has204Response(op: Record<string, unknown>): boolean {
305
- const responses = op.responses as Record<string, Record<string, unknown>> | undefined
306
- if (!responses) return false
307
- return Object.prototype.hasOwnProperty.call(responses, "204")
199
+ function isScalarSchema(schema: IRSchema, resolve: (s: IRSchema) => IRSchema): boolean {
200
+ let s = resolve(schema)
201
+ if (s.kind === "nullable") s = resolve(s.inner)
202
+ return s.kind === "scalar" || s.kind === "const"
308
203
  }
309
204
 
310
- function hasJsonResponse(op: Record<string, unknown>): boolean {
311
- const responses = op.responses as Record<string, Record<string, unknown>> | undefined
312
- if (!responses) return false
313
- for (const [code, resp] of Object.entries(responses)) {
314
- const n = Number.parseInt(code, 10)
315
- if (n < 200 || n >= 300 || n === 204) continue
316
- const content = resp.content as Record<string, Record<string, unknown>> | undefined
317
- if (content?.["application/json"]) return true
205
+ /** Go literal for a flag default, or the zero value. */
206
+ function flagDefault(f: Flag): string {
207
+ const d = f.defaultValue
208
+ switch (f.kind) {
209
+ case "int":
210
+ return typeof d === "number" && Number.isInteger(d) ? String(d) : "0"
211
+ case "float":
212
+ return typeof d === "number" && Number.isFinite(d) ? String(d) : "0"
213
+ case "bool":
214
+ return d === true ? "true" : "false"
215
+ case "strings":
216
+ return Array.isArray(d) ? `[]string{${d.map((x) => goString(String(x))).join(", ")}}` : "nil"
217
+ default:
218
+ return typeof d === "string" ? goString(d) : `""`
318
219
  }
319
- return false
320
220
  }
321
221
 
322
222
  /* ── command collection ── */
323
223
 
324
- function collectCLIMethods(spec: Record<string, unknown>): {
325
- groups: Map<string, ResourceGroup>
326
- skipped: string[]
327
- serviceMap: Record<string, Record<string, unknown>>
328
- } {
329
- toIR(spec as Parameters<typeof toIR>[0])
330
- const { serviceMap } = collectSDKMethods(spec as Parameters<typeof collectSDKMethods>[0])
331
- const resolved = resolveRefs(spec as Parameters<typeof resolveRefs>[0]) as unknown as OpenApiSpec
332
-
224
+ function collectCLIMethods(model: SdkModel, pkg: NameScope): { groups: Map<string, ResourceGroup>; skipped: string[] } {
333
225
  const groups = new Map<string, ResourceGroup>()
334
226
  const skipped: string[] = []
335
227
  const seenCmdPaths = new Map<string, string>()
228
+ const fileNames = new NameScope(["root", "main"], (n) => n.toLowerCase())
229
+ const { resolve } = model
336
230
 
337
- for (const [rawPath, pathMethods] of Object.entries(resolved.paths ?? {})) {
338
- for (const [method, operation] of Object.entries(pathMethods)) {
339
- const op = operation as Record<string, unknown>
340
- const operationId = op.operationId as string | undefined
341
- if (!operationId) continue
231
+ for (const op of model.ops) {
232
+ if (op.stream === "ws" || op.stream === "realtime") {
233
+ skipped.push(op.id)
234
+ continue
235
+ }
342
236
 
343
- if (op["x-websocket"] === true) {
344
- skipped.push(operationId)
345
- continue
237
+ const segments = op.segments
238
+ const resource = segments[0] ?? op.id
239
+ /* Top-level ops (single segment) become `<resource> call` — cobra rejects leading dashes. */
240
+ const actionSegments = segments.length === 1 ? ["call"] : segments.slice(1).map((seg) => toKebab(seg))
241
+ const cmdUse = actionSegments.join("-")
242
+
243
+ /* collision check: (resource, cmdUse) must be unique */
244
+ const key = `${resource}|${cmdUse}`
245
+ const prior = seenCmdPaths.get(key)
246
+ if (prior !== undefined) {
247
+ throw new Error(
248
+ `go-cli codegen: command path collision for ${resource} ${cmdUse} — operations: ${prior}, ${op.id}`,
249
+ )
250
+ }
251
+ seenCmdPaths.set(key, op.id)
252
+
253
+ let group = groups.get(resource)
254
+ if (!group) {
255
+ const cmdVarBase = goLocal(resource)
256
+ group = {
257
+ cmdUse: toKebab(resource) || "resource",
258
+ cmdVarBase,
259
+ groupVar: pkg.claim(`${cmdVarBase}Cmd`),
260
+ commands: [],
261
+ fileName: resourceFileName(resource, fileNames),
262
+ resource,
346
263
  }
264
+ groups.set(resource, group)
265
+ }
347
266
 
348
- const segments = operationId.split(".")
349
- const resource = segments[0]
350
- /* Top-level ops (single segment) are conventionally marked `_call` in the
351
- * SDK IR. For the CLI tree we use "call" so the emitted subcommand is
352
- * `<resource> call` — cobra rejects leading dashes. */
353
- const actionSegments = segments.length === 1 ? ["call"] : segments.slice(1).map((seg) => toKebab(seg))
354
- const action = actionSegments.join("-")
355
- const cmdUse = action
356
-
357
- /* collision check: (resource, cmdUse) must be unique */
358
- const key = `${resource}|${cmdUse}`
359
- if (seenCmdPaths.has(key)) {
360
- throw new Error(
361
- `go-cli codegen: command path collision for ${resource} ${cmdUse} — operations: ${seenCmdPaths.get(key)}, ${operationId}`,
362
- )
267
+ const cmdVarName = pkg.claim(`${group.cmdVarBase}${goExported(cmdUse)}Cmd`)
268
+ const flagScope = new NameScope(GLOBAL_FLAGS)
269
+ const varFor = (flag: string) => pkg.claim(`${cmdVarName}${goExported(flag)}Var`)
270
+ const claimFlag = (name: string, prefix: string): string => {
271
+ const base = toFlagName(name)
272
+ return flagScope.has(base) ? flagScope.claim(`${prefix}-${base}`) : flagScope.claim(base)
273
+ }
274
+ const mkFlag = (p: IRParam, where: Flag["where"], prefix: string): Flag => {
275
+ const flagName = claimFlag(p.name, prefix)
276
+ return {
277
+ defaultValue: where === "query" || where === "header" ? p.default : undefined,
278
+ enumValues: enumOf(p.schema, resolve),
279
+ flagName,
280
+ kind: flagKind(p.schema, resolve),
281
+ name: p.name,
282
+ required: where === "path" || p.required === true,
283
+ varName: varFor(flagName),
284
+ where,
363
285
  }
364
- seenCmdPaths.set(key, operationId)
365
-
366
- let group = groups.get(resource)
367
- if (!group) {
368
- group = {
369
- cmdUse: toKebab(resource),
370
- cmdVarBase: safeResourceName(resource.charAt(0).toLowerCase() + resource.slice(1)),
371
- commands: [],
372
- fileName: `${resource}.go`,
373
- resource,
286
+ }
287
+
288
+ /* path flags claim first: a path param keeps its plain flag name */
289
+ const pathFlags = op.pathParams.map((p) => ({ ...mkFlag(p, "path", "path"), kind: "string" as const }))
290
+ const queryFlags: Flag[] = []
291
+ let lastEventIdVar: string | undefined
292
+ for (const q of op.query) {
293
+ if (op.stream === "sse" && /^last[-_]?event[-_]?id$/i.test(q.name)) continue
294
+ queryFlags.push(mkFlag(q, "query", "query"))
295
+ }
296
+ const headerFlags = op.headers.map((h) => mkFlag(h, "header", "header"))
297
+ if (op.stream === "sse") {
298
+ const flagName = flagScope.claim("last-event-id")
299
+ lastEventIdVar = varFor(flagName)
300
+ }
301
+
302
+ let body: BodyShape = { kind: "none" }
303
+ const b = op.body
304
+ if (b) {
305
+ const scalarFlags = (schema: IRSchema): Flag[] | null => {
306
+ const s = resolve(schema)
307
+ if (s.kind !== "object") return null
308
+ const out: Flag[] = []
309
+ for (const f of s.fields) {
310
+ if (!isScalarSchema(f.schema, resolve)) return null
311
+ out.push(mkFlag({ name: f.name, required: false, schema: f.schema }, "body", "body"))
374
312
  }
375
- groups.set(resource, group)
313
+ return out
376
314
  }
377
-
378
- const pathParams = getPathParams(op, rawPath)
379
- const queryParams = getQueryParams(op)
380
- const body = detectBodyShape(op)
381
- const sse = isSSEOperation(op)
382
- const hasLastEventId = queryParams.some((q) => isLastEventIdName(q.name))
383
-
384
- /* body rename: if a body scalar conflicts with a path param, rename flag to "body-<name>" */
385
- if (body.kind === "json-flat") {
386
- const pathNames = new Set(pathParams.map((p) => p.flagName))
387
- body.scalars = body.scalars.map((s) => {
388
- if (pathNames.has(s.flagName)) {
389
- return { ...s, flagName: `body-${s.flagName}`, varName: `body${goPascal(s.propName)}` }
315
+ if (b.kind === "json") {
316
+ const dataFlag = flagScope.claim("data")
317
+ const dataVar = varFor(dataFlag)
318
+ const scalars = scalarFlags(b.schema)
319
+ body = scalars
320
+ ? { dataVar, kind: "json-flat", required: b.required, scalars }
321
+ : { dataVar, kind: "json-complex", required: b.required }
322
+ } else if (b.kind === "form") {
323
+ body = { kind: "form", required: b.required, scalars: scalarFlags(b.schema) ?? [] }
324
+ } else if (b.kind === "multipart") {
325
+ const scalars: Flag[] = []
326
+ const files: Flag[] = []
327
+ const fileParts = b.parts.filter((p) => p.type === "file")
328
+ for (const part of b.parts) {
329
+ if (part.type === "file") {
330
+ const flagName = fileParts.length === 1 ? flagScope.claim("file") : claimFlag(part.name, "file")
331
+ files.push({
332
+ defaultValue: undefined,
333
+ enumValues: [],
334
+ flagName,
335
+ kind: "string",
336
+ name: part.name,
337
+ required: fileParts.length === 1 && b.required,
338
+ varName: varFor(flagName),
339
+ where: "file",
340
+ })
341
+ } else if (part.schema && isScalarSchema(part.schema, resolve)) {
342
+ scalars.push(mkFlag({ name: part.name, required: false, schema: part.schema }, "body", "body"))
390
343
  }
391
- return s
392
- })
393
- }
394
-
395
- /* query rename: cobra reserves --help, --version */
396
- for (const q of queryParams) {
397
- if (q.flagName === "help" || q.flagName === "version") {
398
- q.flagName = `query-${q.flagName}`
399
344
  }
345
+ body = { files, kind: "multipart", scalars }
346
+ } else {
347
+ const dataFlag = flagScope.claim("data")
348
+ body = { contentType: b.contentType, dataVar: varFor(dataFlag), kind: "raw", required: b.required }
400
349
  }
401
-
402
- const actionPascal = goPascal(action)
403
- const cmdVarName = `${group.cmdVarBase}${actionPascal}Cmd`
404
- const has204 = has204Response(op)
405
- const hasJson = hasJsonResponse(op)
406
- let responseType: "json" | "none" = "none"
407
- if (!sse && hasJson) responseType = "json"
408
-
409
- group.commands.push({
410
- action,
411
- body,
412
- cmdUse,
413
- cmdVarName,
414
- description: String(op.description ?? ""),
415
- has204,
416
- hasLastEventId,
417
- isSSE: sse,
418
- method: method.toUpperCase(),
419
- opId: operationId,
420
- path: rawPath,
421
- pathParams,
422
- queryParams,
423
- resource,
424
- responseType,
425
- summary: String(op.summary ?? ""),
426
- })
427
350
  }
351
+
352
+ group.commands.push({ body, cmdUse, cmdVarName, headerFlags, lastEventIdVar, op, pathFlags, queryFlags })
428
353
  }
429
354
 
430
- return { groups, serviceMap: serviceMap as Record<string, Record<string, unknown>>, skipped }
355
+ return { groups, skipped }
431
356
  }
432
357
 
433
358
  /* ── emit: cmd/root.go ── */
434
359
 
435
- function emitRootFile(groups: ResourceGroup[], options: GoCLIOptions): string {
360
+ function emitRootFile(
361
+ groups: ResourceGroup[],
362
+ options: GoCLIOptions,
363
+ auth: { headerName: string; prefix: string },
364
+ ): string {
436
365
  const envPrefix = options.envPrefix ?? toSnakeUpper(options.binaryName)
437
366
  const configName = options.configName ?? options.binaryName
438
367
  const defaultBase = options.defaultBaseURL ?? ""
@@ -444,24 +373,27 @@ function emitRootFile(groups: ResourceGroup[], options: GoCLIOptions): string {
444
373
  l.push(`import (`)
445
374
  l.push(`\t"context"`)
446
375
  l.push(`\t"fmt"`)
447
- l.push(`\t"os"`)
376
+ l.push(`\t"net/http"`)
377
+ l.push(`\t"time"`)
448
378
  l.push(``)
449
379
  l.push(`\t"github.com/spf13/cobra"`)
450
380
  l.push(``)
451
- l.push(`\tcli "${cliImportPath(options)}"`)
381
+ l.push(`\tcli ${goString(cliImportPath(options))}`)
452
382
  l.push(`)`)
453
383
  l.push(``)
454
- l.push(`var _ = fmt.Sprintf`)
455
- l.push(`var _ = os.Exit`)
456
- l.push(``)
457
384
  l.push(`// EnvPrefix is the prefix used for all <PREFIX>_<FLAG> environment variables.`)
458
- l.push(`const EnvPrefix = ${JSON.stringify(envPrefix)}`)
385
+ l.push(`const EnvPrefix = ${goString(envPrefix)}`)
459
386
  l.push(``)
460
387
  l.push(`// ConfigName is the directory name under $XDG_CONFIG_HOME / ~/.config for the config file.`)
461
- l.push(`const ConfigName = ${JSON.stringify(configName)}`)
388
+ l.push(`const ConfigName = ${goString(configName)}`)
462
389
  l.push(``)
463
390
  l.push(`// DefaultBaseURL is used when neither --base-url, env var, nor the config file provide one.`)
464
- l.push(`const DefaultBaseURL = ${JSON.stringify(defaultBase)}`)
391
+ l.push(`const DefaultBaseURL = ${goString(defaultBase)}`)
392
+ l.push(``)
393
+ l.push(`// AuthHeaderName and AuthHeaderPrefix come from the API's security scheme.`)
394
+ l.push(`const AuthHeaderName = ${goString(auth.headerName)}`)
395
+ l.push(``)
396
+ l.push(`const AuthHeaderPrefix = ${goString(auth.prefix)}`)
465
397
  l.push(``)
466
398
  l.push(`type configKey struct{}`)
467
399
  l.push(``)
@@ -470,13 +402,27 @@ function emitRootFile(groups: ResourceGroup[], options: GoCLIOptions): string {
470
402
  l.push(`\tCfg cli.Config`)
471
403
  l.push(`}`)
472
404
  l.push(``)
473
- l.push(`// rootCmd is the top-level cobra command for the ${options.binaryName} CLI.`)
405
+ l.push(`// started is set once flags, args and required flags are valid; any error`)
406
+ l.push(`// before that is a usage error (exit 4), whatever cobra calls it.`)
407
+ l.push(`var started bool`)
408
+ l.push(``)
409
+ l.push(`// rootCmd is the top-level cobra command for the CLI.`)
474
410
  l.push(`var rootCmd = &cobra.Command{`)
475
- l.push(`\tUse: ${JSON.stringify(options.binaryName)},`)
476
- l.push(`\tShort: ${JSON.stringify(options.description ?? `${options.binaryName} CLI`)},`)
411
+ l.push(`\tUse: ${goString(options.binaryName)},`)
412
+ l.push(`\tShort: ${goString(options.description ?? `${options.binaryName} CLI`)},`)
477
413
  l.push(`\tVersion: cli.Version,`)
478
414
  l.push(`\tSilenceUsage: true,`)
415
+ l.push(`\tSilenceErrors: true,`)
479
416
  l.push(`\tPersistentPreRunE: func(cmd *cobra.Command, args []string) error {`)
417
+ l.push(`\t\t/* cobra checks required flags only after this hook, so a missing one would`)
418
+ l.push(`\t\t surface as a generic error (exit 3): check them here, as usage errors */`)
419
+ l.push(`\t\tif err := cmd.ValidateRequiredFlags(); err != nil {`)
420
+ l.push(`\t\t\treturn cli.Usage(err)`)
421
+ l.push(`\t\t}`)
422
+ l.push(`\t\tif err := cmd.ValidateFlagGroups(); err != nil {`)
423
+ l.push(`\t\t\treturn cli.Usage(err)`)
424
+ l.push(`\t\t}`)
425
+ l.push(`\t\tstarted = true`)
480
426
  l.push(`\t\tcfg, err := cli.LoadConfig(cmd.Flags(), EnvPrefix, ConfigName)`)
481
427
  l.push(`\t\tif err != nil {`)
482
428
  l.push(`\t\t\treturn err`)
@@ -484,6 +430,9 @@ function emitRootFile(groups: ResourceGroup[], options: GoCLIOptions): string {
484
430
  l.push(`\t\tif cfg.BaseURL == "" {`)
485
431
  l.push(`\t\t\tcfg.BaseURL = DefaultBaseURL`)
486
432
  l.push(`\t\t}`)
433
+ l.push(`\t\tif _, err := time.ParseDuration(cfg.Timeout); err != nil {`)
434
+ l.push(`\t\t\treturn cli.Usage(err)`)
435
+ l.push(`\t\t}`)
487
436
  l.push(`\t\tctx := context.WithValue(cmd.Context(), configKey{}, runtimeConfig{Cfg: cfg})`)
488
437
  l.push(`\t\tcmd.SetContext(ctx)`)
489
438
  l.push(`\t\treturn nil`)
@@ -492,7 +441,20 @@ function emitRootFile(groups: ResourceGroup[], options: GoCLIOptions): string {
492
441
  l.push(``)
493
442
  l.push(`// Execute runs the root command.`)
494
443
  l.push(`func Execute() error {`)
495
- l.push(`\treturn rootCmd.Execute()`)
444
+ l.push(`\terr := rootCmd.Execute()`)
445
+ l.push(`\tif err != nil && !started {`)
446
+ l.push(`\t\treturn cli.Usage(err)`)
447
+ l.push(`\t}`)
448
+ l.push(`\treturn err`)
449
+ l.push(`}`)
450
+ l.push(``)
451
+ l.push(`// groupRun shows a bare group's help and rejects an unknown subcommand as a`)
452
+ l.push(`// usage error (cobra would print help and exit 0).`)
453
+ l.push(`func groupRun(cmd *cobra.Command, args []string) error {`)
454
+ l.push(`\tif len(args) > 0 {`)
455
+ l.push(`\t\treturn cli.Usage(fmt.Errorf("unknown command %q for %q", args[0], cmd.CommandPath()))`)
456
+ l.push(`\t}`)
457
+ l.push(`\treturn cmd.Help()`)
496
458
  l.push(`}`)
497
459
  l.push(``)
498
460
  l.push(`func configFromCtx(ctx context.Context) cli.Config {`)
@@ -502,17 +464,35 @@ function emitRootFile(groups: ResourceGroup[], options: GoCLIOptions): string {
502
464
  l.push(`\treturn cli.Config{}`)
503
465
  l.push(`}`)
504
466
  l.push(``)
467
+ l.push(`// newHTTPClient returns a client without an overall timeout: regular calls`)
468
+ l.push(`// bound themselves with --timeout, streams and uploads do not.`)
469
+ l.push(`func newHTTPClient() *http.Client {`)
470
+ l.push(`\treturn &http.Client{`)
471
+ l.push(`\t\tCheckRedirect: func(req *http.Request, via []*http.Request) error {`)
472
+ l.push(`\t\t\t/* never forward the API key to another host */`)
473
+ l.push(`\t\t\tif len(via) >= 10 || req.URL.Host != via[0].URL.Host || req.URL.Scheme != via[0].URL.Scheme {`)
474
+ l.push(`\t\t\t\treturn http.ErrUseLastResponse`)
475
+ l.push(`\t\t\t}`)
476
+ l.push(`\t\t\treturn nil`)
477
+ l.push(`\t\t},`)
478
+ l.push(`\t}`)
479
+ l.push(`}`)
480
+ l.push(``)
505
481
  l.push(`func init() {`)
506
- l.push(`\trootCmd.PersistentFlags().String("api-key", "", "API key (overrides env + config file)")`)
482
+ l.push(
483
+ `\trootCmd.PersistentFlags().String("api-key", "", "API key (overrides env + config file; visible to other local users in the process list, prefer the env var)")`,
484
+ )
507
485
  l.push(`\trootCmd.PersistentFlags().String("base-url", "", "Override API base URL")`)
508
486
  l.push(`\trootCmd.PersistentFlags().String("output", "json", "Output mode: json|ndjson|yaml|table")`)
509
487
  l.push(`\trootCmd.PersistentFlags().Bool("verbose", false, "Print request metadata to stderr")`)
510
488
  l.push(`\trootCmd.PersistentFlags().String("config", "", "Path to config file")`)
511
- l.push(`\trootCmd.PersistentFlags().String("timeout", "30s", "Request timeout (Go duration)")`)
512
- for (const g of groups) {
513
- const cmdVar = `${g.cmdVarBase}Cmd`
514
- l.push(`\trootCmd.AddCommand(${cmdVar})`)
515
- }
489
+ l.push(
490
+ `\trootCmd.PersistentFlags().String("timeout", "30s", "Request timeout (Go duration); streams are not bounded")`,
491
+ )
492
+ l.push(`\trootCmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error {`)
493
+ l.push(`\t\treturn cli.Usage(err)`)
494
+ l.push(`\t})`)
495
+ for (const g of groups) l.push(`\trootCmd.AddCommand(${g.groupVar})`)
516
496
  l.push(`}`)
517
497
  l.push(``)
518
498
  return l.join("\n")
@@ -530,114 +510,101 @@ function sdkImportPath(options: GoCLIOptions): string {
530
510
  /* ── emit: cmd/<resource>.go ── */
531
511
 
532
512
  function emitResourceFile(group: ResourceGroup, options: GoCLIOptions): string {
513
+ const body: string[] = []
514
+
515
+ const resShort = group.commands[0]?.op.summary || `${group.cmdUse} commands`
516
+ body.push(`var ${group.groupVar} = &cobra.Command{`)
517
+ body.push(`\tUse: ${goString(group.cmdUse)},`)
518
+ body.push(`\tShort: ${goString(firstLine(resShort))},`)
519
+ body.push(`\tArgs: cobra.ArbitraryArgs,`)
520
+ body.push(`\tRunE: groupRun,`)
521
+ body.push(`}`)
522
+ body.push(``)
523
+
524
+ for (const cmd of group.commands) emitCommand(body, cmd)
525
+
526
+ /* init() wires subcommands + registers their flags */
527
+ body.push(`func init() {`)
528
+ for (const cmd of group.commands) body.push(`\t${group.groupVar}.AddCommand(${cmd.cmdVarName})`)
529
+ for (const cmd of group.commands) emitFlagRegistrations(body, cmd)
530
+ body.push(`}`)
531
+ body.push(``)
532
+
533
+ const src = body.join("\n")
534
+ const imports: string[] = []
535
+ const uses = (re: RegExp) => re.test(src)
536
+ if (uses(/\bcontext\./)) imports.push("context")
537
+ if (uses(/\bbytes\./)) imports.push("bytes")
538
+ if (uses(/\bjson\./)) imports.push("encoding/json")
539
+ if (uses(/\berrors\./)) imports.push("errors")
540
+ if (uses(/\bfmt\./)) imports.push("fmt")
541
+ if (uses(/\bio\./)) imports.push("io")
542
+ if (uses(/\bhttp\./)) imports.push("net/http")
543
+ if (uses(/\burl\./)) imports.push("net/url")
544
+ if (uses(/\bos\./)) imports.push("os")
545
+ if (uses(/\bslices\./)) imports.push("slices")
546
+ if (uses(/\bstrconv\./)) imports.push("strconv")
547
+ if (uses(/\bstrings\./)) imports.push("strings")
548
+ if (uses(/\btime\./)) imports.push("time")
549
+
533
550
  const l: string[] = []
534
- const hasSSE = group.commands.some((c) => c.isSSE)
535
551
  l.push(`// Code generated by honey. DO NOT EDIT.`)
536
552
  l.push(`package cmd`)
537
553
  l.push(``)
538
554
  l.push(`import (`)
539
- l.push(`\t"bytes"`)
540
- l.push(`\t"context"`)
541
- l.push(`\t"encoding/json"`)
542
- l.push(`\t"fmt"`)
543
- l.push(`\t"io"`)
544
- if (hasSSE) l.push(`\t"iter"`)
545
- l.push(`\t"net/http"`)
546
- l.push(`\t"net/url"`)
547
- l.push(`\t"os"`)
548
- l.push(`\t"slices"`)
549
- l.push(`\t"strings"`)
550
- l.push(`\t"time"`)
555
+ for (const p of imports.sort(cmpCodeUnit)) l.push(`\t"${p}"`)
551
556
  l.push(``)
552
557
  l.push(`\t"github.com/spf13/cobra"`)
553
558
  l.push(``)
554
- l.push(`\tcli "${cliImportPath(options)}"`)
555
- l.push(`\tsdk "${sdkImportPath(options)}"`)
559
+ l.push(`\tcli ${goString(cliImportPath(options))}`)
560
+ if (uses(/\bsdk\./)) l.push(`\tsdk ${goString(sdkImportPath(options))}`)
556
561
  l.push(`)`)
557
562
  l.push(``)
558
- l.push(`var _ = bytes.NewReader`)
559
- l.push(`var _ = context.TODO`)
560
- l.push(`var _ = json.Marshal`)
561
- l.push(`var _ = fmt.Sprintf`)
562
- l.push(`var _ = io.Discard`)
563
- l.push(`var _ = http.MethodGet`)
564
- l.push(`var _ = url.PathEscape`)
565
- l.push(`var _ = os.Stdout`)
566
- l.push(`var _ = slices.Contains[[]string]`)
567
- l.push(`var _ = strings.NewReader`)
568
- l.push(`var _ = time.Second`)
569
- l.push(`var _ = sdk.SSEEvent{}`)
570
- l.push(`var _ = cli.Version`)
571
- if (hasSSE) l.push(`var _ iter.Seq2[cli.SSEEvent, error]`)
572
- l.push(``)
563
+ l.push(src)
564
+ return l.join("\n")
565
+ }
573
566
 
574
- /* resource-level var */
575
- const resShort = group.commands[0]?.summary ?? `${group.cmdUse} commands`
576
- l.push(`var ${group.cmdVarBase}Cmd = &cobra.Command{`)
577
- l.push(`\tUse: ${JSON.stringify(group.cmdUse)},`)
578
- l.push(`\tShort: ${JSON.stringify(resShort)},`)
579
- l.push(`}`)
580
- l.push(``)
567
+ function firstLine(s: string): string {
568
+ return s.split(/\r\n|\r|\n/)[0] ?? ""
569
+ }
581
570
 
582
- /* per-command vars */
583
- for (const cmd of group.commands) {
584
- emitCommand(l, group, cmd, options)
585
- }
571
+ function allFlags(cmd: CommandInfo): Flag[] {
572
+ const out = [...cmd.pathFlags, ...cmd.queryFlags, ...cmd.headerFlags]
573
+ if (cmd.body.kind === "json-flat" || cmd.body.kind === "form") out.push(...cmd.body.scalars)
574
+ if (cmd.body.kind === "multipart") out.push(...cmd.body.scalars, ...cmd.body.files)
575
+ return out
576
+ }
586
577
 
587
- /* init() wires subcommands + registers their flags */
588
- l.push(`func init() {`)
589
- for (const cmd of group.commands) {
590
- l.push(`\t${group.cmdVarBase}Cmd.AddCommand(${cmd.cmdVarName})`)
591
- }
592
- for (const cmd of group.commands) {
593
- emitFlagRegistrations(l, cmd)
578
+ function goVarType(kind: FlagKind): string {
579
+ switch (kind) {
580
+ case "int":
581
+ return "int64"
582
+ case "float":
583
+ return "float64"
584
+ case "bool":
585
+ return "bool"
586
+ case "strings":
587
+ return "[]string"
588
+ default:
589
+ return "string"
594
590
  }
595
- l.push(`}`)
596
- l.push(``)
597
-
598
- return l.join("\n")
599
591
  }
600
592
 
601
593
  /** Emits var declarations for every flag-backing Go variable + the cobra.Command literal. */
602
- function emitCommand(l: string[], group: ResourceGroup, cmd: CommandInfo, options: GoCLIOptions): void {
603
- /* flag-backing vars */
604
- for (const p of cmd.pathParams) {
605
- l.push(`var ${cmd.cmdVarName}${goPascal(p.name)}Var string`)
606
- }
607
- for (const q of cmd.queryParams) {
608
- l.push(`var ${cmd.cmdVarName}${goPascal(q.name)}Var ${goFlagVarType(q)}`)
609
- }
610
- if (cmd.body.kind === "json-flat" || cmd.body.kind === "json-complex") {
611
- l.push(`var ${cmd.cmdVarName}DataVar string`)
612
- }
613
- if (cmd.body.kind === "json-flat") {
614
- for (const s of cmd.body.scalars) {
615
- l.push(`var ${cmd.cmdVarName}${goPascal(s.propName)}Var ${goScalarVarType(s.schemaType)}`)
616
- }
617
- }
618
- if (cmd.body.kind === "multipart") {
619
- for (const s of cmd.body.scalarFields) {
620
- l.push(`var ${cmd.cmdVarName}${goPascal(s.propName)}Var ${goScalarVarType(s.schemaType)}`)
621
- }
622
- if (cmd.body.useGenericFileFlag) {
623
- l.push(`var ${cmd.cmdVarName}FileVar string`)
624
- } else {
625
- for (const f of cmd.body.binaryFields) {
626
- l.push(`var ${cmd.cmdVarName}${goPascal(f.name)}Var string`)
627
- }
628
- }
629
- }
630
- if (cmd.hasLastEventId) {
631
- l.push(`var ${cmd.cmdVarName}LastEventIDVar string`)
594
+ function emitCommand(l: string[], cmd: CommandInfo): void {
595
+ for (const f of allFlags(cmd)) l.push(`var ${f.varName} ${goVarType(f.kind)}`)
596
+ if (cmd.body.kind === "json-flat" || cmd.body.kind === "json-complex" || cmd.body.kind === "raw") {
597
+ l.push(`var ${cmd.body.dataVar} string`)
632
598
  }
599
+ if (cmd.lastEventIdVar) l.push(`var ${cmd.lastEventIdVar} string`)
633
600
  l.push(``)
634
601
 
635
- const shortDesc = cmd.summary || cmd.action
602
+ const shortDesc = firstLine(cmd.op.summary) || cmd.cmdUse
636
603
  l.push(`var ${cmd.cmdVarName} = &cobra.Command{`)
637
- l.push(`\tUse: ${JSON.stringify(cmd.cmdUse)},`)
638
- l.push(`\tShort: ${JSON.stringify(shortDesc)},`)
604
+ l.push(`\tUse: ${goString(cmd.cmdUse)},`)
605
+ l.push(`\tShort: ${goString(shortDesc)},`)
606
+ if (cmd.op.description) l.push(`\tLong: ${goString(cmd.op.description)},`)
639
607
 
640
- /* PreRunE: validate required body + enums */
641
608
  const preRunELines = emitPreRunE(cmd)
642
609
  if (preRunELines.length > 0) {
643
610
  l.push(`\tPreRunE: func(cmd *cobra.Command, args []string) error {`)
@@ -647,285 +614,251 @@ function emitCommand(l: string[], group: ResourceGroup, cmd: CommandInfo, option
647
614
  }
648
615
 
649
616
  l.push(`\tRunE: func(cmd *cobra.Command, args []string) error {`)
650
- emitRunE(l, cmd, options)
617
+ emitRunE(l, cmd)
651
618
  l.push(`\t},`)
652
619
  l.push(`}`)
653
620
  l.push(``)
654
621
  }
655
622
 
656
- function goFlagVarType(q: ParamInfo): string {
657
- const t = q.schema.type
658
- if (t === "integer") return "int64"
659
- if (t === "number") return "float64"
660
- if (t === "boolean") return "bool"
661
- if (t === "array") return "[]string"
662
- return "string"
663
- }
664
-
665
- function goScalarVarType(t: string): string {
666
- if (t === "integer") return "int64"
667
- if (t === "number") return "float64"
668
- if (t === "boolean") return "bool"
669
- return "string"
670
- }
671
-
672
623
  function emitFlagRegistrations(l: string[], cmd: CommandInfo): void {
673
- for (const p of cmd.pathParams) {
674
- const v = `${cmd.cmdVarName}${goPascal(p.name)}Var`
675
- /* flag docstring references original param name for discoverability */
676
- l.push(`\t/* Path flag — Use: ${JSON.stringify(p.name)} */`)
677
- l.push(
678
- `\t${cmd.cmdVarName}.Flags().StringVar(&${v}, ${JSON.stringify(p.flagName)}, "", ${JSON.stringify(`Path parameter ${p.name}`)})`,
679
- )
680
- l.push(`\t${cmd.cmdVarName}.MarkFlagRequired(${JSON.stringify(p.flagName)})`)
681
- }
682
- for (const q of cmd.queryParams) {
683
- if (isLastEventIdName(q.name)) continue
684
- emitQueryFlag(l, cmd, q)
685
- }
686
- if (cmd.body.kind === "json-flat" || cmd.body.kind === "json-complex") {
687
- l.push(
688
- `\t${cmd.cmdVarName}.Flags().StringVar(&${cmd.cmdVarName}DataVar, "data", "", "Request body: JSON literal, @file, or - for stdin")`,
689
- )
690
- }
691
- if (cmd.body.kind === "json-flat") {
692
- for (const s of cmd.body.scalars) {
693
- emitBodyScalarFlag(l, cmd, s)
694
- }
695
- }
696
- if (cmd.body.kind === "multipart") {
697
- for (const s of cmd.body.scalarFields) {
698
- emitBodyScalarFlag(l, cmd, s)
624
+ const c = cmd.cmdVarName
625
+ for (const f of allFlags(cmd)) {
626
+ let help: string
627
+ switch (f.where) {
628
+ case "path":
629
+ help = `Path parameter ${f.name}`
630
+ break
631
+ case "query":
632
+ help = `Query param ${f.name}`
633
+ break
634
+ case "header":
635
+ help = `Header ${f.name}`
636
+ break
637
+ case "file":
638
+ help = `Path to the ${f.name} file to upload`
639
+ break
640
+ default:
641
+ help = `Body field ${f.name}`
699
642
  }
700
- if (cmd.body.useGenericFileFlag) {
701
- l.push(`\t${cmd.cmdVarName}.Flags().StringVar(&${cmd.cmdVarName}FileVar, "file", "", "Path to file to upload")`)
702
- const bin = cmd.body.binaryFields[0]
703
- if (bin?.required) {
704
- l.push(`\t${cmd.cmdVarName}.MarkFlagRequired("file")`)
705
- }
706
- } else {
707
- for (const f of cmd.body.binaryFields) {
708
- const v = `${cmd.cmdVarName}${goPascal(f.name)}Var`
709
- l.push(
710
- `\t${cmd.cmdVarName}.Flags().StringVar(&${v}, ${JSON.stringify(toFlagName(f.name))}, "", ${JSON.stringify(`Path to ${f.name} file`)})`,
711
- )
712
- if (f.required) {
713
- l.push(`\t${cmd.cmdVarName}.MarkFlagRequired(${JSON.stringify(toFlagName(f.name))})`)
714
- }
715
- }
643
+ if (f.enumValues.length > 0) help += ` (one of: ${f.enumValues.join(", ")})`
644
+ const h = goString(help)
645
+ const n = goString(f.flagName)
646
+ const def = flagDefault(f)
647
+ switch (f.kind) {
648
+ case "int":
649
+ l.push(`\t${c}.Flags().Int64Var(&${f.varName}, ${n}, ${def}, ${h})`)
650
+ break
651
+ case "float":
652
+ l.push(`\t${c}.Flags().Float64Var(&${f.varName}, ${n}, ${def}, ${h})`)
653
+ break
654
+ case "bool":
655
+ l.push(`\t${c}.Flags().BoolVar(&${f.varName}, ${n}, ${def}, ${h})`)
656
+ break
657
+ case "strings":
658
+ l.push(`\t${c}.Flags().StringSliceVar(&${f.varName}, ${n}, ${def}, ${h})`)
659
+ break
660
+ default:
661
+ l.push(`\t${c}.Flags().StringVar(&${f.varName}, ${n}, ${def}, ${h})`)
716
662
  }
663
+ if (f.required) l.push(`\t${c}.MarkFlagRequired(${n})`)
717
664
  }
718
- if (cmd.hasLastEventId) {
719
- l.push(
720
- `\t${cmd.cmdVarName}.Flags().StringVar(&${cmd.cmdVarName}LastEventIDVar, "last-event-id", "", "SSE Last-Event-ID resume token")`,
721
- )
665
+ if (cmd.body.kind === "json-flat" || cmd.body.kind === "json-complex" || cmd.body.kind === "raw") {
666
+ l.push(`\t${c}.Flags().StringVar(&${cmd.body.dataVar}, "data", "", "Request body: literal, @file, or - for stdin")`)
722
667
  }
723
- }
724
-
725
- function emitQueryFlag(l: string[], cmd: CommandInfo, q: ParamInfo): void {
726
- const v = `${cmd.cmdVarName}${goPascal(q.name)}Var`
727
- const t = q.schema.type
728
- const help =
729
- q.enumValues.length > 0 ? `Query param ${q.name} (one of: ${q.enumValues.join(", ")})` : `Query param ${q.name}`
730
- const helpStr = JSON.stringify(help)
731
-
732
- if (t === "integer") {
733
- const def = typeof q.defaultValue === "number" ? q.defaultValue : 0
734
- l.push(`\t${cmd.cmdVarName}.Flags().Int64Var(&${v}, ${JSON.stringify(q.flagName)}, ${def}, ${helpStr})`)
735
- } else if (t === "number") {
736
- const def = typeof q.defaultValue === "number" ? q.defaultValue : 0
737
- l.push(`\t${cmd.cmdVarName}.Flags().Float64Var(&${v}, ${JSON.stringify(q.flagName)}, ${def}, ${helpStr})`)
738
- } else if (t === "boolean") {
739
- const def = q.defaultValue === true
740
- l.push(
741
- `\t${cmd.cmdVarName}.Flags().BoolVar(&${v}, ${JSON.stringify(q.flagName)}, ${def ? "true" : "false"}, ${helpStr})`,
742
- )
743
- } else if (t === "array") {
744
- const defArr = Array.isArray(q.defaultValue) ? q.defaultValue : []
745
- const defStr = `[]string{${defArr.map((x) => JSON.stringify(String(x))).join(", ")}}`
746
- l.push(`\t${cmd.cmdVarName}.Flags().StringSliceVar(&${v}, ${JSON.stringify(q.flagName)}, ${defStr}, ${helpStr})`)
747
- } else {
748
- const def = typeof q.defaultValue === "string" ? q.defaultValue : ""
749
- l.push(
750
- `\t${cmd.cmdVarName}.Flags().StringVar(&${v}, ${JSON.stringify(q.flagName)}, ${JSON.stringify(def)}, ${helpStr})`,
751
- )
752
- }
753
- if (q.required) {
754
- l.push(`\t${cmd.cmdVarName}.MarkFlagRequired(${JSON.stringify(q.flagName)})`)
755
- }
756
- }
757
-
758
- function emitBodyScalarFlag(l: string[], cmd: CommandInfo, s: BodyScalarInfo): void {
759
- const v = `${cmd.cmdVarName}${goPascal(s.propName)}Var`
760
- const help =
761
- s.enumValues.length > 0
762
- ? `Body field ${s.propName} (one of: ${s.enumValues.join(", ")})`
763
- : `Body field ${s.propName}`
764
- const helpStr = JSON.stringify(help)
765
- if (s.schemaType === "integer") {
766
- l.push(`\t${cmd.cmdVarName}.Flags().Int64Var(&${v}, ${JSON.stringify(s.flagName)}, 0, ${helpStr})`)
767
- } else if (s.schemaType === "number") {
768
- l.push(`\t${cmd.cmdVarName}.Flags().Float64Var(&${v}, ${JSON.stringify(s.flagName)}, 0, ${helpStr})`)
769
- } else if (s.schemaType === "boolean") {
770
- l.push(`\t${cmd.cmdVarName}.Flags().BoolVar(&${v}, ${JSON.stringify(s.flagName)}, false, ${helpStr})`)
771
- } else {
772
- l.push(`\t${cmd.cmdVarName}.Flags().StringVar(&${v}, ${JSON.stringify(s.flagName)}, "", ${helpStr})`)
668
+ if (cmd.lastEventIdVar) {
669
+ l.push(`\t${c}.Flags().StringVar(&${cmd.lastEventIdVar}, "last-event-id", "", "SSE Last-Event-ID resume token")`)
773
670
  }
774
671
  }
775
672
 
776
673
  function emitPreRunE(cmd: CommandInfo): string[] {
777
674
  const lines: string[] = []
778
- /* required body on POST/PUT/PATCH */
779
675
  if (
780
- (cmd.method === "POST" || cmd.method === "PUT" || cmd.method === "PATCH") &&
781
- (cmd.body.kind === "json-flat" || cmd.body.kind === "json-complex") &&
676
+ (cmd.body.kind === "json-flat" || cmd.body.kind === "json-complex" || cmd.body.kind === "raw") &&
782
677
  cmd.body.required
783
678
  ) {
784
- const dataVar = `${cmd.cmdVarName}DataVar`
785
679
  const anyFlag =
786
680
  cmd.body.kind === "json-flat" && cmd.body.scalars.length > 0
787
- ? cmd.body.scalars.map((s) => `cmd.Flags().Changed(${JSON.stringify(s.flagName)})`).join(" || ")
681
+ ? cmd.body.scalars.map((s) => `cmd.Flags().Changed(${goString(s.flagName)})`).join(" || ")
788
682
  : "false"
789
- lines.push(`if ${dataVar} == "" && !(${anyFlag}) {`)
790
- lines.push(`\treturn fmt.Errorf("body required: pass --data or per-field flags")`)
683
+ lines.push(`if ${cmd.body.dataVar} == "" && !(${anyFlag}) {`)
684
+ lines.push(`\treturn cli.Usage(errors.New("body required: pass --data or per-field flags"))`)
791
685
  lines.push(`}`)
792
686
  }
793
- /* enum validation: query + body-flat */
794
- for (const q of cmd.queryParams) {
795
- if (q.enumValues.length === 0) continue
796
- if (q.schema.type !== "string") continue
797
- const v = `${cmd.cmdVarName}${goPascal(q.name)}Var`
798
- const values = q.enumValues.map((x) => JSON.stringify(x)).join(", ")
799
- lines.push(`if cmd.Flags().Changed(${JSON.stringify(q.flagName)}) && !slices.Contains([]string{${values}}, ${v}) {`)
800
- lines.push(`\treturn fmt.Errorf("--${q.flagName} must be one of: ${q.enumValues.join(", ")}")`)
687
+ for (const f of allFlags(cmd)) {
688
+ if (f.enumValues.length === 0 || f.kind !== "string") continue
689
+ const values = f.enumValues.map(goString).join(", ")
690
+ const msg = goString(`--${f.flagName} must be one of: ${f.enumValues.join(", ")}`)
691
+ lines.push(
692
+ `if cmd.Flags().Changed(${goString(f.flagName)}) && !slices.Contains([]string{${values}}, ${f.varName}) {`,
693
+ )
694
+ lines.push(`\treturn cli.Usage(errors.New(${msg}))`)
801
695
  lines.push(`}`)
802
696
  }
803
- if (cmd.body.kind === "json-flat") {
804
- for (const s of cmd.body.scalars) {
805
- if (s.enumValues.length === 0) continue
806
- if (s.schemaType !== "string") continue
807
- const v = `${cmd.cmdVarName}${goPascal(s.propName)}Var`
808
- const values = s.enumValues.map((x) => JSON.stringify(x)).join(", ")
809
- lines.push(
810
- `if cmd.Flags().Changed(${JSON.stringify(s.flagName)}) && !slices.Contains([]string{${values}}, ${v}) {`,
811
- )
812
- lines.push(`\treturn fmt.Errorf("--${s.flagName} must be one of: ${s.enumValues.join(", ")}")`)
813
- lines.push(`}`)
814
- }
815
- }
816
697
  return lines
817
698
  }
818
699
 
819
- function emitRunE(l: string[], cmd: CommandInfo, _options: GoCLIOptions): void {
700
+ /** Go expression rendering a flag variable as a wire string. */
701
+ function scalarString(f: Flag, expr = f.varName): string {
702
+ switch (f.kind) {
703
+ case "int":
704
+ return `strconv.FormatInt(${expr}, 10)`
705
+ case "float":
706
+ return `strconv.FormatFloat(${expr}, 'g', -1, 64)`
707
+ case "bool":
708
+ return `strconv.FormatBool(${expr})`
709
+ default:
710
+ return expr
711
+ }
712
+ }
713
+
714
+ function emitRunE(l: string[], cmd: CommandInfo): void {
820
715
  const ind = "\t\t"
716
+ const { op } = cmd
821
717
  l.push(`${ind}ctx := cmd.Context()`)
822
718
  l.push(`${ind}cfg := configFromCtx(ctx)`)
823
719
  l.push(`${ind}apiKey, err := cli.ResolveAPIKey(cfg)`)
824
- l.push(`${ind}if err != nil { return err }`)
825
- l.push(`${ind}_ = apiKey`)
826
- l.push(`${ind}baseURL := cfg.BaseURL`)
827
- l.push(`${ind}if baseURL == "" { return fmt.Errorf("base URL required: set --base-url, env, or config") }`)
828
- l.push(`${ind}timeout := 30 * time.Second`)
829
- l.push(`${ind}if cfg.Timeout != "" {`)
830
- l.push(`${ind}\tif d, perr := time.ParseDuration(cfg.Timeout); perr == nil { timeout = d }`)
720
+ l.push(`${ind}if err != nil {`)
721
+ l.push(`${ind}\treturn err`)
722
+ l.push(`${ind}}`)
723
+ l.push(`${ind}if cfg.BaseURL == "" {`)
724
+ l.push(`${ind}\treturn cli.Usage(errors.New("base URL required: set --base-url, env, or config"))`)
831
725
  l.push(`${ind}}`)
832
- l.push(`${ind}httpClient := &http.Client{Timeout: timeout}`)
833
726
 
834
- /* path-param local aliases: short Go-idiomatic names used in URL assembly */
835
- for (const p of cmd.pathParams) {
836
- const storageVar = `${cmd.cmdVarName}${goPascal(p.name)}Var`
837
- l.push(`${ind}${p.varName} := ${storageVar}`)
727
+ /* path */
728
+ if (cmd.pathFlags.length > 0) {
729
+ const params = cmd.pathFlags.map((f) => `${goString(f.name)}: ${f.varName}`).join(", ")
730
+ l.push(`${ind}path, err := sdk.HoneyExpandPath(${goString(op.path)}, map[string]string{${params}})`)
731
+ l.push(`${ind}if err != nil {`)
732
+ l.push(`${ind}\treturn cli.Usage(err)`)
733
+ l.push(`${ind}}`)
734
+ } else {
735
+ l.push(`${ind}path := ${goString(op.path)}`)
838
736
  }
839
737
 
840
- /* URL assembly */
841
- const pathExpr = buildPathExpr(cmd)
842
- l.push(`${ind}reqURL := baseURL + ${pathExpr}`)
843
- l.push(`${ind}q := url.Values{}`)
844
- for (const q of cmd.queryParams) {
845
- if (isLastEventIdName(q.name)) continue
846
- emitQueryAssembly(l, ind, cmd, q)
738
+ /* query: a required flag is always sent, an optional one only when set */
739
+ l.push(`${ind}var query [][2]string`)
740
+ for (const f of cmd.queryFlags) {
741
+ const guard = f.required ? "" : `if cmd.Flags().Changed(${goString(f.flagName)}) `
742
+ if (f.kind === "strings") {
743
+ l.push(`${ind}${guard}{`)
744
+ l.push(`${ind}\tfor _, v := range ${f.varName} {`)
745
+ l.push(`${ind}\t\tquery = append(query, [2]string{${goString(f.name)}, v})`)
746
+ l.push(`${ind}\t}`)
747
+ l.push(`${ind}}`)
748
+ } else {
749
+ l.push(`${ind}${guard}{`)
750
+ l.push(`${ind}\tquery = append(query, [2]string{${goString(f.name)}, ${scalarString(f)}})`)
751
+ l.push(`${ind}}`)
752
+ }
847
753
  }
848
- l.push(`${ind}if len(q) > 0 { reqURL += "?" + q.Encode() }`)
754
+ l.push(`${ind}reqURL, err := sdk.HoneyBuildURL(cfg.BaseURL, path, query)`)
755
+ l.push(`${ind}if err != nil {`)
756
+ l.push(`${ind}\treturn cli.Usage(err)`)
757
+ l.push(`${ind}}`)
849
758
 
850
- /* body prep */
851
- let bodyReaderExpr = "nil"
852
- let contentType = ""
853
- if (cmd.body.kind === "json-flat") {
854
- l.push(`${ind}// merge --data + per-prop flags`)
759
+ /* body */
760
+ let bodyExpr = "nil"
761
+ let contentTypeExpr = ""
762
+ const b = cmd.body
763
+ if (b.kind === "json-flat") {
855
764
  l.push(`${ind}merged := map[string]any{}`)
856
- l.push(`${ind}if ${cmd.cmdVarName}DataVar != "" {`)
857
- l.push(`${ind}\tdataBytes, derr := cli.ReadDataFlag(${cmd.cmdVarName}DataVar)`)
858
- l.push(`${ind}\tif derr != nil { return derr }`)
765
+ l.push(`${ind}if ${b.dataVar} != "" {`)
766
+ l.push(`${ind}\tdataBytes, derr := cli.ReadDataFlag(${b.dataVar})`)
767
+ l.push(`${ind}\tif derr != nil {`)
768
+ l.push(`${ind}\t\treturn cli.Usage(derr)`)
769
+ l.push(`${ind}\t}`)
859
770
  l.push(`${ind}\tif len(dataBytes) > 0 {`)
860
- l.push(
861
- `${ind}\t\tif jerr := json.Unmarshal(dataBytes, &merged); jerr != nil { return fmt.Errorf("parse --data: %w", jerr) }`,
862
- )
771
+ l.push(`${ind}\t\tif jerr := json.Unmarshal(dataBytes, &merged); jerr != nil {`)
772
+ l.push(`${ind}\t\t\treturn cli.Usage(fmt.Errorf("parse --data: %w", jerr))`)
773
+ l.push(`${ind}\t\t}`)
863
774
  l.push(`${ind}\t}`)
864
775
  l.push(`${ind}}`)
865
- for (const s of cmd.body.scalars) {
866
- const v = `${cmd.cmdVarName}${goPascal(s.propName)}Var`
867
- l.push(
868
- `${ind}if cmd.Flags().Changed(${JSON.stringify(s.flagName)}) { merged[${JSON.stringify(s.propName)}] = ${v} }`,
869
- )
776
+ for (const s of b.scalars) {
777
+ l.push(`${ind}if cmd.Flags().Changed(${goString(s.flagName)}) {`)
778
+ l.push(`${ind}\tmerged[${goString(s.name)}] = ${s.varName}`)
779
+ l.push(`${ind}}`)
870
780
  }
871
- l.push(`${ind}bodyBytes, merr := json.Marshal(merged)`)
872
- l.push(`${ind}if merr != nil { return merr }`)
873
- bodyReaderExpr = "bytes.NewReader(bodyBytes)"
874
- contentType = "application/json"
875
- } else if (cmd.body.kind === "json-complex") {
876
- l.push(`${ind}bodyBytes, derr := cli.ReadDataFlag(${cmd.cmdVarName}DataVar)`)
877
- l.push(`${ind}if derr != nil { return derr }`)
878
- bodyReaderExpr = "bytes.NewReader(bodyBytes)"
879
- contentType = "application/json"
880
- } else if (cmd.body.kind === "multipart") {
781
+ l.push(`${ind}bodyBytes, err := json.Marshal(merged)`)
782
+ l.push(`${ind}if err != nil {`)
783
+ l.push(`${ind}\treturn err`)
784
+ l.push(`${ind}}`)
785
+ bodyExpr = "bytes.NewReader(bodyBytes)"
786
+ contentTypeExpr = goString(op.body?.contentType ?? "application/json")
787
+ } else if (b.kind === "json-complex" || b.kind === "raw") {
788
+ l.push(`${ind}bodyBytes, err := cli.ReadDataFlag(${b.dataVar})`)
789
+ l.push(`${ind}if err != nil {`)
790
+ l.push(`${ind}\treturn cli.Usage(err)`)
791
+ l.push(`${ind}}`)
792
+ bodyExpr = "bytes.NewReader(bodyBytes)"
793
+ contentTypeExpr = goString(op.body?.contentType ?? "application/json")
794
+ } else if (b.kind === "form") {
795
+ l.push(`${ind}form := url.Values{}`)
796
+ for (const s of b.scalars) {
797
+ l.push(`${ind}if cmd.Flags().Changed(${goString(s.flagName)}) {`)
798
+ l.push(`${ind}\tform.Set(${goString(s.name)}, ${scalarString(s)})`)
799
+ l.push(`${ind}}`)
800
+ }
801
+ bodyExpr = "strings.NewReader(form.Encode())"
802
+ contentTypeExpr = `"application/x-www-form-urlencoded"`
803
+ } else if (b.kind === "multipart") {
881
804
  l.push(`${ind}fields := map[string]string{}`)
882
- for (const s of cmd.body.scalarFields) {
883
- const v = `${cmd.cmdVarName}${goPascal(s.propName)}Var`
884
- if (s.schemaType === "string") {
885
- l.push(
886
- `${ind}if cmd.Flags().Changed(${JSON.stringify(s.flagName)}) { fields[${JSON.stringify(s.propName)}] = ${v} }`,
887
- )
888
- } else {
889
- l.push(
890
- `${ind}if cmd.Flags().Changed(${JSON.stringify(s.flagName)}) { fields[${JSON.stringify(s.propName)}] = fmt.Sprint(${v}) }`,
891
- )
892
- }
805
+ for (const s of b.scalars) {
806
+ l.push(`${ind}if cmd.Flags().Changed(${goString(s.flagName)}) {`)
807
+ l.push(`${ind}\tfields[${goString(s.name)}] = ${scalarString(s)}`)
808
+ l.push(`${ind}}`)
893
809
  }
894
810
  l.push(`${ind}files := map[string]string{}`)
895
- if (cmd.body.useGenericFileFlag) {
896
- const bin = cmd.body.binaryFields[0]
897
- l.push(
898
- `${ind}if ${cmd.cmdVarName}FileVar != "" { files[${JSON.stringify(bin.name)}] = ${cmd.cmdVarName}FileVar }`,
899
- )
900
- } else {
901
- for (const f of cmd.body.binaryFields) {
902
- const v = `${cmd.cmdVarName}${goPascal(f.name)}Var`
903
- l.push(`${ind}if ${v} != "" { files[${JSON.stringify(f.name)}] = ${v} }`)
904
- }
811
+ for (const f of b.files) {
812
+ l.push(`${ind}if ${f.varName} != "" {`)
813
+ l.push(`${ind}\tfiles[${goString(f.name)}] = ${f.varName}`)
814
+ l.push(`${ind}}`)
905
815
  }
906
- l.push(`${ind}mpReader, mpCT, mpErr := cli.BuildMultipart(fields, files)`)
907
- l.push(`${ind}if mpErr != nil { return mpErr }`)
908
- l.push(`${ind}_ = mpCT`)
909
- bodyReaderExpr = "mpReader"
910
- contentType = "__multipart__"
816
+ l.push(`${ind}mpReader, mpCT, err := cli.BuildMultipart(fields, files)`)
817
+ l.push(`${ind}if err != nil {`)
818
+ l.push(`${ind}\treturn err`)
819
+ l.push(`${ind}}`)
820
+ bodyExpr = "mpReader"
821
+ contentTypeExpr = "mpCT"
822
+ }
823
+
824
+ /* regular calls get --timeout; streams and uploads only stop on ^C */
825
+ const bounded = op.stream === null && b.kind !== "multipart"
826
+ if (bounded) {
827
+ l.push(`${ind}timeout, err := time.ParseDuration(cfg.Timeout)`)
828
+ l.push(`${ind}if err != nil {`)
829
+ l.push(`${ind}\treturn cli.Usage(err)`)
830
+ l.push(`${ind}}`)
831
+ l.push(`${ind}ctx, cancel := context.WithTimeout(ctx, timeout)`)
832
+ l.push(`${ind}defer cancel()`)
833
+ }
834
+
835
+ l.push(`${ind}req, err := http.NewRequestWithContext(ctx, ${goString(op.method)}, reqURL, ${bodyExpr})`)
836
+ l.push(`${ind}if err != nil {`)
837
+ l.push(`${ind}\treturn err`)
838
+ l.push(`${ind}}`)
839
+ if (contentTypeExpr) l.push(`${ind}req.Header.Set("Content-Type", ${contentTypeExpr})`)
840
+ for (const f of cmd.headerFlags) {
841
+ const guard = f.required ? "" : `if cmd.Flags().Changed(${goString(f.flagName)}) `
842
+ const value = f.kind === "strings" ? `strings.Join(${f.varName}, ",")` : scalarString(f)
843
+ l.push(`${ind}${guard}{`)
844
+ l.push(`${ind}\treq.Header.Set(${goString(f.name)}, ${value})`)
845
+ l.push(`${ind}}`)
911
846
  }
847
+ l.push(`${ind}cli.ApplyAuth(req.Header, AuthHeaderName, AuthHeaderPrefix, apiKey)`)
912
848
 
913
- /* SSE */
914
- if (cmd.isSSE) {
915
- l.push(`${ind}req, rerr := http.NewRequestWithContext(ctx, "GET", reqURL, nil)`)
916
- l.push(`${ind}if rerr != nil { return rerr }`)
849
+ if (op.stream === "sse") {
917
850
  l.push(`${ind}req.Header.Set("Accept", "text/event-stream")`)
918
- l.push(`${ind}if apiKey != "" { req.Header.Set("Authorization", "Bearer "+apiKey) }`)
919
- if (cmd.hasLastEventId) {
920
- l.push(
921
- `${ind}if ${cmd.cmdVarName}LastEventIDVar != "" { req.Header.Set("Last-Event-ID", ${cmd.cmdVarName}LastEventIDVar) }`,
922
- )
851
+ if (cmd.lastEventIdVar) {
852
+ l.push(`${ind}if ${cmd.lastEventIdVar} != "" {`)
853
+ l.push(`${ind}\treq.Header.Set("Last-Event-ID", ${cmd.lastEventIdVar})`)
854
+ l.push(`${ind}}`)
923
855
  }
924
- l.push(`${ind}resp, herr := httpClient.Do(req)`)
925
- l.push(`${ind}if herr != nil { return herr }`)
926
- l.push(`${ind}defer resp.Body.Close()`)
927
- l.push(`${ind}it := sdk.ParseSSEStreamExported(ctx, resp)`)
856
+ l.push(`${ind}resp, err := newHTTPClient().Do(req)`)
857
+ l.push(`${ind}if err != nil {`)
858
+ l.push(`${ind}\treturn err`)
859
+ l.push(`${ind}}`)
928
860
  /* wrap SDK's Seq2[sdk.SSEEvent] → Seq2[cli.SSEEvent] so the runtime stays SDK-agnostic. */
861
+ l.push(`${ind}it := sdk.HoneyParseSSEStream(ctx, resp)`)
929
862
  l.push(`${ind}wrapped := func(yield func(cli.SSEEvent, error) bool) {`)
930
863
  l.push(`${ind}\tfor ev, iterErr := range it {`)
931
864
  l.push(`${ind}\t\tif !yield(cli.SSEEvent{Data: ev.Data, Event: ev.Event, ID: ev.ID, Retry: ev.Retry}, iterErr) {`)
@@ -937,75 +870,33 @@ function emitRunE(l: string[], cmd: CommandInfo, _options: GoCLIOptions): void {
937
870
  return
938
871
  }
939
872
 
940
- /* regular HTTP */
941
- l.push(`${ind}req, rerr := http.NewRequestWithContext(ctx, "${cmd.method}", reqURL, ${bodyReaderExpr})`)
942
- l.push(`${ind}if rerr != nil { return rerr }`)
943
- if (contentType === "application/json") {
944
- l.push(`${ind}req.Header.Set("Content-Type", "application/json")`)
945
- } else if (contentType === "__multipart__") {
946
- l.push(`${ind}req.Header.Set("Content-Type", mpCT) // multipart/form-data; boundary=...`)
947
- }
948
- l.push(`${ind}if apiKey != "" { req.Header.Set("Authorization", "Bearer "+apiKey) }`)
949
- l.push(`${ind}resp, herr := httpClient.Do(req)`)
950
- l.push(`${ind}if herr != nil { return herr }`)
873
+ l.push(`${ind}resp, err := newHTTPClient().Do(req)`)
874
+ l.push(`${ind}if err != nil {`)
875
+ l.push(`${ind}\treturn err`)
876
+ l.push(`${ind}}`)
951
877
  l.push(`${ind}defer resp.Body.Close()`)
952
- l.push(`${ind}body, berr := io.ReadAll(resp.Body)`)
953
- l.push(`${ind}if berr != nil { return berr }`)
954
- l.push(`${ind}if resp.StatusCode >= 400 {`)
955
- l.push(
956
- `${ind}\treturn &sdk.StatusError{StatusCode: resp.StatusCode, Body: body, Response: resp, Message: string(body)}`,
957
- )
878
+ l.push(`${ind}body, err := io.ReadAll(resp.Body)`)
879
+ l.push(`${ind}if err != nil {`)
880
+ l.push(`${ind}\treturn err`)
958
881
  l.push(`${ind}}`)
959
- if (cmd.has204) {
960
- l.push(`${ind}if resp.StatusCode == http.StatusNoContent { return nil }`)
961
- }
962
- if (cmd.responseType === "json") {
963
- l.push(`${ind}var out any`)
964
- l.push(`${ind}if len(body) > 0 {`)
965
- l.push(`${ind}\tif jerr := json.Unmarshal(body, &out); jerr != nil { return jerr }`)
882
+ l.push(`${ind}if err := sdk.HoneyRaiseForStatus(resp, body); err != nil {`)
883
+ l.push(`${ind}\treturn err`)
884
+ l.push(`${ind}}`)
885
+ const kind = op.success.kind
886
+ if (kind === "json") {
887
+ l.push(`${ind}out, err := cli.DecodeJSON(body)`)
888
+ l.push(`${ind}if err != nil {`)
889
+ l.push(`${ind}\treturn err`)
966
890
  l.push(`${ind}}`)
967
891
  l.push(`${ind}return cli.Emit(out, cfg.Output, os.Stdout)`)
892
+ } else if (kind === "text" || kind === "binary") {
893
+ l.push(`${ind}_, err = os.Stdout.Write(body)`)
894
+ l.push(`${ind}return err`)
968
895
  } else {
969
896
  l.push(`${ind}return nil`)
970
897
  }
971
898
  }
972
899
 
973
- function buildPathExpr(cmd: CommandInfo): string {
974
- if (cmd.pathParams.length === 0) return JSON.stringify(cmd.path)
975
- let expr = JSON.stringify(cmd.path)
976
- for (const p of cmd.pathParams) {
977
- expr = expr.replace(`{${p.name}}`, `" + url.PathEscape(${p.varName}) + "`)
978
- }
979
- expr = expr.replace(/"" \+ /g, "").replace(/ \+ ""/g, "")
980
- return expr
981
- }
982
-
983
- function querySetStmt(q: ParamInfo, v: string): string {
984
- const name = JSON.stringify(q.name)
985
- switch (q.schema.type) {
986
- case "integer":
987
- return `q.Set(${name}, fmt.Sprintf("%d", ${v}))`
988
- case "number":
989
- return `q.Set(${name}, fmt.Sprintf("%g", ${v}))`
990
- case "boolean":
991
- return `q.Set(${name}, fmt.Sprintf("%t", ${v}))`
992
- case "array":
993
- return `for _, v := range ${v} { q.Add(${name}, v) }`
994
- default:
995
- return `q.Set(${name}, ${v})`
996
- }
997
- }
998
-
999
- function emitQueryAssembly(l: string[], ind: string, cmd: CommandInfo, q: ParamInfo): void {
1000
- const v = `${cmd.cmdVarName}${goPascal(q.name)}Var`
1001
- const stmt = querySetStmt(q, v)
1002
- if (q.required) {
1003
- l.push(`${ind}${stmt}`)
1004
- return
1005
- }
1006
- l.push(`${ind}if cmd.Flags().Changed(${JSON.stringify(q.flagName)}) { ${stmt} }`)
1007
- }
1008
-
1009
900
  /* ── emit: main.go ── */
1010
901
 
1011
902
  function emitMainFile(options: GoCLIOptions): string {
@@ -1018,13 +909,13 @@ function emitMainFile(options: GoCLIOptions): string {
1018
909
  l.push(`\t"fmt"`)
1019
910
  l.push(`\t"os"`)
1020
911
  l.push(``)
1021
- l.push(`\tcli "${module}/internal/cli"`)
1022
- l.push(`\t"${module}/cmd"`)
912
+ l.push(`\tcli ${goString(`${module}/internal/cli`)}`)
913
+ l.push(`\t${goString(`${module}/cmd`)}`)
1023
914
  l.push(`)`)
1024
915
  l.push(``)
1025
916
  l.push(`func main() {`)
1026
917
  l.push(`\tif err := cmd.Execute(); err != nil {`)
1027
- l.push(`\t\tfmt.Fprintln(os.Stderr, err)`)
918
+ l.push(`\t\tfmt.Fprintln(os.Stderr, cli.Sanitize(err.Error()))`)
1028
919
  l.push(`\t\tos.Exit(cli.ExitFor(err))`)
1029
920
  l.push(`\t}`)
1030
921
  l.push(`}`)
@@ -1039,7 +930,7 @@ function emitGoMod(options: GoCLIOptions, hasEmbeddedSDK: boolean): string {
1039
930
  const l: string[] = []
1040
931
  l.push(`module ${mod}`)
1041
932
  l.push(``)
1042
- l.push(`go 1.23`)
933
+ l.push(`go 1.24`)
1043
934
  l.push(``)
1044
935
  l.push(`require (`)
1045
936
  l.push(`\tgithub.com/spf13/cobra v1.8.1`)
@@ -1062,10 +953,10 @@ function emitGoMod(options: GoCLIOptions, hasEmbeddedSDK: boolean): string {
1062
953
 
1063
954
  /* ── emit: README.md ── */
1064
955
 
1065
- function emitReadme(spec: OpenApiSpec, options: GoCLIOptions): string {
956
+ function emitReadme(info: IRInfo, options: GoCLIOptions): string {
1066
957
  const envPrefix = options.envPrefix ?? toSnakeUpper(options.binaryName)
1067
958
  const configName = options.configName ?? options.binaryName
1068
- const title = spec.info?.title ?? options.binaryName
959
+ const title = info.title ?? options.binaryName
1069
960
  return [
1070
961
  `# ${options.binaryName}`,
1071
962
  ``,
@@ -1074,22 +965,22 @@ function emitReadme(spec: OpenApiSpec, options: GoCLIOptions): string {
1074
965
  `## Install`,
1075
966
  ``,
1076
967
  "```sh",
1077
- `go build -o ${options.binaryName} ./...`,
968
+ `go build -ldflags "-X ${resolveModulePath(options)}/internal/cli.Version=1.0.0" -o ${options.binaryName} .`,
1078
969
  "```",
1079
970
  ``,
1080
971
  `## Auth`,
1081
972
  ``,
1082
973
  `Resolution order (high → low):`,
1083
974
  ``,
1084
- `1. \`--api-key <value>\``,
975
+ `1. \`--api-key <value>\` (visible in the process list; prefer the env var)`,
1085
976
  `2. \`${envPrefix}_API_KEY\` env var`,
1086
- `3. \`~/.config/${configName}/config.toml\` → \`api_key = "..."\``,
977
+ `3. \`~/.config/${configName}/config.toml\` → \`api_key = "..."\` (keep it \`chmod 600\`)`,
1087
978
  `4. \`$XDG_CONFIG_HOME/${configName}/config.toml\``,
1088
979
  ``,
1089
980
  `## Global flags`,
1090
981
  ``,
1091
982
  `- \`--api-key\`, \`--base-url\`, \`--output\` (json|ndjson|yaml|table)`,
1092
- `- \`--verbose\`, \`--config\`, \`--timeout\``,
983
+ `- \`--verbose\`, \`--config\`, \`--timeout\` (regular calls only; streams and uploads are not bounded)`,
1093
984
  ``,
1094
985
  `## Exit codes`,
1095
986
  ``,
@@ -1097,8 +988,8 @@ function emitReadme(spec: OpenApiSpec, options: GoCLIOptions): string {
1097
988
  `- 1: 4xx client error`,
1098
989
  `- 2: 5xx server error`,
1099
990
  `- 3: network failure / timeout`,
1100
- `- 4: bad flags / missing api-key`,
1101
- `- 5: unexpected 3xx`,
991
+ `- 4: bad flags / bad config / missing api-key`,
992
+ `- 5: unexpected 3xx (redirects to another host are not followed)`,
1102
993
  ``,
1103
994
  ].join("\n")
1104
995
  }
@@ -1116,12 +1007,10 @@ function copyEmbeddedSDK(spec: Record<string, unknown>, options: GoCLIOptions, o
1116
1007
  }
1117
1008
 
1118
1009
  /* ── ParseSSEStreamExported shim ──
1119
- * The SDK exports parseSSEStream lowercase (package-private).
1120
- * We need to re-expose it under the SDK package so the CLI's cmd/*.go can call it.
1121
- * When embedding, add a small sse_export.go into internal/sdk/.
1122
- * When using external SDK, we still need the same symbol — emit an adapter file. */
1010
+ * Older generated CLIs call ParseSSEStreamExported; kept so they keep compiling against
1011
+ * an embedded SDK. New code uses the Honey* helpers in export.go. */
1123
1012
 
1124
- function emitSSEExportShim(options: GoCLIOptions, files: Record<string, string>, hasEmbedded: boolean): void {
1013
+ function emitSSEExportShim(files: Record<string, string>, hasEmbedded: boolean): void {
1125
1014
  const shim = [
1126
1015
  `// Code generated by honey. DO NOT EDIT.`,
1127
1016
  `package sdk`,
@@ -1138,18 +1027,17 @@ function emitSSEExportShim(options: GoCLIOptions, files: Record<string, string>,
1138
1027
  `}`,
1139
1028
  ``,
1140
1029
  ].join("\n")
1141
- if (hasEmbedded) {
1142
- files["internal/sdk/sse_export.go"] = shim
1143
- }
1144
- /* external-SDK mode: consumer's SDK must already export or we rely on ParseSSEStreamExported existing.
1145
- We include the shim in embed mode only; external mode assumes SDK regenerated w/ same honey. */
1030
+ if (hasEmbedded) files["internal/sdk/sse_export.go"] = shim
1146
1031
  }
1147
1032
 
1148
1033
  /* ── public entrypoint ── */
1149
1034
 
1150
1035
  export function generateGoCLI(spec: Record<string, unknown>, options: GoCLIOptions): GeneratedGoCLI {
1151
- const resolved = resolveRefs(spec as Parameters<typeof resolveRefs>[0]) as unknown as OpenApiSpec
1152
- const { groups, skipped, serviceMap } = collectCLIMethods(spec)
1036
+ const input = spec as unknown as OpenApiSpecInput
1037
+ const model = buildSdkModel(input)
1038
+ const serviceMap = serviceMapOf(model.ir)
1039
+ const pkg = new NameScope(CMD_PACKAGE_NAMES)
1040
+ const { groups, skipped } = collectCLIMethods(model, pkg)
1153
1041
 
1154
1042
  const hasEmbeddedSDK = !options.sdkModulePath
1155
1043
  const files: Record<string, string> = {}
@@ -1159,29 +1047,20 @@ export function generateGoCLI(spec: Record<string, unknown>, options: GoCLIOptio
1159
1047
  files[`internal/cli/${name}`] = content
1160
1048
  }
1161
1049
 
1162
- /* main.go */
1163
1050
  files["main.go"] = emitMainFile(options)
1164
1051
 
1165
- /* cmd/root.go */
1166
- const sortedGroups = [...groups.values()].sort((a, b) => a.resource.localeCompare(b.resource))
1167
- files["cmd/root.go"] = emitRootFile(sortedGroups, options)
1052
+ const sortedGroups = [...groups.values()].sort((a, b) => cmpCodeUnit(a.resource, b.resource))
1053
+ files["cmd/root.go"] = emitRootFile(sortedGroups, options, model.ir.auth)
1168
1054
 
1169
- /* cmd/<resource>.go */
1170
1055
  for (const g of sortedGroups) {
1171
1056
  files[`cmd/${g.fileName}`] = emitResourceFile(g, options)
1172
1057
  }
1173
1058
 
1174
- /* go.mod */
1175
1059
  files["go.mod"] = emitGoMod(options, hasEmbeddedSDK)
1060
+ files["README.md"] = emitReadme(model.ir.info, options)
1176
1061
 
1177
- /* README.md */
1178
- files["README.md"] = emitReadme(resolved, options)
1179
-
1180
- /* SDK embed if sdkModulePath not set */
1181
- if (hasEmbeddedSDK) {
1182
- copyEmbeddedSDK(spec, options, files)
1183
- }
1184
- emitSSEExportShim(options, files, hasEmbeddedSDK)
1062
+ if (hasEmbeddedSDK) copyEmbeddedSDK(spec, options, files)
1063
+ emitSSEExportShim(files, hasEmbeddedSDK)
1185
1064
 
1186
- return { files, serviceMap, skippedOperations: skipped }
1065
+ return { files, serviceMap: serviceMap as Record<string, Record<string, unknown>>, skippedOperations: skipped }
1187
1066
  }