@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.
- package/CHANGELOG.md +50 -0
- package/docs/theme.doc.mjs +2 -2
- package/docs/working-with-ai.doc.mjs +6 -6
- package/package.json +31 -8
- package/src/api/component.mjs +253 -12
- package/src/api/discover.mjs +87 -28
- package/src/api/doctor.mjs +7 -22
- package/src/api/index.mjs +1 -0
- package/src/api/layout.mjs +302 -0
- package/src/api/layout.test.mjs +241 -0
- package/src/api/template-integration.test.mjs +225 -0
- package/src/api/template.mjs +421 -122
- package/src/api/template.test.mjs +2 -0
- package/src/api/theme-add.mjs +182 -0
- package/src/api/validate-integration.mjs +370 -0
- package/src/api/validate-integration.test.mjs +222 -0
- package/src/codemod.mjs +93 -0
- package/src/codemod.test.mjs +134 -0
- package/src/codemods/__tests__/registry.test.mjs +2 -0
- package/src/codemods/__tests__/runner.test.mjs +103 -0
- package/src/codemods/integration-discovery.mjs +168 -0
- package/src/codemods/integration-discovery.test.mjs +234 -0
- package/src/codemods/integration-runner.mjs +109 -0
- package/src/codemods/registry.mjs +2 -0
- package/src/codemods/run-codemod.mjs +207 -0
- package/src/codemods/runner.mjs +73 -96
- package/src/codemods/transforms/v0.0.15/index.mjs +0 -13
- package/src/codemods/transforms/v0.1.0/__tests__/v0.1.0-ordering.test.mjs +81 -0
- package/src/codemods/transforms/{v0.0.15 → v0.1.0}/drop-xds-prefix-imports.mjs +15 -4
- package/src/codemods/transforms/v0.1.0/index.mjs +15 -6
- package/src/codemods/transforms/v0.1.2/__tests__/rename-text-color-active-to-accent.test.mjs +120 -0
- package/src/codemods/transforms/v0.1.2/index.mjs +19 -0
- package/src/codemods/transforms/v0.1.2/rename-text-color-active-to-accent.mjs +136 -0
- package/src/codemods/transforms/v0.1.3/__tests__/migrate-layout-components-to-experimental.test.mjs +360 -0
- package/src/codemods/transforms/v0.1.3/index.mjs +19 -0
- package/src/codemods/transforms/v0.1.3/migrate-layout-components-to-experimental.mjs +260 -0
- package/src/commands/build-theme.import-path.test.mjs +22 -2
- package/src/commands/build-theme.mjs +185 -16
- package/src/commands/component/index.mjs +44 -10
- package/src/commands/component-ownership.test.mjs +227 -0
- package/src/commands/discover.mjs +5 -16
- package/src/commands/doctor.test.mjs +3 -3
- package/src/commands/init.mjs +34 -8
- package/src/commands/init.next-steps.test.mjs +46 -0
- package/src/commands/json-contract.test.mjs +0 -32
- package/src/commands/layout.mjs +139 -0
- package/src/commands/swizzle.mjs +241 -224
- package/src/commands/swizzle.path-safety.test.mjs +3 -3
- package/src/commands/swizzle.routing.test.mjs +279 -0
- package/src/commands/template.mjs +33 -37
- package/src/commands/upgrade.config-ordering.test.mjs +273 -0
- package/src/commands/upgrade.integration-policy.test.mjs +204 -0
- package/src/commands/upgrade.mjs +349 -239
- package/src/commands/validate-integration.mjs +110 -0
- package/src/commands/validate-integration.test.mjs +124 -0
- package/src/config.mjs +18 -0
- package/src/config.test.mjs +100 -0
- package/src/index.mjs +13 -3
- package/src/integration.mjs +19 -0
- package/src/lib/component-discovery.mjs +177 -0
- package/src/lib/config-cache.mjs +91 -0
- package/src/lib/config-cache.test.mjs +83 -0
- package/src/lib/config-schema.mjs +105 -0
- package/src/lib/error-codes.mjs +18 -12
- package/src/lib/integration-warnings.mjs +62 -0
- package/src/lib/integration-warnings.test.mjs +102 -0
- package/src/lib/integrations.mjs +132 -0
- package/src/lib/integrations.test.mjs +135 -0
- package/src/lib/levenshtein.mjs +29 -0
- package/src/lib/manifest.mjs +15 -2
- package/src/lib/module-loader.mjs +80 -0
- package/src/lib/module-loader.test.mjs +106 -0
- package/src/lib/package-scanner.mjs +31 -7
- package/src/lib/project.mjs +502 -0
- package/src/lib/project.test.mjs +308 -0
- package/src/lib/string-utils.mjs +5 -14
- package/src/lib/xle/browser.d.ts +100 -0
- package/src/lib/xle/browser.mjs +120 -0
- package/src/lib/xle/expand.mjs +622 -0
- package/src/lib/xle/parse.mjs +581 -0
- package/src/lib/xle/print.mjs +174 -0
- package/src/lib/xle/registry-core.mjs +170 -0
- package/src/lib/xle/registry.mjs +237 -0
- package/src/lib/xle/splice.mjs +137 -0
- package/src/lib/xle/validate.mjs +356 -0
- package/src/lib/xle/xle.test.mjs +333 -0
- package/src/template.mjs +73 -0
- package/src/template.test.mjs +127 -0
- package/src/types/api.d.ts +2 -6
- package/src/types/base.d.ts +3 -9
- package/src/types/codemod.d.ts +81 -0
- package/src/types/component.d.ts +27 -2
- package/src/types/config.d.ts +70 -0
- package/src/types/error-codes.d.ts +5 -3
- package/src/types/index.d.ts +0 -1
- package/src/types/integration.d.ts +29 -0
- package/src/types/swizzle.d.ts +9 -2
- package/src/types/template-api.d.ts +54 -0
- package/src/types/template.d.ts +10 -7
- package/src/types/upgrade.d.ts +29 -0
- package/src/types/validate-integration.d.ts +24 -0
- package/src/utils/github.mjs +0 -252
- package/src/utils/interactive.mjs +2 -2
- package/templates/blocks/components/Card/ClickableCardWithNestedButton.doc.mjs +1 -1
- package/templates/blocks/components/Card/SelectableCardMulti.doc.mjs +1 -1
- package/templates/blocks/components/ChatComposerDrawer/ChatComposerDrawerWithProgress.tsx +1 -1
- package/templates/blocks/components/ChatDictationButton/ChatDictationButtonShowcase.tsx +3 -3
- package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyShowcase.doc.mjs +15 -0
- package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyShowcase.tsx +26 -0
- package/templates/blocks/components/DateInput/DateInputDateRange.doc.mjs +2 -2
- package/templates/blocks/components/DateRangeInput/DateRangeInputWithPresets.doc.mjs +14 -0
- package/templates/blocks/components/DateRangeInput/DateRangeInputWithPresets.tsx +46 -0
- package/templates/blocks/components/DateRangeInput/DateRangeInputWithValidation.doc.mjs +14 -0
- package/templates/blocks/components/DateRangeInput/DateRangeInputWithValidation.tsx +52 -0
- package/templates/blocks/components/DateTimeInput/DateTimeInputWithValidation.doc.mjs +14 -0
- package/templates/blocks/components/DateTimeInput/DateTimeInputWithValidation.tsx +43 -0
- package/templates/blocks/components/FileInput/FileInputShowcase.tsx +4 -2
- package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.doc.mjs +1 -1
- package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.tsx +9 -4
- package/templates/blocks/components/LinkProvider/LinkProviderCustomLink.doc.mjs +14 -0
- package/templates/blocks/components/LinkProvider/LinkProviderCustomLink.tsx +43 -0
- package/templates/blocks/components/MultiSelector/MultiSelectorShowcase.tsx +6 -2
- package/templates/blocks/components/NumberInput/NumberInputShowcase.tsx +6 -2
- package/templates/blocks/components/Outline/OutlineShowcase.doc.mjs +15 -0
- package/templates/blocks/components/Outline/OutlineShowcase.tsx +22 -0
- package/templates/blocks/components/RadioList/RadioListShowcase.tsx +8 -1
- package/templates/blocks/components/SegmentedControl/SegmentedControlShowcase.tsx +3 -1
- package/templates/blocks/components/Selector/SelectorShowcase.tsx +4 -1
- package/templates/blocks/components/Slider/SliderShowcase.tsx +10 -1
- package/templates/blocks/components/Tab/TabShowcase.tsx +3 -1
- package/templates/blocks/components/Tab/TabWithSelectedIcon.doc.mjs +13 -0
- package/templates/blocks/components/Tab/TabWithSelectedIcon.tsx +39 -0
- package/templates/blocks/components/TabList/TabListShowcase.tsx +3 -1
- package/templates/blocks/components/TabMenu/TabMenuShowcase.tsx +3 -1
- package/templates/blocks/components/Table/ColumnResizeHookUsage.doc.mjs +14 -0
- package/templates/blocks/components/Table/ColumnResizeHookUsage.tsx +59 -0
- package/templates/blocks/components/Table/StickyColumnsHookUsage.doc.mjs +14 -0
- package/templates/blocks/components/Table/StickyColumnsHookUsage.tsx +104 -0
- package/templates/blocks/components/Text/TextColors.tsx +20 -5
- package/templates/blocks/components/TextArea/TextAreaShowcase.tsx +4 -2
- package/templates/blocks/components/TextInput/TextInputShowcase.tsx +4 -2
- package/templates/blocks/components/Thumbnail/ThumbnailDisabled.tsx +11 -6
- package/templates/blocks/components/Thumbnail/ThumbnailGallery.tsx +43 -7
- package/templates/blocks/components/Thumbnail/ThumbnailRemovable.tsx +40 -3
- package/templates/blocks/components/Thumbnail/ThumbnailShowcase.tsx +6 -5
- package/templates/blocks/components/Thumbnail/ThumbnailStates.tsx +11 -6
- package/templates/blocks/components/Timestamp/TimestampColors.tsx +2 -2
- package/templates/blocks/components/ToggleButton/ToggleButtonGroup.doc.mjs +1 -1
- package/templates/blocks/components/Tokenizer/TokenizerShowcase.tsx +8 -6
- package/templates/blocks/components/Toolbar/ToolbarTableFilter.doc.mjs +2 -2
- package/templates/blocks/components/Toolbar/ToolbarTableFilter.tsx +23 -14
- package/templates/blocks/components/Typeahead/TypeaheadShowcase.tsx +6 -2
- package/templates/pages/shell-nav/page.tsx +321 -0
- package/templates/pages/shell-nav/template.doc.mjs +12 -0
- package/templates/pages/shell-side-nav/page.tsx +242 -0
- package/templates/pages/shell-side-nav/template.doc.mjs +12 -0
- package/templates/pages/shell-top-nav/page.tsx +224 -0
- package/templates/pages/shell-top-nav/template.doc.mjs +12 -0
- package/templates/pages/theme-showcase/page.tsx +8 -19
- package/templates/themes/butter/butterTheme.ts +916 -0
- package/templates/themes/butter/icons.tsx +77 -0
- package/templates/themes/chocolate/chocolateTheme.ts +230 -0
- package/templates/themes/chocolate/icons.tsx +77 -0
- package/templates/themes/gothic/gothicTheme.ts +657 -0
- package/templates/themes/gothic/icons.tsx +77 -0
- package/templates/themes/manifest.json +90 -0
- package/templates/themes/matcha/icons.tsx +67 -0
- package/templates/themes/matcha/matchaTheme.ts +247 -0
- package/templates/themes/neutral/icons.tsx +77 -0
- package/templates/themes/neutral/neutralTheme.ts +603 -0
- package/templates/themes/stone/icons.tsx +77 -0
- package/templates/themes/stone/stoneTheme.ts +652 -0
- package/templates/themes/y2k/icons.tsx +67 -0
- package/templates/themes/y2k/y2kTheme.ts +617 -0
- package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-config-surfaces.test.mjs +0 -116
- package/src/codemods/transforms/v0.1.0/migrate-xds-config-surfaces.mjs +0 -230
- package/src/commands/gap-report.mjs +0 -456
- package/src/commands/gap-report.test.mjs +0 -163
- package/src/commands/swizzle-gap-safety.test.mjs +0 -273
- package/src/lib/config.mjs +0 -86
- package/src/lib/config.test.mjs +0 -42
- package/src/types/gap-report.d.ts +0 -29
- package/templates/blocks/components/MoreMenu/MoreMenuInToolbar.doc.mjs +0 -14
- package/templates/blocks/components/MoreMenu/MoreMenuInToolbar.tsx +0 -57
- /package/src/codemods/transforms/{v0.0.15 → v0.1.0}/__tests__/drop-xds-prefix-imports.test.mjs +0 -0
package/src/commands/swizzle.mjs
CHANGED
|
@@ -3,40 +3,55 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* @file swizzle command — Copy component source for customization
|
|
5
5
|
*
|
|
6
|
-
* Resolves component
|
|
7
|
-
* copies non-test files to the output directory, and
|
|
8
|
-
* relative imports to use
|
|
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,
|
|
11
|
-
*
|
|
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 {
|
|
19
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
*
|
|
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.
|
|
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
|
|
51
|
-
return `${prefix}
|
|
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(
|
|
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
|
-
|
|
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
|
-
`
|
|
117
|
-
{
|
|
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 (
|
|
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 (
|
|
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(
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
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 (
|
|
213
|
-
|
|
214
|
-
const
|
|
371
|
+
if (json) {
|
|
372
|
+
/** @type {Record<string, unknown>} */
|
|
373
|
+
const payload = {
|
|
215
374
|
component: dirName,
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
375
|
+
package: owner.package,
|
|
376
|
+
outputDir: relOutput,
|
|
377
|
+
filesCopied: copied,
|
|
378
|
+
files: copiedFiles.map(f => f),
|
|
219
379
|
};
|
|
220
|
-
|
|
221
|
-
|
|
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(
|
|
290
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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'
|
|
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
|
|
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'
|
|
108
|
+
['--json', 'swizzle', 'Button'],
|
|
109
109
|
project,
|
|
110
110
|
);
|
|
111
111
|
|