@astryxdesign/cli 0.1.7 → 0.1.8-canary.0c4d8d7
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 +40 -0
- package/README.md +115 -19
- package/docs/cli-integrations.doc.mjs +150 -0
- package/docs/elevation.doc.mjs +79 -1
- 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 +19 -19
- package/package.json +13 -11
- package/scripts/postinstall.mjs +74 -0
- package/src/api/doctor.mjs +3 -3
- package/src/api/error.mjs +2 -2
- package/src/codemods/__tests__/registry.test.mjs +1 -0
- package/src/codemods/ensure-jscodeshift.mjs +11 -27
- package/src/codemods/registry.mjs +1 -0
- package/src/codemods/run-codemod.mjs +1 -1
- package/src/codemods/runner.mjs +2 -2
- package/src/codemods/transforms/v0.1.0/__tests__/drop-xds-prefix-imports.test.mjs +42 -7
- package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-module-specifiers.test.mjs +43 -0
- package/src/codemods/transforms/v0.1.0/drop-xds-prefix-imports.mjs +102 -3
- package/src/codemods/transforms/v0.1.0/migrate-xds-module-specifiers.mjs +72 -6
- package/src/codemods/transforms/v0.1.8/__tests__/rename-avatar-size-scale.test.mjs +161 -0
- package/src/codemods/transforms/v0.1.8/index.mjs +19 -0
- package/src/codemods/transforms/v0.1.8/rename-avatar-size-scale.mjs +234 -0
- package/src/commands/agent-docs.mjs +136 -20
- package/src/commands/agent-docs.test.mjs +123 -10
- package/src/commands/build-theme.mjs +10 -71
- package/src/commands/build.mjs +15 -15
- package/src/commands/cli-postinstall.test.mjs +42 -0
- 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 +2 -2
- package/src/commands/layout.mjs +1 -1
- package/src/commands/search.mjs +4 -4
- package/src/commands/setup-nudge.test.mjs +108 -0
- package/src/commands/swizzle.mjs +12 -35
- package/src/commands/template.mjs +11 -31
- package/src/commands/upgrade.mjs +138 -32
- package/src/commands/upgrade.test.mjs +155 -1
- package/src/index.mjs +40 -6
- package/src/lib/cli-error.mjs +2 -2
- package/src/lib/component-format.mjs +2 -1
- package/src/lib/json-shim.mjs +1 -1
- package/src/lib/json.mjs +3 -3
- package/src/lib/term-log.mjs +48 -0
- package/src/types/api.d.ts +4 -9
- package/src/types/base.d.ts +24 -3
- package/src/types/build.d.ts +23 -0
- package/src/types/index.d.ts +1 -0
- package/src/types/swizzle.d.ts +4 -0
- package/src/types/template-api.d.ts +4 -1
- package/src/types/upgrade.d.ts +39 -2
- package/src/utils/package-manager.mjs +108 -4
- package/src/utils/package-manager.test.mjs +108 -1
- package/src/utils/path-safety.mjs +0 -18
- package/src/utils/update-check.mjs +5 -4
- package/src/utils/update-check.test.mjs +3 -3
- package/templates/blocks/components/Avatar/AvatarFallbackChain.tsx +4 -4
- package/templates/blocks/components/Avatar/AvatarGroup.tsx +2 -2
- package/templates/blocks/components/Avatar/AvatarInitialsFallback.tsx +1 -1
- package/templates/blocks/components/Avatar/AvatarShowcase.tsx +4 -4
- package/templates/blocks/components/Avatar/AvatarUserCard.tsx +1 -1
- package/templates/blocks/components/Avatar/AvatarWithImage.tsx +4 -4
- package/templates/blocks/components/Avatar/AvatarWithStatus.tsx +3 -3
- package/templates/blocks/components/AvatarGroup/AvatarGroupShowcase.tsx +2 -2
- package/templates/blocks/components/AvatarGroupOverflow/AvatarGroupOverflowCustomText.tsx +1 -1
- package/templates/blocks/components/AvatarGroupOverflow/AvatarGroupOverflowDefault.tsx +1 -1
- package/templates/blocks/components/AvatarGroupOverflow/AvatarGroupOverflowShowcase.tsx +2 -2
- package/templates/blocks/components/AvatarStatusDot/AvatarStatusDotShowcase.tsx +3 -3
- package/templates/blocks/components/AvatarStatusDot/AvatarStatusDotVariants.tsx +3 -3
- package/templates/blocks/components/Banner/BannerFloating.doc.mjs +14 -0
- package/templates/blocks/components/Banner/BannerFloating.tsx +16 -0
- package/templates/blocks/components/Button/ButtonFloating.doc.mjs +14 -0
- package/templates/blocks/components/Button/ButtonFloating.tsx +37 -0
- package/templates/blocks/components/ButtonGroup/ButtonGroupFloating.doc.mjs +14 -0
- package/templates/blocks/components/ButtonGroup/ButtonGroupFloating.tsx +23 -0
- package/templates/blocks/components/Card/CardElevations.doc.mjs +14 -0
- package/templates/blocks/components/Card/CardElevations.tsx +32 -0
- package/templates/blocks/components/Card/ClickableCardElevated.doc.mjs +14 -0
- package/templates/blocks/components/Card/ClickableCardElevated.tsx +21 -0
- package/templates/blocks/components/Card/SelectableCardElevated.doc.mjs +14 -0
- package/templates/blocks/components/Card/SelectableCardElevated.tsx +37 -0
- package/templates/blocks/components/Carousel/CarouselSnap.tsx +1 -1
- package/templates/blocks/components/ChatComposer/ChatComposerFlat.doc.mjs +14 -0
- package/templates/blocks/components/ChatComposer/ChatComposerFlat.tsx +75 -0
- package/templates/blocks/components/ChatMessage/ChatMessageAvatarName.tsx +2 -2
- package/templates/blocks/components/ChatMessage/ChatMessageMultiBubble.tsx +1 -1
- package/templates/blocks/components/ChatMessageBubble/ChatMessageBubbleGrouping.tsx +1 -1
- package/templates/blocks/components/ChatMessageBubble/ChatMessageBubbleMetadata.tsx +1 -1
- package/templates/blocks/components/ChatMessageList/ChatMessageListDensity.tsx +5 -9
- package/templates/blocks/components/ChatMessageList/ChatMessageListFullFeatured.tsx +1 -1
- package/templates/blocks/components/CodeBlock/CodeBlockTerminal.tsx +1 -1
- package/templates/blocks/components/HoverCard/HoverCardShowcase.tsx +1 -1
- package/templates/blocks/components/IconButton/IconButtonFloating.doc.mjs +14 -0
- package/templates/blocks/components/IconButton/IconButtonFloating.tsx +37 -0
- package/templates/blocks/components/Item/ItemWithMedia.tsx +2 -2
- package/templates/blocks/components/ListItem/ListItemWithMedia.tsx +2 -2
- package/templates/blocks/components/OverflowList/OverflowListCappedToolbar.doc.mjs +14 -0
- package/templates/blocks/components/OverflowList/OverflowListCappedToolbar.tsx +39 -0
- package/templates/blocks/components/OverflowList/OverflowListMultiRowTags.doc.mjs +14 -0
- package/templates/blocks/components/OverflowList/OverflowListMultiRowTags.tsx +44 -0
- package/templates/blocks/components/Pagination/PaginationDotsCarousel.tsx +2 -6
- package/templates/blocks/components/Stack/StackFillItem.tsx +2 -6
- package/templates/blocks/components/TabList/TabListTabsWithActions.doc.mjs +1 -1
- package/templates/blocks/components/TabList/TabListTabsWithActions.tsx +2 -7
- package/templates/blocks/components/Table/TableRowStatusTable.doc.mjs +14 -0
- package/templates/blocks/components/Table/TableRowStatusTable.tsx +59 -0
- package/templates/blocks/components/Thumbnail/ThumbnailElevated.doc.mjs +14 -0
- package/templates/blocks/components/Thumbnail/ThumbnailElevated.tsx +31 -0
- package/templates/blocks/components/TypeaheadItem/TypeaheadItemShowcase.tsx +1 -1
- package/templates/pages/ai-chat/page.tsx +4 -4
- package/templates/pages/dashboard-portfolio/page.tsx +3 -11
- package/templates/pages/detail-page/page.tsx +5 -12
- package/templates/pages/documentation-design/page.tsx +1 -1
- package/templates/pages/messaging-shell/page.tsx +6 -8
- package/templates/pages/table-grouped/page.tsx +9 -14
- package/templates/pages/table-page/page.tsx +7 -9
- package/templates/pages/table-page-heatmap-status/page.tsx +5 -13
- package/docs/integration-authoring.md +0 -105
- package/src/utils/interactive.mjs +0 -76
- package/src/utils/interactive.test.mjs +0 -70
package/src/commands/init.mjs
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* @file init command —
|
|
4
|
+
* @file init command — non-interactive setup + feature installer
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* `astryx init` is non-interactive by default: it installs the AGENTS.md/
|
|
7
|
+
* CLAUDE.md cheat sheet with NO prompts, so it behaves identically for humans,
|
|
8
|
+
* AI agents, CI, and piped I/O — it never hangs or errors on a missing TTY.
|
|
9
|
+
* Non-interactive feature install: `astryx init --features agents,theme,template`
|
|
8
10
|
* Re-runnable: safe to run multiple times, idempotent
|
|
9
11
|
*
|
|
10
12
|
* Features:
|
|
11
13
|
* agents — Install AGENTS.md/CLAUDE.md cheat sheet for AI coding agents
|
|
12
|
-
* theme —
|
|
14
|
+
* theme — Point to the theme workflow (`astryx theme`)
|
|
13
15
|
* template — Copy a starter page template
|
|
14
16
|
*/
|
|
15
17
|
|
|
16
|
-
import * as p from '@clack/prompts';
|
|
17
18
|
import * as path from 'node:path';
|
|
18
19
|
import * as fs from 'node:fs';
|
|
19
20
|
import {CLI_ROOT} from '../utils/paths.mjs';
|
|
20
21
|
import {PathSafetyError} from '../utils/path-safety.mjs';
|
|
21
|
-
import {
|
|
22
|
+
import {getCliInvocation} from '../utils/package-manager.mjs';
|
|
22
23
|
import {installAgentDocs, removeAgentDocs} from './agent-docs.mjs';
|
|
23
24
|
import {listTemplates} from './template.mjs';
|
|
24
25
|
import {humanLog} from '../lib/json.mjs';
|
|
25
26
|
import {cliError} from '../lib/cli-error.mjs';
|
|
26
27
|
import {ERROR_CODES} from '../lib/error-codes.mjs';
|
|
27
|
-
import {requireInteractive} from '../utils/interactive.mjs';
|
|
28
28
|
|
|
29
29
|
const VALID_FEATURES = ['agents', 'theme', 'template'];
|
|
30
|
-
const run =
|
|
30
|
+
const run = getCliInvocation();
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* Build the "Next steps" lines printed at the end of `astryx init`.
|
|
@@ -40,10 +40,10 @@ const run = getRunPrefix();
|
|
|
40
40
|
*
|
|
41
41
|
* Exported for testing.
|
|
42
42
|
*
|
|
43
|
-
* @param {string}
|
|
43
|
+
* @param {string} invocation install-aware CLI invocation stem (e.g. `npx astryx`, `pnpm exec astryx`, or `npx @astryxdesign/cli` for one-off runs)
|
|
44
44
|
* @returns {string[]} ordered list of human-facing lines
|
|
45
45
|
*/
|
|
46
|
-
export function getNextSteps(
|
|
46
|
+
export function getNextSteps(invocation) {
|
|
47
47
|
return [
|
|
48
48
|
'',
|
|
49
49
|
' Next steps:',
|
|
@@ -54,23 +54,15 @@ export function getNextSteps(runPrefix) {
|
|
|
54
54
|
" import { neutralTheme } from '@astryxdesign/theme-neutral/built'",
|
|
55
55
|
" import '@astryxdesign/theme-neutral/theme.css'",
|
|
56
56
|
' <Theme theme={neutralTheme}>...</Theme>',
|
|
57
|
-
` For custom themes, run \`${
|
|
58
|
-
` 4. ${
|
|
57
|
+
` For custom themes, run \`${invocation} theme build <file>\` to generate the built artifacts.`,
|
|
58
|
+
` 4. ${invocation} --help for all commands`,
|
|
59
59
|
'',
|
|
60
60
|
];
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
function isCancel(value) {
|
|
64
|
-
if (p.isCancel(value)) {
|
|
65
|
-
p.cancel('Setup cancelled.');
|
|
66
|
-
process.exit(0);
|
|
67
|
-
}
|
|
68
|
-
return value;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
63
|
// ─── Feature: agents ─────────────────────────────────────────────────────────
|
|
72
64
|
|
|
73
|
-
function runAgents(targetDir, {
|
|
65
|
+
function runAgents(targetDir, {agent, agentDocsPath} = {}) {
|
|
74
66
|
try {
|
|
75
67
|
const paths = agentDocsPath
|
|
76
68
|
? Array.isArray(agentDocsPath)
|
|
@@ -79,114 +71,55 @@ function runAgents(targetDir, {interactive = true, agent, agentDocsPath} = {}) {
|
|
|
79
71
|
: undefined;
|
|
80
72
|
const written = installAgentDocs(targetDir, {agent, paths});
|
|
81
73
|
const summary = written.join(', ');
|
|
82
|
-
|
|
83
|
-
p.log.success(`AI agent docs installed → ${summary}`);
|
|
84
|
-
} else {
|
|
85
|
-
humanLog(`✓ AI agent docs installed → ${summary}`);
|
|
86
|
-
}
|
|
74
|
+
humanLog(`✓ AI agent docs installed → ${summary}`);
|
|
87
75
|
} catch (err) {
|
|
88
76
|
// PathSafetyError carries a precise, user-actionable message —
|
|
89
77
|
// surface it instead of the generic "could not install" warning so
|
|
90
78
|
// misconfigured --agent-docs-path values aren't silently swallowed.
|
|
91
79
|
if (err instanceof PathSafetyError) {
|
|
92
|
-
|
|
93
|
-
if (interactive) {
|
|
94
|
-
p.log.error(msg);
|
|
95
|
-
} else {
|
|
96
|
-
console.error(msg);
|
|
97
|
-
}
|
|
80
|
+
console.error(`Error: ${err.message}`);
|
|
98
81
|
process.exitCode = 1;
|
|
99
82
|
return;
|
|
100
83
|
}
|
|
101
|
-
|
|
102
|
-
if (interactive) {
|
|
103
|
-
p.log.warning(msg);
|
|
104
|
-
} else {
|
|
105
|
-
console.error(msg);
|
|
106
|
-
}
|
|
84
|
+
console.error(`Could not install agent docs. Try again with \`${run} init --features agents\`.`);
|
|
107
85
|
}
|
|
108
86
|
}
|
|
109
87
|
|
|
110
88
|
// ─── Feature: theme ──────────────────────────────────────────────────────────
|
|
111
89
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
humanLog(`✓ Theme scaffolding requires interactive mode. Run \`${run} astryx theme\` instead.`);
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
p.note(
|
|
119
|
-
'Create a custom theme with your brand colors.\n' +
|
|
120
|
-
`Run \`${run} astryx theme\` for the full theme wizard.\n` +
|
|
121
|
-
`Run \`${run} astryx theme --list\` to see existing themes.`,
|
|
122
|
-
'Themes',
|
|
123
|
-
);
|
|
90
|
+
function runTheme() {
|
|
91
|
+
humanLog(`✓ For a custom theme, run \`${run} theme\` (browse) or \`${run} theme add <slug>\` (scaffold).`);
|
|
124
92
|
}
|
|
125
93
|
|
|
126
94
|
// ─── Feature: template ───────────────────────────────────────────────────────
|
|
127
95
|
|
|
128
|
-
|
|
96
|
+
function runTemplate(targetDir, {templateName} = {}) {
|
|
129
97
|
const templates = listTemplates();
|
|
130
98
|
if (templates.length === 0) return;
|
|
131
99
|
|
|
132
|
-
if (!
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
humanLog('');
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (!templates.includes(templateName)) {
|
|
147
|
-
cliError(`Unknown template "${templateName}". Available: ${templates.join(', ')}`, {code: ERROR_CODES.ERR_UNKNOWN_TEMPLATE});
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const outputDir = path.resolve(targetDir, `./src/pages/${templateName}`);
|
|
152
|
-
const srcPath = path.join(CLI_ROOT, 'templates', 'pages', templateName, 'page.tsx');
|
|
153
|
-
fs.mkdirSync(outputDir, {recursive: true});
|
|
154
|
-
fs.copyFileSync(srcPath, path.join(outputDir, 'page.tsx'));
|
|
155
|
-
humanLog(`✓ Template created at ${path.relative(targetDir, outputDir)}/page.tsx`);
|
|
100
|
+
if (!templateName) {
|
|
101
|
+
// Point agents at the build workflow rather than dumping page-template
|
|
102
|
+
// names — `build` surfaces pages AND blocks AND components for an idea,
|
|
103
|
+
// and `build` with no args is the full how-to-build playbook.
|
|
104
|
+
humanLog('✓ To build UI, use these commands:');
|
|
105
|
+
humanLog('');
|
|
106
|
+
humanLog(` ${run} build "<what you're building>" build a page — kit: closest template + blocks + components`);
|
|
107
|
+
humanLog(` ${run} build the how-to-build workflow (read this first)`);
|
|
108
|
+
humanLog(` ${run} search <query> find anything — components, docs, templates, blocks`);
|
|
109
|
+
humanLog('');
|
|
156
110
|
return;
|
|
157
111
|
}
|
|
158
112
|
|
|
159
|
-
|
|
160
|
-
{
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
{value: 'login', label: 'Login Page — Auth form with inputs'},
|
|
164
|
-
];
|
|
165
|
-
|
|
166
|
-
const templateChoice = isCancel(
|
|
167
|
-
await p.select({
|
|
168
|
-
message: 'Start with a page template?',
|
|
169
|
-
options: TEMPLATE_OPTIONS,
|
|
170
|
-
}),
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
if (templateChoice === 'skip') return;
|
|
174
|
-
|
|
175
|
-
const targetPath = isCancel(
|
|
176
|
-
await p.text({
|
|
177
|
-
message: 'Where should the template be created?',
|
|
178
|
-
initialValue: `./src/pages/${templateChoice}`,
|
|
179
|
-
placeholder: `./src/pages/${templateChoice}`,
|
|
180
|
-
}),
|
|
181
|
-
);
|
|
182
|
-
|
|
183
|
-
const outputDir = path.resolve(targetDir, targetPath);
|
|
184
|
-
const srcPath = path.join(CLI_ROOT, 'templates', 'pages', templateChoice, 'page.tsx');
|
|
113
|
+
if (!templates.includes(templateName)) {
|
|
114
|
+
cliError(`Unknown template "${templateName}". Available: ${templates.join(', ')}`, {code: ERROR_CODES.ERR_UNKNOWN_TEMPLATE});
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
185
117
|
|
|
118
|
+
const outputDir = path.resolve(targetDir, `./src/pages/${templateName}`);
|
|
119
|
+
const srcPath = path.join(CLI_ROOT, 'templates', 'pages', templateName, 'page.tsx');
|
|
186
120
|
fs.mkdirSync(outputDir, {recursive: true});
|
|
187
121
|
fs.copyFileSync(srcPath, path.join(outputDir, 'page.tsx'));
|
|
188
|
-
|
|
189
|
-
p.log.success(`Template created at ${path.relative(targetDir, outputDir)}/page.tsx`);
|
|
122
|
+
humanLog(`✓ Template created at ${path.relative(targetDir, outputDir)}/page.tsx`);
|
|
190
123
|
}
|
|
191
124
|
|
|
192
125
|
// ─── Command ─────────────────────────────────────────────────────────────────
|
|
@@ -200,7 +133,7 @@ export function registerInit(program) {
|
|
|
200
133
|
.option('--remove-agents', 'Remove AI agent docs from all agent doc files')
|
|
201
134
|
.option('--agent <tool>', 'Target AI tool for agent docs: claude, cursor, codex, hermes, all')
|
|
202
135
|
.option('--agent-docs-path <path...>', 'Explicit file path(s) for agent docs')
|
|
203
|
-
.action(
|
|
136
|
+
.action((options) => {
|
|
204
137
|
const targetDir = process.cwd();
|
|
205
138
|
|
|
206
139
|
// Remove mode
|
|
@@ -224,62 +157,25 @@ export function registerInit(program) {
|
|
|
224
157
|
|
|
225
158
|
for (const feature of features) {
|
|
226
159
|
if (feature === 'agents') runAgents(targetDir, {
|
|
227
|
-
interactive: false,
|
|
228
160
|
agent: options.agent,
|
|
229
161
|
agentDocsPath: options.agentDocsPath,
|
|
230
162
|
});
|
|
231
|
-
if (feature === 'theme')
|
|
232
|
-
if (feature === 'template')
|
|
163
|
+
if (feature === 'theme') runTheme();
|
|
164
|
+
if (feature === 'template') runTemplate(targetDir, {});
|
|
233
165
|
}
|
|
234
166
|
return;
|
|
235
167
|
}
|
|
236
168
|
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
hint: `\`${run} astryx init --all\` or \`--features agents,theme,template\``,
|
|
169
|
+
// No flags: TTY-free default. `astryx init` installs the AI agent cheat
|
|
170
|
+
// sheet with NO prompts, so it behaves identically for humans, agents, CI,
|
|
171
|
+
// and piped I/O — it never hangs or errors on a missing TTY. (A guided
|
|
172
|
+
// `--interactive` setup may return later as an explicit opt-in.)
|
|
173
|
+
runAgents(targetDir, {
|
|
174
|
+
agent: options.agent,
|
|
175
|
+
agentDocsPath: options.agentDocsPath,
|
|
245
176
|
});
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
p.note(
|
|
250
|
-
'A design system for building internal tools\nwith 300+ React components.',
|
|
251
|
-
'About',
|
|
252
|
-
);
|
|
253
|
-
|
|
254
|
-
// Feature: agents
|
|
255
|
-
const shouldInstallAgents = isCancel(
|
|
256
|
-
await p.confirm({
|
|
257
|
-
message: 'Install AI agent support? (adds a design system cheat sheet to AGENTS.md)',
|
|
258
|
-
initialValue: true,
|
|
259
|
-
}),
|
|
260
|
-
);
|
|
261
|
-
|
|
262
|
-
if (shouldInstallAgents) {
|
|
263
|
-
const s = p.spinner();
|
|
264
|
-
s.start('Installing agent docs');
|
|
265
|
-
runAgents(targetDir);
|
|
266
|
-
s.stop('Done');
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Feature: swizzle awareness
|
|
270
|
-
p.note(
|
|
271
|
-
`You can customize any component with:\n ${run} astryx swizzle Button\n ${run} astryx swizzle --list`,
|
|
272
|
-
'Component Customization',
|
|
273
|
-
);
|
|
274
|
-
|
|
275
|
-
// Feature: template
|
|
276
|
-
await runTemplate(targetDir);
|
|
277
|
-
|
|
278
|
-
// Feature: theme awareness
|
|
279
|
-
await runTheme();
|
|
280
|
-
|
|
281
|
-
// Outro
|
|
282
|
-
p.outro('Design system initialized!');
|
|
177
|
+
humanLog('');
|
|
178
|
+
humanLog(` Tip: \`${run} init --all\` also points you to the theme and page-building workflows.`);
|
|
283
179
|
|
|
284
180
|
for (const line of getNextSteps(run)) {
|
|
285
181
|
humanLog(line);
|
|
@@ -23,7 +23,7 @@ import {describe, it, expect} from 'vitest';
|
|
|
23
23
|
import {getNextSteps} from './init.mjs';
|
|
24
24
|
|
|
25
25
|
describe('init Next steps theme guidance', () => {
|
|
26
|
-
const text = getNextSteps('npx').join('\n');
|
|
26
|
+
const text = getNextSteps('npx astryx').join('\n');
|
|
27
27
|
|
|
28
28
|
it('mentions the base CSS imports so the app is not left unstyled', () => {
|
|
29
29
|
expect(text).toContain("'@astryxdesign/core/reset.css'");
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* @file Subprocess no-hang tests for
|
|
4
|
+
* @file Subprocess no-hang tests for `astryx init`.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* These tests spawn the CLI
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* status null) — so asserting `signal === null && status === 1` proves the
|
|
12
|
-
* process exited cleanly rather than hanging.
|
|
6
|
+
* TTY was removed from the CLI: `init` is non-interactive by default and must
|
|
7
|
+
* run cleanly (exit 0, writing the agent cheat sheet) instead of hanging on a
|
|
8
|
+
* prompt. These tests spawn the CLI with stdin/stdout NOT a TTY (the CI / piped
|
|
9
|
+
* / agent condition). A hang would show as signal SIGTERM + status null; we
|
|
10
|
+
* assert `signal === null && status === 0` to prove it exits cleanly and works.
|
|
13
11
|
*/
|
|
14
12
|
|
|
15
13
|
import {describe, it, expect, beforeEach, afterEach} from 'vitest';
|
|
@@ -42,25 +40,24 @@ afterEach(() => {
|
|
|
42
40
|
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
43
41
|
});
|
|
44
42
|
|
|
45
|
-
describe('init non-interactive
|
|
46
|
-
it('
|
|
43
|
+
describe('init is non-interactive by default (no TTY needed)', () => {
|
|
44
|
+
it('runs cleanly (exit 0, no hang) and writes agent docs', () => {
|
|
47
45
|
const r = runCli(['init']);
|
|
48
|
-
expect(r.signal).toBeNull();
|
|
49
|
-
expect(r.status).toBe(
|
|
50
|
-
expect(fs.readdirSync(tmpDir)).
|
|
46
|
+
expect(r.signal).toBeNull(); // did not hang
|
|
47
|
+
expect(r.status).toBe(0); // succeeded without a TTY
|
|
48
|
+
expect(fs.readdirSync(tmpDir).length).toBeGreaterThan(0); // wrote the cheat sheet
|
|
51
49
|
});
|
|
52
50
|
|
|
53
|
-
it('
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
expect(
|
|
59
|
-
expect(
|
|
60
|
-
expect(out).toMatch(/--features/);
|
|
51
|
+
it('writes an agent-doc file containing the ASTRYX cheat-sheet marker', () => {
|
|
52
|
+
runCli(['init']);
|
|
53
|
+
// init injects into existing agent-doc files if present, else creates AGENTS.md
|
|
54
|
+
const candidates = ['AGENTS.md', 'CLAUDE.md', '.cursorrules', path.join('.claude', 'CLAUDE.md')];
|
|
55
|
+
const doc = candidates.find(f => fs.existsSync(path.join(tmpDir, f)));
|
|
56
|
+
expect(doc).toBeTruthy();
|
|
57
|
+
expect(fs.readFileSync(path.join(tmpDir, doc), 'utf8')).toMatch(/ASTRYX:START/);
|
|
61
58
|
});
|
|
62
59
|
|
|
63
|
-
it('still runs --features agents non-interactively
|
|
60
|
+
it('still runs --features agents non-interactively', () => {
|
|
64
61
|
const r = runCli(['init', '--features', 'agents']);
|
|
65
62
|
expect(r.signal).toBeNull();
|
|
66
63
|
expect(r.status).toBe(0);
|
|
@@ -45,7 +45,7 @@ function runCli(args, {cwd} = {}) {
|
|
|
45
45
|
|
|
46
46
|
function parseJson(stdout) {
|
|
47
47
|
// CLI emits a single JSON document. If anything else snuck onto stdout
|
|
48
|
-
// (
|
|
48
|
+
// (log output, console.log strings, etc.) JSON.parse will throw —
|
|
49
49
|
// which is exactly the failure mode we want to catch in tests.
|
|
50
50
|
return JSON.parse(stdout);
|
|
51
51
|
}
|
|
@@ -61,7 +61,7 @@ afterEach(() => {
|
|
|
61
61
|
});
|
|
62
62
|
|
|
63
63
|
describe('--json contract: rejects before side effects', () => {
|
|
64
|
-
it('astryx init --json --features agents does not write
|
|
64
|
+
it('astryx init --json --features agents does not write agent docs', () => {
|
|
65
65
|
const before = fs.readdirSync(tmpDir);
|
|
66
66
|
expect(before).toEqual([]);
|
|
67
67
|
|
package/src/commands/layout.mjs
CHANGED
|
@@ -20,7 +20,7 @@ import {layoutExpand, layoutCheck, layoutGrammar} from '../api/layout.mjs';
|
|
|
20
20
|
/** Resolve the expression from arg, --file, or stdin ('-'). */
|
|
21
21
|
async function readExpression(expr, options) {
|
|
22
22
|
if (options.file) return fs.readFileSync(options.file, 'utf-8');
|
|
23
|
-
if (expr === '-'
|
|
23
|
+
if (expr === '-') {
|
|
24
24
|
const chunks = [];
|
|
25
25
|
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
26
26
|
return Buffer.concat(chunks).toString('utf-8');
|
package/src/commands/search.mjs
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* astryx search button --json Typed JSON envelope
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import {
|
|
19
|
+
import {getCliInvocation, formatCliCommand} from '../utils/package-manager.mjs';
|
|
20
20
|
import {jsonOut, humanLog} from '../lib/json.mjs';
|
|
21
21
|
import {cliError} from '../lib/cli-error.mjs';
|
|
22
22
|
import {search as searchApi, SEARCH_DOMAINS} from '../api/search.mjs';
|
|
@@ -63,7 +63,7 @@ export function registerSearch(program) {
|
|
|
63
63
|
if (json) return jsonOut(result.type, result.data);
|
|
64
64
|
|
|
65
65
|
// ── Text output ──────────────────────────────────────────────
|
|
66
|
-
const run =
|
|
66
|
+
const run = getCliInvocation();
|
|
67
67
|
const {query: q, results} = result.data;
|
|
68
68
|
|
|
69
69
|
// No matches is a valid, successful outcome — clean message, exit 0.
|
|
@@ -71,7 +71,7 @@ export function registerSearch(program) {
|
|
|
71
71
|
humanLog('');
|
|
72
72
|
humanLog(`No results for "${q}".`);
|
|
73
73
|
humanLog('');
|
|
74
|
-
humanLog(`Try a broader term, or browse: ${run}
|
|
74
|
+
humanLog(`Try a broader term, or browse: ${run} component --list`);
|
|
75
75
|
humanLog('');
|
|
76
76
|
return;
|
|
77
77
|
}
|
|
@@ -86,7 +86,7 @@ export function registerSearch(program) {
|
|
|
86
86
|
if (r.description) {
|
|
87
87
|
humanLog(` ${r.description}`);
|
|
88
88
|
}
|
|
89
|
-
humanLog(` → ${
|
|
89
|
+
humanLog(` → ${formatCliCommand(r.command)}`);
|
|
90
90
|
if (options.detail) {
|
|
91
91
|
if (r.import) humanLog(` import: ${r.import}`);
|
|
92
92
|
humanLog(` match: ${r.reason} (score ${r.score})`);
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Guardrail tests — the centralized setup check + enforcement layer 3.
|
|
5
|
+
*
|
|
6
|
+
* Unit: isAstryxInitialized() detects the Astryx marker across EVERY agent-doc
|
|
7
|
+
* location (including the previously-missed Hermes files) and legacy XDS blocks.
|
|
8
|
+
*
|
|
9
|
+
* Integration: the CLI nudges (stderr) before a command when the project hasn't
|
|
10
|
+
* run init — INCLUDING in --json mode (agents pass --json, and stderr never
|
|
11
|
+
* corrupts the stdout JSON envelope). Quiet once set up, outside a project, and
|
|
12
|
+
* for the installer command itself.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {describe, it, expect, beforeEach, afterEach} from 'vitest';
|
|
16
|
+
import {spawnSync} from 'node:child_process';
|
|
17
|
+
import * as fs from 'node:fs';
|
|
18
|
+
import * as path from 'node:path';
|
|
19
|
+
import * as os from 'node:os';
|
|
20
|
+
import {fileURLToPath} from 'node:url';
|
|
21
|
+
import {isAstryxInitialized} from './agent-docs.mjs';
|
|
22
|
+
|
|
23
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
const CLI = path.resolve(__dirname, '..', '..', 'bin', 'astryx.mjs');
|
|
25
|
+
const MARKER = '<!-- ASTRYX:START -->';
|
|
26
|
+
const NUDGE = /finish setup and install the Astryx agent prompt/;
|
|
27
|
+
|
|
28
|
+
let tmp;
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-setup-nudge-'));
|
|
31
|
+
});
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
fs.rmSync(tmp, {recursive: true, force: true});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
function write(rel, body) {
|
|
37
|
+
const p = path.join(tmp, rel);
|
|
38
|
+
fs.mkdirSync(path.dirname(p), {recursive: true});
|
|
39
|
+
fs.writeFileSync(p, body);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function run(args, cwd = tmp) {
|
|
43
|
+
return spawnSync(process.execPath, [CLI, ...args], {
|
|
44
|
+
cwd,
|
|
45
|
+
encoding: 'utf8',
|
|
46
|
+
timeout: 30_000,
|
|
47
|
+
env: {...process.env, FORCE_COLOR: '0'},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('isAstryxInitialized — centralized setup check (one place)', () => {
|
|
52
|
+
it('is false in an empty project', () => {
|
|
53
|
+
expect(isAstryxInitialized(tmp)).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Covers EVERY location a preset can write — .hermes.md/HERMES.md were the gap.
|
|
57
|
+
it.each(['AGENTS.md', 'CLAUDE.md', '.claude/CLAUDE.md', '.cursorrules', '.hermes.md', 'HERMES.md'])(
|
|
58
|
+
'detects the marker in %s',
|
|
59
|
+
file => {
|
|
60
|
+
write(file, `# doc\n${MARKER}\nbody`);
|
|
61
|
+
expect(isAstryxInitialized(tmp)).toBe(true);
|
|
62
|
+
},
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
it('detects the legacy XDS marker for back-compat', () => {
|
|
66
|
+
write('AGENTS.md', '<!-- XDS:START -->');
|
|
67
|
+
expect(isAstryxInitialized(tmp)).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('is false when a doc file exists WITHOUT the marker', () => {
|
|
71
|
+
write('AGENTS.md', 'project notes, no astryx block');
|
|
72
|
+
expect(isAstryxInitialized(tmp)).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('enforcement layer 3 — per-command setup nudge', () => {
|
|
77
|
+
const asProject = () => write('package.json', '{"name":"t"}');
|
|
78
|
+
|
|
79
|
+
it('nudges on stderr after a command when not set up', () => {
|
|
80
|
+
asProject();
|
|
81
|
+
const r = run(['docs', 'tokens']);
|
|
82
|
+
expect(r.stderr).toMatch(NUDGE);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('is suppressed in --json (machine mode stays clean), stdout valid JSON', () => {
|
|
86
|
+
asProject();
|
|
87
|
+
const r = run(['--json', 'docs', 'tokens']);
|
|
88
|
+
// --json is machine output with a clean stdout+stderr contract; the human
|
|
89
|
+
// nudge must NOT leak into it (json-shim: error envelopes have empty stderr).
|
|
90
|
+
expect(r.stderr).not.toMatch(NUDGE);
|
|
91
|
+
expect(() => JSON.parse(r.stdout)).not.toThrow();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('is quiet once set up (marker present)', () => {
|
|
95
|
+
asProject();
|
|
96
|
+
write('AGENTS.md', MARKER);
|
|
97
|
+
expect(run(['docs', 'tokens']).stderr).not.toMatch(NUDGE);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('is quiet outside a project (no package.json)', () => {
|
|
101
|
+
expect(run(['docs', 'tokens']).stderr).not.toMatch(NUDGE);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('does not nudge for the installer command itself', () => {
|
|
105
|
+
asProject();
|
|
106
|
+
expect(run(['init']).stderr).not.toMatch(NUDGE);
|
|
107
|
+
});
|
|
108
|
+
});
|
package/src/commands/swizzle.mjs
CHANGED
|
@@ -15,18 +15,16 @@
|
|
|
15
15
|
|
|
16
16
|
import * as fs from 'node:fs';
|
|
17
17
|
import * as path from 'node:path';
|
|
18
|
-
import * as p from '@clack/prompts';
|
|
19
18
|
import {findCoreDir, listComponents} from '../utils/paths.mjs';
|
|
20
19
|
import {
|
|
21
20
|
assertWithin,
|
|
22
21
|
PathSafetyError,
|
|
23
|
-
isNonInteractive,
|
|
24
22
|
} from '../utils/path-safety.mjs';
|
|
25
23
|
import {jsonOut, humanLog} from '../lib/json.mjs';
|
|
26
24
|
import {cliError} from '../lib/cli-error.mjs';
|
|
27
25
|
import {ERROR_CODES} from '../lib/error-codes.mjs';
|
|
28
26
|
import {checkGhCli} from '../utils/github.mjs';
|
|
29
|
-
import {
|
|
27
|
+
import {getCliInvocation} from '../utils/package-manager.mjs';
|
|
30
28
|
import {Project} from '../lib/project.mjs';
|
|
31
29
|
import {
|
|
32
30
|
CORE_PACKAGE,
|
|
@@ -99,14 +97,6 @@ function buildFeedback(component, issuesUrl) {
|
|
|
99
97
|
return feedback;
|
|
100
98
|
}
|
|
101
99
|
|
|
102
|
-
function isCancel(value) {
|
|
103
|
-
if (p.isCancel(value)) {
|
|
104
|
-
p.cancel('Cancelled.');
|
|
105
|
-
process.exit(0);
|
|
106
|
-
}
|
|
107
|
-
return value;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
100
|
/**
|
|
111
101
|
* Load the configured integrations + core issues URL for `cwd`, swallowing any
|
|
112
102
|
* config errors so swizzle never hard-fails on a malformed/absent config. An
|
|
@@ -190,6 +180,7 @@ export function registerSwizzle(program) {
|
|
|
190
180
|
.action(async (component, options) => {
|
|
191
181
|
const coreDir = findCoreDir(process.cwd());
|
|
192
182
|
const json = program.opts().json || false;
|
|
183
|
+
const run = getCliInvocation();
|
|
193
184
|
|
|
194
185
|
if (!coreDir) {
|
|
195
186
|
cliError(
|
|
@@ -207,10 +198,10 @@ export function registerSwizzle(program) {
|
|
|
207
198
|
for (const name of components) {
|
|
208
199
|
humanLog(` ${name}`);
|
|
209
200
|
}
|
|
210
|
-
humanLog(`\nUsage:
|
|
211
|
-
humanLog(
|
|
201
|
+
humanLog(`\nUsage: ${run} swizzle <component>\n`);
|
|
202
|
+
humanLog(`Example: ${run} swizzle Button`);
|
|
212
203
|
humanLog(
|
|
213
|
-
|
|
204
|
+
` ${run} swizzle XDSButton (XDS prefix also works)\n`,
|
|
214
205
|
);
|
|
215
206
|
return;
|
|
216
207
|
}
|
|
@@ -314,25 +305,11 @@ export function registerSwizzle(program) {
|
|
|
314
305
|
|
|
315
306
|
if (existingFiles.length > 0 && !options.overwrite) {
|
|
316
307
|
const relOutputForMsg = path.relative(process.cwd(), outputDir) || '.';
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
const confirmed = isCancel(
|
|
325
|
-
await p.confirm({
|
|
326
|
-
message:
|
|
327
|
-
`Overwrite ${existingFiles.length} existing file(s) in ${relOutputForMsg}/? ` +
|
|
328
|
-
`(${existingFiles.slice(0, 3).join(', ')}${existingFiles.length > 3 ? ', …' : ''})`,
|
|
329
|
-
initialValue: false,
|
|
330
|
-
}),
|
|
331
|
-
);
|
|
332
|
-
if (!confirmed) {
|
|
333
|
-
humanLog('Aborted. Re-run with --overwrite to replace files.');
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
308
|
+
const msg =
|
|
309
|
+
`Refusing to overwrite ${existingFiles.length} existing file(s) in ${relOutputForMsg}/. ` +
|
|
310
|
+
`Re-run with --overwrite (or -f) to replace them.`;
|
|
311
|
+
cliError(msg, {code: ERROR_CODES.ERR_FILE_EXISTS});
|
|
312
|
+
return;
|
|
336
313
|
}
|
|
337
314
|
|
|
338
315
|
fs.mkdirSync(outputDir, {recursive: true});
|
|
@@ -381,7 +358,7 @@ export function registerSwizzle(program) {
|
|
|
381
358
|
const feedback = buildFeedback(dirName, owner.issuesUrl);
|
|
382
359
|
|
|
383
360
|
if (json) {
|
|
384
|
-
/** @type {
|
|
361
|
+
/** @type {import('../types/swizzle').SwizzleCopyResponse['data']} */
|
|
385
362
|
const payload = {
|
|
386
363
|
component: dirName,
|
|
387
364
|
package: owner.package,
|
|
@@ -411,7 +388,7 @@ export function registerSwizzle(program) {
|
|
|
411
388
|
humanLog(
|
|
412
389
|
' Without one they render unstyled (no error). See setup per framework:',
|
|
413
390
|
);
|
|
414
|
-
humanLog(` ${
|
|
391
|
+
humanLog(` ${run} docs styling`);
|
|
415
392
|
humanLog(
|
|
416
393
|
' Next.js note: the StyleX Babel plugin disables SWC and breaks next/font —',
|
|
417
394
|
);
|