@astryxdesign/cli 0.1.1 → 0.1.2-canary.043f3fd

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 (185) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/docs/theme.doc.mjs +2 -2
  3. package/docs/working-with-ai.doc.mjs +6 -6
  4. package/package.json +31 -8
  5. package/src/api/component.mjs +253 -12
  6. package/src/api/discover.mjs +87 -28
  7. package/src/api/doctor.mjs +7 -22
  8. package/src/api/index.mjs +1 -0
  9. package/src/api/layout.mjs +302 -0
  10. package/src/api/layout.test.mjs +241 -0
  11. package/src/api/template-integration.test.mjs +225 -0
  12. package/src/api/template.mjs +421 -122
  13. package/src/api/template.test.mjs +2 -0
  14. package/src/api/theme-add.mjs +182 -0
  15. package/src/api/validate-integration.mjs +370 -0
  16. package/src/api/validate-integration.test.mjs +222 -0
  17. package/src/codemod.mjs +93 -0
  18. package/src/codemod.test.mjs +134 -0
  19. package/src/codemods/__tests__/registry.test.mjs +2 -0
  20. package/src/codemods/__tests__/runner.test.mjs +103 -0
  21. package/src/codemods/integration-discovery.mjs +168 -0
  22. package/src/codemods/integration-discovery.test.mjs +234 -0
  23. package/src/codemods/integration-runner.mjs +109 -0
  24. package/src/codemods/registry.mjs +2 -0
  25. package/src/codemods/run-codemod.mjs +207 -0
  26. package/src/codemods/runner.mjs +73 -96
  27. package/src/codemods/transforms/v0.0.15/index.mjs +0 -13
  28. package/src/codemods/transforms/v0.1.0/__tests__/v0.1.0-ordering.test.mjs +81 -0
  29. package/src/codemods/transforms/{v0.0.15 → v0.1.0}/drop-xds-prefix-imports.mjs +15 -4
  30. package/src/codemods/transforms/v0.1.0/index.mjs +15 -6
  31. package/src/codemods/transforms/v0.1.2/__tests__/rename-text-color-active-to-accent.test.mjs +120 -0
  32. package/src/codemods/transforms/v0.1.2/index.mjs +19 -0
  33. package/src/codemods/transforms/v0.1.2/rename-text-color-active-to-accent.mjs +136 -0
  34. package/src/codemods/transforms/v0.1.3/__tests__/migrate-layout-components-to-experimental.test.mjs +360 -0
  35. package/src/codemods/transforms/v0.1.3/index.mjs +19 -0
  36. package/src/codemods/transforms/v0.1.3/migrate-layout-components-to-experimental.mjs +260 -0
  37. package/src/commands/build-theme.import-path.test.mjs +22 -2
  38. package/src/commands/build-theme.mjs +185 -16
  39. package/src/commands/component/index.mjs +44 -10
  40. package/src/commands/component-ownership.test.mjs +227 -0
  41. package/src/commands/discover.mjs +5 -16
  42. package/src/commands/doctor.test.mjs +3 -3
  43. package/src/commands/init.mjs +34 -8
  44. package/src/commands/init.next-steps.test.mjs +46 -0
  45. package/src/commands/json-contract.test.mjs +0 -32
  46. package/src/commands/layout.mjs +139 -0
  47. package/src/commands/swizzle.mjs +241 -224
  48. package/src/commands/swizzle.path-safety.test.mjs +3 -3
  49. package/src/commands/swizzle.routing.test.mjs +279 -0
  50. package/src/commands/template.mjs +33 -37
  51. package/src/commands/upgrade.config-ordering.test.mjs +273 -0
  52. package/src/commands/upgrade.integration-policy.test.mjs +204 -0
  53. package/src/commands/upgrade.mjs +349 -239
  54. package/src/commands/validate-integration.mjs +110 -0
  55. package/src/commands/validate-integration.test.mjs +124 -0
  56. package/src/config.mjs +18 -0
  57. package/src/config.test.mjs +100 -0
  58. package/src/index.mjs +13 -3
  59. package/src/integration.mjs +19 -0
  60. package/src/lib/component-discovery.mjs +177 -0
  61. package/src/lib/config-cache.mjs +91 -0
  62. package/src/lib/config-cache.test.mjs +83 -0
  63. package/src/lib/config-schema.mjs +105 -0
  64. package/src/lib/error-codes.mjs +18 -12
  65. package/src/lib/integration-warnings.mjs +62 -0
  66. package/src/lib/integration-warnings.test.mjs +102 -0
  67. package/src/lib/integrations.mjs +132 -0
  68. package/src/lib/integrations.test.mjs +135 -0
  69. package/src/lib/levenshtein.mjs +29 -0
  70. package/src/lib/manifest.mjs +15 -2
  71. package/src/lib/module-loader.mjs +80 -0
  72. package/src/lib/module-loader.test.mjs +106 -0
  73. package/src/lib/package-scanner.mjs +31 -7
  74. package/src/lib/project.mjs +502 -0
  75. package/src/lib/project.test.mjs +308 -0
  76. package/src/lib/string-utils.mjs +5 -14
  77. package/src/lib/xle/browser.d.ts +100 -0
  78. package/src/lib/xle/browser.mjs +120 -0
  79. package/src/lib/xle/expand.mjs +622 -0
  80. package/src/lib/xle/parse.mjs +581 -0
  81. package/src/lib/xle/print.mjs +174 -0
  82. package/src/lib/xle/registry-core.mjs +170 -0
  83. package/src/lib/xle/registry.mjs +237 -0
  84. package/src/lib/xle/splice.mjs +137 -0
  85. package/src/lib/xle/validate.mjs +356 -0
  86. package/src/lib/xle/xle.test.mjs +333 -0
  87. package/src/template.mjs +73 -0
  88. package/src/template.test.mjs +127 -0
  89. package/src/types/api.d.ts +2 -6
  90. package/src/types/base.d.ts +3 -9
  91. package/src/types/codemod.d.ts +81 -0
  92. package/src/types/component.d.ts +27 -2
  93. package/src/types/config.d.ts +70 -0
  94. package/src/types/error-codes.d.ts +5 -3
  95. package/src/types/index.d.ts +0 -1
  96. package/src/types/integration.d.ts +29 -0
  97. package/src/types/swizzle.d.ts +9 -2
  98. package/src/types/template-api.d.ts +54 -0
  99. package/src/types/template.d.ts +10 -7
  100. package/src/types/upgrade.d.ts +29 -0
  101. package/src/types/validate-integration.d.ts +24 -0
  102. package/src/utils/github.mjs +0 -252
  103. package/src/utils/interactive.mjs +2 -2
  104. package/templates/blocks/components/Card/ClickableCardWithNestedButton.doc.mjs +1 -1
  105. package/templates/blocks/components/Card/SelectableCardMulti.doc.mjs +1 -1
  106. package/templates/blocks/components/ChatComposerDrawer/ChatComposerDrawerWithProgress.tsx +1 -1
  107. package/templates/blocks/components/ChatDictationButton/ChatDictationButtonShowcase.tsx +3 -3
  108. package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyShowcase.doc.mjs +15 -0
  109. package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyShowcase.tsx +26 -0
  110. package/templates/blocks/components/DateInput/DateInputDateRange.doc.mjs +2 -2
  111. package/templates/blocks/components/DateRangeInput/DateRangeInputWithPresets.doc.mjs +14 -0
  112. package/templates/blocks/components/DateRangeInput/DateRangeInputWithPresets.tsx +46 -0
  113. package/templates/blocks/components/DateRangeInput/DateRangeInputWithValidation.doc.mjs +14 -0
  114. package/templates/blocks/components/DateRangeInput/DateRangeInputWithValidation.tsx +52 -0
  115. package/templates/blocks/components/DateTimeInput/DateTimeInputWithValidation.doc.mjs +14 -0
  116. package/templates/blocks/components/DateTimeInput/DateTimeInputWithValidation.tsx +43 -0
  117. package/templates/blocks/components/FileInput/FileInputShowcase.tsx +4 -2
  118. package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.doc.mjs +1 -1
  119. package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.tsx +9 -4
  120. package/templates/blocks/components/LinkProvider/LinkProviderCustomLink.doc.mjs +14 -0
  121. package/templates/blocks/components/LinkProvider/LinkProviderCustomLink.tsx +43 -0
  122. package/templates/blocks/components/MultiSelector/MultiSelectorShowcase.tsx +6 -2
  123. package/templates/blocks/components/NumberInput/NumberInputShowcase.tsx +6 -2
  124. package/templates/blocks/components/Outline/OutlineShowcase.doc.mjs +15 -0
  125. package/templates/blocks/components/Outline/OutlineShowcase.tsx +22 -0
  126. package/templates/blocks/components/RadioList/RadioListShowcase.tsx +8 -1
  127. package/templates/blocks/components/SegmentedControl/SegmentedControlShowcase.tsx +3 -1
  128. package/templates/blocks/components/Selector/SelectorShowcase.tsx +4 -1
  129. package/templates/blocks/components/Slider/SliderShowcase.tsx +10 -1
  130. package/templates/blocks/components/Tab/TabShowcase.tsx +3 -1
  131. package/templates/blocks/components/Tab/TabWithSelectedIcon.doc.mjs +13 -0
  132. package/templates/blocks/components/Tab/TabWithSelectedIcon.tsx +39 -0
  133. package/templates/blocks/components/TabList/TabListShowcase.tsx +3 -1
  134. package/templates/blocks/components/TabMenu/TabMenuShowcase.tsx +3 -1
  135. package/templates/blocks/components/Table/ColumnResizeHookUsage.doc.mjs +14 -0
  136. package/templates/blocks/components/Table/ColumnResizeHookUsage.tsx +59 -0
  137. package/templates/blocks/components/Table/StickyColumnsHookUsage.doc.mjs +14 -0
  138. package/templates/blocks/components/Table/StickyColumnsHookUsage.tsx +104 -0
  139. package/templates/blocks/components/Text/TextColors.tsx +20 -5
  140. package/templates/blocks/components/TextArea/TextAreaShowcase.tsx +4 -2
  141. package/templates/blocks/components/TextInput/TextInputShowcase.tsx +4 -2
  142. package/templates/blocks/components/Thumbnail/ThumbnailDisabled.tsx +11 -6
  143. package/templates/blocks/components/Thumbnail/ThumbnailGallery.tsx +43 -7
  144. package/templates/blocks/components/Thumbnail/ThumbnailRemovable.tsx +40 -3
  145. package/templates/blocks/components/Thumbnail/ThumbnailShowcase.tsx +6 -5
  146. package/templates/blocks/components/Thumbnail/ThumbnailStates.tsx +11 -6
  147. package/templates/blocks/components/Timestamp/TimestampColors.tsx +2 -2
  148. package/templates/blocks/components/ToggleButton/ToggleButtonGroup.doc.mjs +1 -1
  149. package/templates/blocks/components/Tokenizer/TokenizerShowcase.tsx +8 -6
  150. package/templates/blocks/components/Toolbar/ToolbarTableFilter.doc.mjs +2 -2
  151. package/templates/blocks/components/Toolbar/ToolbarTableFilter.tsx +23 -14
  152. package/templates/blocks/components/Typeahead/TypeaheadShowcase.tsx +6 -2
  153. package/templates/pages/shell-nav/page.tsx +321 -0
  154. package/templates/pages/shell-nav/template.doc.mjs +12 -0
  155. package/templates/pages/shell-side-nav/page.tsx +242 -0
  156. package/templates/pages/shell-side-nav/template.doc.mjs +12 -0
  157. package/templates/pages/shell-top-nav/page.tsx +224 -0
  158. package/templates/pages/shell-top-nav/template.doc.mjs +12 -0
  159. package/templates/pages/theme-showcase/page.tsx +8 -19
  160. package/templates/themes/butter/butterTheme.ts +916 -0
  161. package/templates/themes/butter/icons.tsx +77 -0
  162. package/templates/themes/chocolate/chocolateTheme.ts +230 -0
  163. package/templates/themes/chocolate/icons.tsx +77 -0
  164. package/templates/themes/gothic/gothicTheme.ts +657 -0
  165. package/templates/themes/gothic/icons.tsx +77 -0
  166. package/templates/themes/manifest.json +90 -0
  167. package/templates/themes/matcha/icons.tsx +67 -0
  168. package/templates/themes/matcha/matchaTheme.ts +247 -0
  169. package/templates/themes/neutral/icons.tsx +77 -0
  170. package/templates/themes/neutral/neutralTheme.ts +603 -0
  171. package/templates/themes/stone/icons.tsx +77 -0
  172. package/templates/themes/stone/stoneTheme.ts +652 -0
  173. package/templates/themes/y2k/icons.tsx +67 -0
  174. package/templates/themes/y2k/y2kTheme.ts +617 -0
  175. package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-config-surfaces.test.mjs +0 -116
  176. package/src/codemods/transforms/v0.1.0/migrate-xds-config-surfaces.mjs +0 -230
  177. package/src/commands/gap-report.mjs +0 -456
  178. package/src/commands/gap-report.test.mjs +0 -163
  179. package/src/commands/swizzle-gap-safety.test.mjs +0 -273
  180. package/src/lib/config.mjs +0 -86
  181. package/src/lib/config.test.mjs +0 -42
  182. package/src/types/gap-report.d.ts +0 -29
  183. package/templates/blocks/components/MoreMenu/MoreMenuInToolbar.doc.mjs +0 -14
  184. package/templates/blocks/components/MoreMenu/MoreMenuInToolbar.tsx +0 -57
  185. /package/src/codemods/transforms/{v0.0.15 → v0.1.0}/__tests__/drop-xds-prefix-imports.test.mjs +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,55 @@
1
1
  # @xds/cli
2
2
 
3
+ # 0.1.2
4
+
5
+ #### Breaking Changes
6
+
7
+ - `Text`, `Heading`, `Link`, and `Timestamp` rename the `color="active"` value to `color="accent"`, now mapping to the dedicated `--color-text-accent` token (legible accent text ink) instead of `--color-accent`. Run `astryx upgrade` to migrate call sites automatically. (#2863)
8
+
9
+ #### New Features
10
+
11
+ - Let `astryx.config.mjs` integrations contribute package docs, gap-report hooks, template fetching hooks, upgrade codemods, and post-codemod hooks.
12
+ - Add `astryx theme add <slug> [path]` (and `astryx theme list`) to scaffold a theme's source into your project as editable files you own, with theme sources bundled into the CLI
13
+
14
+ #### Fixes
15
+
16
+ - align `astryx init` theme instructions with the runtime built-theme recommendation (#3080)
17
+ `astryx init` now points users at the pre-built theme path (`@astryxdesign/theme-neutral/built` + `theme.css`) and the base CSS imports, matching the runtime `<Theme>` console guidance, instead of the slower runtime style-injection import that left apps unstyled.
18
+ - `astryx theme build` now derives every output file (.css/.js/.d.ts) from the theme name so they share one naming scheme, shows import paths as bare `./<name>` specifiers (instead of a cwd-rooted `./src/...` path that was wrong when your file already lives under src/), and no longer warns about the `variant` prop on `card`
19
+
20
+ #### Documentation
21
+
22
+ - Rename the ClickableCard and SelectableCard examples to follow the "Component — Variant" title convention (`Clickable Card — Nested Button`, `Selectable Card — Multi-select`), and add playground defaults to both card docs so their docsite previews show realistic card content (#2877)
23
+ - Declare playground scaffolds for the Chat sub-components so they preview at a realistic width (ChatComposer and ChatComposerDrawer wrap in a sized container, and the drawer seeds default content), and drop the redundant visible value label from the ChatComposerDrawer "With Progress" example while keeping the accessible label (#2877)
24
+ - Rename the DateInput "Date Range" example to "Min/Max Constraints" — it demos a single input constrained to a min/max window, not a date-range picker (#2692)
25
+ - Wire local state into more showcase examples that were frozen (static value + no-op onChange): TextInput, TextArea, NumberInput, SegmentedControl, RadioList, Tab, TabList, and TabMenu. Follows the same fix as the Slider/Selector/MultiSelector showcases so the docsite previews are actually interactive
26
+ - Wire local state into the Typeahead, Tokenizer, and FileInput showcase examples (static value + no-op onChange → frozen previews). Completes the interactive-showcase fixes started for Slider/Selector/MultiSelector (#3187-#3189) and the input/tab batch
27
+ - Wire local state into the Slider, Selector, and MultiSelector showcase examples so they are interactive — they were controlled components with a static value and a no-op/missing onChange, so the docsite previews appeared frozen (#3187, #3188, #3189)
28
+ - Add a LinkProvider example block showing how to swap in a framework router link (e.g. Next.js Link) for client-side routing (#2733)
29
+ - Add a showcase block for Outline so its docs page has a hero preview, alongside the existing example blocks (#2871)
30
+ - Remove the "MoreMenu — In Toolbar" example block — it rendered incorrectly and was redundant with the other MoreMenu examples (#2870)
31
+ - Add rendered example blocks for the two column-axis Table plugin hooks,
32
+ shown on their own subcomponent pages:
33
+ - Move the "ToggleButton — Group" example to the ToggleButtonGroup page, where it belongs (it demonstrates grouped toggle behavior) (#2842)
34
+ - Make the Toolbar "Table Filter" example use real Selector controls for its Status and Priority filters instead of buttons styled to look like dropdowns, and add meaningful playground defaults plus richer slot options (buttons, icon buttons, tabs, segmented controls, selectors) to the Toolbar docs (#2877).
35
+
36
+ #### Other Changes
37
+
38
+ - `useTableStickyColumns — Pinned Columns` (on /components/useTableStickyColumns)
39
+ - `useTableColumnResize — Draggable Columns` (on /components/useTableColumnResize)
40
+
41
+ #### Contributors
42
+
43
+ Thanks to everyone who contributed to this release:
44
+
45
+ - @cixzhang
46
+ - @durvesh1992
47
+ - @ejhammond
48
+ - @humbertovirtudes
49
+ - @rubyycheung
50
+
51
+ ---
52
+
3
53
  # 0.1.1
4
54
 
5
55
  #### New Features
@@ -53,7 +53,7 @@ function App() {
53
53
  },
54
54
  {
55
55
  type: 'prose',
56
- text: 'Each theme ships as its own npm package. Install the one you want, then wrap your app in `<Theme>` the same pattern works for every theme; just swap the package and import name.',
56
+ text: 'Each theme ships as its own npm package. Install the one you want, then wrap your app in `<Theme>`. The same pattern works for every theme; just swap the package and import name.',
57
57
  },
58
58
  {
59
59
  type: 'prose',
@@ -261,7 +261,7 @@ const brandTheme = defineTheme({
261
261
  content: [
262
262
  {
263
263
  type: 'prose',
264
- text: 'The `components` field in defineTheme uses semantic component keys and style keys not raw CSS selectors. Use `base` for all instances, `variant:value` or `stateName` for specific props/states, and let the theme pipeline choose the underlying selector. For raw external CSS escape hatches, prefer the data-attribute selector surface documented in `astryx docs styling`.',
264
+ text: 'The `components` field in defineTheme uses semantic component keys and style keys, not raw CSS selectors. Use `base` for all instances, `variant:value` or `stateName` for specific props/states, and let the theme pipeline choose the underlying selector. For raw external CSS escape hatches, prefer the data-attribute selector surface documented in `astryx docs styling`.',
265
265
  },
266
266
  {
267
267
  type: 'code',
@@ -34,11 +34,11 @@ export const docs = {
34
34
  type: 'code',
35
35
  lang: 'text',
36
36
  label: 'Paste this into your AI',
37
- code: 'Install @astryxdesign/cli and run `npx astryx agent-docs` to set up your Astryx context. Read the generated file.',
37
+ code: 'Install @astryxdesign/cli and run `npx astryx init --features agents` to set up your Astryx context. Read the generated file.',
38
38
  },
39
39
  {
40
40
  type: 'prose',
41
- text: 'That\'s it. The agent-docs command generates everything your AI needs (component index, behavioral rules, CLI reference) pulled from your installed version. After a version bump, run it again to update in place.',
41
+ text: 'That\'s it. The `init --features agents` command generates everything your AI needs (component index, behavioral rules, CLI reference) pulled from your installed version. After a version bump, run it again to update in place.',
42
42
  },
43
43
  {
44
44
  type: 'prose',
@@ -48,9 +48,9 @@ export const docs = {
48
48
  type: 'code',
49
49
  lang: 'bash',
50
50
  label: 'Manual options',
51
- code: `npx astryx agent-docs --agent claude # CLAUDE.md
52
- npx astryx agent-docs --agent cursor # .cursorrules
53
- npx astryx agent-docs --agent codex # AGENTS.md (Copilot, Codex, etc.)`,
51
+ code: `npx astryx init --features agents --agent claude # CLAUDE.md
52
+ npx astryx init --features agents --agent cursor # .cursorrules
53
+ npx astryx init --features agents --agent codex # AGENTS.md (Copilot, Codex, etc.)`,
54
54
  },
55
55
  ],
56
56
  },
@@ -88,7 +88,7 @@ npx astryx agent-docs --agent codex # AGENTS.md (Copilot, Codex, etc.)`,
88
88
  lang: 'bash',
89
89
  label: 'Install as a Cursor user rule',
90
90
  code: `mkdir -p ~/.cursor/rules
91
- npx astryx agent-docs --agent-docs-path ~/.cursor/rules/xds.mdc`,
91
+ npx astryx init --features agents --agent-docs-path ~/.cursor/rules/xds.mdc`,
92
92
  },
93
93
  ],
94
94
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.2-canary.043f3fd",
4
4
  "displayName": "CLI",
5
5
  "description": "Scaffold projects, browse templates, generate themes, and get agent-ready docs from the command line.",
6
6
  "author": "Meta Open Source",
@@ -38,6 +38,26 @@
38
38
  "./api": {
39
39
  "types": "./src/types/api.d.ts",
40
40
  "import": "./src/api/index.mjs"
41
+ },
42
+ "./config": {
43
+ "types": "./src/types/config.d.ts",
44
+ "import": "./src/config.mjs"
45
+ },
46
+ "./integration": {
47
+ "types": "./src/types/integration.d.ts",
48
+ "import": "./src/integration.mjs"
49
+ },
50
+ "./template": {
51
+ "types": "./src/types/template-api.d.ts",
52
+ "import": "./src/template.mjs"
53
+ },
54
+ "./codemod": {
55
+ "types": "./src/types/codemod.d.ts",
56
+ "import": "./src/codemod.mjs"
57
+ },
58
+ "./xle": {
59
+ "types": "./src/lib/xle/browser.d.ts",
60
+ "import": "./src/lib/xle/browser.mjs"
41
61
  }
42
62
  },
43
63
  "files": [
@@ -51,12 +71,14 @@
51
71
  "@clack/prompts": "^1.5.1",
52
72
  "commander": "^12.1.0",
53
73
  "jiti": "^2.7.0",
54
- "jscodeshift": "^17.3.0"
74
+ "jscodeshift": "^17.3.0",
75
+ "zod": "^4.4.3"
55
76
  },
56
77
  "peerDependencies": {
57
- "@astryxdesign/core": "*",
58
- "@astryxdesign/lab": "*",
59
- "@astryxdesign/theme-neutral": "*"
78
+ "@astryxdesign/core": "0.1.2-canary.043f3fd",
79
+ "@astryxdesign/lab": "0.1.2-canary.043f3fd",
80
+ "@astryxdesign/theme-neutral": "0.1.2-canary.043f3fd",
81
+ "gpt-tokenizer": "^2.0.0"
60
82
  },
61
83
  "peerDependenciesMeta": {
62
84
  "@astryxdesign/core": {
@@ -70,9 +92,10 @@
70
92
  }
71
93
  },
72
94
  "devDependencies": {
73
- "@astryxdesign/core": "*",
74
- "@astryxdesign/lab": "*",
75
- "@astryxdesign/theme-neutral": "*"
95
+ "@astryxdesign/core": "0.1.2-canary.043f3fd",
96
+ "@astryxdesign/lab": "0.1.2-canary.043f3fd",
97
+ "@astryxdesign/theme-neutral": "0.1.2-canary.043f3fd",
98
+ "gpt-tokenizer": "^2.0.0"
76
99
  },
77
100
  "scripts": {
78
101
  "astryx": "node bin/astryx.mjs",
@@ -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
  }