@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
@@ -3,12 +3,14 @@
3
3
  /**
4
4
  * @file swizzle command — Copy component source for customization
5
5
  *
6
- * Resolves component source from packages/core/src/{Component}/,
7
- * copies non-test files to the output directory, and rewrites
8
- * relative imports to use '@astryxdesign/core' package paths.
6
+ * Resolves a component's owning package (core or a configured integration),
7
+ * copies its non-test/non-doc source files to the output directory, and
8
+ * rewrites escaping relative imports to use the OWNER package's subpaths.
9
9
  *
10
- * After swizzling, optionally prompts the user to file a gap report
11
- * explaining why they needed to customize the component.
10
+ * After swizzling, prints a short maintainer feedback note pointing users at
11
+ * the owner's issue tracker so they can let the team know what gap led them to
12
+ * customize the component. The core feedback URL is routed through app config
13
+ * (`config.issuesUrl`); integration components use their manifest `issuesUrl`.
12
14
  */
13
15
 
14
16
  import * as fs from 'node:fs';
@@ -20,27 +22,36 @@ import {
20
22
  PathSafetyError,
21
23
  isNonInteractive,
22
24
  } from '../utils/path-safety.mjs';
23
- import {isInteractive} from '../utils/interactive.mjs';
24
25
  import {jsonOut, humanLog} from '../lib/json.mjs';
25
26
  import {cliError} from '../lib/cli-error.mjs';
26
27
  import {ERROR_CODES} from '../lib/error-codes.mjs';
28
+ import {checkGhCli} from '../utils/github.mjs';
29
+ import {Project} from '../lib/project.mjs';
27
30
  import {
28
- buildGapReportPreview,
29
- checkGhCli,
30
- createGapReport,
31
- loadGapReportConfig,
32
- GAP_CATEGORIES,
33
- } from '../utils/github.mjs';
34
- import {shouldActuallyFile, formatPreview} from './gap-report.mjs';
31
+ CORE_PACKAGE,
32
+ findIntegrationComponentDoc,
33
+ findIntegrationComponentSource,
34
+ } from '../lib/component-discovery.mjs';
35
+
36
+ /** Default issue tracker for maintainer feedback after swizzling. */
37
+ const DEFAULT_ISSUES_URL = 'https://github.com/facebook/astryx/issues/new';
35
38
 
36
39
  /**
37
- * Rewrite relative imports that point outside the component directory
38
- * to use @astryxdesign/core package paths.
40
+ * Rewrite relative imports that point outside the component directory to use
41
+ * the OWNER package's subpaths. Imports within the copied directory (./x) are
42
+ * left untouched.
39
43
  *
40
- * e.g. '../theme/tokens.stylex' -> '@astryxdesign/core/theme'
44
+ * e.g. with ownerPackage '@astryxdesign/core':
45
+ * '../theme/tokens.stylex' -> '@astryxdesign/core/theme'
41
46
  * '../utils/mergeProps' -> '@astryxdesign/core/utils'
47
+ *
48
+ * `ownerPackage` defaults to '@astryxdesign/core' so existing core behavior is
49
+ * unchanged; integration components pass their own owning package.
50
+ *
51
+ * @param {string} content
52
+ * @param {string} [ownerPackage]
42
53
  */
43
- export function rewriteImports(content) {
54
+ export function rewriteImports(content, ownerPackage = CORE_PACKAGE) {
44
55
  // Match import/export from statements with relative paths going up
45
56
  return content.replace(
46
57
  /(from\s+['"])(\.\.\/.+?)(['"])/g,
@@ -51,12 +62,42 @@ export function rewriteImports(content) {
51
62
  const parts = importPath.replace(/^\.\.\//, '').split('/');
52
63
  const topDir = parts[0];
53
64
 
54
- // Map to @astryxdesign/core subpath
55
- return `${prefix}@astryxdesign/core/${topDir}${suffix}`;
65
+ // Map to the owner package subpath
66
+ return `${prefix}${ownerPackage}/${topDir}${suffix}`;
56
67
  },
57
68
  );
58
69
  }
59
70
 
71
+ /**
72
+ * Build the maintainer feedback note for a swizzled component.
73
+ *
74
+ * Returns { issuesUrl, ghCommand? } or null when no issues URL is available
75
+ * (an integration that ships no `issuesUrl`). When the issues URL is a GitHub
76
+ * issues URL and the `gh` CLI is available, a ready-to-run `gh issue create`
77
+ * command is included so the user can file feedback without leaving the
78
+ * terminal.
79
+ *
80
+ * @param {string} component bare component name (used in the issue title)
81
+ * @param {string|undefined} issuesUrl owner's issue tracker URL
82
+ * @returns {{issuesUrl: string, ghCommand?: string} | null}
83
+ */
84
+ function buildFeedback(component, issuesUrl) {
85
+ if (!issuesUrl) return null;
86
+
87
+ const feedback = {issuesUrl};
88
+
89
+ // Accept any github.com/<owner>/<repo>/issues(/new)? form.
90
+ const match = issuesUrl.match(
91
+ /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/issues(?:\/new)?\/?$/,
92
+ );
93
+ if (match && checkGhCli()) {
94
+ const [, owner, repo] = match;
95
+ feedback.ghCommand = `gh issue create --repo ${owner}/${repo} --title "[${component}] Swizzle feedback"`;
96
+ }
97
+
98
+ return feedback;
99
+ }
100
+
60
101
  function isCancel(value) {
61
102
  if (p.isCancel(value)) {
62
103
  p.cancel('Cancelled.');
@@ -65,26 +106,85 @@ function isCancel(value) {
65
106
  return value;
66
107
  }
67
108
 
109
+ /**
110
+ * Load the configured integrations + core issues URL for `cwd`, swallowing any
111
+ * config errors so swizzle never hard-fails on a malformed/absent config. An
112
+ * empty list means "core only". The core issues URL is routed through the
113
+ * Project (config.issuesUrl, falling back to the default core tracker).
114
+ * @param {string} cwd
115
+ * @returns {Promise<{loadedIntegrations: Array<object>, issuesUrl: string|undefined, project: Project|null}>}
116
+ */
117
+ async function loadConfigSafely(cwd) {
118
+ try {
119
+ const project = await Project.load(cwd);
120
+ return {
121
+ loadedIntegrations: project.loadedIntegrations,
122
+ issuesUrl: project.config.issuesUrl,
123
+ project,
124
+ };
125
+ } catch {
126
+ return {loadedIntegrations: [], issuesUrl: undefined, project: null};
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Build the set of OWNER packages that provide a component with `name` across
132
+ * core + every loaded integration.
133
+ *
134
+ * Core ownership is determined by the source directory existing on disk
135
+ * (`coreDir/src/<name>`) so plain core components without a `.doc.mjs` still
136
+ * resolve as today. Integration ownership is determined by a same-stem doc file
137
+ * (`<name>.doc.{ts,mjs,js}`) via findIntegrationComponentDoc.
138
+ *
139
+ * @param {string} coreDir
140
+ * @param {Array<{name: string, components?: string, issuesUrl?: string}>} loadedIntegrations
141
+ * @param {string} name bare component name (no XDS/Astryx prefix)
142
+ * @param {string|undefined} coreIssuesUrl
143
+ * @returns {Array<{package: string, sourceDir: string|null, ownerPackage: string, issuesUrl: string|undefined}>}
144
+ */
145
+ function resolveOwners(coreDir, loadedIntegrations, name, coreIssuesUrl) {
146
+ const owners = [];
147
+
148
+ const coreComponentDir = path.join(coreDir, 'src', name);
149
+ if (fs.existsSync(coreComponentDir)) {
150
+ owners.push({
151
+ package: CORE_PACKAGE,
152
+ sourceDir: coreComponentDir,
153
+ ownerPackage: CORE_PACKAGE,
154
+ issuesUrl: coreIssuesUrl || DEFAULT_ISSUES_URL,
155
+ });
156
+ }
157
+
158
+ for (const integration of loadedIntegrations) {
159
+ const docPath = findIntegrationComponentDoc(integration, name);
160
+ if (!docPath) continue;
161
+ const sourcePath = findIntegrationComponentSource(integration, name);
162
+ owners.push({
163
+ package: integration.name,
164
+ // The component's own folder — the directory containing its source.
165
+ sourceDir: sourcePath ? path.dirname(sourcePath) : null,
166
+ ownerPackage: integration.name,
167
+ issuesUrl: integration.issuesUrl,
168
+ });
169
+ }
170
+
171
+ return owners;
172
+ }
173
+
174
+ /** Whether a filename should be excluded from the swizzle copy. */
175
+ function isExcludedFromCopy(file) {
176
+ return (
177
+ file.includes('.test.') || file.includes('.doc.') || file === 'README.md'
178
+ );
179
+ }
180
+
68
181
  export function registerSwizzle(program) {
69
182
  program
70
183
  .command('swizzle [component]')
71
184
  .description('Copy component source for customization')
72
185
  .option('--output <dir>', 'Output directory', './components/astryx')
186
+ .option('--package <pkg>', 'Scope to a specific owning package')
73
187
  .option('--list', 'List available components')
74
- .option('--gap <reason>', 'File a gap report explaining why you swizzled')
75
- .option('--gap-category <category>', 'Gap category (for --gap mode)')
76
- .option(
77
- '--no-report',
78
- 'Suppress all gap reporting (interactive prompt AND --gap auto-filing)',
79
- )
80
- .option(
81
- '--dry-run',
82
- 'For --gap: print the issue that would be filed without contacting GitHub. Default in non-TTY / --json mode.',
83
- )
84
- .option(
85
- '--commit',
86
- 'For --gap: actually file the issue. Required in non-interactive mode.',
87
- )
88
188
  .option('-f, --overwrite', 'Overwrite existing files without prompting')
89
189
  .action(async (component, options) => {
90
190
  const coreDir = findCoreDir(process.cwd());
@@ -115,9 +215,25 @@ export function registerSwizzle(program) {
115
215
  }
116
216
 
117
217
  const dirName = component.replace(/^XDS/, '');
118
- const componentDir = path.join(coreDir, 'src', dirName);
119
218
 
120
- if (!fs.existsSync(componentDir)) {
219
+ // Resolve the component's owning package(s) across core + integrations.
220
+ const {loadedIntegrations, project} = await loadConfigSafely(
221
+ process.cwd(),
222
+ );
223
+ // Core feedback URL is routed through the Project (config.issuesUrl,
224
+ // falling back to the default core tracker). When config load failed,
225
+ // resolveOwners applies the same default fallback.
226
+ const coreIssuesUrl = project
227
+ ? project.issuesUrl({package: CORE_PACKAGE})
228
+ : undefined;
229
+ const allOwners = resolveOwners(
230
+ coreDir,
231
+ loadedIntegrations,
232
+ dirName,
233
+ coreIssuesUrl,
234
+ );
235
+
236
+ if (allOwners.length === 0) {
121
237
  cliError(`Component "${component}" not found.`, {
122
238
  suggestions: components.slice(0, 10).map(n => ({name: n})),
123
239
  code: ERROR_CODES.ERR_UNKNOWN_COMPONENT,
@@ -125,6 +241,48 @@ export function registerSwizzle(program) {
125
241
  return;
126
242
  }
127
243
 
244
+ let owner;
245
+ if (options.package) {
246
+ owner = allOwners.find(o => o.package === options.package);
247
+ if (!owner) {
248
+ cliError(
249
+ `Component "${dirName}" is not provided by package "${options.package}".`,
250
+ {
251
+ suggestions: allOwners.map(o => ({
252
+ name: o.package,
253
+ reason: 'provides this component',
254
+ })),
255
+ code: ERROR_CODES.ERR_UNKNOWN_COMPONENT,
256
+ },
257
+ );
258
+ return;
259
+ }
260
+ } else if (allOwners.length > 1) {
261
+ cliError(
262
+ `Component "${dirName}" is provided by multiple packages. Re-run with --package <pkg> to choose one.`,
263
+ {
264
+ suggestions: allOwners.map(o => ({
265
+ name: o.package,
266
+ reason: 'provides this component',
267
+ })),
268
+ code: ERROR_CODES.ERR_AMBIGUOUS_COMPONENT,
269
+ },
270
+ );
271
+ return;
272
+ } else {
273
+ owner = allOwners[0];
274
+ }
275
+
276
+ if (!owner.sourceDir || !fs.existsSync(owner.sourceDir)) {
277
+ cliError(
278
+ `No source found for "${dirName}" in package "${owner.package}".`,
279
+ {code: ERROR_CODES.ERR_NO_SOURCE},
280
+ );
281
+ return;
282
+ }
283
+
284
+ const componentDir = owner.sourceDir;
285
+
128
286
  // Path-safety: --output must resolve inside cwd. Reject absolute
129
287
  // paths and `..` traversal up front, before any directory is created.
130
288
  let outputBase;
@@ -144,7 +302,7 @@ export function registerSwizzle(program) {
144
302
  // Pre-flight overwrite check: collect files we'd write and detect
145
303
  // collisions before mkdir/writeFile so we never half-clobber.
146
304
  const sourceFiles = fs.readdirSync(componentDir).filter(file => {
147
- if (file.includes('.test.') || file === 'README.md') return false;
305
+ if (isExcludedFromCopy(file)) return false;
148
306
  const stat = fs.statSync(path.join(componentDir, file));
149
307
  return stat.isFile();
150
308
  });
@@ -178,13 +336,13 @@ export function registerSwizzle(program) {
178
336
 
179
337
  fs.mkdirSync(outputDir, {recursive: true});
180
338
 
181
- // Copy all non-test, non-README files
339
+ // Copy all non-test, non-doc, non-README files
182
340
  const files = fs.readdirSync(componentDir);
183
341
  let copied = 0;
184
342
 
185
343
  for (const file of files) {
186
- // Skip test files and README
187
- if (file.includes('.test.') || file === 'README.md') continue;
344
+ // Skip test files, doc files, and README
345
+ if (isExcludedFromCopy(file)) continue;
188
346
 
189
347
  const srcPath = path.join(componentDir, file);
190
348
  const stat = fs.statSync(srcPath);
@@ -192,9 +350,9 @@ export function registerSwizzle(program) {
192
350
 
193
351
  let content = fs.readFileSync(srcPath, 'utf-8');
194
352
 
195
- // Rewrite imports for .ts/.tsx files
353
+ // Rewrite escaping imports for .ts/.tsx files to the owner package.
196
354
  if (file.endsWith('.ts') || file.endsWith('.tsx')) {
197
- content = rewriteImports(content);
355
+ content = rewriteImports(content, owner.ownerPackage);
198
356
  }
199
357
 
200
358
  fs.writeFileSync(path.join(outputDir, file), content);
@@ -204,214 +362,45 @@ export function registerSwizzle(program) {
204
362
  const relOutput = path.relative(process.cwd(), outputDir);
205
363
  const copiedFiles = files.filter(
206
364
  f =>
207
- !f.includes('.test.') &&
208
- f !== 'README.md' &&
365
+ !isExcludedFromCopy(f) &&
209
366
  fs.statSync(path.join(componentDir, f)).isFile(),
210
367
  );
211
368
 
212
- // --- Gap reporting ---
213
-
214
- const gapConfig = await loadGapReportConfig();
215
- let gapReportUrl = null;
216
- let gapDryRunPreview = null;
369
+ const feedback = buildFeedback(dirName, owner.issuesUrl);
217
370
 
218
- // CRITICAL: --no-report must suppress BOTH the interactive prompt AND
219
- // the --gap auto-file path. Previously --gap bypassed --no-report.
220
- // commander sets options.report to false for `--no-report`.
221
- const reportingSuppressed = options.report === false;
222
-
223
- if (options.gap && !reportingSuppressed && gapConfig.enabled) {
224
- const category = options.gapCategory || 'other';
225
- const previewArgs = {
226
- component: dirName,
227
- category,
228
- intention: options.gap,
229
- source: 'llm-auto',
230
- };
231
-
232
- const willFile = shouldActuallyFile({
233
- commit: options.commit,
234
- dryRun: options.dryRun,
235
- json,
236
- });
237
-
238
- const preview = await buildGapReportPreview(previewArgs);
239
-
240
- if (!willFile) {
241
- // Dry-run: do NOT call gh. Surface what would have been filed.
242
- gapDryRunPreview = {
243
- dryRun: true,
244
- wouldFile: preview.mode !== 'disabled',
245
- mode: preview.mode,
246
- title: preview.title,
247
- body: preview.body,
248
- repo: preview.repo,
249
- command: preview.command || null,
250
- };
251
- } else if (gapConfig.command || checkGhCli()) {
252
- try {
253
- gapReportUrl = await createGapReport(previewArgs);
254
- } catch (err) {
255
- if (!json)
256
- console.error(
257
- `Warning: Could not file gap report: ${err.message}`,
258
- );
259
- }
260
- }
261
- }
262
-
263
- if (options.gap) {
264
- if (json)
265
- return jsonOut('swizzle.copy', {
266
- component: dirName,
267
- outputDir: relOutput,
268
- filesCopied: copied,
269
- files: copiedFiles.map(f => f),
270
- gapReport: gapReportUrl,
271
- gapReportDryRun: gapDryRunPreview,
272
- gapReportSuppressed: reportingSuppressed || !gapConfig.enabled,
273
- });
274
- humanLog(`\n✓ Copied ${copied} files to ${relOutput}/\n`);
275
- humanLog(
276
- 'Relative imports have been rewritten to use @astryxdesign/core.',
277
- );
278
- humanLog('You can now customize the component source freely.\n');
279
- if (gapReportUrl) {
280
- humanLog(`✓ Gap report filed: ${gapReportUrl}\n`);
281
- } else if (gapDryRunPreview) {
282
- humanLog(
283
- formatPreview(
284
- await buildGapReportPreview({
285
- component: dirName,
286
- category: options.gapCategory || 'other',
287
- intention: options.gap,
288
- source: 'llm-auto',
289
- }),
290
- ),
291
- );
292
- humanLog(
293
- '\n[dry-run] No gap report was filed. Re-run with --commit to file.',
294
- );
295
- } else if (reportingSuppressed) {
296
- humanLog('Gap reporting suppressed by --no-report.');
297
- } else if (!gapConfig.enabled) {
298
- humanLog('Gap reporting is disabled via configuration.');
299
- } else if (!gapConfig.command && !checkGhCli()) {
300
- humanLog('Skipping gap report: gh CLI not available.');
301
- }
302
- return;
303
- }
304
-
305
- if (json)
306
- return jsonOut('swizzle.copy', {
371
+ if (json) {
372
+ /** @type {Record<string, unknown>} */
373
+ const payload = {
307
374
  component: dirName,
375
+ package: owner.package,
308
376
  outputDir: relOutput,
309
377
  filesCopied: copied,
310
378
  files: copiedFiles.map(f => f),
311
- });
379
+ };
380
+ if (feedback) payload.feedback = feedback;
381
+ return jsonOut('swizzle.copy', payload);
382
+ }
312
383
 
313
384
  humanLog(`\n✓ Copied ${copied} files to ${relOutput}/\n`);
314
385
  humanLog(
315
- 'Relative imports have been rewritten to use @astryxdesign/core.',
386
+ `Relative imports have been rewritten to use ${owner.ownerPackage}.`,
316
387
  );
317
388
  humanLog('You can now customize the component source freely.\n');
318
389
 
319
- if (reportingSuppressed || !gapConfig.enabled) {
320
- return;
321
- }
322
-
323
- // Interactive gap report prompt
324
- //
325
- // This prompt is OPTIONAL — the swizzle copy already succeeded above.
326
- // In a non-interactive context (CI, piped I/O, no TTY) we must not
327
- // block on it; skip gracefully rather than hang. Use --gap with
328
- // explicit flags for non-interactive gap reporting.
329
- if (!isInteractive()) {
330
- return;
331
- }
332
-
333
- if (!gapConfig.command && !checkGhCli()) {
334
- // Silently skip if gh isn't available and no custom command configured
335
- return;
336
- }
337
-
338
- const shouldReport = isCancel(
339
- await p.confirm({
340
- message: 'Would you like to report why you swizzled this component?',
341
- initialValue: false,
342
- }),
343
- );
344
-
345
- if (!shouldReport) return;
346
-
347
- const category = isCancel(
348
- await p.select({
349
- message: 'What kind of gap is this?',
350
- options: GAP_CATEGORIES,
351
- }),
352
- );
353
-
354
- const intention = isCancel(
355
- await p.text({
356
- message: 'What were you trying to achieve?',
357
- placeholder:
358
- 'e.g. "Need a compact variant for use in dense data tables"',
359
- validate: val => {
360
- if (!val.trim())
361
- return 'Please describe what you were trying to do';
362
- },
363
- }),
364
- );
365
-
366
- const detail = isCancel(
367
- await p.text({
368
- message: 'Any additional context? (optional)',
369
- placeholder: 'Press Enter to skip',
370
- }),
371
- );
372
-
373
- const previewArgs = {
374
- component: dirName,
375
- category,
376
- intention: intention.trim(),
377
- detail: detail?.trim() || undefined,
378
- source: 'interactive',
379
- };
380
-
381
- const preview = await buildGapReportPreview(previewArgs);
382
-
383
- p.note(
384
- `${preview.mode === 'github' ? `Repo: ${preview.repo}` : `Custom command: ${preview.command}`}\n\n` +
385
- `Title:\n ${preview.title}\n\n` +
386
- `Body:\n${preview.body
387
- .split('\n')
388
- .map(l => ` ${l}`)
389
- .join('\n')}`,
390
- 'Preview — this is exactly what will be filed',
391
- );
392
-
393
- const confirmFile = isCancel(
394
- await p.confirm({
395
- message: 'File this gap report now?',
396
- initialValue: false,
397
- }),
398
- );
399
-
400
- if (!confirmFile) {
401
- humanLog('Cancelled — nothing was filed.');
402
- return;
403
- }
404
-
405
- const s = p.spinner();
406
- s.start('Filing gap report');
407
-
408
- try {
409
- const url = await createGapReport(previewArgs);
410
- s.stop('Gap report filed');
411
- humanLog(`✓ ${url}\n`);
412
- } catch (err) {
413
- s.stop('Failed to file gap report');
414
- console.error(`Warning: Could not file gap report: ${err.message}`);
390
+ // Maintainer feedback note. If we couldn't swizzle cleanly, the team
391
+ // wants to know — point users at the issue tracker. Skipped when the
392
+ // owning package ships no issues URL.
393
+ if (feedback) {
394
+ humanLog(
395
+ 'Customizing a component often signals a gap in the design system.',
396
+ );
397
+ humanLog('Let the maintainers know what you needed:');
398
+ if (feedback.ghCommand) {
399
+ humanLog(` ${feedback.ghCommand}`);
400
+ } else {
401
+ humanLog(` ${feedback.issuesUrl}`);
402
+ }
403
+ humanLog('');
415
404
  }
416
405
  });
417
406
  }
@@ -69,7 +69,7 @@ describe('swizzle path safety', () => {
69
69
  const {project, outside} = buildFakeRepo(tmpDir);
70
70
 
71
71
  const result = runCli(
72
- ['swizzle', 'Button', '--output', '../outside-project', '--no-report'],
72
+ ['swizzle', 'Button', '--output', '../outside-project'],
73
73
  project,
74
74
  );
75
75
 
@@ -87,7 +87,7 @@ describe('swizzle path safety', () => {
87
87
  const absTarget = path.join(tmpDir, 'absolute-target');
88
88
 
89
89
  const result = runCli(
90
- ['swizzle', 'Button', '--output', absTarget, '--no-report'],
90
+ ['swizzle', 'Button', '--output', absTarget],
91
91
  project,
92
92
  );
93
93
 
@@ -105,7 +105,7 @@ describe('swizzle path safety', () => {
105
105
 
106
106
  // Use --json to force non-interactive mode.
107
107
  const result = runCli(
108
- ['--json', 'swizzle', 'Button', '--no-report'],
108
+ ['--json', 'swizzle', 'Button'],
109
109
  project,
110
110
  );
111
111