@astryxdesign/cli 0.1.2 → 0.1.3

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 (238) hide show
  1. package/CHANGELOG.md +64 -1
  2. package/README.md +49 -55
  3. package/docs/layout.doc.dense.mjs +90 -0
  4. package/docs/layout.doc.mjs +160 -0
  5. package/docs/principles.doc.dense.mjs +2 -2
  6. package/docs/principles.doc.mjs +4 -0
  7. package/docs/principles.doc.zh.mjs +2 -2
  8. package/docs/theme.doc.mjs +2 -2
  9. package/package.json +13 -1
  10. package/src/api/blog.mjs +192 -0
  11. package/src/api/blog.test.mjs +144 -0
  12. package/src/api/component.mjs +253 -12
  13. package/src/api/discover.mjs +14 -7
  14. package/src/api/doctor.mjs +10 -25
  15. package/src/api/index.mjs +1 -0
  16. package/src/api/layout.mjs +11 -10
  17. package/src/api/layout.test.mjs +4 -1
  18. package/src/api/template-integration.test.mjs +225 -0
  19. package/src/api/template.mjs +242 -84
  20. package/src/api/validate-integration.mjs +370 -0
  21. package/src/api/validate-integration.test.mjs +222 -0
  22. package/src/codemod.mjs +93 -0
  23. package/src/codemod.test.mjs +134 -0
  24. package/src/codemods/__tests__/registry.test.mjs +1 -0
  25. package/src/codemods/__tests__/runner.test.mjs +103 -0
  26. package/src/codemods/integration-discovery.mjs +168 -0
  27. package/src/codemods/integration-discovery.test.mjs +234 -0
  28. package/src/codemods/integration-runner.mjs +109 -0
  29. package/src/codemods/registry.mjs +1 -0
  30. package/src/codemods/run-codemod.mjs +207 -0
  31. package/src/codemods/runner.mjs +73 -96
  32. package/src/codemods/transforms/v0.0.15/index.mjs +0 -13
  33. package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-css-surfaces.test.mjs +67 -0
  34. package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-declare-module.test.mjs +61 -0
  35. package/src/codemods/transforms/v0.1.0/__tests__/v0.1.0-ordering.test.mjs +104 -0
  36. package/src/codemods/transforms/{v0.0.15 → v0.1.0}/drop-xds-prefix-imports.mjs +15 -4
  37. package/src/codemods/transforms/v0.1.0/index.mjs +43 -6
  38. package/src/codemods/transforms/v0.1.0/migrate-xds-css-surfaces.mjs +77 -0
  39. package/src/codemods/transforms/v0.1.0/migrate-xds-declare-module.mjs +78 -0
  40. package/src/codemods/transforms/v0.1.3/__tests__/migrate-layout-components-to-experimental.test.mjs +360 -0
  41. package/src/codemods/transforms/v0.1.3/index.mjs +19 -0
  42. package/src/codemods/transforms/v0.1.3/migrate-layout-components-to-experimental.mjs +260 -0
  43. package/src/commands/agent-docs.mjs +4 -1
  44. package/src/commands/blog.mjs +60 -0
  45. package/src/commands/build-theme.import-path.test.mjs +2 -12
  46. package/src/commands/build-theme.mjs +117 -0
  47. package/src/commands/build-theme.prose.test.mjs +5 -15
  48. package/src/commands/build-theme.watch.test.mjs +149 -0
  49. package/src/commands/component/index.mjs +44 -10
  50. package/src/commands/component-ownership.test.mjs +227 -0
  51. package/src/commands/discover.mjs +5 -16
  52. package/src/commands/doctor.test.mjs +3 -3
  53. package/src/commands/ensure-core-built.mjs +120 -0
  54. package/src/commands/json-contract.test.mjs +0 -32
  55. package/src/commands/swizzle.mjs +224 -235
  56. package/src/commands/swizzle.path-safety.test.mjs +3 -3
  57. package/src/commands/swizzle.routing.test.mjs +279 -0
  58. package/src/commands/template.mjs +33 -37
  59. package/src/commands/upgrade.config-ordering.test.mjs +273 -0
  60. package/src/commands/upgrade.integration-policy.test.mjs +204 -0
  61. package/src/commands/upgrade.mjs +349 -170
  62. package/src/commands/validate-integration.mjs +110 -0
  63. package/src/commands/validate-integration.test.mjs +124 -0
  64. package/src/config.mjs +7 -20
  65. package/src/config.test.mjs +89 -13
  66. package/src/index.mjs +8 -3
  67. package/src/integration.mjs +19 -0
  68. package/src/lib/component-discovery.mjs +177 -0
  69. package/src/lib/config-cache.mjs +91 -0
  70. package/src/lib/config-cache.test.mjs +83 -0
  71. package/src/lib/config-schema.mjs +41 -55
  72. package/src/lib/error-codes.mjs +15 -12
  73. package/src/lib/integration-warnings.mjs +62 -0
  74. package/src/lib/integration-warnings.test.mjs +102 -0
  75. package/src/lib/integrations.mjs +92 -115
  76. package/src/lib/integrations.test.mjs +88 -107
  77. package/src/lib/manifest.mjs +5 -2
  78. package/src/lib/module-loader.mjs +80 -0
  79. package/src/lib/module-loader.test.mjs +106 -0
  80. package/src/lib/project.mjs +502 -0
  81. package/src/lib/project.test.mjs +308 -0
  82. package/src/lib/site.mjs +20 -0
  83. package/src/template.mjs +73 -0
  84. package/src/template.test.mjs +127 -0
  85. package/src/types/api.d.ts +2 -6
  86. package/src/types/base.d.ts +3 -9
  87. package/src/types/codemod.d.ts +81 -0
  88. package/src/types/component.d.ts +27 -2
  89. package/src/types/config.d.ts +56 -85
  90. package/src/types/error-codes.d.ts +5 -3
  91. package/src/types/index.d.ts +0 -1
  92. package/src/types/integration.d.ts +29 -0
  93. package/src/types/swizzle.d.ts +9 -2
  94. package/src/types/template-api.d.ts +54 -0
  95. package/src/types/template.d.ts +10 -7
  96. package/src/types/upgrade.d.ts +29 -0
  97. package/src/types/validate-integration.d.ts +24 -0
  98. package/src/utils/github.mjs +0 -237
  99. package/src/utils/interactive.mjs +2 -2
  100. package/templates/blocks/components/AvatarStatusDot/AvatarStatusDotVariants.doc.mjs +14 -0
  101. package/templates/blocks/components/AvatarStatusDot/AvatarStatusDotVariants.tsx +28 -0
  102. package/templates/blocks/components/Blockquote/BlockquoteTestimonials.doc.mjs +14 -0
  103. package/templates/blocks/components/Blockquote/BlockquoteTestimonials.tsx +34 -0
  104. package/templates/blocks/components/Blockquote/BlockquoteWithCite.doc.mjs +14 -0
  105. package/templates/blocks/components/Blockquote/BlockquoteWithCite.tsx +21 -0
  106. package/templates/blocks/components/BreadcrumbItem/BreadcrumbItemBasic.doc.mjs +14 -0
  107. package/templates/blocks/components/BreadcrumbItem/BreadcrumbItemBasic.tsx +15 -0
  108. package/templates/blocks/components/ButtonGroup/ButtonGroupBasic.doc.mjs +14 -0
  109. package/templates/blocks/components/ButtonGroup/ButtonGroupBasic.tsx +16 -0
  110. package/templates/blocks/components/ChatDictationButton/ChatDictationButtonBasic.doc.mjs +19 -0
  111. package/templates/blocks/components/ChatDictationButton/ChatDictationButtonBasic.tsx +34 -0
  112. package/templates/blocks/components/CheckboxListItem/CheckboxListItemBasic.doc.mjs +14 -0
  113. package/templates/blocks/components/CheckboxListItem/CheckboxListItemBasic.tsx +30 -0
  114. package/templates/blocks/components/CollapsibleGroup/CollapsibleGroupAccordion.doc.mjs +14 -0
  115. package/templates/blocks/components/CollapsibleGroup/CollapsibleGroupAccordion.tsx +31 -0
  116. package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyBasic.doc.mjs +14 -0
  117. package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyBasic.tsx +26 -0
  118. package/templates/blocks/components/CommandPaletteFooter/CommandPaletteFooterBasic.doc.mjs +14 -0
  119. package/templates/blocks/components/CommandPaletteFooter/CommandPaletteFooterBasic.tsx +32 -0
  120. package/templates/blocks/components/CommandPaletteGroup/CommandPaletteGroupBasic.doc.mjs +18 -0
  121. package/templates/blocks/components/CommandPaletteGroup/CommandPaletteGroupBasic.tsx +32 -0
  122. package/templates/blocks/components/CommandPaletteItem/CommandPaletteItemBasic.doc.mjs +14 -0
  123. package/templates/blocks/components/CommandPaletteItem/CommandPaletteItemBasic.tsx +27 -0
  124. package/templates/blocks/components/ContextMenu/ContextMenuBasic.doc.mjs +14 -0
  125. package/templates/blocks/components/ContextMenu/ContextMenuBasic.tsx +32 -0
  126. package/templates/blocks/components/DateRangeInput/DateRangeInputWithPresets.doc.mjs +14 -0
  127. package/templates/blocks/components/DateRangeInput/DateRangeInputWithPresets.tsx +46 -0
  128. package/templates/blocks/components/DateRangeInput/DateRangeInputWithValidation.doc.mjs +14 -0
  129. package/templates/blocks/components/DateRangeInput/DateRangeInputWithValidation.tsx +52 -0
  130. package/templates/blocks/components/DateTimeInput/DateTimeInputWithValidation.doc.mjs +14 -0
  131. package/templates/blocks/components/DateTimeInput/DateTimeInputWithValidation.tsx +43 -0
  132. package/templates/blocks/components/DialogHeader/DialogHeaderBasic.doc.mjs +14 -0
  133. package/templates/blocks/components/DialogHeader/DialogHeaderBasic.tsx +30 -0
  134. package/templates/blocks/components/DropdownMenu/DropdownMenuShowcase.tsx +0 -6
  135. package/templates/blocks/components/DropdownMenuItem/DropdownMenuItemBasic.doc.mjs +14 -0
  136. package/templates/blocks/components/DropdownMenuItem/DropdownMenuItemBasic.tsx +27 -0
  137. package/templates/blocks/components/DropdownMenuItem/DropdownMenuItemShowcase.tsx +1 -5
  138. package/templates/blocks/components/FieldLabel/FieldLabelBasic.doc.mjs +14 -0
  139. package/templates/blocks/components/FieldLabel/FieldLabelBasic.tsx +20 -0
  140. package/templates/blocks/components/FieldStatus/FieldStatusBasic.doc.mjs +14 -0
  141. package/templates/blocks/components/FieldStatus/FieldStatusBasic.tsx +23 -0
  142. package/templates/blocks/components/FileInput/FileInputBasic.doc.mjs +14 -0
  143. package/templates/blocks/components/FileInput/FileInputBasic.tsx +22 -0
  144. package/templates/blocks/components/GridSpan/GridSpanColumns.doc.mjs +14 -0
  145. package/templates/blocks/components/GridSpan/GridSpanColumns.tsx +38 -0
  146. package/templates/blocks/components/HStack/HStackBasic.doc.mjs +14 -0
  147. package/templates/blocks/components/HStack/HStackBasic.tsx +16 -0
  148. package/templates/blocks/components/Hooks/useKeyboardHintHookUsage.doc.mjs +14 -0
  149. package/templates/blocks/components/Hooks/useKeyboardHintHookUsage.tsx +57 -0
  150. package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.doc.mjs +1 -1
  151. package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.tsx +9 -4
  152. package/templates/blocks/components/InputGroup/InputGroupBasic.doc.mjs +14 -0
  153. package/templates/blocks/components/InputGroup/InputGroupBasic.tsx +27 -0
  154. package/templates/blocks/components/LayoutContent/LayoutContentBasic.doc.mjs +23 -0
  155. package/templates/blocks/components/LayoutContent/LayoutContentBasic.tsx +40 -0
  156. package/templates/blocks/components/LayoutFooter/LayoutFooterActions.doc.mjs +22 -0
  157. package/templates/blocks/components/LayoutFooter/LayoutFooterActions.tsx +41 -0
  158. package/templates/blocks/components/LayoutHeader/LayoutHeaderWithActions.doc.mjs +23 -0
  159. package/templates/blocks/components/LayoutHeader/LayoutHeaderWithActions.tsx +40 -0
  160. package/templates/blocks/components/LayoutPanel/LayoutPanelNavigation.doc.mjs +22 -0
  161. package/templates/blocks/components/LayoutPanel/LayoutPanelNavigation.tsx +37 -0
  162. package/templates/blocks/components/Lightbox/LightboxGallery.doc.mjs +14 -0
  163. package/templates/blocks/components/Lightbox/LightboxGallery.tsx +53 -0
  164. package/templates/blocks/components/Lightbox/LightboxShowcase.tsx +4 -3
  165. package/templates/blocks/components/Lightbox/LightboxVideo.doc.mjs +14 -0
  166. package/templates/blocks/components/Lightbox/LightboxVideo.tsx +27 -0
  167. package/templates/blocks/components/Lightbox/LightboxZoom.doc.mjs +14 -0
  168. package/templates/blocks/components/Lightbox/LightboxZoom.tsx +33 -0
  169. package/templates/blocks/components/LinkProvider/LinkProviderCustomLink.doc.mjs +1 -1
  170. package/templates/blocks/components/MetadataListItem/MetadataListItemBasic.doc.mjs +14 -0
  171. package/templates/blocks/components/MetadataListItem/MetadataListItemBasic.tsx +19 -0
  172. package/templates/blocks/components/MobileNavToggle/MobileNavToggleBasic.doc.mjs +23 -0
  173. package/templates/blocks/components/MobileNavToggle/MobileNavToggleBasic.tsx +42 -0
  174. package/templates/blocks/components/MoreMenu/MoreMenuShowcase.tsx +0 -6
  175. package/templates/blocks/components/NavIcon/NavIconBasic.doc.mjs +14 -0
  176. package/templates/blocks/components/NavIcon/NavIconBasic.tsx +16 -0
  177. package/templates/blocks/components/RadioListItem/RadioListItemBasic.doc.mjs +14 -0
  178. package/templates/blocks/components/RadioListItem/RadioListItemBasic.tsx +30 -0
  179. package/templates/blocks/components/Resizable/ResizableSidebar.doc.mjs +25 -0
  180. package/templates/blocks/components/Resizable/ResizableSidebar.tsx +67 -0
  181. package/templates/blocks/components/SegmentedControlItem/SegmentedControlItemBasic.doc.mjs +14 -0
  182. package/templates/blocks/components/SegmentedControlItem/SegmentedControlItemBasic.tsx +21 -0
  183. package/templates/blocks/components/SelectorOption/SelectorOptionBasic.doc.mjs +14 -0
  184. package/templates/blocks/components/SelectorOption/SelectorOptionBasic.tsx +39 -0
  185. package/templates/blocks/components/SideNavCollapseButton/SideNavCollapseButtonBasic.doc.mjs +14 -0
  186. package/templates/blocks/components/SideNavCollapseButton/SideNavCollapseButtonBasic.tsx +55 -0
  187. package/templates/blocks/components/SideNavHeading/SideNavHeadingBasic.doc.mjs +14 -0
  188. package/templates/blocks/components/SideNavHeading/SideNavHeadingBasic.tsx +38 -0
  189. package/templates/blocks/components/SideNavItem/SideNavItemBasic.doc.mjs +14 -0
  190. package/templates/blocks/components/SideNavItem/SideNavItemBasic.tsx +49 -0
  191. package/templates/blocks/components/SideNavSection/SideNavSectionBasic.doc.mjs +14 -0
  192. package/templates/blocks/components/SideNavSection/SideNavSectionBasic.tsx +71 -0
  193. package/templates/blocks/components/StackItem/StackItemFill.doc.mjs +14 -0
  194. package/templates/blocks/components/StackItem/StackItemFill.tsx +28 -0
  195. package/templates/blocks/components/Tab/TabWithSelectedIcon.doc.mjs +13 -0
  196. package/templates/blocks/components/Tab/TabWithSelectedIcon.tsx +39 -0
  197. package/templates/blocks/components/TabMenu/TabMenuBasic.doc.mjs +14 -0
  198. package/templates/blocks/components/TabMenu/TabMenuBasic.tsx +23 -0
  199. package/templates/blocks/components/Table/StickyColumnsHookUsage.doc.mjs +1 -1
  200. package/templates/blocks/components/ToggleButtonGroup/ToggleButtonGroupVertical.doc.mjs +14 -0
  201. package/templates/blocks/components/ToggleButtonGroup/ToggleButtonGroupVertical.tsx +47 -0
  202. package/templates/blocks/components/TopNavHeading/TopNavHeadingBasic.doc.mjs +14 -0
  203. package/templates/blocks/components/TopNavHeading/TopNavHeadingBasic.tsx +22 -0
  204. package/templates/blocks/components/TopNavItem/TopNavItemBasic.doc.mjs +14 -0
  205. package/templates/blocks/components/TopNavItem/TopNavItemBasic.tsx +21 -0
  206. package/templates/blocks/components/TopNavMegaMenu/TopNavMegaMenuBasic.doc.mjs +20 -0
  207. package/templates/blocks/components/TopNavMegaMenu/TopNavMegaMenuBasic.tsx +46 -0
  208. package/templates/blocks/components/TopNavMegaMenuFeaturedCard/TopNavMegaMenuFeaturedCardBasic.doc.mjs +14 -0
  209. package/templates/blocks/components/TopNavMegaMenuFeaturedCard/TopNavMegaMenuFeaturedCardBasic.tsx +16 -0
  210. package/templates/blocks/components/TopNavMegaMenuItem/TopNavMegaMenuItemBasic.doc.mjs +14 -0
  211. package/templates/blocks/components/TopNavMegaMenuItem/TopNavMegaMenuItemBasic.tsx +26 -0
  212. package/templates/blocks/components/TopNavMenu/TopNavMenuBasic.doc.mjs +14 -0
  213. package/templates/blocks/components/TopNavMenu/TopNavMenuBasic.tsx +43 -0
  214. package/templates/blocks/components/TypeaheadItem/TypeaheadItemBasic.doc.mjs +14 -0
  215. package/templates/blocks/components/TypeaheadItem/TypeaheadItemBasic.tsx +43 -0
  216. package/templates/blocks/components/VStack/VStackBasic.doc.mjs +14 -0
  217. package/templates/blocks/components/VStack/VStackBasic.tsx +20 -0
  218. package/templates/pages/kanban-board/page.tsx +729 -0
  219. package/templates/pages/kanban-board/template.doc.mjs +12 -0
  220. package/templates/pages/shell-nav/page.tsx +321 -0
  221. package/templates/pages/shell-nav/template.doc.mjs +12 -0
  222. package/templates/pages/shell-side-nav/page.tsx +241 -0
  223. package/templates/pages/shell-side-nav/template.doc.mjs +12 -0
  224. package/templates/pages/shell-top-nav/page.tsx +224 -0
  225. package/templates/pages/shell-top-nav/template.doc.mjs +12 -0
  226. package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-config-surfaces.test.mjs +0 -116
  227. package/src/codemods/transforms/v0.1.0/migrate-xds-config-surfaces.mjs +0 -230
  228. package/src/commands/gap-report.mjs +0 -464
  229. package/src/commands/gap-report.test.mjs +0 -168
  230. package/src/commands/swizzle-gap-safety.test.mjs +0 -273
  231. package/src/lib/config.mjs +0 -113
  232. package/src/lib/config.test.mjs +0 -91
  233. package/src/types/gap-report.d.ts +0 -29
  234. package/templates/blocks/components/TreeListBranches/TreeListBranchesShowcase.doc.mjs +0 -14
  235. package/templates/blocks/components/TreeListBranches/TreeListBranchesShowcase.tsx +0 -64
  236. package/templates/blocks/components/TreeListItem/TreeListItemShowcase.doc.mjs +0 -14
  237. package/templates/blocks/components/TreeListItem/TreeListItemShowcase.tsx +0 -60
  238. /package/src/codemods/transforms/{v0.0.15 → v0.1.0}/__tests__/drop-xds-prefix-imports.test.mjs +0 -0
@@ -11,18 +11,48 @@ import * as fs from 'node:fs';
11
11
  import {ERROR_CODES} from '../lib/error-codes.mjs';
12
12
  import {findCoreDir, discoverExternalPackages} from '../utils/paths.mjs';
13
13
  import {
14
+ CORE_PACKAGE,
14
15
  discoverComponents,
15
16
  discoverExternalComponentsGrouped,
17
+ discoverIntegrationComponents,
16
18
  findComponentReadme,
17
19
  findComponentSource,
18
20
  findExternalComponentDoc,
21
+ findIntegrationComponentDoc,
22
+ findIntegrationComponentSource,
19
23
  resolveImportPath,
20
24
  } from '../lib/component-discovery.mjs';
25
+ import {Project} from '../lib/project.mjs';
21
26
  import {loadDocs} from '../lib/component-loader.mjs';
22
27
  import {searchComponents} from '../lib/string-utils.mjs';
23
28
  import {AstryxError} from './error.mjs';
24
29
  import {findShowcase, findRelatedBlocks} from './template.mjs';
25
30
 
31
+ /**
32
+ * Load the configured integrations for `cwd`, swallowing any config errors so
33
+ * component discovery never hard-fails on a malformed/absent integration. An
34
+ * empty list means "core only".
35
+ * @param {string} cwd
36
+ * @returns {Promise<Array<{name: string, components?: string, issuesUrl?: string}>>}
37
+ */
38
+ async function loadIntegrationsSafely(cwd) {
39
+ try {
40
+ const project = await Project.load(cwd);
41
+ return project.loadedIntegrations;
42
+ } catch {
43
+ return [];
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Resolve a loaded integration by package name.
49
+ * @param {Array<{name: string}>} loadedIntegrations
50
+ * @param {string} packageName
51
+ */
52
+ function findLoadedIntegration(loadedIntegrations, packageName) {
53
+ return loadedIntegrations.find(i => i.name === packageName) ?? null;
54
+ }
55
+
26
56
  /**
27
57
  * Resolve an external package by name from the discovered externals list.
28
58
  * @param {string} packageName - e.g. '@acme/xds-widgets'
@@ -131,8 +161,13 @@ export async function component(name, options = {}) {
131
161
  return {type: 'component.full', data: {[match[0]]: entries}};
132
162
  }
133
163
 
134
- // Default: brief — names only
135
- return {type: 'component.list', data: {[match[0]]: match[1]}};
164
+ // Default: brief — package-qualified object list for the category.
165
+ // Pre-1.0 JSON contract: members are {name, package} objects, not bare
166
+ // strings, so consumers can disambiguate ownership.
167
+ return {
168
+ type: 'component.list',
169
+ data: {[match[0]]: match[1].map(n => ({name: n, package: CORE_PACKAGE}))},
170
+ };
136
171
  }
137
172
 
138
173
  // All components — merge core + external packages with grouped subcategories
@@ -179,43 +214,189 @@ export async function component(name, options = {}) {
179
214
  return {type: 'component.full', data: result};
180
215
  }
181
216
 
182
- // Default: brief — names only (with externals merged in)
217
+ // Default: brief — package-qualified object list (core + integrations).
218
+ // Pre-1.0 JSON contract: each group's members are {name, package} objects.
219
+ /** @type {Record<string, Array<{name: string, package: string}>>} */
220
+ const listData = {};
221
+ for (const [cat, comps] of Object.entries(components)) {
222
+ listData[cat] = comps.map(n => ({name: n, package: CORE_PACKAGE}));
223
+ }
224
+
225
+ // Integration components (authoritative source: loadedIntegrations).
226
+ const loadedIntegrations = await loadIntegrationsSafely(cwd);
227
+ const seenIntegration = new Set();
228
+ for (const integration of loadedIntegrations) {
229
+ seenIntegration.add(integration.name);
230
+ const owned = discoverIntegrationComponents(integration);
231
+ // Group integration components by their doc `group`, falling back to the
232
+ // package name. Keys are package-qualified so they never collide with
233
+ // core groups or each other.
234
+ /** @type {Map<string, Array<{name: string, package: string}>>} */
235
+ const byGroup = new Map();
236
+ for (const rec of owned) {
237
+ const groupLabel = rec.group ?? integration.name;
238
+ const key = `${groupLabel} (${integration.name})`;
239
+ if (!byGroup.has(key)) byGroup.set(key, []);
240
+ byGroup.get(key).push({name: rec.name, package: integration.name});
241
+ }
242
+ for (const [key, members] of byGroup) {
243
+ members.sort((a, b) => a.name.localeCompare(b.name));
244
+ listData[key] = members;
245
+ }
246
+ }
247
+
248
+ // Back-compat: node_modules-scanned external packages (pkg.astryx.docs)
249
+ // that are NOT configured integrations. Preserves existing discovery for
250
+ // consumers that haven't adopted the config-integration flow.
183
251
  const externals = discoverExternalPackages(cwd);
184
252
  for (const ext of externals) {
253
+ if (seenIntegration.has(ext.name)) continue;
185
254
  const grouped = discoverExternalComponentsGrouped(ext.docsDir);
186
255
  const groupKeys = Object.keys(grouped);
187
256
  if (groupKeys.length === 0) continue;
188
257
 
189
- // If the package has subcategories (groups), emit each as a separate key.
190
- // If no groups exist, fall back to the flat list under one key.
191
258
  const hasGroups = groupKeys.some(
192
259
  k => grouped[k].length > 1 || grouped[k][0] !== k,
193
260
  );
194
261
 
195
262
  if (hasGroups) {
196
263
  for (const [group, members] of Object.entries(grouped)) {
197
- components[`${group} (${ext.name})`] = members;
264
+ listData[`${group} (${ext.name})`] = members.map(n => ({
265
+ name: n,
266
+ package: ext.name,
267
+ }));
198
268
  }
199
269
  } else {
200
- // All ungrouped — single flat list under the package category
201
270
  const allComps = Object.values(grouped).flat().sort();
202
271
  if (allComps.length > 0) {
203
- components[`${ext.category} (${ext.name})`] = allComps;
272
+ listData[`${ext.category} (${ext.name})`] = allComps.map(n => ({
273
+ name: n,
274
+ package: ext.name,
275
+ }));
204
276
  }
205
277
  }
206
278
  }
207
- return {type: 'component.list', data: components};
279
+ return {type: 'component.list', data: listData};
208
280
  }
209
281
 
210
282
  // ── Single component ───────────────────────────────────────────
211
283
 
284
+ if (typeof name !== 'string') {
285
+ throw new AstryxError(
286
+ `No component named "${String(name)}"`,
287
+ undefined,
288
+ ERROR_CODES.ERR_UNKNOWN_COMPONENT,
289
+ );
290
+ }
291
+
212
292
  const dirName = name.replace(/^XDS/, '');
213
293
 
294
+ // Ownership-aware resolution. Build the set of OWNER packages that provide a
295
+ // component with this name across core + every loaded integration. This is
296
+ // what lets the CLI disambiguate by package and expose the owner's source +
297
+ // issuesUrl (the inputs the future integration-component swizzle needs).
298
+ const loadedIntegrations = await loadIntegrationsSafely(cwd);
299
+ const coreDocPath = findComponentReadme(coreDir, dirName);
300
+ /**
301
+ * @type {Array<{
302
+ * package: string,
303
+ * docPath: string,
304
+ * sourcePath: string|null,
305
+ * issuesUrl: string|undefined,
306
+ * integration: object|null,
307
+ * }>}
308
+ */
309
+ const owners = [];
310
+ if (coreDocPath) {
311
+ owners.push({
312
+ package: CORE_PACKAGE,
313
+ docPath: coreDocPath,
314
+ sourcePath: findComponentSource(coreDir, dirName),
315
+ issuesUrl: undefined,
316
+ integration: null,
317
+ });
318
+ }
319
+ for (const integration of loadedIntegrations) {
320
+ const docPath = findIntegrationComponentDoc(integration, dirName);
321
+ if (!docPath) continue;
322
+ owners.push({
323
+ package: integration.name,
324
+ docPath,
325
+ sourcePath: findIntegrationComponentSource(integration, dirName),
326
+ issuesUrl: integration.issuesUrl,
327
+ integration,
328
+ });
329
+ }
330
+
331
+ /**
332
+ * Augment a loaded `component.detail` doc with ownership metadata. Adds
333
+ * `package`, the resolved `import` specifier, and `sourceAvailable` (whether
334
+ * a swizzleable source file exists for the owner). Existing doc fields
335
+ * (name, usage, props, …) are preserved.
336
+ * @param {object} docs
337
+ * @param {{package: string, sourcePath: string|null}} owner
338
+ * @param {string} componentName
339
+ */
340
+ function withOwnership(docs, owner, componentName) {
341
+ const importSpec =
342
+ owner.package === CORE_PACKAGE
343
+ ? resolveImportPath(coreDir, componentName)
344
+ : `${owner.package}/${componentName}`;
345
+ return {
346
+ ...docs,
347
+ package: owner.package,
348
+ import: importSpec,
349
+ sourceAvailable: owner.sourcePath != null,
350
+ };
351
+ }
352
+
214
353
  // When scoped to a specific package, search that package first.
215
354
  // This is critical for components that exist in both core and an external
216
355
  // package (e.g. AppShell, Button, SideNav) — the package scope ensures
217
356
  // the external package's docs are returned, not core's.
218
357
  if (packageScope) {
358
+ // Core scope: resolve from core directly.
359
+ if (packageScope === CORE_PACKAGE) {
360
+ const owner = owners.find(o => o.package === CORE_PACKAGE);
361
+ if (!owner) {
362
+ throw new AstryxError(`No component "${name}" in package "${packageScope}"`, undefined, ERROR_CODES.ERR_UNKNOWN_COMPONENT);
363
+ }
364
+ if (source) {
365
+ if (!owner.sourcePath) {
366
+ throw new AstryxError(`Source for "${name}" not found`, undefined, ERROR_CODES.ERR_NO_SOURCE);
367
+ }
368
+ return {type: 'component.detail.source', data: {component: dirName, source: fs.readFileSync(owner.sourcePath, 'utf-8')}};
369
+ }
370
+ const docs = await loadDocs(owner.docPath, {zh, dense, lang});
371
+ if (props) {
372
+ const p = docs.props || (docs.components ? docs.components.flatMap(c => c.props || []) : []);
373
+ return {type: 'component.detail.props', data: p};
374
+ }
375
+ return {type: 'component.detail', data: withOwnership(docs, owner, dirName)};
376
+ }
377
+
378
+ // Integration scope (authoritative): resolve from the loaded integration.
379
+ const integration = findLoadedIntegration(loadedIntegrations, packageScope);
380
+ if (integration) {
381
+ const owner = owners.find(o => o.package === packageScope);
382
+ if (!owner) {
383
+ throw new AstryxError(`No component "${name}" in package "${packageScope}"`, undefined, ERROR_CODES.ERR_UNKNOWN_COMPONENT);
384
+ }
385
+ if (source) {
386
+ if (!owner.sourcePath) {
387
+ throw new AstryxError(`Source for "${name}" not found in package "${packageScope}"`, undefined, ERROR_CODES.ERR_NO_SOURCE);
388
+ }
389
+ return {type: 'component.detail.source', data: {component: dirName, source: fs.readFileSync(owner.sourcePath, 'utf-8')}};
390
+ }
391
+ const docs = await loadDocs(owner.docPath, {zh, dense, lang});
392
+ if (props) {
393
+ const p = docs.props || (docs.components ? docs.components.flatMap(c => c.props || []) : []);
394
+ return {type: 'component.detail.props', data: p};
395
+ }
396
+ return {type: 'component.detail', data: withOwnership(docs, owner, dirName)};
397
+ }
398
+
399
+ // Legacy fallback: node_modules `pkg.astryx.docs` external package.
219
400
  const ext = resolveExternalPackage(packageScope, cwd);
220
401
  if (!ext) {
221
402
  throw new AstryxError(`External package "${packageScope}" not found`, undefined, ERROR_CODES.ERR_UNKNOWN_PACKAGE);
@@ -244,11 +425,49 @@ export async function component(name, options = {}) {
244
425
  const p = docs.props || (docs.components ? docs.components.flatMap(c => c.props || []) : []);
245
426
  return {type: 'component.detail.props', data: p};
246
427
  }
247
- return {type: 'component.detail', data: docs};
428
+ return {
429
+ type: 'component.detail',
430
+ data: withOwnership(docs, {package: ext.name, sourcePath: null}, dirName),
431
+ };
248
432
  }
249
433
  throw new AstryxError(`No component "${name}" in package "${packageScope}"`, undefined, ERROR_CODES.ERR_UNKNOWN_COMPONENT);
250
434
  }
251
435
 
436
+ // Ambiguity: when the name is owned by MORE THAN ONE package (core and/or
437
+ // integrations) and the caller did not scope with --package, refuse to guess.
438
+ // NOTE: legacy `pkg.astryx.docs` externals are intentionally NOT part of this
439
+ // ambiguity set — they retain their historical core-first fallback below so
440
+ // existing consumers (and tests) keep working. Only config-driven integration
441
+ // ownership participates here.
442
+ if (owners.length > 1) {
443
+ throw new AstryxError(
444
+ `Component "${dirName}" is provided by multiple packages. Re-run with --package <pkg> to choose one.`,
445
+ owners.map(o => ({name: o.package, reason: 'provides this component'})),
446
+ ERROR_CODES.ERR_UNKNOWN_COMPONENT,
447
+ );
448
+ }
449
+
450
+ // Single non-core owner (an integration provides it, core does not) — resolve
451
+ // from that integration so the integration component is authoritative.
452
+ if (owners.length === 1 && owners[0].package !== CORE_PACKAGE) {
453
+ const owner = owners[0];
454
+ if (source) {
455
+ if (!owner.sourcePath) {
456
+ throw new AstryxError(`Source for "${name}" not found`, undefined, ERROR_CODES.ERR_NO_SOURCE);
457
+ }
458
+ return {type: 'component.detail.source', data: {component: dirName, source: fs.readFileSync(owner.sourcePath, 'utf-8')}};
459
+ }
460
+ if (showcase) {
461
+ throw new AstryxError(`No showcase found for "${name}"`, undefined, ERROR_CODES.ERR_NO_SHOWCASE);
462
+ }
463
+ const docs = await loadDocs(owner.docPath, {zh, dense, lang});
464
+ if (props) {
465
+ const p = docs.props || (docs.components ? docs.components.flatMap(c => c.props || []) : []);
466
+ return {type: 'component.detail.props', data: p};
467
+ }
468
+ return {type: 'component.detail', data: withOwnership(docs, owner, dirName)};
469
+ }
470
+
252
471
  if (source) {
253
472
  const sourcePath = findComponentSource(coreDir, dirName);
254
473
  if (!sourcePath) {
@@ -274,6 +493,10 @@ export async function component(name, options = {}) {
274
493
 
275
494
  let readmePath = findComponentReadme(coreDir, dirName);
276
495
  let resolvedName = dirName;
496
+ // Track the resolving owner so the detail payload can carry ownership info.
497
+ // Defaults to core; the legacy-external fallback below may reassign it.
498
+ let resolvedOwnerPackage = CORE_PACKAGE;
499
+ let resolvedSourcePath = readmePath ? findComponentSource(coreDir, dirName) : null;
277
500
 
278
501
  if (!readmePath) {
279
502
  const externals = discoverExternalPackages(cwd);
@@ -281,6 +504,8 @@ export async function component(name, options = {}) {
281
504
  const extDocPath = findExternalComponentDoc(ext.docsDir, dirName);
282
505
  if (extDocPath) {
283
506
  readmePath = extDocPath;
507
+ resolvedOwnerPackage = ext.name;
508
+ resolvedSourcePath = null;
284
509
  break;
285
510
  }
286
511
  }
@@ -299,6 +524,8 @@ export async function component(name, options = {}) {
299
524
  if (topScore >= 90 && topTied.length === 1 && gap >= 20) {
300
525
  resolvedName = topTied[0].name;
301
526
  readmePath = findComponentReadme(coreDir, resolvedName);
527
+ resolvedOwnerPackage = CORE_PACKAGE;
528
+ resolvedSourcePath = findComponentSource(coreDir, resolvedName);
302
529
  } else {
303
530
  const threshold = Math.max(topScore - 20, 1);
304
531
  const candidates = results.filter(r => r.score >= threshold).slice(0, 5);
@@ -382,7 +609,14 @@ export async function component(name, options = {}) {
382
609
  if (props) {
383
610
  return {type: 'component.detail.props', data: matchingComponent.props || []};
384
611
  }
385
- return {type: 'component.detail', data: scoped};
612
+ return {
613
+ type: 'component.detail',
614
+ data: withOwnership(
615
+ scoped,
616
+ {package: resolvedOwnerPackage, sourcePath: resolvedSourcePath},
617
+ dirName,
618
+ ),
619
+ };
386
620
  }
387
621
 
388
622
  if (props) {
@@ -390,5 +624,12 @@ export async function component(name, options = {}) {
390
624
  return {type: 'component.detail.props', data: p};
391
625
  }
392
626
 
393
- return {type: 'component.detail', data: docs};
627
+ return {
628
+ type: 'component.detail',
629
+ data: withOwnership(
630
+ docs,
631
+ {package: resolvedOwnerPackage, sourcePath: resolvedSourcePath},
632
+ resolvedName,
633
+ ),
634
+ };
394
635
  }
@@ -4,7 +4,7 @@
4
4
  * @file Programmatic API for the discover command.
5
5
  */
6
6
 
7
- import {loadConfig} from '../lib/config.mjs';
7
+ import {Project} from '../lib/project.mjs';
8
8
  import {
9
9
  scanAllPackages,
10
10
  findComponentInPackages,
@@ -40,7 +40,7 @@ function validateDocs(docs) {
40
40
  */
41
41
  export async function discover(query, options = {}) {
42
42
  const {lang = null, zh = false} = options;
43
- const config = await loadConfig();
43
+ const project = await Project.load();
44
44
  const toEntry = pkg => ({
45
45
  name: pkg.name,
46
46
  category: pkg.category,
@@ -50,14 +50,21 @@ export async function discover(query, options = {}) {
50
50
  displayName: pkg.displayName,
51
51
  });
52
52
 
53
- const explicitPackages = (config.loadedIntegrations ?? [])
54
- .map(integration => integration.package)
55
- .filter(Boolean);
56
- if (config.packages.length === 0 && explicitPackages.length === 0) {
53
+ // External packages come from configured integrations that declare a
54
+ // components root. Each becomes a scannable package keyed by its docsDir.
55
+ const explicitPackages = project.loadedIntegrations
56
+ .filter(integration => integration.components)
57
+ .map(integration => ({
58
+ name: integration.name,
59
+ version: integration.version,
60
+ category: integration.name,
61
+ docsDir: integration.components,
62
+ }));
63
+ if (explicitPackages.length === 0) {
57
64
  return {type: 'discover.list', data: [], meta: {configured: false}};
58
65
  }
59
66
 
60
- const packages = scanAllPackages(config.packages, explicitPackages);
67
+ const packages = scanAllPackages([], explicitPackages);
61
68
 
62
69
  if (packages.length === 0) {
63
70
  return {type: 'discover.list', data: [], meta: {configured: true}};
@@ -27,7 +27,7 @@ import {createRequire} from 'node:module';
27
27
  import {MIN_NODE_VERSION, isNodeVersionSupported} from '../lib/node-version.mjs';
28
28
  import {CLI_ROOT, findCoreDir} from '../utils/paths.mjs';
29
29
  import {detectPackageManager} from '../utils/package-manager.mjs';
30
- import {findConfigPath, loadConfig} from '../lib/config.mjs';
30
+ import {findConfigPath, Project} from '../lib/project.mjs';
31
31
  import {semverCompare} from '../utils/semver.mjs';
32
32
 
33
33
  const _require = createRequire(import.meta.url);
@@ -240,7 +240,7 @@ export function checkThemes(ctx) {
240
240
  label: 'Theme packages',
241
241
  status: 'warn',
242
242
  message: 'No @astryxdesign/theme-* packages are installed.',
243
- fix: 'Install a theme, e.g. `npm install @astryxdesign/theme-neutral`, then import its CSS or set xds.theme.',
243
+ fix: 'Install a theme, e.g. `npm install @astryxdesign/theme-neutral`, then import its CSS or set astryx.theme.',
244
244
  };
245
245
  }
246
246
 
@@ -279,8 +279,9 @@ export async function checkConfig(ctx) {
279
279
  };
280
280
  }
281
281
 
282
- // loadConfig swallows errors and returns defaults, so re-import directly to
283
- // surface a genuine load failure as a FAIL.
282
+ // Project.load swallows nothing it surfaces a genuine load failure — but
283
+ // the config check wants to report a bad default export precisely, so we
284
+ // re-import directly to surface a genuine load failure as a FAIL.
284
285
  try {
285
286
  const {pathToFileURL} = await import('node:url');
286
287
  const mod = await import(pathToFileURL(ctx.configPath).href);
@@ -291,25 +292,9 @@ export async function checkConfig(ctx) {
291
292
  label: 'astryx.config.mjs',
292
293
  status: 'fail',
293
294
  message: `astryx.config.mjs default export is not an object (got ${typeof config}).`,
294
- fix: 'Export a default object from astryx.config.mjs, e.g. `export default { theme: "default" };`.',
295
+ fix: 'Export a default object from astryx.config.mjs, e.g. `export default { integrations: [] };`.',
295
296
  };
296
297
  }
297
- // Validate that `packages`, if present, is a string or array of strings.
298
- if (config?.packages !== undefined) {
299
- const arr = Array.isArray(config.packages)
300
- ? config.packages
301
- : [config.packages];
302
- const bad = arr.some(p => typeof p !== 'string');
303
- if (bad) {
304
- return {
305
- id: 'config',
306
- label: 'astryx.config.mjs',
307
- status: 'fail',
308
- message: 'astryx.config.mjs `packages` must be a string or array of strings.',
309
- fix: 'Set `packages` to a path string or array of path strings.',
310
- };
311
- }
312
- }
313
298
  return {
314
299
  id: 'config',
315
300
  label: 'astryx.config.mjs',
@@ -369,7 +354,7 @@ export function checkAgentDocs(ctx) {
369
354
  label: 'AI agent docs',
370
355
  status: 'warn',
371
356
  message: `Agent docs present (${present.join(', ')}) but no Astryx section markers found.`,
372
- fix: 'Add the XDS section to your agent docs with `astryx init --features agents`.',
357
+ fix: 'Add the Astryx section to your agent docs with `astryx init --features agents`.',
373
358
  };
374
359
  }
375
360
 
@@ -377,7 +362,7 @@ export function checkAgentDocs(ctx) {
377
362
  id: 'agent-docs',
378
363
  label: 'AI agent docs',
379
364
  status: 'pass',
380
- message: `XDS agent docs section present in ${withMarkers.join(', ')}.`,
365
+ message: `Astryx agent docs section present in ${withMarkers.join(', ')}.`,
381
366
  };
382
367
  }
383
368
 
@@ -493,8 +478,8 @@ export async function runChecks(options = {}) {
493
478
  // Resolve a possible theme key from config (best-effort; never throws).
494
479
  let configTheme = null;
495
480
  try {
496
- const loaded = await loadConfig(cwd);
497
- configTheme = loaded?.theme ?? null;
481
+ const project = await Project.load(cwd);
482
+ configTheme = project.config?.theme ?? null;
498
483
  } catch {
499
484
  // Best-effort: a missing/invalid config leaves configTheme null.
500
485
  }
package/src/api/index.mjs CHANGED
@@ -21,6 +21,7 @@
21
21
 
22
22
  export {component} from './component.mjs';
23
23
  export {docs} from './docs.mjs';
24
+ export {blog} from './blog.mjs';
24
25
  export {discover} from './discover.mjs';
25
26
  export {template} from './template.mjs';
26
27
  export {themeAdd, listThemes} from './theme-add.mjs';
@@ -25,13 +25,14 @@ import {expand} from '../lib/xle/expand.mjs';
25
25
  import {toCompact, toOutline} from '../lib/xle/print.mjs';
26
26
  import {buildRegistry, ALIAS_TABLE} from '../lib/xle/registry.mjs';
27
27
  import {discoverTemplates, stripTemplateAssetRefs} from './template.mjs';
28
- import {loadConfig} from '../lib/config.mjs';
28
+ import {Project} from '../lib/project.mjs';
29
29
 
30
30
  /**
31
31
  * The catalog a `{hint}` can resolve to: template blocks (spliced inline) plus
32
- * any app-registered local components from astryx.config.mjs `layout.components`
33
- * (imported by name). App components are how XLE reaches domain pieces — the
34
- * KpiCard/chart/drawer set that the @astryxdesign/core registry can't see.
32
+ * any app-registered local components from astryx.config.mjs
33
+ * `experimental.xle.components` (imported by name). App components are how XLE
34
+ * reaches domain pieces — the KpiCard/chart/drawer set that the
35
+ * @astryxdesign/core registry can't see.
35
36
  */
36
37
  async function loadBlocks(cwd) {
37
38
  const blocks = [];
@@ -42,20 +43,20 @@ async function loadBlocks(cwd) {
42
43
  // discovery is best-effort
43
44
  }
44
45
  try {
45
- const config = await loadConfig(cwd);
46
- const components = config.layout?.components || {};
46
+ const project = await Project.load(cwd);
47
+ const components = project.config.experimental?.xle?.components ?? {};
47
48
  for (const [name, spec] of Object.entries(components)) {
48
- const importPath = typeof spec === 'string' ? spec : spec.from;
49
+ const importPath = spec.from;
49
50
  if (!importPath) continue;
50
51
  blocks.push({
51
52
  type: 'block',
52
53
  kind: 'component',
53
54
  dirName: name,
54
55
  name,
55
- description: typeof spec === 'object' ? spec.description || '' : '',
56
+ description: spec.description ?? '',
56
57
  category: 'app',
57
58
  importPath,
58
- isDefault: typeof spec === 'object' ? Boolean(spec.default) : false,
59
+ isDefault: Boolean(spec.default),
59
60
  });
60
61
  }
61
62
  } catch {
@@ -281,7 +282,7 @@ TEMPLATE REFERENCING ({hint} pulls in real content — this is how XLE reaches
281
282
  {kpi-card} standalone reference (no wrapper element) — place a component directly
282
283
  {kpi-card}*4 repeat a reference; the definition/import is emitted once
283
284
  app components register local ones in astryx.config.mjs to import them by name:
284
- export default {layout: {components: {KpiCard: '@/components/KpiCard'}}}
285
+ export default {experimental: {xle: {components: {KpiCard: {from: '@/components/KpiCard'}}}}}
285
286
  then {kpi-card} → import {KpiCard} + <KpiCard /> (kebab ↔ Pascal)
286
287
 
287
288
  STRUCTURE THE EXPANDER HANDLES
@@ -196,11 +196,14 @@ describe('template referencing', () => {
196
196
 
197
197
  it('imports app-registered local components (the local-component bridge)', async () => {
198
198
  // Inside the workspace so @astryxdesign/core resolves; cleaned up after.
199
+ // A package.json beside the config makes Project.load resolve it as the
200
+ // sibling-of-nearest-package.json (the standard config resolution).
199
201
  const cwd = mkdtempSync(join(process.cwd(), '.xle-imp-test-'));
200
202
  try {
203
+ writeFileSync(join(cwd, 'package.json'), '{"name": "xle-imp-fixture"}\n');
201
204
  writeFileSync(
202
205
  join(cwd, 'astryx.config.mjs'),
203
- `export default {layout: {components: {KpiCard: '@/components/KpiCard', TimeRangePicker: {from: '@/components/TimeRangePicker'}}}};\n`,
206
+ `export default {experimental: {xle: {components: {KpiCard: {from: '@/components/KpiCard'}, TimeRangePicker: {from: '@/components/TimeRangePicker'}}}}};\n`,
204
207
  );
205
208
  const result = await layoutExpand('S[p6] > (G[c4 g4] > {kpi-card}*4) + {time-range-picker}', {
206
209
  name: 'Demo',