@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
package/README.md CHANGED
@@ -43,11 +43,50 @@ unless you opt out with `--no-skills`.
43
43
  - `askr skills sync [--cwd <dir>]`
44
44
  - `askr ssg --config <path> --output <dir> [--incremental]`
45
45
  - `askr openapi [--entry ./src/api.ts] [--output ./openapi.yml] [--check]`
46
- - `askr update [packages...] [-u] [-f] [--workspace <glob>] [--tag <tag>] [--json]`
46
+ - `askr outdated [packages...] [--workspace <glob>] [--tag <tag>] [--json]`
47
+ - `askr update [packages...] [--workspace <glob>] [--tag <tag>] [--json]`
48
+ - `askr upgrade [packages...] [--workspace <glob>] [--tag <tag>] [--json]`
47
49
 
48
50
  The installed command is `askr`. Subcommands are intentionally not published
49
51
  as compatibility binaries.
50
52
 
53
+ ## Static sitemap generation
54
+
55
+ `askr ssg` writes `sitemap.xml` from the concrete routes generated during the
56
+ build. Set the canonical deploy URL in the TypeScript config:
57
+
58
+ ```ts
59
+ export const staticConfig = {
60
+ registry,
61
+ outputDir: "./dist",
62
+ siteUrl: "https://example.com",
63
+ sitemap: {
64
+ defaults: { changeFrequency: "weekly" },
65
+ routes: {
66
+ "/404": false,
67
+ "/docs": { priority: 0.8, lastModified: "2026-07-18" },
68
+ },
69
+ },
70
+ };
71
+ ```
72
+
73
+ The sitemap config types are available from `@askrjs/cli/ssg` for projects that
74
+ want an explicit annotation.
75
+
76
+ Successful and incrementally skipped concrete routes are included by default.
77
+ Failed routes and wildcard templates are excluded. Route settings support a
78
+ canonical URL override, `lastModified`, `changeFrequency`, `priority`, and
79
+ `hreflang` alternates. Use `resolve(route)` for data-driven metadata or return
80
+ `false` to exclude a route. Set `sitemap: false` for an intentional opt-out.
81
+
82
+ The CLI validates real calendar dates, bounds asynchronous metadata resolution,
83
+ and enforces the sitemap protocol limits of 50,000 URLs and 50 MB per file.
84
+ Larger sites receive deterministic sitemap chunks plus a sitemap index. It also
85
+ creates or updates `robots.txt` while preserving unrelated directives and tracks
86
+ owned files so stale chunks and previous output paths are removed. Full and
87
+ incremental builds publish through a sibling staging directory, so route output,
88
+ metadata, assets, and sitemap artifacts change together or not at all.
89
+
51
90
  ## OpenAPI artifacts
52
91
 
53
92
  `askr openapi` loads a TypeScript module whose default export exposes
@@ -69,24 +108,24 @@ registers it in route-first SPA branches (`src/pages/app/_routes.tsx` or
69
108
 
70
109
  ## Dependency updates
71
110
 
72
- `askr update` is an Askr-owned, dry-run-first dependency updater. It discovers
73
- the containing npm or pnpm workspace from nested directories, includes the root
111
+ The dependency commands are Askr-owned and discover
112
+ the containing npm or pnpm workspace from nested directories, include the root
74
113
  manifest and declared workspaces, and reports safe, breaking, local, manual,
75
114
  and failed decisions without running an install.
76
115
 
77
116
  ```bash
117
+ askr outdated
78
118
  askr update
79
- askr update -u
80
- askr update -u -f
119
+ askr upgrade
81
120
  askr update vite "@types/*"
82
121
  askr update --workspace "@scope/app" --tag next --json
83
122
  ```
84
123
 
85
- `-u` writes compatible range changes. `-u -f` additionally permits breaking
124
+ `askr update` writes safe range changes. `askr upgrade` additionally permits next-version
86
125
  major changes for stable packages and breaking minor changes for `0.x`
87
126
  packages. Peer requirements from co-dependencies are still enforced: a target
88
127
  is left for manual review if it would move outside another selected package's
89
- published peer range. Force never bypasses that compatibility guard.
128
+ published peer range. `askr upgrade` never bypasses that compatibility guard.
90
129
 
91
130
  The updater preserves exact, caret, tilde, and x-range styles. It can widen one
92
131
  bounded interval, and it updates only the highest clause of a simple OR union.
package/dist/add.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ //#region src/file-changes.d.ts
2
+ interface FileChange {
3
+ readonly filePath: string;
4
+ readonly content: string;
5
+ }
6
+ interface FileChangeWriterOptions {
7
+ readonly replace?: (temporaryPath: string, filePath: string) => Promise<void>;
8
+ }
9
+ declare function writeFileChanges(changes: readonly FileChange[], options?: FileChangeWriterOptions): Promise<void>;
10
+ //#endregion
11
+ //#region src/bin/add.d.ts
12
+ type CliIo = Pick<Console, "error" | "log">;
13
+ type WriteChanges = typeof writeFileChanges;
14
+ declare function runAddCli(args?: string[], io?: CliIo, writeChanges?: WriteChanges): Promise<number>;
15
+ //#endregion
16
+ export { runAddCli };
package/dist/add.js CHANGED
@@ -1,3 +1,560 @@
1
1
  #!/usr/bin/env node
2
- import { t as runAddCli } from "./add-DqhqRzBQ.js";
2
+ import { t as isDirectExecution } from "./is-direct-execution-Cdlr-ZUl.js";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { randomUUID } from "node:crypto";
6
+ //#region src/file-changes.ts
7
+ async function remove(paths) {
8
+ await Promise.all(paths.map((filePath) => fs.rm(filePath, { force: true }).catch(() => void 0)));
9
+ }
10
+ async function restore(changes) {
11
+ let complete = true;
12
+ for (const change of [...changes].reverse()) try {
13
+ if (change.original === null) {
14
+ await fs.rm(change.filePath, { force: true });
15
+ continue;
16
+ }
17
+ const rollbackPath = path.join(path.dirname(change.filePath), `.${path.basename(change.filePath)}.askr-rollback-${randomUUID()}`);
18
+ await fs.writeFile(rollbackPath, change.original, {
19
+ flag: "wx",
20
+ mode: change.mode
21
+ });
22
+ await fs.rename(rollbackPath, change.filePath);
23
+ } catch {
24
+ complete = false;
25
+ }
26
+ return complete;
27
+ }
28
+ async function writeFileChanges(changes, options = {}) {
29
+ const ordered = [...changes].sort((left, right) => left.filePath.localeCompare(right.filePath));
30
+ if (new Set(ordered.map((change) => change.filePath)).size !== ordered.length) throw new Error("File changes contain duplicate target paths.");
31
+ const replace = options.replace ?? fs.rename;
32
+ const staged = [];
33
+ try {
34
+ for (const change of ordered) {
35
+ await fs.mkdir(path.dirname(change.filePath), { recursive: true });
36
+ const stat = await fs.stat(change.filePath).catch(() => null);
37
+ const original = stat ? await fs.readFile(change.filePath) : null;
38
+ const temporaryPath = path.join(path.dirname(change.filePath), `.${path.basename(change.filePath)}.askr-change-${randomUUID()}`);
39
+ const mode = stat?.mode ?? 420;
40
+ await fs.writeFile(temporaryPath, change.content, {
41
+ flag: "wx",
42
+ mode
43
+ });
44
+ staged.push({
45
+ ...change,
46
+ original,
47
+ mode,
48
+ temporaryPath
49
+ });
50
+ }
51
+ } catch (error) {
52
+ await remove(staged.map((change) => change.temporaryPath));
53
+ throw error;
54
+ }
55
+ const replaced = [];
56
+ try {
57
+ for (const change of staged) {
58
+ await replace(change.temporaryPath, change.filePath);
59
+ replaced.push(change);
60
+ }
61
+ } catch {
62
+ const complete = await restore(replaced);
63
+ await remove(staged.map((change) => change.temporaryPath));
64
+ throw new Error(complete ? "File replacement failed; completed changes were rolled back." : "File replacement failed and rollback was incomplete.");
65
+ }
66
+ }
67
+ //#endregion
68
+ //#region src/bin/add.ts
69
+ const BRANCH_CONFIG = {
70
+ app: {
71
+ badge: "app route",
72
+ pagesDir: [
73
+ "src",
74
+ "pages",
75
+ "app"
76
+ ],
77
+ routesFile: [
78
+ "src",
79
+ "pages",
80
+ "app",
81
+ "_routes.tsx"
82
+ ]
83
+ },
84
+ public: {
85
+ badge: "public route",
86
+ pagesDir: [
87
+ "src",
88
+ "pages",
89
+ "public"
90
+ ],
91
+ routesFile: [
92
+ "src",
93
+ "pages",
94
+ "public",
95
+ "_routes.tsx"
96
+ ]
97
+ }
98
+ };
99
+ function helpText() {
100
+ return [
101
+ "askr add - Generate code into an existing Askr project",
102
+ "",
103
+ "Usage:",
104
+ " askr add page <name> [--branch app|public] [--cwd <dir>] [--title <title>] [--route <path>] [--force]",
105
+ " askr add action <name> --route <path> [--cwd <dir>] [--force]",
106
+ "",
107
+ "Commands:",
108
+ " page Scaffold a route page and register it in a route-first SPA branch",
109
+ " action Scaffold a browser descriptor, server handler, registration, authorization, and test",
110
+ "",
111
+ "Options:",
112
+ " --branch <name> Route branch to target (default: app)",
113
+ " --cwd <dir> Project directory (default: current directory)",
114
+ " --title <text> Override the generated page title",
115
+ " --route <path> Override the generated route path",
116
+ " --force Overwrite an existing page file",
117
+ " --help Show this help message",
118
+ "",
119
+ "Examples:",
120
+ " askr add page audit-log",
121
+ " askr add page ops/audit-log --branch public",
122
+ " askr add page approvals --title \"Human approvals\" --route /app/approvals",
123
+ " askr add action approve-request --route /requests/{id}",
124
+ "",
125
+ "Notes:",
126
+ " Page generation supports route-first SPA projects created from `askr create spa`.",
127
+ " Action generation supports projects created from `askr create full-stack`."
128
+ ].join("\n");
129
+ }
130
+ function parseArgs(args) {
131
+ const positional = [];
132
+ const parsed = {
133
+ branch: "app",
134
+ cwd: process.cwd(),
135
+ force: false,
136
+ help: false,
137
+ routePath: "",
138
+ title: "",
139
+ errors: []
140
+ };
141
+ for (let index = 0; index < args.length; index += 1) {
142
+ const arg = args[index];
143
+ if (arg === "--help" || arg === "-h") parsed.help = true;
144
+ else if (arg === "--force") parsed.force = true;
145
+ else if (arg === "--branch") if (index + 1 >= args.length || args[index + 1].startsWith("-")) parsed.errors.push("Missing value for --branch");
146
+ else {
147
+ parsed.branch = args[index + 1];
148
+ index += 1;
149
+ }
150
+ else if (arg === "--cwd") if (index + 1 >= args.length || args[index + 1].startsWith("-")) parsed.errors.push("Missing value for --cwd");
151
+ else {
152
+ parsed.cwd = args[index + 1];
153
+ index += 1;
154
+ }
155
+ else if (arg === "--title") if (index + 1 >= args.length || args[index + 1].startsWith("-")) parsed.errors.push("Missing value for --title");
156
+ else {
157
+ parsed.title = args[index + 1].trim();
158
+ index += 1;
159
+ }
160
+ else if (arg === "--route") if (index + 1 >= args.length || args[index + 1].startsWith("-")) parsed.errors.push("Missing value for --route");
161
+ else {
162
+ parsed.routePath = args[index + 1].trim();
163
+ index += 1;
164
+ }
165
+ else if (arg.startsWith("-")) parsed.errors.push(`Unknown option: ${arg}`);
166
+ else positional.push(arg);
167
+ }
168
+ return {
169
+ ...parsed,
170
+ command: positional[0] || "",
171
+ name: positional[1] || "",
172
+ positionals: positional
173
+ };
174
+ }
175
+ async function pathExists(filePath) {
176
+ return Boolean(await fs.stat(filePath).catch(() => null));
177
+ }
178
+ function slugifySegment(value) {
179
+ return value.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
180
+ }
181
+ function normalizePageSegments(input) {
182
+ const segments = input.replace(/^\/+|\/+$/g, "").split("/").map((segment) => slugifySegment(segment.trim())).filter(Boolean);
183
+ if (segments.length === 0 || segments.some((segment) => segment.startsWith("_"))) return [];
184
+ return segments;
185
+ }
186
+ function toWords(segments) {
187
+ return segments.flatMap((segment) => segment.split("-").filter(Boolean));
188
+ }
189
+ function toTitleCase(words) {
190
+ return words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
191
+ }
192
+ function buildComponentName(segments) {
193
+ return `${toWords(segments).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("")}Page`;
194
+ }
195
+ function buildIdentifier(segments) {
196
+ const [first = "action", ...rest] = toWords(segments);
197
+ return [first.toLowerCase(), ...rest.map((word) => word.charAt(0).toUpperCase() + word.slice(1))].join("").replace(/^[^a-z_$]/i, "action");
198
+ }
199
+ function renderAuthorizationRegistry(actions) {
200
+ const imports = actions.map((action) => `import { ${action.descriptorName} } from './actions/${action.slug}.js';`);
201
+ const routes = /* @__PURE__ */ new Map();
202
+ for (const action of actions) {
203
+ const entries = routes.get(action.routePath) ?? [];
204
+ entries.push(action);
205
+ routes.set(action.routePath, entries);
206
+ }
207
+ const routeLines = [...routes].sort(([left], [right]) => left.localeCompare(right)).map(([routePath, entries]) => ` ${JSON.stringify(routePath)}: [${entries.map((entry) => entry.descriptorName).join(", ")}],`);
208
+ return [
209
+ "import type { ActionDescriptor } from '@askrjs/askr/actions';",
210
+ ...imports,
211
+ "",
212
+ "const actionsByRoute: Readonly<Record<string, readonly ActionDescriptor[]>> = Object.freeze({",
213
+ ...routeLines,
214
+ "});",
215
+ "",
216
+ "export function actionsFor(path: string): readonly ActionDescriptor[] {",
217
+ " return actionsByRoute[path] ?? [];",
218
+ "}",
219
+ ""
220
+ ].join("\n");
221
+ }
222
+ function renderServerActionRegistry(actions) {
223
+ return [
224
+ "import { defineServerActions, handleAction } from '@askrjs/server/askr';",
225
+ ...actions.map((action) => `import { ${action.descriptorName} } from '../actions/${action.slug}.js';`),
226
+ ...actions.map((action) => `import { ${action.handlerName} } from './actions/${action.slug}.js';`),
227
+ "import type { AppDependencies } from './dependencies.js';",
228
+ "",
229
+ "export function createActions(deps: AppDependencies) {",
230
+ " return defineServerActions({ dependencies: deps,",
231
+ " csrf: { secret: process.env.CSRF_SECRET ?? 'development-only-secret' },",
232
+ " },",
233
+ ...actions.map((action) => ` handleAction(${action.descriptorName}, ${action.handlerName}),`),
234
+ " );",
235
+ "}",
236
+ ""
237
+ ].join("\n");
238
+ }
239
+ async function discoverDeclaredActions(projectRoot, replacement) {
240
+ const actionsDir = path.join(projectRoot, "src", "actions");
241
+ const names = await fs.readdir(actionsDir).catch(() => []);
242
+ const files = new Set(names.filter((name) => name.endsWith(".ts")).map((name) => path.join(actionsDir, name)));
243
+ files.add(replacement.filePath);
244
+ const actions = [];
245
+ for (const filePath of [...files].sort()) {
246
+ const content = filePath === replacement.filePath ? replacement.content : await fs.readFile(filePath, "utf8");
247
+ const descriptor = /export const (\w+Action) = defineAction\(/.exec(content)?.[1];
248
+ if (!descriptor) continue;
249
+ const routeName = `${descriptor}Route`;
250
+ const routeMatch = new RegExp(`export const ${routeName} = (['"])(.*?)\\1;`).exec(content);
251
+ if (!routeMatch) throw new Error(`Action descriptor ${path.basename(filePath)} must declare ${routeName}.`);
252
+ const slug = path.basename(filePath, ".ts");
253
+ actions.push({
254
+ slug,
255
+ descriptorName: descriptor,
256
+ handlerName: buildIdentifier([slug]),
257
+ routePath: routeMatch[2]
258
+ });
259
+ }
260
+ return actions.sort((left, right) => left.slug.localeCompare(right.slug));
261
+ }
262
+ function renderActionDescriptor(options) {
263
+ return [
264
+ "import { defineAction } from '@askrjs/askr/actions';",
265
+ "import { schema } from '@askrjs/schema';",
266
+ "",
267
+ `export const ${options.descriptorName}Route = ${JSON.stringify(options.routePath)};`,
268
+ "",
269
+ `export const ${options.descriptorName} = defineAction({`,
270
+ ` id: '${options.slug}',`,
271
+ " input: schema.object({ value: schema.string({ minLength: 1 }) }),",
272
+ " invalidates: [],",
273
+ "});",
274
+ ""
275
+ ].join("\n");
276
+ }
277
+ function renderActionHandler(options) {
278
+ return [
279
+ "import type { InferSchema } from '@askrjs/schema';",
280
+ `import { ${options.descriptorName} } from '../../actions/${options.slug}.js';`,
281
+ "import type { AppDependencies } from '../dependencies.js';",
282
+ "",
283
+ `type Input = InferSchema<typeof ${options.descriptorName}.input>;`,
284
+ "",
285
+ `export async function ${options.handlerName}(`,
286
+ " _context: unknown,",
287
+ " input: Input,",
288
+ " deps: Pick<AppDependencies, 'actions'>,",
289
+ ") {",
290
+ " await deps.actions.record({ action: " + JSON.stringify(options.slug) + ", value: input.value });",
291
+ " return { result: { accepted: true } };",
292
+ "}",
293
+ ""
294
+ ].join("\n");
295
+ }
296
+ function renderActionTest(options) {
297
+ return [
298
+ "import { describe, expect, it } from 'vitest';",
299
+ `import { ${options.descriptorName} } from '../../src/actions/${options.slug}.js';`,
300
+ "",
301
+ `describe('${options.slug} action', () => {`,
302
+ " it('should expose a browser-safe executable input contract', () => {",
303
+ ` expect(${options.descriptorName}.id).toBe('${options.slug}');`,
304
+ ` expect(${options.descriptorName}.input.safeParse({ value: 'example' }).success).toBe(true);`,
305
+ ` expect(${options.descriptorName}.input.safeParse({ value: '' }).success).toBe(false);`,
306
+ " });",
307
+ "});",
308
+ ""
309
+ ].join("\n");
310
+ }
311
+ function buildRoutePath(branch, segments, overridePath = "") {
312
+ if (!overridePath) {
313
+ const suffix = segments.join("/");
314
+ return branch === "app" ? `/app/${suffix}` : `/${suffix}`;
315
+ }
316
+ const normalized = overridePath.startsWith("/") ? overridePath : `/${overridePath}`;
317
+ if (branch === "app") return normalized === "/app" || normalized.startsWith("/app/") ? normalized : `/app${normalized}`;
318
+ if (normalized === "/app" || normalized.startsWith("/app/")) throw new Error("Public branch routes cannot live under /app.");
319
+ return normalized;
320
+ }
321
+ function toImportSpecifier(fromFile, toFile) {
322
+ const withoutExtension = path.relative(path.dirname(fromFile), toFile).replace(/\\/g, "/").replace(/\.tsx$/i, "");
323
+ return withoutExtension.startsWith(".") ? withoutExtension : `./${withoutExtension}`;
324
+ }
325
+ function createUpdatedRouteFile(routeFileContent, options) {
326
+ const normalized = routeFileContent.replace(/\r\n/g, "\n");
327
+ const importLine = `import ${options.componentName} from '${options.importSpecifier}';`;
328
+ if (normalized.includes(importLine) || normalized.includes(`route('${options.routePath}',`) || normalized.includes(`route("${options.routePath}",`)) throw new Error(`Route '${options.routePath}' is already registered.`);
329
+ const importMarker = "// askr:imports";
330
+ const routeMarker = " // askr:routes";
331
+ if (normalized.split(importMarker).length !== 2 || normalized.split(routeMarker).length !== 2) throw new Error("Unsupported route file: expected exactly one // askr:imports and // askr:routes marker.");
332
+ return `${normalized.replace(importMarker, `${importMarker}\n${importLine}`).replace(routeMarker, `${routeMarker}\n route('${options.routePath}', ${options.componentName});`).replace(/\n+$/, "")}\n`;
333
+ }
334
+ function renderPageFile(options) {
335
+ return [
336
+ "import {",
337
+ " Badge,",
338
+ " Card,",
339
+ " CardContent,",
340
+ " CardDescription,",
341
+ " CardHeader,",
342
+ " CardTitle,",
343
+ "} from '@askrjs/themes/components';",
344
+ "import { Stack } from '@askrjs/themes/components';",
345
+ "",
346
+ `export default function ${options.componentName}() {`,
347
+ " return (",
348
+ " <Stack gap=\"5\">",
349
+ " <section class=\"page-heading\">",
350
+ " <Stack gap=\"2\">",
351
+ ` <Badge>${options.badge}</Badge>`,
352
+ ` <h1>${options.title}</h1>`,
353
+ " <p class=\"lead\">",
354
+ ` Route-first scaffold for ${options.routePath}. Keep adapters, queries, and mutations outside the page as this surface grows.`,
355
+ " </p>",
356
+ " </Stack>",
357
+ " </section>",
358
+ "",
359
+ " <Card>",
360
+ " <CardHeader>",
361
+ " <CardTitle>Next step</CardTitle>",
362
+ " <CardDescription>",
363
+ " Move view logic into feature folders and keep loading, empty, error, and success states explicit.",
364
+ " </CardDescription>",
365
+ " </CardHeader>",
366
+ " <CardContent>",
367
+ " <Stack gap=\"3\">",
368
+ " <p>Start with a focused feature boundary instead of inline data fetching.</p>",
369
+ " <p>Prefer resource(), derive(), and For once the page takes on real state.</p>",
370
+ " </Stack>",
371
+ " </CardContent>",
372
+ " </Card>",
373
+ " </Stack>",
374
+ " );",
375
+ "}",
376
+ ""
377
+ ].join("\n");
378
+ }
379
+ async function addPage(parsed, io, writeChanges) {
380
+ if (!parsed.name) {
381
+ io.error("Page name is required.");
382
+ return 1;
383
+ }
384
+ const branch = parsed.branch.toLowerCase();
385
+ const branchConfig = BRANCH_CONFIG[branch];
386
+ if (!branchConfig) {
387
+ io.error(`Unsupported branch '${parsed.branch}'. Use app or public.`);
388
+ return 1;
389
+ }
390
+ const segments = normalizePageSegments(parsed.name);
391
+ if (segments.length === 0) {
392
+ io.error("Page name must contain at least one non-empty path segment.");
393
+ return 1;
394
+ }
395
+ const projectRoot = path.resolve(parsed.cwd);
396
+ const routesFile = path.join(projectRoot, ...branchConfig.routesFile);
397
+ if (!await pathExists(routesFile)) {
398
+ io.error(`Unsupported project layout. Expected route file: ${routesFile}`);
399
+ io.error("askr add page currently supports route-first SPA projects created by `askr create spa`.");
400
+ return 1;
401
+ }
402
+ const pageFile = path.join(projectRoot, ...branchConfig.pagesDir, ...segments).concat(".tsx");
403
+ if (await pathExists(pageFile) && !parsed.force) {
404
+ io.error(`Page file already exists: ${pageFile}`);
405
+ io.error("Pass --force to overwrite the existing file.");
406
+ return 1;
407
+ }
408
+ const title = parsed.title || toTitleCase(toWords(segments));
409
+ const componentName = buildComponentName(segments);
410
+ let routePath = "";
411
+ try {
412
+ routePath = buildRoutePath(branch, segments, parsed.routePath);
413
+ } catch (error) {
414
+ io.error(error instanceof Error ? error.message : String(error));
415
+ return 1;
416
+ }
417
+ const importSpecifier = toImportSpecifier(routesFile, pageFile);
418
+ let updatedRoutes = "";
419
+ try {
420
+ updatedRoutes = createUpdatedRouteFile(await fs.readFile(routesFile, "utf8"), {
421
+ componentName,
422
+ importSpecifier,
423
+ routePath
424
+ });
425
+ } catch (error) {
426
+ io.error(error instanceof Error ? error.message : String(error));
427
+ return 1;
428
+ }
429
+ try {
430
+ await writeChanges([{
431
+ filePath: pageFile,
432
+ content: renderPageFile({
433
+ badge: branchConfig.badge,
434
+ componentName,
435
+ routePath,
436
+ title
437
+ })
438
+ }, {
439
+ filePath: routesFile,
440
+ content: updatedRoutes
441
+ }]);
442
+ } catch (error) {
443
+ io.error("Failed to write generated page artifacts.");
444
+ io.error(error instanceof Error ? error.message : String(error));
445
+ return 1;
446
+ }
447
+ io.log(`Added ${branch} page '${title}'.`);
448
+ io.log(` File: ${path.relative(projectRoot, pageFile).replace(/\\/g, "/")}`);
449
+ io.log(` Route: ${routePath}`);
450
+ io.log(` Router: ${path.relative(projectRoot, routesFile).replace(/\\/g, "/")}`);
451
+ return 0;
452
+ }
453
+ async function addAction(parsed, io, writeChanges) {
454
+ if (!parsed.name) {
455
+ io.error("Action name is required.");
456
+ return 1;
457
+ }
458
+ if (!parsed.routePath || !parsed.routePath.startsWith("/")) {
459
+ io.error("Action generation requires an absolute --route path.");
460
+ return 1;
461
+ }
462
+ const segments = normalizePageSegments(parsed.name);
463
+ if (segments.length === 0) {
464
+ io.error("Action name must contain at least one non-empty path segment.");
465
+ return 1;
466
+ }
467
+ const projectRoot = path.resolve(parsed.cwd);
468
+ const slug = segments.join("-");
469
+ const handlerName = buildIdentifier(segments);
470
+ const descriptorName = `${handlerName}Action`;
471
+ const descriptorFile = path.join(projectRoot, "src", "actions", `${slug}.ts`);
472
+ const handlerFile = path.join(projectRoot, "src", "server", "actions", `${slug}.ts`);
473
+ const registryFile = path.join(projectRoot, "src", "server", "action-registry.ts");
474
+ const authorizationFile = path.join(projectRoot, "src", "action-authorizations.ts");
475
+ const testFile = path.join(projectRoot, "tests", "actions", `${slug}.test.ts`);
476
+ if (!await pathExists(registryFile) || !await pathExists(authorizationFile)) {
477
+ io.error("Unsupported project layout. Expected an `askr create full-stack` project.");
478
+ return 1;
479
+ }
480
+ if (!parsed.force && (await pathExists(descriptorFile) || await pathExists(handlerFile))) {
481
+ io.error(`Action '${slug}' already exists. Pass --force to replace its generated files.`);
482
+ return 1;
483
+ }
484
+ try {
485
+ const descriptor = renderActionDescriptor({
486
+ descriptorName,
487
+ routePath: parsed.routePath,
488
+ slug
489
+ });
490
+ const actions = await discoverDeclaredActions(projectRoot, {
491
+ filePath: descriptorFile,
492
+ content: descriptor
493
+ });
494
+ await writeChanges([
495
+ {
496
+ filePath: descriptorFile,
497
+ content: descriptor
498
+ },
499
+ {
500
+ filePath: handlerFile,
501
+ content: renderActionHandler({
502
+ descriptorName,
503
+ handlerName,
504
+ slug
505
+ })
506
+ },
507
+ {
508
+ filePath: testFile,
509
+ content: renderActionTest({
510
+ descriptorName,
511
+ slug
512
+ })
513
+ },
514
+ {
515
+ filePath: registryFile,
516
+ content: renderServerActionRegistry(actions)
517
+ },
518
+ {
519
+ filePath: authorizationFile,
520
+ content: renderAuthorizationRegistry(actions)
521
+ }
522
+ ]);
523
+ } catch (error) {
524
+ io.error("Failed to write generated action artifacts.");
525
+ io.error(error instanceof Error ? error.message : String(error));
526
+ return 1;
527
+ }
528
+ io.log(`Added action '${slug}' for ${parsed.routePath}.`);
529
+ io.log(` Descriptor: ${path.relative(projectRoot, descriptorFile).replace(/\\/g, "/")}`);
530
+ io.log(` Handler: ${path.relative(projectRoot, handlerFile).replace(/\\/g, "/")}`);
531
+ io.log(` Test: ${path.relative(projectRoot, testFile).replace(/\\/g, "/")}`);
532
+ return 0;
533
+ }
534
+ async function runAddCli(args = process.argv.slice(2), io = console, writeChanges = writeFileChanges) {
535
+ const parsed = parseArgs(args);
536
+ if (parsed.errors.length > 0) {
537
+ for (const message of parsed.errors) io.error(message);
538
+ return 1;
539
+ }
540
+ if (!parsed.command || parsed.help) {
541
+ io.log(helpText());
542
+ return 0;
543
+ }
544
+ if (parsed.positionals.length !== 2 || !parsed.name) {
545
+ io.error("A command and exactly one generator name are required.");
546
+ return 1;
547
+ }
548
+ if (parsed.command === "page") return addPage(parsed, io, writeChanges);
549
+ if (parsed.command === "action") return addAction(parsed, io, writeChanges);
550
+ io.error(`Unknown add command: ${parsed.command}`);
551
+ io.error("Run `askr add --help` to see available commands.");
552
+ return 1;
553
+ }
554
+ async function main() {
555
+ const code = await runAddCli(process.argv.slice(2));
556
+ process.exit(code);
557
+ }
558
+ if (isDirectExecution(import.meta.url)) main();
559
+ //#endregion
3
560
  export { runAddCli };
package/dist/cli.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ //#region src/bin/cli.d.ts
2
+ type CliIo = Pick<Console, "error" | "log">;
3
+ declare function runCli(args?: string[], io?: CliIo): Promise<number>;
4
+ //#endregion
5
+ export { runCli };