@askrjs/cli 0.0.2 → 0.0.4

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 (310) hide show
  1. package/README.md +46 -7
  2. package/dist/add.d.ts +16 -0
  3. package/dist/add.js +558 -1
  4. package/dist/cli.d.ts +5 -0
  5. package/dist/cli.js +43 -16
  6. package/dist/create.d.ts +5 -0
  7. package/dist/create.js +104 -26
  8. package/dist/directory-swap-DWoHtx7C.js +35 -0
  9. package/dist/discovery-Difb7Y4G.js +323 -0
  10. package/dist/generate.d.ts +5 -0
  11. package/dist/generate.js +461 -0
  12. package/dist/is-direct-execution-Cdlr-ZUl.js +0 -2
  13. package/dist/openapi.d.ts +25 -0
  14. package/dist/openapi.js +49 -16
  15. package/dist/planner-VAj7qlxr.js +285 -0
  16. package/dist/range-YUs9eimn.js +125 -0
  17. package/dist/registry-CyrRHRG5.js +209 -0
  18. package/dist/skills/askr-dashboard-charts/SKILL.md +61 -44
  19. package/dist/skills/askr-dashboard-charts/agents/openai.yaml +2 -2
  20. package/dist/skills/askr-ssr-ssg/SKILL.md +3 -1
  21. package/dist/{skills-Bh0mEhIx.js → skills-B7CbWur9.js} +89 -26
  22. package/dist/skills.d.ts +18 -0
  23. package/dist/skills.js +1 -1
  24. package/dist/specification-DXnDOC-0.js +49 -0
  25. package/dist/ssg-config.d.ts +41 -0
  26. package/dist/ssg-config.js +1 -0
  27. package/dist/ssg.d.ts +61 -0
  28. package/dist/ssg.js +360 -24
  29. package/dist/templates/full-stack/AGENTS.md +0 -1
  30. package/dist/templates/full-stack/README.md +0 -1
  31. package/dist/templates/full-stack/index.html +0 -1
  32. package/dist/templates/full-stack/package.json +5 -3
  33. package/dist/templates/full-stack/server.ts +5 -9
  34. package/dist/templates/full-stack/src/action-authorizations.ts +2 -2
  35. package/dist/templates/full-stack/src/actions/create-message.ts +5 -5
  36. package/dist/templates/full-stack/src/i18n.ts +4 -4
  37. package/dist/templates/full-stack/src/main.tsx +4 -5
  38. package/dist/templates/full-stack/src/pages/home.tsx +5 -5
  39. package/dist/templates/full-stack/src/pages/layout.tsx +15 -5
  40. package/dist/templates/full-stack/src/pages/not-found.tsx +5 -2
  41. package/dist/templates/full-stack/src/routes.tsx +11 -12
  42. package/dist/templates/full-stack/src/schemas.ts +1 -2
  43. package/dist/templates/full-stack/src/server/action-registry.ts +5 -10
  44. package/dist/templates/full-stack/src/server/actions/create-message.ts +4 -4
  45. package/dist/templates/full-stack/src/server/app.ts +67 -57
  46. package/dist/templates/full-stack/src/server/dependencies.ts +10 -10
  47. package/dist/templates/full-stack/src/server/entry-server.ts +3 -3
  48. package/dist/templates/full-stack/src/telemetry.ts +1 -2
  49. package/dist/templates/full-stack/tests/actions/create-message.test.ts +7 -8
  50. package/dist/templates/full-stack/tsconfig.json +1 -2
  51. package/dist/templates/full-stack/vite.config.ts +5 -6
  52. package/dist/templates/spa/AGENTS.md +1 -1
  53. package/dist/templates/spa/package.json +3 -4
  54. package/dist/templates/spa/src/adapters/operations-client.ts +7 -2
  55. package/dist/templates/spa/src/pages/app/_routes.tsx +2 -0
  56. package/dist/templates/spa/src/pages/app/admin-home.tsx +21 -3
  57. package/dist/templates/spa/src/pages/public/_routes.tsx +2 -0
  58. package/dist/templates/spa/src/styles.css +1 -1
  59. package/dist/templates/spa/tsconfig.json +2 -1
  60. package/dist/templates/spa/vitest.config.ts +0 -4
  61. package/dist/templates/ssg/README.md +1 -1
  62. package/dist/templates/ssg/package.json +1 -1
  63. package/dist/templates/ssg/ssg.config.ts +7 -0
  64. package/dist/templates/ssg/tests/ssg-config.test.ts +5 -3
  65. package/dist/templates/ssg/tsconfig.json +2 -1
  66. package/dist/templates/ssg/vitest.config.ts +0 -4
  67. package/dist/templates/ssr/package.json +2 -2
  68. package/dist/templates/ssr/server.ts +4 -10
  69. package/dist/templates/ssr/src/entry-server.tsx +6 -4
  70. package/dist/templates/startkit/package.json +2 -2
  71. package/dist/templates/startkit/src/lib/mock-data.ts +17 -39
  72. package/dist/templates/startkit/tsconfig.json +2 -1
  73. package/dist/update.d.ts +41 -0
  74. package/dist/update.js +282 -2
  75. package/dist/writer-D8qe_7ud.js +226 -0
  76. package/package.json +23 -22
  77. package/dist/add-DqhqRzBQ.js +0 -555
  78. package/dist/add-DqhqRzBQ.js.map +0 -1
  79. package/dist/cli.js.map +0 -1
  80. package/dist/create.js.map +0 -1
  81. package/dist/is-direct-execution-Cdlr-ZUl.js.map +0 -1
  82. package/dist/openapi.js.map +0 -1
  83. package/dist/skills-Bh0mEhIx.js.map +0 -1
  84. package/dist/ssg.js.map +0 -1
  85. package/dist/templates/full-stack/src/vite-server.d.ts +0 -6
  86. package/dist/templates/spa/src/styles.d.ts +0 -1
  87. package/dist/templates/ssg/src/vite-env.d.ts +0 -5
  88. package/dist/templates/ssr/src/vite-server.d.ts +0 -7
  89. package/dist/templates/startkit/src/vite-env.d.ts +0 -5
  90. package/dist/update-DyW1na5V.js +0 -1140
  91. package/dist/update-DyW1na5V.js.map +0 -1
  92. package/skills/askr-accessibility/SKILL.md +0 -68
  93. package/skills/askr-accessibility/agents/openai.yaml +0 -4
  94. package/skills/askr-agent-execution/SKILL.md +0 -85
  95. package/skills/askr-agent-execution/agents/openai.yaml +0 -4
  96. package/skills/askr-agent-workflows/SKILL.md +0 -57
  97. package/skills/askr-agent-workflows/agents/openai.yaml +0 -4
  98. package/skills/askr-api-integration/SKILL.md +0 -79
  99. package/skills/askr-api-integration/agents/openai.yaml +0 -4
  100. package/skills/askr-app-builder/SKILL.md +0 -99
  101. package/skills/askr-app-builder/agents/openai.yaml +0 -4
  102. package/skills/askr-auth-access/SKILL.md +0 -74
  103. package/skills/askr-auth-access/agents/openai.yaml +0 -4
  104. package/skills/askr-cli-vite/SKILL.md +0 -76
  105. package/skills/askr-cli-vite/agents/openai.yaml +0 -4
  106. package/skills/askr-dashboard-charts/SKILL.md +0 -82
  107. package/skills/askr-dashboard-charts/agents/openai.yaml +0 -4
  108. package/skills/askr-design-system/SKILL.md +0 -86
  109. package/skills/askr-design-system/agents/openai.yaml +0 -4
  110. package/skills/askr-env-config/SKILL.md +0 -74
  111. package/skills/askr-env-config/agents/openai.yaml +0 -4
  112. package/skills/askr-error-loading-empty/SKILL.md +0 -89
  113. package/skills/askr-error-loading-empty/agents/openai.yaml +0 -4
  114. package/skills/askr-file-upload-artifacts/SKILL.md +0 -77
  115. package/skills/askr-file-upload-artifacts/agents/openai.yaml +0 -4
  116. package/skills/askr-forms-tables-crud/SKILL.md +0 -98
  117. package/skills/askr-forms-tables-crud/agents/openai.yaml +0 -4
  118. package/skills/askr-mental-model/SKILL.md +0 -109
  119. package/skills/askr-mental-model/agents/openai.yaml +0 -4
  120. package/skills/askr-migration-react/SKILL.md +0 -81
  121. package/skills/askr-migration-react/agents/openai.yaml +0 -4
  122. package/skills/askr-observability-debugging/SKILL.md +0 -83
  123. package/skills/askr-observability-debugging/agents/openai.yaml +0 -4
  124. package/skills/askr-project-structure/SKILL.md +0 -79
  125. package/skills/askr-project-structure/agents/openai.yaml +0 -4
  126. package/skills/askr-query-mutation/SKILL.md +0 -115
  127. package/skills/askr-query-mutation/agents/openai.yaml +0 -4
  128. package/skills/askr-realtime-streaming/SKILL.md +0 -76
  129. package/skills/askr-realtime-streaming/agents/openai.yaml +0 -4
  130. package/skills/askr-resources-data/SKILL.md +0 -100
  131. package/skills/askr-resources-data/agents/openai.yaml +0 -4
  132. package/skills/askr-routing-layouts/SKILL.md +0 -107
  133. package/skills/askr-routing-layouts/agents/openai.yaml +0 -4
  134. package/skills/askr-runtime-reactivity/SKILL.md +0 -92
  135. package/skills/askr-runtime-reactivity/agents/openai.yaml +0 -4
  136. package/skills/askr-ssr-ssg/SKILL.md +0 -76
  137. package/skills/askr-ssr-ssg/agents/openai.yaml +0 -4
  138. package/skills/askr-testing-determinism/SKILL.md +0 -75
  139. package/skills/askr-testing-determinism/agents/openai.yaml +0 -4
  140. package/skills/askr-theming/SKILL.md +0 -86
  141. package/skills/askr-theming/agents/openai.yaml +0 -4
  142. package/skills/askr-ui-composition/SKILL.md +0 -92
  143. package/skills/askr-ui-composition/agents/openai.yaml +0 -4
  144. package/templates/full-stack/AGENTS.md +0 -12
  145. package/templates/full-stack/README.md +0 -18
  146. package/templates/full-stack/gitignore.template +0 -4
  147. package/templates/full-stack/index.html +0 -14
  148. package/templates/full-stack/package.json +0 -37
  149. package/templates/full-stack/server.ts +0 -13
  150. package/templates/full-stack/src/action-authorizations.ts +0 -10
  151. package/templates/full-stack/src/actions/create-message.ts +0 -10
  152. package/templates/full-stack/src/i18n.ts +0 -8
  153. package/templates/full-stack/src/main.tsx +0 -12
  154. package/templates/full-stack/src/pages/home.tsx +0 -25
  155. package/templates/full-stack/src/pages/layout.tsx +0 -33
  156. package/templates/full-stack/src/pages/not-found.tsx +0 -4
  157. package/templates/full-stack/src/routes.tsx +0 -20
  158. package/templates/full-stack/src/schemas.ts +0 -11
  159. package/templates/full-stack/src/server/action-registry.ts +0 -12
  160. package/templates/full-stack/src/server/actions/create-message.ts +0 -14
  161. package/templates/full-stack/src/server/app.ts +0 -66
  162. package/templates/full-stack/src/server/dependencies.ts +0 -55
  163. package/templates/full-stack/src/server/entry-server.ts +0 -8
  164. package/templates/full-stack/src/telemetry.ts +0 -4
  165. package/templates/full-stack/src/vite-server.d.ts +0 -6
  166. package/templates/full-stack/tests/actions/create-message.test.ts +0 -13
  167. package/templates/full-stack/tsconfig.json +0 -16
  168. package/templates/full-stack/vite.config.ts +0 -11
  169. package/templates/spa/AGENTS.md +0 -60
  170. package/templates/spa/README.md +0 -96
  171. package/templates/spa/gitignore.template +0 -16
  172. package/templates/spa/index.html +0 -13
  173. package/templates/spa/package.json +0 -35
  174. package/templates/spa/src/adapters/operations-client.ts +0 -90
  175. package/templates/spa/src/components/shared/metric-card.tsx +0 -27
  176. package/templates/spa/src/components/shared/status-badge.tsx +0 -24
  177. package/templates/spa/src/features/operations/operations.query.ts +0 -9
  178. package/templates/spa/src/main.tsx +0 -10
  179. package/templates/spa/src/pages/_layout.tsx +0 -9
  180. package/templates/spa/src/pages/_routes.tsx +0 -27
  181. package/templates/spa/src/pages/app/_layout.tsx +0 -94
  182. package/templates/spa/src/pages/app/_routes.tsx +0 -10
  183. package/templates/spa/src/pages/app/admin-home.tsx +0 -152
  184. package/templates/spa/src/pages/app/agent-runs.tsx +0 -93
  185. package/templates/spa/src/pages/app/settings.tsx +0 -91
  186. package/templates/spa/src/pages/auth/_layout.tsx +0 -38
  187. package/templates/spa/src/pages/auth/_routes.tsx +0 -6
  188. package/templates/spa/src/pages/auth/login.tsx +0 -63
  189. package/templates/spa/src/pages/not-found.tsx +0 -22
  190. package/templates/spa/src/pages/public/_layout.tsx +0 -47
  191. package/templates/spa/src/pages/public/_routes.tsx +0 -6
  192. package/templates/spa/src/pages/public/home.tsx +0 -143
  193. package/templates/spa/src/shared/format.ts +0 -12
  194. package/templates/spa/src/shared/navigation.ts +0 -15
  195. package/templates/spa/src/styles/components.css +0 -49
  196. package/templates/spa/src/styles/layout.css +0 -192
  197. package/templates/spa/src/styles/reset.css +0 -44
  198. package/templates/spa/src/styles/theme.css +0 -57
  199. package/templates/spa/src/styles/tokens.css +0 -20
  200. package/templates/spa/src/styles.css +0 -7
  201. package/templates/spa/src/styles.d.ts +0 -1
  202. package/templates/spa/tests/app.test.tsx +0 -37
  203. package/templates/spa/tests/components/shared.test.tsx +0 -12
  204. package/templates/spa/tests/resources.test.ts +0 -22
  205. package/templates/spa/tsconfig.json +0 -20
  206. package/templates/spa/tsconfig.node.json +0 -10
  207. package/templates/spa/vite.config.ts +0 -24
  208. package/templates/spa/vitest.config.ts +0 -17
  209. package/templates/ssg/AGENTS.md +0 -55
  210. package/templates/ssg/README.md +0 -39
  211. package/templates/ssg/gitignore.template +0 -17
  212. package/templates/ssg/index.html +0 -13
  213. package/templates/ssg/package.json +0 -35
  214. package/templates/ssg/src/app.tsx +0 -14
  215. package/templates/ssg/src/components/badge.tsx +0 -3
  216. package/templates/ssg/src/components/counter.tsx +0 -30
  217. package/templates/ssg/src/components/site-shell.tsx +0 -111
  218. package/templates/ssg/src/jsx.d.ts +0 -23
  219. package/templates/ssg/src/main.tsx +0 -8
  220. package/templates/ssg/src/pages/about.tsx +0 -82
  221. package/templates/ssg/src/pages/content.tsx +0 -63
  222. package/templates/ssg/src/pages/example.tsx +0 -97
  223. package/templates/ssg/src/pages/home.tsx +0 -51
  224. package/templates/ssg/src/resources/user.ts +0 -15
  225. package/templates/ssg/src/routes.tsx +0 -15
  226. package/templates/ssg/src/styles.css +0 -89
  227. package/templates/ssg/src/vite-env.d.ts +0 -5
  228. package/templates/ssg/ssg.config.ts +0 -34
  229. package/templates/ssg/tests/app.test.tsx +0 -19
  230. package/templates/ssg/tests/components/counter.test.tsx +0 -18
  231. package/templates/ssg/tests/resources.test.ts +0 -11
  232. package/templates/ssg/tests/ssg-config.test.ts +0 -11
  233. package/templates/ssg/tsconfig.json +0 -20
  234. package/templates/ssg/tsconfig.node.json +0 -10
  235. package/templates/ssg/vite.config.ts +0 -24
  236. package/templates/ssg/vitest.config.ts +0 -17
  237. package/templates/ssr/AGENTS.md +0 -28
  238. package/templates/ssr/README.md +0 -38
  239. package/templates/ssr/gitignore.template +0 -16
  240. package/templates/ssr/index.html +0 -13
  241. package/templates/ssr/package.json +0 -34
  242. package/templates/ssr/server.ts +0 -16
  243. package/templates/ssr/src/app.tsx +0 -21
  244. package/templates/ssr/src/components/counter.tsx +0 -26
  245. package/templates/ssr/src/entry-server.tsx +0 -10
  246. package/templates/ssr/src/main.tsx +0 -11
  247. package/templates/ssr/src/pages/about.tsx +0 -75
  248. package/templates/ssr/src/pages/example.tsx +0 -113
  249. package/templates/ssr/src/pages/home.tsx +0 -56
  250. package/templates/ssr/src/resources/user.ts +0 -15
  251. package/templates/ssr/src/routes.tsx +0 -13
  252. package/templates/ssr/src/styles.css +0 -148
  253. package/templates/ssr/src/vite-server.d.ts +0 -7
  254. package/templates/ssr/tests/app.test.tsx +0 -24
  255. package/templates/ssr/tests/components/counter.test.tsx +0 -13
  256. package/templates/ssr/tests/resources.test.ts +0 -11
  257. package/templates/ssr/tsconfig.json +0 -22
  258. package/templates/ssr/tsconfig.node.json +0 -10
  259. package/templates/ssr/tsconfig.server.json +0 -15
  260. package/templates/ssr/vite.config.ts +0 -25
  261. package/templates/ssr/vitest.config.ts +0 -13
  262. package/templates/startkit/AGENTS.md +0 -77
  263. package/templates/startkit/README.md +0 -192
  264. package/templates/startkit/gitignore.template +0 -16
  265. package/templates/startkit/index.html +0 -13
  266. package/templates/startkit/package.json +0 -35
  267. package/templates/startkit/src/components/app-header.tsx +0 -93
  268. package/templates/startkit/src/components/app-sidebar.tsx +0 -95
  269. package/templates/startkit/src/components/data-table.tsx +0 -80
  270. package/templates/startkit/src/components/empty-state.tsx +0 -13
  271. package/templates/startkit/src/components/page-header.tsx +0 -23
  272. package/templates/startkit/src/components/stat-card.tsx +0 -24
  273. package/templates/startkit/src/features/accounts/account-filters.tsx +0 -61
  274. package/templates/startkit/src/features/accounts/account-table.tsx +0 -79
  275. package/templates/startkit/src/lib/format.ts +0 -31
  276. package/templates/startkit/src/lib/mock-data.ts +0 -441
  277. package/templates/startkit/src/lib/routes.ts +0 -116
  278. package/templates/startkit/src/main.tsx +0 -12
  279. package/templates/startkit/src/pages/_layout.tsx +0 -56
  280. package/templates/startkit/src/pages/auth/_layout.tsx +0 -7
  281. package/templates/startkit/src/pages/auth/login.tsx +0 -129
  282. package/templates/startkit/src/pages/home.tsx +0 -83
  283. package/templates/startkit/src/pages/not-found.tsx +0 -24
  284. package/templates/startkit/src/pages/workspace/_layout.tsx +0 -17
  285. package/templates/startkit/src/pages/workspace/accounts/index.tsx +0 -217
  286. package/templates/startkit/src/pages/workspace/dashboard.tsx +0 -118
  287. package/templates/startkit/src/pages/workspace/settings.tsx +0 -218
  288. package/templates/startkit/src/router.tsx +0 -7
  289. package/templates/startkit/src/routes/auth-config.ts +0 -38
  290. package/templates/startkit/src/routes/auth.ts +0 -8
  291. package/templates/startkit/src/routes/index.ts +0 -28
  292. package/templates/startkit/src/routes/public.ts +0 -8
  293. package/templates/startkit/src/routes/workspace/accounts.ts +0 -8
  294. package/templates/startkit/src/routes/workspace/index.ts +0 -14
  295. package/templates/startkit/src/styles/components.css +0 -396
  296. package/templates/startkit/src/styles/layout.css +0 -192
  297. package/templates/startkit/src/styles/reset.css +0 -38
  298. package/templates/startkit/src/styles/theme.css +0 -76
  299. package/templates/startkit/src/styles/tokens.css +0 -67
  300. package/templates/startkit/src/styles.css +0 -7
  301. package/templates/startkit/src/toast.ts +0 -55
  302. package/templates/startkit/src/utils/join-classes.ts +0 -4
  303. package/templates/startkit/src/vite-env.d.ts +0 -5
  304. package/templates/startkit/tests/app.test.tsx +0 -32
  305. package/templates/startkit/tests/preferences.test.ts +0 -43
  306. package/templates/startkit/tests/resources.test.ts +0 -82
  307. package/templates/startkit/tsconfig.json +0 -20
  308. package/templates/startkit/tsconfig.node.json +0 -10
  309. package/templates/startkit/vite.config.ts +0 -32
  310. package/templates/startkit/vitest.config.ts +0 -13
@@ -0,0 +1,461 @@
1
+ import { mkdir, mkdtemp, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
2
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
3
+ import { fileURLToPath, pathToFileURL } from "node:url";
4
+ import { load } from "js-yaml";
5
+ //#region src/generate/generator.ts
6
+ const OWNED = [
7
+ ".askr-generated.json",
8
+ "schemas.ts",
9
+ "operations.ts",
10
+ "api.ts",
11
+ "index.ts"
12
+ ];
13
+ const METHODS = [
14
+ "get",
15
+ "post",
16
+ "put",
17
+ "patch",
18
+ "delete",
19
+ "head",
20
+ "options"
21
+ ];
22
+ const identifier = /^[A-Za-z_$][\w$]*$/;
23
+ const pointer = (...parts) => `#/${parts.map((p) => String(p).replace(/~/g, "~0").replace(/\//g, "~1")).join("/")}`;
24
+ var GenerationError = class extends Error {};
25
+ const fail = (message, at, operationId) => {
26
+ throw new GenerationError(`${operationId ? `${operationId}: ` : ""}${message} at ${at}`);
27
+ };
28
+ const pascal = (name) => {
29
+ const result = name.replace(/(^|[^A-Za-z0-9]+)([A-Za-z0-9])/g, (_, _s, c) => c.toUpperCase()).replace(/[^A-Za-z0-9_$]/g, "");
30
+ return /^\d/.test(result) ? `_${result}` : result;
31
+ };
32
+ function schemaType(schema, at, direction = "response") {
33
+ if (schema.$ref) {
34
+ const name = decodeURIComponent(String(schema.$ref).split("/").at(-1));
35
+ return pascal(name);
36
+ }
37
+ if (Array.isArray(schema.type)) {
38
+ const includesNull = schema.type.includes("null");
39
+ const rendered = [...new Set(schema.type.filter((value) => value !== "null"))].map((variant) => schemaType({
40
+ ...schema,
41
+ type: variant,
42
+ nullable: false
43
+ }, at, direction));
44
+ if (includesNull) rendered.push("null");
45
+ if (rendered.length === 0) fail("Schema type array must not be empty", at);
46
+ return [...new Set(rendered)].join(" | ");
47
+ }
48
+ if (schema.const !== void 0) return JSON.stringify(schema.const);
49
+ if (schema.enum) return schema.enum.map((value) => JSON.stringify(value)).join(" | ") || "never";
50
+ if (schema.oneOf || schema.anyOf) return (schema.oneOf ?? schema.anyOf).map((item, i) => schemaType(item, `${at}/${schema.oneOf ? "oneOf" : "anyOf"}/${i}`, direction)).join(" | ");
51
+ if (schema.allOf) return schema.allOf.map((item, i) => schemaType(item, `${at}/allOf/${i}`, direction)).join(" & ");
52
+ let type = "never";
53
+ if (schema.type === "string") type = "string";
54
+ else if (schema.type === "number" || schema.type === "integer") type = "number";
55
+ else if (schema.type === "boolean") type = "boolean";
56
+ else if (schema.type === "null") type = "null";
57
+ else if (schema.type === "array" || schema.items) type = `Array<${schemaType(schema.items ?? {}, `${at}/items`, direction)}>`;
58
+ else if (schema.type === "object" || schema.properties || schema.additionalProperties !== void 0) {
59
+ const required = new Set(schema.required ?? []);
60
+ const properties = Object.entries(schema.properties ?? {}).filter(([, value]) => !(direction === "request" ? value.readOnly : value.writeOnly)).map(([key, value]) => ` ${JSON.stringify(key)}${required.has(key) ? "" : "?"}: ${schemaType(value, `${at}/properties/${key}`, direction)};`);
61
+ if (schema.additionalProperties && typeof schema.additionalProperties === "object") properties.push(` [key: string]: ${schemaType(schema.additionalProperties, `${at}/additionalProperties`, direction)};`);
62
+ type = `{\n${properties.join("\n")}\n}`;
63
+ } else if (!schema.type) fail("Unsupported schema without a type", at);
64
+ else fail(`Unsupported schema type ${schema.type}`, at);
65
+ return schema.nullable || Array.isArray(schema.type) && schema.type.includes("null") ? `${type} | null` : type;
66
+ }
67
+ function responseSchema(operation, at, success) {
68
+ return Object.entries(operation.responses ?? {}).filter(([status]) => status === "default" ? !success : success === /^2\d\d$/.test(status)).sort(([a], [b]) => a.localeCompare(b, void 0, { numeric: true }));
69
+ }
70
+ function pickContent(content, at, direction) {
71
+ const entries = Object.entries(content ?? {}).sort(([a], [b]) => a.localeCompare(b));
72
+ if (!entries.length) return {
73
+ type: "undefined",
74
+ codec: "empty()",
75
+ media: []
76
+ };
77
+ const types = entries.map(([mediaType, value]) => ({
78
+ mediaType,
79
+ type: schemaType(value.schema ?? {}, `${at}/content/${mediaType.replace(/~/g, "~0").replace(/\//g, "~1")}/schema`, direction)
80
+ }));
81
+ const union = [...new Set(types.map((v) => v.type))].join(" | ");
82
+ return {
83
+ type: union,
84
+ codec: types.length === 1 ? codecFor(types[0].mediaType, union) : `content({ ${types.map((v) => `${JSON.stringify(v.mediaType)}: ${codecFor(v.mediaType, v.type)}`).join(", ")} })`,
85
+ media: types.map((v) => v.mediaType)
86
+ };
87
+ }
88
+ function codecFor(mediaType, type) {
89
+ if (mediaType === "application/json" || mediaType.endsWith("+json")) return `json<${type}>()`;
90
+ if (mediaType.startsWith("text/")) return "text()";
91
+ if (mediaType === "application/x-www-form-urlencoded") return "urlEncoded()";
92
+ if (mediaType === "multipart/form-data") return "multipart()";
93
+ if (mediaType === "application/octet-stream") return "arrayBuffer()";
94
+ fail(`Unsupported media type ${mediaType}`, "#/content");
95
+ }
96
+ const pointerPart = (value) => decodeURIComponent(value).replace(/~1/g, "/").replace(/~0/g, "~");
97
+ const pointerValue = (document, fragment) => {
98
+ let value = document;
99
+ for (const part of fragment.replace(/^#?\/?/, "").split("/").filter(Boolean)) {
100
+ if (!value || typeof value !== "object") throw new GenerationError(`Reference target is missing at #/${part}`);
101
+ value = value[pointerPart(part)];
102
+ }
103
+ if (value === void 0) throw new GenerationError(`Reference target is missing at ${fragment || "#"}`);
104
+ return value;
105
+ };
106
+ function parseOpenApiDocument(contents, source) {
107
+ let parsed;
108
+ try {
109
+ parsed = JSON.parse(contents);
110
+ } catch {
111
+ try {
112
+ parsed = load(contents);
113
+ } catch (error) {
114
+ throw new GenerationError(`Unable to parse OpenAPI document ${source}: ${error instanceof Error ? error.message : String(error)}`);
115
+ }
116
+ }
117
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new GenerationError(`OpenAPI document must be an object: ${source}`);
118
+ return parsed;
119
+ }
120
+ async function readSource(uri) {
121
+ if (uri.startsWith("http://") || uri.startsWith("https://")) {
122
+ const response = await fetch(uri);
123
+ if (!response.ok) throw new GenerationError(`Unable to fetch OpenAPI reference ${uri}: ${response.status} ${response.statusText}`);
124
+ return {
125
+ uri,
126
+ document: parseOpenApiDocument(await response.text(), uri)
127
+ };
128
+ }
129
+ const path = fileURLToPath(uri);
130
+ return {
131
+ uri,
132
+ document: parseOpenApiDocument(await readFile(path, "utf8"), path)
133
+ };
134
+ }
135
+ var OpenApiBundler = class {
136
+ root;
137
+ rootUri;
138
+ sources = /* @__PURE__ */ new Map();
139
+ aliases = /* @__PURE__ */ new Map();
140
+ names = /* @__PURE__ */ new Map();
141
+ constructor(root, rootUri) {
142
+ this.root = root;
143
+ this.rootUri = rootUri;
144
+ }
145
+ async bundle() {
146
+ if (!this.root.components) this.root.components = {};
147
+ if (!this.root.components.schemas) this.root.components.schemas = {};
148
+ await this.expandObject(this.root, this.rootUri, this.root, /* @__PURE__ */ new Set());
149
+ return this.root;
150
+ }
151
+ async source(uri) {
152
+ let pending = this.sources.get(uri);
153
+ if (!pending) {
154
+ pending = readSource(uri);
155
+ this.sources.set(uri, pending);
156
+ }
157
+ return pending;
158
+ }
159
+ componentName(uri, fragment) {
160
+ const canonical = `${uri}#${fragment}`;
161
+ const existing = this.aliases.get(canonical);
162
+ if (existing) return existing;
163
+ const raw = fragment.split("/").filter(Boolean).at(-1) ?? basename(new URL(uri).pathname).replace(/\.[^.]+$/, "");
164
+ const base = pascal(pointerPart(raw)) || "ExternalSchema";
165
+ let name = base;
166
+ let suffix = 2;
167
+ while (this.names.has(name) && this.names.get(name) !== canonical) name = `${base}${suffix++}`;
168
+ this.aliases.set(canonical, name);
169
+ this.names.set(name, canonical);
170
+ return name;
171
+ }
172
+ async expandObject(value, uri, document, stack) {
173
+ for (const [key, child] of Object.entries(value)) value[key] = await this.expand(child, uri, document, stack);
174
+ }
175
+ async expand(value, uri, document, stack) {
176
+ if (Array.isArray(value)) return Promise.all(value.map((item) => this.expand(item, uri, document, stack)));
177
+ if (!value || typeof value !== "object") return value;
178
+ const object = value;
179
+ if (typeof object.$ref === "string") {
180
+ const resolved = await this.resolveRef(object.$ref, uri, document, stack);
181
+ const siblingEntries = Object.entries(object).filter(([key]) => key !== "$ref");
182
+ if (!siblingEntries.length) return resolved;
183
+ const result = resolved && typeof resolved === "object" && !Array.isArray(resolved) ? { ...resolved } : {};
184
+ for (const [key, child] of siblingEntries) result[key] = await this.expand(child, uri, document, stack);
185
+ return result;
186
+ }
187
+ const result = {};
188
+ for (const [key, child] of Object.entries(object)) result[key] = await this.expand(child, uri, document, stack);
189
+ return result;
190
+ }
191
+ async resolveRef(ref, baseUri, document, stack) {
192
+ const hash = ref.indexOf("#");
193
+ const targetUri = hash === -1 ? new URL(ref, baseUri).href : new URL(ref.slice(0, hash) || baseUri, baseUri).href;
194
+ const fragment = hash === -1 ? "" : ref.slice(hash + 1);
195
+ const canonical = `${targetUri}#${fragment}`;
196
+ const targetSource = targetUri === this.rootUri ? {
197
+ uri: targetUri,
198
+ document
199
+ } : await this.source(targetUri);
200
+ const target = pointerValue(targetSource.document, fragment);
201
+ if (targetUri === this.rootUri && fragment.startsWith("/components/schemas/")) return { $ref: `#${fragment}` };
202
+ if (fragment.startsWith("/components/schemas/")) {
203
+ const name = this.componentName(targetUri, fragment);
204
+ if (stack.has(canonical)) return { $ref: `#/components/schemas/${name}` };
205
+ if (!(name in this.root.components.schemas)) {
206
+ this.root.components.schemas[name] = {};
207
+ const next = new Set(stack).add(canonical);
208
+ this.root.components.schemas[name] = await this.expand(target, targetSource.uri, targetSource.document, next);
209
+ }
210
+ return { $ref: `#/components/schemas/${name}` };
211
+ }
212
+ if (stack.has(canonical)) throw new GenerationError(`Circular non-schema reference at ${ref}`);
213
+ return this.expand(target, targetSource.uri, targetSource.document, new Set(stack).add(canonical));
214
+ }
215
+ };
216
+ async function loadOpenApi(input) {
217
+ const source = await readSource(/^https?:\/\//.test(input) ? input : pathToFileURL(resolve(input)).href);
218
+ return new OpenApiBundler(source.document, source.uri).bundle();
219
+ }
220
+ function generateFiles(document) {
221
+ const version = String(document.openapi ?? "");
222
+ if (!/^3\.(0|1)\.\d+$/.test(version)) fail(`Unsupported OpenAPI version ${version || "missing"}`, "#/openapi");
223
+ if (document.webhooks) fail("Webhooks are unsupported", "#/webhooks");
224
+ const schemas = document.components?.schemas ?? {};
225
+ const names = /* @__PURE__ */ new Map();
226
+ for (const name of Object.keys(schemas).sort()) {
227
+ const safe = pascal(name);
228
+ if (!safe || names.has(safe)) fail(`Component name collision for ${name}`, pointer("components", "schemas", name));
229
+ names.set(safe, name);
230
+ }
231
+ const schemaLines = [...names].map(([safe, original]) => `export type ${safe} = ${schemaType(schemas[original], pointer("components", "schemas", original))};`);
232
+ const operationTypes = [];
233
+ const descriptors = [];
234
+ const seen = /* @__PURE__ */ new Set();
235
+ for (const path of Object.keys(document.paths ?? {}).sort()) for (const method of METHODS) {
236
+ const operation = document.paths[path]?.[method];
237
+ if (!operation) continue;
238
+ const at = pointer("paths", path, method);
239
+ const id = operation.operationId;
240
+ if (!id || !identifier.test(id)) fail("A unique JavaScript-identifier operationId is required", `${at}/operationId`, id);
241
+ if (seen.has(id)) fail("Duplicate operationId", `${at}/operationId`, id);
242
+ seen.add(id);
243
+ if (operation.callbacks) fail("Callbacks are unsupported", `${at}/callbacks`, id);
244
+ const parametersByIdentity = /* @__PURE__ */ new Map();
245
+ for (const parameter of [...document.paths[path].parameters ?? [], ...operation.parameters ?? []]) {
246
+ if (parameter.$ref) fail("Parameter references must be bundled", `${at}/parameters`, id);
247
+ if (typeof parameter.name !== "string" || typeof parameter.in !== "string") fail("Parameters require string name and in fields", `${at}/parameters`, id);
248
+ parametersByIdentity.set(`${parameter.in}\0${parameter.name}`, parameter);
249
+ }
250
+ const parameters = [...parametersByIdentity.values()];
251
+ const byLocation = {
252
+ path: [],
253
+ query: [],
254
+ header: []
255
+ };
256
+ for (const parameter of parameters) {
257
+ if (parameter.in === "cookie") fail("Cookie parameters are unsupported", `${at}/parameters`, id);
258
+ if (!byLocation[parameter.in]) fail(`Unsupported parameter location ${parameter.in}`, `${at}/parameters`, id);
259
+ byLocation[parameter.in].push(parameter);
260
+ }
261
+ const templateParameters = new Set([...path.matchAll(/\{([^{}]+)\}/g)].map((match) => match[1]));
262
+ const declaredPathParameters = new Set(byLocation.path.map((parameter) => parameter.name));
263
+ for (const name of templateParameters) {
264
+ const parameter = byLocation.path.find((candidate) => candidate.name === name);
265
+ if (!parameter) fail(`Path template parameter ${name} is not declared`, `${at}/parameters`, id);
266
+ if (!parameter || parameter.required !== true) fail(`Path parameter ${name} must be required`, `${at}/parameters`, id);
267
+ }
268
+ for (const name of declaredPathParameters) if (!templateParameters.has(name)) fail(`Path parameter ${name} is not present in the path template`, `${at}/parameters`, id);
269
+ const prefix = pascal(id);
270
+ const renderParameters = (location) => {
271
+ const list = byLocation[location].sort((a, b) => a.name.localeCompare(b.name));
272
+ if (!list.length) return void 0;
273
+ const name = `${prefix}${pascal(location)}`;
274
+ operationTypes.push(`export type ${name} = {\n${list.map((p) => ` ${JSON.stringify(p.name)}${p.required || location === "path" ? "" : "?"}: ${schemaType(p.schema ?? {}, `${at}/parameters`, "request")};`).join("\n")}\n};`);
275
+ const defaultStyle = location === "query" ? "form" : "simple";
276
+ return {
277
+ name,
278
+ spec: `{ ${list.map((p) => {
279
+ const style = p.style ?? defaultStyle;
280
+ const explode = p.explode ?? style === "form";
281
+ return `${JSON.stringify(p.name)}: { style: ${JSON.stringify(style)}, explode: ${explode} }`;
282
+ }).join(", ")} }`
283
+ };
284
+ };
285
+ const params = renderParameters("path"), query = renderParameters("query"), headers = renderParameters("header");
286
+ const body = operation.requestBody ? pickContent(operation.requestBody.content, `${at}/requestBody`, "request") : void 0;
287
+ if (body) operationTypes.push(`export type ${prefix}Body = ${body.type};`);
288
+ const successes = responseSchema(operation, at, true);
289
+ if (!successes.length) fail("At least one 2xx response is required", `${at}/responses`, id);
290
+ const errors = responseSchema(operation, at, false);
291
+ const chain = [`${method === "delete" ? "del" : method}(${JSON.stringify(path)})`];
292
+ if (params) chain.push(`params<${params.name}>(${params.spec})`);
293
+ if (query) chain.push(`query<${query.name}>(${query.spec})`);
294
+ if (headers) chain.push(`headers<${headers.name}>(${headers.spec})`);
295
+ if (body) chain.push(`body(${body.codec})`);
296
+ for (const [status, response] of successes) {
297
+ if (response.links) fail("Response links are unsupported", `${at}/responses/${status}/links`, id);
298
+ const value = pickContent(response.content, `${at}/responses/${status}`, "response");
299
+ const typeName = `${prefix}Response${status}`;
300
+ operationTypes.push(`export type ${typeName} = ${value.type};`);
301
+ chain.push(`returns(${status === "200" ? "" : `${status}, `}${value.codec})`);
302
+ }
303
+ if (errors.length) {
304
+ const rendered = errors.map(([status, response]) => {
305
+ if (response.links) fail("Response links are unsupported", `${at}/responses/${status}/links`, id);
306
+ const value = pickContent(response.content, `${at}/responses/${status}`, "response");
307
+ const typeName = `${prefix}Error${pascal(status)}`;
308
+ operationTypes.push(`export type ${typeName} = ${value.type};`);
309
+ return `${JSON.stringify(status)}: ${value.codec}`;
310
+ });
311
+ chain.push(`errors({ ${rendered.join(", ")} })`);
312
+ }
313
+ if (operation.security ?? document.security) chain.push(`security(${JSON.stringify(operation.security ?? document.security)})`);
314
+ descriptors.push(` ${id}: ${chain.join("\n .")},`);
315
+ }
316
+ const imports = [...new Set(descriptors.join(" ").match(/\b(get|post|put|patch|del|head|options|json|text|urlEncoded|multipart|arrayBuffer|empty|content)\b/g) ?? [])].sort();
317
+ const files = {
318
+ "schemas.ts": `${schemaLines.join("\n\n")}\n`,
319
+ "operations.ts": `${names.size ? `import type { ${[...names.keys()].join(", ")} } from "./schemas";\n\n` : ""}${operationTypes.join("\n\n")}\n`,
320
+ "api.ts": `import { ${[
321
+ "defineApi",
322
+ "createClient",
323
+ ...imports
324
+ ].join(", ")} } from "@askrjs/fetch";\nimport type { ClientOptions } from "@askrjs/fetch";\nimport type { ${operationTypes.map((line) => line.match(/^export type (\w+)/)?.[1]).filter(Boolean).join(", ")} } from "./operations";\n\nexport const api = defineApi({\n${descriptors.join("\n")}\n}, ${JSON.stringify({
325
+ servers: (document.servers ?? []).map((s) => s.url),
326
+ securitySchemes: document.components?.securitySchemes ?? {}
327
+ }, null, 2)});\n\nexport const createApiClient = (options?: ClientOptions) => createClient(api, options);\n`,
328
+ "index.ts": `export * from "./schemas";\nexport * from "./operations";\nexport { api, createApiClient } from "./api";\n`,
329
+ ".askr-generated.json": ""
330
+ };
331
+ files[".askr-generated.json"] = `${JSON.stringify({
332
+ version: 1,
333
+ files: [...OWNED]
334
+ }, null, 2)}\n`;
335
+ return files;
336
+ }
337
+ async function existingFiles(directory) {
338
+ try {
339
+ return (await readdir(directory)).sort();
340
+ } catch {
341
+ return [];
342
+ }
343
+ }
344
+ async function writeGenerated(directory, files, check) {
345
+ const output = resolve(directory);
346
+ const outputStat = await stat(output).catch(() => null);
347
+ if (outputStat && !outputStat.isDirectory()) throw new GenerationError(`Generated output must be a directory: ${output}`);
348
+ const entries = await existingFiles(output);
349
+ if (check) {
350
+ if (!entries.length) throw new GenerationError(`Generated directory is missing: ${output}`);
351
+ const expected = Object.keys(files).sort();
352
+ if (JSON.stringify(entries) !== JSON.stringify(expected)) throw new GenerationError(`Generated file ownership differs in ${output}`);
353
+ for (const name of expected) if (await readFile(join(output, name), "utf8") !== files[name]) throw new GenerationError(`Generated file is stale: ${name}`);
354
+ return;
355
+ }
356
+ if (entries.length && !entries.includes(".askr-generated.json")) throw new GenerationError(`Refusing to overwrite non-generated directory: ${output}`);
357
+ const stage = await mkdtemp(join(dirname(output), `.${basename(output)}-stage-`));
358
+ for (const [name, content] of Object.entries(files)) await writeFile(join(stage, name), content);
359
+ const backup = `${output}.backup-${process.pid}`;
360
+ let moved = false;
361
+ try {
362
+ if (entries.length) {
363
+ await rename(output, backup);
364
+ moved = true;
365
+ }
366
+ await rename(stage, output);
367
+ if (moved) await rm(backup, {
368
+ recursive: true,
369
+ force: true
370
+ });
371
+ } catch (error) {
372
+ await rm(output, {
373
+ recursive: true,
374
+ force: true
375
+ });
376
+ if (moved) await rename(backup, output);
377
+ throw error;
378
+ } finally {
379
+ await rm(stage, {
380
+ recursive: true,
381
+ force: true
382
+ });
383
+ }
384
+ }
385
+ async function generate(input, output, check = false) {
386
+ if (!/^https?:\/\//.test(input)) {
387
+ const resolvedInput = resolve(input);
388
+ const inputWithinOutput = relative(resolve(output), resolvedInput);
389
+ if (!inputWithinOutput.startsWith("..") && !isAbsolute(inputWithinOutput)) throw new GenerationError("Generated output must not contain its OpenAPI input document");
390
+ }
391
+ const files = generateFiles(await loadOpenApi(input));
392
+ await mkdir(dirname(resolve(output)), { recursive: true });
393
+ await writeGenerated(output, files, check);
394
+ }
395
+ //#endregion
396
+ //#region src/bin/generate.ts
397
+ async function runGenerateCli(args, io = console) {
398
+ const wantsJson = args.includes("--json");
399
+ let output;
400
+ let check = false;
401
+ let json = false;
402
+ let help = false;
403
+ const inputs = [];
404
+ for (let i = 0; i < args.length; i++) {
405
+ const arg = args[i];
406
+ if (arg === "--check") check = true;
407
+ else if (arg === "--json") json = true;
408
+ else if (arg === "--help" || arg === "-h") help = true;
409
+ else if (arg === "-o" || arg === "--output") {
410
+ const value = args[i + 1];
411
+ if (!value || value.startsWith("-")) {
412
+ const message = `${arg} requires a path`;
413
+ io.error(wantsJson ? JSON.stringify({
414
+ status: "error",
415
+ error: message
416
+ }) : message);
417
+ return 1;
418
+ }
419
+ output = value;
420
+ i += 1;
421
+ } else if (arg.startsWith("--output=")) output = arg.slice(9);
422
+ else if (arg.startsWith("-")) {
423
+ const message = `Unknown option: ${arg}`;
424
+ io.error(wantsJson ? JSON.stringify({
425
+ status: "error",
426
+ error: message
427
+ }) : message);
428
+ return 1;
429
+ } else inputs.push(arg);
430
+ }
431
+ const usage = "Usage: askr generate <input> -o <output-directory> [--check] [--json]";
432
+ if (help) {
433
+ io.log(usage);
434
+ return 0;
435
+ }
436
+ if (inputs.length !== 1 || !output) {
437
+ io.error(json ? JSON.stringify({
438
+ status: "error",
439
+ error: usage
440
+ }) : usage);
441
+ return 1;
442
+ }
443
+ try {
444
+ await generate(inputs[0], output, check);
445
+ io.log(json ? JSON.stringify({
446
+ status: "ok",
447
+ action: check ? "checked" : "generated",
448
+ output
449
+ }) : check ? "Generated client is current." : `Generated client in ${output}.`);
450
+ return 0;
451
+ } catch (error) {
452
+ const message = error instanceof Error ? error.message : String(error);
453
+ io.error(json ? JSON.stringify({
454
+ status: "error",
455
+ error: message
456
+ }) : message);
457
+ return 1;
458
+ }
459
+ }
460
+ //#endregion
461
+ export { runGenerateCli };
@@ -16,5 +16,3 @@ function isDirectExecution(moduleUrl, argvPath = process.argv[1]) {
16
16
  }
17
17
  //#endregion
18
18
  export { isDirectExecution as t };
19
-
20
- //# sourceMappingURL=is-direct-execution-Cdlr-ZUl.js.map
@@ -0,0 +1,25 @@
1
+ //#region src/bin/openapi.d.ts
2
+ type CliIo = Pick<Console, "error" | "log">;
3
+ interface ParsedArgs {
4
+ entry: string;
5
+ output: string;
6
+ check: boolean;
7
+ help: boolean;
8
+ json: boolean;
9
+ }
10
+ interface OpenApiDeps {
11
+ cwd(): string;
12
+ importModule(filePath: string): Promise<unknown>;
13
+ readFile(filePath: string): Promise<string>;
14
+ mkdir(directory: string): Promise<void>;
15
+ writeFile(filePath: string, contents: string): Promise<void>;
16
+ rename(from: string, to: string): Promise<void>;
17
+ rm(filePath: string): Promise<void>;
18
+ temporarySuffix(): string;
19
+ }
20
+ declare function parseOpenApiArgs(args: readonly string[]): ParsedArgs;
21
+ declare function serializeOpenApi(document: unknown): string;
22
+ declare function validateOpenApiDocument(document: unknown): asserts document is Record<string, unknown>;
23
+ declare function runOpenApiCli(args?: string[], io?: CliIo, overrides?: Partial<OpenApiDeps>): Promise<number>;
24
+ //#endregion
25
+ export { parseOpenApiArgs, runOpenApiCli, serializeOpenApi, validateOpenApiDocument };
package/dist/openapi.js CHANGED
@@ -3,8 +3,8 @@ import { t as isDirectExecution } from "./is-direct-execution-Cdlr-ZUl.js";
3
3
  import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { pathToFileURL } from "node:url";
6
+ import { dump } from "js-yaml";
6
7
  import { tsImport } from "tsx/esm/api";
7
- import { stringify } from "yaml";
8
8
  //#region src/bin/openapi.ts
9
9
  const helpText = `askr openapi - Generate an OpenAPI YAML artifact
10
10
 
@@ -15,6 +15,7 @@ Options:
15
15
  --entry <path> TypeScript API module (default: ./src/api.ts)
16
16
  --output <path> YAML artifact (default: ./openapi.yml)
17
17
  --check Fail when the artifact is missing or byte-stale; never write
18
+ --json Print machine-readable command results
18
19
  --help, -h Show help
19
20
  `;
20
21
  const defaultDeps = {
@@ -32,17 +33,19 @@ function parseOpenApiArgs(args) {
32
33
  entry: "./src/api.ts",
33
34
  output: "./openapi.yml",
34
35
  check: false,
35
- help: false
36
+ help: false,
37
+ json: false
36
38
  };
37
39
  for (let index = 0; index < args.length; index += 1) {
38
40
  const value = args[index];
39
41
  if (value === "--entry" || value === "--output") {
40
42
  const next = args[index + 1];
41
- if (!next) throw new Error(`${value} requires a path`);
43
+ if (!next || next.startsWith("-")) throw new Error(`${value} requires a path`);
42
44
  if (value === "--entry") parsed.entry = next;
43
45
  else parsed.output = next;
44
46
  index += 1;
45
47
  } else if (value === "--check") parsed.check = true;
48
+ else if (value === "--json") parsed.json = true;
46
49
  else if (value === "--help" || value === "-h") parsed.help = true;
47
50
  else throw new Error(`Unknown option: ${value}`);
48
51
  }
@@ -56,14 +59,24 @@ function exporterFrom(moduleValue) {
56
59
  return exported;
57
60
  }
58
61
  function serializeOpenApi(document) {
59
- return `${stringify(document, {
60
- aliasDuplicateObjects: false,
61
- lineWidth: 0
62
+ return `${dump(document, {
63
+ lineWidth: -1,
64
+ noCompatMode: true,
65
+ noRefs: true
62
66
  }).replace(/\n+$/, "")}\n`;
63
67
  }
64
- async function generate(entry, deps) {
65
- const document = exporterFrom(await deps.importModule(entry)).toOpenApiDocument();
68
+ function validateOpenApiDocument(document) {
66
69
  if (!document || typeof document !== "object" || Array.isArray(document)) throw new Error("toOpenApiDocument() must return an OpenAPI document object");
70
+ const candidate = document;
71
+ if (typeof candidate.openapi !== "string" || !/^3\.(?:0|1)\.\d+(?:[-+].+)?$/.test(candidate.openapi)) throw new Error("OpenAPI document must declare a supported openapi 3.0.x or 3.1.x version");
72
+ if (!candidate.info || typeof candidate.info !== "object" || Array.isArray(candidate.info)) throw new Error("OpenAPI document must contain an info object");
73
+ const info = candidate.info;
74
+ if (typeof info.title !== "string" || !info.title.trim() || typeof info.version !== "string" || !info.version.trim()) throw new Error("OpenAPI info must contain non-empty title and version strings");
75
+ if (!candidate.paths || typeof candidate.paths !== "object" || Array.isArray(candidate.paths)) throw new Error("OpenAPI document must contain a paths object");
76
+ }
77
+ async function generate(entry, deps) {
78
+ const document = await exporterFrom(await deps.importModule(entry)).toOpenApiDocument();
79
+ validateOpenApiDocument(document);
67
80
  return serializeOpenApi(document);
68
81
  }
69
82
  async function atomicWrite(output, contents, deps) {
@@ -82,6 +95,7 @@ async function runOpenApiCli(args = process.argv.slice(2), io = console, overrid
82
95
  ...defaultDeps,
83
96
  ...overrides
84
97
  };
98
+ const wantsJson = args.includes("--json");
85
99
  try {
86
100
  const parsed = parseOpenApiArgs(args);
87
101
  if (parsed.help) {
@@ -90,6 +104,7 @@ async function runOpenApiCli(args = process.argv.slice(2), io = console, overrid
90
104
  }
91
105
  const entry = path.resolve(deps.cwd(), parsed.entry);
92
106
  const output = path.resolve(deps.cwd(), parsed.output);
107
+ if (entry === output) throw new Error("OpenAPI output must not overwrite its source entry");
93
108
  const expected = await generate(entry, deps);
94
109
  if (parsed.check) {
95
110
  let actual;
@@ -97,23 +112,43 @@ async function runOpenApiCli(args = process.argv.slice(2), io = console, overrid
97
112
  actual = await deps.readFile(output);
98
113
  } catch (error) {
99
114
  if (error.code === "ENOENT") {
100
- io.error(`OpenAPI artifact is missing: ${output}`);
115
+ io.error(parsed.json ? JSON.stringify({
116
+ status: "error",
117
+ reason: "missing",
118
+ output
119
+ }) : `OpenAPI artifact is missing: ${output}`);
101
120
  return 1;
102
121
  }
103
122
  throw error;
104
123
  }
105
124
  if (actual !== expected) {
106
- io.error(`OpenAPI artifact is stale: ${output}`);
125
+ io.error(parsed.json ? JSON.stringify({
126
+ status: "error",
127
+ reason: "stale",
128
+ output
129
+ }) : `OpenAPI artifact is stale: ${output}`);
107
130
  return 1;
108
131
  }
109
- io.log(`OpenAPI artifact is current: ${output}`);
132
+ io.log(parsed.json ? JSON.stringify({
133
+ status: "ok",
134
+ action: "checked",
135
+ output
136
+ }) : `OpenAPI artifact is current: ${output}`);
110
137
  return 0;
111
138
  }
112
139
  await atomicWrite(output, expected, deps);
113
- io.log(`Generated OpenAPI artifact: ${output}`);
140
+ io.log(parsed.json ? JSON.stringify({
141
+ status: "ok",
142
+ action: "generated",
143
+ output
144
+ }) : `Generated OpenAPI artifact: ${output}`);
114
145
  return 0;
115
146
  } catch (error) {
116
- io.error(error instanceof Error ? error.message : String(error));
147
+ const message = error instanceof Error ? error.message : String(error);
148
+ io.error(wantsJson ? JSON.stringify({
149
+ status: "error",
150
+ error: message
151
+ }) : message);
117
152
  return 1;
118
153
  }
119
154
  }
@@ -122,6 +157,4 @@ async function main() {
122
157
  }
123
158
  if (isDirectExecution(import.meta.url)) main();
124
159
  //#endregion
125
- export { parseOpenApiArgs, runOpenApiCli, serializeOpenApi };
126
-
127
- //# sourceMappingURL=openapi.js.map
160
+ export { parseOpenApiArgs, runOpenApiCli, serializeOpenApi, validateOpenApiDocument };