@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
@@ -3,40 +3,55 @@
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';
15
17
  import * as path from 'node:path';
16
18
  import * as p from '@clack/prompts';
17
19
  import {findCoreDir, listComponents} from '../utils/paths.mjs';
18
- import {assertWithin, PathSafetyError, isNonInteractive} from '../utils/path-safety.mjs';
19
- import {isInteractive} from '../utils/interactive.mjs';
20
+ import {
21
+ assertWithin,
22
+ PathSafetyError,
23
+ isNonInteractive,
24
+ } from '../utils/path-safety.mjs';
20
25
  import {jsonOut, humanLog} from '../lib/json.mjs';
21
26
  import {cliError} from '../lib/cli-error.mjs';
22
27
  import {ERROR_CODES} from '../lib/error-codes.mjs';
28
+ import {checkGhCli} from '../utils/github.mjs';
29
+ import {Project} from '../lib/project.mjs';
23
30
  import {
24
- buildGapReportPreview,
25
- checkGhCli,
26
- createGapReport,
27
- loadGapReportConfig,
28
- GAP_CATEGORIES,
29
- } from '../utils/github.mjs';
30
- 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';
31
38
 
32
39
  /**
33
- * Rewrite relative imports that point outside the component directory
34
- * 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.
35
43
  *
36
- * e.g. '../theme/tokens.stylex' -> '@astryxdesign/core/theme'
44
+ * e.g. with ownerPackage '@astryxdesign/core':
45
+ * '../theme/tokens.stylex' -> '@astryxdesign/core/theme'
37
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]
38
53
  */
39
- export function rewriteImports(content) {
54
+ export function rewriteImports(content, ownerPackage = CORE_PACKAGE) {
40
55
  // Match import/export from statements with relative paths going up
41
56
  return content.replace(
42
57
  /(from\s+['"])(\.\.\/.+?)(['"])/g,
@@ -47,12 +62,42 @@ export function rewriteImports(content) {
47
62
  const parts = importPath.replace(/^\.\.\//, '').split('/');
48
63
  const topDir = parts[0];
49
64
 
50
- // Map to @astryxdesign/core subpath
51
- return `${prefix}@astryxdesign/core/${topDir}${suffix}`;
65
+ // Map to the owner package subpath
66
+ return `${prefix}${ownerPackage}/${topDir}${suffix}`;
52
67
  },
53
68
  );
54
69
  }
55
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
+
56
101
  function isCancel(value) {
57
102
  if (p.isCancel(value)) {
58
103
  p.cancel('Cancelled.');
@@ -61,26 +106,85 @@ function isCancel(value) {
61
106
  return value;
62
107
  }
63
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
+
64
181
  export function registerSwizzle(program) {
65
182
  program
66
183
  .command('swizzle [component]')
67
184
  .description('Copy component source for customization')
68
185
  .option('--output <dir>', 'Output directory', './components/astryx')
186
+ .option('--package <pkg>', 'Scope to a specific owning package')
69
187
  .option('--list', 'List available components')
70
- .option('--gap <reason>', 'File a gap report explaining why you swizzled')
71
- .option('--gap-category <category>', 'Gap category (for --gap mode)')
72
- .option(
73
- '--no-report',
74
- 'Suppress all gap reporting (interactive prompt AND --gap auto-filing)',
75
- )
76
- .option(
77
- '--dry-run',
78
- 'For --gap: print the issue that would be filed without contacting GitHub. Default in non-TTY / --json mode.',
79
- )
80
- .option(
81
- '--commit',
82
- 'For --gap: actually file the issue. Required in non-interactive mode.',
83
- )
84
188
  .option('-f, --overwrite', 'Overwrite existing files without prompting')
85
189
  .action(async (component, options) => {
86
190
  const coreDir = findCoreDir(process.cwd());
@@ -104,21 +208,81 @@ export function registerSwizzle(program) {
104
208
  }
105
209
  humanLog(`\nUsage: astryx swizzle <component>\n`);
106
210
  humanLog('Example: astryx swizzle Button');
107
- humanLog(' astryx swizzle XDSButton (XDS prefix also works)\n');
211
+ humanLog(
212
+ ' astryx swizzle XDSButton (XDS prefix also works)\n',
213
+ );
108
214
  return;
109
215
  }
110
216
 
111
217
  const dirName = component.replace(/^XDS/, '');
112
- const componentDir = path.join(coreDir, 'src', dirName);
113
218
 
114
- 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) {
237
+ cliError(`Component "${component}" not found.`, {
238
+ suggestions: components.slice(0, 10).map(n => ({name: n})),
239
+ code: ERROR_CODES.ERR_UNKNOWN_COMPONENT,
240
+ });
241
+ return;
242
+ }
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)) {
115
277
  cliError(
116
- `Component "${component}" not found.`,
117
- {suggestions: components.slice(0, 10).map((n) => ({name: n})), code: ERROR_CODES.ERR_UNKNOWN_COMPONENT},
278
+ `No source found for "${dirName}" in package "${owner.package}".`,
279
+ {code: ERROR_CODES.ERR_NO_SOURCE},
118
280
  );
119
281
  return;
120
282
  }
121
283
 
284
+ const componentDir = owner.sourceDir;
285
+
122
286
  // Path-safety: --output must resolve inside cwd. Reject absolute
123
287
  // paths and `..` traversal up front, before any directory is created.
124
288
  let outputBase;
@@ -138,7 +302,7 @@ export function registerSwizzle(program) {
138
302
  // Pre-flight overwrite check: collect files we'd write and detect
139
303
  // collisions before mkdir/writeFile so we never half-clobber.
140
304
  const sourceFiles = fs.readdirSync(componentDir).filter(file => {
141
- if (file.includes('.test.') || file === 'README.md') return false;
305
+ if (isExcludedFromCopy(file)) return false;
142
306
  const stat = fs.statSync(path.join(componentDir, file));
143
307
  return stat.isFile();
144
308
  });
@@ -172,13 +336,13 @@ export function registerSwizzle(program) {
172
336
 
173
337
  fs.mkdirSync(outputDir, {recursive: true});
174
338
 
175
- // Copy all non-test, non-README files
339
+ // Copy all non-test, non-doc, non-README files
176
340
  const files = fs.readdirSync(componentDir);
177
341
  let copied = 0;
178
342
 
179
343
  for (const file of files) {
180
- // Skip test files and README
181
- if (file.includes('.test.') || file === 'README.md') continue;
344
+ // Skip test files, doc files, and README
345
+ if (isExcludedFromCopy(file)) continue;
182
346
 
183
347
  const srcPath = path.join(componentDir, file);
184
348
  const stat = fs.statSync(srcPath);
@@ -186,9 +350,9 @@ export function registerSwizzle(program) {
186
350
 
187
351
  let content = fs.readFileSync(srcPath, 'utf-8');
188
352
 
189
- // Rewrite imports for .ts/.tsx files
353
+ // Rewrite escaping imports for .ts/.tsx files to the owner package.
190
354
  if (file.endsWith('.ts') || file.endsWith('.tsx')) {
191
- content = rewriteImports(content);
355
+ content = rewriteImports(content, owner.ownerPackage);
192
356
  }
193
357
 
194
358
  fs.writeFileSync(path.join(outputDir, file), content);
@@ -196,194 +360,47 @@ export function registerSwizzle(program) {
196
360
  }
197
361
 
198
362
  const relOutput = path.relative(process.cwd(), outputDir);
199
- const copiedFiles = files.filter(f => !f.includes('.test.') && f !== 'README.md' && fs.statSync(path.join(componentDir, f)).isFile());
200
-
201
- // --- Gap reporting ---
202
-
203
- const gapConfig = loadGapReportConfig();
204
- let gapReportUrl = null;
205
- let gapDryRunPreview = null;
363
+ const copiedFiles = files.filter(
364
+ f =>
365
+ !isExcludedFromCopy(f) &&
366
+ fs.statSync(path.join(componentDir, f)).isFile(),
367
+ );
206
368
 
207
- // CRITICAL: --no-report must suppress BOTH the interactive prompt AND
208
- // the --gap auto-file path. Previously --gap bypassed --no-report.
209
- // commander sets options.report to false for `--no-report`.
210
- const reportingSuppressed = options.report === false;
369
+ const feedback = buildFeedback(dirName, owner.issuesUrl);
211
370
 
212
- if (options.gap && !reportingSuppressed && gapConfig.enabled) {
213
- const category = options.gapCategory || 'other';
214
- const previewArgs = {
371
+ if (json) {
372
+ /** @type {Record<string, unknown>} */
373
+ const payload = {
215
374
  component: dirName,
216
- category,
217
- intention: options.gap,
218
- source: 'llm-auto',
375
+ package: owner.package,
376
+ outputDir: relOutput,
377
+ filesCopied: copied,
378
+ files: copiedFiles.map(f => f),
219
379
  };
220
-
221
- const willFile = shouldActuallyFile({
222
- commit: options.commit,
223
- dryRun: options.dryRun,
224
- json,
225
- });
226
-
227
- const preview = buildGapReportPreview(previewArgs);
228
-
229
- if (!willFile) {
230
- // Dry-run: do NOT call gh. Surface what would have been filed.
231
- gapDryRunPreview = {
232
- dryRun: true,
233
- wouldFile: preview.mode !== 'disabled',
234
- mode: preview.mode,
235
- title: preview.title,
236
- body: preview.body,
237
- repo: preview.repo,
238
- command: preview.command || null,
239
- };
240
- } else if (gapConfig.command || checkGhCli()) {
241
- try {
242
- gapReportUrl = createGapReport(previewArgs);
243
- } catch (err) {
244
- if (!json)
245
- console.error(`Warning: Could not file gap report: ${err.message}`);
246
- }
247
- }
380
+ if (feedback) payload.feedback = feedback;
381
+ return jsonOut('swizzle.copy', payload);
248
382
  }
249
383
 
250
- if (options.gap) {
251
- if (json)
252
- return jsonOut('swizzle.copy', {
253
- component: dirName,
254
- outputDir: relOutput,
255
- filesCopied: copied,
256
- files: copiedFiles.map(f => f),
257
- gapReport: gapReportUrl,
258
- gapReportDryRun: gapDryRunPreview,
259
- gapReportSuppressed: reportingSuppressed || !gapConfig.enabled,
260
- });
261
- humanLog(`\n✓ Copied ${copied} files to ${relOutput}/\n`);
262
- humanLog('Relative imports have been rewritten to use @astryxdesign/core.');
263
- humanLog('You can now customize the component source freely.\n');
264
- if (gapReportUrl) {
265
- humanLog(`✓ Gap report filed: ${gapReportUrl}\n`);
266
- } else if (gapDryRunPreview) {
267
- humanLog(formatPreview(buildGapReportPreview({
268
- component: dirName,
269
- category: options.gapCategory || 'other',
270
- intention: options.gap,
271
- source: 'llm-auto',
272
- })));
273
- humanLog(
274
- '\n[dry-run] No gap report was filed. Re-run with --commit to file.',
275
- );
276
- } else if (reportingSuppressed) {
277
- humanLog('Gap reporting suppressed by --no-report.');
278
- } else if (!gapConfig.enabled) {
279
- humanLog('Gap reporting is disabled via configuration.');
280
- } else if (!gapConfig.command && !checkGhCli()) {
281
- humanLog('Skipping gap report: gh CLI not available.');
282
- }
283
- return;
284
- }
285
-
286
- if (json) return jsonOut('swizzle.copy', {component: dirName, outputDir: relOutput, filesCopied: copied, files: copiedFiles.map(f => f)});
287
-
288
384
  humanLog(`\n✓ Copied ${copied} files to ${relOutput}/\n`);
289
- humanLog('Relative imports have been rewritten to use @astryxdesign/core.');
290
- humanLog('You can now customize the component source freely.\n');
291
-
292
- if (reportingSuppressed || !gapConfig.enabled) {
293
- return;
294
- }
295
-
296
- // Interactive gap report prompt
297
- //
298
- // This prompt is OPTIONAL — the swizzle copy already succeeded above.
299
- // In a non-interactive context (CI, piped I/O, no TTY) we must not
300
- // block on it; skip gracefully rather than hang. Use --gap with
301
- // explicit flags for non-interactive gap reporting.
302
- if (!isInteractive()) {
303
- return;
304
- }
305
-
306
- if (!gapConfig.command && !checkGhCli()) {
307
- // Silently skip if gh isn't available and no custom command configured
308
- return;
309
- }
310
-
311
- const shouldReport = isCancel(
312
- await p.confirm({
313
- message: 'Would you like to report why you swizzled this component?',
314
- initialValue: false,
315
- }),
316
- );
317
-
318
- if (!shouldReport) return;
319
-
320
- const category = isCancel(
321
- await p.select({
322
- message: 'What kind of gap is this?',
323
- options: GAP_CATEGORIES,
324
- }),
325
- );
326
-
327
- const intention = isCancel(
328
- await p.text({
329
- message: 'What were you trying to achieve?',
330
- placeholder:
331
- 'e.g. "Need a compact variant for use in dense data tables"',
332
- validate: val => {
333
- if (!val.trim()) return 'Please describe what you were trying to do';
334
- },
335
- }),
336
- );
337
-
338
- const detail = isCancel(
339
- await p.text({
340
- message: 'Any additional context? (optional)',
341
- placeholder: 'Press Enter to skip',
342
- }),
343
- );
344
-
345
- const previewArgs = {
346
- component: dirName,
347
- category,
348
- intention: intention.trim(),
349
- detail: detail?.trim() || undefined,
350
- source: 'interactive',
351
- };
352
-
353
- const preview = buildGapReportPreview(previewArgs);
354
-
355
- p.note(
356
- `${preview.mode === 'github' ? `Repo: ${preview.repo}` : `Custom command: ${preview.command}`}\n\n` +
357
- `Title:\n ${preview.title}\n\n` +
358
- `Body:\n${preview.body
359
- .split('\n')
360
- .map(l => ` ${l}`)
361
- .join('\n')}`,
362
- 'Preview — this is exactly what will be filed',
363
- );
364
-
365
- const confirmFile = isCancel(
366
- await p.confirm({
367
- message: 'File this gap report now?',
368
- initialValue: false,
369
- }),
385
+ humanLog(
386
+ `Relative imports have been rewritten to use ${owner.ownerPackage}.`,
370
387
  );
388
+ humanLog('You can now customize the component source freely.\n');
371
389
 
372
- if (!confirmFile) {
373
- humanLog('Cancellednothing was filed.');
374
- return;
375
- }
376
-
377
- const s = p.spinner();
378
- s.start('Filing gap report');
379
-
380
- try {
381
- const url = createGapReport(previewArgs);
382
- s.stop('Gap report filed');
383
- humanLog(`✓ ${url}\n`);
384
- } catch (err) {
385
- s.stop('Failed to file gap report');
386
- 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('');
387
404
  }
388
405
  });
389
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