@astryxdesign/cli 0.1.0 → 0.1.1-canary.a514b99
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 +68 -0
- package/README.md +117 -75
- package/bin/astryx.mjs +22 -7
- package/docs/getting-started.doc.mjs +11 -11
- package/docs/icons.doc.mjs +1 -1
- package/docs/migration.doc.mjs +2 -2
- package/docs/shape.doc.mjs +1 -1
- package/docs/styling.doc.mjs +3 -4
- package/docs/theme.doc.dense.mjs +2 -2
- package/docs/theme.doc.mjs +14 -0
- package/docs/theme.doc.zh.mjs +2 -2
- package/docs/working-with-ai.doc.mjs +4 -4
- package/package.json +8 -8
- package/src/api/doctor.mjs +3 -3
- package/src/api/search.mjs +207 -13
- package/src/api/template.mjs +2 -1
- package/src/codemods/__tests__/registry.test.mjs +1 -0
- package/src/codemods/registry.mjs +1 -0
- package/src/codemods/runner.mjs +105 -51
- package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-config-surfaces.test.mjs +116 -0
- package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-module-specifiers.test.mjs +51 -0
- package/src/codemods/transforms/v0.1.0/index.mjs +28 -0
- package/src/codemods/transforms/v0.1.0/migrate-xds-config-surfaces.mjs +230 -0
- package/src/codemods/transforms/v0.1.0/migrate-xds-module-specifiers.mjs +84 -0
- package/src/commands/agent-docs.mjs +119 -66
- package/src/commands/agent-docs.path-safety.test.mjs +1 -1
- package/src/commands/agent-docs.test.mjs +87 -31
- package/src/commands/build-theme.import-path.test.mjs +1 -1
- package/src/commands/build-theme.path-safety.test.mjs +1 -1
- package/src/commands/build-theme.prose.test.mjs +1 -1
- package/src/commands/build.mjs +196 -0
- package/src/commands/component-package.test.mjs +1 -1
- package/src/commands/component.test.mjs +1 -1
- package/src/commands/docs.test.mjs +1 -1
- package/src/commands/doctor.test.mjs +1 -1
- package/src/commands/external-showcase.test.mjs +1 -1
- package/src/commands/init.mjs +10 -2
- package/src/commands/interactive-guard.test.mjs +1 -1
- package/src/commands/json-contract.test.mjs +10 -3
- package/src/commands/swizzle-gap-safety.test.mjs +1 -1
- package/src/commands/swizzle.path-safety.test.mjs +1 -1
- package/src/commands/template.path-safety.test.mjs +1 -1
- package/src/commands/template.test.mjs +1 -1
- package/src/commands/upgrade.mjs +353 -169
- package/src/commands/upgrade.test.mjs +41 -27
- package/src/index.mjs +1 -0
- package/src/lib/config.mjs +12 -0
- package/src/lib/config.test.mjs +42 -0
- package/src/lib/error-codes.mjs +3 -0
- package/src/types/error-codes.d.ts +1 -0
- package/src/utils/interactive.mjs +1 -1
- package/src/utils/interactive.test.mjs +2 -0
- package/src/utils/package-manager.mjs +1 -1
- package/src/utils/package-manager.test.mjs +1 -1
- package/src/utils/path-safety.test.mjs +1 -1
- package/src/utils/paths.test.mjs +8 -8
- package/src/utils/update-check.mjs +4 -26
- package/src/utils/update-check.test.mjs +2 -64
- package/templates/blocks/components/AppShell/AppShellContentOnly.tsx +1 -9
- package/templates/blocks/components/AppShell/AppShellShowcase.tsx +1 -10
- package/templates/blocks/components/AppShell/AppShellSideNavOnly.tsx +1 -9
- package/templates/blocks/components/AppShell/AppShellTopNavOnly.tsx +1 -9
- package/templates/blocks/components/AppShell/AppShellTopNavWithSideNav.tsx +1 -9
- package/templates/blocks/components/AppShell/AppShellWithBanner.tsx +1 -9
- package/templates/blocks/components/AspectRatio/AspectRatioShowcase.tsx +12 -19
- package/templates/blocks/components/Banner/BannerShowcase.tsx +1 -8
- package/templates/blocks/components/Blockquote/BlockquoteShowcase.tsx +1 -8
- package/templates/blocks/components/Carousel/CarouselShowcase.tsx +2 -12
- package/templates/blocks/components/ChatComposerDrawer/ChatComposerDrawerShowcase.tsx +6 -9
- package/templates/blocks/components/ChatLayout/ChatLayoutPanelChat.tsx +10 -12
- package/templates/blocks/components/ChatMessageList/ChatMessageListDensity.tsx +1 -9
- package/templates/blocks/components/ChatMessageList/ChatMessageListFullFeatured.tsx +1 -9
- package/templates/blocks/components/ChatMessageList/ChatMessageListShowcase.tsx +1 -9
- package/templates/blocks/components/ChatMessageMetadata/ChatMessageMetadataShowcase.tsx +1 -8
- package/templates/blocks/components/ChatSendButton/ChatSendButtonInComposer.tsx +1 -8
- package/templates/blocks/components/Citation/CitationInlineText.tsx +4 -4
- package/templates/blocks/components/Code/CodeInlineInParagraph.tsx +1 -8
- package/templates/blocks/components/CodeBlock/CodeBlockBashCommand.tsx +1 -1
- package/templates/blocks/components/CodeBlock/CodeBlockJSONConfig.tsx +1 -1
- package/templates/blocks/components/CommandPaletteItem/CommandPaletteItemShowcase.tsx +9 -12
- package/templates/blocks/components/ContextMenu/ContextMenuShowcase.tsx +13 -15
- package/templates/blocks/components/Divider/DividerShowcase.tsx +1 -8
- package/templates/blocks/components/Divider/DividerVertical.tsx +7 -9
- package/templates/blocks/components/Field/FieldShowcase.tsx +1 -8
- package/templates/blocks/components/FormLayout/FormLayoutHorizontal.tsx +1 -6
- package/templates/blocks/components/Grid/GridResponsiveAutoFit.tsx +1 -9
- package/templates/blocks/components/HoverCard/HoverCardInlineTextHoverCard.tsx +4 -6
- package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.tsx +1 -6
- package/templates/blocks/components/HoverCard/HoverCardProfileHoverCard.tsx +2 -8
- package/templates/blocks/components/HoverCard/HoverCardShowcase.tsx +1 -8
- package/templates/blocks/components/MoreMenu/MoreMenuInToolbar.tsx +2 -12
- package/templates/blocks/components/OverflowList/OverflowListOverflowBadges.tsx +8 -11
- package/templates/blocks/components/OverflowList/OverflowListOverflowDropdownActions.tsx +9 -12
- package/templates/blocks/components/Overlay/OverlayBottomStrip.tsx +4 -17
- package/templates/blocks/components/Overlay/OverlayHoverReveal.tsx +15 -16
- package/templates/blocks/components/Overlay/OverlayShowcase.tsx +5 -21
- package/templates/blocks/components/Pagination/PaginationDotsCarousel.tsx +2 -14
- package/templates/blocks/components/Pagination/PaginationPageSize.tsx +12 -14
- package/templates/blocks/components/Pagination/PaginationVariants.tsx +1 -8
- package/templates/blocks/components/Pagination/PaginationWithTable.tsx +2 -14
- package/templates/blocks/components/Tokenizer/TokenizerClear.tsx +1 -6
- package/templates/blocks/components/Tokenizer/TokenizerCreatable.tsx +2 -7
- package/templates/blocks/components/Tokenizer/TokenizerEndContent.tsx +1 -6
- package/templates/blocks/components/Tokenizer/TokenizerIcon.tsx +1 -6
- package/templates/blocks/components/Tokenizer/TokenizerMaxEntries.tsx +1 -6
- package/templates/blocks/components/Tokenizer/TokenizerOverflow.tsx +2 -7
- package/templates/blocks/components/Tokenizer/TokenizerShowcase.tsx +1 -6
- package/templates/blocks/components/Tokenizer/TokenizerStates.tsx +4 -9
- package/templates/blocks/components/Toolbar/ToolbarCardHeader.tsx +1 -10
- package/templates/blocks/components/Toolbar/ToolbarSizes.tsx +1 -8
- package/templates/blocks/components/Toolbar/ToolbarTableFilter.tsx +1 -8
- package/templates/blocks/components/Toolbar/ToolbarThreeSlot.tsx +1 -10
- package/templates/blocks/components/Toolbar/ToolbarWithTabs.tsx +8 -11
- package/templates/pages/ai-chat/page.tsx +71 -64
- package/templates/pages/ai-chat-landing/page.tsx +8 -12
- package/templates/pages/centered-hero/page.tsx +13 -15
- package/templates/pages/classic-gallery/page.tsx +27 -34
- package/templates/pages/detail-page/page.tsx +18 -18
- package/templates/pages/documentation/page.tsx +42 -58
- package/templates/pages/documentation-design/page.tsx +82 -60
- package/templates/pages/documentation-technical/page.tsx +101 -60
- package/templates/pages/editor/page.tsx +42 -54
- package/templates/pages/file-explorer/page.tsx +13 -16
- package/templates/pages/form-two-column/page.tsx +13 -17
- package/templates/pages/gallery-hero/page.tsx +13 -15
- package/templates/pages/ide/page.tsx +188 -264
- package/templates/pages/library/page.tsx +16 -23
- package/templates/pages/login/page.tsx +14 -18
- package/templates/pages/login-card/page.tsx +14 -18
- package/templates/pages/login-split/page.tsx +50 -48
- package/templates/pages/login-sso/page.tsx +9 -13
- package/templates/pages/mixed-gallery/page.tsx +51 -45
- package/templates/pages/payment-form/page.tsx +56 -70
- package/templates/pages/product-detail/page.tsx +27 -33
- package/templates/pages/product-gallery/page.tsx +7 -13
- package/templates/pages/settings-dialog/page.tsx +35 -43
- package/templates/pages/settings-sidebar/page.tsx +39 -47
- package/templates/pages/side-gallery/page.tsx +6 -9
- package/templates/pages/table-grouped/page.tsx +11 -15
- package/templates/pages/theme-showcase/page.tsx +33 -37
|
@@ -6,6 +6,7 @@ import * as path from 'node:path';
|
|
|
6
6
|
import * as os from 'node:os';
|
|
7
7
|
import {
|
|
8
8
|
generateCompressedIndex,
|
|
9
|
+
detectStylingSystem,
|
|
9
10
|
getXdsVersion,
|
|
10
11
|
installAgentDocs,
|
|
11
12
|
injectAgentsMd,
|
|
@@ -20,7 +21,7 @@ import {
|
|
|
20
21
|
let tmpDir;
|
|
21
22
|
|
|
22
23
|
beforeEach(() => {
|
|
23
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), '
|
|
24
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-agent-docs-test-'));
|
|
24
25
|
});
|
|
25
26
|
|
|
26
27
|
afterEach(() => {
|
|
@@ -32,38 +33,93 @@ describe('generateCompressedIndex', () => {
|
|
|
32
33
|
it('includes the version number', () => {
|
|
33
34
|
const result = generateCompressedIndex('1.2.3');
|
|
34
35
|
expect(result).toContain('Astryx v1.2.3');
|
|
35
|
-
expect(result).toContain('<!--
|
|
36
|
-
expect(result).toContain('<!--
|
|
36
|
+
expect(result).toContain('<!-- ASTRYX:START -->');
|
|
37
|
+
expect(result).toContain('<!-- ASTRYX:END -->');
|
|
37
38
|
});
|
|
38
39
|
|
|
39
40
|
it('includes theme nudge rule', () => {
|
|
40
41
|
const result = generateCompressedIndex('1.0.0');
|
|
41
42
|
expect(result).toMatch(/astryx theme/);
|
|
42
|
-
expect(result).toMatch(/never override --
|
|
43
|
+
expect(result).toMatch(/never override --color-/);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('defaults to the CSS-variable styling path (no compiler)', () => {
|
|
47
|
+
const result = generateCompressedIndex('1.0.0');
|
|
48
|
+
expect(result).toMatch(/style\/className with tokens/);
|
|
49
|
+
expect(result).toMatch(/var\(--color-\*/);
|
|
50
|
+
// Must NOT push xstyle when no StyleX compiler is present.
|
|
51
|
+
expect(result).not.toMatch(/xstyle prop/);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('recommends xstyle when StyleX is configured', () => {
|
|
55
|
+
const result = generateCompressedIndex('1.0.0', {stylingSystem: 'stylex'});
|
|
56
|
+
expect(result).toMatch(/xstyle prop \/ StyleX tokens/);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('recommends Tailwind utilities when Tailwind is configured', () => {
|
|
60
|
+
const result = generateCompressedIndex('1.0.0', {stylingSystem: 'tailwind'});
|
|
61
|
+
expect(result).toMatch(/Tailwind utilities backed by tokens/);
|
|
62
|
+
expect(result).toMatch(/tailwind-theme\.css/);
|
|
43
63
|
});
|
|
44
64
|
|
|
45
65
|
it('includes upgrade command and migration rule', () => {
|
|
46
66
|
const result = generateCompressedIndex('1.0.0');
|
|
47
|
-
expect(result).toContain('
|
|
48
|
-
expect(result).
|
|
49
|
-
expect(result).toMatch(/always run .+ astryx upgrade --apply/);
|
|
67
|
+
expect(result).toContain('upgrade --apply');
|
|
68
|
+
expect(result).toMatch(/after any @astryxdesign\/core bump/);
|
|
50
69
|
});
|
|
51
70
|
|
|
52
|
-
it('
|
|
71
|
+
it('states the runPrefix once in the CLI header', () => {
|
|
53
72
|
const result = generateCompressedIndex('1.0.0', {runPrefix: 'yarn'});
|
|
54
|
-
expect(result).toContain('yarn astryx
|
|
55
|
-
expect(result).toContain('yarn astryx upgrade --apply');
|
|
56
|
-
expect(result).toContain('after @astryxdesign/core bump, always run yarn astryx upgrade --apply');
|
|
73
|
+
expect(result).toContain('yarn astryx <cmd>');
|
|
57
74
|
expect(result).not.toContain('npx astryx');
|
|
58
75
|
});
|
|
59
76
|
|
|
60
77
|
it('uses pnpm exec prefix', () => {
|
|
61
78
|
const result = generateCompressedIndex('1.0.0', {runPrefix: 'pnpm exec'});
|
|
62
|
-
expect(result).toContain('pnpm exec astryx
|
|
79
|
+
expect(result).toContain('pnpm exec astryx <cmd>');
|
|
63
80
|
expect(result).not.toContain('npx astryx');
|
|
64
81
|
});
|
|
65
82
|
});
|
|
66
83
|
|
|
84
|
+
describe('detectStylingSystem', () => {
|
|
85
|
+
function writePkg(deps) {
|
|
86
|
+
fs.writeFileSync(
|
|
87
|
+
path.join(tmpDir, 'package.json'),
|
|
88
|
+
JSON.stringify({name: 'x', devDependencies: deps}),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
it('defaults to css when no package.json', () => {
|
|
93
|
+
expect(detectStylingSystem(tmpDir)).toBe('css');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('returns css for a plain project', () => {
|
|
97
|
+
writePkg({react: '19.0.0', vite: '6.0.0'});
|
|
98
|
+
expect(detectStylingSystem(tmpDir)).toBe('css');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('detects stylex when the compiler plugin is present', () => {
|
|
102
|
+
writePkg({'@stylexjs/babel-plugin': '0.0.1'});
|
|
103
|
+
expect(detectStylingSystem(tmpDir)).toBe('stylex');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('detects tailwind when tailwindcss is present', () => {
|
|
107
|
+
writePkg({tailwindcss: '4.0.0'});
|
|
108
|
+
expect(detectStylingSystem(tmpDir)).toBe('tailwind');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('does NOT treat the StyleX runtime alone as a compiler', () => {
|
|
112
|
+
// Only the runtime, no compiler plugin → must stay on the safe css path.
|
|
113
|
+
writePkg({'@stylexjs/stylex': '0.0.1'});
|
|
114
|
+
expect(detectStylingSystem(tmpDir)).toBe('css');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('prefers stylex over tailwind when both are configured', () => {
|
|
118
|
+
writePkg({'@stylexjs/babel-plugin': '0.0.1', tailwindcss: '4.0.0'});
|
|
119
|
+
expect(detectStylingSystem(tmpDir)).toBe('stylex');
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
67
123
|
describe('getXdsVersion', () => {
|
|
68
124
|
it('reads version from core package.json', () => {
|
|
69
125
|
const coreDir = path.join(tmpDir, 'core');
|
|
@@ -82,19 +138,19 @@ describe('injectXdsBlock', () => {
|
|
|
82
138
|
const filePath = path.join(tmpDir, 'test.md');
|
|
83
139
|
fs.writeFileSync(filePath, '# Existing content\n');
|
|
84
140
|
|
|
85
|
-
const result = injectXdsBlock(filePath, '<!--
|
|
141
|
+
const result = injectXdsBlock(filePath, '<!-- ASTRYX:START -->\nnew\n<!-- ASTRYX:END -->');
|
|
86
142
|
|
|
87
143
|
expect(result).toBe(true);
|
|
88
144
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
89
145
|
expect(content).toContain('# Existing content');
|
|
90
|
-
expect(content).toContain('<!--
|
|
146
|
+
expect(content).toContain('<!-- ASTRYX:START -->');
|
|
91
147
|
});
|
|
92
148
|
|
|
93
149
|
it('replaces existing markers', () => {
|
|
94
150
|
const filePath = path.join(tmpDir, 'test.md');
|
|
95
151
|
fs.writeFileSync(filePath, 'before\n<!-- XDS:START -->\nold\n<!-- XDS:END -->\nafter\n');
|
|
96
152
|
|
|
97
|
-
injectXdsBlock(filePath, '<!--
|
|
153
|
+
injectXdsBlock(filePath, '<!-- ASTRYX:START -->\nnew\n<!-- ASTRYX:END -->');
|
|
98
154
|
|
|
99
155
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
100
156
|
expect(content).toContain('new');
|
|
@@ -106,7 +162,7 @@ describe('injectXdsBlock', () => {
|
|
|
106
162
|
it('returns false and does not create file when createIfMissing is false', () => {
|
|
107
163
|
const filePath = path.join(tmpDir, 'nonexistent.md');
|
|
108
164
|
|
|
109
|
-
const result = injectXdsBlock(filePath, '<!--
|
|
165
|
+
const result = injectXdsBlock(filePath, '<!-- ASTRYX:START -->\ncontent\n<!-- ASTRYX:END -->');
|
|
110
166
|
|
|
111
167
|
expect(result).toBe(false);
|
|
112
168
|
expect(fs.existsSync(filePath)).toBe(false);
|
|
@@ -116,11 +172,11 @@ describe('injectXdsBlock', () => {
|
|
|
116
172
|
const filePath = path.join(tmpDir, 'test.md');
|
|
117
173
|
fs.writeFileSync(filePath, '# Existing content\n\nNo XDS markers here.\n');
|
|
118
174
|
|
|
119
|
-
const result = injectXdsBlock(filePath, '<!--
|
|
175
|
+
const result = injectXdsBlock(filePath, '<!-- ASTRYX:START -->\nnew\n<!-- ASTRYX:END -->', {onlyReplace: true});
|
|
120
176
|
|
|
121
177
|
expect(result).toBe(false);
|
|
122
178
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
123
|
-
expect(content).not.toContain('<!--
|
|
179
|
+
expect(content).not.toContain('<!-- ASTRYX:START -->');
|
|
124
180
|
expect(content).toBe('# Existing content\n\nNo XDS markers here.\n');
|
|
125
181
|
});
|
|
126
182
|
|
|
@@ -128,7 +184,7 @@ describe('injectXdsBlock', () => {
|
|
|
128
184
|
const filePath = path.join(tmpDir, 'test.md');
|
|
129
185
|
fs.writeFileSync(filePath, 'before\n<!-- XDS:START -->\nold\n<!-- XDS:END -->\nafter\n');
|
|
130
186
|
|
|
131
|
-
const result = injectXdsBlock(filePath, '<!--
|
|
187
|
+
const result = injectXdsBlock(filePath, '<!-- ASTRYX:START -->\nnew\n<!-- ASTRYX:END -->', {onlyReplace: true});
|
|
132
188
|
|
|
133
189
|
expect(result).toBe(true);
|
|
134
190
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
@@ -139,7 +195,7 @@ describe('injectXdsBlock', () => {
|
|
|
139
195
|
it('creates file when createIfMissing is true', () => {
|
|
140
196
|
const filePath = path.join(tmpDir, 'new.md');
|
|
141
197
|
|
|
142
|
-
const result = injectXdsBlock(filePath, '<!--
|
|
198
|
+
const result = injectXdsBlock(filePath, '<!-- ASTRYX:START -->\ncontent\n<!-- ASTRYX:END -->', {
|
|
143
199
|
createIfMissing: true,
|
|
144
200
|
header: '# Header',
|
|
145
201
|
});
|
|
@@ -147,7 +203,7 @@ describe('injectXdsBlock', () => {
|
|
|
147
203
|
expect(result).toBe(true);
|
|
148
204
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
149
205
|
expect(content).toContain('# Header');
|
|
150
|
-
expect(content).toContain('<!--
|
|
206
|
+
expect(content).toContain('<!-- ASTRYX:START -->');
|
|
151
207
|
});
|
|
152
208
|
});
|
|
153
209
|
|
|
@@ -157,9 +213,9 @@ describe('injectAgentsMd', () => {
|
|
|
157
213
|
|
|
158
214
|
const content = fs.readFileSync(path.join(tmpDir, 'AGENTS.md'), 'utf-8');
|
|
159
215
|
expect(content).toContain('# AGENTS.md');
|
|
160
|
-
expect(content).toContain('<!--
|
|
216
|
+
expect(content).toContain('<!-- ASTRYX:START -->');
|
|
161
217
|
expect(content).toContain('Astryx v1.0.0');
|
|
162
|
-
expect(content).toContain('<!--
|
|
218
|
+
expect(content).toContain('<!-- ASTRYX:END -->');
|
|
163
219
|
});
|
|
164
220
|
|
|
165
221
|
it('updates existing AGENTS.md by replacing XDS markers', () => {
|
|
@@ -195,7 +251,7 @@ Existing agent docs.
|
|
|
195
251
|
|
|
196
252
|
const content = fs.readFileSync(path.join(tmpDir, 'AGENTS.md'), 'utf-8');
|
|
197
253
|
expect(content).toContain('Existing agent docs.');
|
|
198
|
-
expect(content).toContain('<!--
|
|
254
|
+
expect(content).toContain('<!-- ASTRYX:START -->');
|
|
199
255
|
expect(content).toContain('Astryx v1.0.0');
|
|
200
256
|
});
|
|
201
257
|
});
|
|
@@ -210,7 +266,7 @@ describe('injectClaudeMd', () => {
|
|
|
210
266
|
const content = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8');
|
|
211
267
|
expect(content).toContain('# Claude Config');
|
|
212
268
|
expect(content).toContain('Existing rules.');
|
|
213
|
-
expect(content).toContain('<!--
|
|
269
|
+
expect(content).toContain('<!-- ASTRYX:START -->');
|
|
214
270
|
expect(content).toContain('Astryx v1.0.0');
|
|
215
271
|
});
|
|
216
272
|
|
|
@@ -322,7 +378,7 @@ describe('installAgentDocs', () => {
|
|
|
322
378
|
expect(fs.existsSync(path.join(tmpDir, '.claude', 'CLAUDE.md'))).toBe(true);
|
|
323
379
|
expect(fs.existsSync(path.join(tmpDir, 'AGENTS.md'))).toBe(false);
|
|
324
380
|
const content = fs.readFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), 'utf-8');
|
|
325
|
-
expect(content).toContain('<!--
|
|
381
|
+
expect(content).toContain('<!-- ASTRYX:START -->');
|
|
326
382
|
});
|
|
327
383
|
|
|
328
384
|
it('injects into CLAUDE.md at root when it exists', () => {
|
|
@@ -334,7 +390,7 @@ describe('installAgentDocs', () => {
|
|
|
334
390
|
expect(written).toEqual(['CLAUDE.md']);
|
|
335
391
|
expect(fs.existsSync(path.join(tmpDir, 'AGENTS.md'))).toBe(false);
|
|
336
392
|
const claudeContent = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8');
|
|
337
|
-
expect(claudeContent).toContain('<!--
|
|
393
|
+
expect(claudeContent).toContain('<!-- ASTRYX:START -->');
|
|
338
394
|
expect(claudeContent).toContain('Project rules.');
|
|
339
395
|
});
|
|
340
396
|
|
|
@@ -349,8 +405,8 @@ describe('installAgentDocs', () => {
|
|
|
349
405
|
expect(written).toContain('CLAUDE.md');
|
|
350
406
|
const agentsContent = fs.readFileSync(path.join(tmpDir, 'AGENTS.md'), 'utf-8');
|
|
351
407
|
const claudeContent = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8');
|
|
352
|
-
expect(agentsContent).toContain('<!--
|
|
353
|
-
expect(claudeContent).toContain('<!--
|
|
408
|
+
expect(agentsContent).toContain('<!-- ASTRYX:START -->');
|
|
409
|
+
expect(claudeContent).toContain('<!-- ASTRYX:START -->');
|
|
354
410
|
});
|
|
355
411
|
|
|
356
412
|
it('updates existing .claude/CLAUDE.md', () => {
|
|
@@ -363,7 +419,7 @@ describe('installAgentDocs', () => {
|
|
|
363
419
|
expect(written).toEqual(['.claude/CLAUDE.md']);
|
|
364
420
|
const content = fs.readFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), 'utf-8');
|
|
365
421
|
expect(content).toContain('Existing content.');
|
|
366
|
-
expect(content).toContain('<!--
|
|
422
|
+
expect(content).toContain('<!-- ASTRYX:START -->');
|
|
367
423
|
});
|
|
368
424
|
|
|
369
425
|
it('respects --agent claude preset: finds existing CLAUDE.md', () => {
|
|
@@ -410,7 +466,7 @@ describe('installAgentDocs', () => {
|
|
|
410
466
|
|
|
411
467
|
expect(written).toEqual([]);
|
|
412
468
|
const content = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8');
|
|
413
|
-
expect(content).not.toContain('<!--
|
|
469
|
+
expect(content).not.toContain('<!-- ASTRYX:START -->');
|
|
414
470
|
expect(content).toBe('# Claude\n\nProject rules only.\n');
|
|
415
471
|
});
|
|
416
472
|
|
|
@@ -70,7 +70,7 @@ beforeAll(() => {
|
|
|
70
70
|
|
|
71
71
|
let tmpDir;
|
|
72
72
|
beforeEach(() => {
|
|
73
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), '
|
|
73
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-build-theme-import-path-'));
|
|
74
74
|
});
|
|
75
75
|
afterEach(() => {
|
|
76
76
|
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
@@ -54,7 +54,7 @@ function writeTheme(dir, name) {
|
|
|
54
54
|
|
|
55
55
|
let tmpDir;
|
|
56
56
|
beforeEach(() => {
|
|
57
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), '
|
|
57
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-build-theme-paths-'));
|
|
58
58
|
});
|
|
59
59
|
afterEach(() => {
|
|
60
60
|
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
@@ -82,7 +82,7 @@ beforeAll(() => {
|
|
|
82
82
|
|
|
83
83
|
let tmpDir;
|
|
84
84
|
beforeEach(() => {
|
|
85
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), '
|
|
85
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-build-theme-prose-'));
|
|
86
86
|
});
|
|
87
87
|
afterEach(() => {
|
|
88
88
|
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file build command — the page-building assistant.
|
|
5
|
+
*
|
|
6
|
+
* Two modes:
|
|
7
|
+
* astryx build → the PLAYBOOK: how to build a page with Astryx
|
|
8
|
+
* astryx build "<what>" → a COMPOSITION KIT for what you're building:
|
|
9
|
+
* the closest page template (scaffold or layout
|
|
10
|
+
* reference), the blocks that cover parts, and
|
|
11
|
+
* the components to fill gaps, plus a one-line
|
|
12
|
+
* "Compose:" suggestion.
|
|
13
|
+
*
|
|
14
|
+
* `build` is the opinionated "assemble a page" verb. For a neutral lookup across
|
|
15
|
+
* the whole CLI, use `astryx search <query>` instead.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {getRunPrefix} from '../utils/package-manager.mjs';
|
|
19
|
+
import {jsonOut, humanLog} from '../lib/json.mjs';
|
|
20
|
+
import {cliError} from '../lib/cli-error.mjs';
|
|
21
|
+
import {search as searchApi} from '../api/search.mjs';
|
|
22
|
+
|
|
23
|
+
/** A page scoring at/above this is confident enough to call a direct match. */
|
|
24
|
+
const PAGE_DIRECT = 95;
|
|
25
|
+
/** Below this a page is too weak to even offer as a layout reference. */
|
|
26
|
+
const PAGE_FLOOR = 50;
|
|
27
|
+
/** Below this a block/domain-component match is incidental noise, not surfaced. */
|
|
28
|
+
const DOMAIN_FLOOR = 55;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Always-surfaced primitives. Every page needs a shell + layout/typography/
|
|
32
|
+
* action atoms, but these never keyword-match an idea ("dashboard" != "Stack"),
|
|
33
|
+
* so search alone never returns them. We list them unconditionally so an agent
|
|
34
|
+
* composing from scratch has the whole kit (esp. off-template).
|
|
35
|
+
*/
|
|
36
|
+
const FRAME = ['AppShell', 'TopNav', 'SideNav', 'Layout'];
|
|
37
|
+
const FOUNDATION = [
|
|
38
|
+
'VStack', 'HStack', 'Grid', 'StackItem', 'Card', 'Section',
|
|
39
|
+
'Text', 'Heading', 'Button', 'Icon', 'Badge', 'Divider',
|
|
40
|
+
];
|
|
41
|
+
const ALWAYS = new Set([...FRAME, ...FOUNDATION]);
|
|
42
|
+
|
|
43
|
+
/** Print the build playbook (shown when `build` is run with no query). */
|
|
44
|
+
function printPlaybook(run) {
|
|
45
|
+
const lines = [
|
|
46
|
+
'',
|
|
47
|
+
'How to build a page with Astryx',
|
|
48
|
+
'',
|
|
49
|
+
"1. Find a starting point for what you're building:",
|
|
50
|
+
` ${run} astryx build "<what you're building>"`,
|
|
51
|
+
' → returns the closest [page] template, the [block]s that cover parts,',
|
|
52
|
+
' and the [component]s to fill the gaps, with a "Compose:" suggestion.',
|
|
53
|
+
'',
|
|
54
|
+
'2. If a [page] template matches → scaffold it and adapt:',
|
|
55
|
+
` ${run} astryx template <name> [path]`,
|
|
56
|
+
'',
|
|
57
|
+
'3. If nothing matches exactly → compose:',
|
|
58
|
+
` ${run} astryx template <name> --skeleton # study a close page's layout`,
|
|
59
|
+
` ${run} astryx template <BlockName> # drop in each block from the kit`,
|
|
60
|
+
` ${run} astryx component <Name> # fill remaining gaps (read props)`,
|
|
61
|
+
'',
|
|
62
|
+
'4. Rules (keep it on-system):',
|
|
63
|
+
' - No <div>/raw HTML for layout — use VStack/HStack/Grid/Stack/Card etc.',
|
|
64
|
+
' - No style={{}} — use component props; design tokens via `astryx docs tokens`.',
|
|
65
|
+
' - Wrap the app in <Theme theme={...}> and import core reset.css + astryx.css.',
|
|
66
|
+
'',
|
|
67
|
+
`Tip: \`${run} astryx build "<idea>"\` is the fastest way in. For a neutral`,
|
|
68
|
+
`lookup of any component/doc/template, use \`${run} astryx search <query>\`.`,
|
|
69
|
+
'',
|
|
70
|
+
];
|
|
71
|
+
for (const l of lines) humanLog(l);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function registerBuild(program) {
|
|
75
|
+
program
|
|
76
|
+
.command('build [query]')
|
|
77
|
+
.description('Build a page: composition kit for an idea, or the workflow playbook (no args)')
|
|
78
|
+
.option('--type <domain>', 'Filter the kit to one domain (component|hook|template)')
|
|
79
|
+
.option('--limit <n>', 'Max candidates to draw from (default 60)')
|
|
80
|
+
.option('--detail', 'Verbose output (include import paths and match reason)')
|
|
81
|
+
.action(async (query, options) => {
|
|
82
|
+
const run = getRunPrefix();
|
|
83
|
+
const json = program.opts().json || false;
|
|
84
|
+
|
|
85
|
+
// No query → print the playbook (the "how to build" skill).
|
|
86
|
+
if (!query || !String(query).trim()) {
|
|
87
|
+
if (json) return jsonOut('build.help', {playbook: true});
|
|
88
|
+
printPlaybook(run);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Default to a deep pool so each role section has candidates after grouping.
|
|
93
|
+
let limit = 60;
|
|
94
|
+
if (options.limit != null) {
|
|
95
|
+
const parsed = Number.parseInt(options.limit, 10);
|
|
96
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
97
|
+
cliError(`Invalid --limit value "${options.limit}". Must be a positive integer.`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
limit = parsed;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let result;
|
|
104
|
+
try {
|
|
105
|
+
result = await searchApi(query, {cwd: process.cwd(), type: options.type, limit});
|
|
106
|
+
} catch (e) {
|
|
107
|
+
cliError(e.message, {suggestions: e.suggestions});
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (json) return jsonOut(result.type, result.data);
|
|
112
|
+
|
|
113
|
+
const {query: q, results} = result.data;
|
|
114
|
+
|
|
115
|
+
if (results.length === 0) {
|
|
116
|
+
humanLog('');
|
|
117
|
+
humanLog(`No matches for "${q}".`);
|
|
118
|
+
humanLog(`Try a broader term, or browse: ${run} astryx component --list`);
|
|
119
|
+
humanLog('');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── Group results by role (the build kit) ──────────────────────
|
|
124
|
+
const pages = results
|
|
125
|
+
.filter(r => r.domain === 'template' && r.kind !== 'block' && r.score >= PAGE_FLOOR)
|
|
126
|
+
.slice(0, 3);
|
|
127
|
+
const blocks = results
|
|
128
|
+
.filter(r => r.domain === 'template' && r.kind === 'block' && r.score >= DOMAIN_FLOOR)
|
|
129
|
+
.slice(0, 5);
|
|
130
|
+
// Idea-specific atoms = matched components/hooks MINUS the always-on kit.
|
|
131
|
+
const domain = results
|
|
132
|
+
.filter(r => (r.domain === 'component' || r.domain === 'hook') && r.score >= DOMAIN_FLOOR && !ALWAYS.has(r.name))
|
|
133
|
+
.slice(0, 6);
|
|
134
|
+
const directMatch = pages.length > 0 && pages[0].score >= PAGE_DIRECT;
|
|
135
|
+
|
|
136
|
+
const printItem = (r, label) => {
|
|
137
|
+
const display = r.domain === 'template' && r.displayName ? r.displayName : r.name;
|
|
138
|
+
humanLog('');
|
|
139
|
+
humanLog(` [${label}] ${display}`);
|
|
140
|
+
if (r.description) humanLog(` ${r.description}`);
|
|
141
|
+
humanLog(` → ${run} ${r.command}`);
|
|
142
|
+
if (options.detail) {
|
|
143
|
+
if (r.import) humanLog(` import: ${r.import}`);
|
|
144
|
+
humanLog(` match: ${r.reason} (score ${r.score})`);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
humanLog('');
|
|
149
|
+
humanLog(`Building "${q}":`);
|
|
150
|
+
|
|
151
|
+
// START — the single recommended path.
|
|
152
|
+
humanLog('');
|
|
153
|
+
if (directMatch) {
|
|
154
|
+
humanLog(`START → Scaffold the \`${pages[0].name}\` page template, then adapt: ${run} astryx template ${pages[0].name} ./src/App.tsx`);
|
|
155
|
+
} else if (pages.length) {
|
|
156
|
+
humanLog(`START → No exact page template. Use \`${pages[0].name}\` as a layout reference (${run} astryx template ${pages[0].name} --skeleton) and compose the pieces below.`);
|
|
157
|
+
} else {
|
|
158
|
+
humanLog(`START → No page template fits. Frame with AppShell and compose the blocks + components below.`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// PAGE
|
|
162
|
+
if (pages.length) {
|
|
163
|
+
humanLog('');
|
|
164
|
+
humanLog(directMatch ? 'PAGE TEMPLATE — direct match:' : 'CLOSEST PAGE TEMPLATES — layout reference:');
|
|
165
|
+
pages.forEach(p => printItem(p, directMatch ? 'page' : 'closest'));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// FRAME — always (the page shell).
|
|
169
|
+
humanLog('');
|
|
170
|
+
humanLog(`FRAME — page shell (always): ${FRAME.join(', ')}`);
|
|
171
|
+
humanLog(` full-page → AppShell; or Layout + SideNav/TopNav. ${run} astryx component AppShell`);
|
|
172
|
+
|
|
173
|
+
// BLOCKS — idea-specific composed patterns.
|
|
174
|
+
if (blocks.length) {
|
|
175
|
+
humanLog('');
|
|
176
|
+
humanLog('BLOCKS — drop-in patterns that cover parts of it:');
|
|
177
|
+
blocks.forEach(b => printItem(b, 'block'));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// DOMAIN COMPONENTS — idea-specific atoms.
|
|
181
|
+
if (domain.length) {
|
|
182
|
+
humanLog('');
|
|
183
|
+
humanLog('DOMAIN COMPONENTS — specific to this idea:');
|
|
184
|
+
domain.forEach(c => printItem(c, c.domain === 'hook' ? 'hook' : 'component'));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// FOUNDATION — always (layout/typography/actions).
|
|
188
|
+
humanLog('');
|
|
189
|
+
humanLog(`FOUNDATION — always available (layout/text/actions): ${FOUNDATION.join(' ')}`);
|
|
190
|
+
|
|
191
|
+
// SETUP — so it renders / stays on-system.
|
|
192
|
+
humanLog('');
|
|
193
|
+
humanLog('SETUP — import "@astryxdesign/core/reset.css" + "astryx.css". No <div>/style for layout — use Stack/Grid + tokens.');
|
|
194
|
+
humanLog('');
|
|
195
|
+
});
|
|
196
|
+
}
|
|
@@ -10,7 +10,7 @@ import {registerDocs} from './docs.mjs';
|
|
|
10
10
|
let tmpDir;
|
|
11
11
|
|
|
12
12
|
beforeEach(() => {
|
|
13
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), '
|
|
13
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-docs-test-'));
|
|
14
14
|
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
15
15
|
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
16
16
|
});
|
|
@@ -26,7 +26,7 @@ let logCalls;
|
|
|
26
26
|
let exitCode;
|
|
27
27
|
|
|
28
28
|
beforeEach(() => {
|
|
29
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), '
|
|
29
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-doctor-test-'));
|
|
30
30
|
logCalls = [];
|
|
31
31
|
exitCode = undefined;
|
|
32
32
|
vi.spyOn(console, 'log').mockImplementation((...args) => {
|
package/src/commands/init.mjs
CHANGED
|
@@ -100,7 +100,15 @@ async function runTemplate(targetDir, {interactive = true, templateName} = {}) {
|
|
|
100
100
|
|
|
101
101
|
if (!interactive) {
|
|
102
102
|
if (!templateName) {
|
|
103
|
-
|
|
103
|
+
// Point agents at the build workflow rather than dumping page-template
|
|
104
|
+
// names — `build` surfaces pages AND blocks AND components for an idea,
|
|
105
|
+
// and `build` with no args is the full how-to-build playbook.
|
|
106
|
+
humanLog('✓ To build UI, use these commands:');
|
|
107
|
+
humanLog('');
|
|
108
|
+
humanLog(` ${run} astryx build "<what you're building>" build a page — kit: closest template + blocks + components`);
|
|
109
|
+
humanLog(` ${run} astryx build the how-to-build workflow (read this first)`);
|
|
110
|
+
humanLog(` ${run} astryx search <query> find anything — components, docs, templates, blocks`);
|
|
111
|
+
humanLog('');
|
|
104
112
|
return;
|
|
105
113
|
}
|
|
106
114
|
|
|
@@ -248,7 +256,7 @@ export function registerInit(program) {
|
|
|
248
256
|
humanLog(' 2. Optionally add a theme:');
|
|
249
257
|
humanLog(" import { neutralTheme } from '@astryxdesign/theme-neutral'");
|
|
250
258
|
humanLog(' <Theme theme={neutralTheme}>...</Theme>');
|
|
251
|
-
humanLog(` 3. ${run}
|
|
259
|
+
humanLog(` 3. ${run} astryx --help for all commands`);
|
|
252
260
|
humanLog('');
|
|
253
261
|
});
|
|
254
262
|
}
|
|
@@ -53,7 +53,7 @@ function parseJson(stdout) {
|
|
|
53
53
|
let tmpDir;
|
|
54
54
|
|
|
55
55
|
beforeEach(() => {
|
|
56
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), '
|
|
56
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-json-contract-'));
|
|
57
57
|
});
|
|
58
58
|
|
|
59
59
|
afterEach(() => {
|
|
@@ -177,9 +177,16 @@ describe('--json contract: supported commands emit valid envelopes', () => {
|
|
|
177
177
|
});
|
|
178
178
|
|
|
179
179
|
it('astryx upgrade --json (already up to date) emits upgrade.status', () => {
|
|
180
|
-
// Force a no-op range: from >
|
|
180
|
+
// Force a no-op range: from > installed target.
|
|
181
|
+
const coreDir = path.join(tmpDir, 'node_modules', '@astryxdesign', 'core');
|
|
182
|
+
fs.mkdirSync(coreDir, {recursive: true});
|
|
183
|
+
fs.writeFileSync(
|
|
184
|
+
path.join(coreDir, 'package.json'),
|
|
185
|
+
JSON.stringify({name: '@astryxdesign/core', version: '0.0.1'}, null, 2),
|
|
186
|
+
);
|
|
187
|
+
|
|
181
188
|
const {status, stdout} = runCli(
|
|
182
|
-
['upgrade', '--json', '--from', '99.0.0'
|
|
189
|
+
['upgrade', '--json', '--from', '99.0.0'],
|
|
183
190
|
{cwd: tmpDir},
|
|
184
191
|
);
|
|
185
192
|
expect(status).toBe(0);
|
|
@@ -36,7 +36,7 @@ let markerFile;
|
|
|
36
36
|
let baseEnv;
|
|
37
37
|
|
|
38
38
|
beforeAll(() => {
|
|
39
|
-
shimDir = fs.mkdtempSync(path.join(os.tmpdir(), '
|
|
39
|
+
shimDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-gh-shim-'));
|
|
40
40
|
markerFile = path.join(shimDir, 'gh-issue-create-was-called.marker');
|
|
41
41
|
|
|
42
42
|
// Sabotaged `gh` shim. Records ONLY `gh issue create` invocations to the
|
|
@@ -58,7 +58,7 @@ function runCli(args, cwd) {
|
|
|
58
58
|
|
|
59
59
|
let tmpDir;
|
|
60
60
|
beforeEach(() => {
|
|
61
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), '
|
|
61
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-swizzle-paths-'));
|
|
62
62
|
});
|
|
63
63
|
afterEach(() => {
|
|
64
64
|
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
@@ -17,7 +17,7 @@ let tmpDir;
|
|
|
17
17
|
let templateApi;
|
|
18
18
|
|
|
19
19
|
beforeEach(async () => {
|
|
20
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), '
|
|
20
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-template-paths-'));
|
|
21
21
|
templateApi = (await import('../api/template.mjs')).template;
|
|
22
22
|
});
|
|
23
23
|
afterEach(() => {
|