@astryxdesign/cli 0.6.4 → 0.6.5-canary.01972bc

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 (405) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +103 -95
  3. package/api/build/_adapter.d.mts +36 -2
  4. package/api/build/_adapter.mjs +41 -10
  5. package/api/build/build.doc.mjs +8 -3
  6. package/api/build/build.test.mjs +60 -2
  7. package/api/build/kit/kit.mjs +109 -26
  8. package/api/build/kit/rank.d.mts +24 -8
  9. package/api/build/kit/rank.mjs +277 -97
  10. package/api/build/kit/rank.test.mjs +231 -48
  11. package/api/component/_adapter.d.mts +25 -0
  12. package/api/component/_adapter.mjs +59 -5
  13. package/api/component/component.d.mts +6 -3
  14. package/api/component/component.doc.mjs +37 -17
  15. package/api/component/component.mjs +249 -9
  16. package/api/component/component.type.d.mts +25 -0
  17. package/api/component/component.type.mjs +44 -0
  18. package/api/discover/_adapter.d.mts +114 -6
  19. package/api/discover/_adapter.mjs +372 -17
  20. package/api/discover/_adapter.test.mjs +215 -0
  21. package/api/discover/_catalog-view.d.mts +115 -0
  22. package/api/discover/_catalog-view.mjs +203 -0
  23. package/api/discover/_catalog-view.test.mjs +128 -0
  24. package/api/discover/detail/detail.d.mts +18 -6
  25. package/api/discover/detail/detail.mjs +67 -13
  26. package/api/discover/detail/detail.test.mjs +85 -0
  27. package/api/discover/detail/item/item.d.mts +26 -0
  28. package/api/discover/detail/item/item.mjs +78 -0
  29. package/api/discover/detail/item/item.test.mjs +73 -0
  30. package/api/discover/discover.d.mts +3 -9
  31. package/api/discover/discover.doc.mjs +61 -18
  32. package/api/discover/discover.mjs +220 -36
  33. package/api/discover/discover.test.mjs +11 -2
  34. package/api/discover/discover.type.d.mts +147 -8
  35. package/api/discover/discover.type.mjs +102 -12
  36. package/api/discover/list/list.d.mts +20 -6
  37. package/api/discover/list/list.mjs +45 -12
  38. package/api/discover/list/list.test.mjs +46 -0
  39. package/api/discover/search/search.d.mts +18 -16
  40. package/api/discover/search/search.mjs +102 -56
  41. package/api/discover/search/search.test.mjs +144 -10
  42. package/api/docs/_adapter.d.mts +8 -3
  43. package/api/docs/_adapter.mjs +14 -6
  44. package/api/docs/docOverlays.test.mjs +27 -1
  45. package/api/docs/docs.doc.mjs +2 -2
  46. package/api/docs/docs.test.mjs +54 -18
  47. package/api/docs/integration-tree.test.mjs +17 -0
  48. package/api/docs/integrationDocs.test.mjs +27 -5
  49. package/api/doctor/doctor.d.mts +8 -3
  50. package/api/doctor/doctor.doc.mjs +17 -8
  51. package/api/doctor/doctor.mjs +90 -9
  52. package/api/doctor/doctor.test.mjs +122 -10
  53. package/api/doctor/doctor.type.d.mts +1 -1
  54. package/api/doctor/doctor.type.mjs +1 -1
  55. package/api/error.d.mts +22 -0
  56. package/api/error.mjs +42 -0
  57. package/api/gap-report/gap-report.doc.mjs +19 -10
  58. package/api/hook/hook.doc.mjs +6 -3
  59. package/api/index.d.mts +1 -0
  60. package/api/index.mjs +5 -3
  61. package/api/init/init.doc.mjs +17 -12
  62. package/api/integration/add-contribution.d.mts +2 -1
  63. package/api/integration/add-contribution.mjs +7 -3
  64. package/api/integration/add-contribution.test.mjs +3 -3
  65. package/api/integration/add-helpers.d.mts +5 -2
  66. package/api/integration/add-helpers.mjs +36 -9
  67. package/api/integration/add-theme.mjs +266 -23
  68. package/api/integration/add-theme.test.mjs +247 -0
  69. package/api/integration/authoring-checks.mjs +12 -9
  70. package/api/integration/authoring-checks.type.d.mts +8 -0
  71. package/api/integration/authoring-checks.type.mjs +7 -3
  72. package/api/integration/integration-authoring.type.d.mts +4 -1
  73. package/api/integration/integration-authoring.type.mjs +6 -1
  74. package/api/integration/integrationAdd.doc.mjs +6 -0
  75. package/api/integration/integrationAddTheme.doc.mjs +10 -0
  76. package/api/integration/integrationComponentConflicts.doc.mjs +1 -1
  77. package/api/integration/integrationDocConflicts.doc.mjs +1 -1
  78. package/api/integration/integrationPackCheck.doc.mjs +3 -3
  79. package/api/integration/integrationTemplateConflicts.doc.mjs +1 -1
  80. package/api/integration/pack-check.lifecycle-output.test.mjs +107 -0
  81. package/api/integration/pack-check.mjs +92 -10
  82. package/api/integration/pack-check.test.mjs +140 -1
  83. package/api/integration/pack-check.type.mjs +1 -1
  84. package/api/integration/validate-integration.d.mts +4 -2
  85. package/api/integration/validate-integration.mjs +7 -2
  86. package/api/integration/validate-integration.test.mjs +55 -0
  87. package/api/integration/validate-integration.type.d.mts +5 -0
  88. package/api/integration/validate-integration.type.mjs +5 -1
  89. package/api/integration/validateIntegration.doc.mjs +1 -1
  90. package/api/json/assertResponse.doc.mjs +1 -1
  91. package/api/json/isError.doc.mjs +1 -1
  92. package/api/layout/expand/expand.mjs +12 -7
  93. package/api/layout/expand/expand.receipt.test.mjs +74 -0
  94. package/api/layout/layout.type.d.mts +1 -0
  95. package/api/layout/layout.type.mjs +1 -0
  96. package/api/layout/layoutExpand.doc.mjs +1 -1
  97. package/api/search/search.d.mts +51 -1
  98. package/api/search/search.doc.mjs +2 -2
  99. package/api/search/search.mjs +299 -17
  100. package/api/search/search.test.mjs +216 -18
  101. package/api/swizzle/copy/copy.mjs +66 -3
  102. package/api/swizzle/swizzle.doc.mjs +11 -5
  103. package/api/template/copy/copy.mjs +15 -9
  104. package/api/template/copy/copy.receipt.test.mjs +77 -0
  105. package/api/template/copy/copy.test.mjs +9 -0
  106. package/api/template/show/show.mjs +15 -4
  107. package/api/template/show/show.test.mjs +76 -0
  108. package/api/template/template-integration.test.mjs +14 -0
  109. package/api/template/template.d.mts +1 -1
  110. package/api/template/template.doc.mjs +8 -3
  111. package/api/template/template.mjs +1 -0
  112. package/api/template/template.type.d.mts +2 -0
  113. package/api/template/template.type.mjs +2 -0
  114. package/api/theme/add/add.mjs +17 -25
  115. package/api/theme/add/add.rollback.test.mjs +158 -0
  116. package/api/theme/add/add.staging.test.mjs +40 -23
  117. package/api/theme/build/build.d.mts +24 -0
  118. package/api/theme/build/build.family.test.mjs +7 -12
  119. package/api/theme/build/build.mjs +243 -26
  120. package/api/theme/build/build.project-core.test.mjs +165 -0
  121. package/api/theme/build/build.rollback.test.mjs +148 -0
  122. package/api/theme/generateTonalPalette.doc.mjs +1 -2
  123. package/api/theme/listThemes.doc.mjs +1 -1
  124. package/api/theme/themeAdd.doc.mjs +9 -10
  125. package/api/theme/themeBuild.doc.mjs +13 -13
  126. package/api/theme/themeList.doc.mjs +1 -1
  127. package/api/theme/themeListAvailable.doc.mjs +2 -1
  128. package/api/theme/themePaletteGenerate.doc.mjs +15 -8
  129. package/api/theme/themeTargets.doc.mjs +3 -2
  130. package/api/theme/themeTemplate.doc.mjs +2 -1
  131. package/api/upgrade/run/files-changed.test.mjs +111 -0
  132. package/api/upgrade/run/run.mjs +25 -6
  133. package/api/upgrade/run/run.test.mjs +45 -1
  134. package/api/upgrade/upgrade.doc.mjs +24 -22
  135. package/api/upgrade/upgrade.type.d.mts +1 -0
  136. package/api/upgrade/upgrade.type.mjs +3 -2
  137. package/assets/codemods/__tests__/runner.test.mjs +3 -1
  138. package/assets/codemods/file-count.test.mjs +163 -0
  139. package/assets/codemods/integration-runner.mjs +3 -3
  140. package/assets/codemods/runner.mjs +5 -4
  141. package/assets/docs/README.md +4 -2
  142. package/assets/docs/browser-support.doc.mjs +11 -11
  143. package/assets/docs/color.doc.mjs +8 -2
  144. package/assets/docs/elevation.doc.mjs +6 -4
  145. package/assets/docs/getting-started.doc.mjs +5 -16
  146. package/assets/docs/icons.doc.mjs +3 -21
  147. package/assets/docs/illustrations.doc.mjs +7 -15
  148. package/assets/docs/internationalization.doc.mjs +7 -5
  149. package/assets/docs/layout.doc.dense.mjs +130 -82
  150. package/assets/docs/layout.doc.mjs +133 -77
  151. package/assets/docs/migration.doc.mjs +19 -21
  152. package/assets/docs/motion.doc.mjs +16 -3
  153. package/assets/docs/principles.doc.dense.mjs +5 -5
  154. package/assets/docs/principles.doc.mjs +8 -0
  155. package/assets/docs/principles.doc.zh.mjs +6 -6
  156. package/assets/docs/shape.doc.mjs +8 -3
  157. package/assets/docs/spacing.doc.mjs +7 -2
  158. package/assets/docs/styling-libraries.doc.mjs +6 -2
  159. package/assets/docs/styling.doc.mjs +19 -23
  160. package/assets/docs/theme.doc.dense.mjs +58 -18
  161. package/assets/docs/theme.doc.mjs +57 -47
  162. package/assets/docs/theme.doc.zh.mjs +9 -8
  163. package/assets/docs/tokens.doc.dense.mjs +2 -2
  164. package/assets/docs/tokens.doc.mjs +389 -8
  165. package/assets/docs/tokens.doc.zh.mjs +2 -2
  166. package/assets/docs/tree/add-a-component.doc.mjs +75 -0
  167. package/assets/docs/tree/add-a-theme.doc.mjs +85 -0
  168. package/assets/docs/tree/add-a-topic.doc.mjs +144 -0
  169. package/assets/docs/tree/agent-guidance.doc.mjs +138 -0
  170. package/assets/docs/tree/block-template.doc.mjs +130 -0
  171. package/assets/docs/tree/build-the-template.doc.mjs +28 -0
  172. package/assets/docs/tree/building-blocks.doc.mjs +46 -0
  173. package/assets/docs/tree/check-your-docs.doc.mjs +137 -0
  174. package/assets/docs/tree/checks.doc.mjs +119 -0
  175. package/assets/docs/tree/codemods.doc.mjs +147 -0
  176. package/assets/docs/tree/component-family.doc.mjs +113 -0
  177. package/assets/docs/tree/component-imports.doc.mjs +69 -0
  178. package/assets/docs/tree/component-lookups.doc.mjs +149 -0
  179. package/assets/docs/tree/components.doc.mjs +23 -0
  180. package/assets/docs/tree/configuration.doc.mjs +23 -0
  181. package/assets/docs/tree/debug-and-gap-reports.doc.mjs +182 -0
  182. package/assets/docs/tree/define-the-theme.doc.mjs +118 -0
  183. package/assets/docs/tree/describe-the-component.doc.mjs +57 -0
  184. package/assets/docs/tree/docs.doc.mjs +21 -0
  185. package/assets/docs/tree/document-the-template.doc.mjs +28 -0
  186. package/assets/docs/tree/document-the-theme.doc.mjs +68 -0
  187. package/assets/docs/tree/export-template-assets.doc.mjs +147 -0
  188. package/assets/docs/tree/extend-or-replace.doc.mjs +103 -0
  189. package/assets/docs/tree/fonts-and-assets.doc.mjs +106 -0
  190. package/assets/docs/tree/generate-a-palette.doc.mjs +66 -0
  191. package/assets/docs/tree/grade-template-with-agent.doc.mjs +105 -0
  192. package/assets/docs/tree/help.doc.mjs +16 -0
  193. package/assets/docs/tree/integrations.doc.mjs +25 -451
  194. package/assets/docs/tree/links.doc.mjs +98 -0
  195. package/assets/docs/tree/package-and-test.doc.mjs +32 -0
  196. package/assets/docs/tree/page-template.doc.mjs +71 -0
  197. package/assets/docs/tree/publishing.doc.mjs +111 -0
  198. package/assets/docs/tree/quick-start.doc.mjs +272 -0
  199. package/assets/docs/tree/replace-a-core-component.doc.mjs +104 -0
  200. package/assets/docs/tree/replace-a-core-template.doc.mjs +172 -0
  201. package/assets/docs/tree/sections-and-placement.doc.mjs +108 -0
  202. package/assets/docs/tree/see-it-in-an-app.doc.mjs +59 -0
  203. package/assets/docs/tree/ship.doc.mjs +16 -0
  204. package/assets/docs/tree/short-and-findable.doc.mjs +108 -0
  205. package/assets/docs/tree/single-component.doc.mjs +165 -0
  206. package/assets/docs/tree/start-a-template.doc.mjs +143 -0
  207. package/assets/docs/tree/subcomponent.doc.mjs +115 -0
  208. package/assets/docs/tree/template-assets.doc.mjs +64 -0
  209. package/assets/docs/tree/template-doc-overview.doc.mjs +121 -0
  210. package/assets/docs/tree/template-fonts.doc.mjs +102 -0
  211. package/assets/docs/tree/template-grading-rubric.doc.mjs +452 -0
  212. package/assets/docs/tree/template-icons.doc.mjs +97 -0
  213. package/assets/docs/tree/template-images-media.doc.mjs +127 -0
  214. package/assets/docs/tree/template-styles.doc.mjs +93 -0
  215. package/assets/docs/tree/templates.doc.mjs +34 -0
  216. package/assets/docs/tree/test-in-an-app.doc.mjs +115 -0
  217. package/assets/docs/tree/test-template-in-app.doc.mjs +128 -0
  218. package/assets/docs/tree/themes.doc.mjs +39 -0
  219. package/assets/docs/tree/troubleshooting.doc.mjs +153 -0
  220. package/assets/docs/tree/upgrading.doc.mjs +103 -0
  221. package/assets/docs/tree/use-a-theme-in-an-app.doc.mjs +51 -0
  222. package/assets/docs/tree/verify-packed-template.doc.mjs +77 -0
  223. package/assets/docs/tree/versioning.doc.mjs +162 -0
  224. package/assets/docs/tree/write-good-templates.doc.mjs +64 -0
  225. package/assets/docs/tree/write-the-template-file.doc.mjs +154 -0
  226. package/assets/docs/typography.doc.mjs +24 -4
  227. package/assets/docs/working-with-ai.doc.mjs +30 -22
  228. package/assets/templates/blocks/components/InternationalizationProvider/InternationalizationProvider01ShippedLocale.tsx +1 -1
  229. package/assets/templates/pages/ai-chat/template.doc.mjs +16 -1
  230. package/assets/templates/pages/ai-chat-landing/template.doc.mjs +9 -1
  231. package/assets/templates/pages/blank/template.doc.mjs +3 -1
  232. package/assets/templates/pages/canvas-editor/template.doc.mjs +9 -1
  233. package/assets/templates/pages/centered-hero/template.doc.mjs +3 -1
  234. package/assets/templates/pages/checkout-wizard/template.doc.mjs +1 -0
  235. package/assets/templates/pages/classic-gallery/template.doc.mjs +3 -1
  236. package/assets/templates/pages/contact-form/template.doc.mjs +16 -1
  237. package/assets/templates/pages/dashboard/template.doc.mjs +15 -1
  238. package/assets/templates/pages/dashboard-alert-rail/template.doc.mjs +23 -1
  239. package/assets/templates/pages/dashboard-cohort-funnel/template.doc.mjs +10 -1
  240. package/assets/templates/pages/dashboard-comparison/template.doc.mjs +9 -1
  241. package/assets/templates/pages/dashboard-composition/template.doc.mjs +10 -1
  242. package/assets/templates/pages/dashboard-progress/template.doc.mjs +17 -1
  243. package/assets/templates/pages/dashboard-scorecard/template.doc.mjs +2 -1
  244. package/assets/templates/pages/detail-page/template.doc.mjs +8 -0
  245. package/assets/templates/pages/documentation/template.doc.mjs +10 -1
  246. package/assets/templates/pages/documentation-design/template.doc.mjs +10 -1
  247. package/assets/templates/pages/documentation-technical/template.doc.mjs +10 -1
  248. package/assets/templates/pages/editor/template.doc.mjs +9 -1
  249. package/assets/templates/pages/file-explorer/template.doc.mjs +3 -1
  250. package/assets/templates/pages/form-two-column/template.doc.mjs +17 -1
  251. package/assets/templates/pages/form-wizard/template.doc.mjs +14 -1
  252. package/assets/templates/pages/form-wizard-dialog/template.doc.mjs +1 -0
  253. package/assets/templates/pages/gallery-hero/template.doc.mjs +10 -1
  254. package/assets/templates/pages/ide/template.doc.mjs +9 -1
  255. package/assets/templates/pages/incident-console/template.doc.mjs +10 -1
  256. package/assets/templates/pages/kanban-board/template.doc.mjs +11 -1
  257. package/assets/templates/pages/library/template.doc.mjs +15 -1
  258. package/assets/templates/pages/login/template.doc.mjs +10 -1
  259. package/assets/templates/pages/login-card/template.doc.mjs +11 -1
  260. package/assets/templates/pages/login-split/template.doc.mjs +10 -1
  261. package/assets/templates/pages/login-sso/template.doc.mjs +10 -1
  262. package/assets/templates/pages/messaging-shell/template.doc.mjs +11 -1
  263. package/assets/templates/pages/mixed-gallery/template.doc.mjs +10 -1
  264. package/assets/templates/pages/payment-form/template.doc.mjs +3 -1
  265. package/assets/templates/pages/product-detail/template.doc.mjs +9 -1
  266. package/assets/templates/pages/product-gallery/template.doc.mjs +10 -1
  267. package/assets/templates/pages/settings/template.doc.mjs +3 -1
  268. package/assets/templates/pages/settings-dialog/template.doc.mjs +9 -1
  269. package/assets/templates/pages/settings-sidebar/template.doc.mjs +9 -1
  270. package/assets/templates/pages/shell-nav/template.doc.mjs +10 -1
  271. package/assets/templates/pages/shell-side-nav/template.doc.mjs +14 -1
  272. package/assets/templates/pages/shell-top-nav/template.doc.mjs +11 -1
  273. package/assets/templates/pages/side-gallery/template.doc.mjs +3 -1
  274. package/assets/templates/pages/table/template.doc.mjs +11 -1
  275. package/assets/templates/pages/table-filter/template.doc.mjs +21 -1
  276. package/assets/templates/pages/table-grouped/template.doc.mjs +15 -1
  277. package/assets/templates/pages/table-inbox/template.doc.mjs +18 -6
  278. package/assets/templates/pages/table-page/template.doc.mjs +20 -1
  279. package/assets/templates/pages/table-tree/template.doc.mjs +14 -1
  280. package/assets/templates/pages/theme-showcase/template.doc.mjs +10 -1
  281. package/assets/templates/pages/work-item-detail/template.doc.mjs +10 -0
  282. package/assets/templates/themes/butter/icons.tsx +2 -0
  283. package/assets/templates/themes/chocolate/icons.tsx +2 -0
  284. package/assets/templates/themes/gothic/icons.tsx +2 -0
  285. package/assets/templates/themes/matcha/icons.tsx +2 -0
  286. package/assets/templates/themes/neutral/icons.tsx +2 -0
  287. package/assets/templates/themes/stone/icons.tsx +2 -0
  288. package/assets/templates/themes/y2k/icons.tsx +2 -0
  289. package/authoring/config/config.doc.mjs +9 -1
  290. package/authoring/config/parse.d.mts +2 -0
  291. package/authoring/config/parse.mjs +19 -0
  292. package/authoring/config/parse.test.mjs +8 -0
  293. package/authoring/config/type.ts +11 -0
  294. package/authoring/discover/discover.doc.d.mts +13 -0
  295. package/authoring/discover/discover.doc.mjs +138 -0
  296. package/authoring/discover/parse.d.mts +24 -0
  297. package/authoring/discover/parse.mjs +128 -0
  298. package/authoring/discover/parse.test.mjs +124 -0
  299. package/authoring/discover/type.ts +87 -0
  300. package/authoring/doctypes/_schema.d.mts +3 -2
  301. package/authoring/doctypes/_schema.mjs +6 -0
  302. package/authoring/doctypes/base/graph-fields.doc.mjs +3 -3
  303. package/authoring/doctypes/base/type.ts +4 -2
  304. package/authoring/doctypes/component/component.doc.mjs +6 -0
  305. package/authoring/doctypes/component/type.ts +8 -0
  306. package/authoring/doctypes/reference/reference.doc.mjs +7 -0
  307. package/authoring/doctypes/reference/type.ts +5 -0
  308. package/authoring/doctypes/schema/schema.doc.mjs +2 -2
  309. package/authoring/doctypes/template/parse.d.mts +2 -0
  310. package/authoring/doctypes/template/parse.mjs +1 -0
  311. package/authoring/doctypes/template/parse.test.mjs +21 -0
  312. package/authoring/doctypes/template/template.doc.mjs +7 -1
  313. package/authoring/doctypes/template/type.ts +12 -2
  314. package/authoring/index.d.mts +1 -0
  315. package/authoring/index.d.ts +10 -0
  316. package/authoring/index.mjs +1 -0
  317. package/authoring/integration/integration.doc.mjs +12 -10
  318. package/clients/cli/commands/component/index.mjs +152 -55
  319. package/clients/cli/commands/component-batch.test.mjs +341 -0
  320. package/clients/cli/commands/component-ownership.test.mjs +89 -0
  321. package/clients/cli/commands/component.doc.mjs +27 -9
  322. package/clients/cli/commands/discover.doc.mjs +53 -9
  323. package/clients/cli/commands/discover.mjs +393 -118
  324. package/clients/cli/commands/discover.sources.test.mjs +267 -0
  325. package/clients/cli/commands/docs.doc.mjs +1 -1
  326. package/clients/cli/commands/docs.mjs +60 -17
  327. package/clients/cli/commands/docs.test.mjs +113 -24
  328. package/clients/cli/commands/doctor-integration-docs.doc.mjs +3 -2
  329. package/clients/cli/commands/doctor-integration.test.mjs +53 -0
  330. package/clients/cli/commands/doctor.doc.mjs +3 -1
  331. package/clients/cli/commands/doctor.mjs +53 -9
  332. package/clients/cli/commands/gap-report.doc.mjs +10 -9
  333. package/clients/cli/commands/init.doc.mjs +9 -6
  334. package/clients/cli/commands/integration-add.doc.mjs +17 -7
  335. package/clients/cli/commands/integration-authoring.test.mjs +70 -9
  336. package/clients/cli/commands/integration-pack.doc.mjs +5 -9
  337. package/clients/cli/commands/integration-real-world.test.mjs +1 -1
  338. package/clients/cli/commands/integration-verify.doc.mjs +22 -0
  339. package/clients/cli/commands/integration.doc.mjs +4 -4
  340. package/clients/cli/commands/integration.mjs +76 -43
  341. package/clients/cli/commands/layout-expand.doc.mjs +3 -1
  342. package/clients/cli/commands/layout.expand-receipt.test.mjs +94 -0
  343. package/clients/cli/commands/layout.mjs +16 -0
  344. package/clients/cli/commands/manifest.doc.mjs +1 -1
  345. package/clients/cli/commands/search.doc.mjs +10 -3
  346. package/clients/cli/commands/search.mjs +21 -2
  347. package/clients/cli/commands/search.test.mjs +21 -4
  348. package/clients/cli/commands/swizzle.doc.mjs +1 -1
  349. package/clients/cli/commands/template.copy-receipt.test.mjs +60 -0
  350. package/clients/cli/commands/template.doc.mjs +1 -1
  351. package/clients/cli/commands/template.mjs +19 -5
  352. package/clients/cli/commands/template.show-media.test.mjs +62 -0
  353. package/clients/cli/commands/text-json-parity.test.mjs +7 -1
  354. package/clients/cli/commands/theme-add.doc.mjs +1 -1
  355. package/clients/cli/commands/theme-palette-generate.doc.mjs +3 -2
  356. package/clients/cli/commands/theme-palette.doc.mjs +1 -2
  357. package/clients/cli/commands/theme-targets.doc.mjs +2 -2
  358. package/clients/cli/commands/theme.doc.mjs +2 -1
  359. package/clients/cli/commands/upgrade.ascii-output.test.mjs +14 -0
  360. package/clients/cli/commands/upgrade.doc.mjs +62 -3
  361. package/clients/cli/commands/write-failure.test.mjs +175 -0
  362. package/clients/cli/index.mjs +28 -6
  363. package/clients/cli/lib/define-command.mjs +28 -4
  364. package/clients/cli/lib/define-command.test.mjs +54 -0
  365. package/clients/cli/lib/exit-codes.test.mjs +17 -1
  366. package/clients/cli/lib/json-shim.mjs +24 -14
  367. package/clients/cli/lib/manifest.mjs +48 -156
  368. package/clients/cli/lib/manifest.test.mjs +103 -9
  369. package/clients/cli/lib/parse-error-format.test.mjs +81 -0
  370. package/foundation/agent-docs/agent-docs.mjs +1 -1
  371. package/foundation/agent-docs/agent-docs.test.mjs +3 -2
  372. package/foundation/discovery/authoring-self-docs.mjs +1 -0
  373. package/foundation/discovery/authoring-self-docs.test.mjs +6 -2
  374. package/foundation/discovery/cli-self-docs.mjs +16 -2
  375. package/foundation/discovery/cli-self-docs.test.mjs +20 -0
  376. package/foundation/discovery/docs-discovery.mjs +5 -1
  377. package/foundation/discovery/docs-discovery.test.mjs +21 -0
  378. package/foundation/discovery/docs-section-key.d.mts +1 -1
  379. package/foundation/discovery/docs-section-key.mjs +1 -1
  380. package/foundation/discovery/template-adapter.d.mts +14 -0
  381. package/foundation/discovery/template-adapter.fixture-refs.test.mjs +37 -1
  382. package/foundation/discovery/template-adapter.mjs +21 -1
  383. package/foundation/doc-compiler/doc-loads.test.mjs +5 -4
  384. package/foundation/doc-compiler/inputs.test.mjs +0 -1
  385. package/foundation/doc-compiler/tree.d.mts +4 -0
  386. package/foundation/doc-compiler/tree.mjs +6 -1
  387. package/foundation/doc-compiler/tree.test.mjs +65 -14
  388. package/foundation/integrations/cli-requirement.d.mts +75 -11
  389. package/foundation/integrations/cli-requirement.mjs +120 -23
  390. package/foundation/integrations/cli-requirement.test.mjs +141 -9
  391. package/foundation/integrations/contribution-inventory.mjs +1 -1
  392. package/foundation/integrations/integrations.d.mts +14 -1
  393. package/foundation/integrations/integrations.mjs +41 -1
  394. package/foundation/integrations/integrations.test.mjs +31 -0
  395. package/foundation/response/batch.type.d.mts +33 -0
  396. package/foundation/response/batch.type.mjs +34 -0
  397. package/foundation/response/error-codes.doc.mjs +6 -8
  398. package/foundation/response/error-codes.test.mjs +30 -5
  399. package/foundation/response/response-types.doc.d.mts +5 -4
  400. package/foundation/response/response-types.doc.mjs +49 -19
  401. package/foundation/response/response-types.doc.test.mjs +23 -0
  402. package/foundation/response/response.doc.mjs +11 -10
  403. package/package.json +9 -9
  404. package/assets/docs/tree/integrations.test.mjs +0 -62
  405. package/assets/docs/tree/writing-docs.doc.mjs +0 -286
@@ -24,7 +24,10 @@ import * as fs from 'node:fs';
24
24
  import * as os from 'node:os';
25
25
  import * as path from 'node:path';
26
26
  import {fileURLToPath} from 'node:url';
27
+ import {docs} from '../docs/docs.mjs';
27
28
  import {
29
+ headingWithPhrase,
30
+ titleInQuery,
28
31
  search,
29
32
  scoreCandidate,
30
33
  scoreQuery,
@@ -98,17 +101,18 @@ describe('search leaf — docs at the grain a reader reads them', () => {
98
101
  it(
99
102
  'finds one section of a guide, and a docs-tree leaf by its own name',
100
103
  async () => {
101
- const guide = await search('codemod protected files', {cwd, type: 'doc'});
102
- expect(guide.data.results.slice(0, 3)).toContainEqual(
103
- expect.objectContaining({
104
- domain: 'doc',
105
- name: 'cli/integrations',
106
- section: 'codemods',
107
- title: 'Astryx CLI › CLI Integrations › Codemods',
108
- parent: 'astryx docs cli/integrations --index',
109
- command: 'astryx docs cli/integrations codemods',
110
- }),
111
- );
104
+ const guide = await search('when a codemod runs', {cwd, type: 'doc'});
105
+ // Found by the section's own title, wherever the guide sits in the tree.
106
+ const hit = guide.data.results
107
+ .slice(0, 3)
108
+ .find(result => result.section === 'which-codemods-run');
109
+ expect(hit).toMatchObject({
110
+ domain: 'doc',
111
+ name: expect.stringMatching(/^cli\/integrations\/(?:.+\/)?codemods$/),
112
+ title: expect.stringMatching(/ › Codemods › Choose when a codemod runs$/),
113
+ });
114
+ expect(hit.parent).toBe(`astryx docs ${hit.name} --index`);
115
+ expect(hit.command).toBe(`astryx docs ${hit.name} which-codemods-run`);
112
116
  const block = await search('token-ref', {cwd});
113
117
  expect(block.data.results[0]).toMatchObject({
114
118
  name: 'authoring',
@@ -127,6 +131,30 @@ describe('search leaf — docs at the grain a reader reads them', () => {
127
131
  SLOW,
128
132
  );
129
133
 
134
+ it(
135
+ 'finds the integration guides for the ways people ask to make one',
136
+ async () => {
137
+ // "make", "build", and "an" are stopwords, so each of the first three
138
+ // tokenizes to `integration` alone; the phrase still matches the
139
+ // keywords and the title the guides declare. A namespace's own
140
+ // keywords count, and a plural name is the name.
141
+ for (const [query, route] of [
142
+ ['make an integration', /^cli\/integrations$/],
143
+ ['build an integration', /^cli\/integrations$/],
144
+ ['create an integration', /^cli\/integrations$/],
145
+ ['integration', /^cli\/integrations$/],
146
+ ['publish an integration', /^cli\/integrations$/],
147
+ // The troubleshooting guide, wherever the tree places it.
148
+ ['troubleshoot integration', /^cli\/integrations\/(?:.+\/)?troubleshooting$/],
149
+ ]) {
150
+ const r = await search(query, {cwd, type: 'doc'});
151
+ const names = r.data.results.slice(0, 3).map(result => result.name);
152
+ expect(names.some(name => route.test(name)), `${query}: ${names.join(', ')}`).toBe(true);
153
+ }
154
+ },
155
+ SLOW,
156
+ );
157
+
130
158
  it(
131
159
  'gives a top-level namespace hit the topic list as its parent',
132
160
  async () => {
@@ -147,10 +175,15 @@ describe('search leaf — docs at the grain a reader reads them', () => {
147
175
  it(
148
176
  'points a topic hit at its index, never a whole-topic read',
149
177
  async () => {
150
- const r = await search('cli/integrations', {cwd, type: 'doc'});
178
+ // A guide the tree places, read from the tree rather than named.
179
+ const {data: integrations} = await docs('cli/integrations');
180
+ const guide = integrations.slots
181
+ .flatMap(slot => slot.children)
182
+ .find(child => child.kind === 'generic').route;
183
+ const r = await search(guide, {cwd, type: 'doc'});
151
184
  expect(r.data.results[0]).toMatchObject({
152
- name: 'cli/integrations',
153
- command: 'astryx docs cli/integrations --index',
185
+ name: guide,
186
+ command: `astryx docs ${guide} --index`,
154
187
  });
155
188
  expect(r.data.results[0]).not.toHaveProperty('section');
156
189
  },
@@ -253,6 +286,163 @@ describe('search leaf — exact keyword phrase outranks incidental token matches
253
286
  }, SLOW);
254
287
  });
255
288
 
289
+ describe('search leaf — a whole-query phrase in a title or heading is top tier', () => {
290
+ /**
291
+ * @param {string} q
292
+ * @param {object} candidate
293
+ * @returns {number}
294
+ */
295
+ const score = (q, candidate) => scoreQuery(q, tokenizeQuery(q), candidate)?.score ?? 0;
296
+
297
+ it('finds the whole query, in order, inside a title or heading', () => {
298
+ expect(headingWithPhrase('dark mode', ['Light/Dark Mode'])).toBe('Light/Dark Mode');
299
+ expect(headingWithPhrase('nested theme', ['Theme Props', 'Nested themes'])).toBe(
300
+ 'Nested themes',
301
+ );
302
+ // A plural on either side is the same word.
303
+ expect(headingWithPhrase('data attributes selector', ['Data attribute selectors'])).toBe(
304
+ 'Data attribute selectors',
305
+ );
306
+ // Out of order, split up, or one word: not a phrase.
307
+ expect(headingWithPhrase('mode dark', ['Light/Dark Mode'])).toBeNull();
308
+ expect(headingWithPhrase('dark mode', ['Dark sidebar and mode toggle'])).toBeNull();
309
+ expect(headingWithPhrase('dark', ['Light/Dark Mode'])).toBeNull();
310
+ expect(headingWithPhrase('dark mode', undefined)).toBeNull();
311
+ });
312
+
313
+ it('ranks a section titled with the phrase above an exact code-tick match of one word', () => {
314
+ // The reported miss: `search "dark mode"` put "Light/Dark Mode" at #28,
315
+ // under API enum docs that name `mode` in code ticks.
316
+ const section = {name: 'light-dark-mode', titles: ['Light/Dark Mode'], keywords: ['Light/Dark Mode']};
317
+ const enumDoc = {name: 'response-types', keywords: ['mode', 'dark'], description: 'mode'};
318
+ expect(score('dark mode', section)).toBe(170);
319
+ expect(score('dark mode', section)).toBeGreaterThan(score('dark mode', enumDoc));
320
+ });
321
+
322
+ it('ranks a question that names a whole title just below that', () => {
323
+ expect(titleInQuery('how do i add dark mode', ['Dark mode'])).toBe('Dark mode');
324
+ expect(titleInQuery('how do nested themes work', ['Nested themes'])).toBe('Nested themes');
325
+ // One-word titles are too common to count, and order still matters.
326
+ expect(titleInQuery('how do i theme my app', ['Theme'])).toBeNull();
327
+ expect(titleInQuery('mode dark please', ['Dark mode'])).toBeNull();
328
+ const section = {name: 'light-dark-mode', titles: ['Dark mode'], keywords: ['Dark mode']};
329
+ const named = score('how do i add dark mode', section);
330
+ expect(named).toBeGreaterThanOrEqual(160);
331
+ expect(named).toBeLessThan(170);
332
+ // Sections that share a title are ordered by how much of the rest of the
333
+ // question they answer.
334
+ const spacing = {name: 'best-practices', titles: ['Best Practices'], prose: ['Use spacing tokens']};
335
+ const color = {name: 'best-practices', titles: ['Best Practices'], prose: ['Use color tokens']};
336
+ expect(score('best practices for spacing', spacing)).toBeGreaterThan(
337
+ score('best practices for spacing', color),
338
+ );
339
+ });
340
+
341
+ it('reads a plural of a name as the name, and only a real plural', () => {
342
+ // One point under the exact spelling, so the doc named `tokens` outranks
343
+ // the Token component for `tokens`.
344
+ expect(scoreCandidate('integration', {name: 'integrations'})?.score).toBe(99);
345
+ expect(scoreCandidate('box', {name: 'boxes'})?.score).toBe(99);
346
+ expect(scoreCandidate('tabs', {name: 'tab'})?.score).toBe(99);
347
+ expect(scoreCandidate('tokens', {name: 'tokens'})?.score).toBe(100);
348
+ // `es` only follows s, x, z, ch, or sh.
349
+ expect(scoreCandidate('not', {name: 'notes'})?.score ?? 0).toBeLessThan(100);
350
+ expect(scoreCandidate('mod', {name: 'modes'})?.score ?? 0).toBeLessThan(100);
351
+ });
352
+
353
+ it('keeps an exact name or keyword above a title phrase', () => {
354
+ const titled = {name: 'x', titles: ['Table of contents for long pages']};
355
+ const keyword = {name: 'Outline', keywords: ['table of contents']};
356
+ expect(score('table of contents', keyword)).toBeGreaterThan(score('table of contents', titled));
357
+ });
358
+
359
+ it('puts the dark mode section first for a docs search', async () => {
360
+ for (const query of ['dark mode', 'how do I add dark mode']) {
361
+ const r = await search(query, {cwd, type: 'doc'});
362
+ expect(r.data.results[0]).toMatchObject({name: 'theme', section: 'light-dark-mode'});
363
+ }
364
+ }, SLOW);
365
+ });
366
+
367
+ describe('search leaf — a candidate that matches every word outranks a partial match', () => {
368
+ /**
369
+ * @param {string} q
370
+ * @param {object} candidate
371
+ * @returns {number}
372
+ */
373
+ const score = (q, candidate) => scoreQuery(q, tokenizeQuery(q), candidate)?.score ?? 0;
374
+
375
+ it('ranks a doc with both words above a doc named after one of them', () => {
376
+ // The reported regression: `search troubleshoot integration` put the
377
+ // troubleshooting guide 30th, under docs that each match `integration`
378
+ // alone (by name, 108; in a code tick, 98).
379
+ const guide = {
380
+ name: 'troubleshooting',
381
+ keywords: ['Troubleshooting'],
382
+ description: 'What to check when an integration does not load.',
383
+ };
384
+ const byName = {name: 'integration', keywords: ['integration-add']};
385
+ const byCodeTick = {name: 'integration-add', keywords: ['integration']};
386
+ const q = 'troubleshoot integration';
387
+ expect(score(q, guide)).toBeGreaterThan(score(q, byName));
388
+ expect(score(q, guide)).toBeGreaterThan(score(q, byCodeTick));
389
+ expect(scoreQuery(q, tokenizeQuery(q), guide)).toMatchObject({matched: 2, total: 2});
390
+ });
391
+
392
+ it('holds for longer queries too, and stays below the title tiers', () => {
393
+ const all = {name: 'x', keywords: ['alphas'], description: 'alpha beta gamma delta'};
394
+ const threeOfFour = {name: 'alpha', keywords: ['beta', 'gamma']};
395
+ const q = 'alpha beta gamma delta';
396
+ expect(score(q, all)).toBeGreaterThan(score(q, threeOfFour));
397
+ expect(score(q, all)).toBeLessThan(160);
398
+ });
399
+
400
+ // Among candidates that match every word, the stronger match should come
401
+ // first. Today every all-word match with a keyword hit gets the same score,
402
+ // so this records the order without enforcing it: it fails, as expected,
403
+ // until the scoring tells the two apart.
404
+ it.fails('ranks the stronger of two all-word matches first', () => {
405
+ const all = {name: 'x', keywords: ['alphas'], description: 'alpha beta gamma delta'};
406
+ const q = 'alpha beta gamma delta';
407
+ expect(score(q, {name: 'y', keywords: ['alpha'], description: 'beta gamma delta'})).toBeGreaterThan(
408
+ score(q, all),
409
+ );
410
+ });
411
+
412
+ it('keeps passing mentions of every word below an exact hit on one word', () => {
413
+ // Mentions in prose, or the components a page happens to render, are
414
+ // breadth: a page that says "empty state" is not the EmptyState answer.
415
+ const mentions = {name: 'ai-chat-landing', description: 'A landing page with an empty state.'};
416
+ const keyword = {name: 'x', keywords: ['empty']};
417
+ expect(score('empty state', mentions)).toBeLessThan(score('empty state', keyword));
418
+ });
419
+
420
+ it('finds a component by its name typed as words, and a guide by its route', async () => {
421
+ for (const [query, name] of [
422
+ ['command palette', 'CommandPalette'],
423
+ ['empty state', 'EmptyState'],
424
+ ]) {
425
+ const r = await search(query, {cwd});
426
+ expect(r.data.results[0], query).toMatchObject({domain: 'component', name});
427
+ }
428
+ const tokens = await search('tokens', {cwd});
429
+ expect(tokens.data.results[0]).toMatchObject({domain: 'doc', name: 'tokens'});
430
+ for (const [query, route] of [
431
+ ['codemods', /^cli\/integrations\/(?:.+\/)?codemods$/],
432
+ ['quick start', /^cli\/integrations\/(?:.+\/)?quick-start$/],
433
+ ['test in an app', /^cli\/integrations\/(?:.+\/)?test-in-an-app$/],
434
+ ]) {
435
+ const r = await search(query, {cwd, type: 'doc'});
436
+ // The guide is among the hits that share the top score: two guides
437
+ // that both declare the phrase tie, and the tie's order is not pinned.
438
+ const top = r.data.results
439
+ .filter(result => result.score === r.data.results[0].score)
440
+ .map(result => result.name);
441
+ expect(top.some(name => route.test(name)), `${query}: ${top.join(', ')}`).toBe(true);
442
+ }
443
+ }, SLOW);
444
+ });
445
+
256
446
  describe('search leaf — error paths (pinned)', () => {
257
447
  it('throws ERR_INVALID_ARGUMENT when the query is empty/whitespace', async () => {
258
448
  await expect(search(' ', {cwd})).rejects.toMatchObject({
@@ -267,12 +457,20 @@ describe('search leaf — error paths (pinned)', () => {
267
457
  ).rejects.toMatchObject({code: 'ERR_INVALID_ARGUMENT'});
268
458
  }, SLOW);
269
459
 
270
- it('throws ERR_CORE_NOT_FOUND when @astryxdesign/core cannot be found', async () => {
460
+ it('searches the docs without @astryxdesign/core, and throws for a domain that needs it', async () => {
271
461
  const empty = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-search-no-core-'));
272
462
  try {
273
- await expect(search('button', {cwd: empty})).rejects.toMatchObject({
274
- code: 'ERR_CORE_NOT_FOUND',
275
- });
463
+ // An open search outside an app covers the docs, as `astryx docs` does.
464
+ const open = await search('make an integration', {cwd: empty});
465
+ expect(open.data.results.length).toBeGreaterThan(0);
466
+ expect(new Set(open.data.results.map(r => r.domain))).toEqual(
467
+ new Set(['doc']),
468
+ );
469
+ for (const type of ['component', 'hook', 'template']) {
470
+ await expect(
471
+ search('button', {cwd: empty, type: /** @type {any} */ (type)}),
472
+ ).rejects.toMatchObject({code: 'ERR_CORE_NOT_FOUND'});
473
+ }
276
474
  } finally {
277
475
  fs.rmSync(empty, {recursive: true, force: true});
278
476
  }
@@ -25,7 +25,7 @@ import {
25
25
  findIntegrationComponentSource,
26
26
  } from '../../../foundation/discovery/component-discovery.mjs';
27
27
  import {ERROR_CODES} from '../../../foundation/response/error-codes.mjs';
28
- import {AstryxError} from '../../error.mjs';
28
+ import {AstryxError, writeFailed} from '../../error.mjs';
29
29
 
30
30
  /** Default issue tracker for maintainer feedback after swizzling. */
31
31
  const DEFAULT_ISSUES_URL = 'https://github.com/facebook/astryx/issues/new';
@@ -290,11 +290,21 @@ export async function swizzleCopy(component, options = {}) {
290
290
  );
291
291
  }
292
292
 
293
- fs.mkdirSync(outputDir, {recursive: true});
293
+ const outputDirExisted = fs.existsSync(outputDir);
294
+ try {
295
+ fs.mkdirSync(outputDir, {recursive: true});
296
+ } catch (err) {
297
+ throw writeFailed(outputDir, cwd, err);
298
+ }
294
299
 
295
300
  const files = fs.readdirSync(componentDir);
296
301
  let copied = 0;
297
302
  let usesStyleX = false;
303
+ // A copy that fails part-way undoes what it already wrote, so the report is
304
+ // true and a retry does not trip over half a component. Each entry keeps
305
+ // the bytes the file had before this run (null when the copy created it).
306
+ /** @type {Array<{dest: string, original: Buffer|null}>} */
307
+ const written = [];
298
308
  for (const file of files) {
299
309
  if (isExcludedFromCopy(file)) continue;
300
310
  const srcPath = path.join(componentDir, file);
@@ -309,7 +319,25 @@ export async function swizzleCopy(component, options = {}) {
309
319
  ) {
310
320
  usesStyleX = true;
311
321
  }
312
- fs.writeFileSync(path.join(outputDir, file), content);
322
+ const dest = path.join(outputDir, file);
323
+ try {
324
+ // The snapshot read is guarded too: a destination that cannot be read
325
+ // back (no permission, or a directory with this name) must still undo
326
+ // the earlier writes and report ERR_WRITE_FAILED.
327
+ const original = fs.existsSync(dest) ? fs.readFileSync(dest) : null;
328
+ written.push({dest, original});
329
+ fs.writeFileSync(dest, content);
330
+ } catch (err) {
331
+ const unrestored = undoCopy(written);
332
+ if (!outputDirExisted) {
333
+ try {
334
+ fs.rmdirSync(outputDir);
335
+ } catch {
336
+ // Not empty (something could not be undone), or already gone.
337
+ }
338
+ }
339
+ throw writeFailed(dest, cwd, err, unrestored);
340
+ }
313
341
  copied++;
314
342
  }
315
343
 
@@ -333,3 +361,38 @@ export async function swizzleCopy(component, options = {}) {
333
361
  if (feedback) data.feedback = feedback;
334
362
  return {type: 'swizzle.copy', data};
335
363
  }
364
+
365
+ /**
366
+ * Undo the writes of a copy that failed part-way, newest first: delete the
367
+ * files the copy created and put back the bytes of the files it replaced. A
368
+ * file whose bytes are already the original ones is left alone, so a write
369
+ * that failed before changing anything is not reported as unrestored.
370
+ *
371
+ * @param {Array<{dest: string, original: Buffer|null}>} written
372
+ * @returns {string[]} the files it could not restore
373
+ */
374
+ function undoCopy(written) {
375
+ /** @type {string[]} */
376
+ const unrestored = [];
377
+ for (const {dest, original} of [...written].reverse()) {
378
+ try {
379
+ if (original == null) {
380
+ fs.rmSync(dest, {force: true});
381
+ continue;
382
+ }
383
+ /** @type {Buffer|null} */
384
+ let current = null;
385
+ try {
386
+ current = fs.readFileSync(dest);
387
+ } catch {
388
+ current = null;
389
+ }
390
+ if (current == null || !current.equals(original)) {
391
+ fs.writeFileSync(dest, original);
392
+ }
393
+ } catch {
394
+ unrestored.push(dest);
395
+ }
396
+ }
397
+ return unrestored;
398
+ }
@@ -29,17 +29,19 @@ export const doc = {
29
29
  name: 'component',
30
30
  type: 'string',
31
31
  description:
32
- 'Bare or XDS-prefixed component name to copy. Omit to list the swizzlable components.',
32
+ "Component name to copy (e.g. 'Button'). Omit to list the swizzlable components.",
33
33
  },
34
34
  {
35
35
  name: 'options.cwd',
36
36
  type: 'string',
37
37
  description: 'Directory to resolve @astryxdesign/core from.',
38
+ default: 'process.cwd()',
38
39
  },
39
40
  {
40
41
  name: 'options.output',
41
42
  type: 'string',
42
- description: 'Output directory; must resolve inside cwd.',
43
+ description:
44
+ 'Output directory, relative to cwd. An absolute path, or one that resolves outside cwd, throws ERR_PATH_TRAVERSAL.',
43
45
  default: "'./components/astryx'",
44
46
  },
45
47
  {
@@ -71,7 +73,7 @@ export const doc = {
71
73
  {
72
74
  type: 'swizzle.copy',
73
75
  description:
74
- 'A receipt after copying the component into the project: the component name, owning package, output directory, files-copied count, the written file names, whether any file uses StyleX, and an optional maintainer-feedback note.',
76
+ 'A receipt after copying the component into the project: the component name, owning package, output directory, files-copied count, the written file names, whether any file uses StyleX, and, when the owner has an issues URL, feedback ({issuesUrl, ghCommand?}): where to report the gap that led to swizzling.',
75
77
  },
76
78
  ],
77
79
  throws: [
@@ -81,7 +83,7 @@ export const doc = {
81
83
  },
82
84
  {
83
85
  code: 'ERR_PATH_TRAVERSAL',
84
- when: 'the component name contains a path separator or traversal, output resolves outside cwd, or an existing output file or directory is a symlink that resolves outside cwd',
86
+ when: 'the component name contains a path separator or traversal, output is absolute or resolves outside cwd, or an existing output file or directory is a symlink that resolves outside cwd',
85
87
  },
86
88
  {
87
89
  code: 'ERR_UNKNOWN_COMPONENT',
@@ -99,6 +101,10 @@ export const doc = {
99
101
  code: 'ERR_FILE_EXISTS',
100
102
  when: 'copying would overwrite existing files and overwrite is not set',
101
103
  },
104
+ {
105
+ code: 'ERR_WRITE_FAILED',
106
+ when: 'the output directory or a copied file could not be written (no permission, read-only mount, full disk)',
107
+ },
102
108
  ],
103
109
  examples: [
104
110
  {
@@ -108,7 +114,7 @@ export const doc = {
108
114
  {label: 'Eject a component', code: "await swizzle('Button');"},
109
115
  {
110
116
  label: 'Disambiguate by package',
111
- code: "await swizzle('Button', {package: '@astryxdesign/core'});",
117
+ code: "await swizzle('Button', {package: '@astryxdesign/core', overwrite: true});",
112
118
  },
113
119
  {
114
120
  label: 'Custom output directory',
@@ -16,9 +16,9 @@ import {
16
16
  isFilePathArg,
17
17
  PathSafetyError,
18
18
  } from '../../../foundation/fs/path-safety.mjs';
19
- import {AstryxError} from '../../error.mjs';
19
+ import {AstryxError, writeFailed} from '../../error.mjs';
20
20
  import {ERROR_CODES} from '../../../foundation/response/error-codes.mjs';
21
- import {stripTemplateAssetRefs} from '../../../foundation/discovery/template-adapter.mjs';
21
+ import {replaceDemoMedia} from '../../../foundation/discovery/template-adapter.mjs';
22
22
 
23
23
  /**
24
24
  * Scaffold an already-resolved template to `targetPath` (relative to `cwd`) and
@@ -74,19 +74,24 @@ export function templateCopy(match, {targetPath, cwd, overwrite = false}) {
74
74
  if (!overwrite && fs.existsSync(outputFilePath)) {
75
75
  const rel = path.relative(cwd, outputFilePath) || outputFilePath;
76
76
  throw new AstryxError(
77
- `Refusing to overwrite existing file ${rel}. Re-run with overwrite to replace it.`,
77
+ `Refusing to overwrite existing file ${rel}. Re-run with --overwrite (or -f) to replace it.`,
78
78
  undefined,
79
79
  ERROR_CODES.ERR_FILE_EXISTS,
80
80
  );
81
81
  }
82
82
 
83
- fs.mkdirSync(outputDir, {recursive: true});
84
-
85
83
  // Strip demo image references so the scaffolded file renders without a
86
- // Meta-only network dependency.
87
- const source = fs.readFileSync(match.filePath, 'utf-8');
88
- const outputSource = stripTemplateAssetRefs(source);
89
- fs.writeFileSync(outputFilePath, outputSource);
84
+ // Meta-only network dependency. Read before any write, so a failure below
85
+ // leaves nothing behind.
86
+ const {source: outputSource, demoMediaReplaced} = replaceDemoMedia(
87
+ fs.readFileSync(match.filePath, 'utf-8'),
88
+ );
89
+ try {
90
+ fs.mkdirSync(outputDir, {recursive: true});
91
+ fs.writeFileSync(outputFilePath, outputSource);
92
+ } catch (err) {
93
+ throw writeFailed(outputFilePath, cwd, err);
94
+ }
90
95
 
91
96
  const relOutput = path.relative(cwd, outputDir) || '.';
92
97
  return {
@@ -96,6 +101,7 @@ export function templateCopy(match, {targetPath, cwd, overwrite = false}) {
96
101
  outputDir: relOutput,
97
102
  fileName: outputFileName,
98
103
  filesCopied: 1,
104
+ demoMediaReplaced,
99
105
  },
100
106
  };
101
107
  }
@@ -0,0 +1,77 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file The template.copy receipt discloses replaced demo media. Templates are
5
+ * picked by what their source contains, not by slug, so catalog renames do not
6
+ * break the suite.
7
+ */
8
+
9
+ import {describe, it, expect, beforeEach, afterEach} from 'vitest';
10
+ import * as fs from 'node:fs';
11
+ import * as os from 'node:os';
12
+ import * as path from 'node:path';
13
+ import {template, discoverTemplates} from '../template.mjs';
14
+
15
+ const SLOW = 60_000;
16
+ const FIXTURE_REF = /\/template-assets\/[\w.-]+\.(\w+)/g;
17
+ const VIDEO = new Set(['mp4', 'webm', 'mov', 'ogv', 'm4v']);
18
+
19
+ /** @param {string} filePath */
20
+ const fixtureRefs = filePath =>
21
+ [...fs.readFileSync(filePath, 'utf-8').matchAll(FIXTURE_REF)].map(m =>
22
+ m[1].toLowerCase(),
23
+ );
24
+
25
+ /**
26
+ * @param {(refs: string[]) => boolean} predicate
27
+ * @param {'page' | 'block'} type
28
+ */
29
+ async function findTemplate(type, predicate) {
30
+ const all = /** @type {Array<{dirName: string, type: string, filePath: string}>} */ (
31
+ await discoverTemplates()
32
+ );
33
+ const ids = new Map();
34
+ for (const t of all) ids.set(t.dirName, (ids.get(t.dirName) ?? 0) + 1);
35
+ const found = all.find(
36
+ t =>
37
+ t.type === type &&
38
+ ids.get(t.dirName) === 1 &&
39
+ fs.existsSync(t.filePath) &&
40
+ predicate(fixtureRefs(t.filePath)),
41
+ );
42
+ if (!found) throw new Error(`no ${type} template matches`);
43
+ return {id: found.dirName, refs: fixtureRefs(found.filePath)};
44
+ }
45
+
46
+ describe('template.copy receipt — replaced demo media', () => {
47
+ let dir;
48
+ beforeEach(() => {
49
+ dir = fs.mkdtempSync(path.join(os.tmpdir(), 'tmpl-receipt-'));
50
+ });
51
+ afterEach(() => fs.rmSync(dir, {recursive: true, force: true}));
52
+
53
+ it('counts every demo image a page template carried, and leaves none behind', async () => {
54
+ const {id, refs} = await findTemplate(
55
+ 'page',
56
+ r => r.length > 0 && r.every(ext => !VIDEO.has(ext)),
57
+ );
58
+ const res = await template(id, {targetPath: './dest', cwd: dir});
59
+ expect(res.type).toBe('template.copy');
60
+ expect(res.data.demoMediaReplaced).toBe(refs.length);
61
+ expect(
62
+ fs.readFileSync(path.join(dir, 'dest', 'page.tsx'), 'utf-8'),
63
+ ).not.toContain('/template-assets/');
64
+ }, SLOW);
65
+
66
+ it('counts a demo video in a block template', async () => {
67
+ const {id, refs} = await findTemplate('block', r => r.some(ext => VIDEO.has(ext)));
68
+ const res = await template(id, {targetPath: './dest', cwd: dir});
69
+ expect(res.data.demoMediaReplaced).toBe(refs.length);
70
+ }, SLOW);
71
+
72
+ it('reports 0 when the template carries no demo media', async () => {
73
+ const {id} = await findTemplate('page', r => r.length === 0);
74
+ const res = await template(id, {targetPath: './dest', cwd: dir});
75
+ expect(res.data.demoMediaReplaced).toBe(0);
76
+ }, SLOW);
77
+ });
@@ -35,6 +35,15 @@ describe('template.copy — overwrite + path safety', () => {
35
35
  expect(fs.readFileSync(path.join(dir, 'mine.tsx'), 'utf-8')).toBe('USER CODE');
36
36
  }, SLOW);
37
37
 
38
+ it('names the flag that replaces the file, as swizzle and theme add do', async () => {
39
+ fs.writeFileSync(path.join(dir, 'mine.tsx'), 'USER CODE');
40
+ await expect(
41
+ template('blank', {targetPath: './mine.tsx', cwd: dir}),
42
+ ).rejects.toThrow(
43
+ 'Refusing to overwrite existing file mine.tsx. Re-run with --overwrite (or -f) to replace it.',
44
+ );
45
+ }, SLOW);
46
+
38
47
  it('overwrites when overwrite:true is passed', async () => {
39
48
  fs.writeFileSync(path.join(dir, 'mine.tsx'), 'USER CODE');
40
49
  const res = await template('blank', {targetPath: './mine.tsx', overwrite: true, cwd: dir});
@@ -1,8 +1,8 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
3
  /**
4
- * @file `template.show` leaf — return a resolved template's raw source plus the
5
- * components it composes.
4
+ * @file `template.show` leaf — return a resolved template's source, exactly as
5
+ * `template.copy` writes it, plus the components it composes.
6
6
  *
7
7
  * @position api/template/show — reads the resolved match's source file; the
8
8
  * template dispatcher routes `show` (and the no-target-path default) here.
@@ -11,7 +11,10 @@
11
11
  import * as fs from 'node:fs';
12
12
  import {AstryxError} from '../../error.mjs';
13
13
  import {ERROR_CODES} from '../../../foundation/response/error-codes.mjs';
14
- import {extractComponents} from '../../../foundation/discovery/template-adapter.mjs';
14
+ import {
15
+ extractComponents,
16
+ replaceDemoMedia,
17
+ } from '../../../foundation/discovery/template-adapter.mjs';
15
18
 
16
19
  /**
17
20
  * Build the `template.show` envelope for an already-resolved template.
@@ -27,6 +30,13 @@ export function templateShow(match) {
27
30
  );
28
31
  }
29
32
 
33
+ // The source template.copy writes (spec:AST-028 FR7): a template printed and
34
+ // pasted must not keep a media path only Astryx's previews serve, and the
35
+ // caller is told how many it replaced, the way the copy receipt tells it.
36
+ const {source, demoMediaReplaced} = replaceDemoMedia(
37
+ fs.readFileSync(match.filePath, 'utf-8'),
38
+ );
39
+
30
40
  return {
31
41
  type: 'template.show',
32
42
  data: {
@@ -34,7 +44,8 @@ export function templateShow(match) {
34
44
  description: match.description,
35
45
  type: match.type,
36
46
  components: extractComponents(match.filePath),
37
- source: fs.readFileSync(match.filePath, 'utf-8'),
47
+ source,
48
+ demoMediaReplaced,
38
49
  },
39
50
  };
40
51
  }