@astryxdesign/cli 0.1.6-canary.ff5dfca → 0.1.7-canary.04cd8f7
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 +33 -0
- package/README.md +115 -19
- package/docs/cli-integrations.doc.mjs +150 -0
- package/docs/getting-started.doc.mjs +9 -9
- package/docs/migration.doc.mjs +18 -18
- package/docs/principles.doc.dense.mjs +1 -1
- package/docs/principles.doc.mjs +6 -6
- package/docs/principles.doc.zh.mjs +1 -1
- package/docs/styling-libraries.doc.mjs +3 -3
- package/docs/styling.doc.mjs +4 -4
- package/docs/theme.doc.dense.mjs +2 -2
- package/docs/theme.doc.mjs +7 -7
- package/docs/theme.doc.zh.mjs +1 -1
- package/docs/tokens.doc.mjs +1 -1
- package/docs/working-with-ai.doc.mjs +18 -18
- package/package.json +9 -10
- package/src/api/doctor.mjs +3 -3
- package/src/codemods/ensure-jscodeshift.mjs +11 -27
- package/src/codemods/run-codemod.mjs +1 -1
- package/src/codemods/runner.mjs +2 -2
- package/src/commands/agent-docs.mjs +7 -8
- package/src/commands/agent-docs.test.mjs +11 -4
- package/src/commands/build-theme.mjs +10 -71
- package/src/commands/build.mjs +15 -15
- package/src/commands/component/index.mjs +4 -4
- package/src/commands/discover.mjs +7 -5
- package/src/commands/docs.mjs +4 -4
- package/src/commands/hook/index.mjs +4 -4
- package/src/commands/init.mjs +48 -152
- package/src/commands/init.next-steps.test.mjs +1 -1
- package/src/commands/interactive-guard.test.mjs +19 -22
- package/src/commands/json-contract.test.mjs +1 -1
- package/src/commands/layout.mjs +1 -1
- package/src/commands/search.mjs +4 -4
- package/src/commands/swizzle.mjs +11 -34
- package/src/commands/template.mjs +11 -31
- package/src/commands/upgrade.mjs +9 -6
- package/src/commands/upgrade.test.mjs +1 -1
- package/src/index.mjs +5 -6
- package/src/lib/component-format.mjs +2 -1
- package/src/lib/term-log.mjs +48 -0
- package/src/utils/package-manager.mjs +78 -0
- package/src/utils/package-manager.test.mjs +108 -1
- package/src/utils/path-safety.mjs +0 -18
- package/src/utils/update-check.mjs +2 -1
- package/templates/blocks/components/TabList/TabListTabsWithActions.doc.mjs +1 -1
- package/templates/blocks/components/TabList/TabListTabsWithActions.tsx +2 -7
- package/docs/integration-authoring.md +0 -105
- package/src/utils/interactive.mjs +0 -76
- package/src/utils/interactive.test.mjs +0 -70
package/docs/theme.doc.mjs
CHANGED
|
@@ -148,7 +148,7 @@ function App() {
|
|
|
148
148
|
type: 'code',
|
|
149
149
|
lang: 'bash',
|
|
150
150
|
label: 'Scaffold with CLI',
|
|
151
|
-
code: '
|
|
151
|
+
code: 'astryx theme',
|
|
152
152
|
},
|
|
153
153
|
],
|
|
154
154
|
},
|
|
@@ -287,7 +287,7 @@ const brandTheme = defineTheme({
|
|
|
287
287
|
},
|
|
288
288
|
{
|
|
289
289
|
type: 'prose',
|
|
290
|
-
text: 'Run `
|
|
290
|
+
text: 'Run `astryx component <Name>` to see a component\'s theming targets, public CSS variables, and which standard CSS properties are supported.',
|
|
291
291
|
},
|
|
292
292
|
{
|
|
293
293
|
type: 'list',
|
|
@@ -361,13 +361,13 @@ const brandTheme = defineTheme({
|
|
|
361
361
|
content: [
|
|
362
362
|
{
|
|
363
363
|
type: 'prose',
|
|
364
|
-
text: '`
|
|
364
|
+
text: '`astryx theme build` compiles a defineTheme file into production-ready artifacts. Recommended for SSR apps (Next.js, Remix) where styles must be present on first paint.',
|
|
365
365
|
},
|
|
366
366
|
{
|
|
367
367
|
type: 'code',
|
|
368
368
|
lang: 'bash',
|
|
369
369
|
label: 'Build a theme',
|
|
370
|
-
code: '
|
|
370
|
+
code: 'astryx theme build ./src/themes/ocean.ts',
|
|
371
371
|
},
|
|
372
372
|
{
|
|
373
373
|
type: 'prose',
|
|
@@ -432,7 +432,7 @@ import './themes/ocean.css';
|
|
|
432
432
|
[
|
|
433
433
|
'Import (custom theme)',
|
|
434
434
|
'defineTheme() directly',
|
|
435
|
-
"Built .js + .css from `
|
|
435
|
+
"Built .js + .css from `astryx theme build`",
|
|
436
436
|
],
|
|
437
437
|
[
|
|
438
438
|
'How it works',
|
|
@@ -462,7 +462,7 @@ import './themes/ocean.css';
|
|
|
462
462
|
items: [
|
|
463
463
|
'Use the /built subpath + theme.css for production SSR apps.',
|
|
464
464
|
'Use runtime themes during development for fast iteration.',
|
|
465
|
-
'Run `
|
|
465
|
+
'Run `astryx theme build` for custom themes to get the built artifacts.',
|
|
466
466
|
],
|
|
467
467
|
},
|
|
468
468
|
{
|
|
@@ -610,7 +610,7 @@ function ChartConfig() {
|
|
|
610
610
|
},
|
|
611
611
|
{
|
|
612
612
|
type: 'prose',
|
|
613
|
-
text: 'See `
|
|
613
|
+
text: 'See `astryx docs styling-libraries` for styling-library interop and `astryx docs tokens` for the full token reference.',
|
|
614
614
|
},
|
|
615
615
|
],
|
|
616
616
|
},
|
package/docs/theme.doc.zh.mjs
CHANGED
|
@@ -10,7 +10,7 @@ export const docsZh = {
|
|
|
10
10
|
{ section: 'Theme Props', title: 'Theme 属性', content: [null] },
|
|
11
11
|
{ section: 'Creating a Custom Theme', title: '创建自定义主题', content: [{ type: 'prose', text: '使用 CLI 向导(推荐)或手动 defineTheme。只覆盖与默认值不同的令牌。' }, null] },
|
|
12
12
|
{ section: 'defineTheme', title: 'defineTheme', content: [{ type: 'prose', text: '支持比例配置(typography、radius、motion)+ 显式令牌覆盖 + 组件覆盖。' }, null, null] },
|
|
13
|
-
{ section: 'Building Themes for Production', title: '生产构建', content: [{ type: 'prose', text: '
|
|
13
|
+
{ section: 'Building Themes for Production', title: '生产构建', content: [{ type: 'prose', text: 'astryx theme build 将 defineTheme 编译为静态 CSS。输出 .css + .js(__built:true)+ .d.ts。' }, null, null, null, null] },
|
|
14
14
|
{ section: 'Runtime vs Built Themes', title: '运行时 vs 构建', content: [{ type: 'prose', text: '运行时:useInsertionEffect 在客户端注入样式。构建:静态 CSS 在首次渲染时就存在。SSR 应用请使用 /built + theme.css。' }, null, null, null] },
|
|
15
15
|
{ section: 'Light/Dark Mode', title: '亮/暗模式', content: [{ type: 'prose', text: "令牌值使用 [light, dark] 元组实现自动模式切换。Theme 上 mode='system'(默认)跟随系统偏好。" }, null, null] },
|
|
16
16
|
{ section: 'Nesting Themes', title: '嵌套主题', content: [{ type: 'prose', text: '将不同部分包裹在独立的 <Theme> 提供者中。' }, null] },
|
package/docs/tokens.doc.mjs
CHANGED
|
@@ -1068,7 +1068,7 @@ export const docs = {
|
|
|
1068
1068
|
},
|
|
1069
1069
|
{
|
|
1070
1070
|
"type": "prose",
|
|
1071
|
-
"text": "See `
|
|
1071
|
+
"text": "See `astryx docs styling` for how to apply tokens via xstyle, className, and compound component patterns. See `astryx docs theme` for overriding tokens with defineTheme."
|
|
1072
1072
|
}
|
|
1073
1073
|
]
|
|
1074
1074
|
}
|
|
@@ -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
|
|
37
|
+
code: 'Install @astryxdesign/cli and run `npx @astryxdesign/cli init --features agents` to set up your Astryx context. Read the generated file.',
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
type: 'prose',
|
|
@@ -48,9 +48,9 @@ export const docs = {
|
|
|
48
48
|
type: 'code',
|
|
49
49
|
lang: 'bash',
|
|
50
50
|
label: 'Manual options',
|
|
51
|
-
code: `npx
|
|
52
|
-
npx
|
|
53
|
-
npx
|
|
51
|
+
code: `npx @astryxdesign/cli init --features agents --agent claude # CLAUDE.md
|
|
52
|
+
npx @astryxdesign/cli init --features agents --agent cursor # .cursorrules
|
|
53
|
+
npx @astryxdesign/cli init --features agents --agent codex # AGENTS.md (Copilot, Codex, etc.)`,
|
|
54
54
|
},
|
|
55
55
|
],
|
|
56
56
|
},
|
|
@@ -65,9 +65,9 @@ npx astryx init --features agents --agent codex # AGENTS.md (Copilot, Codex,
|
|
|
65
65
|
type: 'list',
|
|
66
66
|
style: 'ordered',
|
|
67
67
|
items: [
|
|
68
|
-
'`
|
|
69
|
-
'`
|
|
70
|
-
'`
|
|
68
|
+
'`astryx template --list`: find a related page pattern to use as reference',
|
|
69
|
+
'`astryx template <name> --skeleton`: study the layout structure',
|
|
70
|
+
'`astryx component <Name>`: read props and examples for every component used',
|
|
71
71
|
],
|
|
72
72
|
},
|
|
73
73
|
{
|
|
@@ -88,7 +88,7 @@ npx astryx init --features agents --agent codex # AGENTS.md (Copilot, Codex,
|
|
|
88
88
|
lang: 'bash',
|
|
89
89
|
label: 'Install as a Cursor user rule',
|
|
90
90
|
code: `mkdir -p ~/.cursor/rules
|
|
91
|
-
npx
|
|
91
|
+
npx @astryxdesign/cli init --features agents --agent-docs-path ~/.cursor/rules/xds.mdc`,
|
|
92
92
|
},
|
|
93
93
|
],
|
|
94
94
|
},
|
|
@@ -109,12 +109,12 @@ npx astryx init --features agents --agent-docs-path ~/.cursor/rules/xds.mdc`,
|
|
|
109
109
|
2. How do you make an Dialog non-dismissible?
|
|
110
110
|
3. What prop does Selector use for its items?
|
|
111
111
|
|
|
112
|
-
If you don't know all three, run \`npx
|
|
112
|
+
If you don't know all three, run \`npx @astryxdesign/cli init --features agents\` to generate agent docs, then read the generated file.`,
|
|
113
113
|
},
|
|
114
114
|
],
|
|
115
115
|
},
|
|
116
116
|
{
|
|
117
|
-
title: 'The
|
|
117
|
+
title: 'The astryx Pattern',
|
|
118
118
|
content: [
|
|
119
119
|
{
|
|
120
120
|
type: 'prose',
|
|
@@ -130,16 +130,16 @@ If you don't know all three, run \`npx astryx init --features agents\` to genera
|
|
|
130
130
|
},
|
|
131
131
|
{
|
|
132
132
|
type: 'prose',
|
|
133
|
-
text: 'With this alias, agents use `
|
|
133
|
+
text: 'With this alias, agents use `astryx component --list` instead of guessing the binary path. The `--` separator is standard npm convention for passing flags to scripts.',
|
|
134
134
|
},
|
|
135
135
|
{
|
|
136
136
|
type: 'code',
|
|
137
137
|
lang: 'bash',
|
|
138
138
|
label: 'Reliable CLI invocation',
|
|
139
|
-
code: `
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
139
|
+
code: `astryx component --list
|
|
140
|
+
astryx component Dialog --dense
|
|
141
|
+
astryx docs styling --dense
|
|
142
|
+
astryx docs tokens --dense`,
|
|
143
143
|
},
|
|
144
144
|
],
|
|
145
145
|
},
|
|
@@ -154,9 +154,9 @@ npx astryx docs tokens --dense`,
|
|
|
154
154
|
type: 'code',
|
|
155
155
|
lang: 'bash',
|
|
156
156
|
label: 'Dense output for pasting into AI conversations',
|
|
157
|
-
code: `
|
|
158
|
-
|
|
159
|
-
|
|
157
|
+
code: `astryx component Dialog --dense
|
|
158
|
+
astryx docs styling --dense
|
|
159
|
+
astryx docs tokens --dense`,
|
|
160
160
|
},
|
|
161
161
|
],
|
|
162
162
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7-canary.04cd8f7",
|
|
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",
|
|
@@ -72,17 +72,16 @@
|
|
|
72
72
|
"CHANGELOG.md"
|
|
73
73
|
],
|
|
74
74
|
"dependencies": {
|
|
75
|
-
"@clack/prompts": "^1.7.0",
|
|
76
75
|
"commander": "^12.1.0",
|
|
77
76
|
"jiti": "^2.7.0",
|
|
78
77
|
"jscodeshift": "^17.3.0",
|
|
79
78
|
"zod": "^4.4.3"
|
|
80
79
|
},
|
|
81
80
|
"peerDependencies": {
|
|
82
|
-
"@astryxdesign/charts": "0.1.
|
|
83
|
-
"@astryxdesign/core": "0.1.
|
|
84
|
-
"@astryxdesign/lab": "0.1.
|
|
85
|
-
"@astryxdesign/theme-neutral": "0.1.
|
|
81
|
+
"@astryxdesign/charts": "0.1.7-canary.04cd8f7",
|
|
82
|
+
"@astryxdesign/core": "0.1.7-canary.04cd8f7",
|
|
83
|
+
"@astryxdesign/lab": "0.1.7-canary.04cd8f7",
|
|
84
|
+
"@astryxdesign/theme-neutral": "0.1.7-canary.04cd8f7",
|
|
86
85
|
"gpt-tokenizer": "^3.4.0"
|
|
87
86
|
},
|
|
88
87
|
"peerDependenciesMeta": {
|
|
@@ -100,10 +99,10 @@
|
|
|
100
99
|
}
|
|
101
100
|
},
|
|
102
101
|
"devDependencies": {
|
|
103
|
-
"@astryxdesign/charts": "0.1.
|
|
104
|
-
"@astryxdesign/core": "0.1.
|
|
105
|
-
"@astryxdesign/lab": "0.1.
|
|
106
|
-
"@astryxdesign/theme-neutral": "0.1.
|
|
102
|
+
"@astryxdesign/charts": "0.1.7-canary.04cd8f7",
|
|
103
|
+
"@astryxdesign/core": "0.1.7-canary.04cd8f7",
|
|
104
|
+
"@astryxdesign/lab": "0.1.7-canary.04cd8f7",
|
|
105
|
+
"@astryxdesign/theme-neutral": "0.1.7-canary.04cd8f7",
|
|
107
106
|
"gpt-tokenizer": "^3.4.0"
|
|
108
107
|
},
|
|
109
108
|
"scripts": {
|
package/src/api/doctor.mjs
CHANGED
|
@@ -26,7 +26,7 @@ import {createRequire} from 'node:module';
|
|
|
26
26
|
|
|
27
27
|
import {MIN_NODE_VERSION, isNodeVersionSupported} from '../lib/node-version.mjs';
|
|
28
28
|
import {CLI_ROOT, findCoreDir} from '../utils/paths.mjs';
|
|
29
|
-
import {detectPackageManager} from '../utils/package-manager.mjs';
|
|
29
|
+
import {detectPackageManager, getCliInvocation} from '../utils/package-manager.mjs';
|
|
30
30
|
import {findConfigPath, Project} from '../lib/project.mjs';
|
|
31
31
|
import {semverCompare} from '../utils/semver.mjs';
|
|
32
32
|
|
|
@@ -343,7 +343,7 @@ export function checkAgentDocs(ctx) {
|
|
|
343
343
|
label: 'AI agent docs',
|
|
344
344
|
status: 'info',
|
|
345
345
|
message: 'No agent docs (CLAUDE.md / AGENTS.md / .cursorrules) found.',
|
|
346
|
-
fix:
|
|
346
|
+
fix: `Generate agent docs with \`${getCliInvocation(ctx.cwd)} init --features agents\`.`,
|
|
347
347
|
};
|
|
348
348
|
}
|
|
349
349
|
|
|
@@ -365,7 +365,7 @@ export function checkAgentDocs(ctx) {
|
|
|
365
365
|
label: 'AI agent docs',
|
|
366
366
|
status: 'warn',
|
|
367
367
|
message: `Agent docs present (${present.join(', ')}) but no Astryx section markers found.`,
|
|
368
|
-
fix:
|
|
368
|
+
fix: `Add the Astryx section to your agent docs with \`${getCliInvocation(ctx.cwd)} init --features agents\`.`,
|
|
369
369
|
};
|
|
370
370
|
}
|
|
371
371
|
|
|
@@ -3,16 +3,15 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* @file Lazy jscodeshift installer
|
|
5
5
|
*
|
|
6
|
-
* Checks if jscodeshift is available and
|
|
6
|
+
* Checks if jscodeshift is available and installs it on-demand.
|
|
7
7
|
* Keeps the CLI lean — jscodeshift is only needed for codemods.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* or the command will fail with a helpful error message.
|
|
9
|
+
* Non-interactive by default (no prompts): pass `installDeps: true` to
|
|
10
|
+
* auto-install, otherwise the command fails fast with a helpful error.
|
|
12
11
|
*/
|
|
13
12
|
|
|
14
13
|
import {execSync} from 'node:child_process';
|
|
15
|
-
import * as p from '
|
|
14
|
+
import * as p from '../lib/term-log.mjs';
|
|
16
15
|
import {detectPackageManager} from '../utils/package-manager.mjs';
|
|
17
16
|
|
|
18
17
|
/**
|
|
@@ -30,32 +29,17 @@ export async function ensureJscodeshift({installDeps = false, silent = false} =
|
|
|
30
29
|
} catch {
|
|
31
30
|
log.warn('jscodeshift is required for codemods but not installed.');
|
|
32
31
|
|
|
33
|
-
const isInteractive = process.stdout.isTTY && !process.env.CI;
|
|
34
|
-
|
|
35
32
|
if (installDeps) {
|
|
36
|
-
// Explicit opt-in — install without prompting
|
|
33
|
+
// Explicit opt-in — install without prompting.
|
|
37
34
|
return installJscodeshift(silent);
|
|
38
35
|
}
|
|
39
36
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return false;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Interactive TTY — prompt as before
|
|
50
|
-
const shouldInstall = await p.confirm({
|
|
51
|
-
message: 'Install jscodeshift now?',
|
|
52
|
-
initialValue: true,
|
|
53
|
-
});
|
|
54
|
-
if (p.isCancel(shouldInstall) || !shouldInstall) {
|
|
55
|
-
p.log.error('Cannot run codemods without jscodeshift.');
|
|
56
|
-
return false;
|
|
57
|
-
}
|
|
58
|
-
return installJscodeshift(silent);
|
|
37
|
+
// Non-interactive by default: fail fast with a helpful message instead of
|
|
38
|
+
// prompting (the CLI never blocks on a TTY).
|
|
39
|
+
log.error(
|
|
40
|
+
'Cannot run codemods without jscodeshift. Use --install-deps to auto-install.',
|
|
41
|
+
);
|
|
42
|
+
return false;
|
|
59
43
|
}
|
|
60
44
|
}
|
|
61
45
|
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
|
|
30
30
|
import * as fs from 'node:fs';
|
|
31
31
|
import * as path from 'node:path';
|
|
32
|
-
import * as p from '
|
|
32
|
+
import * as p from '../lib/term-log.mjs';
|
|
33
33
|
import {findConfigPath} from '../lib/project.mjs';
|
|
34
34
|
import {fixDirectiveCorruption, validateOutput} from './runner.mjs';
|
|
35
35
|
|
package/src/codemods/runner.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import * as fs from 'node:fs';
|
|
12
12
|
import * as path from 'node:path';
|
|
13
|
-
import * as p from '
|
|
13
|
+
import * as p from '../lib/term-log.mjs';
|
|
14
14
|
import {humanLog} from '../lib/json.mjs';
|
|
15
15
|
import {runConfigCodemod} from './run-codemod.mjs';
|
|
16
16
|
|
|
@@ -184,7 +184,7 @@ export async function runCodemods(
|
|
|
184
184
|
versionManifests,
|
|
185
185
|
{apply, path: srcPath, codemod, skipCodemods, silent = false},
|
|
186
186
|
) {
|
|
187
|
-
// No-op stub object so silent mode skips
|
|
187
|
+
// No-op stub object so silent mode skips log output entirely without
|
|
188
188
|
// littering the body with `if (!silent)` guards.
|
|
189
189
|
const log = silent
|
|
190
190
|
? {step() {}, info() {}, success() {}, warn() {}, error() {}, message() {}}
|
|
@@ -22,7 +22,7 @@ import * as fs from 'node:fs';
|
|
|
22
22
|
import * as path from 'node:path';
|
|
23
23
|
import {findCoreDir, CLI_ROOT} from '../utils/paths.mjs';
|
|
24
24
|
import {assertWithin, PathSafetyError} from '../utils/path-safety.mjs';
|
|
25
|
-
import {
|
|
25
|
+
import {getCliInvocation} from '../utils/package-manager.mjs';
|
|
26
26
|
import {discoverComponents} from '../lib/component-discovery.mjs';
|
|
27
27
|
import {humanLog} from '../lib/json.mjs';
|
|
28
28
|
import {cliError} from '../lib/cli-error.mjs';
|
|
@@ -144,8 +144,8 @@ export function detectStylingSystem(targetDir) {
|
|
|
144
144
|
* configured (see {@link detectStylingSystem}) so the agent never reaches for a
|
|
145
145
|
* styling path that isn't compiled here.
|
|
146
146
|
*/
|
|
147
|
-
export function generateCompressedIndex(version, {coreDir,
|
|
148
|
-
const run =
|
|
147
|
+
export function generateCompressedIndex(version, {coreDir, invocation = getCliInvocation(), stylingSystem = 'css'} = {}) {
|
|
148
|
+
const run = invocation;
|
|
149
149
|
const lines = [MARKER_START];
|
|
150
150
|
|
|
151
151
|
// Component count from live discovery
|
|
@@ -392,7 +392,7 @@ export function removeAgentDocs(targetDir) {
|
|
|
392
392
|
|
|
393
393
|
/**
|
|
394
394
|
* Programmatic entry point for installing agent docs.
|
|
395
|
-
* Used by the init
|
|
395
|
+
* Used by the init command, upgrade command, and agent-docs command.
|
|
396
396
|
*
|
|
397
397
|
* Strategy (when no agent/paths specified):
|
|
398
398
|
* - Discover all existing agent doc files and update them
|
|
@@ -410,9 +410,9 @@ export function removeAgentDocs(targetDir) {
|
|
|
410
410
|
export function installAgentDocs(targetDir, {zh = false, lang, agent, paths, onlyReplace = false} = {}) {
|
|
411
411
|
const coreDir = findCoreDir(targetDir);
|
|
412
412
|
const version = getXdsVersion(coreDir);
|
|
413
|
-
const
|
|
413
|
+
const invocation = getCliInvocation(targetDir);
|
|
414
414
|
const stylingSystem = detectStylingSystem(targetDir);
|
|
415
|
-
const compressedIndex = generateCompressedIndex(version, {coreDir, zh, lang,
|
|
415
|
+
const compressedIndex = generateCompressedIndex(version, {coreDir, zh, lang, invocation, stylingSystem});
|
|
416
416
|
const written = [];
|
|
417
417
|
|
|
418
418
|
// Explicit paths override everything
|
|
@@ -538,8 +538,7 @@ export function registerAgentDocs(program) {
|
|
|
538
538
|
throw err;
|
|
539
539
|
}
|
|
540
540
|
|
|
541
|
-
const
|
|
542
|
-
const run = `${runPrefix} astryx`;
|
|
541
|
+
const run = getCliInvocation(targetDir);
|
|
543
542
|
|
|
544
543
|
for (const t of targets) {
|
|
545
544
|
humanLog(`✓ ${t}`);
|
|
@@ -88,17 +88,24 @@ describe('generateCompressedIndex', () => {
|
|
|
88
88
|
expect(result).toMatch(/after any @astryxdesign\/core bump/);
|
|
89
89
|
});
|
|
90
90
|
|
|
91
|
-
it('states the
|
|
92
|
-
const result = generateCompressedIndex('1.0.0', {
|
|
91
|
+
it('states the invocation once in the CLI header (yarn)', () => {
|
|
92
|
+
const result = generateCompressedIndex('1.0.0', {invocation: 'yarn astryx'});
|
|
93
93
|
expect(result).toContain('yarn astryx <cmd>');
|
|
94
94
|
expect(result).not.toContain('npx astryx');
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
-
it('uses pnpm exec
|
|
98
|
-
const result = generateCompressedIndex('1.0.0', {
|
|
97
|
+
it('uses the pnpm exec invocation', () => {
|
|
98
|
+
const result = generateCompressedIndex('1.0.0', {invocation: 'pnpm exec astryx'});
|
|
99
99
|
expect(result).toContain('pnpm exec astryx <cmd>');
|
|
100
100
|
expect(result).not.toContain('npx astryx');
|
|
101
101
|
});
|
|
102
|
+
|
|
103
|
+
it('uses the scoped package for one-off (uninstalled) runs so agents never hit the bare name', () => {
|
|
104
|
+
const result = generateCompressedIndex('1.0.0', {invocation: 'npx @astryxdesign/cli'});
|
|
105
|
+
expect(result).toContain('npx @astryxdesign/cli <cmd>');
|
|
106
|
+
// The header defines the mapping; the bare "run every command as `npx astryx`" footgun must be absent.
|
|
107
|
+
expect(result).not.toContain('npx astryx <cmd>');
|
|
108
|
+
});
|
|
102
109
|
});
|
|
103
110
|
|
|
104
111
|
describe('detectStylingSystem', () => {
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* - An updated JS module that references the built className
|
|
9
9
|
*
|
|
10
10
|
* Usage:
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* astryx theme build ./src/themes/ocean.ts
|
|
12
|
+
* astryx theme build ./src/themes/ocean.ts --out ./dist/ocean.css
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import * as fs from 'node:fs';
|
|
@@ -17,16 +17,15 @@ import * as path from 'node:path';
|
|
|
17
17
|
import {pathToFileURL, fileURLToPath} from 'node:url';
|
|
18
18
|
import {spawn} from 'node:child_process';
|
|
19
19
|
import {createJiti} from 'jiti';
|
|
20
|
-
import {
|
|
20
|
+
import {getCliInvocation} from '../utils/package-manager.mjs';
|
|
21
21
|
import {
|
|
22
22
|
sanitizeName,
|
|
23
23
|
PathSafetyError,
|
|
24
|
-
isNonInteractive,
|
|
25
24
|
} from '../utils/path-safety.mjs';
|
|
26
25
|
import {jsonOut, humanLog} from '../lib/json.mjs';
|
|
27
26
|
import {cliError} from '../lib/cli-error.mjs';
|
|
28
27
|
import {ERROR_CODES} from '../lib/error-codes.mjs';
|
|
29
|
-
import {themeAdd
|
|
28
|
+
import {themeAdd} from '../api/theme-add.mjs';
|
|
30
29
|
|
|
31
30
|
// Import shared theme processing from core. `astryx theme build` MUST produce the
|
|
32
31
|
// exact same CSS as the `<Theme>` runtime, so it has exactly one generation
|
|
@@ -500,7 +499,7 @@ function generateBuiltModule(themeDef, iconInfo) {
|
|
|
500
499
|
.join('\n');
|
|
501
500
|
|
|
502
501
|
return `${iconImport}/**
|
|
503
|
-
* ${themeDef.name} theme — built by \`${
|
|
502
|
+
* ${themeDef.name} theme — built by \`${getCliInvocation()} theme build\`
|
|
504
503
|
* Import the CSS file alongside this module:
|
|
505
504
|
*
|
|
506
505
|
* import { ${toIdentifier(themeDef.name)}Theme } from './${themeDef.name}';
|
|
@@ -1130,7 +1129,7 @@ Or with a <link> tag:
|
|
|
1130
1129
|
if (t.description) humanLog(` ${t.description}`);
|
|
1131
1130
|
}
|
|
1132
1131
|
humanLog('\nUsage:');
|
|
1133
|
-
humanLog(
|
|
1132
|
+
humanLog(` ${getCliInvocation()} theme add <slug> [target-path] Scaffold a theme file you own\n`);
|
|
1134
1133
|
});
|
|
1135
1134
|
|
|
1136
1135
|
theme
|
|
@@ -1141,32 +1140,9 @@ Or with a <link> tag:
|
|
|
1141
1140
|
.action(async (slug, targetPath, options) => {
|
|
1142
1141
|
const json = program.opts().json || false;
|
|
1143
1142
|
|
|
1144
|
-
//
|
|
1145
|
-
//
|
|
1146
|
-
// ERR_FILE_EXISTS guard.
|
|
1147
|
-
const interactive =
|
|
1148
|
-
!json && !isNonInteractive({json}) && Boolean(process.stdin.isTTY);
|
|
1149
|
-
if (slug && !options.list && !options.overwrite && interactive) {
|
|
1150
|
-
const collision = await detectThemeCollision(slug, targetPath);
|
|
1151
|
-
if (collision) {
|
|
1152
|
-
const rel = path.relative(process.cwd(), collision) || collision;
|
|
1153
|
-
const p = await import('@clack/prompts');
|
|
1154
|
-
const confirmed = await p.confirm({
|
|
1155
|
-
message: `Overwrite existing file ${rel}?`,
|
|
1156
|
-
initialValue: false,
|
|
1157
|
-
});
|
|
1158
|
-
if (p.isCancel(confirmed)) {
|
|
1159
|
-
p.cancel('Cancelled.');
|
|
1160
|
-
return;
|
|
1161
|
-
}
|
|
1162
|
-
if (!confirmed) {
|
|
1163
|
-
humanLog('Aborted. Re-run with --overwrite to replace the file.');
|
|
1164
|
-
return;
|
|
1165
|
-
}
|
|
1166
|
-
options.overwrite = true;
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1143
|
+
// The CLI is non-interactive: never prompt to confirm an overwrite.
|
|
1144
|
+
// Existing files require an explicit --overwrite; otherwise themeAdd's
|
|
1145
|
+
// ERR_FILE_EXISTS guard rejects the write.
|
|
1170
1146
|
let result;
|
|
1171
1147
|
try {
|
|
1172
1148
|
result = await themeAdd(slug, {
|
|
@@ -1191,7 +1167,7 @@ Or with a <link> tag:
|
|
|
1191
1167
|
if (t.description) humanLog(` ${t.description}`);
|
|
1192
1168
|
}
|
|
1193
1169
|
humanLog('\nUsage:');
|
|
1194
|
-
humanLog(
|
|
1170
|
+
humanLog(` ${getCliInvocation()} theme add <slug> [target-path] Scaffold a theme file you own\n`);
|
|
1195
1171
|
return;
|
|
1196
1172
|
}
|
|
1197
1173
|
|
|
@@ -1219,40 +1195,3 @@ This is your copy of the ${displayName} theme — edit ${entry} to make it your
|
|
|
1219
1195
|
});
|
|
1220
1196
|
}
|
|
1221
1197
|
|
|
1222
|
-
/**
|
|
1223
|
-
* First existing file that scaffolding <slug> into <targetPath> would clobber,
|
|
1224
|
-
* or null. Used to prompt before invoking the API; the API re-validates and
|
|
1225
|
-
* owns any authoritative error.
|
|
1226
|
-
*
|
|
1227
|
-
* @param {string} slug
|
|
1228
|
-
* @param {string} [targetPath]
|
|
1229
|
-
* @returns {Promise<string|null>}
|
|
1230
|
-
*/
|
|
1231
|
-
async function detectThemeCollision(slug, targetPath) {
|
|
1232
|
-
let themes;
|
|
1233
|
-
try {
|
|
1234
|
-
themes = listThemes();
|
|
1235
|
-
} catch {
|
|
1236
|
-
return null;
|
|
1237
|
-
}
|
|
1238
|
-
const match = themes.find(t => t.slug.toLowerCase() === slug.toLowerCase());
|
|
1239
|
-
if (!match) return null;
|
|
1240
|
-
|
|
1241
|
-
const rawTarget = targetPath || path.join('src', 'themes', match.slug);
|
|
1242
|
-
let resolvedDir;
|
|
1243
|
-
try {
|
|
1244
|
-
// Fail soft (null) on traversal; the API surfaces the real error.
|
|
1245
|
-
const {assertWithin} = await import('../utils/path-safety.mjs');
|
|
1246
|
-
resolvedDir = assertWithin(rawTarget, process.cwd(), {
|
|
1247
|
-
label: 'theme target path',
|
|
1248
|
-
});
|
|
1249
|
-
} catch {
|
|
1250
|
-
return null;
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
for (const name of match.files) {
|
|
1254
|
-
const dest = path.join(resolvedDir, name);
|
|
1255
|
-
if (fs.existsSync(dest)) return dest;
|
|
1256
|
-
}
|
|
1257
|
-
return null;
|
|
1258
|
-
}
|
package/src/commands/build.mjs
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* the whole CLI, use `astryx search <query>` instead.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import {
|
|
18
|
+
import {getCliInvocation, formatCliCommand} from '../utils/package-manager.mjs';
|
|
19
19
|
import {jsonOut, humanLog} from '../lib/json.mjs';
|
|
20
20
|
import {cliError} from '../lib/cli-error.mjs';
|
|
21
21
|
import {search as searchApi} from '../api/search.mjs';
|
|
@@ -47,25 +47,25 @@ function printPlaybook(run) {
|
|
|
47
47
|
'How to build a page with Astryx',
|
|
48
48
|
'',
|
|
49
49
|
"1. Find a starting point for what you're building:",
|
|
50
|
-
` ${run}
|
|
50
|
+
` ${run} build "<what you're building>"`,
|
|
51
51
|
' → returns the closest [page] template, the [block]s that cover parts,',
|
|
52
52
|
' and the [component]s to fill the gaps, with a "Compose:" suggestion.',
|
|
53
53
|
'',
|
|
54
54
|
'2. If a [page] template matches → scaffold it and adapt:',
|
|
55
|
-
` ${run}
|
|
55
|
+
` ${run} template <name> [path]`,
|
|
56
56
|
'',
|
|
57
57
|
'3. If nothing matches exactly → compose:',
|
|
58
|
-
` ${run}
|
|
59
|
-
` ${run}
|
|
60
|
-
` ${run}
|
|
58
|
+
` ${run} template <name> --skeleton # study a close page's layout`,
|
|
59
|
+
` ${run} template <BlockName> # drop in each block from the kit`,
|
|
60
|
+
` ${run} component <Name> # fill remaining gaps (read props)`,
|
|
61
61
|
'',
|
|
62
62
|
'4. Rules (keep it on-system):',
|
|
63
63
|
' - No <div>/raw HTML for layout — use VStack/HStack/Grid/Stack/Card etc.',
|
|
64
|
-
|
|
64
|
+
` - No style={{}} — use component props; design tokens via \`${run} docs tokens\`.`,
|
|
65
65
|
' - Wrap the app in <Theme theme={...}> and import core reset.css + astryx.css.',
|
|
66
66
|
'',
|
|
67
|
-
`Tip: \`${run}
|
|
68
|
-
`lookup of any component/doc/template, use \`${run}
|
|
67
|
+
`Tip: \`${run} build "<idea>"\` is the fastest way in. For a neutral`,
|
|
68
|
+
`lookup of any component/doc/template, use \`${run} search <query>\`.`,
|
|
69
69
|
'',
|
|
70
70
|
];
|
|
71
71
|
for (const l of lines) humanLog(l);
|
|
@@ -79,7 +79,7 @@ export function registerBuild(program) {
|
|
|
79
79
|
.option('--limit <n>', 'Max candidates to draw from (default 60)')
|
|
80
80
|
.option('--detail', 'Verbose output (include import paths and match reason)')
|
|
81
81
|
.action(async (query, options) => {
|
|
82
|
-
const run =
|
|
82
|
+
const run = getCliInvocation();
|
|
83
83
|
const json = program.opts().json || false;
|
|
84
84
|
|
|
85
85
|
// No query → print the playbook (the "how to build" skill).
|
|
@@ -115,7 +115,7 @@ export function registerBuild(program) {
|
|
|
115
115
|
if (results.length === 0) {
|
|
116
116
|
humanLog('');
|
|
117
117
|
humanLog(`No matches for "${q}".`);
|
|
118
|
-
humanLog(`Try a broader term, or browse: ${run}
|
|
118
|
+
humanLog(`Try a broader term, or browse: ${run} component --list`);
|
|
119
119
|
humanLog('');
|
|
120
120
|
return;
|
|
121
121
|
}
|
|
@@ -138,7 +138,7 @@ export function registerBuild(program) {
|
|
|
138
138
|
humanLog('');
|
|
139
139
|
humanLog(` [${label}] ${display}`);
|
|
140
140
|
if (r.description) humanLog(` ${r.description}`);
|
|
141
|
-
humanLog(` → ${
|
|
141
|
+
humanLog(` → ${formatCliCommand(r.command)}`);
|
|
142
142
|
if (options.detail) {
|
|
143
143
|
if (r.import) humanLog(` import: ${r.import}`);
|
|
144
144
|
humanLog(` match: ${r.reason} (score ${r.score})`);
|
|
@@ -151,9 +151,9 @@ export function registerBuild(program) {
|
|
|
151
151
|
// START — the single recommended path.
|
|
152
152
|
humanLog('');
|
|
153
153
|
if (directMatch) {
|
|
154
|
-
humanLog(`START → Scaffold the \`${pages[0].name}\` page template, then adapt: ${run}
|
|
154
|
+
humanLog(`START → Scaffold the \`${pages[0].name}\` page template, then adapt: ${run} template ${pages[0].name} ./src/App.tsx`);
|
|
155
155
|
} else if (pages.length) {
|
|
156
|
-
humanLog(`START → No exact page template. Use \`${pages[0].name}\` as a layout reference (${run}
|
|
156
|
+
humanLog(`START → No exact page template. Use \`${pages[0].name}\` as a layout reference (${run} template ${pages[0].name} --skeleton) and compose the pieces below.`);
|
|
157
157
|
} else {
|
|
158
158
|
humanLog(`START → No page template fits. Frame with AppShell and compose the blocks + components below.`);
|
|
159
159
|
}
|
|
@@ -168,7 +168,7 @@ export function registerBuild(program) {
|
|
|
168
168
|
// FRAME — always (the page shell).
|
|
169
169
|
humanLog('');
|
|
170
170
|
humanLog(`FRAME — page shell (always): ${FRAME.join(', ')}`);
|
|
171
|
-
humanLog(` full-page → AppShell; or Layout + SideNav/TopNav. ${run}
|
|
171
|
+
humanLog(` full-page → AppShell; or Layout + SideNav/TopNav. ${run} component AppShell`);
|
|
172
172
|
|
|
173
173
|
// BLOCKS — idea-specific composed patterns.
|
|
174
174
|
if (blocks.length) {
|