@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
@@ -0,0 +1,76 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Colocated tests for the template.show leaf — the source it returns is
5
+ * the source template.copy writes. An agent that prints a template and pastes
6
+ * it must end up with the same file as one that scaffolds it, demo media
7
+ * included (spec:AST-028 FR7).
8
+ */
9
+
10
+ import {describe, it, expect, beforeAll, afterAll} from 'vitest';
11
+ import * as fs from 'node:fs';
12
+ import * as os from 'node:os';
13
+ import * as path from 'node:path';
14
+ import {discoverAll, template} from '../template.mjs';
15
+ import {templateCopy} from '../copy/copy.mjs';
16
+ import {templateShow} from './show.mjs';
17
+
18
+ const SLOW = 60_000;
19
+ /** Astryx's demo-media namespace: only its own previews serve these files. */
20
+ const FIXTURE = '/template-assets/';
21
+ const FIXTURE_IMAGE = /\/template-assets\/[^'"`\s]+\.(png|jpe?g|webp)/i;
22
+
23
+ const read = (/** @type {string} */ file) => fs.readFileSync(file, 'utf-8');
24
+
25
+ describe('template.show', () => {
26
+ /** @type {string} */
27
+ let dir;
28
+ /** @type {Awaited<ReturnType<typeof discoverAll>>} */
29
+ let templates;
30
+ beforeAll(async () => {
31
+ dir = fs.mkdtempSync(path.join(os.tmpdir(), 'tmpl-show-'));
32
+ templates = await discoverAll(dir);
33
+ }, SLOW);
34
+ afterAll(() => fs.rmSync(dir, {recursive: true, force: true}));
35
+
36
+ it('returns exactly the source copy writes, for every template', () => {
37
+ let withFixtures = 0;
38
+ templates.forEach((match, index) => {
39
+ if (read(match.filePath).includes(FIXTURE)) withFixtures += 1;
40
+ const shown = templateShow(match).data.source;
41
+ const copied = templateCopy(match, {targetPath: `out-${index}`, cwd: dir});
42
+ const written = read(
43
+ path.join(dir, copied.data.outputDir, copied.data.fileName),
44
+ );
45
+ expect(shown, match.dirName).toBe(written);
46
+ });
47
+ // The templates with demo media are the ones that used to differ.
48
+ expect(withFixtures).toBeGreaterThan(0);
49
+ }, SLOW);
50
+
51
+ it('reports a replaced count when the source changed, and 0 when it did not', () => {
52
+ let reported = 0;
53
+ for (const match of templates) {
54
+ const raw = read(match.filePath);
55
+ const {source, demoMediaReplaced} = templateShow(match).data;
56
+ if (source === raw) expect(demoMediaReplaced, match.dirName).toBe(0);
57
+ else expect(demoMediaReplaced, match.dirName).toBeGreaterThan(0);
58
+ reported += demoMediaReplaced;
59
+ }
60
+ expect(reported).toBeGreaterThan(0);
61
+ }, SLOW);
62
+
63
+ it('prints a placeholder where a template shows a demo image', async () => {
64
+ const match = templates.find(
65
+ t => t.type === 'page' && FIXTURE_IMAGE.test(read(t.filePath)),
66
+ );
67
+ expect(match).toBeDefined();
68
+ if (!match) return;
69
+ const res = await template(match.dirName, {type: 'page', cwd: dir});
70
+ expect(res.type).toBe('template.show');
71
+ if (res.type !== 'template.show') return;
72
+ expect(res.data.source).not.toContain(FIXTURE);
73
+ expect(res.data.source).toContain('data:image/svg+xml,');
74
+ expect(res.data.demoMediaReplaced).toBeGreaterThan(0);
75
+ }, SLOW);
76
+ });
@@ -125,6 +125,20 @@ describe('integration template discovery', () => {
125
125
  expect(entry.description).toBe('pricing desc');
126
126
  });
127
127
 
128
+ it("reads an integration template's keywords in search and build", async () => {
129
+ const pkgDir = installWidgets(tmpDir);
130
+ writeTemplate(pkgDir, 'observatory', {
131
+ kind: 'page',
132
+ body: "export default {type: 'page', name: 'Observatory', description: 'Live tiles over a sky view.', category: 'Dashboard - Observatory', keywords: ['telescope', 'star map']};\n",
133
+ });
134
+
135
+ const found = await search('telescope', {cwd: tmpDir, type: 'template'});
136
+ expect(found.data.results.map(r => r.name)).toContain('observatory');
137
+ const kit = await build('telescope star map page', {cwd: tmpDir});
138
+ if (kit.type !== 'build.kit') throw new Error(kit.type);
139
+ expect(kit.data.start?.name).toBe('observatory');
140
+ });
141
+
128
142
  it('preserves integration block showcase metadata in list output', async () => {
129
143
  const pkgDir = installWidgets(tmpDir);
130
144
  writeTemplate(pkgDir, 'chart-showcase', {
@@ -41,4 +41,4 @@ export function template(name?: string, options?: {
41
41
  export type DiscoveredTemplate = import("../../foundation/discovery/template-adapter.mjs").DiscoveredTemplate;
42
42
  export type TemplateDiscoveryError = import("../../foundation/discovery/template-adapter.mjs").TemplateDiscoveryError;
43
43
  export type TemplateDocModule = import("../../foundation/discovery/template-adapter.mjs").TemplateDocModule;
44
- export { discoverAll, discoverAll as discoverTemplates, discoverAllWithErrors, discoverCoreTemplates, discoverIntegrationTemplatesForOne, stripTemplateAssetRefs, listTemplates, findRelatedBlocks, findShowcase, extractComponents } from "../../foundation/discovery/template-adapter.mjs";
44
+ export { discoverAll, discoverAll as discoverTemplates, discoverAllWithErrors, discoverCoreTemplates, discoverIntegrationTemplatesForOne, stripTemplateAssetRefs, replaceDemoMedia, listTemplates, findRelatedBlocks, findShowcase, extractComponents } from "../../foundation/discovery/template-adapter.mjs";
@@ -102,6 +102,7 @@ export const doc = {
102
102
  type: 'string',
103
103
  description:
104
104
  'Directory to discover templates and resolve the target path from.',
105
+ default: 'process.cwd()',
105
106
  },
106
107
  ],
107
108
  returns: [
@@ -113,7 +114,7 @@ export const doc = {
113
114
  {
114
115
  type: 'template.show',
115
116
  description:
116
- "The resolved template's raw source plus its description, kind, and the component names it composes.",
117
+ "The resolved template's source, exactly as a copy writes it, plus its description, kind, the component names it composes, and `demoMediaReplaced` — how many Astryx demo media references (images, posters, videos) in the source were replaced with placeholders for you to swap for your own media.",
117
118
  },
118
119
  {
119
120
  type: 'template.skeleton',
@@ -123,7 +124,7 @@ export const doc = {
123
124
  {
124
125
  type: 'template.copy',
125
126
  description:
126
- 'A receipt after scaffolding the template into the project: the template id, output directory, written file name, and file count.',
127
+ 'A receipt after scaffolding the template into the project: the template id, output directory, written file name, file count, and `demoMediaReplaced` — how many Astryx demo media references (images, posters, videos) in the written file were replaced with placeholders for you to swap for your own media.',
127
128
  },
128
129
  {
129
130
  type: 'template.cdn',
@@ -134,7 +135,7 @@ export const doc = {
134
135
  throws: [
135
136
  {
136
137
  code: 'ERR_UNKNOWN_TEMPLATE',
137
- when: 'the named template does not exist, or --skeleton is run without a name',
138
+ when: 'the named template does not exist, or options.skeleton is set without a name',
138
139
  },
139
140
  {
140
141
  code: 'ERR_AMBIGUOUS_TEMPLATE',
@@ -152,6 +153,10 @@ export const doc = {
152
153
  code: 'ERR_FILE_EXISTS',
153
154
  when: 'the copy target already exists and overwrite is not set',
154
155
  },
156
+ {
157
+ code: 'ERR_WRITE_FAILED',
158
+ when: 'the copy target could not be written (no permission, read-only mount, full disk); nothing is written',
159
+ },
155
160
  ],
156
161
  examples: [
157
162
  {label: 'List templates', code: 'const {data} = await template();'},
@@ -41,6 +41,7 @@ export {
41
41
  discoverCoreTemplates,
42
42
  discoverIntegrationTemplatesForOne,
43
43
  stripTemplateAssetRefs,
44
+ replaceDemoMedia,
44
45
  listTemplates,
45
46
  findRelatedBlocks,
46
47
  findShowcase,
@@ -67,6 +67,7 @@ export type TemplateShowResponse = {
67
67
  type: "page" | "block";
68
68
  components: string[];
69
69
  source: string;
70
+ demoMediaReplaced: number;
70
71
  };
71
72
  };
72
73
  /**
@@ -91,6 +92,7 @@ export type TemplateCopyResponse = {
91
92
  outputDir: string;
92
93
  fileName: string;
93
94
  filesCopied: number;
95
+ demoMediaReplaced: number;
94
96
  };
95
97
  };
96
98
  /**
@@ -53,6 +53,7 @@
53
53
  * @property {'page' | 'block'} data.type
54
54
  * @property {string[]} data.components
55
55
  * @property {string} data.source
56
+ * @property {number} data.demoMediaReplaced Astryx demo media references (images, posters, videos) replaced in the returned source: images with a neutral placeholder, videos with an empty source. Swap in your own media at those points; no media is installed. 0 when the template carried none.
56
57
  */
57
58
 
58
59
  /**
@@ -75,6 +76,7 @@
75
76
  * @property {string} data.outputDir
76
77
  * @property {string} data.fileName
77
78
  * @property {number} data.filesCopied
79
+ * @property {number} data.demoMediaReplaced Astryx demo media references (images, posters, videos) replaced in the written file: images with a neutral placeholder, videos with an empty source. Swap in your own media at those points; no media is installed. 0 when the template carried none.
78
80
  */
79
81
 
80
82
  /**
@@ -15,6 +15,7 @@ import {
15
15
  import {AstryxError} from '../../error.mjs';
16
16
  import {ERROR_CODES} from '../../../foundation/response/error-codes.mjs';
17
17
  import {listAvailableThemes, findTheme} from '../_adapter.mjs';
18
+ import {applyWrites} from '../../integration/add-helpers.mjs';
18
19
  // Scaffolded files must not carry our repo boilerplate into a consumer's tree.
19
20
  import {stripCopyrightHeader} from '../../../foundation/text/copyright-header.mjs';
20
21
 
@@ -136,37 +137,28 @@ export async function themeAdd(slug, options = {}) {
136
137
  }
137
138
  }
138
139
 
139
- // Stage to temp files then rename, rolling back partials on failure so a
140
- // failed write never leaves a half-written theme. mkdir is inside the try so
141
- // a failure (e.g. an ancestor is a file → EEXIST/ENOTDIR) surfaces as a
142
- // stable ERR_WRITE_FAILED rather than leaking a raw fs errno + absolute path.
143
- const staged = [];
140
+ // One transaction: every file publishes, or every replaced file gets its
141
+ // previous bytes back and every created file is removed. mkdir is inside the
142
+ // try so a failure (e.g. an ancestor is a file → EEXIST/ENOTDIR) surfaces as
143
+ // a stable ERR_WRITE_FAILED rather than leaking a raw fs errno + absolute path.
144
144
  try {
145
145
  fs.mkdirSync(resolvedDir, {recursive: true});
146
- for (const w of writes) {
146
+ const plans = writes.map(w => {
147
+ fs.mkdirSync(path.dirname(w.dest), {recursive: true});
148
+ // Confine again once the directories exist: one may have been swapped
149
+ // for a link since the destination was first checked.
147
150
  const dest = assertWithin(w.name, resolvedDir, {
148
151
  label: `theme destination for ${w.name}`,
149
152
  });
150
- fs.mkdirSync(path.dirname(dest), {recursive: true});
151
- // The staging file is an output path too: confine it, and never write
152
- // through an entry that already exists under its name.
153
- const tmp = assertWithin(`${w.name}.${process.pid}.tmp`, resolvedDir, {
154
- label: `theme staging file for ${w.name}`,
155
- });
156
- fs.writeFileSync(tmp, scaffoldContents(fs.readFileSync(w.src)), {flag: 'wx'});
157
- staged.push({tmp, dest});
158
- }
159
- for (const s of staged) {
160
- fs.renameSync(s.tmp, s.dest);
161
- }
153
+ return {
154
+ path: dest,
155
+ contents: scaffoldContents(fs.readFileSync(w.src)),
156
+ createOnly: !overwrite,
157
+ };
158
+ });
159
+ applyWrites(plans);
162
160
  } catch (err) {
163
- for (const s of staged) {
164
- try {
165
- fs.rmSync(s.tmp, {force: true});
166
- } catch {
167
- /* best-effort */
168
- }
169
- }
161
+ if (err instanceof AstryxError) throw err;
170
162
  if (err instanceof PathSafetyError) {
171
163
  throw new AstryxError(
172
164
  err.message,
@@ -0,0 +1,158 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * `theme add` writes a theme as one transaction: when a later file fails to
5
+ * publish, every file it already replaced gets its previous bytes back and
6
+ * every file it created is removed. Publishing is forced to fail by wrapping
7
+ * the two calls that publish a staged file: `linkSync` for a new file and
8
+ * `renameSync` for a replacement.
9
+ *
10
+ * Separate file because vi.mock is hoisted and affects the whole module.
11
+ */
12
+
13
+ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
14
+ import * as os from 'node:os';
15
+ import * as path from 'node:path';
16
+
17
+ const failures = vi.hoisted(() => ({
18
+ /** Fail the Nth staged-file publish (1-based); 0 disables. */
19
+ publish: 0,
20
+ /** Also fail every rollback restore. */
21
+ restore: false,
22
+ count: 0,
23
+ }));
24
+
25
+ vi.mock('node:fs', async importOriginal => {
26
+ const actual = /** @type {typeof import('node:fs')} */ (
27
+ await importOriginal()
28
+ );
29
+ /** @param {string} source */
30
+ const isStaged = source => path.basename(String(source)).includes('.tmp');
31
+ /** @param {string} source */
32
+ const isRestore = source =>
33
+ path.basename(String(source)).includes('.restore-');
34
+ /** @param {string} source @param {string} op */
35
+ const maybeFail = (source, op) => {
36
+ if (isStaged(source) && failures.publish > 0) {
37
+ failures.count++;
38
+ if (failures.count === failures.publish) {
39
+ throw Object.assign(new Error(`EIO: forced ${op} failure`), {
40
+ code: 'EIO',
41
+ });
42
+ }
43
+ }
44
+ if (isRestore(source) && failures.restore) {
45
+ throw Object.assign(new Error(`EIO: forced restore failure`), {
46
+ code: 'EIO',
47
+ });
48
+ }
49
+ };
50
+ return {
51
+ ...actual,
52
+ linkSync: vi.fn((source, destination) => {
53
+ maybeFail(source, 'link');
54
+ return actual.linkSync(source, destination);
55
+ }),
56
+ renameSync: vi.fn((source, destination) => {
57
+ maybeFail(source, 'rename');
58
+ return actual.renameSync(source, destination);
59
+ }),
60
+ };
61
+ });
62
+
63
+ const fs = await import('node:fs');
64
+ const {themeAdd} = await import('./add.mjs');
65
+ const {listThemes} = await import('../_adapter.mjs');
66
+
67
+ let tmpDir;
68
+
69
+ beforeEach(() => {
70
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-themeadd-rollback-'));
71
+ failures.publish = 0;
72
+ failures.restore = false;
73
+ failures.count = 0;
74
+ });
75
+
76
+ afterEach(() => {
77
+ failures.publish = 0;
78
+ failures.restore = false;
79
+ fs.rmSync(tmpDir, {recursive: true, force: true});
80
+ });
81
+
82
+ function stoneFiles() {
83
+ const theme = listThemes().find(entry => entry.slug === 'stone');
84
+ if (!theme) throw new Error('missing bundled theme stone');
85
+ expect(theme.files.length).toBeGreaterThanOrEqual(2);
86
+ return theme.files.map(name =>
87
+ path.join(tmpDir, 'src', 'themes', 'stone', name),
88
+ );
89
+ }
90
+
91
+ /** Entries staging or rollback left behind in the theme directory. */
92
+ function strays() {
93
+ const dir = path.join(tmpDir, 'src', 'themes', 'stone');
94
+ if (!fs.existsSync(dir)) return [];
95
+ return fs
96
+ .readdirSync(dir, {recursive: true})
97
+ .map(String)
98
+ .filter(name => name.includes('.tmp') || name.includes('.restore-'));
99
+ }
100
+
101
+ describe('themeAdd rolls back a partial write', () => {
102
+ it('removes every created file when the second publish fails', async () => {
103
+ const files = stoneFiles();
104
+ failures.publish = 2;
105
+
106
+ await expect(themeAdd('stone', {cwd: tmpDir})).rejects.toMatchObject({
107
+ code: 'ERR_WRITE_FAILED',
108
+ });
109
+
110
+ for (const file of files) expect(fs.existsSync(file)).toBe(false);
111
+ expect(strays()).toEqual([]);
112
+ });
113
+
114
+ it('restores every replaced byte when the second publish fails', async () => {
115
+ const files = stoneFiles();
116
+ fs.mkdirSync(path.dirname(files[0]), {recursive: true});
117
+ const before = Buffer.from([0x00, 0xff, 0x0a, 0x62, 0x65, 0x66]);
118
+ fs.writeFileSync(files[0], before);
119
+ failures.publish = 2;
120
+
121
+ await expect(
122
+ themeAdd('stone', {cwd: tmpDir, overwrite: true}),
123
+ ).rejects.toMatchObject({code: 'ERR_WRITE_FAILED'});
124
+
125
+ expect(fs.readFileSync(files[0]).equals(before)).toBe(true);
126
+ for (const file of files.slice(1)) expect(fs.existsSync(file)).toBe(false);
127
+ expect(strays()).toEqual([]);
128
+ });
129
+
130
+ it('names a file it could not restore', async () => {
131
+ const files = stoneFiles();
132
+ fs.mkdirSync(path.dirname(files[0]), {recursive: true});
133
+ fs.writeFileSync(files[0], 'before\n');
134
+ failures.publish = 2;
135
+ failures.restore = true;
136
+
137
+ let error;
138
+ try {
139
+ await themeAdd('stone', {cwd: tmpDir, overwrite: true});
140
+ } catch (caught) {
141
+ error = caught;
142
+ }
143
+
144
+ expect(error?.code).toBe('ERR_WRITE_FAILED');
145
+ expect(error?.message).toContain('Could not restore');
146
+ expect(error?.message).toContain(files[0]);
147
+ });
148
+
149
+ it('writes every file when nothing fails', async () => {
150
+ const files = stoneFiles();
151
+
152
+ const result = await themeAdd('stone', {cwd: tmpDir});
153
+
154
+ expect(result.type).toBe('theme.add');
155
+ for (const file of files) expect(fs.existsSync(file)).toBe(true);
156
+ expect(strays()).toEqual([]);
157
+ });
158
+ });
@@ -6,14 +6,15 @@ import * as os from 'node:os';
6
6
  import * as path from 'node:path';
7
7
  import {themeAdd} from './add.mjs';
8
8
  import {listThemes} from '../_adapter.mjs';
9
- import {isErrorCode} from '../../../foundation/response/error-codes.mjs';
10
9
 
11
10
  let tmpDir;
12
11
  let outsideDir;
13
12
 
14
13
  beforeEach(() => {
15
14
  tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-themeadd-staging-'));
16
- outsideDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-themeadd-outside-'));
15
+ outsideDir = fs.mkdtempSync(
16
+ path.join(os.tmpdir(), 'astryx-themeadd-outside-'),
17
+ );
17
18
  });
18
19
 
19
20
  afterEach(() => {
@@ -22,45 +23,61 @@ afterEach(() => {
22
23
  });
23
24
 
24
25
  /**
25
- * Put a symlink where `theme add` stages the first file of `slug`.
26
+ * Where `theme add` writes the first file of `slug`.
26
27
  * @param {string} slug
27
- * @param {string} target
28
28
  */
29
- function plantStagingLink(slug, target) {
29
+ function firstDestination(slug) {
30
30
  const theme = listThemes().find(entry => entry.slug === slug);
31
31
  if (!theme) throw new Error(`missing bundled theme ${slug}`);
32
- const first = theme.files[0];
33
- const dest = path.join(tmpDir, 'src', 'themes', slug, first);
32
+ const dest = path.join(tmpDir, 'src', 'themes', slug, theme.files[0]);
34
33
  fs.mkdirSync(path.dirname(dest), {recursive: true});
35
- fs.symlinkSync(target, `${dest}.${process.pid}.tmp`);
36
34
  return dest;
37
35
  }
38
36
 
37
+ // Staging names are unpredictable and created exclusively, so an entry planted
38
+ // at a predictable name beside the destination is never written through.
39
39
  describe('themeAdd staging writes stay inside the project', () => {
40
- it('refuses a staging path that links outside the project', async () => {
40
+ it('never writes through a link planted at a predictable staging name', async () => {
41
41
  const victim = path.join(outsideDir, 'victim.txt');
42
42
  fs.writeFileSync(victim, 'outside\n');
43
- const dest = plantStagingLink('stone', victim);
43
+ const dest = firstDestination('stone');
44
+ fs.symlinkSync(victim, `${dest}.${process.pid}.tmp`);
45
+
46
+ await themeAdd('stone', {cwd: tmpDir});
44
47
 
45
- await expect(themeAdd('stone', {cwd: tmpDir})).rejects.toMatchObject({
46
- code: 'ERR_PATH_TRAVERSAL',
47
- });
48
48
  expect(fs.readFileSync(victim, 'utf-8')).toBe('outside\n');
49
- expect(fs.existsSync(dest)).toBe(false);
49
+ expect(fs.lstatSync(dest).isFile()).toBe(true);
50
50
  });
51
51
 
52
- it('never creates a file through a dangling staging link', async () => {
52
+ it('never creates a file through a dangling link at a predictable staging name', async () => {
53
53
  const victim = path.join(outsideDir, 'created.txt');
54
- plantStagingLink('stone', victim);
54
+ fs.symlinkSync(victim, `${firstDestination('stone')}.${process.pid}.tmp`);
55
+
56
+ await themeAdd('stone', {cwd: tmpDir});
57
+
58
+ expect(fs.existsSync(victim)).toBe(false);
59
+ });
60
+
61
+ it('refuses to replace a destination that links outside the project', async () => {
62
+ const victim = path.join(outsideDir, 'victim.txt');
63
+ fs.writeFileSync(victim, 'outside\n');
64
+ const dest = firstDestination('stone');
65
+ fs.symlinkSync(victim, dest);
55
66
 
56
- let error;
57
- try {
58
- await themeAdd('stone', {cwd: tmpDir});
59
- } catch (caught) {
60
- error = caught;
61
- }
67
+ await expect(
68
+ themeAdd('stone', {cwd: tmpDir, overwrite: true}),
69
+ ).rejects.toMatchObject({code: 'ERR_PATH_TRAVERSAL'});
70
+ expect(fs.readFileSync(victim, 'utf-8')).toBe('outside\n');
71
+ expect(fs.lstatSync(dest).isSymbolicLink()).toBe(true);
72
+ });
62
73
 
63
- expect(isErrorCode(error?.code)).toBe(true);
74
+ it('never creates a file through a dangling destination link', async () => {
75
+ const victim = path.join(outsideDir, 'created.txt');
76
+ fs.symlinkSync(victim, firstDestination('stone'));
77
+
78
+ await expect(themeAdd('stone', {cwd: tmpDir})).rejects.toMatchObject({
79
+ code: 'ERR_PATH_TRAVERSAL',
80
+ });
64
81
  expect(fs.existsSync(victim)).toBe(false);
65
82
  });
66
83
  });
@@ -1,6 +1,16 @@
1
1
  // @generated by scripts/sync-api-types.mjs from the JSDoc in api/**/*.mjs.
2
2
  // DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
3
3
 
4
+ /**
5
+ * Why `theme build` has no Core to generate with, and the fix for where it
6
+ * ran: the Astryx repository builds Core, an app installs it, and an installed
7
+ * Core that will not load is named.
8
+ *
9
+ * @param {string} cwd
10
+ * @param {any} importError - Why the import failed, when it threw.
11
+ * @returns {string}
12
+ */
13
+ export function coreUnavailableMessage(cwd: string, importError: any): string;
4
14
  /**
5
15
  * Import specifier for install/scaffold instructions. Drops a leading `src/`
6
16
  * from the cwd-relative dir (most consumers import from a file under src/) but
@@ -106,3 +116,17 @@ export function themeBuildFamily(files: string[], options: {
106
116
  * validateAdaptationEnrollment)
107
117
  */
108
118
  export type ComponentValueClass = "builtin" | "enrollment" | "unresolved";
119
+ /**
120
+ * The bindings one Core provides: the namespaces interception spreads, the
121
+ * functions the build calls, and why the import failed, if it did.
122
+ */
123
+ export type CoreBindings = {
124
+ themeModule: any;
125
+ rootModule: any;
126
+ importError: any;
127
+ defineTheme: any;
128
+ generateThemeRulesSplit: any;
129
+ generateOnMediaCSS: any;
130
+ generateAdaptationCSS: any;
131
+ dataTokenDefaults: any;
132
+ };
@@ -251,23 +251,18 @@ describe('themeBuildFamily()', () => {
251
251
  const dir = makeDir();
252
252
  const files = writeFamily(dir);
253
253
  const outputDir = path.join(dir, 'themes');
254
- const blockedTmp = path.join(
255
- outputDir,
256
- `${FAMILY_KEY}.js.${process.pid}.tmp`,
257
- );
258
- fs.mkdirSync(blockedTmp);
254
+ // A directory where the JS output goes fails its staging after the CSS
255
+ // output was already staged.
256
+ fs.mkdirSync(path.join(outputDir, `${FAMILY_KEY}.js`));
259
257
 
260
258
  await expect(build(dir, files)).rejects.toThrow(
261
259
  /Failed to write theme outputs/,
262
260
  );
263
- for (const output of OUTPUTS) {
264
- expect(fs.existsSync(path.join(outputDir, output))).toBe(false);
265
- }
261
+ expect(fs.existsSync(path.join(outputDir, `${FAMILY_KEY}.css`))).toBe(false);
262
+ expect(fs.existsSync(path.join(outputDir, `${FAMILY_KEY}.d.ts`))).toBe(false);
266
263
  expect(
267
- fs.existsSync(
268
- path.join(outputDir, `${FAMILY_KEY}.css.${process.pid}.tmp`),
269
- ),
270
- ).toBe(false);
264
+ fs.readdirSync(outputDir).filter(name => name.includes('.tmp-')),
265
+ ).toEqual([]);
271
266
  });
272
267
 
273
268
  it('rejects invalid graphs and keys before touching an existing trio', async () => {