@astryxdesign/cli 0.1.0-canary.6fc892b → 0.1.0-canary.7517fdf
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/icons.doc.mjs +1 -1
- package/docs/migration.doc.mjs +2 -2
- package/docs/shape.doc.mjs +1 -1
- package/docs/styling.doc.mjs +2 -2
- 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 +3 -3
- package/package.json +8 -8
- package/src/commands/agent-docs.mjs +89 -70
- package/src/commands/agent-docs.path-safety.test.mjs +1 -1
- package/src/commands/agent-docs.test.mjs +66 -10
- 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/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/interactive-guard.test.mjs +1 -1
- package/src/commands/json-contract.test.mjs +1 -1
- 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.test.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.test.mjs +1 -1
package/docs/icons.doc.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* @file Icons reference doc: semantic icon names available in
|
|
4
|
+
* @file Icons reference doc: semantic icon names available in Astryx
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
/** @type {import('../../core/src/docs-types').ReferenceDoc} */
|
package/docs/migration.doc.mjs
CHANGED
|
@@ -234,7 +234,7 @@ export function AppRoot({children}: {children: React.ReactNode}) {
|
|
|
234
234
|
type: 'code',
|
|
235
235
|
lang: 'text',
|
|
236
236
|
label: 'Paste this into your AI',
|
|
237
|
-
code: `We are migrating this existing Tailwind/shadcn app to
|
|
237
|
+
code: `We are migrating this existing Tailwind/shadcn app to Astryx incrementally.
|
|
238
238
|
|
|
239
239
|
First run:
|
|
240
240
|
- npx astryx docs migration --dense
|
|
@@ -242,7 +242,7 @@ First run:
|
|
|
242
242
|
- npx astryx docs styling --dense
|
|
243
243
|
- npx astryx template AppShellTopNavWithSideNav --skeleton
|
|
244
244
|
|
|
245
|
-
Then migrate one route or shell surface at a time. Keep business logic and routing intact. Replace shadcn/Radix/Tailwind primitives with
|
|
245
|
+
Then migrate one route or shell surface at a time. Keep business logic and routing intact. Replace shadcn/Radix/Tailwind primitives with Astryx components, remove hardcoded colors, verify light and dark mode, and take screenshots before moving to the next surface.`,
|
|
246
246
|
},
|
|
247
247
|
],
|
|
248
248
|
},
|
package/docs/shape.doc.mjs
CHANGED
|
@@ -44,7 +44,7 @@ export const docs = {
|
|
|
44
44
|
type: 'code',
|
|
45
45
|
lang: 'css',
|
|
46
46
|
label: 'Concentric radius formula',
|
|
47
|
-
code: `/* Automatic in
|
|
47
|
+
code: `/* Automatic in Astryx Card */
|
|
48
48
|
--card-concentric-radius: max(0px, calc(var(--_card-radius) - var(--card-padding)));`,
|
|
49
49
|
},
|
|
50
50
|
],
|
package/docs/styling.doc.mjs
CHANGED
|
@@ -231,7 +231,7 @@ const overrides = stylex.create({
|
|
|
231
231
|
content: [
|
|
232
232
|
{
|
|
233
233
|
type: 'prose',
|
|
234
|
-
text: 'When external CSS needs to target an
|
|
234
|
+
text: 'When external CSS needs to target an Astryx component by prop or state, combine the stable component class with reflected data attributes. The component class identifies the component (`.astryx-button`, `.astryx-card`); data attributes identify the axis and value (`data-variant`, `data-size`, `data-level`, etc.). This is the preferred selector surface for new CSS because it is explicit and collision-resistant.',
|
|
235
235
|
},
|
|
236
236
|
{
|
|
237
237
|
type: 'code',
|
|
@@ -273,7 +273,7 @@ const overrides = stylex.create({
|
|
|
273
273
|
content: [
|
|
274
274
|
{
|
|
275
275
|
type: 'prose',
|
|
276
|
-
text: '
|
|
276
|
+
text: 'Astryx still emits legacy bare prop/state classes such as `.primary`, `.sm`, `.level-2`, and `.checked` for compatibility with existing apps and built themes. Do not write new CSS against these bare classes. The stable base component classes (`.astryx-button`, `.astryx-card`, etc.) are not deprecated; only the unprefixed prop/state classes are the legacy surface.',
|
|
277
277
|
},
|
|
278
278
|
{
|
|
279
279
|
type: 'code',
|
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, { 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).' }] },
|
|
8
|
+
{ title: 'Quick Start', content: [null, null, null, null, { type: 'prose', text: 'default import = runtime injection. /built import = pre-compiled CSS (pair with theme.css).' }] },
|
|
9
|
+
{ title: 'Themes', content: [null, 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,6 +14,12 @@ 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
|
+
},
|
|
17
23
|
{
|
|
18
24
|
type: 'code',
|
|
19
25
|
lang: 'tsx',
|
|
@@ -45,6 +51,10 @@ function App() {
|
|
|
45
51
|
);
|
|
46
52
|
}`,
|
|
47
53
|
},
|
|
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
|
+
},
|
|
48
58
|
{
|
|
49
59
|
type: 'prose',
|
|
50
60
|
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.',
|
|
@@ -55,6 +65,10 @@ function App() {
|
|
|
55
65
|
title: 'Available Themes',
|
|
56
66
|
category: 'guide',
|
|
57
67
|
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
|
+
},
|
|
58
72
|
{
|
|
59
73
|
type: 'table',
|
|
60
74
|
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, { 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)。' }] },
|
|
8
|
+
{ title: '快速开始', content: [null, null, null, null, { type: 'prose', text: '默认导入使用运行时样式注入。/built 导入使用预编译 CSS(需配合 theme.css)。' }] },
|
|
9
|
+
{ title: '可用主题', content: [null, 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] },
|
|
@@ -34,7 +34,7 @@ export const docs = {
|
|
|
34
34
|
type: 'code',
|
|
35
35
|
lang: 'text',
|
|
36
36
|
label: 'Paste this into your AI',
|
|
37
|
-
code: 'Install @astryxdesign/cli and run `npx astryx agent-docs` to set up your
|
|
37
|
+
code: 'Install @astryxdesign/cli and run `npx astryx agent-docs` to set up your Astryx context. Read the generated file.',
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
type: 'prose',
|
|
@@ -103,7 +103,7 @@ npx astryx agent-docs --agent-docs-path ~/.cursor/rules/xds.mdc`,
|
|
|
103
103
|
type: 'code',
|
|
104
104
|
lang: 'text',
|
|
105
105
|
label: 'Paste this into your AI',
|
|
106
|
-
code: `Before writing any
|
|
106
|
+
code: `Before writing any Astryx code, check your knowledge:
|
|
107
107
|
|
|
108
108
|
1. What is the correct import path for Button?
|
|
109
109
|
2. How do you make an Dialog non-dismissible?
|
|
@@ -165,7 +165,7 @@ npx astryx docs tokens --dense`,
|
|
|
165
165
|
content: [
|
|
166
166
|
{
|
|
167
167
|
type: 'prose',
|
|
168
|
-
text: '
|
|
168
|
+
text: 'Astryx ships a Model Context Protocol (MCP) server that any MCP-compatible AI tool can connect to. Instead of manually pasting CLI output, the AI can query the Astryx design system directly, searching for components, reading full documentation, and pulling code examples on demand.',
|
|
169
169
|
},
|
|
170
170
|
{
|
|
171
171
|
type: 'prose',
|
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.7517fdf",
|
|
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
|
+
"astryx",
|
|
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.7517fdf",
|
|
58
|
+
"@astryxdesign/lab": "0.1.0-canary.7517fdf",
|
|
59
|
+
"@astryxdesign/theme-neutral": "0.1.0-canary.7517fdf"
|
|
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.7517fdf",
|
|
74
|
+
"@astryxdesign/lab": "0.1.0-canary.7517fdf",
|
|
75
|
+
"@astryxdesign/theme-neutral": "0.1.0-canary.7517fdf"
|
|
76
76
|
},
|
|
77
77
|
"scripts": {
|
|
78
78
|
"astryx": "node bin/astryx.mjs",
|
|
@@ -93,14 +93,55 @@ 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
|
+
|
|
96
133
|
/**
|
|
97
134
|
* Generate the agent cheat sheet from live CLI metadata.
|
|
98
135
|
*
|
|
99
136
|
* Structured as: workflow (behavioral) → rules (error prevention) → CLI reference.
|
|
100
137
|
* Templates are positioned first in the workflow to teach agents the
|
|
101
138
|
* "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.
|
|
102
143
|
*/
|
|
103
|
-
export function generateCompressedIndex(version, {coreDir, runPrefix = getRunPrefix()} = {}) {
|
|
144
|
+
export function generateCompressedIndex(version, {coreDir, runPrefix = getRunPrefix(), stylingSystem = 'css'} = {}) {
|
|
104
145
|
const run = `${runPrefix} astryx`;
|
|
105
146
|
const lines = [MARKER_START];
|
|
106
147
|
|
|
@@ -117,86 +158,57 @@ export function generateCompressedIndex(version, {coreDir, runPrefix = getRunPre
|
|
|
117
158
|
}
|
|
118
159
|
}
|
|
119
160
|
|
|
120
|
-
// Header
|
|
121
|
-
lines.push(`Astryx v${version}
|
|
161
|
+
// Header — state the CLI prefix once; commands below are shown as `astryx <cmd>`.
|
|
162
|
+
lines.push(`Astryx v${version} · ${componentCount} components`);
|
|
163
|
+
lines.push(`CLI: run every command as \`${run} <cmd>\` (shown below as \`astryx ...\`).`);
|
|
122
164
|
lines.push('');
|
|
123
165
|
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
// astryx.css); only the CSS imports are required.
|
|
128
|
-
lines.push('SETUP (required, once) — in your app entry (e.g. main.tsx):');
|
|
166
|
+
// Required setup — components ship precompiled CSS; without these imports
|
|
167
|
+
// everything renders unstyled. Theme is optional (a default ships in astryx.css).
|
|
168
|
+
lines.push('SETUP (once, in your app entry e.g. main.tsx) — without these, components render unstyled:');
|
|
129
169
|
lines.push(' import "@astryxdesign/core/reset.css";');
|
|
130
170
|
lines.push(' import "@astryxdesign/core/astryx.css";');
|
|
131
|
-
lines.push('Without these CSS imports, components render completely unstyled.');
|
|
132
|
-
lines.push(`Optional: apply a specific theme with <Theme> — see \`${run} docs theme\`.`);
|
|
133
171
|
lines.push('');
|
|
134
172
|
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
lines.push('
|
|
138
|
-
lines.push(
|
|
139
|
-
|
|
140
|
-
);
|
|
141
|
-
lines.push(`2. \`${run} template <name> --skeleton\` — scaffold a matched [page], or study its layout (gap, padding, nesting)`);
|
|
142
|
-
lines.push(`3. \`${run} template <BlockName>\` — drop in each [block] the kit surfaced (ready-made multi-component patterns) instead of hand-building`);
|
|
143
|
-
lines.push(`4. \`${run} component <Name>\` — read props + examples for EVERY component you use`);
|
|
144
|
-
lines.push('');
|
|
145
|
-
lines.push('Templates and blocks are reference code — read them for composition patterns, not just scaffolding.');
|
|
146
|
-
lines.push(`(\`${run} search <query>\` is a neutral find across components/hooks/docs/templates when you just need to look something up.)`);
|
|
173
|
+
// Workflow — `build` is the front door; discover before writing UI.
|
|
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.');
|
|
147
178
|
lines.push('');
|
|
148
179
|
|
|
149
|
-
// Rules —
|
|
150
|
-
lines.push('
|
|
151
|
-
lines.push('
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
180
|
+
// Rules — the top error-preventers.
|
|
181
|
+
lines.push('RULES:');
|
|
182
|
+
lines.push('- No <div> — components do all layout/spacing. Full page → AppShell; sidebar nav → SideNav.');
|
|
183
|
+
// Styling guidance tailored to the project's configured system — never
|
|
184
|
+
// recommend a path that isn't compiled here (xstyle needs the StyleX compiler;
|
|
185
|
+
// utilities need Tailwind). Tokens are always the source of truth.
|
|
186
|
+
if (stylingSystem === 'stylex') {
|
|
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.');
|
|
156
194
|
lines.push('');
|
|
157
195
|
|
|
158
|
-
//
|
|
159
|
-
lines.push(
|
|
160
|
-
lines.push(
|
|
161
|
-
lines.push(
|
|
162
|
-
lines.push(
|
|
163
|
-
|
|
164
|
-
// Doc topics from live discovery
|
|
196
|
+
// Command reference — build/template/component are covered in WORKFLOW above.
|
|
197
|
+
lines.push('MORE CLI:');
|
|
198
|
+
lines.push(' search "<query>" find any component / hook / doc / template / block');
|
|
199
|
+
lines.push(` component --list ${componentCount} components by category`);
|
|
200
|
+
lines.push(' template --list page + block recipes');
|
|
165
201
|
const docsDir = path.join(CLI_ROOT, 'docs');
|
|
166
202
|
if (fs.existsSync(docsDir)) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
let desc = topic;
|
|
172
|
-
try {
|
|
173
|
-
const fileContent = fs.readFileSync(path.join(docsDir, file), 'utf-8');
|
|
174
|
-
const descMatch = fileContent.match(/description:\s*['"](.+?)['"]/);
|
|
175
|
-
if (descMatch) desc = descMatch[1];
|
|
176
|
-
} catch {
|
|
177
|
-
// Best-effort: fall back to the topic name if the file is unreadable.
|
|
178
|
-
}
|
|
179
|
-
if (desc.length > 50) desc = desc.slice(0, 47) + '...';
|
|
180
|
-
lines.push(`${run} docs ${topic.padEnd(20)} ${desc}`);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Templates
|
|
185
|
-
const templatesDir = path.join(CLI_ROOT, 'templates');
|
|
186
|
-
if (fs.existsSync(templatesDir)) {
|
|
187
|
-
const templates = fs.readdirSync(templatesDir, {withFileTypes: true})
|
|
188
|
-
.filter(e => e.isDirectory())
|
|
189
|
-
.map(e => e.name)
|
|
203
|
+
const topics = fs.readdirSync(docsDir)
|
|
204
|
+
.map(f => f.match(/^(\w+)\.doc\.mjs$/))
|
|
205
|
+
.filter(Boolean)
|
|
206
|
+
.map(m => m[1])
|
|
190
207
|
.sort();
|
|
191
|
-
if (
|
|
192
|
-
lines.push(`${run} template --list page recipes with component lists`);
|
|
193
|
-
lines.push(`${run} template <name> [path] scaffold from template`);
|
|
194
|
-
}
|
|
208
|
+
if (topics.length > 0) lines.push(` docs <topic> ${topics.join(', ')}`);
|
|
195
209
|
}
|
|
196
|
-
|
|
197
|
-
lines.push(
|
|
198
|
-
lines.push(`${run} upgrade --apply codemods after version bump`);
|
|
199
|
-
lines.push(`after @astryxdesign/core bump, always run ${run} upgrade --apply`);
|
|
210
|
+
lines.push(' swizzle <Name> eject component source (--gap reports why)');
|
|
211
|
+
lines.push(' upgrade --apply run after any @astryxdesign/core bump');
|
|
200
212
|
lines.push(MARKER_END);
|
|
201
213
|
|
|
202
214
|
return lines.join('\n');
|
|
@@ -275,7 +287,10 @@ export function injectXdsBlock(filePath, compressedIndex, {createIfMissing = fal
|
|
|
275
287
|
*/
|
|
276
288
|
export function injectAgentsMd(targetDir, version) {
|
|
277
289
|
const agentsPath = path.join(targetDir, AGENTS_MD);
|
|
278
|
-
const compressedIndex = generateCompressedIndex(version, {
|
|
290
|
+
const compressedIndex = generateCompressedIndex(version, {
|
|
291
|
+
coreDir: findCoreDir(targetDir),
|
|
292
|
+
stylingSystem: detectStylingSystem(targetDir),
|
|
293
|
+
});
|
|
279
294
|
injectXdsBlock(agentsPath, compressedIndex, {
|
|
280
295
|
createIfMissing: true,
|
|
281
296
|
header: `# AGENTS.md\n\nProject-specific guidance for AI coding agents.`,
|
|
@@ -290,7 +305,10 @@ export function injectAgentsMd(targetDir, version) {
|
|
|
290
305
|
*/
|
|
291
306
|
export function injectClaudeMd(targetDir, version) {
|
|
292
307
|
const claudePath = path.join(targetDir, CLAUDE_MD);
|
|
293
|
-
const compressedIndex = generateCompressedIndex(version, {
|
|
308
|
+
const compressedIndex = generateCompressedIndex(version, {
|
|
309
|
+
coreDir: findCoreDir(targetDir),
|
|
310
|
+
stylingSystem: detectStylingSystem(targetDir),
|
|
311
|
+
});
|
|
294
312
|
return injectXdsBlock(claudePath, compressedIndex);
|
|
295
313
|
}
|
|
296
314
|
|
|
@@ -373,7 +391,8 @@ export function installAgentDocs(targetDir, {zh = false, lang, agent, paths, onl
|
|
|
373
391
|
const coreDir = findCoreDir(targetDir);
|
|
374
392
|
const version = getXdsVersion(coreDir);
|
|
375
393
|
const runPrefix = getRunPrefix(targetDir);
|
|
376
|
-
const
|
|
394
|
+
const stylingSystem = detectStylingSystem(targetDir);
|
|
395
|
+
const compressedIndex = generateCompressedIndex(version, {coreDir, zh, lang, runPrefix, stylingSystem});
|
|
377
396
|
const written = [];
|
|
378
397
|
|
|
379
398
|
// Explicit paths override everything
|
|
@@ -20,7 +20,7 @@ import {PathSafetyError} from '../utils/path-safety.mjs';
|
|
|
20
20
|
|
|
21
21
|
let tmpDir;
|
|
22
22
|
beforeEach(() => {
|
|
23
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), '
|
|
23
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-agent-docs-paths-'));
|
|
24
24
|
});
|
|
25
25
|
afterEach(() => {
|
|
26
26
|
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
@@ -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(() => {
|
|
@@ -39,31 +40,86 @@ describe('generateCompressedIndex', () => {
|
|
|
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');
|
|
@@ -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});
|
|
@@ -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) => {
|
|
@@ -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(() => {
|
|
@@ -14,7 +14,7 @@ let stdoutCalls;
|
|
|
14
14
|
let exitCode;
|
|
15
15
|
|
|
16
16
|
beforeEach(() => {
|
|
17
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), '
|
|
17
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-upgrade-test-'));
|
|
18
18
|
originalCwd = process.cwd();
|
|
19
19
|
process.chdir(tmpDir);
|
|
20
20
|
logCalls = [];
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
|
|
14
14
|
let tmpDir;
|
|
15
15
|
beforeEach(() => {
|
|
16
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), '
|
|
16
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-path-safety-'));
|
|
17
17
|
});
|
|
18
18
|
afterEach(() => {
|
|
19
19
|
fs.rmSync(tmpDir, {recursive: true, force: true});
|
package/src/utils/paths.test.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import {findCoreDir, findProjectRoot, listComponents, discoverExternalPackages}
|
|
|
9
9
|
let tmpDir;
|
|
10
10
|
|
|
11
11
|
function makeTmpDir() {
|
|
12
|
-
return fs.mkdtempSync(path.join(os.tmpdir(), '
|
|
12
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-paths-test-'));
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
beforeEach(() => {
|
|
@@ -69,12 +69,12 @@ describe('findProjectRoot', () => {
|
|
|
69
69
|
describe('discoverExternalPackages', () => {
|
|
70
70
|
it('finds packages with an "astryx" field in package.json', () => {
|
|
71
71
|
const nm = path.join(tmpDir, 'node_modules');
|
|
72
|
-
const extDir = path.join(nm, '
|
|
72
|
+
const extDir = path.join(nm, 'astryx-charts');
|
|
73
73
|
fs.mkdirSync(extDir, {recursive: true});
|
|
74
74
|
fs.writeFileSync(
|
|
75
75
|
path.join(extDir, 'package.json'),
|
|
76
76
|
JSON.stringify({
|
|
77
|
-
name: '
|
|
77
|
+
name: 'astryx-charts',
|
|
78
78
|
astryx: {docs: './src', category: 'Data Viz'},
|
|
79
79
|
}),
|
|
80
80
|
);
|
|
@@ -82,7 +82,7 @@ describe('discoverExternalPackages', () => {
|
|
|
82
82
|
const result = discoverExternalPackages(tmpDir);
|
|
83
83
|
expect(result).toEqual([
|
|
84
84
|
{
|
|
85
|
-
name: '
|
|
85
|
+
name: 'astryx-charts',
|
|
86
86
|
category: 'Data Viz',
|
|
87
87
|
docsDir: path.join(extDir, 'src'),
|
|
88
88
|
blocksDir: null,
|
|
@@ -92,7 +92,7 @@ describe('discoverExternalPackages', () => {
|
|
|
92
92
|
|
|
93
93
|
it('handles scoped packages (@org/pkg)', () => {
|
|
94
94
|
const nm = path.join(tmpDir, 'node_modules');
|
|
95
|
-
const scopedDir = path.join(nm, '@acme', '
|
|
95
|
+
const scopedDir = path.join(nm, '@acme', 'astryx-widgets');
|
|
96
96
|
fs.mkdirSync(scopedDir, {recursive: true});
|
|
97
97
|
fs.writeFileSync(
|
|
98
98
|
path.join(scopedDir, 'package.json'),
|
|
@@ -165,12 +165,12 @@ describe('discoverExternalPackages', () => {
|
|
|
165
165
|
|
|
166
166
|
it('walks up directories to find node_modules', () => {
|
|
167
167
|
const nm = path.join(tmpDir, 'node_modules');
|
|
168
|
-
const extDir = path.join(nm, '
|
|
168
|
+
const extDir = path.join(nm, 'astryx-ext');
|
|
169
169
|
fs.mkdirSync(extDir, {recursive: true});
|
|
170
170
|
fs.writeFileSync(
|
|
171
171
|
path.join(extDir, 'package.json'),
|
|
172
172
|
JSON.stringify({
|
|
173
|
-
name: '
|
|
173
|
+
name: 'astryx-ext',
|
|
174
174
|
astryx: {docs: './src'},
|
|
175
175
|
}),
|
|
176
176
|
);
|
|
@@ -180,7 +180,7 @@ describe('discoverExternalPackages', () => {
|
|
|
180
180
|
|
|
181
181
|
const result = discoverExternalPackages(nested);
|
|
182
182
|
expect(result).toHaveLength(1);
|
|
183
|
-
expect(result[0].name).toBe('
|
|
183
|
+
expect(result[0].name).toBe('astryx-ext');
|
|
184
184
|
});
|
|
185
185
|
|
|
186
186
|
it('handles invalid JSON in package.json gracefully', () => {
|
|
@@ -14,7 +14,7 @@ let tmpDir;
|
|
|
14
14
|
const originalEnv = {};
|
|
15
15
|
|
|
16
16
|
beforeEach(() => {
|
|
17
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), '
|
|
17
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-update-check-'));
|
|
18
18
|
// Save and clear env
|
|
19
19
|
originalEnv.ASTRYX_LATEST_VERSION = process.env.ASTRYX_LATEST_VERSION;
|
|
20
20
|
delete process.env.ASTRYX_LATEST_VERSION;
|