@astryxdesign/cli 0.1.0-canary.e2d38fb → 0.1.0-canary.eb78210
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/docs/theme.doc.dense.mjs +2 -2
- package/docs/theme.doc.mjs +0 -14
- package/docs/theme.doc.zh.mjs +2 -2
- package/package.json +8 -8
- package/src/codemods/__tests__/registry.test.mjs +0 -1
- package/src/codemods/registry.mjs +0 -1
- package/src/codemods/runner.mjs +51 -105
- package/src/commands/agent-docs.mjs +56 -92
- package/src/commands/agent-docs.test.mjs +9 -65
- package/src/commands/build.mjs +29 -52
- package/src/commands/init.mjs +1 -9
- package/src/commands/upgrade.mjs +38 -117
- package/src/lib/config.mjs +0 -12
- package/src/lib/error-codes.mjs +0 -3
- package/src/types/error-codes.d.ts +0 -1
- package/src/utils/update-check.mjs +26 -4
- package/src/utils/update-check.test.mjs +63 -1
- package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-config-surfaces.test.mjs +0 -116
- package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-module-specifiers.test.mjs +0 -51
- package/src/codemods/transforms/v0.1.0/index.mjs +0 -28
- package/src/codemods/transforms/v0.1.0/migrate-xds-config-surfaces.mjs +0 -230
- package/src/codemods/transforms/v0.1.0/migrate-xds-module-specifiers.mjs +0 -84
- package/src/lib/config.test.mjs +0 -42
package/docs/theme.doc.dense.mjs
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
export const docsDense = {
|
|
6
6
|
description: 'Theme provider, custom themes, light/dark, component overrides',
|
|
7
7
|
sections: [
|
|
8
|
-
{ title: 'Quick Start', content: [null, null,
|
|
9
|
-
{ title: 'Themes', content: [null,
|
|
8
|
+
{ title: 'Quick Start', content: [null, null, { type: 'prose', text: 'default import = runtime injection. /built import = pre-compiled CSS (pair with theme.css).' }] },
|
|
9
|
+
{ title: 'Themes', content: [null, { type: 'prose', text: 'published: neutral (start here), butter, chocolate, gothic (dark-only), matcha, stone, y2k. @astryxdesign/theme-{name} = source (runtime). @astryxdesign/theme-{name}/built = optimized (+ theme.css).' }] },
|
|
10
10
|
{ title: 'Props', content: [null] },
|
|
11
11
|
{ title: 'Custom Theme', content: [{ type: 'prose', text: 'CLI wizard or manual defineTheme. only override tokens that differ.' }, null] },
|
|
12
12
|
{ title: 'defineTheme', content: [{ type: 'prose', text: 'scale configs (color, typography, radius, motion) + explicit token overrides + component overrides. color derives full palette from accent hex via HCT.' }, null, null] },
|
package/docs/theme.doc.mjs
CHANGED
|
@@ -14,12 +14,6 @@ export const docs = {
|
|
|
14
14
|
title: 'Quick Start',
|
|
15
15
|
category: 'guide',
|
|
16
16
|
content: [
|
|
17
|
-
{
|
|
18
|
-
type: 'code',
|
|
19
|
-
lang: 'bash',
|
|
20
|
-
label: 'Install a theme package',
|
|
21
|
-
code: 'npm install @astryxdesign/theme-neutral',
|
|
22
|
-
},
|
|
23
17
|
{
|
|
24
18
|
type: 'code',
|
|
25
19
|
lang: 'tsx',
|
|
@@ -51,10 +45,6 @@ function App() {
|
|
|
51
45
|
);
|
|
52
46
|
}`,
|
|
53
47
|
},
|
|
54
|
-
{
|
|
55
|
-
type: 'prose',
|
|
56
|
-
text: 'Each theme ships as its own npm package. Install the one you want, then wrap your app in `<Theme>` — the same pattern works for every theme; just swap the package and import name.',
|
|
57
|
-
},
|
|
58
48
|
{
|
|
59
49
|
type: 'prose',
|
|
60
50
|
text: 'The default import uses runtime style injection, which works everywhere with no build step. The `/built` import skips injection and relies on the pre-compiled CSS file for better performance and SSR support.',
|
|
@@ -65,10 +55,6 @@ function App() {
|
|
|
65
55
|
title: 'Available Themes',
|
|
66
56
|
category: 'guide',
|
|
67
57
|
content: [
|
|
68
|
-
{
|
|
69
|
-
type: 'prose',
|
|
70
|
-
text: 'Install the theme package you want with `npm install @astryxdesign/theme-{name}`, then import its theme object as shown below.',
|
|
71
|
-
},
|
|
72
58
|
{
|
|
73
59
|
type: 'table',
|
|
74
60
|
headers: ['Theme', 'Import', 'Description'],
|
package/docs/theme.doc.zh.mjs
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
export const docsZh = {
|
|
6
6
|
description: 'Theme 提供者、自定义主题、亮/暗模式和组件样式覆盖。',
|
|
7
7
|
sections: [
|
|
8
|
-
{ title: '快速开始', content: [null, null,
|
|
9
|
-
{ title: '可用主题', content: [null,
|
|
8
|
+
{ title: '快速开始', content: [null, null, { type: 'prose', text: '默认导入使用运行时样式注入。/built 导入使用预编译 CSS(需配合 theme.css)。' }] },
|
|
9
|
+
{ title: '可用主题', content: [null, { type: 'prose', text: '已发布主题:neutral(推荐起点)、butter、chocolate、gothic(仅暗色)、matcha、stone、y2k。@astryxdesign/theme-{name} = 源码版(运行时注入)。@astryxdesign/theme-{name}/built = 优化版(配合 theme.css)。' }] },
|
|
10
10
|
{ title: 'Theme 属性', content: [null] },
|
|
11
11
|
{ title: '创建自定义主题', content: [{ type: 'prose', text: '使用 CLI 向导(推荐)或手动 defineTheme。只覆盖与默认值不同的令牌。' }, null] },
|
|
12
12
|
{ title: 'defineTheme', content: [{ type: 'prose', text: '支持比例配置(typography、radius、motion)+ 显式令牌覆盖 + 组件覆盖。' }, null, null] },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/cli",
|
|
3
|
-
"version": "0.1.0-canary.
|
|
3
|
+
"version": "0.1.0-canary.eb78210",
|
|
4
4
|
"displayName": "CLI",
|
|
5
5
|
"description": "Scaffold projects, browse templates, generate themes, and get agent-ready docs from the command line.",
|
|
6
6
|
"author": "Meta Open Source",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"url": "https://github.com/facebook/astryx/issues"
|
|
16
16
|
},
|
|
17
17
|
"keywords": [
|
|
18
|
-
"
|
|
18
|
+
"xds",
|
|
19
19
|
"cli",
|
|
20
20
|
"design-system",
|
|
21
21
|
"init",
|
|
@@ -54,9 +54,9 @@
|
|
|
54
54
|
"jscodeshift": "^17.3.0"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
|
-
"@astryxdesign/core": "0.1.0-canary.
|
|
58
|
-
"@astryxdesign/lab": "0.1.0-canary.
|
|
59
|
-
"@astryxdesign/theme-neutral": "0.1.0-canary.
|
|
57
|
+
"@astryxdesign/core": "0.1.0-canary.eb78210",
|
|
58
|
+
"@astryxdesign/lab": "0.1.0-canary.eb78210",
|
|
59
|
+
"@astryxdesign/theme-neutral": "0.1.0-canary.eb78210"
|
|
60
60
|
},
|
|
61
61
|
"peerDependenciesMeta": {
|
|
62
62
|
"@astryxdesign/core": {
|
|
@@ -70,9 +70,9 @@
|
|
|
70
70
|
}
|
|
71
71
|
},
|
|
72
72
|
"devDependencies": {
|
|
73
|
-
"@astryxdesign/core": "0.1.0-canary.
|
|
74
|
-
"@astryxdesign/lab": "0.1.0-canary.
|
|
75
|
-
"@astryxdesign/theme-neutral": "0.1.0-canary.
|
|
73
|
+
"@astryxdesign/core": "0.1.0-canary.eb78210",
|
|
74
|
+
"@astryxdesign/lab": "0.1.0-canary.eb78210",
|
|
75
|
+
"@astryxdesign/theme-neutral": "0.1.0-canary.eb78210"
|
|
76
76
|
},
|
|
77
77
|
"scripts": {
|
|
78
78
|
"astryx": "node bin/astryx.mjs",
|
|
@@ -17,7 +17,6 @@ const registry = new Map([
|
|
|
17
17
|
['0.0.13', () => import('./transforms/v0.0.13/index.mjs')],
|
|
18
18
|
['0.0.14', () => import('./transforms/v0.0.14/index.mjs')],
|
|
19
19
|
['0.0.15', () => import('./transforms/v0.0.15/index.mjs')],
|
|
20
|
-
['0.1.0', () => import('./transforms/v0.1.0/index.mjs')],
|
|
21
20
|
]);
|
|
22
21
|
|
|
23
22
|
// Re-export from the shared utility so registry callers and other consumers
|
package/src/codemods/runner.mjs
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import * as fs from 'node:fs';
|
|
12
12
|
import * as path from 'node:path';
|
|
13
13
|
import * as p from '@clack/prompts';
|
|
14
|
+
import {getRunPrefix} from '../utils/package-manager.mjs';
|
|
14
15
|
import {humanLog} from '../lib/json.mjs';
|
|
15
16
|
|
|
16
17
|
// Known corruption patterns that indicate a broken transform.
|
|
@@ -86,6 +87,39 @@ function findSourceFiles(dir) {
|
|
|
86
87
|
return results.sort();
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Detect the project's formatter and run it on changed files.
|
|
92
|
+
* Tries prettier, then biome. Silently skips if none found.
|
|
93
|
+
*
|
|
94
|
+
* @param {string[]} files - Absolute paths to files that were modified
|
|
95
|
+
* @param {boolean} [silent] - Suppress human-facing output
|
|
96
|
+
*/
|
|
97
|
+
async function formatChangedFiles(files, silent = false) {
|
|
98
|
+
if (files.length === 0) return;
|
|
99
|
+
|
|
100
|
+
const {execSync} = await import('node:child_process');
|
|
101
|
+
const fileArgs = files.map(f => `"${f}"`).join(' ');
|
|
102
|
+
const prefix = getRunPrefix();
|
|
103
|
+
|
|
104
|
+
const formatters = [
|
|
105
|
+
{name: 'prettier', cmd: `${prefix} prettier --write ${fileArgs}`},
|
|
106
|
+
{name: 'biome', cmd: `${prefix} biome format --write ${fileArgs}`},
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
for (const {name, cmd} of formatters) {
|
|
110
|
+
try {
|
|
111
|
+
execSync(cmd, {stdio: 'pipe', timeout: 30000});
|
|
112
|
+
if (!silent)
|
|
113
|
+
p.log.info(
|
|
114
|
+
`Formatted ${files.length} file${files.length === 1 ? '' : 's'} with ${name}`,
|
|
115
|
+
);
|
|
116
|
+
return;
|
|
117
|
+
} catch {
|
|
118
|
+
// Formatter not available or failed — try next
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
89
123
|
/**
|
|
90
124
|
* Validate transform output before writing to disk.
|
|
91
125
|
*
|
|
@@ -128,86 +162,6 @@ export function validateOutput(result, source, j, {parse = true} = {}) {
|
|
|
128
162
|
return {valid: true};
|
|
129
163
|
}
|
|
130
164
|
|
|
131
|
-
function isConfigCodemod(transformEntry) {
|
|
132
|
-
return transformEntry.meta?.codemodType === 'config';
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const CONFIG_CODEMOD_PATHS = new Set([
|
|
136
|
-
'package.json',
|
|
137
|
-
'astryx.config.mjs',
|
|
138
|
-
'xds.config.mjs',
|
|
139
|
-
]);
|
|
140
|
-
|
|
141
|
-
function resolveConfigPath(relativePath) {
|
|
142
|
-
if (!CONFIG_CODEMOD_PATHS.has(relativePath)) {
|
|
143
|
-
throw new Error(`unsupported config codemod path: ${relativePath}`);
|
|
144
|
-
}
|
|
145
|
-
return path.resolve(process.cwd(), relativePath);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function readOptionalConfigFile(relativePath) {
|
|
149
|
-
const fullPath = resolveConfigPath(relativePath);
|
|
150
|
-
if (!fs.existsSync(fullPath)) return null;
|
|
151
|
-
return {path: relativePath, source: fs.readFileSync(fullPath, 'utf-8')};
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function getConfigCodemodContext() {
|
|
155
|
-
return {
|
|
156
|
-
packageJson: readOptionalConfigFile('package.json'),
|
|
157
|
-
astryxConfig: readOptionalConfigFile('astryx.config.mjs'),
|
|
158
|
-
xdsConfig: readOptionalConfigFile('xds.config.mjs'),
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
async function runConfigCodemod(transformEntry, {apply, log}) {
|
|
163
|
-
const {transform} = transformEntry;
|
|
164
|
-
const api = {config: getConfigCodemodContext()};
|
|
165
|
-
const result = await transform({path: process.cwd(), source: ''}, api);
|
|
166
|
-
const errors = result?.errors ?? [];
|
|
167
|
-
if (errors.length > 0) {
|
|
168
|
-
for (const error of errors) {
|
|
169
|
-
log.error(` ✗ ${error.file ?? 'config'} — ${error.error}`);
|
|
170
|
-
}
|
|
171
|
-
return {filesChanged: 0, errors};
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const changes = result?.changes ?? [];
|
|
175
|
-
if (changes.length === 0) return {filesChanged: 0, errors: []};
|
|
176
|
-
|
|
177
|
-
for (const change of changes) {
|
|
178
|
-
const fullPath = resolveConfigPath(change.path);
|
|
179
|
-
if (change.delete) {
|
|
180
|
-
if (apply) fs.rmSync(fullPath, {force: true});
|
|
181
|
-
log[apply ? 'success' : 'warn'](
|
|
182
|
-
` ${apply ? '✓' : '~'} ${change.path} (delete${apply ? '' : ', dry run'})`,
|
|
183
|
-
);
|
|
184
|
-
continue;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (apply) {
|
|
188
|
-
fs.writeFileSync(fullPath, change.source, 'utf-8');
|
|
189
|
-
}
|
|
190
|
-
log[apply ? 'success' : 'warn'](
|
|
191
|
-
` ${apply ? '✓' : '~'} ${change.path}${apply ? '' : ' (would change)'}`,
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
if (changes.length > 0) {
|
|
196
|
-
const verb = apply ? 'Updated' : 'Would update';
|
|
197
|
-
log.info(
|
|
198
|
-
` ${verb} ${changes.length} config file${changes.length === 1 ? '' : 's'}`,
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
return {
|
|
203
|
-
filesChanged: changes.length,
|
|
204
|
-
writtenFiles: changes
|
|
205
|
-
.filter(change => !change.delete && path.extname(change.path) !== '.json')
|
|
206
|
-
.map(change => resolveConfigPath(change.path)),
|
|
207
|
-
errors: [],
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
|
|
211
165
|
/**
|
|
212
166
|
* Run codemods against source files.
|
|
213
167
|
*
|
|
@@ -243,12 +197,18 @@ export async function runCodemods(
|
|
|
243
197
|
|
|
244
198
|
if (files.length === 0) {
|
|
245
199
|
log.warn('No source files found.');
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
200
|
+
return {
|
|
201
|
+
ok: true,
|
|
202
|
+
totalFilesChanged: 0,
|
|
203
|
+
totalTransformsApplied: 0,
|
|
204
|
+
errors: [],
|
|
205
|
+
writtenFiles: [],
|
|
206
|
+
skippedOptional: [],
|
|
207
|
+
};
|
|
250
208
|
}
|
|
251
209
|
|
|
210
|
+
log.info(`Found ${files.length} source file${files.length === 1 ? '' : 's'}`);
|
|
211
|
+
|
|
252
212
|
// Dynamically import jscodeshift
|
|
253
213
|
const jscodeshift = (await import('jscodeshift')).default;
|
|
254
214
|
|
|
@@ -279,24 +239,6 @@ export async function runCodemods(
|
|
|
279
239
|
|
|
280
240
|
log.info(` ${meta.title}`);
|
|
281
241
|
|
|
282
|
-
if (isConfigCodemod(transformEntry)) {
|
|
283
|
-
const result = await runConfigCodemod(transformEntry, {apply, log});
|
|
284
|
-
if (result.errors.length > 0) {
|
|
285
|
-
for (const error of result.errors) {
|
|
286
|
-
errors.push({
|
|
287
|
-
file: error.file ?? 'config',
|
|
288
|
-
codemod: name,
|
|
289
|
-
error: error.error,
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
|
-
} else if (result.filesChanged > 0) {
|
|
293
|
-
totalFilesChanged += result.filesChanged;
|
|
294
|
-
totalTransformsApplied += result.filesChanged;
|
|
295
|
-
writtenFiles.push(...(result.writtenFiles ?? []));
|
|
296
|
-
}
|
|
297
|
-
continue;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
242
|
let filesChanged = 0;
|
|
301
243
|
|
|
302
244
|
for (const filePath of files) {
|
|
@@ -381,6 +323,12 @@ export async function runCodemods(
|
|
|
381
323
|
}
|
|
382
324
|
}
|
|
383
325
|
|
|
326
|
+
// Post-codemod formatting: run the project's formatter on changed files
|
|
327
|
+
// so codemods don't introduce style drift (jscodeshift may change quotes, etc.)
|
|
328
|
+
if (apply && writtenFiles.length > 0) {
|
|
329
|
+
await formatChangedFiles(writtenFiles, silent);
|
|
330
|
+
}
|
|
331
|
+
|
|
384
332
|
if (totalValidationBlocked > 0) {
|
|
385
333
|
log.warn(
|
|
386
334
|
`${totalValidationBlocked} file${totalValidationBlocked === 1 ? ' was' : 's were'} blocked by validation — no changes written to ${totalValidationBlocked === 1 ? 'that file' : 'those files'}.`,
|
|
@@ -418,9 +366,7 @@ export async function runCodemods(
|
|
|
418
366
|
if (meta.description) {
|
|
419
367
|
log.info(` ${meta.description}`);
|
|
420
368
|
}
|
|
421
|
-
log.info(
|
|
422
|
-
` Run: astryx upgrade --codemod ${name} --path <dir> --apply`,
|
|
423
|
-
);
|
|
369
|
+
log.info(` Run: astryx upgrade --codemod ${name} --path <dir> --apply`);
|
|
424
370
|
}
|
|
425
371
|
}
|
|
426
372
|
|
|
@@ -93,55 +93,14 @@ export function resolveAgentPaths(targetDir, agent) {
|
|
|
93
93
|
return {inject: [], create: [searchPaths[searchPaths.length - 1]]};
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
/**
|
|
97
|
-
* Detect which styling system the consumer project has wired up, so the agent
|
|
98
|
-
* docs recommend a path that actually compiles in THIS project.
|
|
99
|
-
*
|
|
100
|
-
* `xstyle`/StyleX needs the StyleX compiler (the `@stylexjs/stylex` runtime
|
|
101
|
-
* alone throws at runtime → blank page); Tailwind utilities need Tailwind.
|
|
102
|
-
* Recommending either when it isn't configured yields unstyled or blank output.
|
|
103
|
-
* Plain CSS variables (via `style`/`className`) always work, so they're the
|
|
104
|
-
* safe default. Precedence: stylex (compiler wired) → tailwind → css.
|
|
105
|
-
*
|
|
106
|
-
* @param {string} targetDir
|
|
107
|
-
* @returns {'stylex' | 'tailwind' | 'css'}
|
|
108
|
-
*/
|
|
109
|
-
export function detectStylingSystem(targetDir) {
|
|
110
|
-
try {
|
|
111
|
-
const pkgPath = path.join(targetDir, 'package.json');
|
|
112
|
-
if (!fs.existsSync(pkgPath)) return 'css';
|
|
113
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
114
|
-
const deps = {...pkg.dependencies, ...pkg.devDependencies};
|
|
115
|
-
// Key off a StyleX *compiler* plugin — the runtime alone won't render.
|
|
116
|
-
const stylexCompilers = [
|
|
117
|
-
'@stylexjs/babel-plugin',
|
|
118
|
-
'vite-plugin-stylex',
|
|
119
|
-
'unplugin-stylex',
|
|
120
|
-
'@stylexswc/unplugin',
|
|
121
|
-
'@stylexswc/nextjs-plugin',
|
|
122
|
-
'stylex-webpack',
|
|
123
|
-
];
|
|
124
|
-
if (stylexCompilers.some(d => d in deps)) return 'stylex';
|
|
125
|
-
if ('tailwindcss' in deps) return 'tailwind';
|
|
126
|
-
return 'css';
|
|
127
|
-
} catch {
|
|
128
|
-
// Best-effort: default to the universally-safe CSS-variable path.
|
|
129
|
-
return 'css';
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
96
|
/**
|
|
134
97
|
* Generate the agent cheat sheet from live CLI metadata.
|
|
135
98
|
*
|
|
136
99
|
* Structured as: workflow (behavioral) → rules (error prevention) → CLI reference.
|
|
137
100
|
* Templates are positioned first in the workflow to teach agents the
|
|
138
101
|
* "look at reference code" reflex before writing any UI.
|
|
139
|
-
*
|
|
140
|
-
* `stylingSystem` tailors the custom-styling guidance to what the project has
|
|
141
|
-
* configured (see {@link detectStylingSystem}) so the agent never reaches for a
|
|
142
|
-
* styling path that isn't compiled here.
|
|
143
102
|
*/
|
|
144
|
-
export function generateCompressedIndex(version, {coreDir, runPrefix = getRunPrefix()
|
|
103
|
+
export function generateCompressedIndex(version, {coreDir, runPrefix = getRunPrefix()} = {}) {
|
|
145
104
|
const run = `${runPrefix} astryx`;
|
|
146
105
|
const lines = [MARKER_START];
|
|
147
106
|
|
|
@@ -158,57 +117,69 @@ export function generateCompressedIndex(version, {coreDir, runPrefix = getRunPre
|
|
|
158
117
|
}
|
|
159
118
|
}
|
|
160
119
|
|
|
161
|
-
// Header
|
|
162
|
-
lines.push(`Astryx v${version}
|
|
163
|
-
lines.push(`CLI: run every command as \`${run} <cmd>\` (shown below as \`astryx ...\`).`);
|
|
120
|
+
// Header
|
|
121
|
+
lines.push(`Astryx v${version} — ${componentCount} components`);
|
|
164
122
|
lines.push('');
|
|
165
123
|
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
lines.push(
|
|
169
|
-
lines.push(
|
|
170
|
-
lines.push(
|
|
124
|
+
// Behavioral workflow — templates first, then component lookup
|
|
125
|
+
lines.push('Before writing any UI code:');
|
|
126
|
+
lines.push(`1. \`${run} template --list\` — find a related page pattern`);
|
|
127
|
+
lines.push(`2. \`${run} template <name> --skeleton\` — study layout structure (gap, padding, nesting)`);
|
|
128
|
+
lines.push(`3. \`${run} component <Name>\` — read props + examples for EVERY component you use`);
|
|
171
129
|
lines.push('');
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
lines.push("WORKFLOW — discover, don't guess. Before writing UI:");
|
|
175
|
-
lines.push('1. `astryx build "<idea>"` — START HERE: returns a kit (closest [page] + [block]s + [component]s). No args = full playbook.');
|
|
176
|
-
lines.push('2. `astryx template <name> [--skeleton]` — scaffold the [page]/[block]s it named, or study their layout. Templates are reference code.');
|
|
177
|
-
lines.push('3. `astryx component <Name>` — props + examples for every component you use.');
|
|
130
|
+
lines.push('Templates are reference code — read them for composition patterns, not just scaffolding.');
|
|
131
|
+
lines.push('Full pages → dashboard (uses AppShell). Forms → contact-form. Tables → data-table. Settings → settings-sidebar.');
|
|
178
132
|
lines.push('');
|
|
179
133
|
|
|
180
|
-
// Rules — the top error
|
|
181
|
-
lines.push('
|
|
182
|
-
lines.push('-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
lines.push('- Custom styling: component props first; else the xstyle prop / StyleX tokens (@astryxdesign/core/theme/tokens.stylex). No raw hex/px.');
|
|
188
|
-
} else if (stylingSystem === 'tailwind') {
|
|
189
|
-
lines.push('- Custom styling: component props first; else Tailwind utilities backed by tokens (bg-surface, text-primary, rounded-lg) via tailwind-theme.css. No raw hex/px.');
|
|
190
|
-
} else {
|
|
191
|
-
lines.push("- Custom styling: component props first; else style/className with tokens — var(--color-*|--spacing-*|--radius-*). No raw hex/px. (No StyleX/Tailwind compiler here — don't use xstyle/utility classes.)");
|
|
192
|
-
}
|
|
193
|
-
lines.push('- Tokens for every value (`astryx docs tokens`). Brand/accent via `astryx theme` — never override --color-* in :root.');
|
|
134
|
+
// Rules — inline, compact, prevents the top error categories
|
|
135
|
+
lines.push('No <div> anywhere — not for layout, not for wrappers, not for spacing. Use components.');
|
|
136
|
+
lines.push('Full-page shells → AppShell (not Layout). Sidebar nav → SideNav (not List).');
|
|
137
|
+
lines.push('No style={{}} — use the xstyle prop on components for custom styling.');
|
|
138
|
+
lines.push('If a component prop does what you need, use it — never replicate with CSS/stylex.');
|
|
139
|
+
lines.push(`No magic values — run \`${run} docs tokens\` for spacing/color/radius.`);
|
|
140
|
+
lines.push(`To change accent/brand colors: \`${run} theme\` — never override --astryx-color-* in :root.`);
|
|
194
141
|
lines.push('');
|
|
195
142
|
|
|
196
|
-
//
|
|
197
|
-
lines.push(
|
|
198
|
-
lines.push(
|
|
199
|
-
|
|
200
|
-
|
|
143
|
+
// CLI quick reference
|
|
144
|
+
lines.push(`${run} component --list ${componentCount} components by category`);
|
|
145
|
+
lines.push(`${run} component <Name> props, types, examples`);
|
|
146
|
+
|
|
147
|
+
// Doc topics from live discovery
|
|
201
148
|
const docsDir = path.join(CLI_ROOT, 'docs');
|
|
202
149
|
if (fs.existsSync(docsDir)) {
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
150
|
+
for (const file of fs.readdirSync(docsDir).sort()) {
|
|
151
|
+
const match = file.match(/^(\w+)\.doc\.mjs$/);
|
|
152
|
+
if (!match) continue;
|
|
153
|
+
const topic = match[1];
|
|
154
|
+
let desc = topic;
|
|
155
|
+
try {
|
|
156
|
+
const fileContent = fs.readFileSync(path.join(docsDir, file), 'utf-8');
|
|
157
|
+
const descMatch = fileContent.match(/description:\s*['"](.+?)['"]/);
|
|
158
|
+
if (descMatch) desc = descMatch[1];
|
|
159
|
+
} catch {
|
|
160
|
+
// Best-effort: fall back to the topic name if the file is unreadable.
|
|
161
|
+
}
|
|
162
|
+
if (desc.length > 50) desc = desc.slice(0, 47) + '...';
|
|
163
|
+
lines.push(`${run} docs ${topic.padEnd(20)} ${desc}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Templates
|
|
168
|
+
const templatesDir = path.join(CLI_ROOT, 'templates');
|
|
169
|
+
if (fs.existsSync(templatesDir)) {
|
|
170
|
+
const templates = fs.readdirSync(templatesDir, {withFileTypes: true})
|
|
171
|
+
.filter(e => e.isDirectory())
|
|
172
|
+
.map(e => e.name)
|
|
207
173
|
.sort();
|
|
208
|
-
if (
|
|
174
|
+
if (templates.length > 0) {
|
|
175
|
+
lines.push(`${run} template --list page recipes with component lists`);
|
|
176
|
+
lines.push(`${run} template <name> [path] scaffold from template`);
|
|
177
|
+
}
|
|
209
178
|
}
|
|
210
|
-
|
|
211
|
-
lines.push(
|
|
179
|
+
|
|
180
|
+
lines.push(`${run} swizzle <Name> eject source (--gap to report why)`);
|
|
181
|
+
lines.push(`${run} upgrade --apply codemods after version bump`);
|
|
182
|
+
lines.push(`after @astryxdesign/core bump, always run ${run} upgrade --apply`);
|
|
212
183
|
lines.push(MARKER_END);
|
|
213
184
|
|
|
214
185
|
return lines.join('\n');
|
|
@@ -287,10 +258,7 @@ export function injectXdsBlock(filePath, compressedIndex, {createIfMissing = fal
|
|
|
287
258
|
*/
|
|
288
259
|
export function injectAgentsMd(targetDir, version) {
|
|
289
260
|
const agentsPath = path.join(targetDir, AGENTS_MD);
|
|
290
|
-
const compressedIndex = generateCompressedIndex(version, {
|
|
291
|
-
coreDir: findCoreDir(targetDir),
|
|
292
|
-
stylingSystem: detectStylingSystem(targetDir),
|
|
293
|
-
});
|
|
261
|
+
const compressedIndex = generateCompressedIndex(version, {coreDir: findCoreDir(targetDir)});
|
|
294
262
|
injectXdsBlock(agentsPath, compressedIndex, {
|
|
295
263
|
createIfMissing: true,
|
|
296
264
|
header: `# AGENTS.md\n\nProject-specific guidance for AI coding agents.`,
|
|
@@ -305,10 +273,7 @@ export function injectAgentsMd(targetDir, version) {
|
|
|
305
273
|
*/
|
|
306
274
|
export function injectClaudeMd(targetDir, version) {
|
|
307
275
|
const claudePath = path.join(targetDir, CLAUDE_MD);
|
|
308
|
-
const compressedIndex = generateCompressedIndex(version, {
|
|
309
|
-
coreDir: findCoreDir(targetDir),
|
|
310
|
-
stylingSystem: detectStylingSystem(targetDir),
|
|
311
|
-
});
|
|
276
|
+
const compressedIndex = generateCompressedIndex(version, {coreDir: findCoreDir(targetDir)});
|
|
312
277
|
return injectXdsBlock(claudePath, compressedIndex);
|
|
313
278
|
}
|
|
314
279
|
|
|
@@ -391,8 +356,7 @@ export function installAgentDocs(targetDir, {zh = false, lang, agent, paths, onl
|
|
|
391
356
|
const coreDir = findCoreDir(targetDir);
|
|
392
357
|
const version = getXdsVersion(coreDir);
|
|
393
358
|
const runPrefix = getRunPrefix(targetDir);
|
|
394
|
-
const
|
|
395
|
-
const compressedIndex = generateCompressedIndex(version, {coreDir, zh, lang, runPrefix, stylingSystem});
|
|
359
|
+
const compressedIndex = generateCompressedIndex(version, {coreDir, zh, lang, runPrefix});
|
|
396
360
|
const written = [];
|
|
397
361
|
|
|
398
362
|
// Explicit paths override everything
|
|
@@ -6,7 +6,6 @@ import * as path from 'node:path';
|
|
|
6
6
|
import * as os from 'node:os';
|
|
7
7
|
import {
|
|
8
8
|
generateCompressedIndex,
|
|
9
|
-
detectStylingSystem,
|
|
10
9
|
getXdsVersion,
|
|
11
10
|
installAgentDocs,
|
|
12
11
|
injectAgentsMd,
|
|
@@ -40,86 +39,31 @@ describe('generateCompressedIndex', () => {
|
|
|
40
39
|
it('includes theme nudge rule', () => {
|
|
41
40
|
const result = generateCompressedIndex('1.0.0');
|
|
42
41
|
expect(result).toMatch(/astryx theme/);
|
|
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/);
|
|
42
|
+
expect(result).toMatch(/never override --astryx-color/);
|
|
63
43
|
});
|
|
64
44
|
|
|
65
45
|
it('includes upgrade command and migration rule', () => {
|
|
66
46
|
const result = generateCompressedIndex('1.0.0');
|
|
67
|
-
expect(result).toContain('upgrade
|
|
68
|
-
expect(result).
|
|
47
|
+
expect(result).toContain('astryx upgrade');
|
|
48
|
+
expect(result).toContain('astryx upgrade --apply');
|
|
49
|
+
expect(result).toMatch(/always run .+ astryx upgrade --apply/);
|
|
69
50
|
});
|
|
70
51
|
|
|
71
|
-
it('
|
|
52
|
+
it('uses custom runPrefix when provided', () => {
|
|
72
53
|
const result = generateCompressedIndex('1.0.0', {runPrefix: 'yarn'});
|
|
73
|
-
expect(result).toContain('yarn astryx <
|
|
54
|
+
expect(result).toContain('yarn astryx component <Name>');
|
|
55
|
+
expect(result).toContain('yarn astryx upgrade --apply');
|
|
56
|
+
expect(result).toContain('after @astryxdesign/core bump, always run yarn astryx upgrade --apply');
|
|
74
57
|
expect(result).not.toContain('npx astryx');
|
|
75
58
|
});
|
|
76
59
|
|
|
77
60
|
it('uses pnpm exec prefix', () => {
|
|
78
61
|
const result = generateCompressedIndex('1.0.0', {runPrefix: 'pnpm exec'});
|
|
79
|
-
expect(result).toContain('pnpm exec astryx <
|
|
62
|
+
expect(result).toContain('pnpm exec astryx component <Name>');
|
|
80
63
|
expect(result).not.toContain('npx astryx');
|
|
81
64
|
});
|
|
82
65
|
});
|
|
83
66
|
|
|
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
|
-
|
|
123
67
|
describe('getXdsVersion', () => {
|
|
124
68
|
it('reads version from core package.json', () => {
|
|
125
69
|
const coreDir = path.join(tmpDir, 'core');
|