@astryxdesign/cli 0.6.4 → 0.6.5-canary.00f1ed9

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
@@ -4,6 +4,9 @@
4
4
  * @file `astryx integration add theme` — scaffold one strongly typed,
5
5
  * same-stem source/descriptor pair into an integration package and declare the
6
6
  * themes root on first use.
7
+ *
8
+ * `--from <base>` forks an existing theme's source files as the starting point
9
+ * instead of writing a blank `defineTheme` skeleton.
7
10
  */
8
11
 
9
12
  import * as fs from 'node:fs';
@@ -15,7 +18,10 @@ import {
15
18
  PathSafetyError,
16
19
  sanitizeName,
17
20
  } from '../../foundation/fs/path-safety.mjs';
18
- import {discoverThemeDirectory} from '../../foundation/discovery/theme-discovery.mjs';
21
+ import {
22
+ discoverBundledThemes,
23
+ discoverThemeDirectory,
24
+ } from '../../foundation/discovery/theme-discovery.mjs';
19
25
  import {
20
26
  findLocalIntegrationManifestOrNull,
21
27
  IntegrationRootConflictError,
@@ -24,6 +30,12 @@ import {
24
30
  import {loadManifestObject} from '../../foundation/integrations/integrations.mjs';
25
31
  import {themeDescriptorSource} from '../../foundation/integrations/theme-descriptor.mjs';
26
32
  import {assertContributionVisible} from '../../foundation/integrations/contribution-inventory.mjs';
33
+ import {
34
+ THEMES_CLI,
35
+ themesCliProblem,
36
+ withCliPeer,
37
+ } from '../../foundation/integrations/cli-requirement.mjs';
38
+ import {stripCopyrightHeader} from '../../foundation/text/copyright-header.mjs';
27
39
  import {
28
40
  applyWrites,
29
41
  findPackageDir,
@@ -65,6 +77,7 @@ function themeIdentity(slug) {
65
77
  .join(' ');
66
78
  return {
67
79
  slug,
80
+ identifier,
68
81
  displayName,
69
82
  exportName: `${identifier}Theme`,
70
83
  entry: `${identifier}Theme.ts`,
@@ -88,6 +101,182 @@ function themeDescriptor(identity) {
88
101
  });
89
102
  }
90
103
 
104
+ // ── --from: resolve and fork a base theme ───────────────────────────
105
+
106
+ /**
107
+ * Resolve a theme slug to its discovered record, searching the bundled themes
108
+ * and the package's own themes root (when it has one).
109
+ * @param {string} slug
110
+ * @param {string} packageDir
111
+ * @param {string} owner
112
+ * @param {string | undefined} themesRoot resolved absolute themes root, if any
113
+ * @returns {import('../../foundation/discovery/theme-discovery.mjs').DiscoveredTheme}
114
+ */
115
+ function resolveBaseTheme(slug, packageDir, owner, themesRoot) {
116
+ const normalized = slug.toLowerCase();
117
+
118
+ /** @type {import('../../foundation/discovery/theme-discovery.mjs').DiscoveredTheme[]} */
119
+ let candidates = [];
120
+ try {
121
+ candidates = discoverBundledThemes();
122
+ } catch {
123
+ // Bundled themes unavailable — not fatal, package themes may suffice.
124
+ }
125
+
126
+ // Also check the package's own themes root.
127
+ if (themesRoot && fs.existsSync(themesRoot)) {
128
+ try {
129
+ candidates = [...candidates, ...discoverThemeDirectory(themesRoot, owner)];
130
+ } catch {
131
+ // A broken local root is not the caller's problem here.
132
+ }
133
+ }
134
+
135
+ const match = candidates.find(t => t.slug.toLowerCase() === normalized);
136
+ if (!match) {
137
+ throw new AstryxError(
138
+ `Unknown base theme "${slug}". Available themes: ${candidates.map(t => t.slug).join(', ') || '(none)'}.`,
139
+ candidates.map(t => ({
140
+ name: t.slug,
141
+ reason: t.bundled ? 'bundled theme' : `provided by ${t.package}`,
142
+ })),
143
+ ERROR_CODES.ERR_UNKNOWN_THEME,
144
+ );
145
+ }
146
+ return match;
147
+ }
148
+
149
+ /**
150
+ * Rename a file from the base theme's namespace to the new theme's namespace.
151
+ * Files whose name starts with the base identifier get the new identifier;
152
+ * others keep their name as-is.
153
+ * @param {string} file POSIX relative path inside the theme directory
154
+ * @param {string} baseIdentifier
155
+ * @param {string} newIdentifier
156
+ * @returns {string}
157
+ */
158
+ function renameThemeFile(file, baseIdentifier, newIdentifier) {
159
+ const basename = path.posix.basename(file);
160
+ const dir = path.posix.dirname(file);
161
+ if (!basename.startsWith(baseIdentifier)) return file;
162
+ const renamed = newIdentifier + basename.slice(baseIdentifier.length);
163
+ return dir === '.' ? renamed : `${dir}/${renamed}`;
164
+ }
165
+
166
+ /**
167
+ * Rewrite a source file's contents from the base theme's names to the new
168
+ * theme's names. This replaces:
169
+ * 1. Identifier prefixes (camelCase variables, import paths): baseIdentifier
170
+ * followed by an uppercase letter.
171
+ * 2. The theme slug in string literals: 'baseSlug' → 'newSlug' and
172
+ * 'astryx-baseSlug' → 'astryx-newSlug'.
173
+ * 3. CSS custom properties scoped to the theme: --astryx-theme-baseSlug-.
174
+ * @param {string} source
175
+ * @param {string} baseSlug
176
+ * @param {string} baseIdentifier
177
+ * @param {string} newSlug
178
+ * @param {string} newIdentifier
179
+ * @returns {string}
180
+ */
181
+ function rewriteThemeSource(
182
+ source,
183
+ baseSlug,
184
+ baseIdentifier,
185
+ newSlug,
186
+ newIdentifier,
187
+ ) {
188
+ let result = source;
189
+ // 1. Identifier prefixes: neutralPalettes → oceanPalettes, neutralTheme → oceanTheme
190
+ const identRe = new RegExp(`${escapeRegExp(baseIdentifier)}(?=[A-Z])`, 'gu');
191
+ result = result.replace(identRe, newIdentifier);
192
+ // 2. String-literal theme names
193
+ result = result.replace(
194
+ new RegExp(`'astryx-${escapeRegExp(baseSlug)}'`, 'gu'),
195
+ `'astryx-${newSlug}'`,
196
+ );
197
+ result = result.replace(
198
+ new RegExp(`'${escapeRegExp(baseSlug)}'`, 'gu'),
199
+ `'${newSlug}'`,
200
+ );
201
+ // 3. CSS custom properties scoped to the base theme
202
+ result = result.replace(
203
+ new RegExp(
204
+ `--astryx-theme-${escapeRegExp(baseSlug)}-`,
205
+ 'gu',
206
+ ),
207
+ `--astryx-theme-${newSlug}-`,
208
+ );
209
+ return result;
210
+ }
211
+
212
+ /** @param {string} s */
213
+ function escapeRegExp(s) {
214
+ return s.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
215
+ }
216
+
217
+ /**
218
+ * Build the write plans for a forked theme.
219
+ * @param {import('../../foundation/discovery/theme-discovery.mjs').DiscoveredTheme} base
220
+ * @param {ReturnType<typeof themeIdentity>} identity new theme identity
221
+ * @param {string} themeDir absolute target directory
222
+ * @returns {import('./add-helpers.mjs').WritePlan[]}
223
+ */
224
+ function forkThemePlans(base, identity, themeDir) {
225
+ const baseIdentity = themeIdentity(base.slug);
226
+ /** @type {import('./add-helpers.mjs').WritePlan[]} */
227
+ const plans = [];
228
+
229
+ // Copy every file the base theme ships (entry first, then the rest), except
230
+ // the descriptor — we write a fresh one.
231
+ const filesToCopy = base.files.filter(
232
+ file => path.posix.basename(file) !== `${baseIdentity.exportName}.doc.mjs`,
233
+ );
234
+
235
+ for (const file of filesToCopy) {
236
+ const srcPath = path.join(base.sourceDir, file);
237
+ const newFileName = renameThemeFile(
238
+ file,
239
+ baseIdentity.identifier,
240
+ identity.identifier,
241
+ );
242
+ const destPath = assertWithin(newFileName, themeDir, {
243
+ label: `forked theme file "${newFileName}"`,
244
+ });
245
+
246
+ // Read source bytes; for text, strip copyright and rewrite identifiers.
247
+ const bytes = fs.readFileSync(srcPath);
248
+ const text = bytes.toString('utf-8');
249
+ const isText = Buffer.from(text, 'utf-8').equals(bytes);
250
+
251
+ /** @type {string | Buffer} */
252
+ let contents;
253
+ if (isText) {
254
+ contents = rewriteThemeSource(
255
+ stripCopyrightHeader(text),
256
+ base.slug,
257
+ baseIdentity.identifier,
258
+ identity.slug,
259
+ identity.identifier,
260
+ );
261
+ } else {
262
+ contents = bytes;
263
+ }
264
+ plans.push({path: destPath, contents, createOnly: true});
265
+ }
266
+
267
+ // Write a fresh descriptor — the fork is a new theme, not a derivative.
268
+ const descriptorPath = assertWithin(identity.descriptor, themeDir, {
269
+ label: 'theme descriptor file',
270
+ });
271
+ plans.push({
272
+ path: descriptorPath,
273
+ contents: themeDescriptor(identity),
274
+ createOnly: true,
275
+ });
276
+
277
+ return plans;
278
+ }
279
+
91
280
  /**
92
281
  * Read bytes back through the same discovery seam Project uses. A write is not
93
282
  * successful until the requested slug resolves.
@@ -125,7 +314,7 @@ async function verifyThemeContribution(packageDir, manifestFile, owner, slug) {
125
314
  * @returns {Promise<import('./integration-authoring.type.mjs').IntegrationAddResponse>}
126
315
  */
127
316
  export async function integrationAddTheme(name, options = {}) {
128
- const {cwd = process.cwd(), dryRun = false} = options;
317
+ const {cwd = process.cwd(), dryRun = false, from} = options;
129
318
  const packageDir = findPackageDir(cwd);
130
319
  const packageFile = path.join(packageDir, 'package.json');
131
320
  let pkg;
@@ -207,37 +396,86 @@ export async function integrationAddTheme(name, options = {}) {
207
396
  const themeDir = assertWithin(identity.slug, root, {
208
397
  label: 'theme directory',
209
398
  });
210
- const sourceFile = assertWithin(identity.entry, themeDir, {
211
- label: 'theme source file',
212
- });
213
- const descriptorFile = assertWithin(identity.descriptor, themeDir, {
214
- label: 'theme descriptor file',
215
- });
216
- // An existing folder is filled; only a file this would write is refused.
217
- for (const file of [sourceFile, descriptorFile]) {
218
- if (fs.existsSync(file)) {
399
+
400
+ // Resolve the base theme when --from is given, before writing anything.
401
+ /** @type {import('../../foundation/discovery/theme-discovery.mjs').DiscoveredTheme | undefined} */
402
+ let baseTheme;
403
+ if (from != null) {
404
+ if (from === name) {
219
405
  throw new AstryxError(
220
- `Refusing to overwrite existing file ${projectPath(path.relative(packageDir, file))}.`,
406
+ `Cannot fork theme "${from}" into itself.`,
221
407
  undefined,
222
- ERROR_CODES.ERR_FILE_EXISTS,
408
+ ERROR_CODES.ERR_INVALID_ARGUMENT,
223
409
  );
224
410
  }
411
+ baseTheme = resolveBaseTheme(from, packageDir, owner, root);
225
412
  }
226
413
 
227
414
  /** @type {import('./add-helpers.mjs').WritePlan[]} */
228
- const plans = [
229
- {path: sourceFile, contents: themeSource(identity), createOnly: true},
230
- {
231
- path: descriptorFile,
232
- contents: themeDescriptor(identity),
233
- createOnly: true,
234
- },
235
- ];
236
- const packageUpdate = packageJsonUpdate(
415
+ let plans;
416
+
417
+ if (baseTheme) {
418
+ // Fork: copy the base theme's files, renamed and rewritten.
419
+ plans = forkThemePlans(baseTheme, identity, themeDir);
420
+ // Check that no target file already exists.
421
+ for (const plan of plans) {
422
+ if (fs.existsSync(plan.path)) {
423
+ throw new AstryxError(
424
+ `Refusing to overwrite existing file ${projectPath(path.relative(packageDir, plan.path))}.`,
425
+ undefined,
426
+ ERROR_CODES.ERR_FILE_EXISTS,
427
+ );
428
+ }
429
+ }
430
+ } else {
431
+ // Blank scaffold.
432
+ const sourceFile = assertWithin(identity.entry, themeDir, {
433
+ label: 'theme source file',
434
+ });
435
+ const descriptorFile = assertWithin(identity.descriptor, themeDir, {
436
+ label: 'theme descriptor file',
437
+ });
438
+ for (const file of [sourceFile, descriptorFile]) {
439
+ if (fs.existsSync(file)) {
440
+ throw new AstryxError(
441
+ `Refusing to overwrite existing file ${projectPath(path.relative(packageDir, file))}.`,
442
+ undefined,
443
+ ERROR_CODES.ERR_FILE_EXISTS,
444
+ );
445
+ }
446
+ }
447
+ plans = [
448
+ {path: sourceFile, contents: themeSource(identity), createOnly: true},
449
+ {
450
+ path: descriptorFile,
451
+ contents: themeDescriptor(identity),
452
+ createOnly: true,
453
+ },
454
+ ];
455
+ }
456
+
457
+ let packageUpdate = packageJsonUpdate(
237
458
  packageFile,
238
459
  rootPath,
239
460
  path.basename(manifestFile),
240
461
  );
462
+ // A CLI older than the one that reads typed theme descriptors rejects the
463
+ // themes root and withholds the package's themes and docs. Declare the CLI
464
+ // that reads them as a peer, so an older one is flagged at install instead.
465
+ {
466
+ const expectedOriginal =
467
+ packageUpdate?.expectedOriginal ?? fs.readFileSync(packageFile);
468
+ const text = packageUpdate?.contents ?? expectedOriginal.toString('utf-8');
469
+ const current = JSON.parse(text);
470
+ if (themesCliProblem(current) != null) {
471
+ packageUpdate = {
472
+ contents:
473
+ JSON.stringify(withCliPeer(current, THEMES_CLI), null, 2) +
474
+ (text.endsWith('\n') ? '\n' : ''),
475
+ expectedOriginal,
476
+ };
477
+ }
478
+ }
241
479
  if (packageUpdate != null) {
242
480
  plans.push({
243
481
  path: packageFile,
@@ -287,7 +525,8 @@ export async function integrationAddTheme(name, options = {}) {
287
525
  writtenFiles.push(manifestPath);
288
526
  }
289
527
 
290
- return {
528
+ /** @type {import('./integration-authoring.type.mjs').IntegrationAddResponse} */
529
+ const response = {
291
530
  type: 'integration.add',
292
531
  data: {
293
532
  kind: 'theme',
@@ -299,4 +538,8 @@ export async function integrationAddTheme(name, options = {}) {
299
538
  dryRun,
300
539
  },
301
540
  };
541
+ if (from != null) {
542
+ response.data.from = from;
543
+ }
544
+ return response;
302
545
  }
@@ -77,6 +77,40 @@ describe('integrationAddTheme', () => {
77
77
  expect((await validateLocalIntegration(tmpDir)).issues).toEqual([]);
78
78
  });
79
79
 
80
+ it('declares the optional CLI peer that reads typed theme descriptors', async () => {
81
+ setup({includeFiles: false});
82
+ const result = await integrationAddTheme('ocean', {cwd: tmpDir});
83
+ expect(result.data.files).toContain('package.json');
84
+ const pkg = JSON.parse(
85
+ fs.readFileSync(path.join(tmpDir, 'package.json'), 'utf-8'),
86
+ );
87
+ expect(pkg.peerDependencies).toEqual({'@astryxdesign/cli': '>=0.6.4'});
88
+ expect(pkg.peerDependenciesMeta).toEqual({
89
+ '@astryxdesign/cli': {optional: true},
90
+ });
91
+ });
92
+
93
+ it('keeps a CLI peer that already reads typed theme descriptors', async () => {
94
+ const pkg = {
95
+ name: '@acme/themes',
96
+ version: '1.0.0',
97
+ peerDependencies: {'@astryxdesign/cli': '^0.7.2'},
98
+ };
99
+ fs.writeFileSync(
100
+ path.join(tmpDir, 'package.json'),
101
+ `${JSON.stringify(pkg, null, 2)}\n`,
102
+ );
103
+ fs.writeFileSync(
104
+ path.join(tmpDir, 'astryx.integration.mjs'),
105
+ 'export default {};\n',
106
+ );
107
+ const result = await integrationAddTheme('ocean', {cwd: tmpDir});
108
+ expect(result.data.files).not.toContain('package.json');
109
+ expect(
110
+ JSON.parse(fs.readFileSync(path.join(tmpDir, 'package.json'), 'utf-8')),
111
+ ).toEqual(pkg);
112
+ });
113
+
80
114
  it('dry-runs the identical receipt without writing anything', async () => {
81
115
  setup();
82
116
  const beforeManifest = fs.readFileSync(
@@ -276,3 +310,216 @@ describe('integrationAddTheme', () => {
276
310
  );
277
311
  });
278
312
  });
313
+
314
+ describe('integrationAddTheme --from', () => {
315
+ it('forks a bundled theme into the integration package', async () => {
316
+ setup();
317
+ const result = await integrationAddTheme('ocean', {
318
+ cwd: tmpDir,
319
+ from: 'neutral',
320
+ });
321
+
322
+ expect(result.type).toBe('integration.add');
323
+ expect(result.data.kind).toBe('theme');
324
+ expect(result.data.name).toBe('ocean');
325
+ expect(result.data.from).toBe('neutral');
326
+ expect(result.data.written).toBe(true);
327
+ expect(result.data.dryRun).toBe(false);
328
+ expect(result.data.root).toEqual({path: './themes', created: true});
329
+
330
+ // The entry file must exist with the new export name.
331
+ const entry = fs.readFileSync(
332
+ path.join(tmpDir, 'themes/ocean/oceanTheme.ts'),
333
+ 'utf-8',
334
+ );
335
+ expect(entry).toContain('oceanTheme');
336
+ expect(entry).toContain("name: 'ocean'");
337
+ // It must NOT contain the original theme identifier as a prefix.
338
+ expect(entry).not.toMatch(/\bneutralTheme\b/);
339
+ expect(entry).not.toMatch(/\bneutralPalettes\b/);
340
+ expect(entry).not.toMatch(/\bneutralSyntax\b/);
341
+ expect(entry).toContain('defineTheme');
342
+
343
+ // A fresh descriptor, not copied from the base.
344
+ const descriptor = fs.readFileSync(
345
+ path.join(tmpDir, 'themes/ocean/oceanTheme.doc.mjs'),
346
+ 'utf-8',
347
+ );
348
+ expect(descriptor).toContain("name: 'ocean'");
349
+ expect(descriptor).toContain("@astryxdesign/cli/authoring').ThemeDoc");
350
+ expect(descriptor).toContain('maintained: true');
351
+
352
+ // The theme must validate.
353
+ expect((await validateLocalIntegration(tmpDir)).issues).toEqual([]);
354
+ });
355
+
356
+ it('renames palette and support files from the base theme', async () => {
357
+ setup();
358
+ await integrationAddTheme('ocean', {cwd: tmpDir, from: 'neutral'});
359
+
360
+ const themeDir = path.join(tmpDir, 'themes/ocean');
361
+ const files = fs.readdirSync(themeDir).sort();
362
+ // neutralPalettes.ts → oceanPalettes.ts, etc.
363
+ expect(files).toContain('oceanTheme.ts');
364
+ expect(files).toContain('oceanTheme.doc.mjs');
365
+ expect(files).toContain('oceanPalettes.ts');
366
+ expect(files).toContain('oceanPaletteRefs.generated.ts');
367
+ expect(files).toContain('icons.tsx');
368
+ // Must not contain any file starting with 'neutral'.
369
+ expect(files.filter(f => f.startsWith('neutral'))).toEqual([]);
370
+ });
371
+
372
+ it('rewrites import paths inside forked files', async () => {
373
+ setup();
374
+ await integrationAddTheme('ocean', {cwd: tmpDir, from: 'neutral'});
375
+
376
+ const entry = fs.readFileSync(
377
+ path.join(tmpDir, 'themes/ocean/oceanTheme.ts'),
378
+ 'utf-8',
379
+ );
380
+ // Import references must use the new identifier.
381
+ expect(entry).toContain('./oceanPaletteRefs.generated');
382
+ expect(entry).not.toContain('./neutralPaletteRefs.generated');
383
+ // The icon registry import must also be renamed.
384
+ expect(entry).toContain('oceanIconRegistry');
385
+ expect(entry).not.toContain('neutralIconRegistry');
386
+ });
387
+
388
+ it('rewrites CSS custom properties scoped to the base theme', async () => {
389
+ setup();
390
+ await integrationAddTheme('ocean', {cwd: tmpDir, from: 'neutral'});
391
+
392
+ const entry = fs.readFileSync(
393
+ path.join(tmpDir, 'themes/ocean/oceanTheme.ts'),
394
+ 'utf-8',
395
+ );
396
+ expect(entry).toContain('--astryx-theme-ocean-');
397
+ expect(entry).not.toContain('--astryx-theme-neutral-');
398
+ });
399
+
400
+ it('rewrites the syntax theme name', async () => {
401
+ setup();
402
+ await integrationAddTheme('ocean', {cwd: tmpDir, from: 'neutral'});
403
+
404
+ const entry = fs.readFileSync(
405
+ path.join(tmpDir, 'themes/ocean/oceanTheme.ts'),
406
+ 'utf-8',
407
+ );
408
+ expect(entry).toContain("'astryx-ocean'");
409
+ expect(entry).not.toContain("'astryx-neutral'");
410
+ });
411
+
412
+ it('strips the copyright header from forked files', async () => {
413
+ setup();
414
+ await integrationAddTheme('ocean', {cwd: tmpDir, from: 'neutral'});
415
+
416
+ const entry = fs.readFileSync(
417
+ path.join(tmpDir, 'themes/ocean/oceanTheme.ts'),
418
+ 'utf-8',
419
+ );
420
+ expect(entry).not.toContain('Copyright (c) Meta Platforms');
421
+ });
422
+
423
+ it('includes data.from in the receipt only when --from is used', async () => {
424
+ setup();
425
+ const blank = await integrationAddTheme('plain', {cwd: tmpDir});
426
+ expect(blank.data).not.toHaveProperty('from');
427
+
428
+ const forked = await integrationAddTheme('ocean', {
429
+ cwd: tmpDir,
430
+ from: 'neutral',
431
+ });
432
+ expect(forked.data.from).toBe('neutral');
433
+ });
434
+
435
+ it('refuses to fork a theme into itself', async () => {
436
+ setup();
437
+ await expect(
438
+ integrationAddTheme('neutral', {cwd: tmpDir, from: 'neutral'}),
439
+ ).rejects.toMatchObject({code: 'ERR_INVALID_ARGUMENT'});
440
+ });
441
+
442
+ it('refuses an unknown base theme with a helpful suggestion list', async () => {
443
+ setup();
444
+ await expect(
445
+ integrationAddTheme('ocean', {cwd: tmpDir, from: 'nonexistent'}),
446
+ ).rejects.toMatchObject({
447
+ code: 'ERR_UNKNOWN_THEME',
448
+ message: expect.stringContaining('nonexistent'),
449
+ });
450
+ });
451
+
452
+ it('dry-runs with --from without writing anything', async () => {
453
+ setup();
454
+ const result = await integrationAddTheme('ocean', {
455
+ cwd: tmpDir,
456
+ from: 'neutral',
457
+ dryRun: true,
458
+ });
459
+
460
+ expect(result.data.written).toBe(false);
461
+ expect(result.data.dryRun).toBe(true);
462
+ expect(result.data.from).toBe('neutral');
463
+ expect(result.data.files).toContain('themes/ocean/oceanTheme.ts');
464
+ expect(result.data.files).toContain('themes/ocean/oceanTheme.doc.mjs');
465
+ expect(fs.existsSync(path.join(tmpDir, 'themes'))).toBe(false);
466
+ });
467
+
468
+ it('refuses to overwrite when the target already exists', async () => {
469
+ setup();
470
+ await integrationAddTheme('ocean', {cwd: tmpDir, from: 'neutral'});
471
+ await expect(
472
+ integrationAddTheme('ocean', {cwd: tmpDir, from: 'neutral'}),
473
+ ).rejects.toMatchObject({code: 'ERR_FILE_EXISTS'});
474
+ });
475
+
476
+ it('works with different bundled themes', async () => {
477
+ setup();
478
+ const result = await integrationAddTheme('dusk', {
479
+ cwd: tmpDir,
480
+ from: 'gothic',
481
+ });
482
+
483
+ expect(result.data.from).toBe('gothic');
484
+ const entry = fs.readFileSync(
485
+ path.join(tmpDir, 'themes/dusk/duskTheme.ts'),
486
+ 'utf-8',
487
+ );
488
+ expect(entry).toContain('duskTheme');
489
+ expect(entry).toContain("name: 'dusk'");
490
+ expect(entry).not.toMatch(/\bgothicTheme\b/);
491
+ expect((await validateLocalIntegration(tmpDir)).issues).toEqual([]);
492
+ });
493
+
494
+ it('forks from a theme the package already owns', async () => {
495
+ setup();
496
+ // First, add a blank theme.
497
+ await integrationAddTheme('base', {cwd: tmpDir});
498
+ // Then fork it.
499
+ const result = await integrationAddTheme('variant', {
500
+ cwd: tmpDir,
501
+ from: 'base',
502
+ });
503
+
504
+ expect(result.data.from).toBe('base');
505
+ const entry = fs.readFileSync(
506
+ path.join(tmpDir, 'themes/variant/variantTheme.ts'),
507
+ 'utf-8',
508
+ );
509
+ expect(entry).toContain('variantTheme');
510
+ expect(entry).toContain("name: 'variant'");
511
+ expect((await validateLocalIntegration(tmpDir)).issues).toEqual([]);
512
+ });
513
+
514
+ it('declares the optional CLI peer that reads typed theme descriptors', async () => {
515
+ setup({includeFiles: false});
516
+ await integrationAddTheme('ocean', {cwd: tmpDir, from: 'neutral'});
517
+ const pkg = JSON.parse(
518
+ fs.readFileSync(path.join(tmpDir, 'package.json'), 'utf-8'),
519
+ );
520
+ expect(pkg.peerDependencies).toEqual({'@astryxdesign/cli': '>=0.6.4'});
521
+ expect(pkg.peerDependenciesMeta).toEqual({
522
+ '@astryxdesign/cli': {optional: true},
523
+ });
524
+ });
525
+ });