@astryxdesign/cli 0.6.4-canary.f0355e3 → 0.6.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 (331) hide show
  1. package/README.md +96 -99
  2. package/api/build/build.doc.mjs +1 -6
  3. package/api/build/build.test.mjs +0 -22
  4. package/api/build/kit/kit.mjs +5 -44
  5. package/api/component/_adapter.d.mts +0 -25
  6. package/api/component/_adapter.mjs +5 -59
  7. package/api/component/component.d.mts +3 -6
  8. package/api/component/component.doc.mjs +17 -37
  9. package/api/component/component.mjs +9 -249
  10. package/api/component/component.type.d.mts +0 -25
  11. package/api/component/component.type.mjs +0 -44
  12. package/api/discover/_adapter.d.mts +6 -114
  13. package/api/discover/_adapter.mjs +17 -372
  14. package/api/discover/detail/detail.d.mts +6 -18
  15. package/api/discover/detail/detail.mjs +13 -67
  16. package/api/discover/detail/detail.test.mjs +0 -85
  17. package/api/discover/discover.d.mts +9 -3
  18. package/api/discover/discover.doc.mjs +18 -61
  19. package/api/discover/discover.mjs +36 -220
  20. package/api/discover/discover.test.mjs +2 -11
  21. package/api/discover/discover.type.d.mts +8 -147
  22. package/api/discover/discover.type.mjs +12 -102
  23. package/api/discover/list/list.d.mts +6 -20
  24. package/api/discover/list/list.mjs +12 -45
  25. package/api/discover/list/list.test.mjs +0 -46
  26. package/api/discover/search/search.d.mts +16 -18
  27. package/api/discover/search/search.mjs +56 -102
  28. package/api/discover/search/search.test.mjs +10 -144
  29. package/api/docs/_adapter.d.mts +3 -8
  30. package/api/docs/_adapter.mjs +6 -14
  31. package/api/docs/docOverlays.test.mjs +1 -27
  32. package/api/docs/docs.doc.mjs +2 -2
  33. package/api/docs/docs.test.mjs +243 -0
  34. package/api/docs/integration-tree.test.mjs +555 -0
  35. package/api/docs/integrationDocs.test.mjs +314 -0
  36. package/api/doctor/doctor.d.mts +3 -8
  37. package/api/doctor/doctor.doc.mjs +8 -17
  38. package/api/doctor/doctor.mjs +9 -90
  39. package/api/doctor/doctor.test.mjs +10 -122
  40. package/api/doctor/doctor.type.d.mts +1 -1
  41. package/api/doctor/doctor.type.mjs +1 -1
  42. package/api/gap-report/gap-report.doc.mjs +10 -19
  43. package/api/hook/hook.doc.mjs +3 -6
  44. package/api/index.d.mts +2 -1
  45. package/api/index.mjs +5 -5
  46. package/api/init/init.doc.mjs +12 -17
  47. package/api/integration/add-helpers.d.mts +2 -5
  48. package/api/integration/add-helpers.mjs +9 -36
  49. package/api/integration/add-theme.mjs +1 -22
  50. package/api/integration/add-theme.test.mjs +0 -34
  51. package/api/integration/authoring-checks.mjs +2 -2
  52. package/api/integration/integrationPackCheck.doc.mjs +3 -3
  53. package/api/integration/pack-check.mjs +9 -82
  54. package/api/integration/pack-check.test.mjs +0 -90
  55. package/api/integration/pack-check.type.mjs +1 -1
  56. package/api/json/assertResponse.doc.mjs +1 -1
  57. package/api/json/index.ts +1 -0
  58. package/api/json/isError.doc.mjs +1 -1
  59. package/api/layout/_adapter.d.mts +34 -0
  60. package/api/layout/_adapter.mjs +148 -0
  61. package/api/layout/check/check.d.mts +16 -0
  62. package/api/layout/check/check.mjs +40 -0
  63. package/api/layout/expand/expand.d.mts +22 -0
  64. package/api/layout/expand/expand.mjs +155 -0
  65. package/api/layout/expand/expand.path-safety.test.mjs +53 -0
  66. package/api/layout/grammar/grammar.d.mts +13 -0
  67. package/api/layout/grammar/grammar.mjs +87 -0
  68. package/api/layout/layout.d.mts +6 -0
  69. package/api/layout/layout.mjs +17 -0
  70. package/api/layout/layout.test.mjs +297 -0
  71. package/api/layout/layout.type.d.mts +89 -0
  72. package/api/layout/layout.type.mjs +103 -0
  73. package/api/layout/layoutCheck.doc.d.mts +11 -0
  74. package/api/layout/layoutCheck.doc.mjs +85 -0
  75. package/api/layout/layoutExpand.doc.d.mts +11 -0
  76. package/api/layout/layoutExpand.doc.mjs +107 -0
  77. package/api/layout/layoutGrammar.doc.d.mts +11 -0
  78. package/api/layout/layoutGrammar.doc.mjs +57 -0
  79. package/api/search/search.d.mts +1 -27
  80. package/api/search/search.doc.mjs +2 -2
  81. package/api/search/search.mjs +16 -228
  82. package/api/search/search.test.mjs +512 -0
  83. package/api/swizzle/swizzle.doc.mjs +5 -7
  84. package/api/template/copy/copy.mjs +1 -1
  85. package/api/template/copy/copy.test.mjs +0 -9
  86. package/api/template/template-integration.test.mjs +65 -1
  87. package/api/template/template.doc.mjs +1 -2
  88. package/api/template/template.mjs +1 -1
  89. package/api/theme/add/add.mjs +25 -17
  90. package/api/theme/add/add.staging.test.mjs +23 -40
  91. package/api/theme/build/build.family.test.mjs +12 -7
  92. package/api/theme/build/build.mjs +18 -8
  93. package/api/theme/generateTonalPalette.doc.mjs +2 -1
  94. package/api/theme/listThemes.doc.mjs +1 -1
  95. package/api/theme/themeAdd.doc.mjs +10 -9
  96. package/api/theme/themeBuild.doc.mjs +13 -13
  97. package/api/theme/themeList.doc.mjs +1 -1
  98. package/api/theme/themeListAvailable.doc.mjs +1 -2
  99. package/api/theme/themePaletteGenerate.doc.mjs +8 -15
  100. package/api/theme/themeTargets.doc.mjs +2 -3
  101. package/api/theme/themeTemplate.doc.mjs +1 -2
  102. package/api/upgrade/run/run.mjs +4 -6
  103. package/api/upgrade/upgrade.doc.mjs +22 -24
  104. package/api/upgrade/upgrade.type.mjs +2 -2
  105. package/assets/codemods/__tests__/runner.test.mjs +1 -3
  106. package/assets/codemods/integration-runner.mjs +3 -3
  107. package/assets/codemods/runner.mjs +4 -5
  108. package/assets/docs/README.md +2 -4
  109. package/assets/docs/browser-support.doc.mjs +11 -11
  110. package/assets/docs/color.doc.mjs +2 -8
  111. package/assets/docs/elevation.doc.mjs +4 -6
  112. package/assets/docs/getting-started.doc.mjs +16 -5
  113. package/assets/docs/icons.doc.mjs +21 -2
  114. package/assets/docs/illustrations.doc.mjs +15 -7
  115. package/assets/docs/internationalization.doc.mjs +5 -7
  116. package/assets/docs/layout.doc.dense.mjs +82 -130
  117. package/assets/docs/layout.doc.mjs +77 -133
  118. package/assets/docs/migration.doc.mjs +21 -19
  119. package/assets/docs/motion.doc.mjs +3 -16
  120. package/assets/docs/principles.doc.dense.mjs +5 -5
  121. package/assets/docs/principles.doc.mjs +0 -8
  122. package/assets/docs/principles.doc.zh.mjs +6 -6
  123. package/assets/docs/shape.doc.mjs +3 -8
  124. package/assets/docs/spacing.doc.mjs +2 -7
  125. package/assets/docs/styling-libraries.doc.mjs +2 -6
  126. package/assets/docs/styling.doc.mjs +23 -19
  127. package/assets/docs/theme.doc.dense.mjs +18 -58
  128. package/assets/docs/theme.doc.mjs +46 -56
  129. package/assets/docs/theme.doc.zh.mjs +8 -9
  130. package/assets/docs/tokens.doc.dense.mjs +2 -2
  131. package/assets/docs/tokens.doc.mjs +8 -389
  132. package/assets/docs/tokens.doc.zh.mjs +2 -2
  133. package/assets/docs/tree/integrations.doc.mjs +451 -25
  134. package/assets/docs/tree/integrations.test.mjs +62 -0
  135. package/assets/docs/tree/writing-docs.doc.mjs +286 -0
  136. package/assets/docs/typography.doc.mjs +4 -24
  137. package/assets/docs/working-with-ai.doc.mjs +22 -30
  138. package/assets/templates/blocks/components/InternationalizationProvider/InternationalizationProvider01ShippedLocale.tsx +1 -1
  139. package/authoring/config/config.doc.mjs +2 -10
  140. package/authoring/config/parse.d.mts +0 -2
  141. package/authoring/config/parse.mjs +0 -19
  142. package/authoring/config/parse.test.mjs +0 -8
  143. package/authoring/config/type.ts +2 -13
  144. package/authoring/doctypes/_schema.d.mts +2 -3
  145. package/authoring/doctypes/_schema.mjs +0 -6
  146. package/authoring/doctypes/base/graph-fields.doc.mjs +3 -3
  147. package/authoring/doctypes/base/type.ts +2 -4
  148. package/authoring/doctypes/command/command.doc.mjs +1 -1
  149. package/authoring/doctypes/command/type.ts +1 -1
  150. package/authoring/doctypes/component/component.doc.mjs +0 -6
  151. package/authoring/doctypes/component/type.ts +0 -8
  152. package/authoring/doctypes/reference/reference.doc.mjs +0 -7
  153. package/authoring/doctypes/reference/type.ts +0 -5
  154. package/authoring/doctypes/schema/schema.doc.mjs +2 -2
  155. package/authoring/doctypes/template/template.doc.mjs +1 -1
  156. package/authoring/doctypes/template/type.ts +2 -2
  157. package/authoring/index.d.mts +0 -1
  158. package/authoring/index.d.ts +0 -10
  159. package/authoring/index.mjs +0 -1
  160. package/authoring/integration/integration.doc.mjs +10 -12
  161. package/clients/cli/command-result-coverage.test.mjs +7 -7
  162. package/clients/cli/commands/component/index.mjs +55 -152
  163. package/clients/cli/commands/component-ownership.test.mjs +0 -89
  164. package/clients/cli/commands/component.doc.mjs +9 -27
  165. package/clients/cli/commands/debug-result-summary.test.mjs +2 -2
  166. package/clients/cli/commands/discover.doc.mjs +9 -53
  167. package/clients/cli/commands/discover.mjs +118 -393
  168. package/clients/cli/commands/docs.doc.mjs +1 -1
  169. package/clients/cli/commands/docs.mjs +17 -60
  170. package/clients/cli/commands/docs.test.mjs +294 -0
  171. package/clients/cli/commands/doctor-integration-docs.doc.mjs +2 -3
  172. package/clients/cli/commands/doctor-integration.test.mjs +0 -53
  173. package/clients/cli/commands/doctor.doc.mjs +1 -3
  174. package/clients/cli/commands/doctor.mjs +5 -49
  175. package/clients/cli/commands/gap-report.doc.mjs +9 -10
  176. package/clients/cli/commands/init.doc.mjs +6 -9
  177. package/clients/cli/commands/integration-add.doc.mjs +9 -9
  178. package/clients/cli/commands/integration-authoring.test.mjs +10 -61
  179. package/clients/cli/commands/integration-pack.doc.mjs +9 -5
  180. package/clients/cli/commands/integration-real-world.test.mjs +1 -1
  181. package/clients/cli/commands/integration.doc.mjs +4 -4
  182. package/clients/cli/commands/integration.mjs +43 -74
  183. package/clients/cli/commands/layout-check.doc.mjs +65 -0
  184. package/clients/cli/commands/layout-expand.doc.mjs +83 -0
  185. package/clients/cli/commands/layout-grammar.doc.mjs +30 -0
  186. package/clients/cli/commands/layout.doc.mjs +34 -0
  187. package/clients/cli/commands/layout.error-codes.test.mjs +66 -0
  188. package/clients/cli/commands/layout.exit-parity.test.mjs +41 -0
  189. package/clients/cli/commands/layout.mjs +275 -0
  190. package/clients/cli/commands/layout.path-help.test.mjs +33 -0
  191. package/clients/cli/commands/layout.stdin-cap.test.mjs +47 -0
  192. package/clients/cli/commands/layout.text-fields.test.mjs +39 -0
  193. package/clients/cli/commands/manifest.doc.mjs +1 -1
  194. package/clients/cli/commands/search.doc.mjs +3 -10
  195. package/clients/cli/commands/search.mjs +2 -21
  196. package/clients/cli/commands/search.test.mjs +4 -21
  197. package/clients/cli/commands/swizzle.doc.mjs +1 -1
  198. package/clients/cli/commands/template.doc.mjs +1 -1
  199. package/clients/cli/commands/text-json-parity.test.mjs +16 -5
  200. package/clients/cli/commands/theme-add.doc.mjs +1 -1
  201. package/clients/cli/commands/theme-palette-generate.doc.mjs +2 -3
  202. package/clients/cli/commands/theme-palette.doc.mjs +2 -1
  203. package/clients/cli/commands/theme-targets.doc.mjs +2 -2
  204. package/clients/cli/commands/theme.doc.mjs +1 -2
  205. package/clients/cli/commands/upgrade.doc.mjs +3 -62
  206. package/clients/cli/index.mjs +10 -28
  207. package/clients/cli/lib/define-command.mjs +4 -28
  208. package/clients/cli/lib/define-command.test.mjs +0 -54
  209. package/clients/cli/lib/exit-codes.test.mjs +9 -18
  210. package/clients/cli/lib/json-shim.mjs +14 -24
  211. package/clients/cli/lib/json-shim.test.mjs +20 -6
  212. package/clients/cli/lib/manifest.mjs +13 -18
  213. package/clients/cli/lib/manifest.test.mjs +2 -5
  214. package/foundation/agent-docs/agent-docs.mjs +1 -1
  215. package/foundation/agent-docs/agent-docs.test.mjs +1159 -0
  216. package/foundation/discovery/authoring-self-docs.mjs +0 -1
  217. package/foundation/discovery/authoring-self-docs.test.mjs +2 -6
  218. package/foundation/discovery/cli-self-docs.mjs +2 -16
  219. package/foundation/discovery/cli-self-docs.test.mjs +0 -20
  220. package/foundation/discovery/docs-discovery.mjs +1 -5
  221. package/foundation/discovery/docs-discovery.test.mjs +0 -21
  222. package/foundation/discovery/docs-section-key.d.mts +1 -1
  223. package/foundation/discovery/docs-section-key.mjs +1 -1
  224. package/foundation/discovery/template-adapter.mjs +1 -1
  225. package/foundation/doc-compiler/doc-loads.test.mjs +14 -3
  226. package/foundation/doc-compiler/tree.d.mts +0 -4
  227. package/foundation/doc-compiler/tree.mjs +1 -6
  228. package/foundation/doc-compiler/tree.test.mjs +598 -0
  229. package/foundation/integrations/cli-requirement.d.mts +6 -26
  230. package/foundation/integrations/cli-requirement.mjs +11 -46
  231. package/foundation/integrations/cli-requirement.test.mjs +2 -7
  232. package/foundation/integrations/contribution-inventory.mjs +1 -1
  233. package/foundation/integrations/integrations.d.mts +1 -14
  234. package/foundation/integrations/integrations.mjs +1 -41
  235. package/foundation/integrations/integrations.test.mjs +0 -31
  236. package/foundation/response/error-codes.doc.mjs +8 -6
  237. package/foundation/response/error-codes.test.mjs +5 -30
  238. package/foundation/response/response-types.doc.d.mts +3 -4
  239. package/foundation/response/response-types.doc.mjs +27 -40
  240. package/foundation/response/response-types.doc.test.mjs +0 -23
  241. package/foundation/response/response.doc.mjs +10 -11
  242. package/foundation/xle/browser.d.mts +3 -3
  243. package/foundation/xle/browser.mjs +3 -3
  244. package/foundation/xle/expand.mjs +2 -2
  245. package/foundation/xle/parse.mjs +1 -1
  246. package/foundation/xle/print.mjs +2 -2
  247. package/foundation/xle/splice.mjs +1 -1
  248. package/package.json +9 -9
  249. package/api/discover/_adapter.test.mjs +0 -215
  250. package/api/discover/_catalog-view.d.mts +0 -115
  251. package/api/discover/_catalog-view.mjs +0 -203
  252. package/api/discover/_catalog-view.test.mjs +0 -128
  253. package/api/discover/detail/item/item.d.mts +0 -26
  254. package/api/discover/detail/item/item.mjs +0 -78
  255. package/api/discover/detail/item/item.test.mjs +0 -73
  256. package/api/integration/pack-check.lifecycle-output.test.mjs +0 -107
  257. package/api/theme/add/add.rollback.test.mjs +0 -158
  258. package/api/theme/build/build.rollback.test.mjs +0 -148
  259. package/api/upgrade/run/files-changed.test.mjs +0 -111
  260. package/assets/codemods/file-count.test.mjs +0 -163
  261. package/assets/docs/tree/add-a-component.doc.mjs +0 -75
  262. package/assets/docs/tree/add-a-theme.doc.mjs +0 -85
  263. package/assets/docs/tree/add-a-topic.doc.mjs +0 -144
  264. package/assets/docs/tree/agent-guidance.doc.mjs +0 -138
  265. package/assets/docs/tree/block-template.doc.mjs +0 -130
  266. package/assets/docs/tree/build-the-template.doc.mjs +0 -28
  267. package/assets/docs/tree/building-blocks.doc.mjs +0 -46
  268. package/assets/docs/tree/check-your-docs.doc.mjs +0 -137
  269. package/assets/docs/tree/checks.doc.mjs +0 -119
  270. package/assets/docs/tree/codemods.doc.mjs +0 -147
  271. package/assets/docs/tree/component-family.doc.mjs +0 -113
  272. package/assets/docs/tree/component-imports.doc.mjs +0 -69
  273. package/assets/docs/tree/component-lookups.doc.mjs +0 -149
  274. package/assets/docs/tree/components.doc.mjs +0 -23
  275. package/assets/docs/tree/configuration.doc.mjs +0 -23
  276. package/assets/docs/tree/debug-and-gap-reports.doc.mjs +0 -182
  277. package/assets/docs/tree/define-the-theme.doc.mjs +0 -118
  278. package/assets/docs/tree/describe-the-component.doc.mjs +0 -57
  279. package/assets/docs/tree/docs.doc.mjs +0 -21
  280. package/assets/docs/tree/document-the-template.doc.mjs +0 -28
  281. package/assets/docs/tree/document-the-theme.doc.mjs +0 -68
  282. package/assets/docs/tree/export-template-assets.doc.mjs +0 -147
  283. package/assets/docs/tree/extend-or-replace.doc.mjs +0 -103
  284. package/assets/docs/tree/fonts-and-assets.doc.mjs +0 -106
  285. package/assets/docs/tree/generate-a-palette.doc.mjs +0 -66
  286. package/assets/docs/tree/grade-template-with-agent.doc.mjs +0 -105
  287. package/assets/docs/tree/help.doc.mjs +0 -16
  288. package/assets/docs/tree/links.doc.mjs +0 -98
  289. package/assets/docs/tree/package-and-test.doc.mjs +0 -32
  290. package/assets/docs/tree/page-template.doc.mjs +0 -71
  291. package/assets/docs/tree/publishing.doc.mjs +0 -111
  292. package/assets/docs/tree/quick-start.doc.mjs +0 -272
  293. package/assets/docs/tree/replace-a-core-component.doc.mjs +0 -104
  294. package/assets/docs/tree/replace-a-core-template.doc.mjs +0 -172
  295. package/assets/docs/tree/sections-and-placement.doc.mjs +0 -108
  296. package/assets/docs/tree/see-it-in-an-app.doc.mjs +0 -59
  297. package/assets/docs/tree/ship.doc.mjs +0 -16
  298. package/assets/docs/tree/short-and-findable.doc.mjs +0 -108
  299. package/assets/docs/tree/single-component.doc.mjs +0 -165
  300. package/assets/docs/tree/start-a-template.doc.mjs +0 -143
  301. package/assets/docs/tree/subcomponent.doc.mjs +0 -115
  302. package/assets/docs/tree/template-assets.doc.mjs +0 -64
  303. package/assets/docs/tree/template-doc-overview.doc.mjs +0 -109
  304. package/assets/docs/tree/template-fonts.doc.mjs +0 -102
  305. package/assets/docs/tree/template-grading-rubric.doc.mjs +0 -452
  306. package/assets/docs/tree/template-icons.doc.mjs +0 -97
  307. package/assets/docs/tree/template-images-media.doc.mjs +0 -127
  308. package/assets/docs/tree/template-styles.doc.mjs +0 -93
  309. package/assets/docs/tree/templates.doc.mjs +0 -34
  310. package/assets/docs/tree/test-in-an-app.doc.mjs +0 -115
  311. package/assets/docs/tree/test-template-in-app.doc.mjs +0 -128
  312. package/assets/docs/tree/themes.doc.mjs +0 -39
  313. package/assets/docs/tree/troubleshooting.doc.mjs +0 -149
  314. package/assets/docs/tree/upgrading.doc.mjs +0 -103
  315. package/assets/docs/tree/use-a-theme-in-an-app.doc.mjs +0 -51
  316. package/assets/docs/tree/verify-packed-template.doc.mjs +0 -77
  317. package/assets/docs/tree/versioning.doc.mjs +0 -161
  318. package/assets/docs/tree/write-good-templates.doc.mjs +0 -64
  319. package/assets/docs/tree/write-the-template-file.doc.mjs +0 -154
  320. package/authoring/discover/discover.doc.d.mts +0 -13
  321. package/authoring/discover/discover.doc.mjs +0 -138
  322. package/authoring/discover/parse.d.mts +0 -24
  323. package/authoring/discover/parse.mjs +0 -128
  324. package/authoring/discover/parse.test.mjs +0 -124
  325. package/authoring/discover/type.ts +0 -87
  326. package/clients/cli/commands/component-batch.test.mjs +0 -341
  327. package/clients/cli/commands/discover.sources.test.mjs +0 -267
  328. package/clients/cli/commands/integration-verify.doc.mjs +0 -22
  329. package/clients/cli/lib/parse-error-format.test.mjs +0 -81
  330. package/foundation/response/batch.type.d.mts +0 -33
  331. package/foundation/response/batch.type.mjs +0 -34
@@ -191,18 +191,6 @@ describe('search CLI — exit codes + JSON contract', () => {
191
191
  expect(r.stdout).toContain('No results');
192
192
  });
193
193
 
194
- it('takes every word after `search` as one query', async () => {
195
- // Commander took only the first word, so `search dark mode` searched for
196
- // "dark" and dropped "mode" without a word.
197
- const json = await runCli(['--json', 'search', 'dark', 'mode', '--type', 'doc'], REPO_ROOT);
198
- expect(json.status).toBe(0);
199
- const env = JSON.parse(json.stdout);
200
- expect(env.data.query).toBe('dark mode');
201
- expect(env.data.results[0]).toMatchObject({name: 'theme', section: 'light-dark-mode'});
202
- const text = await runCli(['search', 'dark', 'mode', '--type', 'doc'], REPO_ROOT);
203
- expect(text.stdout).toContain('Results for "dark mode"');
204
- }, SCAN_TIMEOUT);
205
-
206
194
  it('exits 1 for an invalid --type', async () => {
207
195
  const r = await runCli(['search', 'x', '--type', 'bogus'], REPO_ROOT);
208
196
  expect(r.status).toBe(1);
@@ -311,19 +299,14 @@ describe('search CLI — exit codes + JSON contract', () => {
311
299
  expect(r.stdout).toContain('reason:');
312
300
  });
313
301
 
314
- it('searches the docs when no @astryxdesign/core is reachable, and exits 1 for --type component', async () => {
302
+ it('exits 1 with ERR_CORE_NOT_FOUND when no @astryxdesign/core is reachable', async () => {
315
303
  const empty = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-search-cli-no-core-'));
316
304
  try {
317
- const open = await runCli(['--json', 'search', 'make', 'an', 'integration'], empty);
318
- expect(open.status).toBe(0);
319
- expect(JSON.parse(open.stdout).data.results[0]).toMatchObject({domain: 'doc'});
320
- // The text says the search covered the docs alone.
321
- const text = await runCli(['search', 'button'], empty);
322
- expect(text.status).toBe(0);
323
- expect(text.stdout).toContain('only the docs were searched');
324
- const json = await runCli(['--json', 'search', 'button', '--type', 'component'], empty);
305
+ const json = await runCli(['--json', 'search', 'button'], empty);
325
306
  expect(json.status).toBe(1);
326
307
  expect(JSON.parse(json.stdout)).toMatchObject({code: 'ERR_CORE_NOT_FOUND'});
308
+ const text = await runCli(['search', 'button'], empty);
309
+ expect(text.status).toBe(1);
327
310
  } finally {
328
311
  fs.rmSync(empty, {recursive: true, force: true});
329
312
  }
@@ -46,7 +46,7 @@ export const doc = {
46
46
  ],
47
47
  examples: [
48
48
  {label: 'List swizzlable components', cli: 'astryx swizzle --list'},
49
- {label: 'Eject a component', cli: 'astryx swizzle Button'},
49
+ {label: 'Eject a component', cli: 'astryx swizzle XDSButton'},
50
50
  ],
51
51
  exitCodes: [
52
52
  {code: 0, when: 'success'},
@@ -14,7 +14,7 @@ export const doc = {
14
14
  name: 'template',
15
15
  displayName: 'astryx template',
16
16
  namespace: 'cli/commands',
17
- summary: 'List, show, or scaffold page and block templates',
17
+ summary: 'Inject a page or block template',
18
18
  description:
19
19
  'One entry point for the template family: with no name it lists the discovered ' +
20
20
  'templates; with a name it shows the source or a layout skeleton, or scaffolds it ' +
@@ -372,17 +372,28 @@ const CASES = [
372
372
  skipFieldChecks: true,
373
373
  },
374
374
  {
375
- name: 'integration verify',
376
- args: ['integration', 'verify'],
375
+ name: 'integration pack',
376
+ args: ['integration', 'pack', '--check'],
377
377
  // Needs a packable project; pack itself may exit differently.
378
378
  skipFieldChecks: true,
379
379
  },
380
+ // ── Layout command: remove these cases when the layout command is deleted ──
380
381
  {
381
- // The deprecated alias of `integration verify`.
382
- name: 'integration pack',
383
- args: ['integration', 'pack', '--check'],
382
+ name: 'layout check',
383
+ args: ['layout', 'check', 'Button'],
384
+ skipFieldChecks: true,
385
+ },
386
+ {
387
+ name: 'layout expand',
388
+ args: ['layout', 'expand', 'Button'],
389
+ skipFieldChecks: true,
390
+ },
391
+ {
392
+ name: 'layout grammar',
393
+ args: ['layout', 'grammar'],
384
394
  skipFieldChecks: true,
385
395
  },
396
+ // ── End layout cases ──
386
397
  {
387
398
  name: 'manifest',
388
399
  args: ['manifest'],
@@ -41,7 +41,7 @@ export const doc = {
41
41
  examples: [
42
42
  {label: 'Scaffold a theme', cli: 'astryx theme add matcha'},
43
43
  {
44
- label: 'Pick the owner when two packages ship the same slug',
44
+ label: 'Select an integration theme',
45
45
  cli: 'astryx theme add ocean --package @acme/themes',
46
46
  },
47
47
  ],
@@ -35,13 +35,12 @@ export const doc = {
35
35
  {
36
36
  flag: '--preview <path>',
37
37
  param: 'options.preview',
38
- description: 'Write a self-contained HTML preview page; the path must end in .html',
38
+ description: 'Write a standardized self-contained HTML preview',
39
39
  },
40
40
  {
41
41
  flag: '-f, --overwrite',
42
42
  param: 'options.overwrite',
43
- description:
44
- 'Replace existing candidate, receipt, and preview files. Without it, if any of them exists, nothing is written',
43
+ description: 'Replace existing candidate and receipt files',
45
44
  },
46
45
  ],
47
46
  examples: [
@@ -8,7 +8,8 @@ export const doc = {
8
8
  namespace: 'cli/commands',
9
9
  summary: 'Create and work with theme-owned color palettes',
10
10
  description:
11
- 'Palette authoring tools. generate writes a palette candidate for you to review before a theme uses it.',
11
+ 'Palette authoring tools. The initial generate command creates reviewable candidates. ' +
12
+ 'Palette inspection and diagnostic commands are intentionally deferred to follow-up work.',
12
13
  subcommands: ['generate'],
13
14
  examples: [
14
15
  {
@@ -20,8 +20,8 @@ export const doc = {
20
20
  'the component that declares it, and the props and states that are legal override keys ' +
21
21
  'under it. This is the whole themeable surface in one command: what auditing a theme, or ' +
22
22
  'answering "which key paints this pixel?", used to need one `astryx component <Name>` per ' +
23
- 'component to assemble. Pass a component name to scope it; pass any other text to search ' +
24
- 'target keys, classes, and components. `--json` for a list a repo can lint its own theme against.',
23
+ 'component to assemble. Pass a component name to scope it; pass any substring to search ' +
24
+ 'keys. `--json` for a list a repo can lint its own theme against.',
25
25
  fn: 'themeTargets',
26
26
  args: [{name: 'filter', param: 'filter', required: false}],
27
27
  examples: [
@@ -13,8 +13,7 @@ export const doc = {
13
13
  name: 'theme',
14
14
  displayName: 'astryx theme',
15
15
  namespace: 'cli/commands',
16
- summary:
17
- 'Create and build themes: add a shipped one, compile to CSS, or list what a theme can override',
16
+ summary: 'Theme tools: build, export, and manage themes',
18
17
  description:
19
18
  'The theme command group. Running astryx theme with no subcommand prints the ' +
20
19
  'subcommand list; the work happens in the subcommands: compile a theme (build), ' +
@@ -13,8 +13,7 @@ export const doc = {
13
13
  name: 'upgrade',
14
14
  displayName: 'astryx upgrade',
15
15
  namespace: 'cli/commands',
16
- summary:
17
- 'Update your code after upgrading Astryx, and refresh ShadCN-copied components',
16
+ summary: 'Migrate versions and update ShadCN-copied compositions',
18
17
  description:
19
18
  'Migrates project source from a previous Astryx version to the installed one by ' +
20
19
  'running the registered codemods, and refreshes the fully rendered managed ' +
@@ -34,7 +33,7 @@ export const doc = {
34
33
  {
35
34
  flag: '--apply',
36
35
  param: 'options.apply',
37
- description: 'Write changes to disk; without it, the run is a dry run',
36
+ description: 'Write changes to disk (default: dry-run)',
38
37
  default: false,
39
38
  },
40
39
  {
@@ -81,8 +80,7 @@ export const doc = {
81
80
  flag: '--registry',
82
81
  param: 'options.registry',
83
82
  description:
84
- 'Only update ShadCN-copied compositions from their install receipts: unchanged files are updated, ' +
85
- 'edits that do not overlap are merged, and conflicts are left untouched; --from is not required. ' +
83
+ 'Only reconcile ShadCN-copied compositions; --from is not required. ' +
86
84
  'Combining it with --list, --from, --force, --codemod, --skip-codemod, --integration or --install-deps exits 1 with ERR_INVALID_ARGUMENT',
87
85
  default: false,
88
86
  },
@@ -115,61 +113,4 @@ export const doc = {
115
113
  },
116
114
  ],
117
115
  related: ['init', 'doctor'],
118
- notes: [
119
- {type: 'heading', level: 3, text: 'Protected files'},
120
- {
121
- type: 'prose',
122
- text:
123
- 'Codemods never write to a file your project marks as generated, vendored, or ignored. ' +
124
- 'upgrade reads these marks from the files on disk, so the answer is the same with any version control, or none. ' +
125
- 'A file is protected when:',
126
- },
127
- {
128
- type: 'list',
129
- style: 'unordered',
130
- items: [
131
- 'a `.gitattributes` file marks it `linguist-generated` or `linguist-vendored`',
132
- 'its leading comment says `@generated`, `@partially-generated`, or `Code generated ... DO NOT EDIT.`',
133
- 'a `.gitignore`, or the `.hgignore` at the project root, excludes it',
134
- 'it is an installed dependency (such as anything in `node_modules`), is inside `.git`, `.hg`, or `.sl`, is a symbolic link, or is outside the project',
135
- ],
136
- },
137
- {
138
- type: 'prose',
139
- text:
140
- 'Rules work as they do in Git: a later rule wins, so `linguist-generated=false` or a `!` line in an ignore file ' +
141
- 'returns a file to normal handling. A folder name such as `dist` or `generated` protects nothing by itself. ' +
142
- 'If a protection file cannot be read or parsed, upgrade stops with ERR_CODEMOD_PROTECTION_SOURCE before it writes anything.',
143
- },
144
- {
145
- type: 'code',
146
- lang: 'text',
147
- code:
148
- '# .gitattributes\n' +
149
- 'generated/** linguist-generated=true\n' +
150
- 'vendor/** linguist-vendored=true\n' +
151
- '\n' +
152
- '# A later rule returns one authored file to normal handling\n' +
153
- 'generated/hand-authored.ts linguist-generated=false',
154
- },
155
- {
156
- type: 'prose',
157
- text:
158
- 'When a codemod would change a protected file, upgrade makes the change only in memory and leaves the file as it is. ' +
159
- 'The rest of the upgrade goes ahead. With --apply, your other files are written, then upgrade runs the ' +
160
- '`hooks.postCodemod` commands from astryx.config (see `astryx docs authoring config`) and checks the protected ' +
161
- 'files again. If one still needs the change, the run is incomplete: it exits 1, prints ' +
162
- 'ERR_CODEMOD_PROTECTED with each file and the rule that protects it, and does not refresh the agent docs. ' +
163
- 'Regenerate or edit those files, then run the same upgrade again. A dry run reports the same files and writes nothing.',
164
- },
165
- {
166
- type: 'prose',
167
- text:
168
- 'With --json, the receipt says `complete: false` and `errorCode: "ERR_CODEMOD_PROTECTED"`. `modifiedFiles` lists the files ' +
169
- 'upgrade changed (or would change), and `protectedFiles` lists each blocked file with its `reasons`, `declarations` ' +
170
- '(the rules that protect it), `codemods`, and `commands`. A generated file can name the command that rebuilds it on a ' +
171
- '`Command:` line in its header, such as `// Command: pnpm run gen:panel`; upgrade prints it as ' +
172
- '`Regenerate with: <command>` and lists it in `commands`.',
173
- },
174
- ],
175
116
  };
@@ -24,7 +24,7 @@ import {emit, section, text, records} from './formatters/index.mjs';
24
24
  import {ERROR_CODES} from '../../foundation/response/error-codes.mjs';
25
25
  import {levenshteinDistance} from '../../foundation/text/string-utils.mjs';
26
26
  import {installJsonShim} from './lib/json-shim.mjs';
27
- import {addDocHelp, markReportsResult} from './lib/define-command.mjs';
27
+ import {addExitCodesHelp, markReportsResult} from './lib/define-command.mjs';
28
28
  import {doc as manifestDoc} from './commands/manifest.doc.mjs';
29
29
  import {isAstryxInitialized} from '../../foundation/agent-docs/agent-docs.mjs';
30
30
  import * as debug from '../../foundation/debug/index.mjs';
@@ -95,7 +95,6 @@ export const JSON_SUPPORTED = new Set([
95
95
  'theme targets',
96
96
  'theme palette generate',
97
97
  'integration add',
98
- 'integration verify',
99
98
  'integration pack',
100
99
  'upgrade',
101
100
  'manifest',
@@ -104,6 +103,9 @@ export const JSON_SUPPORTED = new Set([
104
103
  'doctor integration templates',
105
104
  'doctor integration components',
106
105
  'doctor integration docs',
106
+ 'layout expand',
107
+ 'layout check',
108
+ 'layout grammar',
107
109
  ]);
108
110
 
109
111
  /**
@@ -262,6 +264,7 @@ const commands = [
262
264
  {name: 'gap-report', path: './commands/gap-report.mjs', register: 'registerGapReport'},
263
265
  // agent-docs folded into init — functions still importable from agent-docs.mjs
264
266
  {name: 'template', path: './commands/template.mjs', register: 'registerTemplate'},
267
+ {name: 'layout', path: './commands/layout.mjs', register: 'registerLayout'},
265
268
  {name: 'upgrade', path: './commands/upgrade.mjs', register: 'registerUpgrade'},
266
269
  {name: 'theme', path: './commands/build-theme.mjs', register: 'registerTheme'},
267
270
  {name: 'integration', path: './commands/integration.mjs', register: 'registerIntegration'},
@@ -339,24 +342,16 @@ export async function createProgram() {
339
342
  .name('astryx')
340
343
  .description('Design system CLI — components, themes, and tooling')
341
344
  .version(pkg.version)
342
- // These four change only the reads named in their text; every other
343
- // command ignores them.
344
- .option(
345
- '--zh',
346
- 'Simplified Chinese for component reads and for docs topics that have a translation (English otherwise)',
347
- )
348
- .option('--dense', 'Token-efficient dense text for component <Name> and docs <topic> reads')
345
+ .option('--zh', 'Output docs in Chinese Simplified')
346
+ .option('--dense', 'Output docs in compressed dense format (token-efficient)')
349
347
  .addOption(
350
348
  new Option(
351
349
  '--lang <locale>',
352
- 'Language or format for component and docs reads: en (default), zh (as --zh), or dense (as --dense)',
350
+ 'Output docs in specified language/format (en, zh, dense)',
353
351
  ).choices(['en', 'zh', 'dense']),
354
352
  )
355
353
  .addOption(
356
- new Option(
357
- '--detail <level>',
358
- 'Detail level for component, hook, and docs tree reads (e.g. docs cli/commands/build). Lists default to brief',
359
- )
354
+ new Option('--detail <level>', 'Output detail level (full, compact, brief)')
360
355
  .choices(['full', 'compact', 'brief'])
361
356
  .default('full'),
362
357
  )
@@ -476,19 +471,6 @@ export async function createProgram() {
476
471
  const fullName = fullCommandName(actionCommand, program);
477
472
  if (JSON_SUPPORTED.has(fullName)) return;
478
473
  process.__xdsJsonHandled = true;
479
- // A group given a word it does not have reports an unknown subcommand and
480
- // lists the ones it has, in JSON as in text, even when a flag follows it.
481
- const extras = actionCommand.commands.length > 0 ? actionCommand.args : [];
482
- const unknown = extras.find(arg => !String(arg).startsWith('-'));
483
- if (unknown != null) {
484
- cliError(`unknown subcommand '${fullName} ${unknown}'`, {
485
- code: ERROR_CODES.ERR_UNKNOWN_SUBCOMMAND,
486
- suggestions: actionCommand.commands.map(child => ({
487
- name: child.name(),
488
- reason: 'available subcommand',
489
- })),
490
- });
491
- }
492
474
  debug.setOutcome('rejected', {
493
475
  exitCode: 1,
494
476
  code: ERROR_CODES.ERR_INVALID_OPTION,
@@ -623,7 +605,7 @@ export async function createProgram() {
623
605
  text(`Run \`${getCliInvocation()} manifest --json\` for the full structured manifest.`),
624
606
  );
625
607
  });
626
- addDocHelp(manifestCommand, manifestDoc);
608
+ addExitCodesHelp(manifestCommand, manifestDoc.exitCodes);
627
609
  markReportsResult(manifestCommand);
628
610
 
629
611
  // Hidden command used by package.json postinstall scripts
@@ -25,8 +25,6 @@
25
25
  */
26
26
 
27
27
  import {recordCommandResult} from '../../../foundation/debug/index.mjs';
28
- import {routeSegment} from '../../../foundation/discovery/docs-section-key.mjs';
29
- import {formatCliCommand} from '../../../foundation/env/package-manager.mjs';
30
28
  import {text} from '../formatters/index.mjs';
31
29
 
32
30
  /**
@@ -145,11 +143,10 @@ export function defineCommand(parent, doc, {fn, action} = {}) {
145
143
  cmd.addOption(option);
146
144
  }
147
145
 
148
- // Help ends with the documented exit codes, the examples, and the docs
149
- // route that reads the whole command. `choices` stay in the option text:
150
- // Commander `.choices()` would replace the api layer's ERR_INVALID_ARGUMENT
151
- // validation.
152
- addDocHelp(cmd, doc);
146
+ // Help ends with the documented exit codes. `choices` stay in the option
147
+ // text: Commander `.choices()` would replace the api layer's
148
+ // ERR_INVALID_ARGUMENT validation.
149
+ addExitCodesHelp(cmd, doc.exitCodes);
153
150
 
154
151
  if (action) {
155
152
  // The recording seam. An action's job ends at "here is what I answered
@@ -168,27 +165,6 @@ export function defineCommand(parent, doc, {fn, action} = {}) {
168
165
  return cmd;
169
166
  }
170
167
 
171
- /**
172
- * End `cmd`'s help with what its CommandDoc says: the exit codes, then the
173
- * examples, then `More:`, the `astryx docs` route that reads the whole command.
174
- * @param {import('commander').Command} cmd
175
- * @param {import('@astryxdesign/cli/authoring').CommandDoc} doc
176
- */
177
- export function addDocHelp(cmd, doc) {
178
- addExitCodesHelp(cmd, doc.exitCodes);
179
- // Rendered when help is shown, so the run prefix (npx astryx, pnpm astryx,
180
- // ...) is looked up then, not on every start.
181
- cmd.addHelpText('after', () => {
182
- const examples = (doc.examples ?? []).flatMap(({label, cli}) => [
183
- ...(label ? [` # ${label}`] : []),
184
- ` ${formatCliCommand(cli)}`,
185
- ]);
186
- const more = `More: ${formatCliCommand(`docs cli/commands/${routeSegment(doc.name)}`)}`;
187
- const blocks = examples.length > 0 ? [['Examples:', ...examples].join('\n'), more] : [more];
188
- return `\n${text(blocks.join('\n\n')).toString()}`;
189
- });
190
- }
191
-
192
168
  /**
193
169
  * End `cmd`'s help with a CommandDoc's exit codes.
194
170
  * @param {import('commander').Command} cmd
@@ -7,7 +7,6 @@
7
7
  import {Command} from 'commander';
8
8
  import {describe, it, expect} from 'vitest';
9
9
  import {defineCommand} from './define-command.mjs';
10
- import {formatCliCommand} from '../../../foundation/env/package-manager.mjs';
11
10
  import {doc as searchCommand} from '../commands/search.doc.mjs';
12
11
  import {doc as searchFn} from '../../../api/search/search.doc.mjs';
13
12
 
@@ -47,57 +46,4 @@ describe('defineCommand', () => {
47
46
  expect(cmd.name()).toBe('build');
48
47
  expect(cmd.registeredArguments.map(a => a.name())).toEqual(['file']);
49
48
  });
50
-
51
- it('ends help with the exit codes, the examples, and the docs route', () => {
52
- const program = new Command();
53
- const group = program.command('grp');
54
- const cmd = defineCommand(
55
- group,
56
- {
57
- type: 'command',
58
- name: 'grp sub',
59
- displayName: 'astryx grp sub',
60
- summary: 'Sub.',
61
- examples: [
62
- {label: 'Run it', cli: 'astryx grp sub x'},
63
- {cli: 'astryx grp sub y --json'},
64
- ],
65
- exitCodes: [{code: 0, when: 'it works'}],
66
- },
67
- {action: () => {}},
68
- );
69
- let out = '';
70
- cmd.configureOutput({writeOut: s => (out += s)});
71
- cmd.outputHelp();
72
- const stem = formatCliCommand('');
73
- expect(out.slice(out.indexOf('\nExit codes:\n'))).toBe(
74
- [
75
- '',
76
- 'Exit codes:',
77
- ' 0 it works',
78
- '',
79
- 'Examples:',
80
- ' # Run it',
81
- ` ${stem} grp sub x`,
82
- ` ${stem} grp sub y --json`,
83
- '',
84
- `More: ${stem} docs cli/commands/grp-sub`,
85
- '',
86
- ].join('\n'),
87
- );
88
- });
89
-
90
- it('still names the docs route when a command has no examples', () => {
91
- const program = new Command();
92
- const cmd = defineCommand(
93
- program,
94
- {type: 'command', name: 'solo', summary: 'Solo.', exitCodes: [{code: 0, when: 'ok'}]},
95
- {action: () => {}},
96
- );
97
- let out = '';
98
- cmd.configureOutput({writeOut: s => (out += s)});
99
- cmd.outputHelp();
100
- expect(out).not.toContain('Examples:');
101
- expect(out.endsWith(`\n\nMore: ${formatCliCommand('docs cli/commands/solo')}\n`)).toBe(true);
102
- });
103
49
  });
@@ -34,14 +34,14 @@ manifest.commands.forEach(walk);
34
34
 
35
35
  describe('command exit codes', () => {
36
36
  it('every CommandDoc documents its exit codes', () => {
37
- expect(commandDocs.length).toBeGreaterThan(25);
37
+ expect(commandDocs.length).toBeGreaterThan(30);
38
38
  for (const doc of commandDocs) {
39
39
  expect(doc.exitCodes?.length, doc.name).toBeGreaterThan(0);
40
40
  }
41
41
  });
42
42
 
43
43
  it.each(commandDocs.map((d) => [d.name, d]))(
44
- '`astryx %s --help` lists the documented exit codes, then the examples and the docs route',
44
+ '`astryx %s --help` lists the documented exit codes',
45
45
  async (name, doc) => {
46
46
  const {status, stdout} = await runCli([...name.split(' '), '--help']);
47
47
  expect(status).toBe(0);
@@ -51,25 +51,16 @@ describe('command exit codes', () => {
51
51
  for (const {code, when} of doc.exitCodes) {
52
52
  expect(section).toContain(`\n ${code} ${when}\n`);
53
53
  }
54
- // Examples follow the exit codes, each under its label, and a `More:`
55
- // line names the route that reads the whole command.
56
- const examples = section.indexOf('\nExamples:\n');
57
- expect(examples > 0, stdout).toBe((doc.examples ?? []).length > 0);
58
- for (const {label, cli} of doc.examples ?? []) {
59
- const line = ` ${cli.replace(/^astryx\s+/, '')}\n`;
60
- expect(section.slice(examples), stdout).toContain(
61
- label ? `\n # ${label}\n` : line,
62
- );
63
- expect(section.slice(examples)).toContain(line);
64
- }
65
- const route = `docs cli/commands/${name.replace(/ /g, '-')}`;
66
- expect(section, stdout).toMatch(
67
- new RegExp(`\\n\\nMore: \\S.* ${route}\\n`),
68
- );
69
- expect(section.indexOf('\nMore: ')).toBeGreaterThan(examples);
70
54
  },
71
55
  );
72
56
 
57
+ it('bare `astryx layout` exits 1 in both modes, as documented', async () => {
58
+ const doc = commandDocs.find((d) => d.name === 'layout');
59
+ expect(doc.exitCodes.find((e) => e.code === 1)?.when).toMatch(/^no subcommand/);
60
+ expect((await runCli(['layout'])).status).toBe(1);
61
+ expect((await runCli(['layout', '--json'])).status).toBe(1);
62
+ });
63
+
73
64
  it('`astryx discover` with a blank query exits 1 only when packages are discovered', async () => {
74
65
  const doc = commandDocs.find((d) => d.name === 'discover');
75
66
  expect(doc.exitCodes.find((e) => e.code === 1)?.when).toMatch(/blank query when packages are discovered/);
@@ -33,12 +33,10 @@
33
33
  * shows help because the invocation failed (`help <unknown>`, or a
34
34
  * command group with no subcommand), which exits 1.
35
35
  *
36
- * Commander writes its own "error: ..." line via configureOutput.writeErr.
37
- * The shim drops that line in both modes. Under --json the error envelope
38
- * replaces it; in text mode `handleCommanderError` writes the Astryx line
39
- * instead (`Error: <message>`, the same message the envelope carries), so
40
- * a parse failure reads like every other CLI error. Other stderr output,
41
- * such as help printed as the failure report, still passes through.
36
+ * Non-JSON behavior is preserved exactly: every code path that printed
37
+ * to stderr before still prints to stderr. Commander writes its
38
+ * "error: ..." line via configureOutput.writeErr, which we pass
39
+ * through verbatim outside of --json mode.
42
40
  */
43
41
 
44
42
  import {API_VERSION, isJsonMode, toErrorEnvelope} from '../../../foundation/response/json.mjs';
@@ -251,20 +249,11 @@ function applyShimRecursively(cmd) {
251
249
  });
252
250
  cmd.configureOutput({
253
251
  writeOut: (str) => process.stdout.write(str),
254
- // Commander's own "error: ..." line never reaches the user. Under --json a
255
- // consumer parsing both streams must not see it alongside the envelope;
256
- // in text mode it is Commander's format, not Astryx's, so an invalid
257
- // global option (`--lang zh-Hans`) printed `error: option '--lang
258
- // <locale>' argument 'zh-Hans' is invalid…` where every other CLI error
259
- // prints `Error: …`. handleCommanderError writes the Astryx line below,
260
- // for both modes, from the same message.
261
- //
262
- // ONLY that line. Commander also writes HELP through this channel when it
263
- // shows help because the invocation failed (a command group with no
264
- // subcommand), and that output is still wanted in text mode.
265
252
  writeErr: (str) => {
253
+ // Suppress Commander's "error: ..." stderr line when --json is
254
+ // active, so a JSON consumer parsing both streams doesn't see
255
+ // it alongside the envelope. Non-JSON callers are unaffected.
266
256
  if (jsonActive()) return;
267
- if (/^error:\s/i.test(str)) return;
268
257
  process.stderr.write(str);
269
258
  },
270
259
  });
@@ -443,15 +432,16 @@ export function handleCommanderError(err) {
443
432
  code: commanderCodeToErrorCode(code, message),
444
433
  });
445
434
 
446
- // Real error paths. Strip Commander's "error: " prefix once: in the envelope
447
- // the key is already `error`, and in text mode the Astryx prefix replaces it.
448
- const cleaned = message.replace(/^error:\s*/i, '');
435
+ // Real error paths.
449
436
  if (jsonActive()) {
437
+ // Strip Commander's "error: " prefix — the envelope key is `error`
438
+ // already, doubled "error" is noise.
439
+ const cleaned = message.replace(/^error:\s*/i, '');
450
440
  emitJsonError(cleaned, undefined, commanderCodeToErrorCode(code, cleaned));
451
441
  } else {
452
- // Commander's own line was suppressed above, so a parse failure reads the
453
- // same as every other CLI error — the `Error: …` line cliError prints.
454
- process.stderr.write(`Error: ${cleaned}\n`);
442
+ // Non-JSON mode: Commander already wrote the "error: ..." line
443
+ // to stderr via configureOutput.writeErr before throwing the
444
+ // CommanderError. Nothing to do — exit with the original code.
455
445
  }
456
446
  process.exit(exitCode || 1);
457
447
  }
@@ -225,6 +225,23 @@ describe('--json shim: help shown for a failed invocation is an error envelope',
225
225
  expect(parsed.error).toMatch(/bogus/);
226
226
  });
227
227
 
228
+ it('astryx layout --json (group, no subcommand) emits ERR_MISSING_ARGUMENT, exit 1', async () => {
229
+ const {status, stdout, stderr} = await runCli(['layout', '--json']);
230
+ expect(status).toBe(1);
231
+ expect(stderr).toBe('');
232
+ const parsed = parseJson(stdout);
233
+ expect(parsed).not.toHaveProperty('type');
234
+ expect(parsed.code).toBe('ERR_MISSING_ARGUMENT');
235
+ expect(parsed.suggestions.map((s) => s.name)).toContain('layout expand');
236
+ });
237
+
238
+ it('astryx layout (no --json) still prints help to stderr, exit 1', async () => {
239
+ const {status, stdout, stderr} = await runCli(['layout']);
240
+ expect(status).toBe(1);
241
+ expect(stdout).toBe('');
242
+ expect(stderr).toMatch(/Usage: astryx layout/);
243
+ });
244
+
228
245
  it('the real binary emits the same envelope for help bogus --json', () => {
229
246
  // One program per process, so the root's own outputHelp patch is used here.
230
247
  const bin = fileURLToPath(new URL('../bin/astryx.mjs', import.meta.url));
@@ -240,9 +257,6 @@ describe('--json shim: help shown for a failed invocation is an error envelope',
240
257
  });
241
258
 
242
259
  it('a group added after install gets the same error envelope', () => {
243
- // This is the only coverage of the no-subcommand path: every group the CLI
244
- // ships has an action of its own (see command-result-coverage.test.mjs), so
245
- // a group without one has to be built here.
246
260
  const program = new Command('astryx');
247
261
  installJsonShim(program);
248
262
  // Added later, so only the prototype-level patch covers its outputHelp.
@@ -268,11 +282,11 @@ describe('--json shim: help shown for a failed invocation is an error envelope',
268
282
  expect(parsed.suggestions).toEqual([{name: 'late child', reason: 'available subcommand'}]);
269
283
  });
270
284
 
271
- it('astryx help theme --json still emits the help envelope, exit 0', async () => {
272
- const {status, stdout} = await runCli(['help', 'theme', '--json']);
285
+ it('astryx help layout --json still emits the help envelope, exit 0', async () => {
286
+ const {status, stdout} = await runCli(['help', 'layout', '--json']);
273
287
  expect(status).toBe(0);
274
288
  const parsed = parseJson(stdout);
275
289
  expect(parsed.type).toBe('help');
276
- expect(parsed.data.command).toBe('astryx theme');
290
+ expect(parsed.data.command).toBe('astryx layout');
277
291
  });
278
292
  });