@astryxdesign/cli 0.7.2-canary.fe2c655 → 0.7.2

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 (509) hide show
  1. package/CHANGELOG.md +119 -0
  2. package/README.md +175 -154
  3. package/api/build/_adapter.d.mts +8 -0
  4. package/api/build/_adapter.mjs +12 -1
  5. package/api/build/build.doc.mjs +4 -0
  6. package/api/build/help/help.mjs +1 -1
  7. package/api/build/kit/kit.mjs +3 -11
  8. package/api/component/_adapter.d.mts +72 -1
  9. package/api/component/_adapter.mjs +212 -18
  10. package/api/component/component.doc.mjs +6 -1
  11. package/api/component/component.mjs +82 -15
  12. package/api/component/component.test.mjs +80 -0
  13. package/api/component/doc-links.test.mjs +386 -0
  14. package/api/component/integration-export-name.test.mjs +644 -0
  15. package/api/component/list/list.mjs +42 -4
  16. package/api/discover/_adapter.d.mts +132 -12
  17. package/api/discover/_adapter.mjs +488 -27
  18. package/api/discover/_adapter.test.mjs +599 -17
  19. package/api/discover/_catalog-view.d.mts +16 -5
  20. package/api/discover/_catalog-view.mjs +32 -8
  21. package/api/discover/detail/doc/doc.mjs +9 -1
  22. package/api/discover/discover.d.mts +11 -0
  23. package/api/discover/discover.doc.mjs +9 -7
  24. package/api/discover/discover.mjs +121 -19
  25. package/api/discover/discover.type.d.mts +20 -7
  26. package/api/discover/discover.type.mjs +17 -7
  27. package/api/discover/list/list.d.mts +4 -3
  28. package/api/discover/list/list.mjs +17 -10
  29. package/api/docs/_adapter.d.mts +74 -0
  30. package/api/docs/_adapter.mjs +553 -16
  31. package/api/docs/docs.doc.mjs +2 -1
  32. package/api/docs/docs.mjs +2 -1
  33. package/api/docs/docs.test.mjs +5 -2
  34. package/api/docs/integration-tree.test.mjs +22 -0
  35. package/api/docs/integrationDocs.test.mjs +124 -0
  36. package/api/docs/list/list.mjs +12 -2
  37. package/api/docs/reference-blocks.test.mjs +1 -3
  38. package/api/doctor/_adapter.d.mts +108 -0
  39. package/api/doctor/_adapter.mjs +223 -4
  40. package/api/doctor/doctor.d.mts +199 -0
  41. package/api/doctor/doctor.doc.mjs +3 -2
  42. package/api/doctor/doctor.mjs +530 -34
  43. package/api/doctor/doctor.test.mjs +180 -14
  44. package/api/doctor/release-checks.test.mjs +156 -0
  45. package/api/doctor/theme-checks.mjs +17 -4
  46. package/api/doctor/theme-checks.test.mjs +223 -7
  47. package/api/error.d.mts +10 -0
  48. package/api/error.mjs +25 -0
  49. package/api/gap-report/gap-report.d.mts +3 -0
  50. package/api/gap-report/gap-report.doc.mjs +11 -3
  51. package/api/gap-report/gap-report.mjs +219 -52
  52. package/api/gap-report/gap-report.test.mjs +233 -0
  53. package/api/gap-report/gap-report.type.d.mts +41 -0
  54. package/api/gap-report/gap-report.type.mjs +18 -0
  55. package/api/hook/_adapter.mjs +3 -11
  56. package/api/hook/hook.doc.mjs +4 -0
  57. package/api/hook/list/list.d.mts +1 -1
  58. package/api/index.d.mts +2 -0
  59. package/api/index.mjs +2 -0
  60. package/api/init/_adapter.d.mts +22 -0
  61. package/api/init/_adapter.mjs +59 -0
  62. package/api/init/init.d.mts +7 -5
  63. package/api/init/init.doc.mjs +28 -7
  64. package/api/init/init.mjs +28 -4
  65. package/api/init/init.type.d.mts +48 -3
  66. package/api/init/init.type.mjs +37 -3
  67. package/api/init/run/run.d.mts +1 -1
  68. package/api/init/run/run.mjs +17 -6
  69. package/api/init/user/user.d.mts +14 -0
  70. package/api/init/user/user.mjs +87 -0
  71. package/api/init/user/user.test.mjs +232 -0
  72. package/api/integration/_adapter.d.mts +45 -0
  73. package/api/integration/_adapter.mjs +112 -0
  74. package/api/integration/add-contribution.component-names.test.mjs +66 -0
  75. package/api/integration/add-contribution.mjs +8 -4
  76. package/api/integration/add-contribution.test.mjs +4 -2
  77. package/api/integration/authoring-checks.mjs +110 -6
  78. package/api/integration/authoring-checks.test.mjs +808 -0
  79. package/api/integration/authoring-checks.type.d.mts +143 -0
  80. package/api/integration/authoring-checks.type.mjs +74 -2
  81. package/api/integration/check/check.d.mts +96 -0
  82. package/api/integration/check/check.mjs +357 -0
  83. package/api/integration/check/check.test.mjs +489 -0
  84. package/api/integration/integration-check.type.d.mts +118 -0
  85. package/api/integration/integration-check.type.mjs +84 -0
  86. package/api/integration/integrationCheck.doc.d.mts +10 -0
  87. package/api/integration/integrationCheck.doc.mjs +73 -0
  88. package/api/integration/integrationComponentConflicts.doc.mjs +45 -6
  89. package/api/integration/integrationDocConflicts.doc.mjs +22 -5
  90. package/api/integration/pack-check.mjs +364 -120
  91. package/api/integration/pack-check.test.mjs +730 -10
  92. package/api/integration/validate-integration-fixes.test.mjs +67 -0
  93. package/api/integration/validate-integration.d.mts +8 -0
  94. package/api/integration/validate-integration.mjs +63 -9
  95. package/api/integration/validate-integration.test.mjs +88 -8
  96. package/api/integration/validate-integration.type.d.mts +16 -0
  97. package/api/integration/validate-integration.type.mjs +14 -1
  98. package/api/integration/validate-unread-theme-folders.test.mjs +3 -2
  99. package/api/integration/validateIntegration.doc.mjs +19 -3
  100. package/api/search/_adapter.d.mts +11 -0
  101. package/api/search/_adapter.mjs +25 -0
  102. package/api/search/search.d.mts +82 -13
  103. package/api/search/search.doc.mjs +8 -3
  104. package/api/search/search.domain-priority.test.mjs +108 -18
  105. package/api/search/search.integration-check.test.mjs +24 -0
  106. package/api/search/search.mjs +645 -64
  107. package/api/search/search.test.mjs +179 -1
  108. package/api/search/search.type.d.mts +14 -1
  109. package/api/search/search.type.mjs +6 -1
  110. package/api/swizzle/_adapter.mjs +2 -2
  111. package/api/swizzle/copy/copy.mjs +29 -15
  112. package/api/template/template-integration.test.mjs +1 -1
  113. package/api/template/template.test.mjs +120 -3
  114. package/api/theme/_adapter.d.mts +16 -0
  115. package/api/theme/_adapter.mjs +32 -5
  116. package/api/theme/build/build.d.mts +2 -1
  117. package/api/theme/build/build.family.test.mjs +64 -0
  118. package/api/theme/build/build.mjs +250 -38
  119. package/api/theme/build/build.packed-old-core.test.mjs +20 -3
  120. package/api/theme/build/font-warning.mjs +5 -3
  121. package/api/theme/build/font-warning.test.mjs +2 -1
  122. package/api/theme/remove/remove.test.mjs +2 -2
  123. package/api/theme/targets/targets.mjs +3 -10
  124. package/api/theme/themeBuild.doc.mjs +4 -4
  125. package/api/theme/themeTargets.doc.mjs +4 -0
  126. package/api/theme/use/use.test.mjs +1 -1
  127. package/api/upgrade/_adapter.d.mts +40 -2
  128. package/api/upgrade/_adapter.mjs +118 -5
  129. package/api/upgrade/run/cli-behind-core.test.mjs +438 -0
  130. package/api/upgrade/run/run.mjs +35 -2
  131. package/api/upgrade/status/status.d.mts +21 -5
  132. package/api/upgrade/status/status.mjs +45 -8
  133. package/api/upgrade/upgrade.doc.mjs +6 -3
  134. package/api/upgrade/upgrade.mjs +5 -1
  135. package/api/upgrade/upgrade.type.d.mts +23 -0
  136. package/api/upgrade/upgrade.type.mjs +16 -1
  137. package/assets/codemods/__tests__/registry.test.mjs +1 -0
  138. package/assets/codemods/registry.mjs +1 -0
  139. package/assets/codemods/runner.mjs +7 -1
  140. package/assets/codemods/transforms/v0.7.2/__tests__/migrate-list-edge-compensation-to-edge.test.mjs +120 -0
  141. package/assets/codemods/transforms/v0.7.2/index.mjs +20 -0
  142. package/assets/codemods/transforms/v0.7.2/migrate-list-edge-compensation-to-edge.mjs +198 -0
  143. package/assets/discover/official-packages.json +367 -0
  144. package/assets/docs/README.md +10 -0
  145. package/assets/docs/author-a-theme.doc.mjs +3 -3
  146. package/assets/docs/browser-support.doc.mjs +1 -1
  147. package/assets/docs/getting-started.doc.mjs +26 -6
  148. package/assets/docs/styling-overview.doc.mjs +1 -1
  149. package/assets/docs/theme.doc.dense.mjs +1 -1
  150. package/assets/docs/theme.doc.mjs +2 -2
  151. package/assets/docs/theme.doc.zh.mjs +1 -1
  152. package/assets/docs/tree/add-a-component.doc.mjs +22 -2
  153. package/assets/docs/tree/add-a-theme.doc.mjs +13 -3
  154. package/assets/docs/tree/add-a-topic.doc.mjs +54 -9
  155. package/assets/docs/tree/agent-guidance.doc.mjs +19 -5
  156. package/assets/docs/tree/block-template.doc.mjs +8 -20
  157. package/assets/docs/tree/build-the-template.doc.mjs +1 -1
  158. package/assets/docs/tree/building-blocks.doc.mjs +4 -0
  159. package/assets/docs/tree/check-the-props.doc.mjs +201 -0
  160. package/assets/docs/tree/check-your-docs.doc.mjs +69 -11
  161. package/assets/docs/tree/checks.doc.mjs +32 -24
  162. package/assets/docs/tree/ci-and-containers.doc.mjs +88 -0
  163. package/assets/docs/tree/cli-command.doc.mjs +142 -0
  164. package/assets/docs/tree/cli.doc.mjs +6 -0
  165. package/assets/docs/tree/codemods.doc.mjs +17 -7
  166. package/assets/docs/tree/commands.doc.mjs +7 -1
  167. package/assets/docs/tree/component-family.doc.mjs +8 -6
  168. package/assets/docs/tree/component-imports.doc.mjs +21 -3
  169. package/assets/docs/tree/components.doc.mjs +7 -1
  170. package/assets/docs/tree/computer-agent-guide.doc.mjs +160 -0
  171. package/assets/docs/tree/computer-integrations.doc.mjs +93 -0
  172. package/assets/docs/tree/configuration.doc.mjs +7 -3
  173. package/assets/docs/tree/debug-and-gap-reports.doc.mjs +41 -22
  174. package/assets/docs/tree/define-the-theme.doc.mjs +6 -5
  175. package/assets/docs/tree/describe-the-component.doc.mjs +3 -3
  176. package/assets/docs/tree/docs.doc.mjs +9 -3
  177. package/assets/docs/tree/document-the-template.doc.mjs +1 -1
  178. package/assets/docs/tree/document-the-theme.doc.mjs +3 -3
  179. package/assets/docs/tree/export-template-assets.doc.mjs +1 -1
  180. package/assets/docs/tree/extend-or-replace.doc.mjs +19 -9
  181. package/assets/docs/tree/fonts-and-assets.doc.mjs +2 -2
  182. package/assets/docs/tree/generate-a-palette.doc.mjs +7 -5
  183. package/assets/docs/tree/global-integrations.doc.mjs +124 -0
  184. package/assets/docs/tree/global.doc.mjs +131 -0
  185. package/assets/docs/tree/grade-template-with-agent.doc.mjs +5 -5
  186. package/assets/docs/tree/install-once.doc.mjs +105 -0
  187. package/assets/docs/tree/integrations.doc.mjs +13 -1
  188. package/assets/docs/tree/layout-spacing.doc.mjs +2 -3
  189. package/assets/docs/tree/layout.doc.mjs +6 -6
  190. package/assets/docs/tree/links.doc.mjs +7 -6
  191. package/assets/docs/tree/migration-setup.doc.mjs +3 -2
  192. package/assets/docs/tree/migration-verification.doc.mjs +0 -1
  193. package/assets/docs/tree/migration.doc.mjs +0 -1
  194. package/assets/docs/tree/monorepos.doc.mjs +84 -0
  195. package/assets/docs/tree/package-and-test.doc.mjs +1 -1
  196. package/assets/docs/tree/page-template.doc.mjs +4 -8
  197. package/assets/docs/tree/problems.doc.mjs +205 -0
  198. package/assets/docs/tree/project-global-or-both.doc.mjs +131 -0
  199. package/assets/docs/tree/publishing.doc.mjs +6 -6
  200. package/assets/docs/tree/quick-start.doc.mjs +53 -27
  201. package/assets/docs/tree/reference.doc.mjs +192 -0
  202. package/assets/docs/tree/replace-a-core-component.doc.mjs +15 -7
  203. package/assets/docs/tree/replace-a-core-template.doc.mjs +10 -12
  204. package/assets/docs/tree/scaffold.doc.mjs +1 -1
  205. package/assets/docs/tree/sections-and-placement.doc.mjs +9 -11
  206. package/assets/docs/tree/see-it-in-an-app.doc.mjs +17 -5
  207. package/assets/docs/tree/short-and-findable.doc.mjs +9 -8
  208. package/assets/docs/tree/single-component.doc.mjs +15 -6
  209. package/assets/docs/tree/start-a-template.doc.mjs +18 -8
  210. package/assets/docs/tree/subcomponent.doc.mjs +6 -6
  211. package/assets/docs/tree/template-assets.doc.mjs +2 -2
  212. package/assets/docs/tree/template-doc-overview.doc.mjs +6 -17
  213. package/assets/docs/tree/template-fonts.doc.mjs +1 -1
  214. package/assets/docs/tree/template-icons.doc.mjs +1 -1
  215. package/assets/docs/tree/template-images-media.doc.mjs +1 -1
  216. package/assets/docs/tree/template-styles.doc.mjs +2 -2
  217. package/assets/docs/tree/templates.doc.mjs +7 -1
  218. package/assets/docs/tree/test-in-an-app.doc.mjs +7 -7
  219. package/assets/docs/tree/test-template-in-app.doc.mjs +5 -5
  220. package/assets/docs/tree/themes.doc.mjs +4 -0
  221. package/assets/docs/tree/tokens-and-setup.doc.mjs +19 -15
  222. package/assets/docs/tree/troubleshooting.doc.mjs +29 -13
  223. package/assets/docs/tree/type-scale.doc.mjs +3 -3
  224. package/assets/docs/tree/uninstall.doc.mjs +109 -0
  225. package/assets/docs/tree/updates.doc.mjs +92 -0
  226. package/assets/docs/tree/upgrading.doc.mjs +6 -6
  227. package/assets/docs/tree/use-a-theme-in-an-app.doc.mjs +14 -2
  228. package/assets/docs/tree/verify-packed-template.doc.mjs +3 -3
  229. package/assets/docs/tree/versioning.doc.mjs +10 -9
  230. package/assets/docs/tree/who-answers.doc.mjs +119 -0
  231. package/assets/docs/tree/write-the-template-file.doc.mjs +7 -6
  232. package/assets/docs/use-a-theme.doc.mjs +2 -2
  233. package/assets/docs/working-with-ai.doc.mjs +66 -13
  234. package/assets/theme.template.ts +1 -1
  235. package/authoring/codemod/codemod.doc.mjs +7 -3
  236. package/authoring/config/config.doc.mjs +9 -6
  237. package/authoring/config/type.ts +4 -2
  238. package/authoring/debug/parse.d.mts +5 -5
  239. package/authoring/discover/discover.doc.mjs +7 -1
  240. package/authoring/doctypes/_schema.d.mts +8 -3
  241. package/authoring/doctypes/_schema.mjs +1 -0
  242. package/authoring/doctypes/base/graph-fields.doc.mjs +6 -4
  243. package/authoring/doctypes/command/command.doc.mjs +20 -12
  244. package/authoring/doctypes/command/type.ts +14 -0
  245. package/authoring/doctypes/component/component.doc.mjs +30 -27
  246. package/authoring/doctypes/component/type.ts +21 -8
  247. package/authoring/doctypes/enum/enum.doc.mjs +7 -4
  248. package/authoring/doctypes/function/function.doc.mjs +9 -8
  249. package/authoring/doctypes/hook/hook.doc.mjs +5 -4
  250. package/authoring/doctypes/hook/type.ts +4 -0
  251. package/authoring/doctypes/namespace/namespace.doc.mjs +7 -6
  252. package/authoring/doctypes/reference/reference.doc.mjs +12 -11
  253. package/authoring/doctypes/schema/schema.doc.mjs +9 -9
  254. package/authoring/doctypes/template/integration-template.doc.d.mts +11 -0
  255. package/authoring/doctypes/template/integration-template.doc.mjs +49 -0
  256. package/authoring/doctypes/template/parse.d.mts +6 -2
  257. package/authoring/doctypes/template/parse.mjs +3 -1
  258. package/authoring/doctypes/template/template.doc.mjs +3 -3
  259. package/authoring/doctypes/template/type.ts +22 -0
  260. package/authoring/doctypes/theme/theme.doc.mjs +2 -2
  261. package/authoring/gap-report/gap-report.doc.mjs +2 -2
  262. package/authoring/identity/identity.doc.mjs +3 -3
  263. package/authoring/index.d.ts +2 -0
  264. package/authoring/integration/integration.doc.mjs +30 -6
  265. package/authoring/integration/parse.test.mjs +21 -0
  266. package/authoring/integration/schema.d.mts +16 -3
  267. package/authoring/integration/schema.mjs +34 -2
  268. package/authoring/integration/type.ts +12 -0
  269. package/clients/cli/bin/astryx.mjs +20 -3
  270. package/clients/cli/cli-command.test.mjs +166 -0
  271. package/clients/cli/commands/blog.doc.mjs +1 -0
  272. package/clients/cli/commands/build-theme.batch-report.test.mjs +17 -3
  273. package/clients/cli/commands/build-theme.font-warning.test.mjs +3 -3
  274. package/clients/cli/commands/build-theme.mjs +26 -13
  275. package/clients/cli/commands/build-theme.path-safety.test.mjs +224 -6
  276. package/clients/cli/commands/build-theme.registry.test.mjs +5 -2
  277. package/clients/cli/commands/build.doc.mjs +1 -0
  278. package/clients/cli/commands/build.exit-codes-doc.test.mjs +4 -0
  279. package/clients/cli/commands/cli-postinstall.test.mjs +49 -1
  280. package/clients/cli/commands/component.doc.mjs +1 -0
  281. package/clients/cli/commands/discover.broken-integration.test.mjs +11 -7
  282. package/clients/cli/commands/discover.doc.mjs +9 -1
  283. package/clients/cli/commands/discover.mjs +144 -93
  284. package/clients/cli/commands/discover.no-source.test.mjs +448 -33
  285. package/clients/cli/commands/discover.sources.test.mjs +20 -1
  286. package/clients/cli/commands/docs.doc.mjs +1 -0
  287. package/clients/cli/commands/docs.mjs +49 -19
  288. package/clients/cli/commands/docs.test.mjs +2 -2
  289. package/clients/cli/commands/doctor-integration-components.doc.mjs +19 -4
  290. package/clients/cli/commands/doctor-integration-docs.doc.mjs +20 -5
  291. package/clients/cli/commands/doctor-integration-templates.doc.mjs +1 -0
  292. package/clients/cli/commands/doctor-integration-validate.doc.mjs +22 -2
  293. package/clients/cli/commands/doctor-integration.test.mjs +888 -3
  294. package/clients/cli/commands/doctor.doc.mjs +12 -2
  295. package/clients/cli/commands/doctor.mjs +292 -7
  296. package/clients/cli/commands/doctor.test.mjs +10 -4
  297. package/clients/cli/commands/gap-report.doc.mjs +15 -2
  298. package/clients/cli/commands/gap-report.mjs +24 -2
  299. package/clients/cli/commands/gap-report.test.mjs +79 -0
  300. package/clients/cli/commands/hook.doc.mjs +1 -0
  301. package/clients/cli/commands/init.behavior.test.mjs +5 -0
  302. package/clients/cli/commands/init.doc.mjs +29 -3
  303. package/clients/cli/commands/integration-add.doc.mjs +7 -0
  304. package/clients/cli/commands/integration-authoring.test.mjs +49 -3
  305. package/clients/cli/commands/integration-check.doc.mjs +93 -0
  306. package/clients/cli/commands/integration-check.test.mjs +441 -0
  307. package/clients/cli/commands/integration-pack.doc.mjs +8 -1
  308. package/clients/cli/commands/integration-verify.doc.mjs +8 -1
  309. package/clients/cli/commands/integration.doc.mjs +6 -2
  310. package/clients/cli/commands/integration.mjs +119 -7
  311. package/clients/cli/commands/json-contract.test.mjs +11 -2
  312. package/clients/cli/commands/manifest.doc.mjs +1 -0
  313. package/clients/cli/commands/outside-project.test.mjs +774 -0
  314. package/clients/cli/commands/search.doc.mjs +1 -0
  315. package/clients/cli/commands/search.mjs +16 -11
  316. package/clients/cli/commands/search.test.mjs +429 -296
  317. package/clients/cli/commands/setup-nudge.test.mjs +102 -2
  318. package/clients/cli/commands/swizzle.doc.mjs +7 -0
  319. package/clients/cli/commands/swizzle.mjs +5 -3
  320. package/clients/cli/commands/swizzle.routing.test.mjs +19 -2
  321. package/clients/cli/commands/template.doc.mjs +1 -0
  322. package/clients/cli/commands/text-json-parity.test.mjs +5 -0
  323. package/clients/cli/commands/theme-add-lifecycle.test.mjs +3 -3
  324. package/clients/cli/commands/theme-add.doc.mjs +8 -1
  325. package/clients/cli/commands/theme-build.doc.mjs +3 -2
  326. package/clients/cli/commands/theme-eject.doc.mjs +1 -0
  327. package/clients/cli/commands/theme-list.behavior.test.mjs +6 -0
  328. package/clients/cli/commands/theme-list.doc.mjs +1 -0
  329. package/clients/cli/commands/theme-palette-generate.doc.mjs +1 -0
  330. package/clients/cli/commands/theme-remove.doc.mjs +7 -0
  331. package/clients/cli/commands/theme-targets.doc.mjs +1 -0
  332. package/clients/cli/commands/theme-template.doc.mjs +1 -0
  333. package/clients/cli/commands/theme-use.doc.mjs +7 -0
  334. package/clients/cli/commands/upgrade.config-ordering.test.mjs +2 -1
  335. package/clients/cli/commands/upgrade.doc.mjs +22 -1
  336. package/clients/cli/commands/upgrade.file-protection.test.mjs +3 -2
  337. package/clients/cli/commands/upgrade.hook-output.test.mjs +2 -1
  338. package/clients/cli/commands/upgrade.older-cli.test.mjs +160 -0
  339. package/clients/cli/index.mjs +112 -16
  340. package/clients/cli/lib/cli-error.mjs +4 -1
  341. package/clients/cli/lib/component-format.test.mjs +1 -1
  342. package/clients/cli/lib/define-command.mjs +2 -2
  343. package/clients/cli/lib/json-shim.mjs +3 -1
  344. package/clients/cli/lib/manifest.d.ts +9 -0
  345. package/clients/cli/lib/manifest.mjs +17 -1
  346. package/clients/cli/lib/manifest.test.mjs +25 -0
  347. package/foundation/agent-docs/agent-doc-state.d.mts +35 -6
  348. package/foundation/agent-docs/agent-doc-state.mjs +66 -6
  349. package/foundation/agent-docs/agent-docs.d.mts +4 -3
  350. package/foundation/agent-docs/agent-docs.mjs +11 -9
  351. package/foundation/agent-docs/computer-agent-text.d.mts +133 -0
  352. package/foundation/agent-docs/computer-agent-text.mjs +623 -0
  353. package/foundation/agent-docs/computer-agent-text.test.mjs +602 -0
  354. package/foundation/config/project.d.mts +35 -3
  355. package/foundation/config/project.mjs +226 -7
  356. package/foundation/config/project.test.mjs +66 -0
  357. package/foundation/config/theme-state.mjs +5 -4
  358. package/foundation/discovery/authoring-self-docs.mjs +2 -1
  359. package/foundation/discovery/cli-self-docs.mjs +20 -3
  360. package/foundation/discovery/cli-self-docs.test.mjs +3 -2
  361. package/foundation/discovery/component-discovery.d.mts +190 -10
  362. package/foundation/discovery/component-discovery.export-names.test.mjs +760 -0
  363. package/foundation/discovery/component-discovery.mjs +864 -56
  364. package/foundation/discovery/component-discovery.test.mjs +96 -0
  365. package/foundation/discovery/component-replacement.mjs +20 -1
  366. package/foundation/discovery/component-replacement.test.mjs +40 -0
  367. package/foundation/discovery/copied-passages.d.mts +216 -0
  368. package/foundation/discovery/copied-passages.mjs +720 -0
  369. package/foundation/discovery/copied-passages.test.mjs +124 -0
  370. package/foundation/discovery/doc-consistency.d.mts +34 -0
  371. package/foundation/discovery/doc-consistency.mjs +513 -0
  372. package/foundation/discovery/doc-consistency.test.mjs +347 -0
  373. package/foundation/discovery/doc-fluff.d.mts +112 -0
  374. package/foundation/discovery/doc-fluff.mjs +328 -0
  375. package/foundation/discovery/doc-fluff.test.mjs +142 -0
  376. package/foundation/discovery/doc-types.d.mts +157 -0
  377. package/foundation/discovery/doc-types.mjs +1231 -0
  378. package/foundation/discovery/doc-types.test.mjs +744 -0
  379. package/foundation/discovery/docs-discovery.d.mts +88 -0
  380. package/foundation/discovery/docs-discovery.mjs +83 -2
  381. package/foundation/discovery/integration-doc-files.d.mts +22 -0
  382. package/foundation/discovery/integration-doc-files.mjs +231 -0
  383. package/foundation/discovery/integration-doc-files.test.mjs +246 -0
  384. package/foundation/discovery/module-exports.d.mts +27 -0
  385. package/foundation/discovery/module-exports.mjs +356 -31
  386. package/foundation/discovery/module-exports.test.mjs +172 -0
  387. package/foundation/discovery/package-tsconfig.d.mts +14 -0
  388. package/foundation/discovery/package-tsconfig.mjs +228 -0
  389. package/foundation/discovery/package-tsconfig.test.mjs +210 -0
  390. package/foundation/discovery/project-typescript.d.mts +32 -0
  391. package/foundation/discovery/project-typescript.mjs +89 -0
  392. package/foundation/discovery/prop-drift.d.mts +297 -0
  393. package/foundation/discovery/prop-drift.mjs +2428 -0
  394. package/foundation/discovery/prop-drift.test.mjs +2337 -0
  395. package/foundation/discovery/runner-allowlist.d.mts +40 -0
  396. package/foundation/discovery/runner-allowlist.mjs +53 -0
  397. package/foundation/discovery/stray-pointers.d.mts +52 -0
  398. package/foundation/discovery/stray-pointers.mjs +293 -0
  399. package/foundation/discovery/stray-pointers.test.mjs +86 -0
  400. package/foundation/discovery/template-adapter.mjs +74 -5
  401. package/foundation/discovery/template-needs.mjs +5 -2
  402. package/foundation/discovery/theme-imports.mjs +8 -4
  403. package/foundation/discovery/theme-imports.test.mjs +1 -1
  404. package/foundation/discovery/type-match.d.mts +53 -0
  405. package/foundation/discovery/type-match.mjs +660 -0
  406. package/foundation/discovery/type-match.test.mjs +169 -0
  407. package/foundation/discovery/type-text.d.mts +68 -0
  408. package/foundation/discovery/type-text.mjs +658 -0
  409. package/foundation/discovery/type-text.test.mjs +196 -0
  410. package/foundation/discovery/unresolved-references.d.mts +62 -0
  411. package/foundation/discovery/unresolved-references.mjs +790 -0
  412. package/foundation/discovery/unresolved-references.test.mjs +567 -0
  413. package/foundation/doc-compiler/doc-loads.test.mjs +51 -1
  414. package/foundation/doc-compiler/inputs.mjs +27 -1
  415. package/foundation/doc-compiler/inputs.test.mjs +0 -1
  416. package/foundation/doc-compiler/links.d.mts +41 -5
  417. package/foundation/doc-compiler/links.mjs +167 -28
  418. package/foundation/doc-compiler/links.test.mjs +97 -4
  419. package/foundation/doc-compiler/read.d.mts +1 -1
  420. package/foundation/doc-compiler/tree.mjs +30 -1
  421. package/foundation/doc-compiler/tree.test.mjs +8 -1
  422. package/foundation/env/cli-command.d.mts +83 -0
  423. package/foundation/env/cli-command.mjs +152 -0
  424. package/foundation/env/cli-command.test.mjs +129 -0
  425. package/foundation/env/node-version.test.mjs +15 -3
  426. package/foundation/env/package-manager.d.mts +93 -9
  427. package/foundation/env/package-manager.global.test.mjs +66 -0
  428. package/foundation/env/package-manager.mjs +184 -13
  429. package/foundation/env/package-manager.test.mjs +341 -15
  430. package/foundation/env/project-scope.d.mts +9 -0
  431. package/foundation/env/project-scope.mjs +33 -0
  432. package/foundation/env/search-path.d.mts +42 -0
  433. package/foundation/env/search-path.mjs +205 -0
  434. package/foundation/env/search-path.test.mjs +122 -0
  435. package/foundation/env/user-dirs.d.mts +39 -0
  436. package/foundation/env/user-dirs.mjs +93 -0
  437. package/foundation/env/user-dirs.test.mjs +72 -0
  438. package/foundation/env/user-settings.d.mts +46 -0
  439. package/foundation/env/user-settings.mjs +106 -0
  440. package/foundation/env/user-settings.test.mjs +64 -0
  441. package/foundation/fs/file-protection.mjs +15 -1
  442. package/foundation/fs/module-loader.d.mts +2 -1
  443. package/foundation/fs/module-loader.mjs +60 -4
  444. package/foundation/fs/module-loader.stdout.test.mjs +3 -2
  445. package/foundation/fs/module-loader.test.mjs +194 -0
  446. package/foundation/fs/path-compare.d.mts +33 -0
  447. package/foundation/fs/path-compare.mjs +73 -0
  448. package/foundation/fs/paths.d.mts +57 -1
  449. package/foundation/fs/paths.mjs +131 -3
  450. package/foundation/fs/paths.test.mjs +10 -1
  451. package/foundation/integrations/computer-integrations.d.mts +78 -0
  452. package/foundation/integrations/computer-integrations.mjs +316 -0
  453. package/foundation/integrations/computer-integrations.test.mjs +292 -0
  454. package/foundation/integrations/contribution-inventory.d.mts +11 -0
  455. package/foundation/integrations/contribution-inventory.mjs +56 -3
  456. package/foundation/integrations/integration-warnings.mjs +3 -2
  457. package/foundation/integrations/integration-warnings.test.mjs +8 -9
  458. package/foundation/integrations/integrations.d.mts +25 -1
  459. package/foundation/integrations/integrations.mjs +46 -3
  460. package/foundation/integrations/issue.ts +6 -0
  461. package/foundation/integrations/validate-contributions.d.mts +2 -0
  462. package/foundation/integrations/validate-contributions.mjs +41 -1
  463. package/foundation/release/agent-text-worker.d.mts +4 -0
  464. package/foundation/release/agent-text-worker.mjs +52 -0
  465. package/foundation/release/cache.d.mts +310 -0
  466. package/foundation/release/cache.mjs +1216 -0
  467. package/foundation/release/cache.test.mjs +920 -0
  468. package/foundation/release/choose.d.mts +207 -0
  469. package/foundation/release/choose.mjs +677 -0
  470. package/foundation/release/choose.test.mjs +427 -0
  471. package/foundation/release/core-worker.d.mts +4 -0
  472. package/foundation/release/core-worker.mjs +38 -0
  473. package/foundation/release/global-state.d.mts +259 -0
  474. package/foundation/release/global-state.mjs +472 -0
  475. package/foundation/release/global-state.test.mjs +381 -0
  476. package/foundation/release/handoff.d.mts +60 -0
  477. package/foundation/release/handoff.e2e.test.mjs +278 -0
  478. package/foundation/release/handoff.mjs +357 -0
  479. package/foundation/release/lock-lease.d.mts +23 -0
  480. package/foundation/release/lock-lease.mjs +80 -0
  481. package/foundation/release/notices.d.mts +253 -0
  482. package/foundation/release/notices.mjs +596 -0
  483. package/foundation/release/notices.test.mjs +565 -0
  484. package/foundation/release/npm-guard.d.mts +4 -0
  485. package/foundation/release/npm-guard.mjs +126 -0
  486. package/foundation/release/project.d.mts +141 -0
  487. package/foundation/release/project.mjs +466 -0
  488. package/foundation/release/project.test.mjs +288 -0
  489. package/foundation/release/refresh-worker.d.mts +4 -0
  490. package/foundation/release/refresh-worker.mjs +27 -0
  491. package/foundation/release/run-context.d.mts +47 -0
  492. package/foundation/release/run-context.mjs +149 -0
  493. package/foundation/release/update-notice-env.d.mts +28 -0
  494. package/foundation/release/update-notice-env.mjs +39 -0
  495. package/foundation/response/base.d.ts +26 -3
  496. package/foundation/response/error-codes.d.mts +3 -1
  497. package/foundation/response/error-codes.doc.mjs +12 -0
  498. package/foundation/response/error-codes.mjs +17 -0
  499. package/foundation/response/error-codes.test.mjs +2 -0
  500. package/foundation/response/json-contract.test.mjs +7 -2
  501. package/foundation/response/json.mjs +4 -1
  502. package/foundation/response/response-types.doc.mjs +22 -11
  503. package/foundation/response/response-types.doc.test.mjs +46 -0
  504. package/foundation/response/response.doc.mjs +3 -3
  505. package/foundation/xle/browser-imports.test.mjs +74 -0
  506. package/foundation/xle/registry.mjs +4 -0
  507. package/foundation/xle/registry.test.mjs +28 -0
  508. package/package.json +11 -11
  509. package/scripts/postinstall.mjs +24 -5
@@ -13,6 +13,7 @@ import {themeBuild, themeBuildFamily} from './build.mjs';
13
13
  import {interceptCore} from './core-interception.mjs';
14
14
  import {ensureCoreBuilt} from '../../../clients/cli/commands/ensure-core-built.mjs';
15
15
  import {runCli} from '../../../test-utils/run-cli.mjs';
16
+ import {getCliInvocation} from '../../../foundation/env/package-manager.mjs';
16
17
  vi.setConfig({testTimeout: 120_000});
17
18
  const CLI_ROOT = path.resolve(import.meta.dirname, '../../..');
18
19
  const FAMILY_KEY = 'ocean-family';
@@ -252,6 +253,69 @@ describe('themeBuildFamily()', () => {
252
253
  expect(fs.existsSync(dtsPath)).toBe(false);
253
254
  });
254
255
 
256
+ it('refuses to write family outputs outside cwd, and checks them read-only', async () => {
257
+ const dir = makeDir();
258
+ const files = writeFamily(dir).map(file => path.join(dir, file));
259
+ const app = path.join(dir, 'app');
260
+ fs.mkdirSync(app);
261
+
262
+ await expect(
263
+ themeBuildFamily(files, {familyKey: FAMILY_KEY}, {cwd: app}),
264
+ ).rejects.toMatchObject({code: 'ERR_PATH_TRAVERSAL'});
265
+ const checked = await themeBuildFamily(
266
+ files,
267
+ {familyKey: FAMILY_KEY, check: true},
268
+ {cwd: app},
269
+ );
270
+ expect(checked?.type).toBe('theme.build.check');
271
+ expect(checked?.data.upToDate).toBe(false);
272
+ // The `.css.d.ts` import stub may be absent in check mode.
273
+ expect(checked?.data.stale).toEqual(
274
+ OUTPUTS.filter(file => !file.endsWith('.css.d.ts')).map(file => ({
275
+ path: `../themes/${file}`,
276
+ reason: 'missing',
277
+ })),
278
+ );
279
+ for (const output of OUTPUTS) {
280
+ expect(fs.existsSync(path.join(dir, 'themes', output))).toBe(false);
281
+ }
282
+ });
283
+
284
+ it('ends a stale family report for outputs outside cwd with a remedy that works', async () => {
285
+ const dir = makeDir();
286
+ const files = writeFamily(dir).map(file => path.join(dir, file));
287
+ const app = path.join(dir, 'app');
288
+ fs.mkdirSync(app);
289
+ const family = ['--family', ...files, '--family-key', FAMILY_KEY];
290
+
291
+ const stale = await runCli(['theme', 'build', ...family, '--check'], app);
292
+ expect(stale.code).toBe(1);
293
+ // The old remedy ran the build from cwd, which refuses these outputs.
294
+ expect(stale.stderr).not.toContain('Rebuild with:');
295
+
296
+ const lines = stale.stderr.split('\n').map(line => line.trim());
297
+ const runDir = lines.find(line => line.startsWith('cd '))?.slice(3);
298
+ expect(runDir).toBe(fs.realpathSync(path.join(dir, 'themes')));
299
+ const stem = `${getCliInvocation(runDir)} theme build `;
300
+ const [buildArgs, checkArgs] = lines
301
+ .filter(line => line.startsWith(stem))
302
+ .map(line => line.slice(stem.length).split(' '));
303
+ expect(checkArgs).toEqual([...buildArgs, '--check']);
304
+
305
+ // Following the remedy builds, and the check from there passes.
306
+ const built = await runCli(['theme', 'build', ...buildArgs], runDir);
307
+ expect(built.code).toBe(0);
308
+ for (const output of OUTPUTS) {
309
+ expect(fs.existsSync(path.join(dir, 'themes', output))).toBe(true);
310
+ }
311
+ const checked = await runCli(
312
+ ['--json', 'theme', 'build', ...checkArgs],
313
+ runDir,
314
+ );
315
+ expect(checked.code).toBe(0);
316
+ expect(JSON.parse(checked.stdout).data.upToDate).toBe(true);
317
+ });
318
+
255
319
  it('fails without publishing a mixed trio when staging a write fails', async () => {
256
320
  const dir = makeDir();
257
321
  const files = writeFamily(dir);
@@ -27,6 +27,10 @@
27
27
  * files; an internal callback lets the CLI make only the process status fail.
28
28
  * Human progress is emitted through the shared `logger` (silent by default), so
29
29
  * the CLI keeps its exact output while a programmatic caller stays quiet.
30
+ * Every output a build writes, explicit or beside the theme file, stays inside
31
+ * `cwd` (`ERR_PATH_TRAVERSAL` otherwise). Check mode writes nothing, so it
32
+ * compares outputs outside `cwd` read-only; only a relative `out` path that
33
+ * escapes `cwd` is refused there too.
30
34
  *
31
35
  * The installed `@astryxdesign/core` is an independently versioned optional
32
36
  * peer, so it can be older than the CLI. A theme that uses only baseline
@@ -58,12 +62,19 @@ import {isBuiltin} from 'node:module';
58
62
  import {fileURLToPath, pathToFileURL} from 'node:url';
59
63
  import {parse} from '@babel/parser';
60
64
  import {createJiti} from 'jiti';
61
- import {getCliInvocation} from '../../../foundation/env/package-manager.mjs';
65
+ import {
66
+ formatCliCommand,
67
+ getCliInvocation,
68
+ } from '../../../foundation/env/package-manager.mjs';
62
69
  import {
63
70
  CLI_ROOT,
64
71
  findCoreDir,
65
72
  findInstalledPackage,
73
+ findProjectCoreDir,
66
74
  } from '../../../foundation/fs/paths.mjs';
75
+ import {isGlobalCopy} from '../../../foundation/release/choose.mjs';
76
+ import {isInReleaseCache} from '../../../foundation/release/cache.mjs';
77
+ import {fetchCarriedThemeCoreDir} from '../_adapter.mjs';
67
78
  import {
68
79
  assertWithin,
69
80
  sanitizeName,
@@ -270,6 +281,25 @@ function realOrSelf(file) {
270
281
  }
271
282
  }
272
283
 
284
+ /**
285
+ * `target` spelled through real directories: the deepest ancestor that exists
286
+ * is resolved through its symlinks, and the missing rest is appended as given.
287
+ * @param {string} target absolute path
288
+ * @returns {string}
289
+ */
290
+ function realLocation(target) {
291
+ /** @type {string[]} */
292
+ const missing = [];
293
+ let existing = target;
294
+ while (!fs.existsSync(existing)) {
295
+ const parent = path.dirname(existing);
296
+ if (parent === existing) break;
297
+ missing.unshift(path.basename(existing));
298
+ existing = parent;
299
+ }
300
+ return path.join(realOrSelf(existing), ...missing);
301
+ }
302
+
273
303
  /**
274
304
  * The Core the project at `cwd` installed, when it differs from the one the
275
305
  * CLI's own import found and loads. The app's `<Theme>` runs on the app's
@@ -280,10 +310,15 @@ function realOrSelf(file) {
280
310
  *
281
311
  * @param {string} cwd
282
312
  * @param {CoreBindings} cliCore - The CLI's own Core, from {@link loadCliCore}.
313
+ * @param {{projectOnly?: boolean}} [options] `projectOnly`: never the
314
+ * carried Core, as for doctor (spec AST-081 FR3)
283
315
  * @returns {Promise<CoreBindings | null>}
284
316
  */
285
- async function loadProjectCore(cwd, cliCore) {
286
- const dir = findInstalledPackage(cwd, '@astryxdesign/core');
317
+ async function loadProjectCore(cwd, cliCore, {projectOnly = false} = {}) {
318
+ // Outside a project, the Core the answering copy carries (spec AST-081 FR3).
319
+ const dir =
320
+ findInstalledPackage(cwd, '@astryxdesign/core') ??
321
+ (projectOnly ? null : fetchCarriedThemeCoreDir(cwd));
287
322
  const themeFile = dir ? importTarget(dir, './theme') : null;
288
323
  if (!dir || !themeFile) return null;
289
324
  if (cliCore.themeModule) {
@@ -329,14 +364,54 @@ const _projectCores = new Map();
329
364
 
330
365
  /**
331
366
  * Choose the Core this build generates with: the project's installed one when
332
- * {@link loadProjectCore} finds it, else the CLI's own.
367
+ * {@link loadProjectCore} finds it, else the CLI's own. With `projectOnly`,
368
+ * only the project's installed Core: a build that cannot use it fails rather
369
+ * than read the Core the answering copy carries (spec AST-081 FR3).
333
370
  * @param {string} cwd
371
+ * @param {{projectOnly?: boolean}} [options]
334
372
  */
335
- async function selectCore(cwd) {
373
+ async function selectCore(cwd, {projectOnly = false} = {}) {
336
374
  const cliCore = await loadCliCore();
337
- if (!_projectCores.has(cwd))
338
- _projectCores.set(cwd, await loadProjectCore(cwd, cliCore));
339
- setCore(_projectCores.get(cwd) ?? cliCore);
375
+ const key = `${projectOnly ? 'project' : 'any'}\0${cwd}`;
376
+ if (!_projectCores.has(key))
377
+ _projectCores.set(key, await loadProjectCore(cwd, cliCore, {projectOnly}));
378
+ const projectCore = _projectCores.get(key);
379
+ // A global or cached copy's own Core is the carried one.
380
+ if (projectOnly && !projectCore && carriesCore() && !cliCoreIsProjects(cwd)) {
381
+ throw new AstryxError(
382
+ "The project's Core isn't installed or does not load.",
383
+ undefined,
384
+ ERROR_CODES.ERR_CORE_NOT_FOUND,
385
+ );
386
+ }
387
+ setCore(projectCore ?? cliCore);
388
+ }
389
+
390
+ /**
391
+ * Whether this CLI copy is one installed for the computer or kept in the
392
+ * per-user release cache, whose own Core import is the carried Core.
393
+ * @returns {boolean}
394
+ */
395
+ function carriesCore() {
396
+ return isGlobalCopy(CLI_ROOT) || isInReleaseCache(CLI_ROOT);
397
+ }
398
+
399
+ /**
400
+ * Whether the Core the CLI's own import loads is the one the project at
401
+ * `cwd` installed.
402
+ * @param {string} cwd
403
+ * @returns {boolean}
404
+ */
405
+ function cliCoreIsProjects(cwd) {
406
+ const dir = findProjectCoreDir(cwd);
407
+ if (!dir) return false;
408
+ try {
409
+ const own = fileURLToPath(import.meta.resolve('@astryxdesign/core/theme'));
410
+ const theirs = importTarget(dir, './theme');
411
+ return theirs != null && realOrSelf(own) === realOrSelf(theirs);
412
+ } catch {
413
+ return false;
414
+ }
340
415
  }
341
416
 
342
417
  /**
@@ -375,8 +450,7 @@ function inAstryxRepository(cwd) {
375
450
  * @returns {string}
376
451
  */
377
452
  export function coreUnavailableMessage(cwd, importError) {
378
- const why =
379
- '`astryx theme build` generates CSS with @astryxdesign/core, the same code the runtime <Theme> uses';
453
+ const why = `\`${formatCliCommand('theme build')}\` generates CSS with @astryxdesign/core, the same code the runtime <Theme> uses`;
380
454
  const detail = importError ? `\n Import error: ${importError.message}` : '';
381
455
  if (inAstryxRepository(cwd)) {
382
456
  return (
@@ -506,6 +580,127 @@ function checkedBuildOutputs(writes, cwd) {
506
580
  .map(write => path.relative(cwd, write.dest));
507
581
  }
508
582
 
583
+ /** Remedies that end a confinement error, by where the output path came from. */
584
+ const OUT_REMEDY = 'Pass an --out path inside it.';
585
+ const SOURCE_REMEDY =
586
+ 'Without --out the outputs go beside the theme file, so run the build from a directory that contains the theme file, or pass an --out path inside the working directory.';
587
+ const FAMILY_REMEDY =
588
+ "Family outputs go beside the family's root theme file, so run the build from a directory that contains it.";
589
+
590
+ /**
591
+ * Refuse an output outside `root`: every write is confined, whether `out` names
592
+ * the path or it defaults to the theme file's directory (architecture:cli-surface
593
+ * INV10). Both sides are compared by real location, so an absolute path that
594
+ * reaches the working directory through a symlink is inside it, and a symlink
595
+ * inside it that leads elsewhere is not.
596
+ * @param {string} dest absolute output path
597
+ * @param {string} root the directory writes stay inside: the working directory
598
+ * @param {string} remedy what to do instead; ends the error message
599
+ * @param {string} [shown] the path as the caller wrote it, for the message
600
+ */
601
+ function confineOutput(dest, root, remedy, shown = dest) {
602
+ try {
603
+ assertWithin(realLocation(dest), realOrSelf(path.resolve(root)), {
604
+ allowAbsolute: true,
605
+ label: 'output path',
606
+ });
607
+ } catch (err) {
608
+ if (!(err instanceof PathSafetyError)) throw err;
609
+ throw new AstryxError(
610
+ err.code === ERROR_CODES.ERR_PATH_TRAVERSAL
611
+ ? `Output path "${shown}" is outside the working directory (${root}). ${remedy}`
612
+ : err.message,
613
+ undefined,
614
+ ERROR_CODES.ERR_PATH_TRAVERSAL,
615
+ );
616
+ }
617
+ }
618
+
619
+ /** @param {Array<{dest: string}>} writes @param {string} root @param {string} remedy */
620
+ function confineOutputs(writes, root, remedy) {
621
+ for (const write of writes) confineOutput(write.dest, root, remedy);
622
+ }
623
+
624
+ /** @param {string} dest absolute output path @param {string} root @returns {boolean} whether a build at `root` refuses to write `dest` */
625
+ function refusedOutput(dest, root) {
626
+ try {
627
+ confineOutput(dest, root, '');
628
+ return false;
629
+ } catch (err) {
630
+ if (
631
+ err instanceof AstryxError &&
632
+ err.code === ERROR_CODES.ERR_PATH_TRAVERSAL
633
+ )
634
+ return true;
635
+ throw err;
636
+ }
637
+ }
638
+
639
+ /** @param {string[]} dirs absolute directories @returns {string | null} the deepest directory containing all of them, or null across roots */
640
+ function commonDirectory(dirs) {
641
+ let common = dirs[0];
642
+ for (const dir of dirs.slice(1)) {
643
+ while (
644
+ path.relative(common, dir).startsWith('..') ||
645
+ path.isAbsolute(path.relative(common, dir))
646
+ ) {
647
+ const parent = path.dirname(common);
648
+ if (parent === common) return null;
649
+ common = parent;
650
+ }
651
+ }
652
+ return common;
653
+ }
654
+
655
+ /** @param {string} value @returns {string} `value` as one shell word */
656
+ function shellWord(value) {
657
+ if (/^[A-Za-z0-9@%+=:,./_-]+$/.test(value)) return value;
658
+ return `'${value.replace(/'/g, `'\\''`)}'`;
659
+ }
660
+
661
+ /**
662
+ * How a stale check report ends when outputs sit outside the write root. A
663
+ * build there refuses them, and each output's `Source:` header records the
664
+ * directory its build ran from, so the build and the check both run from the
665
+ * nearest directory that contains every theme file and output.
666
+ * @param {Array<{dest: string}>} writes
667
+ * @param {string[]} sources absolute theme files the build reads
668
+ * @param {string} root the directory a build here keeps its writes inside
669
+ * @param {(relative: (file: string) => string) => string[]} args the arguments after `theme build`, given each path relative to the directory to run from
670
+ * @returns {string[] | null} report lines, or null when every output is inside `root`
671
+ */
672
+ function outsideCheckRemedy(writes, sources, root, args) {
673
+ if (!writes.some(write => refusedOutput(write.dest, root))) return null;
674
+ const real = (/** @type {string} */ file) => realLocation(path.resolve(file));
675
+ const dir = commonDirectory([
676
+ ...sources.map(file => path.dirname(real(file))),
677
+ ...writes.map(write => path.dirname(real(write.dest))),
678
+ ]);
679
+ if (!dir) {
680
+ return [
681
+ 'These outputs are outside the working directory, so a build here refuses them. Build and check them from one directory that contains the theme files and the outputs.',
682
+ ];
683
+ }
684
+ const command = (/** @type {string[]} */ extra) =>
685
+ formatCliCommand(
686
+ [
687
+ 'theme',
688
+ 'build',
689
+ ...args(file => path.relative(dir, real(file))),
690
+ ...extra,
691
+ ]
692
+ .map(shellWord)
693
+ .join(' '),
694
+ dir,
695
+ );
696
+ return [
697
+ 'These outputs are outside the working directory, so a build here refuses them, and each output records the directory it was built from. Build and check them from a directory that contains them:',
698
+ ` cd ${shellWord(dir)}`,
699
+ ` ${command([])}`,
700
+ ` ${command(['--check'])}`,
701
+ ];
702
+ }
703
+
509
704
  /** @param {Array<{dest: string, content: string}>} writes */
510
705
  function writeBuildOutputs(writes) {
511
706
  if (writes.length === 0) return;
@@ -2344,18 +2539,20 @@ function validateHeadingTypeAugmentationSupport(themeDef) {
2344
2539
  * `logger` (silent by default).
2345
2540
  *
2346
2541
  * @param {string} file - Theme file path, resolved against `cwd`.
2347
- * @param {{out?: string, check?: boolean, iconsSpecifier?: string, __prepareFamily?: boolean, __familyLoader?: any, __familyInterception?: any, __onPrivateVarError?: () => void}} [options] -
2542
+ * @param {{out?: string, check?: boolean, iconsSpecifier?: string, __prepareFamily?: boolean, __familyLoader?: any, __familyInterception?: any, __onPrivateVarError?: () => void, __writeRoot?: string}} [options] -
2348
2543
  * `out` overrides the output CSS path; `check` compares against on-disk outputs
2349
- * instead of writing. `iconsSpecifier` overrides the icon registry import
2544
+ * instead of writing. `__writeRoot` replaces `cwd` as the directory every
2545
+ * output must stay inside, for a CLI-internal scratch build. `iconsSpecifier` overrides the icon registry import
2350
2546
  * specifier in the generated module (e.g. `./icons.mjs`); when omitted, the
2351
2547
  * direct registry specifiers from the theme source are emitted unchanged.
2352
- * @param {{cwd?: string}} [ctx]
2548
+ * @param {{cwd?: string, projectCore?: boolean}} [ctx] `projectCore`: only the
2549
+ * project's installed Core (spec AST-081 FR3)
2353
2550
  * @returns {Promise<any>}
2354
2551
  */
2355
2552
  async function themeBuildInternal(
2356
2553
  file,
2357
2554
  options = {},
2358
- {cwd = process.cwd()} = {},
2555
+ {cwd = process.cwd(), projectCore = false} = {},
2359
2556
  /** @type {{compact?: boolean, trailers?: ThemeBuildTrailer[]}} */ report = {},
2360
2557
  ) {
2361
2558
  const filePath = path.resolve(cwd, file);
@@ -2374,7 +2571,14 @@ async function themeBuildInternal(
2374
2571
  // A compact report has no per-theme heading, so each warning names its file.
2375
2572
  const source = report.compact ? `${path.relative(cwd, filePath)}: ` : '';
2376
2573
 
2377
- await selectCore(cwd);
2574
+ if (projectCore && !findProjectCoreDir(cwd)) {
2575
+ throw new AstryxError(
2576
+ "The project's Core isn't installed.",
2577
+ undefined,
2578
+ ERROR_CODES.ERR_CORE_NOT_FOUND,
2579
+ );
2580
+ }
2581
+ await selectCore(cwd, {projectOnly: projectCore});
2378
2582
 
2379
2583
  // Standalone builds only need interception when an older Core could erase
2380
2584
  // adaptations. Family preparation always supplies the same recorder as its
@@ -2791,24 +2995,17 @@ async function themeBuildInternal(
2791
2995
  // Derive the default CSS name from the theme name so .css/.js/.d.ts
2792
2996
  // share one scheme; an explicit --out still wins.
2793
2997
  const baseName = themeDef.name;
2998
+ // Every write stays inside cwd. A CLI-internal caller that renders into a
2999
+ // scratch directory it creates and removes names that directory instead.
3000
+ const writeRoot = options.__writeRoot ?? cwd;
2794
3001
  let outPath;
2795
3002
  if (options.out) {
2796
3003
  outPath = path.resolve(cwd, options.out);
2797
- // Guard: relative paths must not escape cwd via `../`. Absolute paths are
2798
- // trusted (the user explicitly controls where output goes, like gcc -o).
2799
- if (!path.isAbsolute(options.out)) {
2800
- try {
2801
- assertWithin(options.out, cwd, {label: 'output path'});
2802
- } catch (err) {
2803
- if (err instanceof PathSafetyError) {
2804
- throw new AstryxError(
2805
- err.message,
2806
- undefined,
2807
- ERROR_CODES.ERR_PATH_TRAVERSAL,
2808
- );
2809
- }
2810
- throw err;
2811
- }
3004
+ // A build keeps the explicit output inside the write root, relative or
3005
+ // absolute. Check mode writes nothing, so it refuses only a relative path
3006
+ // that escapes cwd.
3007
+ if (!options.check || !path.isAbsolute(options.out)) {
3008
+ confineOutput(outPath, writeRoot, OUT_REMEDY, options.out);
2812
3009
  }
2813
3010
  } else {
2814
3011
  outPath = path.join(path.dirname(filePath), `${baseName}.css`);
@@ -2905,6 +3102,13 @@ async function themeBuildInternal(
2905
3102
  writes.push({dest: variantDtsPath, content: variantContent});
2906
3103
  }
2907
3104
 
3105
+ // Every output a build writes stays inside cwd, including the defaults
3106
+ // beside the theme file. Check mode writes nothing, so it compares outputs
3107
+ // outside cwd read-only.
3108
+ if (!options.check) {
3109
+ confineOutputs(writes, writeRoot, options.out ? OUT_REMEDY : SOURCE_REMEDY);
3110
+ }
3111
+
2908
3112
  // Check mode: compare generated content against what's on disk instead of
2909
3113
  // writing. A file is "stale" if it's missing or its content differs once the
2910
3114
  // volatile @generated `Command:` line is ignored. Returns a
@@ -2923,7 +3127,10 @@ async function themeBuildInternal(
2923
3127
  ` ${s.reason === 'missing' ? 'missing' : 'stale'}: ${s.path}`,
2924
3128
  );
2925
3129
  }
2926
- logger.error(`\n Rebuild with: ${buildCommand}`);
3130
+ // prettier-ignore
3131
+ const remedy = outsideCheckRemedy(writes, [filePath], writeRoot, relative => [relative(filePath), ...(options.out ? ['--out', relative(outPath)] : []), ...(options.iconsSpecifier ? ['--icons-specifier', options.iconsSpecifier] : [])]);
3132
+ if (remedy) logger.error(`\n ${remedy.join('\n ')}`);
3133
+ else logger.error(`\n Rebuild with: ${buildCommand}`);
2927
3134
  }
2928
3135
  return {
2929
3136
  type: 'theme.build.check',
@@ -3036,7 +3243,7 @@ function printBuildTrailer(trailer) {
3036
3243
  logger.log(`
3037
3244
  Install in your app (paths are relative to a file in src/; adjust if yours lives elsewhere):
3038
3245
 
3039
- import { Theme } from '@astryxdesign/core';
3246
+ import { Theme } from '@astryxdesign/core/theme';
3040
3247
  import { ${trailer.exportName} } from '${trailer.jsImport}';
3041
3248
  import '${trailer.cssImport}';
3042
3249
 
@@ -3046,7 +3253,7 @@ Install in your app (paths are relative to a file in src/; adjust if yours lives
3046
3253
 
3047
3254
  Or with a <link> tag:
3048
3255
 
3049
- import { Theme } from '@astryxdesign/core';
3256
+ import { Theme } from '@astryxdesign/core/theme';
3050
3257
  import { ${trailer.exportName} } from '${trailer.jsImport}';
3051
3258
 
3052
3259
  <link rel="stylesheet" href="${trailer.cssImport}" />
@@ -3072,7 +3279,7 @@ export function printBatchTrailer(trailers) {
3072
3279
  logger.log(`
3073
3280
  Install in your app (paths are relative to a file in src/; adjust if yours lives elsewhere):
3074
3281
 
3075
- import { Theme } from '@astryxdesign/core';
3282
+ import { Theme } from '@astryxdesign/core/theme';
3076
3283
  import { ${first.exportName} } from '${first.jsImport}';
3077
3284
  import '${first.cssImport}';
3078
3285
 
@@ -3082,7 +3289,7 @@ Install in your app (paths are relative to a file in src/; adjust if yours lives
3082
3289
 
3083
3290
  Or with a <link> tag:
3084
3291
 
3085
- import { Theme } from '@astryxdesign/core';
3292
+ import { Theme } from '@astryxdesign/core/theme';
3086
3293
  import { ${first.exportName} } from '${first.jsImport}';
3087
3294
 
3088
3295
  <link rel="stylesheet" href="${first.cssImport}" />
@@ -3121,7 +3328,7 @@ export function printCompactTrailer(trailers, {hint = false} = {}) {
3121
3328
  )
3122
3329
  .join(', ');
3123
3330
  logger.log(
3124
- `[note] Fonts named but not loaded: ${named}. Load them in your app (recipe: astryx docs typography).`,
3331
+ `[note] Fonts named but not loaded: ${named}. Load them in your app (recipe: ${formatCliCommand('docs typography')}).`,
3125
3332
  );
3126
3333
  }
3127
3334
  if (hint) {
@@ -3176,7 +3383,7 @@ export async function themeBuildForReport(
3176
3383
  return {receipt, trailer: trailers[0] ?? null};
3177
3384
  }
3178
3385
 
3179
- /** @param {string} file @param {{out?: string, check?: boolean, iconsSpecifier?: string}} [options] @param {{cwd?: string}} [ctx] @returns {Promise<import('../theme.type.mjs').ThemeBuildResponse | import('../theme.type.mjs').ThemeBuildCheckResponse | null>} */
3386
+ /** @param {string} file @param {{out?: string, check?: boolean, iconsSpecifier?: string}} [options] @param {{cwd?: string, projectCore?: boolean}} [ctx] `projectCore`: read only the project's installed Core, never the carried one (doctor; spec AST-081 FR3) @returns {Promise<import('../theme.type.mjs').ThemeBuildResponse | import('../theme.type.mjs').ThemeBuildCheckResponse | null>} */
3180
3387
  export async function themeBuild(file, options = {}, ctx = {}) {
3181
3388
  return /** @type {any} */ (themeBuildInternal(file, options, ctx));
3182
3389
  }
@@ -3449,6 +3656,8 @@ export async function themeBuildFamily(
3449
3656
  js: path.relative(cwd, jsPath),
3450
3657
  dts: path.relative(cwd, dtsPath),
3451
3658
  };
3659
+ // A family build's outputs stay inside cwd too; check mode only reads them.
3660
+ if (!options.check) confineOutputs(writes, cwd, FAMILY_REMEDY);
3452
3661
  if (options.check) {
3453
3662
  const stale = staleBuildOutputs(writes, cwd);
3454
3663
  const upToDate = stale.length === 0;
@@ -3465,7 +3674,10 @@ export async function themeBuildFamily(
3465
3674
  ` ${entry.reason === 'missing' ? 'missing' : 'stale'}: ${entry.path}`,
3466
3675
  );
3467
3676
  }
3468
- logger.error(`\n Rebuild with: ${buildCommand}`);
3677
+ // prettier-ignore
3678
+ const remedy = outsideCheckRemedy(writes, members.map(member => member.filePath), cwd, relative => ['--family', ...members.map(member => relative(member.filePath)), '--family-key', baseName, ...(options.iconsSpecifier ? ['--icons-specifier', options.iconsSpecifier] : [])]);
3679
+ if (remedy) logger.error(`\n ${remedy.join('\n ')}`);
3680
+ else logger.error(`\n Rebuild with: ${buildCommand}`);
3469
3681
  }
3470
3682
  return {
3471
3683
  type: 'theme.build.check',
@@ -37,6 +37,7 @@ import * as path from 'node:path';
37
37
  import {fileURLToPath} from 'node:url';
38
38
 
39
39
  import {findInstalledPackage} from '../../../foundation/fs/paths.mjs';
40
+ import {CLI_VERSION} from '../../../test-utils/release-fixtures.mjs';
40
41
 
41
42
  const HERE = path.dirname(fileURLToPath(import.meta.url));
42
43
  const REPO_ROOT = path.resolve(HERE, '../../../../..');
@@ -55,8 +56,12 @@ const RUNTIME_DEPS = [
55
56
  'zod',
56
57
  ];
57
58
 
58
- const OLD_CORE_VERSION = '0.5.4';
59
- const CURRENT_CORE_VERSION = '0.6.0';
59
+ // Both Cores carry the packed CLI's own version: a run through the bin in a
60
+ // project whose Core is another release is handed to that release (spec
61
+ // AST-081 FR2), so the old surface is told apart by what it exports, not by
62
+ // its version.
63
+ const OLD_CORE_VERSION = CLI_VERSION;
64
+ const CURRENT_CORE_VERSION = CLI_VERSION;
60
65
  const TSC_BIN = path.join(REPO_ROOT, 'node_modules/typescript/bin/tsc');
61
66
 
62
67
  /**
@@ -278,8 +283,20 @@ function buildTheme(themeFile, extraArgs = []) {
278
283
  'node_modules/@astryxdesign/cli/clients/cli/bin/astryx.mjs',
279
284
  );
280
285
  try {
286
+ // A packed CLI outside any project's package.json reads as installed for
287
+ // the computer, so it gets a home folder of its own: nothing it keeps for
288
+ // the computer lands in the real one.
289
+ const home = path.join(root, 'home');
290
+ const env = {
291
+ ...process.env,
292
+ HOME: home,
293
+ USERPROFILE: home,
294
+ XDG_CONFIG_HOME: path.join(home, '.config'),
295
+ XDG_CACHE_HOME: path.join(home, '.cache'),
296
+ NO_UPDATE_NOTIFIER: '1',
297
+ };
281
298
  // prettier-ignore
282
- const stdout = execFileSync(process.execPath, [bin, 'theme', 'build', ...(Array.isArray(themeFile) ? themeFile : [themeFile]), ...extraArgs, '--json'], {cwd: consumer, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe']});
299
+ const stdout = execFileSync(process.execPath, [bin, 'theme', 'build', ...(Array.isArray(themeFile) ? themeFile : [themeFile]), ...extraArgs, '--json'], {cwd: consumer, encoding: 'utf8', env, stdio: ['ignore', 'pipe', 'pipe']});
283
300
  return {status: 0, envelope: JSON.parse(stdout)};
284
301
  } catch (error) {
285
302
  const e = /** @type {any} */ (error);
@@ -21,9 +21,11 @@
21
21
  * @output Ordered, case-insensitively deduped family names, and the human
22
22
  * help snippet (<link> pair + @font-face) printed after the install
23
23
  * instructions.
24
- * @position Sits beside build.mjs (api/theme/build/). Pure functions, no IO,
25
- * so the `--json` receipt and the human output share one source of truth.
24
+ * @position Sits beside build.mjs (api/theme/build/). Pure functions, no IO
25
+ * beyond the CLI invocation the human help names (formatCliCommand), so the
26
+ * `--json` receipt and the human output share one source of truth.
26
27
  */
28
+ import {formatCliCommand} from '../../../foundation/env/package-manager.mjs';
27
29
 
28
30
  // font-family values that never need loading: CSS generic families, the ui-*
29
31
  // system keywords, and CSS-wide keywords that can appear in a token value.
@@ -254,6 +256,6 @@ function fontLoadingRecipe(families) {
254
256
  font-display: swap;
255
257
  }
256
258
 
257
- Recipe and fallback-stack guidance: astryx docs typography
259
+ Recipe and fallback-stack guidance: ${formatCliCommand('docs typography')}
258
260
  `;
259
261
  }
@@ -15,6 +15,7 @@
15
15
 
16
16
  import {describe, it, expect} from 'vitest';
17
17
  import {collectUnloadedFonts, formatFontLoadingHelp} from './font-warning.mjs';
18
+ import {formatCliCommand} from '../../../foundation/env/package-manager.mjs';
18
19
 
19
20
  describe('collectUnloadedFonts', () => {
20
21
  it('returns webfont families from font-family tokens, skipping known fallbacks', () => {
@@ -255,7 +256,7 @@ describe('formatFontLoadingHelp', () => {
255
256
  });
256
257
 
257
258
  it('points at the docs recipe', () => {
258
- expect(help).toContain('astryx docs typography');
259
+ expect(help).toContain(formatCliCommand('docs typography'));
259
260
  });
260
261
 
261
262
  it('percent-encodes family names in the css2 URL', () => {
@@ -81,14 +81,14 @@ describe('themeRemove', () => {
81
81
  it('refuses to remove the default and names theme use', async () => {
82
82
  await expect(themeRemove('ocean', {cwd: tmpDir})).rejects.toMatchObject({
83
83
  code: 'ERR_THEME_INVALID',
84
- message: expect.stringContaining('astryx theme use'),
84
+ message: expect.stringContaining(' theme use <other-slug>`'),
85
85
  });
86
86
  });
87
87
 
88
88
  it('names theme add when the slug is not added', async () => {
89
89
  await expect(themeRemove('missing', {cwd: tmpDir})).rejects.toMatchObject({
90
90
  code: 'ERR_UNKNOWN_THEME',
91
- message: expect.stringContaining('astryx theme add missing'),
91
+ message: expect.stringContaining(' theme add missing`'),
92
92
  });
93
93
  });
94
94
  });
@@ -12,10 +12,10 @@
12
12
  */
13
13
 
14
14
  import * as path from 'node:path';
15
- import {findCoreDir} from '../../../foundation/fs/paths.mjs';
15
+ import {requireThemeCoreDir} from '../_adapter.mjs';
16
16
  import {collectThemingTargets} from '../../../foundation/discovery/theming-targets.mjs';
17
17
  import {ERROR_CODES} from '../../../foundation/response/error-codes.mjs';
18
- import {AstryxError} from '../../error.mjs';
18
+ import {AstryxError, coreDirOrThrow} from '../../error.mjs';
19
19
 
20
20
  /**
21
21
  * Whether a target matches the caller's filter loosely: any target whose key,
@@ -46,14 +46,7 @@ function matchesLoosely(target, filter) {
46
46
  * @returns {Promise<import('../theme.type.mjs').ThemeTargetsResponse>}
47
47
  */
48
48
  export async function themeTargets(filter, {cwd = process.cwd()} = {}) {
49
- const coreDir = findCoreDir(cwd);
50
- if (!coreDir) {
51
- throw new AstryxError(
52
- 'Could not find @astryxdesign/core package',
53
- undefined,
54
- ERROR_CODES.ERR_CORE_NOT_FOUND,
55
- );
56
- }
49
+ const coreDir = coreDirOrThrow(requireThemeCoreDir, cwd);
57
50
 
58
51
  const all = await collectThemingTargets(path.join(coreDir, 'src'));
59
52
  const needle = filter ? String(filter).toLowerCase() : null;