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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (288) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +97 -90
  3. package/api/build/build.doc.mjs +6 -1
  4. package/api/build/build.test.mjs +22 -0
  5. package/api/build/kit/kit.mjs +44 -5
  6. package/api/component/_adapter.d.mts +25 -0
  7. package/api/component/_adapter.mjs +59 -5
  8. package/api/component/component.d.mts +6 -3
  9. package/api/component/component.doc.mjs +37 -17
  10. package/api/component/component.mjs +249 -9
  11. package/api/component/component.type.d.mts +25 -0
  12. package/api/component/component.type.mjs +44 -0
  13. package/api/discover/_adapter.d.mts +114 -6
  14. package/api/discover/_adapter.mjs +372 -17
  15. package/api/discover/_adapter.test.mjs +215 -0
  16. package/api/discover/_catalog-view.d.mts +115 -0
  17. package/api/discover/_catalog-view.mjs +203 -0
  18. package/api/discover/_catalog-view.test.mjs +128 -0
  19. package/api/discover/detail/detail.d.mts +18 -6
  20. package/api/discover/detail/detail.mjs +67 -13
  21. package/api/discover/detail/detail.test.mjs +85 -0
  22. package/api/discover/detail/item/item.d.mts +26 -0
  23. package/api/discover/detail/item/item.mjs +78 -0
  24. package/api/discover/detail/item/item.test.mjs +73 -0
  25. package/api/discover/discover.d.mts +3 -9
  26. package/api/discover/discover.doc.mjs +61 -18
  27. package/api/discover/discover.mjs +220 -36
  28. package/api/discover/discover.test.mjs +11 -2
  29. package/api/discover/discover.type.d.mts +147 -8
  30. package/api/discover/discover.type.mjs +102 -12
  31. package/api/discover/list/list.d.mts +20 -6
  32. package/api/discover/list/list.mjs +45 -12
  33. package/api/discover/list/list.test.mjs +46 -0
  34. package/api/discover/search/search.d.mts +18 -16
  35. package/api/discover/search/search.mjs +102 -56
  36. package/api/discover/search/search.test.mjs +144 -10
  37. package/api/docs/_adapter.d.mts +8 -3
  38. package/api/docs/_adapter.mjs +14 -6
  39. package/api/docs/docOverlays.test.mjs +27 -1
  40. package/api/docs/docs.doc.mjs +2 -2
  41. package/api/doctor/doctor.d.mts +8 -3
  42. package/api/doctor/doctor.doc.mjs +17 -8
  43. package/api/doctor/doctor.mjs +90 -9
  44. package/api/doctor/doctor.test.mjs +122 -10
  45. package/api/doctor/doctor.type.d.mts +1 -1
  46. package/api/doctor/doctor.type.mjs +1 -1
  47. package/api/gap-report/gap-report.doc.mjs +19 -10
  48. package/api/hook/hook.doc.mjs +6 -3
  49. package/api/index.d.mts +1 -0
  50. package/api/index.mjs +5 -3
  51. package/api/init/init.doc.mjs +17 -12
  52. package/api/integration/add-helpers.d.mts +5 -2
  53. package/api/integration/add-helpers.mjs +36 -9
  54. package/api/integration/add-theme.mjs +22 -1
  55. package/api/integration/add-theme.test.mjs +34 -0
  56. package/api/integration/authoring-checks.mjs +2 -2
  57. package/api/integration/integrationPackCheck.doc.mjs +3 -3
  58. package/api/integration/pack-check.lifecycle-output.test.mjs +107 -0
  59. package/api/integration/pack-check.mjs +82 -9
  60. package/api/integration/pack-check.test.mjs +90 -0
  61. package/api/integration/pack-check.type.mjs +1 -1
  62. package/api/json/assertResponse.doc.mjs +1 -1
  63. package/api/json/isError.doc.mjs +1 -1
  64. package/api/search/search.d.mts +27 -1
  65. package/api/search/search.doc.mjs +2 -2
  66. package/api/search/search.mjs +228 -16
  67. package/api/swizzle/swizzle.doc.mjs +7 -5
  68. package/api/template/copy/copy.mjs +1 -1
  69. package/api/template/copy/copy.test.mjs +9 -0
  70. package/api/template/template.doc.mjs +2 -1
  71. package/api/theme/add/add.mjs +17 -25
  72. package/api/theme/add/add.rollback.test.mjs +158 -0
  73. package/api/theme/add/add.staging.test.mjs +40 -23
  74. package/api/theme/build/build.family.test.mjs +7 -12
  75. package/api/theme/build/build.mjs +8 -18
  76. package/api/theme/build/build.rollback.test.mjs +148 -0
  77. package/api/theme/generateTonalPalette.doc.mjs +1 -2
  78. package/api/theme/listThemes.doc.mjs +1 -1
  79. package/api/theme/themeAdd.doc.mjs +9 -10
  80. package/api/theme/themeBuild.doc.mjs +13 -13
  81. package/api/theme/themeList.doc.mjs +1 -1
  82. package/api/theme/themeListAvailable.doc.mjs +2 -1
  83. package/api/theme/themePaletteGenerate.doc.mjs +15 -8
  84. package/api/theme/themeTargets.doc.mjs +3 -2
  85. package/api/theme/themeTemplate.doc.mjs +2 -1
  86. package/api/upgrade/run/files-changed.test.mjs +111 -0
  87. package/api/upgrade/run/run.mjs +5 -3
  88. package/api/upgrade/upgrade.doc.mjs +24 -22
  89. package/api/upgrade/upgrade.type.mjs +2 -2
  90. package/assets/codemods/__tests__/runner.test.mjs +3 -1
  91. package/assets/codemods/file-count.test.mjs +163 -0
  92. package/assets/codemods/integration-runner.mjs +3 -3
  93. package/assets/codemods/runner.mjs +5 -4
  94. package/assets/docs/README.md +4 -2
  95. package/assets/docs/browser-support.doc.mjs +11 -11
  96. package/assets/docs/color.doc.mjs +8 -2
  97. package/assets/docs/elevation.doc.mjs +6 -4
  98. package/assets/docs/getting-started.doc.mjs +5 -16
  99. package/assets/docs/icons.doc.mjs +2 -21
  100. package/assets/docs/illustrations.doc.mjs +7 -15
  101. package/assets/docs/internationalization.doc.mjs +7 -5
  102. package/assets/docs/layout.doc.dense.mjs +130 -82
  103. package/assets/docs/layout.doc.mjs +133 -77
  104. package/assets/docs/migration.doc.mjs +19 -21
  105. package/assets/docs/motion.doc.mjs +16 -3
  106. package/assets/docs/principles.doc.dense.mjs +5 -5
  107. package/assets/docs/principles.doc.mjs +8 -0
  108. package/assets/docs/principles.doc.zh.mjs +6 -6
  109. package/assets/docs/shape.doc.mjs +8 -3
  110. package/assets/docs/spacing.doc.mjs +7 -2
  111. package/assets/docs/styling-libraries.doc.mjs +6 -2
  112. package/assets/docs/styling.doc.mjs +19 -23
  113. package/assets/docs/theme.doc.dense.mjs +58 -18
  114. package/assets/docs/theme.doc.mjs +57 -47
  115. package/assets/docs/theme.doc.zh.mjs +9 -8
  116. package/assets/docs/tokens.doc.dense.mjs +2 -2
  117. package/assets/docs/tokens.doc.mjs +389 -8
  118. package/assets/docs/tokens.doc.zh.mjs +2 -2
  119. package/assets/docs/tree/add-a-component.doc.mjs +75 -0
  120. package/assets/docs/tree/add-a-theme.doc.mjs +85 -0
  121. package/assets/docs/tree/add-a-topic.doc.mjs +144 -0
  122. package/assets/docs/tree/agent-guidance.doc.mjs +138 -0
  123. package/assets/docs/tree/block-template.doc.mjs +130 -0
  124. package/assets/docs/tree/build-the-template.doc.mjs +28 -0
  125. package/assets/docs/tree/building-blocks.doc.mjs +46 -0
  126. package/assets/docs/tree/check-your-docs.doc.mjs +137 -0
  127. package/assets/docs/tree/checks.doc.mjs +119 -0
  128. package/assets/docs/tree/codemods.doc.mjs +147 -0
  129. package/assets/docs/tree/component-family.doc.mjs +113 -0
  130. package/assets/docs/tree/component-imports.doc.mjs +69 -0
  131. package/assets/docs/tree/component-lookups.doc.mjs +149 -0
  132. package/assets/docs/tree/components.doc.mjs +23 -0
  133. package/assets/docs/tree/configuration.doc.mjs +23 -0
  134. package/assets/docs/tree/debug-and-gap-reports.doc.mjs +182 -0
  135. package/assets/docs/tree/define-the-theme.doc.mjs +118 -0
  136. package/assets/docs/tree/describe-the-component.doc.mjs +57 -0
  137. package/assets/docs/tree/docs.doc.mjs +21 -0
  138. package/assets/docs/tree/document-the-template.doc.mjs +28 -0
  139. package/assets/docs/tree/document-the-theme.doc.mjs +68 -0
  140. package/assets/docs/tree/export-template-assets.doc.mjs +147 -0
  141. package/assets/docs/tree/extend-or-replace.doc.mjs +103 -0
  142. package/assets/docs/tree/fonts-and-assets.doc.mjs +106 -0
  143. package/assets/docs/tree/generate-a-palette.doc.mjs +66 -0
  144. package/assets/docs/tree/grade-template-with-agent.doc.mjs +105 -0
  145. package/assets/docs/tree/help.doc.mjs +16 -0
  146. package/assets/docs/tree/integrations.doc.mjs +25 -451
  147. package/assets/docs/tree/links.doc.mjs +98 -0
  148. package/assets/docs/tree/package-and-test.doc.mjs +32 -0
  149. package/assets/docs/tree/page-template.doc.mjs +71 -0
  150. package/assets/docs/tree/publishing.doc.mjs +111 -0
  151. package/assets/docs/tree/quick-start.doc.mjs +272 -0
  152. package/assets/docs/tree/replace-a-core-component.doc.mjs +104 -0
  153. package/assets/docs/tree/replace-a-core-template.doc.mjs +172 -0
  154. package/assets/docs/tree/sections-and-placement.doc.mjs +108 -0
  155. package/assets/docs/tree/see-it-in-an-app.doc.mjs +59 -0
  156. package/assets/docs/tree/ship.doc.mjs +16 -0
  157. package/assets/docs/tree/short-and-findable.doc.mjs +108 -0
  158. package/assets/docs/tree/single-component.doc.mjs +165 -0
  159. package/assets/docs/tree/start-a-template.doc.mjs +143 -0
  160. package/assets/docs/tree/subcomponent.doc.mjs +115 -0
  161. package/assets/docs/tree/template-assets.doc.mjs +64 -0
  162. package/assets/docs/tree/template-doc-overview.doc.mjs +109 -0
  163. package/assets/docs/tree/template-fonts.doc.mjs +102 -0
  164. package/assets/docs/tree/template-grading-rubric.doc.mjs +452 -0
  165. package/assets/docs/tree/template-icons.doc.mjs +97 -0
  166. package/assets/docs/tree/template-images-media.doc.mjs +127 -0
  167. package/assets/docs/tree/template-styles.doc.mjs +93 -0
  168. package/assets/docs/tree/templates.doc.mjs +34 -0
  169. package/assets/docs/tree/test-in-an-app.doc.mjs +115 -0
  170. package/assets/docs/tree/test-template-in-app.doc.mjs +128 -0
  171. package/assets/docs/tree/themes.doc.mjs +39 -0
  172. package/assets/docs/tree/troubleshooting.doc.mjs +149 -0
  173. package/assets/docs/tree/upgrading.doc.mjs +103 -0
  174. package/assets/docs/tree/use-a-theme-in-an-app.doc.mjs +51 -0
  175. package/assets/docs/tree/verify-packed-template.doc.mjs +77 -0
  176. package/assets/docs/tree/versioning.doc.mjs +161 -0
  177. package/assets/docs/tree/write-good-templates.doc.mjs +64 -0
  178. package/assets/docs/tree/write-the-template-file.doc.mjs +154 -0
  179. package/assets/docs/typography.doc.mjs +24 -4
  180. package/assets/docs/working-with-ai.doc.mjs +30 -22
  181. package/assets/templates/blocks/components/InternationalizationProvider/InternationalizationProvider01ShippedLocale.tsx +1 -1
  182. package/authoring/config/config.doc.mjs +9 -1
  183. package/authoring/config/parse.d.mts +2 -0
  184. package/authoring/config/parse.mjs +19 -0
  185. package/authoring/config/parse.test.mjs +8 -0
  186. package/authoring/config/type.ts +11 -0
  187. package/authoring/discover/discover.doc.d.mts +13 -0
  188. package/authoring/discover/discover.doc.mjs +138 -0
  189. package/authoring/discover/parse.d.mts +24 -0
  190. package/authoring/discover/parse.mjs +128 -0
  191. package/authoring/discover/parse.test.mjs +124 -0
  192. package/authoring/discover/type.ts +87 -0
  193. package/authoring/doctypes/_schema.d.mts +3 -2
  194. package/authoring/doctypes/_schema.mjs +6 -0
  195. package/authoring/doctypes/base/graph-fields.doc.mjs +3 -3
  196. package/authoring/doctypes/base/type.ts +4 -2
  197. package/authoring/doctypes/component/component.doc.mjs +6 -0
  198. package/authoring/doctypes/component/type.ts +8 -0
  199. package/authoring/doctypes/reference/reference.doc.mjs +7 -0
  200. package/authoring/doctypes/reference/type.ts +5 -0
  201. package/authoring/doctypes/schema/schema.doc.mjs +2 -2
  202. package/authoring/doctypes/template/template.doc.mjs +1 -1
  203. package/authoring/doctypes/template/type.ts +2 -2
  204. package/authoring/index.d.mts +1 -0
  205. package/authoring/index.d.ts +10 -0
  206. package/authoring/index.mjs +1 -0
  207. package/authoring/integration/integration.doc.mjs +12 -10
  208. package/clients/cli/commands/component/index.mjs +152 -55
  209. package/clients/cli/commands/component-batch.test.mjs +341 -0
  210. package/clients/cli/commands/component-ownership.test.mjs +89 -0
  211. package/clients/cli/commands/component.doc.mjs +27 -9
  212. package/clients/cli/commands/discover.doc.mjs +53 -9
  213. package/clients/cli/commands/discover.mjs +393 -118
  214. package/clients/cli/commands/discover.sources.test.mjs +267 -0
  215. package/clients/cli/commands/docs.doc.mjs +1 -1
  216. package/clients/cli/commands/docs.mjs +60 -17
  217. package/clients/cli/commands/doctor-integration-docs.doc.mjs +3 -2
  218. package/clients/cli/commands/doctor-integration.test.mjs +53 -0
  219. package/clients/cli/commands/doctor.doc.mjs +3 -1
  220. package/clients/cli/commands/doctor.mjs +49 -5
  221. package/clients/cli/commands/gap-report.doc.mjs +10 -9
  222. package/clients/cli/commands/init.doc.mjs +9 -6
  223. package/clients/cli/commands/integration-add.doc.mjs +9 -9
  224. package/clients/cli/commands/integration-authoring.test.mjs +61 -10
  225. package/clients/cli/commands/integration-pack.doc.mjs +5 -9
  226. package/clients/cli/commands/integration-real-world.test.mjs +1 -1
  227. package/clients/cli/commands/integration-verify.doc.mjs +22 -0
  228. package/clients/cli/commands/integration.doc.mjs +4 -4
  229. package/clients/cli/commands/integration.mjs +74 -43
  230. package/clients/cli/commands/manifest.doc.mjs +1 -1
  231. package/clients/cli/commands/search.doc.mjs +10 -3
  232. package/clients/cli/commands/search.mjs +21 -2
  233. package/clients/cli/commands/search.test.mjs +21 -4
  234. package/clients/cli/commands/swizzle.doc.mjs +1 -1
  235. package/clients/cli/commands/template.doc.mjs +1 -1
  236. package/clients/cli/commands/text-json-parity.test.mjs +7 -1
  237. package/clients/cli/commands/theme-add.doc.mjs +1 -1
  238. package/clients/cli/commands/theme-palette-generate.doc.mjs +3 -2
  239. package/clients/cli/commands/theme-palette.doc.mjs +1 -2
  240. package/clients/cli/commands/theme-targets.doc.mjs +2 -2
  241. package/clients/cli/commands/theme.doc.mjs +2 -1
  242. package/clients/cli/commands/upgrade.doc.mjs +62 -3
  243. package/clients/cli/index.mjs +28 -6
  244. package/clients/cli/lib/define-command.mjs +28 -4
  245. package/clients/cli/lib/define-command.test.mjs +54 -0
  246. package/clients/cli/lib/exit-codes.test.mjs +17 -1
  247. package/clients/cli/lib/json-shim.mjs +24 -14
  248. package/clients/cli/lib/manifest.mjs +18 -5
  249. package/clients/cli/lib/manifest.test.mjs +5 -2
  250. package/clients/cli/lib/parse-error-format.test.mjs +81 -0
  251. package/foundation/agent-docs/agent-docs.mjs +1 -1
  252. package/foundation/discovery/authoring-self-docs.mjs +1 -0
  253. package/foundation/discovery/authoring-self-docs.test.mjs +6 -2
  254. package/foundation/discovery/cli-self-docs.mjs +16 -2
  255. package/foundation/discovery/cli-self-docs.test.mjs +20 -0
  256. package/foundation/discovery/docs-discovery.mjs +5 -1
  257. package/foundation/discovery/docs-discovery.test.mjs +21 -0
  258. package/foundation/discovery/docs-section-key.d.mts +1 -1
  259. package/foundation/discovery/docs-section-key.mjs +1 -1
  260. package/foundation/doc-compiler/doc-loads.test.mjs +3 -2
  261. package/foundation/doc-compiler/inputs.test.mjs +0 -1
  262. package/foundation/doc-compiler/tree.d.mts +4 -0
  263. package/foundation/doc-compiler/tree.mjs +6 -1
  264. package/foundation/integrations/cli-requirement.d.mts +26 -6
  265. package/foundation/integrations/cli-requirement.mjs +46 -11
  266. package/foundation/integrations/cli-requirement.test.mjs +7 -2
  267. package/foundation/integrations/contribution-inventory.mjs +1 -1
  268. package/foundation/integrations/integrations.d.mts +14 -1
  269. package/foundation/integrations/integrations.mjs +41 -1
  270. package/foundation/integrations/integrations.test.mjs +31 -0
  271. package/foundation/response/batch.type.d.mts +33 -0
  272. package/foundation/response/batch.type.mjs +34 -0
  273. package/foundation/response/error-codes.doc.mjs +6 -8
  274. package/foundation/response/error-codes.test.mjs +30 -5
  275. package/foundation/response/response-types.doc.d.mts +4 -3
  276. package/foundation/response/response-types.doc.mjs +40 -10
  277. package/foundation/response/response-types.doc.test.mjs +23 -0
  278. package/foundation/response/response.doc.mjs +11 -10
  279. package/package.json +9 -9
  280. package/api/docs/docs.test.mjs +0 -243
  281. package/api/docs/integration-tree.test.mjs +0 -555
  282. package/api/docs/integrationDocs.test.mjs +0 -314
  283. package/api/search/search.test.mjs +0 -512
  284. package/assets/docs/tree/integrations.test.mjs +0 -62
  285. package/assets/docs/tree/writing-docs.doc.mjs +0 -286
  286. package/clients/cli/commands/docs.test.mjs +0 -294
  287. package/foundation/agent-docs/agent-docs.test.mjs +0 -1159
  288. package/foundation/doc-compiler/tree.test.mjs +0 -598
@@ -0,0 +1,158 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * `theme add` writes a theme as one transaction: when a later file fails to
5
+ * publish, every file it already replaced gets its previous bytes back and
6
+ * every file it created is removed. Publishing is forced to fail by wrapping
7
+ * the two calls that publish a staged file: `linkSync` for a new file and
8
+ * `renameSync` for a replacement.
9
+ *
10
+ * Separate file because vi.mock is hoisted and affects the whole module.
11
+ */
12
+
13
+ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
14
+ import * as os from 'node:os';
15
+ import * as path from 'node:path';
16
+
17
+ const failures = vi.hoisted(() => ({
18
+ /** Fail the Nth staged-file publish (1-based); 0 disables. */
19
+ publish: 0,
20
+ /** Also fail every rollback restore. */
21
+ restore: false,
22
+ count: 0,
23
+ }));
24
+
25
+ vi.mock('node:fs', async importOriginal => {
26
+ const actual = /** @type {typeof import('node:fs')} */ (
27
+ await importOriginal()
28
+ );
29
+ /** @param {string} source */
30
+ const isStaged = source => path.basename(String(source)).includes('.tmp');
31
+ /** @param {string} source */
32
+ const isRestore = source =>
33
+ path.basename(String(source)).includes('.restore-');
34
+ /** @param {string} source @param {string} op */
35
+ const maybeFail = (source, op) => {
36
+ if (isStaged(source) && failures.publish > 0) {
37
+ failures.count++;
38
+ if (failures.count === failures.publish) {
39
+ throw Object.assign(new Error(`EIO: forced ${op} failure`), {
40
+ code: 'EIO',
41
+ });
42
+ }
43
+ }
44
+ if (isRestore(source) && failures.restore) {
45
+ throw Object.assign(new Error(`EIO: forced restore failure`), {
46
+ code: 'EIO',
47
+ });
48
+ }
49
+ };
50
+ return {
51
+ ...actual,
52
+ linkSync: vi.fn((source, destination) => {
53
+ maybeFail(source, 'link');
54
+ return actual.linkSync(source, destination);
55
+ }),
56
+ renameSync: vi.fn((source, destination) => {
57
+ maybeFail(source, 'rename');
58
+ return actual.renameSync(source, destination);
59
+ }),
60
+ };
61
+ });
62
+
63
+ const fs = await import('node:fs');
64
+ const {themeAdd} = await import('./add.mjs');
65
+ const {listThemes} = await import('../_adapter.mjs');
66
+
67
+ let tmpDir;
68
+
69
+ beforeEach(() => {
70
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-themeadd-rollback-'));
71
+ failures.publish = 0;
72
+ failures.restore = false;
73
+ failures.count = 0;
74
+ });
75
+
76
+ afterEach(() => {
77
+ failures.publish = 0;
78
+ failures.restore = false;
79
+ fs.rmSync(tmpDir, {recursive: true, force: true});
80
+ });
81
+
82
+ function stoneFiles() {
83
+ const theme = listThemes().find(entry => entry.slug === 'stone');
84
+ if (!theme) throw new Error('missing bundled theme stone');
85
+ expect(theme.files.length).toBeGreaterThanOrEqual(2);
86
+ return theme.files.map(name =>
87
+ path.join(tmpDir, 'src', 'themes', 'stone', name),
88
+ );
89
+ }
90
+
91
+ /** Entries staging or rollback left behind in the theme directory. */
92
+ function strays() {
93
+ const dir = path.join(tmpDir, 'src', 'themes', 'stone');
94
+ if (!fs.existsSync(dir)) return [];
95
+ return fs
96
+ .readdirSync(dir, {recursive: true})
97
+ .map(String)
98
+ .filter(name => name.includes('.tmp') || name.includes('.restore-'));
99
+ }
100
+
101
+ describe('themeAdd rolls back a partial write', () => {
102
+ it('removes every created file when the second publish fails', async () => {
103
+ const files = stoneFiles();
104
+ failures.publish = 2;
105
+
106
+ await expect(themeAdd('stone', {cwd: tmpDir})).rejects.toMatchObject({
107
+ code: 'ERR_WRITE_FAILED',
108
+ });
109
+
110
+ for (const file of files) expect(fs.existsSync(file)).toBe(false);
111
+ expect(strays()).toEqual([]);
112
+ });
113
+
114
+ it('restores every replaced byte when the second publish fails', async () => {
115
+ const files = stoneFiles();
116
+ fs.mkdirSync(path.dirname(files[0]), {recursive: true});
117
+ const before = Buffer.from([0x00, 0xff, 0x0a, 0x62, 0x65, 0x66]);
118
+ fs.writeFileSync(files[0], before);
119
+ failures.publish = 2;
120
+
121
+ await expect(
122
+ themeAdd('stone', {cwd: tmpDir, overwrite: true}),
123
+ ).rejects.toMatchObject({code: 'ERR_WRITE_FAILED'});
124
+
125
+ expect(fs.readFileSync(files[0]).equals(before)).toBe(true);
126
+ for (const file of files.slice(1)) expect(fs.existsSync(file)).toBe(false);
127
+ expect(strays()).toEqual([]);
128
+ });
129
+
130
+ it('names a file it could not restore', async () => {
131
+ const files = stoneFiles();
132
+ fs.mkdirSync(path.dirname(files[0]), {recursive: true});
133
+ fs.writeFileSync(files[0], 'before\n');
134
+ failures.publish = 2;
135
+ failures.restore = true;
136
+
137
+ let error;
138
+ try {
139
+ await themeAdd('stone', {cwd: tmpDir, overwrite: true});
140
+ } catch (caught) {
141
+ error = caught;
142
+ }
143
+
144
+ expect(error?.code).toBe('ERR_WRITE_FAILED');
145
+ expect(error?.message).toContain('Could not restore');
146
+ expect(error?.message).toContain(files[0]);
147
+ });
148
+
149
+ it('writes every file when nothing fails', async () => {
150
+ const files = stoneFiles();
151
+
152
+ const result = await themeAdd('stone', {cwd: tmpDir});
153
+
154
+ expect(result.type).toBe('theme.add');
155
+ for (const file of files) expect(fs.existsSync(file)).toBe(true);
156
+ expect(strays()).toEqual([]);
157
+ });
158
+ });
@@ -6,14 +6,15 @@ import * as os from 'node:os';
6
6
  import * as path from 'node:path';
7
7
  import {themeAdd} from './add.mjs';
8
8
  import {listThemes} from '../_adapter.mjs';
9
- import {isErrorCode} from '../../../foundation/response/error-codes.mjs';
10
9
 
11
10
  let tmpDir;
12
11
  let outsideDir;
13
12
 
14
13
  beforeEach(() => {
15
14
  tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-themeadd-staging-'));
16
- outsideDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-themeadd-outside-'));
15
+ outsideDir = fs.mkdtempSync(
16
+ path.join(os.tmpdir(), 'astryx-themeadd-outside-'),
17
+ );
17
18
  });
18
19
 
19
20
  afterEach(() => {
@@ -22,45 +23,61 @@ afterEach(() => {
22
23
  });
23
24
 
24
25
  /**
25
- * Put a symlink where `theme add` stages the first file of `slug`.
26
+ * Where `theme add` writes the first file of `slug`.
26
27
  * @param {string} slug
27
- * @param {string} target
28
28
  */
29
- function plantStagingLink(slug, target) {
29
+ function firstDestination(slug) {
30
30
  const theme = listThemes().find(entry => entry.slug === slug);
31
31
  if (!theme) throw new Error(`missing bundled theme ${slug}`);
32
- const first = theme.files[0];
33
- const dest = path.join(tmpDir, 'src', 'themes', slug, first);
32
+ const dest = path.join(tmpDir, 'src', 'themes', slug, theme.files[0]);
34
33
  fs.mkdirSync(path.dirname(dest), {recursive: true});
35
- fs.symlinkSync(target, `${dest}.${process.pid}.tmp`);
36
34
  return dest;
37
35
  }
38
36
 
37
+ // Staging names are unpredictable and created exclusively, so an entry planted
38
+ // at a predictable name beside the destination is never written through.
39
39
  describe('themeAdd staging writes stay inside the project', () => {
40
- it('refuses a staging path that links outside the project', async () => {
40
+ it('never writes through a link planted at a predictable staging name', async () => {
41
41
  const victim = path.join(outsideDir, 'victim.txt');
42
42
  fs.writeFileSync(victim, 'outside\n');
43
- const dest = plantStagingLink('stone', victim);
43
+ const dest = firstDestination('stone');
44
+ fs.symlinkSync(victim, `${dest}.${process.pid}.tmp`);
45
+
46
+ await themeAdd('stone', {cwd: tmpDir});
44
47
 
45
- await expect(themeAdd('stone', {cwd: tmpDir})).rejects.toMatchObject({
46
- code: 'ERR_PATH_TRAVERSAL',
47
- });
48
48
  expect(fs.readFileSync(victim, 'utf-8')).toBe('outside\n');
49
- expect(fs.existsSync(dest)).toBe(false);
49
+ expect(fs.lstatSync(dest).isFile()).toBe(true);
50
50
  });
51
51
 
52
- it('never creates a file through a dangling staging link', async () => {
52
+ it('never creates a file through a dangling link at a predictable staging name', async () => {
53
53
  const victim = path.join(outsideDir, 'created.txt');
54
- plantStagingLink('stone', victim);
54
+ fs.symlinkSync(victim, `${firstDestination('stone')}.${process.pid}.tmp`);
55
+
56
+ await themeAdd('stone', {cwd: tmpDir});
57
+
58
+ expect(fs.existsSync(victim)).toBe(false);
59
+ });
60
+
61
+ it('refuses to replace a destination that links outside the project', async () => {
62
+ const victim = path.join(outsideDir, 'victim.txt');
63
+ fs.writeFileSync(victim, 'outside\n');
64
+ const dest = firstDestination('stone');
65
+ fs.symlinkSync(victim, dest);
55
66
 
56
- let error;
57
- try {
58
- await themeAdd('stone', {cwd: tmpDir});
59
- } catch (caught) {
60
- error = caught;
61
- }
67
+ await expect(
68
+ themeAdd('stone', {cwd: tmpDir, overwrite: true}),
69
+ ).rejects.toMatchObject({code: 'ERR_PATH_TRAVERSAL'});
70
+ expect(fs.readFileSync(victim, 'utf-8')).toBe('outside\n');
71
+ expect(fs.lstatSync(dest).isSymbolicLink()).toBe(true);
72
+ });
62
73
 
63
- expect(isErrorCode(error?.code)).toBe(true);
74
+ it('never creates a file through a dangling destination link', async () => {
75
+ const victim = path.join(outsideDir, 'created.txt');
76
+ fs.symlinkSync(victim, firstDestination('stone'));
77
+
78
+ await expect(themeAdd('stone', {cwd: tmpDir})).rejects.toMatchObject({
79
+ code: 'ERR_PATH_TRAVERSAL',
80
+ });
64
81
  expect(fs.existsSync(victim)).toBe(false);
65
82
  });
66
83
  });
@@ -251,23 +251,18 @@ describe('themeBuildFamily()', () => {
251
251
  const dir = makeDir();
252
252
  const files = writeFamily(dir);
253
253
  const outputDir = path.join(dir, 'themes');
254
- const blockedTmp = path.join(
255
- outputDir,
256
- `${FAMILY_KEY}.js.${process.pid}.tmp`,
257
- );
258
- fs.mkdirSync(blockedTmp);
254
+ // A directory where the JS output goes fails its staging after the CSS
255
+ // output was already staged.
256
+ fs.mkdirSync(path.join(outputDir, `${FAMILY_KEY}.js`));
259
257
 
260
258
  await expect(build(dir, files)).rejects.toThrow(
261
259
  /Failed to write theme outputs/,
262
260
  );
263
- for (const output of OUTPUTS) {
264
- expect(fs.existsSync(path.join(outputDir, output))).toBe(false);
265
- }
261
+ expect(fs.existsSync(path.join(outputDir, `${FAMILY_KEY}.css`))).toBe(false);
262
+ expect(fs.existsSync(path.join(outputDir, `${FAMILY_KEY}.d.ts`))).toBe(false);
266
263
  expect(
267
- fs.existsSync(
268
- path.join(outputDir, `${FAMILY_KEY}.css.${process.pid}.tmp`),
269
- ),
270
- ).toBe(false);
264
+ fs.readdirSync(outputDir).filter(name => name.includes('.tmp-')),
265
+ ).toEqual([]);
271
266
  });
272
267
 
273
268
  it('rejects invalid graphs and keys before touching an existing trio', async () => {
@@ -53,6 +53,7 @@ import {
53
53
  } from '../../../foundation/fs/path-safety.mjs';
54
54
  import {ERROR_CODES} from '../../../foundation/response/error-codes.mjs';
55
55
  import {AstryxError} from '../../error.mjs';
56
+ import {applyWrites} from '../../integration/add-helpers.mjs';
56
57
  import {logger} from '../../logger.mjs';
57
58
  import {loadComponentDoc} from '../../../foundation/discovery/component-loader.mjs';
58
59
  import {
@@ -224,26 +225,15 @@ function staleBuildOutputs(writes, cwd) {
224
225
  /** @param {Array<{dest: string, content: string}>} writes */
225
226
  function writeBuildOutputs(writes) {
226
227
  if (writes.length === 0) return;
227
- /** @type {Array<{tmp: string, dest: string}>} */
228
- const staged = [];
229
228
  try {
230
- fs.mkdirSync(path.dirname(writes[0].dest), {recursive: true});
231
- for (const write of writes) {
232
- const tmp = `${write.dest}.${process.pid}.tmp`;
233
- fs.writeFileSync(tmp, write.content);
234
- staged.push({tmp, dest: write.dest});
235
- }
236
- for (const stagedWrite of staged) {
237
- fs.renameSync(stagedWrite.tmp, stagedWrite.dest);
238
- }
229
+ applyWrites(
230
+ writes.map(write => ({
231
+ path: write.dest,
232
+ contents: write.content,
233
+ createOnly: false,
234
+ })),
235
+ );
239
236
  } catch (error) {
240
- for (const stagedWrite of staged) {
241
- try {
242
- fs.rmSync(stagedWrite.tmp, {force: true});
243
- } catch {
244
- // Best effort: the command still fails and never reports success.
245
- }
246
- }
247
237
  const message = `Failed to write theme outputs: ${/** @type {Error} */ (error).message}`;
248
238
  throw new AstryxError(message, undefined, ERROR_CODES.ERR_WRITE_FAILED);
249
239
  }
@@ -0,0 +1,148 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * `theme build` writes its CSS, JS, and declarations as one transaction: when
5
+ * a later output fails to publish, every output it already replaced gets its
6
+ * previous bytes back and every output it created is removed. Publishing is
7
+ * forced to fail by wrapping the two calls that publish a staged file:
8
+ * `linkSync` for a new file and `renameSync` for a replacement.
9
+ *
10
+ * Separate file because vi.mock is hoisted and affects the whole module.
11
+ * `themeBuild` needs a built core; the `node` project's globalSetup builds it.
12
+ */
13
+
14
+ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
15
+ import * as os from 'node:os';
16
+ import * as path from 'node:path';
17
+
18
+ const failures = vi.hoisted(() => ({
19
+ /** Fail the Nth staged-file publish (1-based); 0 disables. */
20
+ publish: 0,
21
+ count: 0,
22
+ }));
23
+
24
+ vi.mock('node:fs', async importOriginal => {
25
+ const actual = /** @type {typeof import('node:fs')} */ (
26
+ await importOriginal()
27
+ );
28
+ /** @param {string} source @param {string} op */
29
+ const maybeFail = (source, op) => {
30
+ if (!path.basename(String(source)).includes('.tmp')) return;
31
+ if (failures.publish === 0) return;
32
+ failures.count++;
33
+ if (failures.count === failures.publish) {
34
+ throw Object.assign(new Error(`EIO: forced ${op} failure`), {
35
+ code: 'EIO',
36
+ });
37
+ }
38
+ };
39
+ return {
40
+ ...actual,
41
+ linkSync: vi.fn((source, destination) => {
42
+ maybeFail(source, 'link');
43
+ return actual.linkSync(source, destination);
44
+ }),
45
+ renameSync: vi.fn((source, destination) => {
46
+ maybeFail(source, 'rename');
47
+ return actual.renameSync(source, destination);
48
+ }),
49
+ };
50
+ });
51
+
52
+ const fs = await import('node:fs');
53
+ const {themeBuild} = await import('./build.mjs');
54
+
55
+ vi.setConfig({testTimeout: 30000});
56
+
57
+ let tmpDir;
58
+
59
+ beforeEach(() => {
60
+ tmpDir = fs.mkdtempSync(
61
+ path.join(os.tmpdir(), 'astryx-theme-build-rollback-'),
62
+ );
63
+ failures.publish = 0;
64
+ failures.count = 0;
65
+ });
66
+
67
+ afterEach(() => {
68
+ failures.publish = 0;
69
+ fs.rmSync(tmpDir, {recursive: true, force: true});
70
+ });
71
+
72
+ /**
73
+ * Write a source for the theme named `rollbacktheme`. Each call uses a new
74
+ * file so the loader cannot serve an earlier version from its cache.
75
+ * @param {string} file @param {string} background
76
+ */
77
+ function writeTheme(file, background) {
78
+ fs.writeFileSync(
79
+ path.join(tmpDir, file),
80
+ `export default { name: 'rollbacktheme', tokens: { '--color-bg': '${background}' } };\n`,
81
+ );
82
+ return file;
83
+ }
84
+
85
+ const OUTPUTS = ['rollbacktheme.css', 'rollbacktheme.js', 'rollbacktheme.d.ts'];
86
+
87
+ /** @returns {Map<string, Buffer | null>} */
88
+ function readOutputs() {
89
+ return new Map(
90
+ OUTPUTS.map(name => {
91
+ const file = path.join(tmpDir, name);
92
+ return [name, fs.existsSync(file) ? fs.readFileSync(file) : null];
93
+ }),
94
+ );
95
+ }
96
+
97
+ function strays() {
98
+ return fs
99
+ .readdirSync(tmpDir)
100
+ .filter(name => name.includes('.tmp') || name.includes('.restore-'));
101
+ }
102
+
103
+ describe('themeBuild rolls back a partial write', () => {
104
+ it('removes every created output when the second publish fails', async () => {
105
+ const source = writeTheme('first.mjs', '#0a0a0a');
106
+ failures.publish = 2;
107
+
108
+ await expect(themeBuild(source, {}, {cwd: tmpDir})).rejects.toMatchObject({
109
+ code: 'ERR_WRITE_FAILED',
110
+ });
111
+
112
+ for (const bytes of readOutputs().values()) expect(bytes).toBeNull();
113
+ expect(strays()).toEqual([]);
114
+ });
115
+
116
+ it('restores every replaced output when the second publish fails', async () => {
117
+ const first = await themeBuild(
118
+ writeTheme('first.mjs', '#0a0a0a'),
119
+ {},
120
+ {cwd: tmpDir},
121
+ );
122
+ expect(first?.type).toBe('theme.build');
123
+ const before = readOutputs();
124
+ for (const bytes of before.values()) expect(bytes).not.toBeNull();
125
+
126
+ const next = writeTheme('next.mjs', '#fafafa');
127
+ failures.publish = 2;
128
+ await expect(themeBuild(next, {}, {cwd: tmpDir})).rejects.toMatchObject({
129
+ code: 'ERR_WRITE_FAILED',
130
+ });
131
+
132
+ const after = readOutputs();
133
+ for (const name of OUTPUTS) {
134
+ expect(
135
+ after.get(name)?.equals(/** @type {Buffer} */ (before.get(name))),
136
+ ).toBe(true);
137
+ }
138
+
139
+ // The failed build really had different bytes to write.
140
+ failures.publish = 0;
141
+ await themeBuild(next, {}, {cwd: tmpDir});
142
+ const css = readOutputs().get('rollbacktheme.css');
143
+ expect(
144
+ css?.equals(/** @type {Buffer} */ (before.get('rollbacktheme.css'))),
145
+ ).toBe(false);
146
+ expect(strays()).toEqual([]);
147
+ });
148
+ });
@@ -29,7 +29,7 @@ export const doc = {
29
29
  name: 'input',
30
30
  type: 'TonalPaletteGenerationInput',
31
31
  description:
32
- 'Families and seeds plus optional modes, shared stops, anchors, vibrancy from 0 to 100 (default 50), and neutral profile. Only generate an accent family when one is explicitly requested; clarify whether an ambiguous accent means one theme value or a tonal family.',
32
+ 'Families and seeds plus optional modes, shared stops, anchors, vibrancy from 0 to 100 (default 50), and neutral profile.',
33
33
  required: true,
34
34
  },
35
35
  ],
@@ -72,6 +72,5 @@ export const doc = {
72
72
  code: "generateTonalPalette({stops: [12.5, 50], families: [{id: 'blue', seed: '#0074e2'}]});",
73
73
  },
74
74
  ],
75
- command: 'theme palette generate',
76
75
  related: ['themePaletteGenerate'],
77
76
  };
@@ -13,7 +13,7 @@ export const doc = {
13
13
  displayName: 'listThemes()',
14
14
  summary: 'Read the CLI bundled-theme descriptors.',
15
15
  description:
16
- 'Reads the typed same-stem descriptors under templates/themes and returns normalized entries synchronously. This low-level helper keeps its historical bundled-only contract; project-aware themeList() and themeAdd() also discover source themes from installed integrations.',
16
+ "Synchronously returns the themes bundled with the CLI, including each one's entry file, export name and file list. Bundled themes only; use themeListAvailable() to include themes from installed integrations.",
17
17
  importPath: '@astryxdesign/cli/api',
18
18
  signature: 'listThemes(): BundledTheme[]',
19
19
  keywords: ['theme', 'themes', 'descriptor', 'bundled', 'adapter', 'list'],
@@ -44,6 +44,7 @@ export const doc = {
44
44
  type: 'string',
45
45
  description:
46
46
  'Project directory used for integration discovery and target paths.',
47
+ default: 'process.cwd()',
47
48
  },
48
49
  {
49
50
  name: 'options.package',
@@ -57,11 +58,6 @@ export const doc = {
57
58
  description:
58
59
  'Copy receipt with slug, displayName, maintained flag, owner package, outputDir, entry, exportName, and files.',
59
60
  },
60
- {
61
- type: 'theme.list',
62
- description:
63
- 'The CLI list affordance routes a bare `astryx theme add` or `--list` to themeListAvailable() and returns every available theme with its owner.',
64
- },
65
61
  ],
66
62
  throws: [
67
63
  {
@@ -74,7 +70,10 @@ export const doc = {
74
70
  when: 'the selected installed package has a blocking integration or theme-descriptor error',
75
71
  },
76
72
  {code: 'ERR_PATH_TRAVERSAL', when: 'the target path escapes cwd'},
77
- {code: 'ERR_NO_SOURCE', when: 'a theme file to copy is missing'},
73
+ {
74
+ code: 'ERR_NO_SOURCE',
75
+ when: 'the bundled theme descriptors cannot be read, or a theme file to copy is missing',
76
+ },
78
77
  {
79
78
  code: 'ERR_FILE_EXISTS',
80
79
  when: 'a destination exists and overwrite is not set',
@@ -82,12 +81,12 @@ export const doc = {
82
81
  {code: 'ERR_WRITE_FAILED', when: 'writing files fails'},
83
82
  ],
84
83
  examples: [
85
- {label: 'Copy a bundled theme', code: "await themeAdd('ocean');"},
84
+ {label: 'Copy a bundled theme', code: "await themeAdd('butter');"},
86
85
  {
87
- label: 'Copy an integration theme',
88
- code: "await themeAdd('ocean', {package: '@acme/themes'});",
86
+ label: 'Name the owner package and the destination',
87
+ code: "await themeAdd('butter', {package: '@astryxdesign/cli', targetPath: 'src/brand-theme'});",
89
88
  },
90
89
  ],
91
90
  command: 'theme add',
92
- related: ['themeList', 'listThemes'],
91
+ related: ['themeListAvailable', 'themeTemplate', 'listThemes'],
93
92
  };
@@ -14,14 +14,13 @@ export const doc = {
14
14
  name: 'themeBuild',
15
15
  namespace: 'cli/api',
16
16
  displayName: 'themeBuild()',
17
- summary: 'Compile a defineTheme file to CSS + JS + type declarations.',
17
+ summary:
18
+ 'Compile a defineTheme() file to scoped CSS, a JS module, and type declarations, or check committed outputs for drift in CI.',
18
19
  description:
19
- 'The compiler behind `astryx theme build`. Reads a file that calls defineTheme() and, ' +
20
- "via @astryxdesign/core's shared generator (the single source of truth, so the build " +
21
- 'emits the exact CSS the <Theme> runtime does), writes a scoped CSS file, a JS module ' +
22
- 'that re-exports the built theme, and a .d.ts (plus an optional .variants.d.ts when the ' +
23
- 'theme adds custom prop values). When another build step emits the icon registry, ' +
24
- '{iconsSpecifier} declares the fully specified module path for the generated JS import. ' +
20
+ 'The compiler behind `astryx theme build`. Reads a file that calls defineTheme() and ' +
21
+ 'writes a scoped CSS file, a JS module that re-exports the built theme, and a .d.ts ' +
22
+ '(plus an optional .variants.d.ts when the theme adds custom prop values). It uses ' +
23
+ "@astryxdesign/core's own generator, so the CSS matches what the <Theme> runtime emits. " +
25
24
  'With {check: true} it writes nothing and instead compares ' +
26
25
  'each output against disk, returning the drift: the CI guard for committed, generated theme CSS.',
27
26
  importPath: '@astryxdesign/cli/api',
@@ -48,7 +47,7 @@ export const doc = {
48
47
  name: 'options.out',
49
48
  type: 'string',
50
49
  description:
51
- 'Override the output CSS path; the sibling .js and .d.ts derive from it. A relative path must stay within cwd.',
50
+ 'Override the output CSS path. The .js, .d.ts and any .variants.d.ts are written in the same directory, named after the theme (<name>.js), not after the CSS file. A relative path must stay within cwd.',
52
51
  },
53
52
  {
54
53
  name: 'options.check',
@@ -61,13 +60,14 @@ export const doc = {
61
60
  name: 'options.iconsSpecifier',
62
61
  type: 'string',
63
62
  description:
64
- 'Override the icon-registry import specifier in the generated JS module, for example ./icons.mjs. When omitted, the source specifier is preserved.',
63
+ 'Override the import specifier of the icon registry in the generated JS module, for example ./icons.mjs. Takes effect only when the theme sets icons: to a named import; when omitted, the source specifier is kept.',
65
64
  },
66
65
  {
67
66
  name: 'ctx.cwd',
68
67
  type: 'string',
69
68
  description:
70
- 'Directory the theme file and @astryxdesign/core resolve against.',
69
+ 'Directory the theme file, a relative out path, and the returned output paths resolve against.',
70
+ default: 'process.cwd()',
71
71
  },
72
72
  ],
73
73
  returns: [
@@ -79,7 +79,7 @@ export const doc = {
79
79
  {
80
80
  type: 'theme.build.check',
81
81
  description:
82
- 'The {check: true} receipt: theme name, an upToDate flag, the stale outputs (each {path, reason: "missing" | "outdated"}), and the full list of checked paths. Writes nothing.',
82
+ 'The {check: true} receipt: theme name, an upToDate flag, the stale outputs (each {path, reason: "missing" | "outdated"}), and the full list of checked paths. Writes nothing. Resolves to null, like a normal build, when the theme produces no CSS.',
83
83
  },
84
84
  ],
85
85
  throws: [
@@ -102,7 +102,7 @@ export const doc = {
102
102
  },
103
103
  {
104
104
  code: 'ERR_CORE_INCOMPATIBLE',
105
- when: 'the selected theme carries ordered-adaptation intent but the installed @astryxdesign/core does not export generateAdaptationCSS (upgrade core)',
105
+ when: 'the installed @astryxdesign/core does not export generateAdaptationCSS and the theme either declares ordered adaptations or has lineage whose adaptation use could not be observed (upgrade core)',
106
106
  },
107
107
  {
108
108
  code: 'ERR_WRITE_FAILED',
@@ -124,5 +124,5 @@ export const doc = {
124
124
  },
125
125
  ],
126
126
  command: 'theme build',
127
- related: ['themeAdd', 'themeList', 'listThemes'],
127
+ related: ['themeTemplate', 'themeAdd', 'themeListAvailable', 'listThemes'],
128
128
  };
@@ -13,7 +13,7 @@ export const doc = {
13
13
  displayName: 'themeList()',
14
14
  summary: 'List themes bundled with this CLI build.',
15
15
  description:
16
- 'Projects the bundled typed theme descriptors into a synchronous theme.list envelope. This preserves the original programmatic API contract. The CLI command uses themeListAvailable() so installed integrations also appear.',
16
+ 'Synchronous list of the themes bundled with the CLI. It does not include themes from installed integrations; use themeListAvailable() for the list `astryx theme list` shows.',
17
17
  importPath: '@astryxdesign/cli/api',
18
18
  signature: 'themeList(): ThemeListResponse',
19
19
  keywords: ['theme', 'list', 'themes', 'bundled', 'available'],
@@ -13,7 +13,7 @@ export const doc = {
13
13
  displayName: 'themeListAvailable()',
14
14
  summary: 'List bundled and installed integration themes.',
15
15
  description:
16
- 'Loads Project for the requested directory, combines the CLI bundle with source themes from installed integrations, and projects each entry with its owner package. An unreadable project configuration degrades to the bundled descriptors.',
16
+ 'Lists the bundled themes plus source themes from integrations installed in cwd, each with its owner package. If the project configuration cannot be read, it falls back to the bundled themes.',
17
17
  importPath: '@astryxdesign/cli/api',
18
18
  signature:
19
19
  'themeListAvailable(options?: {cwd?: string, package?: string}): Promise<ThemeListResponse>',
@@ -24,6 +24,7 @@ export const doc = {
24
24
  type: 'string',
25
25
  description:
26
26
  'Project directory whose installed integrations contribute themes.',
27
+ default: 'process.cwd()',
27
28
  },
28
29
  {
29
30
  name: 'options.package',