@astryxdesign/cli 0.6.3-canary.db4e378 → 0.6.3-canary.ddb63c3

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 (236) hide show
  1. package/README.md +20 -1
  2. package/api/blog/blog.doc.mjs +1 -0
  3. package/api/build/build.doc.mjs +1 -0
  4. package/api/component/component.doc.mjs +1 -0
  5. package/api/discover/discover.doc.mjs +1 -0
  6. package/api/docs/_adapter.d.mts +3 -8
  7. package/api/docs/_adapter.mjs +13 -105
  8. package/api/docs/docs.doc.mjs +1 -0
  9. package/api/docs/list/list.mjs +3 -7
  10. package/api/doctor/doctor.d.mts +25 -3
  11. package/api/doctor/doctor.doc.mjs +1 -0
  12. package/api/doctor/doctor.mjs +185 -13
  13. package/api/doctor/doctor.test.mjs +326 -1
  14. package/api/gap-report/gap-report.doc.mjs +1 -0
  15. package/api/hook/_adapter.mjs +19 -5
  16. package/api/hook/hook.doc.mjs +1 -0
  17. package/api/hook/list/list.d.mts +1 -1
  18. package/api/hook/list/list.mjs +69 -17
  19. package/api/init/init.doc.mjs +1 -0
  20. package/api/integration/add-contribution.mjs +7 -7
  21. package/api/integration/add-contribution.test.mjs +37 -3
  22. package/api/integration/add-theme.mjs +34 -64
  23. package/api/integration/add-theme.test.mjs +105 -21
  24. package/api/integration/authoring-checks.test.mjs +8 -4
  25. package/api/integration/integration-block-exports.test.mjs +10 -6
  26. package/api/integration/integrationAdd.doc.mjs +1 -0
  27. package/api/integration/integrationAddAgentDoc.doc.mjs +1 -0
  28. package/api/integration/integrationAddCodemod.doc.mjs +1 -0
  29. package/api/integration/integrationAddComponent.doc.mjs +1 -0
  30. package/api/integration/integrationAddDoc.doc.mjs +1 -0
  31. package/api/integration/integrationAddTemplate.doc.mjs +1 -0
  32. package/api/integration/integrationAddTheme.doc.mjs +6 -5
  33. package/api/integration/integrationComponentConflicts.doc.mjs +1 -0
  34. package/api/integration/integrationDocConflicts.doc.mjs +1 -0
  35. package/api/integration/integrationPackCheck.doc.mjs +1 -0
  36. package/api/integration/integrationTemplateConflicts.doc.mjs +1 -0
  37. package/api/integration/pack-check.test.mjs +17 -47
  38. package/api/integration/summarizeIssues.doc.mjs +1 -0
  39. package/api/integration/validate-integration-fixes.test.mjs +1389 -0
  40. package/api/integration/validate-integration.mjs +50 -102
  41. package/api/integration/validate-integration.test.mjs +85 -23
  42. package/api/integration/validate-unread-theme-folders.test.mjs +110 -0
  43. package/api/integration/validateIntegration.doc.mjs +1 -0
  44. package/api/json/assertResponse.doc.mjs +1 -0
  45. package/api/json/isError.doc.mjs +1 -0
  46. package/api/json/parseResponse.doc.mjs +1 -0
  47. package/api/layout/layoutCheck.doc.mjs +1 -0
  48. package/api/layout/layoutExpand.doc.mjs +1 -0
  49. package/api/layout/layoutGrammar.doc.mjs +1 -0
  50. package/api/search/search.doc.mjs +1 -0
  51. package/api/search/search.mjs +16 -6
  52. package/api/swizzle/swizzle.doc.mjs +1 -0
  53. package/api/template/template-suffix.test.mjs +41 -21
  54. package/api/template/template.doc.mjs +1 -0
  55. package/api/theme/_adapter.d.mts +2 -3
  56. package/api/theme/_adapter.mjs +4 -5
  57. package/api/theme/add/add.binary.test.mjs +10 -17
  58. package/api/theme/add/add.test.mjs +14 -1
  59. package/api/theme/generateTonalPalette.doc.mjs +1 -0
  60. package/api/theme/integration-themes.test.mjs +39 -28
  61. package/api/theme/list/list.test.mjs +19 -20
  62. package/api/theme/listThemes.doc.mjs +6 -5
  63. package/api/theme/themeAdd.doc.mjs +4 -3
  64. package/api/theme/themeBuild.doc.mjs +1 -0
  65. package/api/theme/themeList.doc.mjs +6 -3
  66. package/api/theme/themeListAvailable.doc.mjs +5 -3
  67. package/api/theme/themePaletteGenerate.doc.mjs +1 -0
  68. package/api/theme/themeTargets.doc.mjs +1 -0
  69. package/api/theme/themeTemplate.doc.mjs +1 -0
  70. package/api/upgrade/_adapter.d.mts +12 -3
  71. package/api/upgrade/_adapter.mjs +81 -66
  72. package/api/upgrade/provider-agreement.test.mjs +152 -0
  73. package/api/upgrade/run/run.mjs +1 -0
  74. package/api/upgrade/upgrade.doc.mjs +1 -0
  75. package/assets/codemods/integration-discovery.mjs +8 -2
  76. package/assets/codemods/integration-discovery.test.mjs +15 -0
  77. package/assets/codemods/runner.mjs +115 -5
  78. package/assets/codemods/transforms/next/__tests__/migrate-theme-catalog-to-descriptors.test.mjs +216 -0
  79. package/assets/codemods/transforms/next/index.mjs +11 -1
  80. package/assets/codemods/transforms/next/migrate-theme-catalog-to-descriptors.mjs +141 -0
  81. package/assets/docs/cli-integrations.doc.mjs +17 -30
  82. package/assets/docs/cli.doc.mjs +15 -0
  83. package/assets/docs/theme.doc.mjs +1 -1
  84. package/assets/templates/themes/butter/butterTheme.doc.mjs +11 -0
  85. package/assets/templates/themes/chocolate/chocolateTheme.doc.mjs +11 -0
  86. package/assets/templates/themes/gothic/gothicTheme.doc.mjs +11 -0
  87. package/assets/templates/themes/matcha/matchaTheme.doc.mjs +11 -0
  88. package/assets/templates/themes/neutral/neutralTheme.doc.mjs +11 -0
  89. package/assets/templates/themes/stone/stoneTheme.doc.mjs +11 -0
  90. package/assets/templates/themes/y2k/y2kTheme.doc.mjs +11 -0
  91. package/authoring/codemod/codemod.doc.mjs +1 -1
  92. package/authoring/config/config.doc.mjs +1 -1
  93. package/authoring/debug/debug.doc.d.mts +11 -0
  94. package/authoring/debug/debug.doc.mjs +182 -0
  95. package/authoring/doctypes/base/graph-fields.doc.mjs +1 -1
  96. package/authoring/doctypes/command/command.doc.mjs +1 -1
  97. package/authoring/doctypes/command/type.ts +1 -1
  98. package/authoring/doctypes/component/type.ts +2 -2
  99. package/authoring/doctypes/doctypes-new.test.mjs +48 -6
  100. package/authoring/doctypes/enum/enum.doc.mjs +1 -1
  101. package/authoring/doctypes/enum/type.ts +1 -1
  102. package/authoring/doctypes/function/function.doc.mjs +1 -1
  103. package/authoring/doctypes/function/type.ts +1 -1
  104. package/authoring/doctypes/hook/type.ts +2 -2
  105. package/authoring/doctypes/load-contract.test.mjs +2 -1
  106. package/authoring/doctypes/parse.d.mts +4 -2
  107. package/authoring/doctypes/parse.mjs +7 -3
  108. package/authoring/doctypes/reference/reference.doc.mjs +1 -1
  109. package/authoring/doctypes/reference/type.ts +2 -2
  110. package/authoring/doctypes/schema/schema.doc.mjs +1 -1
  111. package/authoring/doctypes/schema/type.ts +1 -2
  112. package/authoring/doctypes/template/template.doc.mjs +3 -3
  113. package/authoring/doctypes/theme/parse.d.mts +35 -0
  114. package/authoring/doctypes/theme/parse.mjs +76 -0
  115. package/authoring/doctypes/theme/theme.doc.d.mts +9 -0
  116. package/authoring/doctypes/theme/theme.doc.mjs +79 -0
  117. package/authoring/doctypes/theme/type.ts +42 -0
  118. package/authoring/doctypes/types.ts +2 -1
  119. package/authoring/gap-report/gap-report.doc.d.mts +12 -0
  120. package/authoring/gap-report/gap-report.doc.mjs +183 -0
  121. package/authoring/index.d.mts +1 -0
  122. package/authoring/index.d.ts +6 -4
  123. package/authoring/index.mjs +2 -1
  124. package/authoring/integration/integration.doc.mjs +2 -2
  125. package/authoring/integration/type.ts +3 -9
  126. package/clients/cli/__tests__/cliManifest.test.ts +27 -29
  127. package/clients/cli/commands/blog.doc.mjs +1 -1
  128. package/clients/cli/commands/build.doc.mjs +1 -1
  129. package/clients/cli/commands/component.doc.mjs +1 -1
  130. package/clients/cli/commands/discover.doc.mjs +1 -1
  131. package/clients/cli/commands/docs.doc.mjs +1 -1
  132. package/clients/cli/commands/doctor-integration-components.doc.mjs +1 -1
  133. package/clients/cli/commands/doctor-integration-docs.doc.mjs +1 -1
  134. package/clients/cli/commands/doctor-integration-templates.doc.mjs +1 -1
  135. package/clients/cli/commands/doctor-integration-validate.doc.mjs +1 -1
  136. package/clients/cli/commands/doctor-integration.doc.mjs +1 -1
  137. package/clients/cli/commands/doctor-integration.test.mjs +15 -7
  138. package/clients/cli/commands/doctor.doc.mjs +1 -1
  139. package/clients/cli/commands/gap-report.doc.mjs +1 -1
  140. package/clients/cli/commands/hook.doc.mjs +1 -1
  141. package/clients/cli/commands/init.doc.mjs +1 -1
  142. package/clients/cli/commands/integration-add.controls.test.mjs +1 -1
  143. package/clients/cli/commands/integration-add.doc.mjs +1 -1
  144. package/clients/cli/commands/integration-pack.doc.mjs +1 -1
  145. package/clients/cli/commands/integration-real-world.test.mjs +3 -9
  146. package/clients/cli/commands/integration.doc.mjs +1 -1
  147. package/clients/cli/commands/layout-check.doc.mjs +1 -1
  148. package/clients/cli/commands/layout-expand.doc.mjs +1 -1
  149. package/clients/cli/commands/layout-grammar.doc.mjs +1 -1
  150. package/clients/cli/commands/layout.doc.mjs +1 -1
  151. package/clients/cli/commands/manifest.doc.mjs +1 -1
  152. package/clients/cli/commands/search.doc.mjs +1 -1
  153. package/clients/cli/commands/swizzle.doc.mjs +1 -1
  154. package/clients/cli/commands/template.doc.mjs +1 -1
  155. package/clients/cli/commands/theme-add.doc.mjs +2 -2
  156. package/clients/cli/commands/theme-build.doc.mjs +1 -1
  157. package/clients/cli/commands/theme-list.doc.mjs +2 -2
  158. package/clients/cli/commands/theme-palette-generate.doc.mjs +3 -3
  159. package/clients/cli/commands/theme-palette.doc.mjs +1 -1
  160. package/clients/cli/commands/theme-targets.doc.mjs +1 -1
  161. package/clients/cli/commands/theme-template.doc.mjs +1 -1
  162. package/clients/cli/commands/theme.doc.mjs +1 -1
  163. package/clients/cli/commands/upgrade.doc.mjs +1 -1
  164. package/clients/cli/lib/hook-format.mjs +14 -5
  165. package/foundation/config/project-themes.test.mjs +11 -19
  166. package/foundation/config/project.d.mts +8 -0
  167. package/foundation/config/project.mjs +50 -21
  168. package/foundation/config/project.test.mjs +3 -16
  169. package/foundation/discovery/authoring-self-docs.d.mts +6 -0
  170. package/foundation/discovery/authoring-self-docs.mjs +17 -7
  171. package/foundation/discovery/authoring-surface.d.mts +74 -0
  172. package/foundation/discovery/authoring-surface.mjs +525 -0
  173. package/foundation/discovery/authoring-surface.test.mjs +392 -0
  174. package/foundation/discovery/cli-self-docs.d.mts +113 -0
  175. package/foundation/discovery/cli-self-docs.mjs +514 -0
  176. package/foundation/discovery/cli-self-docs.test.mjs +437 -0
  177. package/foundation/discovery/component-loader.d.mts +35 -38
  178. package/foundation/discovery/component-loader.mjs +53 -222
  179. package/foundation/discovery/docs-discovery.d.mts +3 -2
  180. package/foundation/discovery/docs-discovery.mjs +8 -12
  181. package/foundation/discovery/docs-discovery.test.mjs +8 -5
  182. package/foundation/discovery/template-adapter.d.mts +7 -0
  183. package/foundation/discovery/template-adapter.mjs +27 -40
  184. package/foundation/discovery/template-adapter.test.mjs +15 -0
  185. package/foundation/discovery/theme-discovery.d.mts +67 -7
  186. package/foundation/discovery/theme-discovery.mjs +916 -186
  187. package/foundation/discovery/theme-discovery.test.mjs +613 -219
  188. package/foundation/doc-compiler/bundle.d.mts +47 -0
  189. package/foundation/doc-compiler/bundle.mjs +218 -0
  190. package/foundation/doc-compiler/bundle.test.mjs +255 -0
  191. package/foundation/doc-compiler/compile.d.mts +200 -0
  192. package/foundation/doc-compiler/compile.mjs +254 -5
  193. package/foundation/doc-compiler/diagnostics.d.mts +126 -0
  194. package/foundation/doc-compiler/diagnostics.mjs +277 -0
  195. package/foundation/doc-compiler/doc-compiler.test.mjs +28 -1
  196. package/foundation/doc-compiler/doc-loads.test.mjs +1642 -0
  197. package/foundation/doc-compiler/import.d.mts +24 -0
  198. package/foundation/doc-compiler/import.mjs +59 -0
  199. package/foundation/doc-compiler/inputs.d.mts +102 -0
  200. package/foundation/doc-compiler/inputs.mjs +286 -0
  201. package/foundation/doc-compiler/inputs.test.mjs +299 -0
  202. package/foundation/doc-compiler/ir.d.mts +13 -0
  203. package/foundation/doc-compiler/ir.mjs +189 -12
  204. package/foundation/doc-compiler/lower-doc.test.mjs +492 -0
  205. package/foundation/doc-compiler/overlays.d.mts +37 -0
  206. package/foundation/doc-compiler/overlays.mjs +206 -0
  207. package/foundation/doc-compiler/parse-readable.d.mts +9 -0
  208. package/foundation/doc-compiler/parse-readable.mjs +29 -0
  209. package/foundation/doc-compiler/read.d.mts +126 -0
  210. package/foundation/doc-compiler/read.mjs +320 -0
  211. package/foundation/doc-compiler/read.test.mjs +313 -0
  212. package/foundation/doc-compiler/source.d.mts +33 -0
  213. package/foundation/doc-compiler/source.mjs +128 -0
  214. package/foundation/integrations/autolink.d.mts +58 -1
  215. package/foundation/integrations/autolink.mjs +143 -57
  216. package/foundation/integrations/contribution-fixes.d.mts +145 -0
  217. package/foundation/integrations/contribution-fixes.mjs +1284 -0
  218. package/foundation/integrations/contribution-inventory.d.mts +1 -1
  219. package/foundation/integrations/contribution-inventory.mjs +17 -20
  220. package/foundation/integrations/contribution-inventory.test.mjs +67 -27
  221. package/foundation/integrations/integrations.d.mts +3 -0
  222. package/foundation/integrations/integrations.mjs +11 -97
  223. package/foundation/integrations/provider-ledger.test.mjs +275 -0
  224. package/foundation/integrations/provider-resolution.d.mts +152 -0
  225. package/foundation/integrations/provider-resolution.mjs +576 -0
  226. package/foundation/integrations/provider-resolution.test.mjs +369 -0
  227. package/foundation/integrations/theme-descriptor.d.mts +8 -0
  228. package/foundation/integrations/theme-descriptor.mjs +44 -0
  229. package/foundation/integrations/validate-contributions.mjs +104 -10
  230. package/foundation/response/error-codes.doc.mjs +2 -2
  231. package/foundation/response/error-codes.mjs +1 -1
  232. package/foundation/response/response-types.doc.mjs +1 -1
  233. package/foundation/response/response.doc.mjs +1 -1
  234. package/foundation/text/string-utils.mjs +22 -10
  235. package/package.json +9 -9
  236. package/assets/templates/themes/manifest.json +0 -95
@@ -77,6 +77,7 @@ describe('integrationAdd component', () => {
77
77
  path.join(tmpDir, 'components/MyWidget.doc.mjs'),
78
78
  'utf-8',
79
79
  );
80
+ expect(doc).toContain("@astryxdesign/cli/authoring').ComponentDoc");
80
81
  expect(doc).toContain("type: 'component'");
81
82
  expect(doc).toContain("name: 'MyWidget'");
82
83
  expect(doc).toContain('props: []');
@@ -304,6 +305,7 @@ describe('integrationAdd doc', () => {
304
305
  path.join(tmpDir, 'docs/my-guide.doc.mjs'),
305
306
  'utf-8',
306
307
  );
308
+ expect(doc).toContain("@astryxdesign/cli/authoring').ReferenceDoc");
307
309
  expect(doc).toContain("type: 'generic'");
308
310
  expect(doc).toContain("name: 'my-guide'");
309
311
 
@@ -390,13 +392,14 @@ describe('integrationAdd template', () => {
390
392
  expect(result.data.kind).toBe('template');
391
393
  expect(result.data.name).toBe('my-widget');
392
394
  expect(result.data.root).toEqual({path: './templates', created: true});
393
- expect(result.data.files).toContain('templates/my-widget.template.mjs');
395
+ expect(result.data.files).toContain('templates/my-widget.doc.mjs');
394
396
  expect(result.data.files).toContain('templates/my-widget.tsx');
395
397
 
396
398
  const spec = fs.readFileSync(
397
- path.join(tmpDir, 'templates/my-widget.template.mjs'),
399
+ path.join(tmpDir, 'templates/my-widget.doc.mjs'),
398
400
  'utf-8',
399
401
  );
402
+ expect(spec).toContain("@astryxdesign/cli/authoring').TemplateDoc");
400
403
  expect(spec).toContain("type: 'page'");
401
404
  expect(spec).toContain("name: 'my-widget'");
402
405
 
@@ -432,10 +435,11 @@ describe('integrationAdd template', () => {
432
435
  templateType: 'block',
433
436
  });
434
437
  const spec = fs.readFileSync(
435
- path.join(tmpDir, 'templates/my-card.template.mjs'),
438
+ path.join(tmpDir, 'templates/my-card.doc.mjs'),
436
439
  'utf-8',
437
440
  );
438
441
  expect(spec).toContain("type: 'block'");
442
+ expect(spec).toContain('aspectRatio: 1');
439
443
  });
440
444
 
441
445
  it('rejects an invalid template type', async () => {
@@ -676,6 +680,35 @@ describe('custom root preservation', () => {
676
680
  });
677
681
  });
678
682
 
683
+ describe('typed descriptor conformance', () => {
684
+ it('emits a strongly typed .doc.mjs for every canonical item writer', async () => {
685
+ setup();
686
+ await integrationAdd('component', 'MyWidget', {cwd: tmpDir});
687
+ await integrationAdd('doc', 'my-guide', {cwd: tmpDir});
688
+ await integrationAdd('template', 'my-page', {cwd: tmpDir});
689
+ await integrationAdd('theme', 'ocean', {cwd: tmpDir});
690
+
691
+ // Codemod and agent-doc keep their released formats for compatibility;
692
+ // no new kind may copy those exceptions.
693
+ for (const [relativePath, type] of [
694
+ ['components/MyWidget.doc.mjs', 'ComponentDoc'],
695
+ ['docs/my-guide.doc.mjs', 'ReferenceDoc'],
696
+ ['templates/my-page.doc.mjs', 'TemplateDoc'],
697
+ ['themes/ocean/oceanTheme.doc.mjs', 'ThemeDoc'],
698
+ ]) {
699
+ const source = fs.readFileSync(path.join(tmpDir, relativePath), 'utf-8');
700
+ expect(source).toContain(`@astryxdesign/cli/authoring').${type}`);
701
+ expect(source).toContain('export default {');
702
+ }
703
+ expect(
704
+ fs.existsSync(path.join(tmpDir, 'templates/my-page.template.mjs')),
705
+ ).toBe(false);
706
+ expect(fs.existsSync(path.join(tmpDir, 'themes/manifest.json'))).toBe(
707
+ false,
708
+ );
709
+ });
710
+ });
711
+
679
712
  describe('public per-kind APIs', () => {
680
713
  it('exposes narrow functions for every non-theme contribution kind', async () => {
681
714
  setup();
@@ -711,6 +744,7 @@ describe('public dispatcher', () => {
711
744
  const result = await integrationAdd('theme', 'ocean', {cwd: tmpDir});
712
745
  expect(result.data).toMatchObject({kind: 'theme', name: 'ocean'});
713
746
  expect(result.data.files).toContain('themes/ocean/oceanTheme.ts');
747
+ expect(result.data.files).toContain('themes/ocean/oceanTheme.doc.mjs');
714
748
  });
715
749
 
716
750
  it('refuses a kind-specific option on the wrong kind', async () => {
@@ -1,9 +1,9 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
3
  /**
4
- * @file `astryx integration add theme` — scaffold one valid source theme into
5
- * an integration package, update its theme catalog, and declare the root on
6
- * first use.
4
+ * @file `astryx integration add theme` — scaffold one strongly typed,
5
+ * same-stem source/descriptor pair into an integration package and declare the
6
+ * themes root on first use.
7
7
  */
8
8
 
9
9
  import * as fs from 'node:fs';
@@ -15,16 +15,14 @@ import {
15
15
  PathSafetyError,
16
16
  sanitizeName,
17
17
  } from '../../foundation/fs/path-safety.mjs';
18
- import {
19
- discoverThemeCatalog,
20
- THEME_MANIFEST_BASENAME,
21
- } from '../../foundation/discovery/theme-discovery.mjs';
18
+ import {discoverThemeDirectory} from '../../foundation/discovery/theme-discovery.mjs';
22
19
  import {
23
20
  findLocalIntegrationManifestOrNull,
24
21
  IntegrationRootConflictError,
25
22
  patchIntegrationRoot,
26
23
  } from '../../foundation/integrations/manifest-writer.mjs';
27
24
  import {loadManifestObject} from '../../foundation/integrations/integrations.mjs';
25
+ import {themeDescriptorSource} from '../../foundation/integrations/theme-descriptor.mjs';
28
26
  import {assertContributionVisible} from '../../foundation/integrations/contribution-inventory.mjs';
29
27
  import {
30
28
  applyWrites,
@@ -70,6 +68,7 @@ function themeIdentity(slug) {
70
68
  displayName,
71
69
  exportName: `${identifier}Theme`,
72
70
  entry: `${identifier}Theme.ts`,
71
+ descriptor: `${identifier}Theme.doc.mjs`,
73
72
  };
74
73
  }
75
74
 
@@ -78,9 +77,20 @@ function themeSource(identity) {
78
77
  return `import {defineTheme} from '@astryxdesign/core/theme';\n\nexport const ${identity.exportName} = defineTheme({\n name: '${identity.slug}',\n});\n`;
79
78
  }
80
79
 
80
+ /** @param {{slug: string, displayName: string}} identity */
81
+ function themeDescriptor(identity) {
82
+ return themeDescriptorSource({
83
+ type: 'theme',
84
+ name: identity.slug,
85
+ displayName: identity.displayName,
86
+ description: `${identity.displayName} theme.`,
87
+ maintained: true,
88
+ });
89
+ }
90
+
81
91
  /**
82
- * Read the bytes back through the same integration-theme discovery seam Project
83
- * uses. A write is not successful until the requested slug resolves.
92
+ * Read bytes back through the same discovery seam Project uses. A write is not
93
+ * successful until the requested slug resolves.
84
94
  * @param {string} packageDir
85
95
  * @param {string} manifestFile
86
96
  * @param {string} owner
@@ -176,6 +186,9 @@ export async function integrationAddTheme(name, options = {}) {
176
186
  dryRun: true,
177
187
  createIfMissing: true,
178
188
  });
189
+ if (fs.existsSync(root)) {
190
+ discoverThemeDirectory(root, owner);
191
+ }
179
192
  } catch (error) {
180
193
  if (error instanceof IntegrationRootConflictError) {
181
194
  throw new AstryxError(
@@ -197,72 +210,29 @@ export async function integrationAddTheme(name, options = {}) {
197
210
  const sourceFile = assertWithin(identity.entry, themeDir, {
198
211
  label: 'theme source file',
199
212
  });
200
- if (fs.existsSync(sourceFile)) {
201
- throw new AstryxError(
202
- `Refusing to overwrite existing file ${projectPath(path.relative(packageDir, sourceFile))}.`,
203
- undefined,
204
- ERROR_CODES.ERR_FILE_EXISTS,
205
- );
206
- }
207
-
208
- const catalogFile = assertWithin(THEME_MANIFEST_BASENAME, root, {
209
- label: 'theme catalog manifest',
213
+ const descriptorFile = assertWithin(identity.descriptor, themeDir, {
214
+ label: 'theme descriptor file',
210
215
  });
211
- /** @type {{version: 1, themes: Array<Record<string, unknown>>, [key: string]: unknown}} */
212
- let catalog = {version: 1, themes: []};
213
- /** @type {Buffer|null} */
214
- let catalogOriginal = null;
215
- if (fs.existsSync(catalogFile)) {
216
- try {
217
- catalogOriginal = fs.readFileSync(catalogFile);
218
- catalog = JSON.parse(catalogOriginal.toString('utf-8'));
219
- // The discovery seam is the schema/source-of-truth validator. The write
220
- // plan carries catalogOriginal, so any concurrent change between this read
221
- // and publish is refused rather than overwritten with this snapshot.
222
- discoverThemeCatalog(root, owner);
223
- } catch (error) {
216
+ // An existing folder is filled; only a file this would write is refused.
217
+ for (const file of [sourceFile, descriptorFile]) {
218
+ if (fs.existsSync(file)) {
224
219
  throw new AstryxError(
225
- error instanceof Error ? error.message : String(error),
220
+ `Refusing to overwrite existing file ${projectPath(path.relative(packageDir, file))}.`,
226
221
  undefined,
227
- ERROR_CODES.ERR_THEME_INVALID,
222
+ ERROR_CODES.ERR_FILE_EXISTS,
228
223
  );
229
224
  }
230
225
  }
231
- if (
232
- catalog.themes.some(
233
- theme =>
234
- typeof theme.slug === 'string' &&
235
- theme.slug.toLowerCase() === identity.slug.toLowerCase(),
236
- )
237
- ) {
238
- throw new AstryxError(
239
- `Theme "${identity.slug}" already exists in ${projectPath(path.relative(packageDir, catalogFile))}.`,
240
- undefined,
241
- ERROR_CODES.ERR_FILE_EXISTS,
242
- );
243
- }
244
-
245
- catalog.themes.push({
246
- slug: identity.slug,
247
- displayName: identity.displayName,
248
- description: `${identity.displayName} theme.`,
249
- maintained: true,
250
- entry: identity.entry,
251
- exportName: identity.exportName,
252
- files: [identity.entry],
253
- });
254
226
 
255
227
  /** @type {import('./add-helpers.mjs').WritePlan[]} */
256
- const plans = [];
257
- plans.push(
228
+ const plans = [
258
229
  {path: sourceFile, contents: themeSource(identity), createOnly: true},
259
230
  {
260
- path: catalogFile,
261
- contents: `${JSON.stringify(catalog, null, 2)}\n`,
262
- createOnly: catalogOriginal == null,
263
- expectedOriginal: catalogOriginal ?? undefined,
231
+ path: descriptorFile,
232
+ contents: themeDescriptor(identity),
233
+ createOnly: true,
264
234
  },
265
- );
235
+ ];
266
236
  const packageUpdate = packageJsonUpdate(
267
237
  packageFile,
268
238
  rootPath,
@@ -32,7 +32,7 @@ afterEach(() => {
32
32
  });
33
33
 
34
34
  describe('integrationAddTheme', () => {
35
- it('writes a valid source theme, catalog, root, package files entry, and receipt', async () => {
35
+ it('writes a typed same-stem descriptor and source with no central catalog', async () => {
36
36
  setup();
37
37
  const result = await integrationAddTheme('ocean', {cwd: tmpDir});
38
38
 
@@ -45,7 +45,7 @@ describe('integrationAddTheme', () => {
45
45
  manifest: 'astryx.integration.mjs',
46
46
  files: [
47
47
  'themes/ocean/oceanTheme.ts',
48
- 'themes/manifest.json',
48
+ 'themes/ocean/oceanTheme.doc.mjs',
49
49
  'package.json',
50
50
  'astryx.integration.mjs',
51
51
  ],
@@ -56,21 +56,17 @@ describe('integrationAddTheme', () => {
56
56
  expect(
57
57
  fs.readFileSync(path.join(tmpDir, 'themes/ocean/oceanTheme.ts'), 'utf-8'),
58
58
  ).toContain('export const oceanTheme = defineTheme');
59
- expect(
60
- JSON.parse(
61
- fs.readFileSync(path.join(tmpDir, 'themes/manifest.json'), 'utf-8'),
62
- ),
63
- ).toMatchObject({
64
- version: 1,
65
- themes: [
66
- {
67
- slug: 'ocean',
68
- entry: 'oceanTheme.ts',
69
- exportName: 'oceanTheme',
70
- files: ['oceanTheme.ts'],
71
- },
72
- ],
73
- });
59
+
60
+ const descriptor = fs.readFileSync(
61
+ path.join(tmpDir, 'themes/ocean/oceanTheme.doc.mjs'),
62
+ 'utf-8',
63
+ );
64
+ expect(descriptor).toContain("@astryxdesign/cli/authoring').ThemeDoc");
65
+ expect(descriptor).toContain("type: 'theme'");
66
+ expect(descriptor).toContain("name: 'ocean'");
67
+ expect(fs.existsSync(path.join(tmpDir, 'themes/manifest.json'))).toBe(
68
+ false,
69
+ );
74
70
  expect(
75
71
  fs.readFileSync(path.join(tmpDir, 'astryx.integration.mjs'), 'utf-8'),
76
72
  ).toContain("themes: './themes'");
@@ -96,6 +92,7 @@ describe('integrationAddTheme', () => {
96
92
  expect(result.data.dryRun).toBe(true);
97
93
  expect(result.data.root).toEqual({path: './themes', created: true});
98
94
  expect(result.data.files).toContain('themes/ocean/oceanTheme.ts');
95
+ expect(result.data.files).toContain('themes/ocean/oceanTheme.doc.mjs');
99
96
  expect(fs.existsSync(path.join(tmpDir, 'themes'))).toBe(false);
100
97
  expect(
101
98
  fs.readFileSync(path.join(tmpDir, 'astryx.integration.mjs'), 'utf-8'),
@@ -125,22 +122,69 @@ describe('integrationAddTheme', () => {
125
122
  expect(
126
123
  fs.existsSync(path.join(tmpDir, 'src/themes/ocean/oceanTheme.ts')),
127
124
  ).toBe(true);
125
+ expect(
126
+ fs.existsSync(path.join(tmpDir, 'src/themes/ocean/oceanTheme.doc.mjs')),
127
+ ).toBe(true);
128
128
  });
129
129
 
130
- it('refuses an invalid existing catalog without changing its bytes', async () => {
130
+ it('keeps each theme to its own folder: a second theme touches no shared file', async () => {
131
+ setup({manifest: "export default {themes: './themes'};\n"});
132
+ await integrationAddTheme('ocean', {cwd: tmpDir});
133
+ const shared = ['package.json', 'astryx.integration.mjs'].map(file =>
134
+ fs.readFileSync(path.join(tmpDir, file), 'utf-8'),
135
+ );
136
+
137
+ const result = await integrationAddTheme('reef', {cwd: tmpDir});
138
+
139
+ expect(result.data.files).toEqual([
140
+ 'themes/reef/reefTheme.ts',
141
+ 'themes/reef/reefTheme.doc.mjs',
142
+ ]);
143
+ expect(fs.readdirSync(path.join(tmpDir, 'themes')).sort()).toEqual([
144
+ 'ocean',
145
+ 'reef',
146
+ ]);
147
+ expect(
148
+ ['package.json', 'astryx.integration.mjs'].map(file =>
149
+ fs.readFileSync(path.join(tmpDir, file), 'utf-8'),
150
+ ),
151
+ ).toEqual(shared);
152
+ });
153
+
154
+ it('refuses an obsolete central catalog without changing its bytes', async () => {
131
155
  setup();
132
156
  fs.mkdirSync(path.join(tmpDir, 'themes'));
133
157
  const catalog = path.join(tmpDir, 'themes', 'manifest.json');
134
- fs.writeFileSync(catalog, '{not-json\n');
158
+ fs.writeFileSync(catalog, '{"version":1}\n');
135
159
 
136
160
  await expect(
137
161
  integrationAddTheme('ocean', {cwd: tmpDir}),
138
162
  ).rejects.toMatchObject({code: 'ERR_THEME_INVALID'});
139
- expect(fs.readFileSync(catalog, 'utf-8')).toBe('{not-json\n');
163
+ expect(fs.readFileSync(catalog, 'utf-8')).toBe('{"version":1}\n');
140
164
  expect(fs.existsSync(path.join(tmpDir, 'themes', 'ocean'))).toBe(false);
141
165
  });
142
166
 
143
- it('refuses duplicate themes and existing source files', async () => {
167
+ it('refuses an invalid existing descriptor without changing it', async () => {
168
+ setup();
169
+ const existing = path.join(tmpDir, 'themes', 'forest');
170
+ fs.mkdirSync(existing, {recursive: true});
171
+ const descriptor = path.join(existing, 'forestTheme.doc.mjs');
172
+ fs.writeFileSync(descriptor, 'export default {type: "theme"};\n');
173
+ fs.writeFileSync(
174
+ path.join(existing, 'forestTheme.ts'),
175
+ 'export const forestTheme = {};\n',
176
+ );
177
+
178
+ await expect(
179
+ integrationAddTheme('ocean', {cwd: tmpDir}),
180
+ ).rejects.toMatchObject({code: 'ERR_THEME_INVALID'});
181
+ expect(fs.readFileSync(descriptor, 'utf-8')).toBe(
182
+ 'export default {type: "theme"};\n',
183
+ );
184
+ expect(fs.existsSync(path.join(tmpDir, 'themes', 'ocean'))).toBe(false);
185
+ });
186
+
187
+ it('refuses a duplicate valid theme directory', async () => {
144
188
  setup();
145
189
  await integrationAddTheme('ocean', {cwd: tmpDir});
146
190
  await expect(
@@ -148,6 +192,46 @@ describe('integrationAddTheme', () => {
148
192
  ).rejects.toMatchObject({code: 'ERR_FILE_EXISTS'});
149
193
  });
150
194
 
195
+ it('refuses an existing invalid theme directory', async () => {
196
+ setup();
197
+ fs.mkdirSync(path.join(tmpDir, 'themes', 'ocean'), {recursive: true});
198
+ fs.writeFileSync(
199
+ path.join(tmpDir, 'themes', 'ocean', 'oceanTheme.ts'),
200
+ 'export const oceanTheme = {};\n',
201
+ );
202
+ await expect(
203
+ integrationAddTheme('ocean', {cwd: tmpDir}),
204
+ ).rejects.toMatchObject({code: 'ERR_THEME_INVALID'});
205
+ });
206
+
207
+ it.each([
208
+ ['an empty folder', []],
209
+ ['a folder holding .gitkeep', ['.gitkeep']],
210
+ ['a folder holding NOTES.md', ['NOTES.md']],
211
+ ])('fills %s', async (_, files) => {
212
+ setup();
213
+ const dir = path.join(tmpDir, 'themes', 'ocean');
214
+ fs.mkdirSync(dir, {recursive: true});
215
+ for (const file of files) fs.writeFileSync(path.join(dir, file), 'x\n');
216
+ const result = await integrationAddTheme('ocean', {cwd: tmpDir});
217
+ expect(result.data.written).toBe(true);
218
+ expect(fs.readdirSync(dir).sort()).toEqual(
219
+ [...files, 'oceanTheme.doc.mjs', 'oceanTheme.ts'].sort(),
220
+ );
221
+ });
222
+
223
+ it('refuses to overwrite a file it would write', async () => {
224
+ setup();
225
+ await integrationAddTheme('ocean', {cwd: tmpDir});
226
+ await expect(
227
+ integrationAddTheme('ocean', {cwd: tmpDir}),
228
+ ).rejects.toMatchObject({
229
+ code: 'ERR_FILE_EXISTS',
230
+ message:
231
+ 'Refusing to overwrite existing file themes/ocean/oceanTheme.ts.',
232
+ });
233
+ });
234
+
151
235
  it.each(['../ocean', 'Ocean', 'ocean theme', '.ocean'])(
152
236
  'refuses unsafe or noncanonical name %s',
153
237
  async name => {
@@ -33,7 +33,7 @@ function writeIntegration({id, name, type = 'block', root = tmpDir}) {
33
33
  const stem = path.join(root, 'templates', id);
34
34
  fs.mkdirSync(path.dirname(stem), {recursive: true});
35
35
  fs.writeFileSync(
36
- `${stem}.template.mjs`,
36
+ `${stem}.doc.mjs`,
37
37
  `export default {type: '${type}', name: ${JSON.stringify(name)}, description: 'fixture'};\n`,
38
38
  );
39
39
  fs.writeFileSync(
@@ -176,7 +176,7 @@ describe('integrationTemplateConflicts', () => {
176
176
  );
177
177
  fs.mkdirSync(path.join(tmpDir, 'templates'), {recursive: true});
178
178
  fs.writeFileSync(
179
- path.join(tmpDir, 'templates', 'broken.template.mjs'),
179
+ path.join(tmpDir, 'templates', 'broken.doc.mjs'),
180
180
  `throw new Error('broken fixture');\n`,
181
181
  );
182
182
  fs.writeFileSync(
@@ -221,7 +221,9 @@ describe('integrationComponentConflicts', () => {
221
221
  expect(core).toBeDefined();
222
222
  writeComponentIntegration(core.name);
223
223
 
224
- const result = await integrationComponentConflicts(undefined, {cwd: tmpDir});
224
+ const result = await integrationComponentConflicts(undefined, {
225
+ cwd: tmpDir,
226
+ });
225
227
 
226
228
  expect(result.type).toBe('integration.component-conflicts');
227
229
  expect(result.data.issues).toEqual([]);
@@ -240,7 +242,9 @@ describe('integrationComponentConflicts', () => {
240
242
  it('does not flag an integration-only component name', async () => {
241
243
  writeComponentIntegration('AcmeOnlyWidget');
242
244
 
243
- const result = await integrationComponentConflicts(undefined, {cwd: tmpDir});
245
+ const result = await integrationComponentConflicts(undefined, {
246
+ cwd: tmpDir,
247
+ });
244
248
 
245
249
  expect(result.data.conflicts).toEqual([]);
246
250
  expect(result.data.issues).toEqual([]);
@@ -51,7 +51,7 @@ const ESBUILD_BIN = resolveBin('esbuild/bin/esbuild');
51
51
  let tmpDir;
52
52
 
53
53
  /**
54
- * Build an @acme/widgets fixture: a block template (`.template.ts` doc +
54
+ * Build an @acme/widgets fixture: a block template (released `.template.ts` compatibility descriptor +
55
55
  * same-stem `.tsx` source) under `templates/`, plus an astryx.integration
56
56
  * manifest, plus a caller-supplied `exports` map.
57
57
  * @param {Record<string, string> | undefined} exportsMap
@@ -71,7 +71,7 @@ function makeWidgets(exportsMap) {
71
71
  path.join(pkgDir, 'astryx.integration.mjs'),
72
72
  `export default { templates: './templates' };\n`,
73
73
  );
74
- // The template-spec doc (canonical `.template.*` family).
74
+ // The released `.template.*` compatibility form remains resolvable.
75
75
  fs.writeFileSync(
76
76
  path.join(blockDir, 'GaugeShowcase.template.ts'),
77
77
  `export default { name: 'Gauge showcase', description: 'A gauge.' };\n`,
@@ -127,10 +127,14 @@ function writeTsconfig() {
127
127
  /** @returns {{ok: boolean, out: string}} */
128
128
  function runTsc() {
129
129
  try {
130
- execFileSync(process.execPath, [TSC_BIN, '--noEmit', '-p', 'tsconfig.json'], {
131
- cwd: tmpDir,
132
- stdio: 'pipe',
133
- });
130
+ execFileSync(
131
+ process.execPath,
132
+ [TSC_BIN, '--noEmit', '-p', 'tsconfig.json'],
133
+ {
134
+ cwd: tmpDir,
135
+ stdio: 'pipe',
136
+ },
137
+ );
134
138
  return {ok: true, out: ''};
135
139
  } catch (e) {
136
140
  return {ok: false, out: `${e.stdout ?? ''}${e.stderr ?? ''}`};
@@ -5,6 +5,7 @@ export const doc = {
5
5
  type: 'function',
6
6
  kind: 'api',
7
7
  name: 'integrationAdd',
8
+ namespace: 'cli/api',
8
9
  displayName: 'integrationAdd()',
9
10
  summary: 'Add one working contribution to an integration package.',
10
11
  description:
@@ -5,6 +5,7 @@ export const doc = {
5
5
  type: 'function',
6
6
  kind: 'api',
7
7
  name: 'integrationAddAgentDoc',
8
+ namespace: 'cli/api',
8
9
  displayName: 'integrationAddAgentDoc()',
9
10
  summary: 'Append one validated agent-guidance line.',
10
11
  description:
@@ -5,6 +5,7 @@ export const doc = {
5
5
  type: 'function',
6
6
  kind: 'api',
7
7
  name: 'integrationAddCodemod',
8
+ namespace: 'cli/api',
8
9
  displayName: 'integrationAddCodemod()',
9
10
  summary: 'Add a safe identity codemod at an exact target version.',
10
11
  description:
@@ -5,6 +5,7 @@ export const doc = {
5
5
  type: 'function',
6
6
  kind: 'api',
7
7
  name: 'integrationAddComponent',
8
+ namespace: 'cli/api',
8
9
  displayName: 'integrationAddComponent()',
9
10
  summary: 'Add a complete component contribution.',
10
11
  description:
@@ -5,6 +5,7 @@ export const doc = {
5
5
  type: 'function',
6
6
  kind: 'api',
7
7
  name: 'integrationAddDoc',
8
+ namespace: 'cli/api',
8
9
  displayName: 'integrationAddDoc()',
9
10
  summary: 'Add a complete reference-doc contribution.',
10
11
  description:
@@ -5,6 +5,7 @@ export const doc = {
5
5
  type: 'function',
6
6
  kind: 'api',
7
7
  name: 'integrationAddTemplate',
8
+ namespace: 'cli/api',
8
9
  displayName: 'integrationAddTemplate()',
9
10
  summary: 'Add a complete page or block template contribution.',
10
11
  description:
@@ -5,10 +5,11 @@ export const doc = {
5
5
  type: 'function',
6
6
  kind: 'api',
7
7
  name: 'integrationAddTheme',
8
+ namespace: 'cli/api',
8
9
  displayName: 'integrationAddTheme()',
9
10
  summary: 'Add a source theme to an integration package.',
10
11
  description:
11
- 'Creates an integration manifest when the package has none, writes a minimal named-export defineTheme source file, adds its entry to the root theme catalog, declares the optional themes root on first use, and includes the manifest and root in package.json files when the package already uses an allowlist. Every path is confined to the package and existing theme files are never overwritten.',
12
+ 'Creates an integration manifest when the package has none, writes a minimal named-export defineTheme source file plus its strongly typed same-stem ThemeDoc, declares the optional themes root on first use, and includes the manifest and root in package.json files when the package already uses an allowlist. Every path is confined to the package and existing theme directories are never overwritten.',
12
13
  importPath: '@astryxdesign/cli/api',
13
14
  signature:
14
15
  'integrationAddTheme(name: string, options?: IntegrationAddThemeOptions): Promise<IntegrationAddResponse>',
@@ -17,7 +18,7 @@ export const doc = {
17
18
  {
18
19
  name: 'name',
19
20
  type: 'string',
20
- description: 'Lowercase kebab-case theme name and catalog slug.',
21
+ description: 'Lowercase kebab-case theme name and directory slug.',
21
22
  required: true,
22
23
  },
23
24
  {
@@ -51,7 +52,7 @@ export const doc = {
51
52
  },
52
53
  {
53
54
  code: 'ERR_FILE_EXISTS',
54
- when: 'the theme slug or source file already exists',
55
+ when: 'the theme slug directory already exists',
55
56
  },
56
57
  {
57
58
  code: 'ERR_PATH_TRAVERSAL',
@@ -63,7 +64,7 @@ export const doc = {
63
64
  },
64
65
  {
65
66
  code: 'ERR_THEME_INVALID',
66
- when: 'the package, integration manifest, or theme catalog is invalid',
67
+ when: 'the package, integration manifest, or theme descriptor is invalid',
67
68
  },
68
69
  {
69
70
  code: 'ERR_WRITE_FAILED',
@@ -80,6 +81,6 @@ export const doc = {
80
81
  code: "await integrationAddTheme('ocean', {dryRun: true});",
81
82
  },
82
83
  ],
83
- command: 'integration add',
84
+ command: 'integration add theme',
84
85
  related: ['themeList', 'themeAdd', 'validateIntegration'],
85
86
  };
@@ -7,6 +7,7 @@ export const doc = {
7
7
  type: 'function',
8
8
  kind: 'api',
9
9
  name: 'integrationComponentConflicts',
10
+ namespace: 'cli/api',
10
11
  displayName: 'integrationComponentConflicts()',
11
12
  summary: 'Find integration component names that also exist in Core.',
12
13
  description:
@@ -7,6 +7,7 @@ export const doc = {
7
7
  type: 'function',
8
8
  kind: 'api',
9
9
  name: 'integrationDocConflicts',
10
+ namespace: 'cli/api',
10
11
  displayName: 'integrationDocConflicts()',
11
12
  summary: 'Classify integration doc overlaps with Core topics.',
12
13
  description:
@@ -5,6 +5,7 @@ export const doc = {
5
5
  type: 'function',
6
6
  kind: 'api',
7
7
  name: 'integrationPackCheck',
8
+ namespace: 'cli/api',
8
9
  displayName: 'integrationPackCheck()',
9
10
  summary: 'Prove an integration package survives npm packing.',
10
11
  description:
@@ -9,6 +9,7 @@ export const doc = {
9
9
  type: 'function',
10
10
  kind: 'api',
11
11
  name: 'integrationTemplateConflicts',
12
+ namespace: 'cli/api',
12
13
  displayName: 'integrationTemplateConflicts()',
13
14
  summary: 'Find integration template ids that also exist in Core.',
14
15
  description: