@astryxdesign/cli 0.6.4 → 0.6.5-canary.031021b

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 (288) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +97 -90
  3. package/api/build/build.doc.mjs +6 -1
  4. package/api/build/build.test.mjs +22 -0
  5. package/api/build/kit/kit.mjs +44 -5
  6. package/api/component/_adapter.d.mts +25 -0
  7. package/api/component/_adapter.mjs +59 -5
  8. package/api/component/component.d.mts +6 -3
  9. package/api/component/component.doc.mjs +37 -17
  10. package/api/component/component.mjs +249 -9
  11. package/api/component/component.type.d.mts +25 -0
  12. package/api/component/component.type.mjs +44 -0
  13. package/api/discover/_adapter.d.mts +114 -6
  14. package/api/discover/_adapter.mjs +372 -17
  15. package/api/discover/_adapter.test.mjs +215 -0
  16. package/api/discover/_catalog-view.d.mts +115 -0
  17. package/api/discover/_catalog-view.mjs +203 -0
  18. package/api/discover/_catalog-view.test.mjs +128 -0
  19. package/api/discover/detail/detail.d.mts +18 -6
  20. package/api/discover/detail/detail.mjs +67 -13
  21. package/api/discover/detail/detail.test.mjs +85 -0
  22. package/api/discover/detail/item/item.d.mts +26 -0
  23. package/api/discover/detail/item/item.mjs +78 -0
  24. package/api/discover/detail/item/item.test.mjs +73 -0
  25. package/api/discover/discover.d.mts +3 -9
  26. package/api/discover/discover.doc.mjs +61 -18
  27. package/api/discover/discover.mjs +220 -36
  28. package/api/discover/discover.test.mjs +11 -2
  29. package/api/discover/discover.type.d.mts +147 -8
  30. package/api/discover/discover.type.mjs +102 -12
  31. package/api/discover/list/list.d.mts +20 -6
  32. package/api/discover/list/list.mjs +45 -12
  33. package/api/discover/list/list.test.mjs +46 -0
  34. package/api/discover/search/search.d.mts +18 -16
  35. package/api/discover/search/search.mjs +102 -56
  36. package/api/discover/search/search.test.mjs +144 -10
  37. package/api/docs/_adapter.d.mts +8 -3
  38. package/api/docs/_adapter.mjs +14 -6
  39. package/api/docs/docOverlays.test.mjs +27 -1
  40. package/api/docs/docs.doc.mjs +2 -2
  41. package/api/doctor/doctor.d.mts +8 -3
  42. package/api/doctor/doctor.doc.mjs +17 -8
  43. package/api/doctor/doctor.mjs +90 -9
  44. package/api/doctor/doctor.test.mjs +122 -10
  45. package/api/doctor/doctor.type.d.mts +1 -1
  46. package/api/doctor/doctor.type.mjs +1 -1
  47. package/api/gap-report/gap-report.doc.mjs +19 -10
  48. package/api/hook/hook.doc.mjs +6 -3
  49. package/api/index.d.mts +1 -0
  50. package/api/index.mjs +5 -3
  51. package/api/init/init.doc.mjs +17 -12
  52. package/api/integration/add-helpers.d.mts +5 -2
  53. package/api/integration/add-helpers.mjs +36 -9
  54. package/api/integration/add-theme.mjs +22 -1
  55. package/api/integration/add-theme.test.mjs +34 -0
  56. package/api/integration/authoring-checks.mjs +2 -2
  57. package/api/integration/integrationPackCheck.doc.mjs +3 -3
  58. package/api/integration/pack-check.lifecycle-output.test.mjs +107 -0
  59. package/api/integration/pack-check.mjs +82 -9
  60. package/api/integration/pack-check.test.mjs +90 -0
  61. package/api/integration/pack-check.type.mjs +1 -1
  62. package/api/json/assertResponse.doc.mjs +1 -1
  63. package/api/json/isError.doc.mjs +1 -1
  64. package/api/search/search.d.mts +27 -1
  65. package/api/search/search.doc.mjs +2 -2
  66. package/api/search/search.mjs +228 -16
  67. package/api/swizzle/swizzle.doc.mjs +7 -5
  68. package/api/template/copy/copy.mjs +1 -1
  69. package/api/template/copy/copy.test.mjs +9 -0
  70. package/api/template/template.doc.mjs +2 -1
  71. package/api/theme/add/add.mjs +17 -25
  72. package/api/theme/add/add.rollback.test.mjs +158 -0
  73. package/api/theme/add/add.staging.test.mjs +40 -23
  74. package/api/theme/build/build.family.test.mjs +7 -12
  75. package/api/theme/build/build.mjs +8 -18
  76. package/api/theme/build/build.rollback.test.mjs +148 -0
  77. package/api/theme/generateTonalPalette.doc.mjs +1 -2
  78. package/api/theme/listThemes.doc.mjs +1 -1
  79. package/api/theme/themeAdd.doc.mjs +9 -10
  80. package/api/theme/themeBuild.doc.mjs +13 -13
  81. package/api/theme/themeList.doc.mjs +1 -1
  82. package/api/theme/themeListAvailable.doc.mjs +2 -1
  83. package/api/theme/themePaletteGenerate.doc.mjs +15 -8
  84. package/api/theme/themeTargets.doc.mjs +3 -2
  85. package/api/theme/themeTemplate.doc.mjs +2 -1
  86. package/api/upgrade/run/files-changed.test.mjs +111 -0
  87. package/api/upgrade/run/run.mjs +5 -3
  88. package/api/upgrade/upgrade.doc.mjs +24 -22
  89. package/api/upgrade/upgrade.type.mjs +2 -2
  90. package/assets/codemods/__tests__/runner.test.mjs +3 -1
  91. package/assets/codemods/file-count.test.mjs +163 -0
  92. package/assets/codemods/integration-runner.mjs +3 -3
  93. package/assets/codemods/runner.mjs +5 -4
  94. package/assets/docs/README.md +4 -2
  95. package/assets/docs/browser-support.doc.mjs +11 -11
  96. package/assets/docs/color.doc.mjs +8 -2
  97. package/assets/docs/elevation.doc.mjs +6 -4
  98. package/assets/docs/getting-started.doc.mjs +5 -16
  99. package/assets/docs/icons.doc.mjs +2 -21
  100. package/assets/docs/illustrations.doc.mjs +7 -15
  101. package/assets/docs/internationalization.doc.mjs +7 -5
  102. package/assets/docs/layout.doc.dense.mjs +130 -82
  103. package/assets/docs/layout.doc.mjs +133 -77
  104. package/assets/docs/migration.doc.mjs +19 -21
  105. package/assets/docs/motion.doc.mjs +16 -3
  106. package/assets/docs/principles.doc.dense.mjs +5 -5
  107. package/assets/docs/principles.doc.mjs +8 -0
  108. package/assets/docs/principles.doc.zh.mjs +6 -6
  109. package/assets/docs/shape.doc.mjs +8 -3
  110. package/assets/docs/spacing.doc.mjs +7 -2
  111. package/assets/docs/styling-libraries.doc.mjs +6 -2
  112. package/assets/docs/styling.doc.mjs +19 -23
  113. package/assets/docs/theme.doc.dense.mjs +58 -18
  114. package/assets/docs/theme.doc.mjs +57 -47
  115. package/assets/docs/theme.doc.zh.mjs +9 -8
  116. package/assets/docs/tokens.doc.dense.mjs +2 -2
  117. package/assets/docs/tokens.doc.mjs +389 -8
  118. package/assets/docs/tokens.doc.zh.mjs +2 -2
  119. package/assets/docs/tree/add-a-component.doc.mjs +75 -0
  120. package/assets/docs/tree/add-a-theme.doc.mjs +85 -0
  121. package/assets/docs/tree/add-a-topic.doc.mjs +144 -0
  122. package/assets/docs/tree/agent-guidance.doc.mjs +138 -0
  123. package/assets/docs/tree/block-template.doc.mjs +130 -0
  124. package/assets/docs/tree/build-the-template.doc.mjs +28 -0
  125. package/assets/docs/tree/building-blocks.doc.mjs +46 -0
  126. package/assets/docs/tree/check-your-docs.doc.mjs +137 -0
  127. package/assets/docs/tree/checks.doc.mjs +119 -0
  128. package/assets/docs/tree/codemods.doc.mjs +147 -0
  129. package/assets/docs/tree/component-family.doc.mjs +113 -0
  130. package/assets/docs/tree/component-imports.doc.mjs +69 -0
  131. package/assets/docs/tree/component-lookups.doc.mjs +149 -0
  132. package/assets/docs/tree/components.doc.mjs +23 -0
  133. package/assets/docs/tree/configuration.doc.mjs +23 -0
  134. package/assets/docs/tree/debug-and-gap-reports.doc.mjs +182 -0
  135. package/assets/docs/tree/define-the-theme.doc.mjs +118 -0
  136. package/assets/docs/tree/describe-the-component.doc.mjs +57 -0
  137. package/assets/docs/tree/docs.doc.mjs +21 -0
  138. package/assets/docs/tree/document-the-template.doc.mjs +28 -0
  139. package/assets/docs/tree/document-the-theme.doc.mjs +68 -0
  140. package/assets/docs/tree/export-template-assets.doc.mjs +147 -0
  141. package/assets/docs/tree/extend-or-replace.doc.mjs +103 -0
  142. package/assets/docs/tree/fonts-and-assets.doc.mjs +106 -0
  143. package/assets/docs/tree/generate-a-palette.doc.mjs +66 -0
  144. package/assets/docs/tree/grade-template-with-agent.doc.mjs +105 -0
  145. package/assets/docs/tree/help.doc.mjs +16 -0
  146. package/assets/docs/tree/integrations.doc.mjs +25 -451
  147. package/assets/docs/tree/links.doc.mjs +98 -0
  148. package/assets/docs/tree/package-and-test.doc.mjs +32 -0
  149. package/assets/docs/tree/page-template.doc.mjs +71 -0
  150. package/assets/docs/tree/publishing.doc.mjs +111 -0
  151. package/assets/docs/tree/quick-start.doc.mjs +272 -0
  152. package/assets/docs/tree/replace-a-core-component.doc.mjs +104 -0
  153. package/assets/docs/tree/replace-a-core-template.doc.mjs +172 -0
  154. package/assets/docs/tree/sections-and-placement.doc.mjs +108 -0
  155. package/assets/docs/tree/see-it-in-an-app.doc.mjs +59 -0
  156. package/assets/docs/tree/ship.doc.mjs +16 -0
  157. package/assets/docs/tree/short-and-findable.doc.mjs +108 -0
  158. package/assets/docs/tree/single-component.doc.mjs +165 -0
  159. package/assets/docs/tree/start-a-template.doc.mjs +143 -0
  160. package/assets/docs/tree/subcomponent.doc.mjs +115 -0
  161. package/assets/docs/tree/template-assets.doc.mjs +64 -0
  162. package/assets/docs/tree/template-doc-overview.doc.mjs +109 -0
  163. package/assets/docs/tree/template-fonts.doc.mjs +102 -0
  164. package/assets/docs/tree/template-grading-rubric.doc.mjs +452 -0
  165. package/assets/docs/tree/template-icons.doc.mjs +97 -0
  166. package/assets/docs/tree/template-images-media.doc.mjs +127 -0
  167. package/assets/docs/tree/template-styles.doc.mjs +93 -0
  168. package/assets/docs/tree/templates.doc.mjs +34 -0
  169. package/assets/docs/tree/test-in-an-app.doc.mjs +115 -0
  170. package/assets/docs/tree/test-template-in-app.doc.mjs +128 -0
  171. package/assets/docs/tree/themes.doc.mjs +39 -0
  172. package/assets/docs/tree/troubleshooting.doc.mjs +149 -0
  173. package/assets/docs/tree/upgrading.doc.mjs +103 -0
  174. package/assets/docs/tree/use-a-theme-in-an-app.doc.mjs +51 -0
  175. package/assets/docs/tree/verify-packed-template.doc.mjs +77 -0
  176. package/assets/docs/tree/versioning.doc.mjs +161 -0
  177. package/assets/docs/tree/write-good-templates.doc.mjs +64 -0
  178. package/assets/docs/tree/write-the-template-file.doc.mjs +154 -0
  179. package/assets/docs/typography.doc.mjs +24 -4
  180. package/assets/docs/working-with-ai.doc.mjs +30 -22
  181. package/assets/templates/blocks/components/InternationalizationProvider/InternationalizationProvider01ShippedLocale.tsx +1 -1
  182. package/authoring/config/config.doc.mjs +9 -1
  183. package/authoring/config/parse.d.mts +2 -0
  184. package/authoring/config/parse.mjs +19 -0
  185. package/authoring/config/parse.test.mjs +8 -0
  186. package/authoring/config/type.ts +11 -0
  187. package/authoring/discover/discover.doc.d.mts +13 -0
  188. package/authoring/discover/discover.doc.mjs +138 -0
  189. package/authoring/discover/parse.d.mts +24 -0
  190. package/authoring/discover/parse.mjs +128 -0
  191. package/authoring/discover/parse.test.mjs +124 -0
  192. package/authoring/discover/type.ts +87 -0
  193. package/authoring/doctypes/_schema.d.mts +3 -2
  194. package/authoring/doctypes/_schema.mjs +6 -0
  195. package/authoring/doctypes/base/graph-fields.doc.mjs +3 -3
  196. package/authoring/doctypes/base/type.ts +4 -2
  197. package/authoring/doctypes/component/component.doc.mjs +6 -0
  198. package/authoring/doctypes/component/type.ts +8 -0
  199. package/authoring/doctypes/reference/reference.doc.mjs +7 -0
  200. package/authoring/doctypes/reference/type.ts +5 -0
  201. package/authoring/doctypes/schema/schema.doc.mjs +2 -2
  202. package/authoring/doctypes/template/template.doc.mjs +1 -1
  203. package/authoring/doctypes/template/type.ts +2 -2
  204. package/authoring/index.d.mts +1 -0
  205. package/authoring/index.d.ts +10 -0
  206. package/authoring/index.mjs +1 -0
  207. package/authoring/integration/integration.doc.mjs +12 -10
  208. package/clients/cli/commands/component/index.mjs +152 -55
  209. package/clients/cli/commands/component-batch.test.mjs +341 -0
  210. package/clients/cli/commands/component-ownership.test.mjs +89 -0
  211. package/clients/cli/commands/component.doc.mjs +27 -9
  212. package/clients/cli/commands/discover.doc.mjs +53 -9
  213. package/clients/cli/commands/discover.mjs +393 -118
  214. package/clients/cli/commands/discover.sources.test.mjs +267 -0
  215. package/clients/cli/commands/docs.doc.mjs +1 -1
  216. package/clients/cli/commands/docs.mjs +60 -17
  217. package/clients/cli/commands/doctor-integration-docs.doc.mjs +3 -2
  218. package/clients/cli/commands/doctor-integration.test.mjs +53 -0
  219. package/clients/cli/commands/doctor.doc.mjs +3 -1
  220. package/clients/cli/commands/doctor.mjs +49 -5
  221. package/clients/cli/commands/gap-report.doc.mjs +10 -9
  222. package/clients/cli/commands/init.doc.mjs +9 -6
  223. package/clients/cli/commands/integration-add.doc.mjs +9 -9
  224. package/clients/cli/commands/integration-authoring.test.mjs +61 -10
  225. package/clients/cli/commands/integration-pack.doc.mjs +5 -9
  226. package/clients/cli/commands/integration-real-world.test.mjs +1 -1
  227. package/clients/cli/commands/integration-verify.doc.mjs +22 -0
  228. package/clients/cli/commands/integration.doc.mjs +4 -4
  229. package/clients/cli/commands/integration.mjs +74 -43
  230. package/clients/cli/commands/manifest.doc.mjs +1 -1
  231. package/clients/cli/commands/search.doc.mjs +10 -3
  232. package/clients/cli/commands/search.mjs +21 -2
  233. package/clients/cli/commands/search.test.mjs +21 -4
  234. package/clients/cli/commands/swizzle.doc.mjs +1 -1
  235. package/clients/cli/commands/template.doc.mjs +1 -1
  236. package/clients/cli/commands/text-json-parity.test.mjs +7 -1
  237. package/clients/cli/commands/theme-add.doc.mjs +1 -1
  238. package/clients/cli/commands/theme-palette-generate.doc.mjs +3 -2
  239. package/clients/cli/commands/theme-palette.doc.mjs +1 -2
  240. package/clients/cli/commands/theme-targets.doc.mjs +2 -2
  241. package/clients/cli/commands/theme.doc.mjs +2 -1
  242. package/clients/cli/commands/upgrade.doc.mjs +62 -3
  243. package/clients/cli/index.mjs +28 -6
  244. package/clients/cli/lib/define-command.mjs +28 -4
  245. package/clients/cli/lib/define-command.test.mjs +54 -0
  246. package/clients/cli/lib/exit-codes.test.mjs +17 -1
  247. package/clients/cli/lib/json-shim.mjs +24 -14
  248. package/clients/cli/lib/manifest.mjs +18 -5
  249. package/clients/cli/lib/manifest.test.mjs +5 -2
  250. package/clients/cli/lib/parse-error-format.test.mjs +81 -0
  251. package/foundation/agent-docs/agent-docs.mjs +1 -1
  252. package/foundation/discovery/authoring-self-docs.mjs +1 -0
  253. package/foundation/discovery/authoring-self-docs.test.mjs +6 -2
  254. package/foundation/discovery/cli-self-docs.mjs +16 -2
  255. package/foundation/discovery/cli-self-docs.test.mjs +20 -0
  256. package/foundation/discovery/docs-discovery.mjs +5 -1
  257. package/foundation/discovery/docs-discovery.test.mjs +21 -0
  258. package/foundation/discovery/docs-section-key.d.mts +1 -1
  259. package/foundation/discovery/docs-section-key.mjs +1 -1
  260. package/foundation/doc-compiler/doc-loads.test.mjs +3 -2
  261. package/foundation/doc-compiler/inputs.test.mjs +0 -1
  262. package/foundation/doc-compiler/tree.d.mts +4 -0
  263. package/foundation/doc-compiler/tree.mjs +6 -1
  264. package/foundation/integrations/cli-requirement.d.mts +26 -6
  265. package/foundation/integrations/cli-requirement.mjs +46 -11
  266. package/foundation/integrations/cli-requirement.test.mjs +7 -2
  267. package/foundation/integrations/contribution-inventory.mjs +1 -1
  268. package/foundation/integrations/integrations.d.mts +14 -1
  269. package/foundation/integrations/integrations.mjs +41 -1
  270. package/foundation/integrations/integrations.test.mjs +31 -0
  271. package/foundation/response/batch.type.d.mts +33 -0
  272. package/foundation/response/batch.type.mjs +34 -0
  273. package/foundation/response/error-codes.doc.mjs +6 -8
  274. package/foundation/response/error-codes.test.mjs +30 -5
  275. package/foundation/response/response-types.doc.d.mts +4 -3
  276. package/foundation/response/response-types.doc.mjs +40 -10
  277. package/foundation/response/response-types.doc.test.mjs +23 -0
  278. package/foundation/response/response.doc.mjs +11 -10
  279. package/package.json +9 -9
  280. package/api/docs/docs.test.mjs +0 -243
  281. package/api/docs/integration-tree.test.mjs +0 -555
  282. package/api/docs/integrationDocs.test.mjs +0 -314
  283. package/api/search/search.test.mjs +0 -512
  284. package/assets/docs/tree/integrations.test.mjs +0 -62
  285. package/assets/docs/tree/writing-docs.doc.mjs +0 -286
  286. package/clients/cli/commands/docs.test.mjs +0 -294
  287. package/foundation/agent-docs/agent-docs.test.mjs +0 -1159
  288. package/foundation/doc-compiler/tree.test.mjs +0 -598
@@ -38,6 +38,7 @@ import {doc as componentFn} from '../../../../api/component/component.doc.mjs';
38
38
  *
39
39
  * @typedef {(
40
40
  * | import('../../../../api/component/component.type.mjs').ComponentListResponse
41
+ * | import('../../../../api/component/component.type.mjs').ComponentBatchResponse
41
42
  * | import('../../../../api/component/component.type.mjs').ComponentDetailResponse
42
43
  * | import('../../../../api/component/component.type.mjs').ComponentDetailPropsResponse
43
44
  * | import('../../../../api/component/component.type.mjs').ComponentDetailSourceResponse
@@ -67,6 +68,11 @@ function summarize(result) {
67
68
  );
68
69
  return resultSet({count, resultKind: 'component'});
69
70
  }
71
+ case 'component.batch':
72
+ return resultSet({
73
+ count: result.data.results.filter(row => row.status === 'found').length,
74
+ resultKind: 'component',
75
+ });
70
76
  case 'component.detail':
71
77
  case 'component.detail.props':
72
78
  case 'component.detail.source':
@@ -82,19 +88,103 @@ function summarize(result) {
82
88
  }
83
89
  }
84
90
 
91
+ /**
92
+ * Project one single-component API result through the existing formatter kit.
93
+ * @param {import('../../../../api/component/component.type.mjs').ComponentSingleResponse} result
94
+ * @param {string} requestedName
95
+ * @param {'full'|'compact'|'brief'} detail
96
+ * @param {ReturnType<typeof resolveTheme>} themeData
97
+ * @returns {import('../../formatters/index.mjs').Block[]}
98
+ */
99
+ function componentDetailBlocks(result, requestedName, detail, themeData) {
100
+ const resolvedName = (requestedName.split('/').pop() ?? requestedName).replace(
101
+ /^XDS/,
102
+ '',
103
+ );
104
+ switch (result.type) {
105
+ case 'component.detail': {
106
+ /** @type {import('../../formatters/index.mjs').Block[]} */
107
+ const out = [];
108
+ if (result.data.parentDoc) {
109
+ out.push(record(result.data, {fields: ['parentDoc']}));
110
+ }
111
+ out.push(
112
+ detail === 'brief'
113
+ ? code(
114
+ formatBrief(result.data, resolvedName, result.data.import, {
115
+ themeData,
116
+ }),
117
+ )
118
+ : detail === 'compact'
119
+ ? code(
120
+ formatCompact(result.data, resolvedName, result.data.import),
121
+ )
122
+ : code(
123
+ formatFull(result.data, {
124
+ themeData,
125
+ importHint: result.data.import,
126
+ }),
127
+ ),
128
+ );
129
+ return out;
130
+ }
131
+ case 'component.detail.props':
132
+ return [code(formatProps({props: result.data}, resolvedName))];
133
+ case 'component.detail.source':
134
+ case 'component.detail.showcase':
135
+ return [code(result.data.source)];
136
+ case 'component.detail.blocks': {
137
+ const {showcase, examples, related} = result.data;
138
+ /** @type {import('../../formatters/index.mjs').Block[]} */
139
+ const out = [];
140
+ if (showcase) {
141
+ out.push(
142
+ section('Showcase'),
143
+ record(showcase, {fields: ['displayName', 'description']}),
144
+ );
145
+ }
146
+ if (examples.length > 0) {
147
+ out.push(
148
+ section('Examples'),
149
+ records(examples, {fields: ['name', 'description']}),
150
+ );
151
+ }
152
+ if (related.length > 0) {
153
+ out.push(
154
+ section(
155
+ `Related: ${related.length} blocks that use ${result.data.component}`,
156
+ ),
157
+ list(related.map(block => block.name)),
158
+ );
159
+ }
160
+ if (!showcase && examples.length === 0 && related.length === 0) {
161
+ out.push(text(`No blocks found for ${result.data.component}`));
162
+ }
163
+ return out;
164
+ }
165
+ default:
166
+ return [];
167
+ }
168
+ }
169
+
85
170
  /**
86
171
  * @param {import('commander').Command} program
87
172
  */
88
173
  export function registerComponent(program) {
89
174
  defineCommand(program, componentCommand, {
90
175
  fn: componentFn,
91
- action: async (/** @type {string | undefined} */ name, /** @type {{list?: boolean, category?: string, props?: boolean, source?: boolean, showcase?: boolean, blocks?: boolean, package?: string}} */ options) => {
176
+ action: async (
177
+ /** @type {string[] | undefined} */ names,
178
+ /** @type {{list?: boolean, category?: string, props?: boolean, source?: boolean, showcase?: boolean, blocks?: boolean, package?: string}} */ options,
179
+ ) => {
92
180
  const run = getCliInvocation();
181
+ const name = names?.length === 1 ? names[0] : undefined;
182
+ const apiInput = !names?.length ? undefined : name ?? names;
93
183
  const zh = program.opts().zh || false;
94
184
  const dense = program.opts().dense || false;
95
185
  const lang = program.opts().lang || null;
96
186
  const detailSource = program.getOptionValueSource('detail');
97
- const isListView = options.list || options.category || !name;
187
+ const isListView = options.list || options.category || !names?.length;
98
188
  // Default detail level is full for single-component view, brief for list views.
99
189
  // (List views are scannable name lists; users can opt into compact/full.)
100
190
  let detail = program.opts().detail || 'full';
@@ -119,7 +209,7 @@ export function registerComponent(program) {
119
209
  /** @type {ComponentResult} */
120
210
  let result;
121
211
  try {
122
- result = /** @type {ComponentResult} */ (await componentApi(name, {
212
+ result = /** @type {ComponentResult} */ (await componentApi(apiInput, {
123
213
  cwd: process.cwd(),
124
214
  list: options.list,
125
215
  category: options.category,
@@ -137,6 +227,12 @@ export function registerComponent(program) {
137
227
  }
138
228
 
139
229
  const answered = summarize(result);
230
+ if (
231
+ result.type === 'component.batch' &&
232
+ result.data.results.some(row => row.status !== 'found')
233
+ ) {
234
+ process.exitCode = 1;
235
+ }
140
236
  if (json) {
141
237
  jsonOut(result);
142
238
  return answered;
@@ -152,7 +248,7 @@ export function registerComponent(program) {
152
248
  const listFooter = text(
153
249
  [
154
250
  `Import from the path shown (e.g. import {Button} from '@astryxdesign/core/Button')`,
155
- `Usage: ${run} component <name>`,
251
+ `Usage: ${run} component <name> [name...]`,
156
252
  ].join('\n'),
157
253
  );
158
254
 
@@ -231,64 +327,65 @@ export function registerComponent(program) {
231
327
  break;
232
328
  }
233
329
 
234
- case 'component.detail': {
235
- const resolvedName = (name || '').replace(/^XDS/, '');
236
- const importHint = result.data.import;
237
- if (result.data.parentDoc) emit(record(result.data, {fields: ['parentDoc']}));
238
- const doc =
239
- detail === 'brief'
240
- ? code(formatBrief(result.data, resolvedName, importHint, {themeData}))
241
- : detail === 'compact'
242
- ? code(formatCompact(result.data, resolvedName, importHint))
243
- : code(formatFull(result.data, {themeData, importHint}));
244
- emit(doc);
245
- break;
246
- }
247
-
248
- case 'component.detail.props': {
249
- const resolvedName = (name || '').replace(/^XDS/, '');
250
- emit(code(formatProps({props: result.data}, resolvedName)));
251
- break;
252
- }
253
-
254
- case 'component.detail.source': {
255
- emit(code(result.data.source));
256
- break;
257
- }
258
-
259
- case 'component.detail.showcase': {
260
- emit(code(result.data.source));
261
- break;
262
- }
263
-
264
- case 'component.detail.blocks': {
265
- const {showcase, examples, related} = result.data;
330
+ case 'component.batch': {
266
331
  /** @type {import('../../formatters/index.mjs').Block[]} */
267
- const out = [];
268
- if (showcase) {
269
- out.push(
270
- section('Showcase'),
271
- record(showcase, {fields: ['displayName', 'description']}),
272
- );
273
- }
274
- if (examples.length > 0) {
275
- out.push(
276
- section('Examples'),
277
- records(examples, {fields: ['name', 'description']}),
278
- );
279
- }
280
- if (related.length > 0) {
332
+ const out = [
333
+ section('Component batch'),
334
+ record({count: result.data.count}),
335
+ section('Results'),
336
+ ];
337
+ for (const row of result.data.results) {
338
+ out.push(section(row.selector));
339
+ if (row.status === 'found') {
340
+ out.push(
341
+ record(row, {fields: ['selector', 'status']}),
342
+ section('Result'),
343
+ ...componentDetailBlocks(
344
+ row.result,
345
+ row.selector,
346
+ detail,
347
+ themeData,
348
+ ),
349
+ );
350
+ continue;
351
+ }
281
352
  out.push(
282
- section(`Related: ${related.length} blocks that use ${result.data.component}`),
283
- list(related.map(b => b.name)),
353
+ record(row, {
354
+ fields: ['selector', 'status', 'code', 'error'],
355
+ }),
284
356
  );
285
- }
286
- if (!showcase && examples.length === 0 && related.length === 0) {
287
- out.push(text(`No blocks found for ${result.data.component}`));
357
+ if (row.status === 'ambiguous') {
358
+ out.push(
359
+ section('Candidates'),
360
+ records(row.candidates, {
361
+ fields: ['package', 'component', 'kind', 'installed'],
362
+ }),
363
+ );
364
+ } else if (row.suggestions?.length) {
365
+ out.push(
366
+ section('Suggestions'),
367
+ records(row.suggestions, {fields: ['name', 'reason']}),
368
+ );
369
+ }
288
370
  }
289
371
  emit(...out);
290
372
  break;
291
373
  }
374
+
375
+ case 'component.detail':
376
+ case 'component.detail.props':
377
+ case 'component.detail.source':
378
+ case 'component.detail.showcase':
379
+ case 'component.detail.blocks':
380
+ emit(
381
+ ...componentDetailBlocks(
382
+ result,
383
+ name ?? '',
384
+ detail,
385
+ themeData,
386
+ ),
387
+ );
388
+ break;
292
389
  }
293
390
  return answered;
294
391
  },
@@ -0,0 +1,341 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Multi-selector component lookup contract: one ordered batch envelope,
5
+ * complete per-selector states, unchanged single-selector responses, and matching
6
+ * text/JSON exit behavior.
7
+ */
8
+
9
+ import {describe, it, expect} from 'vitest';
10
+ import * as path from 'node:path';
11
+ import {
12
+ component,
13
+ COMPONENT_BATCH_SELECTOR_LIMIT,
14
+ } from '../../../api/component/component.mjs';
15
+ import {runCli} from '../../../test-utils/run-cli.mjs';
16
+
17
+ const REPO_ROOT = path.resolve(import.meta.dirname, '../../../../..');
18
+ const SLOW = 60_000;
19
+
20
+ describe('component() batch lookup', () => {
21
+ it(
22
+ 'keeps one found row per selector, in order, including duplicates',
23
+ async () => {
24
+ const result = await component(['Button', 'Badge', 'Button'], {
25
+ cwd: REPO_ROOT,
26
+ detail: 'brief',
27
+ });
28
+
29
+ expect(result.type).toBe('component.batch');
30
+ expect(result.data.count).toBe(3);
31
+ expect(result.data.results.map(row => row.selector)).toEqual([
32
+ 'Button',
33
+ 'Badge',
34
+ 'Button',
35
+ ]);
36
+ expect(result.data.results.map(row => row.status)).toEqual([
37
+ 'found',
38
+ 'found',
39
+ 'found',
40
+ ]);
41
+ expect(
42
+ result.data.results.map(row =>
43
+ row.status === 'found' ? row.result.type : null,
44
+ ),
45
+ ).toEqual(['component.detail', 'component.detail', 'component.detail']);
46
+ },
47
+ SLOW,
48
+ );
49
+
50
+ it(
51
+ 'applies every focused projection to each found row',
52
+ async () => {
53
+ const projections = [
54
+ {options: {props: true}, expectedType: 'component.detail.props'},
55
+ {options: {source: true}, expectedType: 'component.detail.source'},
56
+ {options: {showcase: true}, expectedType: 'component.detail.showcase'},
57
+ {options: {blocks: true}, expectedType: 'component.detail.blocks'},
58
+ ];
59
+ for (const projection of projections) {
60
+ const result = await component(['Button', 'Card'], {
61
+ cwd: REPO_ROOT,
62
+ ...projection.options,
63
+ });
64
+ expect(result.type).toBe('component.batch');
65
+ expect(
66
+ result.data.results.map(row =>
67
+ row.status === 'found' ? row.result.type : null,
68
+ ),
69
+ ).toEqual([projection.expectedType, projection.expectedType]);
70
+ }
71
+ },
72
+ SLOW,
73
+ );
74
+
75
+ it(
76
+ 'uses input shape to distinguish catalog, single, and batch calls',
77
+ async () => {
78
+ expect((await component(undefined, {cwd: REPO_ROOT})).type).toBe(
79
+ 'component.list',
80
+ );
81
+ expect((await component('Button', {cwd: REPO_ROOT})).type).toBe(
82
+ 'component.detail',
83
+ );
84
+
85
+ const emptyBatch = await component([], {cwd: REPO_ROOT});
86
+ expect(emptyBatch).toEqual({
87
+ type: 'component.batch',
88
+ data: {count: 0, results: []},
89
+ });
90
+ expect(
91
+ await component([], {
92
+ cwd: REPO_ROOT,
93
+ list: true,
94
+ category: 'Form',
95
+ }),
96
+ ).toEqual(emptyBatch);
97
+
98
+ const oneRowBatch = await component(['Button'], {
99
+ cwd: REPO_ROOT,
100
+ detail: 'brief',
101
+ });
102
+ expect(oneRowBatch).toMatchObject({
103
+ type: 'component.batch',
104
+ data: {
105
+ count: 1,
106
+ results: [
107
+ {
108
+ selector: 'Button',
109
+ status: 'found',
110
+ result: {type: 'component.detail'},
111
+ },
112
+ ],
113
+ },
114
+ });
115
+ },
116
+ SLOW,
117
+ );
118
+
119
+ it('rejects an oversized array before component resolution starts', async () => {
120
+ const selectors = Array.from(
121
+ {length: COMPONENT_BATCH_SELECTOR_LIMIT + 1},
122
+ () => 'Button',
123
+ );
124
+ await expect(
125
+ component(selectors, {
126
+ cwd: path.join(REPO_ROOT, 'missing-batch-cwd'),
127
+ }),
128
+ ).rejects.toMatchObject({
129
+ code: 'ERR_INVALID_ARGUMENT',
130
+ message: `Component batch accepts at most ${COMPONENT_BATCH_SELECTOR_LIMIT} selectors; received ${selectors.length}`,
131
+ });
132
+ });
133
+
134
+ it(
135
+ 'keeps receipt invariants across a deterministic selector chaos matrix',
136
+ async () => {
137
+ const found = new Set(['Button', 'Card']);
138
+ const selectorCases = [
139
+ [],
140
+ ['Button'],
141
+ ['ZzzNope99'],
142
+ ['Button', 'Button'],
143
+ ['ZzzNope98', 'ZzzNope99'],
144
+ ['Button', 'ZzzNope99', 'Card'],
145
+ ['ZzzNope99', 'Button', 'ZzzNope99', 'Button'],
146
+ ['Card', 'Button', 'Card', 'ZzzNope98', 'Button'],
147
+ ];
148
+
149
+ for (const selectors of selectorCases) {
150
+ const result = await component(selectors, {
151
+ cwd: REPO_ROOT,
152
+ detail: 'brief',
153
+ });
154
+ expect(result.type).toBe('component.batch');
155
+ expect(result.data.count).toBe(selectors.length);
156
+ expect(result.data.results.map(row => row.selector)).toEqual(selectors);
157
+ expect(result.data.results.map(row => row.status)).toEqual(
158
+ selectors.map(selector =>
159
+ found.has(selector) ? 'found' : 'not_found',
160
+ ),
161
+ );
162
+ for (const row of result.data.results) {
163
+ if (row.status === 'found') {
164
+ expect(row.result.type).toBe('component.detail');
165
+ } else {
166
+ expect(row.code).toBe('ERR_UNKNOWN_COMPONENT');
167
+ }
168
+ }
169
+ }
170
+ },
171
+ SLOW,
172
+ );
173
+
174
+ it(
175
+ 'composes every detail and language control over a batch',
176
+ async () => {
177
+ /** @type {Array<{detail?: 'full'|'compact'|'brief'; lang?: string; zh?: boolean; dense?: boolean}>} */
178
+ const controls = [
179
+ {detail: 'brief'},
180
+ {detail: 'compact'},
181
+ {detail: 'full'},
182
+ {lang: 'en'},
183
+ {lang: 'dense'},
184
+ {zh: true},
185
+ {dense: true},
186
+ ];
187
+ for (const control of controls) {
188
+ const result = await component(['Button', 'Card'], {
189
+ cwd: REPO_ROOT,
190
+ ...control,
191
+ });
192
+ expect(result.type).toBe('component.batch');
193
+ expect(result.data.results).toHaveLength(2);
194
+ expect(
195
+ result.data.results.every(
196
+ row =>
197
+ row.status === 'found' && row.result.type === 'component.detail',
198
+ ),
199
+ ).toBe(true);
200
+ }
201
+ },
202
+ SLOW,
203
+ );
204
+ });
205
+
206
+ describe('astryx component batch lookup', () => {
207
+ it('documents variadic selector grammar in --help', async () => {
208
+ const run = await runCli(['component', '--help'], REPO_ROOT);
209
+ expect(run.code).toBe(0);
210
+ expect(run.stdout).toContain(
211
+ 'Usage: astryx component [options] [names...]',
212
+ );
213
+ expect(run.stdout).toContain('@scope/package@version/Name');
214
+ expect(run.stdout).toContain('Two or more return one ordered batch');
215
+ expect(run.stdout).toContain('At most 100 selectors');
216
+ });
217
+
218
+ it(
219
+ 'keeps catalog and single-result CLI envelopes at zero and one selectors',
220
+ async () => {
221
+ const catalog = await runCli(['--json', 'component'], REPO_ROOT);
222
+ expect(catalog.code).toBe(0);
223
+ expect(JSON.parse(catalog.stdout).type).toBe('component.list');
224
+
225
+ const single = await runCli(
226
+ ['--json', '--detail', 'brief', 'component', 'Button'],
227
+ REPO_ROOT,
228
+ );
229
+ expect(single.code).toBe(0);
230
+ expect(JSON.parse(single.stdout).type).toBe('component.detail');
231
+ },
232
+ SLOW,
233
+ );
234
+
235
+ it(
236
+ 'emits one component.batch JSON envelope for several found selectors',
237
+ async () => {
238
+ const run = await runCli(
239
+ ['--json', '--detail', 'brief', 'component', 'Button', 'Badge'],
240
+ REPO_ROOT,
241
+ );
242
+ expect(run.code).toBe(0);
243
+ const envelope = JSON.parse(run.stdout);
244
+ expect(envelope.type).toBe('component.batch');
245
+ expect(envelope.data.count).toBe(2);
246
+ expect(envelope.data.results.map(row => row.selector)).toEqual([
247
+ 'Button',
248
+ 'Badge',
249
+ ]);
250
+ expect(envelope.data.results.every(row => row.status === 'found')).toBe(
251
+ true,
252
+ );
253
+ },
254
+ SLOW,
255
+ );
256
+
257
+ it(
258
+ 'rejects an oversized batch before emitting JSON or text results',
259
+ async () => {
260
+ const selectors = Array.from(
261
+ {length: COMPONENT_BATCH_SELECTOR_LIMIT + 1},
262
+ () => 'Button',
263
+ );
264
+ const json = await runCli(
265
+ ['--json', 'component', ...selectors],
266
+ REPO_ROOT,
267
+ );
268
+ expect(json.code).toBe(1);
269
+ expect(JSON.parse(json.stdout)).toMatchObject({
270
+ code: 'ERR_INVALID_ARGUMENT',
271
+ error: `Component batch accepts at most ${COMPONENT_BATCH_SELECTOR_LIMIT} selectors; received ${selectors.length}`,
272
+ });
273
+ expect(JSON.parse(json.stdout).type).toBeUndefined();
274
+
275
+ const text = await runCli(['component', ...selectors], REPO_ROOT);
276
+ expect(text.code).toBe(1);
277
+ expect(text.stdout + text.stderr).toContain(
278
+ `accepts at most ${COMPONENT_BATCH_SELECTOR_LIMIT} selectors`,
279
+ );
280
+ },
281
+ SLOW,
282
+ );
283
+
284
+ it(
285
+ 'emits every row and exits 1 when one selector is not found',
286
+ async () => {
287
+ const run = await runCli(
288
+ ['--json', '--detail', 'brief', 'component', 'Button', 'ZzzNope99'],
289
+ REPO_ROOT,
290
+ );
291
+ expect(run.code).toBe(1);
292
+ const envelope = JSON.parse(run.stdout);
293
+ expect(envelope.type).toBe('component.batch');
294
+ expect(envelope.data.results).toMatchObject([
295
+ {selector: 'Button', status: 'found'},
296
+ {
297
+ selector: 'ZzzNope99',
298
+ status: 'not_found',
299
+ code: 'ERR_UNKNOWN_COMPONENT',
300
+ },
301
+ ]);
302
+ },
303
+ SLOW,
304
+ );
305
+
306
+ it(
307
+ 'emits every failed row and exits 1 when no selector resolves',
308
+ async () => {
309
+ const run = await runCli(
310
+ ['--json', 'component', 'ZzzNope98', 'ZzzNope99'],
311
+ REPO_ROOT,
312
+ );
313
+ expect(run.code).toBe(1);
314
+ const envelope = JSON.parse(run.stdout);
315
+ expect(envelope.type).toBe('component.batch');
316
+ expect(envelope.data.count).toBe(2);
317
+ expect(envelope.data.results).toMatchObject([
318
+ {selector: 'ZzzNope98', status: 'not_found'},
319
+ {selector: 'ZzzNope99', status: 'not_found'},
320
+ ]);
321
+ },
322
+ SLOW,
323
+ );
324
+
325
+ it(
326
+ 'projects every row in text and keeps the same failing exit status',
327
+ async () => {
328
+ const run = await runCli(
329
+ ['--detail', 'brief', 'component', 'Button', 'ZzzNope99'],
330
+ REPO_ROOT,
331
+ );
332
+ expect(run.code).toBe(1);
333
+ expect(run.stdout).toContain('count: 2');
334
+ expect(run.stdout).toMatch(/selector: Button\nstatus:\s+found/);
335
+ expect(run.stdout).toMatch(
336
+ /selector: ZzzNope99\nstatus:\s+not_found\ncode:\s+ERR_UNKNOWN_COMPONENT/,
337
+ );
338
+ },
339
+ SLOW,
340
+ );
341
+ });