@astryxdesign/cli 0.1.7-canary.fb9c68c → 0.1.7
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/README.md +19 -115
- package/docs/getting-started.doc.mjs +9 -9
- package/docs/integration-authoring.md +105 -0
- 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 +10 -11
- package/src/api/doctor.mjs +3 -3
- package/src/codemods/ensure-jscodeshift.mjs +27 -11
- package/src/codemods/run-codemod.mjs +1 -1
- package/src/codemods/runner.mjs +2 -2
- package/src/commands/agent-docs.mjs +14 -56
- package/src/commands/agent-docs.test.mjs +4 -11
- package/src/commands/build-theme.mjs +71 -10
- package/src/commands/build.mjs +15 -15
- package/src/commands/component/index.mjs +4 -4
- package/src/commands/discover.mjs +5 -7
- package/src/commands/docs.mjs +4 -4
- package/src/commands/hook/index.mjs +4 -4
- package/src/commands/init.mjs +152 -48
- package/src/commands/init.next-steps.test.mjs +1 -1
- package/src/commands/interactive-guard.test.mjs +22 -19
- 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 +34 -11
- package/src/commands/template.mjs +31 -11
- package/src/commands/upgrade.mjs +6 -9
- package/src/commands/upgrade.test.mjs +1 -1
- package/src/index.mjs +6 -40
- package/src/lib/component-format.mjs +1 -2
- package/src/utils/interactive.mjs +76 -0
- package/src/utils/interactive.test.mjs +70 -0
- package/src/utils/package-manager.mjs +0 -78
- package/src/utils/package-manager.test.mjs +1 -108
- package/src/utils/path-safety.mjs +18 -0
- package/src/utils/update-check.mjs +1 -2
- package/templates/blocks/components/TabList/TabListTabsWithActions.doc.mjs +1 -1
- package/templates/blocks/components/TabList/TabListTabsWithActions.tsx +7 -2
- package/docs/cli-integrations.doc.mjs +0 -150
- package/scripts/postinstall.mjs +0 -74
- package/src/commands/cli-postinstall.test.mjs +0 -42
- package/src/commands/setup-nudge.test.mjs +0 -108
- package/src/lib/term-log.mjs +0 -48
|
@@ -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 astryx 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 astryx init --features agents --agent claude # CLAUDE.md
|
|
52
|
+
npx astryx init --features agents --agent cursor # .cursorrules
|
|
53
|
+
npx astryx init --features agents --agent codex # AGENTS.md (Copilot, Codex, etc.)`,
|
|
54
54
|
},
|
|
55
55
|
],
|
|
56
56
|
},
|
|
@@ -65,9 +65,9 @@ npx @astryxdesign/cli init --features agents --agent codex # AGENTS.md (Copi
|
|
|
65
65
|
type: 'list',
|
|
66
66
|
style: 'ordered',
|
|
67
67
|
items: [
|
|
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',
|
|
68
|
+
'`npx astryx template --list`: find a related page pattern to use as reference',
|
|
69
|
+
'`npx astryx template <name> --skeleton`: study the layout structure',
|
|
70
|
+
'`npx astryx component <Name>`: read props and examples for every component used',
|
|
71
71
|
],
|
|
72
72
|
},
|
|
73
73
|
{
|
|
@@ -88,7 +88,7 @@ npx @astryxdesign/cli init --features agents --agent codex # AGENTS.md (Copi
|
|
|
88
88
|
lang: 'bash',
|
|
89
89
|
label: 'Install as a Cursor user rule',
|
|
90
90
|
code: `mkdir -p ~/.cursor/rules
|
|
91
|
-
npx
|
|
91
|
+
npx astryx init --features agents --agent-docs-path ~/.cursor/rules/xds.mdc`,
|
|
92
92
|
},
|
|
93
93
|
],
|
|
94
94
|
},
|
|
@@ -109,12 +109,12 @@ npx @astryxdesign/cli init --features agents --agent-docs-path ~/.cursor/rules/x
|
|
|
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 astryx init --features agents\` to generate agent docs, then read the generated file.`,
|
|
113
113
|
},
|
|
114
114
|
],
|
|
115
115
|
},
|
|
116
116
|
{
|
|
117
|
-
title: 'The astryx Pattern',
|
|
117
|
+
title: 'The npx astryx Pattern',
|
|
118
118
|
content: [
|
|
119
119
|
{
|
|
120
120
|
type: 'prose',
|
|
@@ -130,16 +130,16 @@ If you don't know all three, run \`npx @astryxdesign/cli init --features agents\
|
|
|
130
130
|
},
|
|
131
131
|
{
|
|
132
132
|
type: 'prose',
|
|
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.',
|
|
133
|
+
text: 'With this alias, agents use `npx 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: `astryx component --list
|
|
140
|
-
astryx component Dialog --dense
|
|
141
|
-
astryx docs styling --dense
|
|
142
|
-
astryx docs tokens --dense`,
|
|
139
|
+
code: `npx astryx component --list
|
|
140
|
+
npx astryx component Dialog --dense
|
|
141
|
+
npx astryx docs styling --dense
|
|
142
|
+
npx astryx docs tokens --dense`,
|
|
143
143
|
},
|
|
144
144
|
],
|
|
145
145
|
},
|
|
@@ -154,9 +154,9 @@ astryx docs tokens --dense`,
|
|
|
154
154
|
type: 'code',
|
|
155
155
|
lang: 'bash',
|
|
156
156
|
label: 'Dense output for pasting into AI conversations',
|
|
157
|
-
code: `astryx component Dialog --dense
|
|
158
|
-
astryx docs styling --dense
|
|
159
|
-
astryx docs tokens --dense`,
|
|
157
|
+
code: `npx astryx component Dialog --dense
|
|
158
|
+
npx astryx docs styling --dense
|
|
159
|
+
npx 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.7
|
|
3
|
+
"version": "0.1.7",
|
|
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",
|
|
@@ -67,22 +67,22 @@
|
|
|
67
67
|
"files": [
|
|
68
68
|
"bin",
|
|
69
69
|
"src",
|
|
70
|
-
"scripts/postinstall.mjs",
|
|
71
70
|
"templates",
|
|
72
71
|
"docs",
|
|
73
72
|
"CHANGELOG.md"
|
|
74
73
|
],
|
|
75
74
|
"dependencies": {
|
|
75
|
+
"@clack/prompts": "^1.7.0",
|
|
76
76
|
"commander": "^12.1.0",
|
|
77
77
|
"jiti": "^2.7.0",
|
|
78
78
|
"jscodeshift": "^17.3.0",
|
|
79
79
|
"zod": "^4.4.3"
|
|
80
80
|
},
|
|
81
81
|
"peerDependencies": {
|
|
82
|
-
"@astryxdesign/charts": "
|
|
83
|
-
"@astryxdesign/core": "
|
|
84
|
-
"@astryxdesign/lab": "
|
|
85
|
-
"@astryxdesign/theme-neutral": "
|
|
82
|
+
"@astryxdesign/charts": "*",
|
|
83
|
+
"@astryxdesign/core": "*",
|
|
84
|
+
"@astryxdesign/lab": "*",
|
|
85
|
+
"@astryxdesign/theme-neutral": "*",
|
|
86
86
|
"gpt-tokenizer": "^3.4.0"
|
|
87
87
|
},
|
|
88
88
|
"peerDependenciesMeta": {
|
|
@@ -100,15 +100,14 @@
|
|
|
100
100
|
}
|
|
101
101
|
},
|
|
102
102
|
"devDependencies": {
|
|
103
|
-
"@astryxdesign/charts": "
|
|
104
|
-
"@astryxdesign/core": "
|
|
105
|
-
"@astryxdesign/lab": "
|
|
106
|
-
"@astryxdesign/theme-neutral": "
|
|
103
|
+
"@astryxdesign/charts": "*",
|
|
104
|
+
"@astryxdesign/core": "*",
|
|
105
|
+
"@astryxdesign/lab": "*",
|
|
106
|
+
"@astryxdesign/theme-neutral": "*",
|
|
107
107
|
"gpt-tokenizer": "^3.4.0"
|
|
108
108
|
},
|
|
109
109
|
"scripts": {
|
|
110
110
|
"astryx": "node bin/astryx.mjs",
|
|
111
|
-
"postinstall": "node scripts/postinstall.mjs",
|
|
112
111
|
"typecheck:template-docs": "tsc --project tsconfig.template-docs.json",
|
|
113
112
|
"typecheck:json-api": "tsc --project tsconfig.json-api.json && tsc --project tsconfig.api-contract.json"
|
|
114
113
|
}
|
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
|
|
29
|
+
import {detectPackageManager} 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 `astryx 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 `astryx init --features agents`.',
|
|
369
369
|
};
|
|
370
370
|
}
|
|
371
371
|
|
|
@@ -3,15 +3,16 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* @file Lazy jscodeshift installer
|
|
5
5
|
*
|
|
6
|
-
* Checks if jscodeshift is available and
|
|
6
|
+
* Checks if jscodeshift is available and offers to install it on-demand.
|
|
7
7
|
* Keeps the CLI lean — jscodeshift is only needed for codemods.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* In non-interactive environments (CI, LLM agents), the interactive prompt
|
|
10
|
+
* is skipped. Pass `installDeps: true` to auto-install without prompting,
|
|
11
|
+
* or the command will fail with a helpful error message.
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
import {execSync} from 'node:child_process';
|
|
14
|
-
import * as p from '
|
|
15
|
+
import * as p from '@clack/prompts';
|
|
15
16
|
import {detectPackageManager} from '../utils/package-manager.mjs';
|
|
16
17
|
|
|
17
18
|
/**
|
|
@@ -29,17 +30,32 @@ export async function ensureJscodeshift({installDeps = false, silent = false} =
|
|
|
29
30
|
} catch {
|
|
30
31
|
log.warn('jscodeshift is required for codemods but not installed.');
|
|
31
32
|
|
|
33
|
+
const isInteractive = process.stdout.isTTY && !process.env.CI;
|
|
34
|
+
|
|
32
35
|
if (installDeps) {
|
|
33
|
-
// Explicit opt-in — install without prompting
|
|
36
|
+
// Explicit opt-in — install without prompting
|
|
34
37
|
return installJscodeshift(silent);
|
|
35
38
|
}
|
|
36
39
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
if (!isInteractive || silent) {
|
|
41
|
+
// Non-interactive environment (or --json) — fail fast with a helpful message
|
|
42
|
+
log.error(
|
|
43
|
+
'Cannot run codemods without jscodeshift. ' +
|
|
44
|
+
'Use --install-deps to auto-install in non-interactive environments.',
|
|
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);
|
|
43
59
|
}
|
|
44
60
|
}
|
|
45
61
|
|
|
@@ -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 '@clack/prompts';
|
|
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 '@clack/prompts';
|
|
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 clack stdout 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 {getRunPrefix} 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';
|
|
@@ -31,9 +31,6 @@ import {ERROR_CODES} from '../lib/error-codes.mjs';
|
|
|
31
31
|
const AGENTS_MD = 'AGENTS.md';
|
|
32
32
|
const CLAUDE_MD = 'CLAUDE.md';
|
|
33
33
|
const CLAUDE_DIR_MD = '.claude/CLAUDE.md'; // cross-platform literal
|
|
34
|
-
const CURSOR_RULES = '.cursorrules';
|
|
35
|
-
const HERMES_DOT_MD = '.hermes.md';
|
|
36
|
-
const HERMES_MD = 'HERMES.md';
|
|
37
34
|
|
|
38
35
|
|
|
39
36
|
const MARKER_START = '<!-- ASTRYX:START -->';
|
|
@@ -48,60 +45,20 @@ const LEGACY_MARKER_END = '<!-- XDS:END -->';
|
|
|
48
45
|
*/
|
|
49
46
|
const AGENT_PRESETS = {
|
|
50
47
|
claude: [CLAUDE_MD, CLAUDE_DIR_MD],
|
|
51
|
-
cursor: [
|
|
48
|
+
cursor: ['.cursorrules', AGENTS_MD],
|
|
52
49
|
codex: [AGENTS_MD],
|
|
53
|
-
hermes: [
|
|
50
|
+
hermes: ['.hermes.md', 'HERMES.md', AGENTS_MD],
|
|
54
51
|
};
|
|
55
52
|
|
|
56
53
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* `isAstryxInitialized` predicate all derive from this list, so "where init
|
|
60
|
-
* writes" and "where we look" can never drift. (Explicit --agent-docs-path
|
|
61
|
-
* targets are user-chosen and not enumerable here.)
|
|
62
|
-
*/
|
|
63
|
-
const AGENT_DOC_PATHS = [
|
|
64
|
-
AGENTS_MD, // Codex / ChatGPT / generic
|
|
65
|
-
CLAUDE_MD, // Claude Code (root)
|
|
66
|
-
CLAUDE_DIR_MD, // Claude Code (.claude/CLAUDE.md)
|
|
67
|
-
CURSOR_RULES, // Cursor
|
|
68
|
-
HERMES_DOT_MD, // Hermes
|
|
69
|
-
HERMES_MD, // Hermes
|
|
70
|
-
];
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Find all existing agent doc files in a directory, across EVERY location any
|
|
74
|
-
* preset can write (see {@link AGENT_DOC_PATHS}: AGENTS.md, CLAUDE.md,
|
|
75
|
-
* .claude/CLAUDE.md, .cursorrules, .hermes.md, HERMES.md).
|
|
54
|
+
* Find all existing agent doc files in a directory.
|
|
55
|
+
* Searches all known locations (AGENTS.md, CLAUDE.md, .claude/CLAUDE.md, .cursorrules).
|
|
76
56
|
* @param {string} targetDir
|
|
77
57
|
* @returns {string[]} Relative paths of existing agent doc files
|
|
78
58
|
*/
|
|
79
59
|
export function discoverAgentDocs(targetDir) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Single source of truth for "is Astryx set up in this project?" — true when any
|
|
85
|
-
* agent-doc file already carries the Astryx marker, i.e. `init` / `agent-docs`
|
|
86
|
-
* has run. Reused by the init & upgrade commands, the per-command setup nudge
|
|
87
|
-
* (enforcement layer 3), and the cli postinstall nudge (layer 2). Core's
|
|
88
|
-
* postinstall (separate package, layer 1) mirrors the same marker contract.
|
|
89
|
-
*
|
|
90
|
-
* @param {string} [targetDir=process.cwd()]
|
|
91
|
-
* @returns {boolean}
|
|
92
|
-
*/
|
|
93
|
-
export function isAstryxInitialized(targetDir = process.cwd()) {
|
|
94
|
-
for (const rel of discoverAgentDocs(targetDir)) {
|
|
95
|
-
try {
|
|
96
|
-
const content = fs.readFileSync(path.join(targetDir, rel), 'utf-8');
|
|
97
|
-
if (content.includes(MARKER_START) || content.includes(LEGACY_MARKER_START)) {
|
|
98
|
-
return true;
|
|
99
|
-
}
|
|
100
|
-
} catch {
|
|
101
|
-
// Unreadable file — ignore and keep checking the others.
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
return false;
|
|
60
|
+
const allPaths = [AGENTS_MD, CLAUDE_MD, CLAUDE_DIR_MD, '.cursorrules'];
|
|
61
|
+
return allPaths.filter(p => fs.existsSync(path.join(targetDir, p)));
|
|
105
62
|
}
|
|
106
63
|
|
|
107
64
|
/**
|
|
@@ -187,8 +144,8 @@ export function detectStylingSystem(targetDir) {
|
|
|
187
144
|
* configured (see {@link detectStylingSystem}) so the agent never reaches for a
|
|
188
145
|
* styling path that isn't compiled here.
|
|
189
146
|
*/
|
|
190
|
-
export function generateCompressedIndex(version, {coreDir,
|
|
191
|
-
const run =
|
|
147
|
+
export function generateCompressedIndex(version, {coreDir, runPrefix = getRunPrefix(), stylingSystem = 'css'} = {}) {
|
|
148
|
+
const run = `${runPrefix} astryx`;
|
|
192
149
|
const lines = [MARKER_START];
|
|
193
150
|
|
|
194
151
|
// Component count from live discovery
|
|
@@ -435,7 +392,7 @@ export function removeAgentDocs(targetDir) {
|
|
|
435
392
|
|
|
436
393
|
/**
|
|
437
394
|
* Programmatic entry point for installing agent docs.
|
|
438
|
-
* Used by the init
|
|
395
|
+
* Used by the init wizard, upgrade command, and agent-docs command.
|
|
439
396
|
*
|
|
440
397
|
* Strategy (when no agent/paths specified):
|
|
441
398
|
* - Discover all existing agent doc files and update them
|
|
@@ -453,9 +410,9 @@ export function removeAgentDocs(targetDir) {
|
|
|
453
410
|
export function installAgentDocs(targetDir, {zh = false, lang, agent, paths, onlyReplace = false} = {}) {
|
|
454
411
|
const coreDir = findCoreDir(targetDir);
|
|
455
412
|
const version = getXdsVersion(coreDir);
|
|
456
|
-
const
|
|
413
|
+
const runPrefix = getRunPrefix(targetDir);
|
|
457
414
|
const stylingSystem = detectStylingSystem(targetDir);
|
|
458
|
-
const compressedIndex = generateCompressedIndex(version, {coreDir, zh, lang,
|
|
415
|
+
const compressedIndex = generateCompressedIndex(version, {coreDir, zh, lang, runPrefix, stylingSystem});
|
|
459
416
|
const written = [];
|
|
460
417
|
|
|
461
418
|
// Explicit paths override everything
|
|
@@ -581,7 +538,8 @@ export function registerAgentDocs(program) {
|
|
|
581
538
|
throw err;
|
|
582
539
|
}
|
|
583
540
|
|
|
584
|
-
const
|
|
541
|
+
const runPrefix = getRunPrefix(targetDir);
|
|
542
|
+
const run = `${runPrefix} astryx`;
|
|
585
543
|
|
|
586
544
|
for (const t of targets) {
|
|
587
545
|
humanLog(`✓ ${t}`);
|
|
@@ -88,24 +88,17 @@ 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 runPrefix once in the CLI header', () => {
|
|
92
|
+
const result = generateCompressedIndex('1.0.0', {runPrefix: 'yarn'});
|
|
93
93
|
expect(result).toContain('yarn astryx <cmd>');
|
|
94
94
|
expect(result).not.toContain('npx astryx');
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
-
it('uses
|
|
98
|
-
const result = generateCompressedIndex('1.0.0', {
|
|
97
|
+
it('uses pnpm exec prefix', () => {
|
|
98
|
+
const result = generateCompressedIndex('1.0.0', {runPrefix: 'pnpm exec'});
|
|
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
|
-
});
|
|
109
102
|
});
|
|
110
103
|
|
|
111
104
|
describe('detectStylingSystem', () => {
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* - An updated JS module that references the built className
|
|
9
9
|
*
|
|
10
10
|
* Usage:
|
|
11
|
-
* astryx theme build ./src/themes/ocean.ts
|
|
12
|
-
* astryx theme build ./src/themes/ocean.ts --out ./dist/ocean.css
|
|
11
|
+
* npx astryx theme build ./src/themes/ocean.ts
|
|
12
|
+
* npx astryx theme build ./src/themes/ocean.ts --out ./dist/ocean.css
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import * as fs from 'node:fs';
|
|
@@ -17,15 +17,16 @@ 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 {getRunPrefix} from '../utils/package-manager.mjs';
|
|
21
21
|
import {
|
|
22
22
|
sanitizeName,
|
|
23
23
|
PathSafetyError,
|
|
24
|
+
isNonInteractive,
|
|
24
25
|
} from '../utils/path-safety.mjs';
|
|
25
26
|
import {jsonOut, humanLog} from '../lib/json.mjs';
|
|
26
27
|
import {cliError} from '../lib/cli-error.mjs';
|
|
27
28
|
import {ERROR_CODES} from '../lib/error-codes.mjs';
|
|
28
|
-
import {themeAdd} from '../api/theme-add.mjs';
|
|
29
|
+
import {themeAdd, listThemes} from '../api/theme-add.mjs';
|
|
29
30
|
|
|
30
31
|
// Import shared theme processing from core. `astryx theme build` MUST produce the
|
|
31
32
|
// exact same CSS as the `<Theme>` runtime, so it has exactly one generation
|
|
@@ -499,7 +500,7 @@ function generateBuiltModule(themeDef, iconInfo) {
|
|
|
499
500
|
.join('\n');
|
|
500
501
|
|
|
501
502
|
return `${iconImport}/**
|
|
502
|
-
* ${themeDef.name} theme — built by \`${
|
|
503
|
+
* ${themeDef.name} theme — built by \`${getRunPrefix()} astryx theme build\`
|
|
503
504
|
* Import the CSS file alongside this module:
|
|
504
505
|
*
|
|
505
506
|
* import { ${toIdentifier(themeDef.name)}Theme } from './${themeDef.name}';
|
|
@@ -1129,7 +1130,7 @@ Or with a <link> tag:
|
|
|
1129
1130
|
if (t.description) humanLog(` ${t.description}`);
|
|
1130
1131
|
}
|
|
1131
1132
|
humanLog('\nUsage:');
|
|
1132
|
-
humanLog(
|
|
1133
|
+
humanLog(' astryx theme add <slug> [target-path] Scaffold a theme file you own\n');
|
|
1133
1134
|
});
|
|
1134
1135
|
|
|
1135
1136
|
theme
|
|
@@ -1140,9 +1141,32 @@ Or with a <link> tag:
|
|
|
1140
1141
|
.action(async (slug, targetPath, options) => {
|
|
1141
1142
|
const json = program.opts().json || false;
|
|
1142
1143
|
|
|
1143
|
-
//
|
|
1144
|
-
//
|
|
1145
|
-
// ERR_FILE_EXISTS guard
|
|
1144
|
+
// Only prompt with a real TTY on stdin — a piped/redirected stdin would
|
|
1145
|
+
// make clack hang. Non-interactive callers fall through to the API's
|
|
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
|
+
|
|
1146
1170
|
let result;
|
|
1147
1171
|
try {
|
|
1148
1172
|
result = await themeAdd(slug, {
|
|
@@ -1167,7 +1191,7 @@ Or with a <link> tag:
|
|
|
1167
1191
|
if (t.description) humanLog(` ${t.description}`);
|
|
1168
1192
|
}
|
|
1169
1193
|
humanLog('\nUsage:');
|
|
1170
|
-
humanLog(
|
|
1194
|
+
humanLog(' astryx theme add <slug> [target-path] Scaffold a theme file you own\n');
|
|
1171
1195
|
return;
|
|
1172
1196
|
}
|
|
1173
1197
|
|
|
@@ -1195,3 +1219,40 @@ This is your copy of the ${displayName} theme — edit ${entry} to make it your
|
|
|
1195
1219
|
});
|
|
1196
1220
|
}
|
|
1197
1221
|
|
|
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 {getRunPrefix} 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} build "<what you're building>"`,
|
|
50
|
+
` ${run} astryx 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} template <name> [path]`,
|
|
55
|
+
` ${run} astryx template <name> [path]`,
|
|
56
56
|
'',
|
|
57
57
|
'3. If nothing matches exactly → compose:',
|
|
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)`,
|
|
58
|
+
` ${run} astryx template <name> --skeleton # study a close page's layout`,
|
|
59
|
+
` ${run} astryx template <BlockName> # drop in each block from the kit`,
|
|
60
|
+
` ${run} astryx component <Name> # fill remaining gaps (read props)`,
|
|
61
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 `astryx docs tokens`.',
|
|
65
65
|
' - Wrap the app in <Theme theme={...}> and import core reset.css + astryx.css.',
|
|
66
66
|
'',
|
|
67
|
-
`Tip: \`${run} build "<idea>"\` is the fastest way in. For a neutral`,
|
|
68
|
-
`lookup of any component/doc/template, use \`${run} search <query>\`.`,
|
|
67
|
+
`Tip: \`${run} astryx build "<idea>"\` is the fastest way in. For a neutral`,
|
|
68
|
+
`lookup of any component/doc/template, use \`${run} astryx search <query>\`.`,
|
|
69
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 = getRunPrefix();
|
|
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} component --list`);
|
|
118
|
+
humanLog(`Try a broader term, or browse: ${run} astryx 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(` → ${run} ${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} template ${pages[0].name} ./src/App.tsx`);
|
|
154
|
+
humanLog(`START → Scaffold the \`${pages[0].name}\` page template, then adapt: ${run} astryx 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} template ${pages[0].name} --skeleton) and compose the pieces below.`);
|
|
156
|
+
humanLog(`START → No exact page template. Use \`${pages[0].name}\` as a layout reference (${run} astryx template ${pages[0].name} --skeleton) and compose the pieces below.`);
|
|
157
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} component AppShell`);
|
|
171
|
+
humanLog(` full-page → AppShell; or Layout + SideNav/TopNav. ${run} astryx component AppShell`);
|
|
172
172
|
|
|
173
173
|
// BLOCKS — idea-specific composed patterns.
|
|
174
174
|
if (blocks.length) {
|