@astryxdesign/cli 0.1.7-canary.cc83e7a → 0.1.7-canary.d91c851
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 +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 +11 -10
- package/scripts/postinstall.mjs +74 -0
- package/src/api/doctor.mjs +3 -3
- 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 +128 -14
- package/src/commands/agent-docs.test.mjs +97 -4
- 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 +1 -1
- 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 +11 -34
- 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/component-format.mjs +2 -1
- package/src/lib/term-log.mjs +48 -0
- package/src/types/upgrade.d.ts +39 -2
- 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/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/Carousel/CarouselSnap.tsx +1 -1
- 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/HoverCard/HoverCardShowcase.tsx +1 -1
- package/templates/blocks/components/Item/ItemWithMedia.tsx +2 -2
- package/templates/blocks/components/ListItem/ListItemWithMedia.tsx +2 -2
- 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/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
|
@@ -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';
|
|
@@ -31,6 +31,9 @@ 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';
|
|
34
37
|
|
|
35
38
|
|
|
36
39
|
const MARKER_START = '<!-- ASTRYX:START -->';
|
|
@@ -45,20 +48,132 @@ const LEGACY_MARKER_END = '<!-- XDS:END -->';
|
|
|
45
48
|
*/
|
|
46
49
|
const AGENT_PRESETS = {
|
|
47
50
|
claude: [CLAUDE_MD, CLAUDE_DIR_MD],
|
|
48
|
-
cursor: [
|
|
51
|
+
cursor: [CURSOR_RULES, AGENTS_MD],
|
|
49
52
|
codex: [AGENTS_MD],
|
|
50
|
-
hermes: [
|
|
53
|
+
hermes: [HERMES_DOT_MD, HERMES_MD, AGENTS_MD],
|
|
51
54
|
};
|
|
52
55
|
|
|
53
56
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
57
|
+
* The canonical set of EVERY location an --agent preset (or the default) can
|
|
58
|
+
* write the Astryx block. SINGLE SOURCE OF TRUTH: discovery, removal, and the
|
|
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).
|
|
56
76
|
* @param {string} targetDir
|
|
57
77
|
* @returns {string[]} Relative paths of existing agent doc files
|
|
58
78
|
*/
|
|
59
79
|
export function discoverAgentDocs(targetDir) {
|
|
60
|
-
|
|
61
|
-
|
|
80
|
+
return AGENT_DOC_PATHS.filter(p => fs.existsSync(path.join(targetDir, p)));
|
|
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;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Parse the Astryx version a managed block was generated for, from its header
|
|
109
|
+
* line ("Astryx v1.2.3 · N components"). Returns null when the block predates
|
|
110
|
+
* the versioned header (e.g. a legacy XDS block) or has no header at all.
|
|
111
|
+
*
|
|
112
|
+
* @param {string} content File contents, or just the block text.
|
|
113
|
+
* @returns {string|null}
|
|
114
|
+
*/
|
|
115
|
+
export function parseBlockVersion(content) {
|
|
116
|
+
const match = /Astryx v(\d+\.\d+\.\d+[^\s·]*)/.exec(content ?? '');
|
|
117
|
+
return match ? match[1] : null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Read-only staleness assessment of the managed agent-docs block(s) against the
|
|
122
|
+
* installed core version. This is the detection half of the `astryx upgrade`
|
|
123
|
+
* agent-docs refresh: it never writes, so `upgrade` can run it on EVERY path —
|
|
124
|
+
* including the up-to-date / no-codemods short-circuits — and decide whether to
|
|
125
|
+
* rewrite a stale block, nudge an uninitialized repo, or stay silent.
|
|
126
|
+
*
|
|
127
|
+
* A managed block is:
|
|
128
|
+
* - `stale` — it carries a legacy XDS marker, has no parseable version, or
|
|
129
|
+
* records a version other than the installed one.
|
|
130
|
+
* - `current` — every managed block already matches the installed version.
|
|
131
|
+
* And the project is `missing` when no managed block exists anywhere (the repo
|
|
132
|
+
* has agent-doc files without our markers, or none at all — i.e. never `init`ed).
|
|
133
|
+
*
|
|
134
|
+
* @param {string} targetDir
|
|
135
|
+
* @param {string} [installedVersion] Defaults to the installed core version.
|
|
136
|
+
* @returns {{
|
|
137
|
+
* installedVersion: string,
|
|
138
|
+
* status: 'missing' | 'stale' | 'current',
|
|
139
|
+
* files: Array<{path: string, blockVersion: string|null, legacy: boolean, stale: boolean}>,
|
|
140
|
+
* staleFiles: string[],
|
|
141
|
+
* blockVersions: string[],
|
|
142
|
+
* }}
|
|
143
|
+
*/
|
|
144
|
+
export function inspectAgentDocs(targetDir, installedVersion) {
|
|
145
|
+
const version = installedVersion ?? getXdsVersion(findCoreDir(targetDir));
|
|
146
|
+
const files = [];
|
|
147
|
+
|
|
148
|
+
for (const rel of discoverAgentDocs(targetDir)) {
|
|
149
|
+
let content;
|
|
150
|
+
try {
|
|
151
|
+
content = fs.readFileSync(path.join(targetDir, rel), 'utf-8');
|
|
152
|
+
} catch {
|
|
153
|
+
continue; // Unreadable — treat as absent.
|
|
154
|
+
}
|
|
155
|
+
const hasNew = content.includes(MARKER_START);
|
|
156
|
+
const hasLegacy = content.includes(LEGACY_MARKER_START);
|
|
157
|
+
if (!hasNew && !hasLegacy) continue; // Not a block we manage.
|
|
158
|
+
|
|
159
|
+
const legacy = !hasNew && hasLegacy;
|
|
160
|
+
const blockVersion = parseBlockVersion(content);
|
|
161
|
+
const stale = legacy || blockVersion == null || blockVersion !== version;
|
|
162
|
+
files.push({path: rel, blockVersion, legacy, stale});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const staleEntries = files.filter(f => f.stale);
|
|
166
|
+
const staleFiles = staleEntries.map(f => f.path);
|
|
167
|
+
const blockVersions = [
|
|
168
|
+
...new Set(staleEntries.map(f => f.blockVersion).filter(Boolean)),
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
let status;
|
|
172
|
+
if (files.length === 0) status = 'missing';
|
|
173
|
+
else if (staleFiles.length > 0) status = 'stale';
|
|
174
|
+
else status = 'current';
|
|
175
|
+
|
|
176
|
+
return {installedVersion: version, status, files, staleFiles, blockVersions};
|
|
62
177
|
}
|
|
63
178
|
|
|
64
179
|
/**
|
|
@@ -144,8 +259,8 @@ export function detectStylingSystem(targetDir) {
|
|
|
144
259
|
* configured (see {@link detectStylingSystem}) so the agent never reaches for a
|
|
145
260
|
* styling path that isn't compiled here.
|
|
146
261
|
*/
|
|
147
|
-
export function generateCompressedIndex(version, {coreDir,
|
|
148
|
-
const run =
|
|
262
|
+
export function generateCompressedIndex(version, {coreDir, invocation = getCliInvocation(), stylingSystem = 'css'} = {}) {
|
|
263
|
+
const run = invocation;
|
|
149
264
|
const lines = [MARKER_START];
|
|
150
265
|
|
|
151
266
|
// Component count from live discovery
|
|
@@ -392,7 +507,7 @@ export function removeAgentDocs(targetDir) {
|
|
|
392
507
|
|
|
393
508
|
/**
|
|
394
509
|
* Programmatic entry point for installing agent docs.
|
|
395
|
-
* Used by the init
|
|
510
|
+
* Used by the init command, upgrade command, and agent-docs command.
|
|
396
511
|
*
|
|
397
512
|
* Strategy (when no agent/paths specified):
|
|
398
513
|
* - Discover all existing agent doc files and update them
|
|
@@ -410,9 +525,9 @@ export function removeAgentDocs(targetDir) {
|
|
|
410
525
|
export function installAgentDocs(targetDir, {zh = false, lang, agent, paths, onlyReplace = false} = {}) {
|
|
411
526
|
const coreDir = findCoreDir(targetDir);
|
|
412
527
|
const version = getXdsVersion(coreDir);
|
|
413
|
-
const
|
|
528
|
+
const invocation = getCliInvocation(targetDir);
|
|
414
529
|
const stylingSystem = detectStylingSystem(targetDir);
|
|
415
|
-
const compressedIndex = generateCompressedIndex(version, {coreDir, zh, lang,
|
|
530
|
+
const compressedIndex = generateCompressedIndex(version, {coreDir, zh, lang, invocation, stylingSystem});
|
|
416
531
|
const written = [];
|
|
417
532
|
|
|
418
533
|
// Explicit paths override everything
|
|
@@ -538,8 +653,7 @@ export function registerAgentDocs(program) {
|
|
|
538
653
|
throw err;
|
|
539
654
|
}
|
|
540
655
|
|
|
541
|
-
const
|
|
542
|
-
const run = `${runPrefix} astryx`;
|
|
656
|
+
const run = getCliInvocation(targetDir);
|
|
543
657
|
|
|
544
658
|
for (const t of targets) {
|
|
545
659
|
humanLog(`✓ ${t}`);
|
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
removeXdsBlock,
|
|
17
17
|
discoverAgentDocs,
|
|
18
18
|
resolveAgentPaths,
|
|
19
|
+
parseBlockVersion,
|
|
20
|
+
inspectAgentDocs,
|
|
19
21
|
} from './agent-docs.mjs';
|
|
20
22
|
|
|
21
23
|
let tmpDir;
|
|
@@ -88,17 +90,24 @@ describe('generateCompressedIndex', () => {
|
|
|
88
90
|
expect(result).toMatch(/after any @astryxdesign\/core bump/);
|
|
89
91
|
});
|
|
90
92
|
|
|
91
|
-
it('states the
|
|
92
|
-
const result = generateCompressedIndex('1.0.0', {
|
|
93
|
+
it('states the invocation once in the CLI header (yarn)', () => {
|
|
94
|
+
const result = generateCompressedIndex('1.0.0', {invocation: 'yarn astryx'});
|
|
93
95
|
expect(result).toContain('yarn astryx <cmd>');
|
|
94
96
|
expect(result).not.toContain('npx astryx');
|
|
95
97
|
});
|
|
96
98
|
|
|
97
|
-
it('uses pnpm exec
|
|
98
|
-
const result = generateCompressedIndex('1.0.0', {
|
|
99
|
+
it('uses the pnpm exec invocation', () => {
|
|
100
|
+
const result = generateCompressedIndex('1.0.0', {invocation: 'pnpm exec astryx'});
|
|
99
101
|
expect(result).toContain('pnpm exec astryx <cmd>');
|
|
100
102
|
expect(result).not.toContain('npx astryx');
|
|
101
103
|
});
|
|
104
|
+
|
|
105
|
+
it('uses the scoped package for one-off (uninstalled) runs so agents never hit the bare name', () => {
|
|
106
|
+
const result = generateCompressedIndex('1.0.0', {invocation: 'npx @astryxdesign/cli'});
|
|
107
|
+
expect(result).toContain('npx @astryxdesign/cli <cmd>');
|
|
108
|
+
// The header defines the mapping; the bare "run every command as `npx astryx`" footgun must be absent.
|
|
109
|
+
expect(result).not.toContain('npx astryx <cmd>');
|
|
110
|
+
});
|
|
102
111
|
});
|
|
103
112
|
|
|
104
113
|
describe('detectStylingSystem', () => {
|
|
@@ -602,3 +611,87 @@ describe('resolveAgentPaths', () => {
|
|
|
602
611
|
expect(result).toEqual({inject: [], create: ['.claude/CLAUDE.md']});
|
|
603
612
|
});
|
|
604
613
|
});
|
|
614
|
+
|
|
615
|
+
describe('parseBlockVersion', () => {
|
|
616
|
+
it('reads the version from the header of a generated block', () => {
|
|
617
|
+
expect(parseBlockVersion(generateCompressedIndex('1.2.3'))).toBe('1.2.3');
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
it('reads prerelease versions', () => {
|
|
621
|
+
expect(parseBlockVersion('Astryx v1.2.3-beta.4 · 10 components')).toBe(
|
|
622
|
+
'1.2.3-beta.4',
|
|
623
|
+
);
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
it('returns null when no versioned header is present', () => {
|
|
627
|
+
expect(parseBlockVersion('<!-- XDS:START -->\nold\n<!-- XDS:END -->')).toBeNull();
|
|
628
|
+
expect(parseBlockVersion('')).toBeNull();
|
|
629
|
+
expect(parseBlockVersion(undefined)).toBeNull();
|
|
630
|
+
});
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
describe('inspectAgentDocs', () => {
|
|
634
|
+
function writeBlock(rel, version) {
|
|
635
|
+
const filePath = path.join(tmpDir, rel);
|
|
636
|
+
fs.mkdirSync(path.dirname(filePath), {recursive: true});
|
|
637
|
+
fs.writeFileSync(filePath, `# Doc\n\n${generateCompressedIndex(version)}\n`);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
it('reports missing when no agent docs exist at all', () => {
|
|
641
|
+
const res = inspectAgentDocs(tmpDir, '1.0.0');
|
|
642
|
+
expect(res.status).toBe('missing');
|
|
643
|
+
expect(res.files).toEqual([]);
|
|
644
|
+
expect(res.staleFiles).toEqual([]);
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
it('reports missing when agent files exist but carry no Astryx marker', () => {
|
|
648
|
+
fs.writeFileSync(path.join(tmpDir, 'AGENTS.md'), '# Agents\n\nHand-written notes.\n');
|
|
649
|
+
expect(inspectAgentDocs(tmpDir, '1.0.0').status).toBe('missing');
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
it('reports current when the only block matches the installed version', () => {
|
|
653
|
+
writeBlock('AGENTS.md', '1.0.0');
|
|
654
|
+
const res = inspectAgentDocs(tmpDir, '1.0.0');
|
|
655
|
+
expect(res.status).toBe('current');
|
|
656
|
+
expect(res.staleFiles).toEqual([]);
|
|
657
|
+
expect(res.blockVersions).toEqual([]);
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
it('reports stale (with the old version) when the block is behind', () => {
|
|
661
|
+
writeBlock('AGENTS.md', '1.0.0');
|
|
662
|
+
const res = inspectAgentDocs(tmpDir, '2.0.0');
|
|
663
|
+
expect(res.status).toBe('stale');
|
|
664
|
+
expect(res.staleFiles).toEqual(['AGENTS.md']);
|
|
665
|
+
expect(res.blockVersions).toEqual(['1.0.0']);
|
|
666
|
+
expect(res.installedVersion).toBe('2.0.0');
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
it('treats a legacy XDS block as stale even with no parseable version', () => {
|
|
670
|
+
fs.writeFileSync(
|
|
671
|
+
path.join(tmpDir, 'CLAUDE.md'),
|
|
672
|
+
'# Claude\n\n<!-- XDS:START -->\nlegacy index\n<!-- XDS:END -->\n',
|
|
673
|
+
);
|
|
674
|
+
const res = inspectAgentDocs(tmpDir, '1.0.0');
|
|
675
|
+
expect(res.status).toBe('stale');
|
|
676
|
+
expect(res.files[0].legacy).toBe(true);
|
|
677
|
+
expect(res.blockVersions).toEqual([]);
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
it('is stale if ANY marked file is behind (mixed current + stale)', () => {
|
|
681
|
+
writeBlock('AGENTS.md', '2.0.0');
|
|
682
|
+
writeBlock('CLAUDE.md', '1.0.0');
|
|
683
|
+
const res = inspectAgentDocs(tmpDir, '2.0.0');
|
|
684
|
+
expect(res.status).toBe('stale');
|
|
685
|
+
expect(res.staleFiles).toEqual(['CLAUDE.md']);
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
it('defaults the installed version to the core package when omitted', () => {
|
|
689
|
+
const coreDir = path.join(tmpDir, 'node_modules', '@astryxdesign', 'core');
|
|
690
|
+
fs.mkdirSync(coreDir, {recursive: true});
|
|
691
|
+
fs.writeFileSync(path.join(coreDir, 'package.json'), JSON.stringify({version: '3.0.0'}));
|
|
692
|
+
writeBlock('AGENTS.md', '3.0.0');
|
|
693
|
+
const res = inspectAgentDocs(tmpDir);
|
|
694
|
+
expect(res.installedVersion).toBe('3.0.0');
|
|
695
|
+
expect(res.status).toBe('current');
|
|
696
|
+
});
|
|
697
|
+
});
|
|
@@ -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) {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Guardrail tests for the @astryxdesign/cli postinstall nudge (layer 2).
|
|
5
|
+
*
|
|
6
|
+
* Tests the pure decision matrix (shouldNudge) — nudges for a real dependency
|
|
7
|
+
* install when not set up; stays quiet in the monorepo/source, during npx's
|
|
8
|
+
* transient fetch (path _npx or npm_command=exec), and once set up.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {describe, it, expect} from 'vitest';
|
|
12
|
+
import {shouldNudge} from '../../scripts/postinstall.mjs';
|
|
13
|
+
|
|
14
|
+
const DEP = '/proj/node_modules/@astryxdesign/cli/scripts/postinstall.mjs'; // real dep install
|
|
15
|
+
const NPX = '/Users/x/.npm/_npx/a1b2/node_modules/@astryxdesign/cli/scripts/postinstall.mjs';
|
|
16
|
+
const REPO = '/repo/packages/cli/scripts/postinstall.mjs'; // monorepo/source
|
|
17
|
+
|
|
18
|
+
describe('cli postinstall — shouldNudge', () => {
|
|
19
|
+
it('nudges for a real dependency install when not set up', () => {
|
|
20
|
+
expect(shouldNudge({scriptPath: DEP, npmCommand: 'install', isSetUp: false})).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('quiet in the monorepo/source (not under node_modules)', () => {
|
|
24
|
+
expect(shouldNudge({scriptPath: REPO, npmCommand: 'install', isSetUp: false})).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('quiet during npx transient install (path contains _npx)', () => {
|
|
28
|
+
expect(shouldNudge({scriptPath: NPX, npmCommand: 'install', isSetUp: false})).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('quiet during npx (npm_command=exec) — avoids double-nudge before init', () => {
|
|
32
|
+
expect(shouldNudge({scriptPath: DEP, npmCommand: 'exec', isSetUp: false})).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('quiet once the project is already set up', () => {
|
|
36
|
+
expect(shouldNudge({scriptPath: DEP, npmCommand: 'install', isSetUp: true})).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('quiet with no script path (defensive)', () => {
|
|
40
|
+
expect(shouldNudge({})).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
formatBriefAll,
|
|
19
19
|
} from '../../lib/component-format.mjs';
|
|
20
20
|
import {resolveTheme} from '../../lib/resolve-theme.mjs';
|
|
21
|
-
import {
|
|
21
|
+
import {getCliInvocation} from '../../utils/package-manager.mjs';
|
|
22
22
|
import {jsonOut, humanLog} from '../../lib/json.mjs';
|
|
23
23
|
import {cliError} from '../../lib/cli-error.mjs';
|
|
24
24
|
import {ERROR_CODES} from '../../lib/error-codes.mjs';
|
|
@@ -39,7 +39,7 @@ export function registerComponent(program) {
|
|
|
39
39
|
.option('--blocks', 'List example blocks: showcase, examples, and related')
|
|
40
40
|
.option('--package <name>', 'Scope lookup to an external package (e.g. @acme/xds-widgets)')
|
|
41
41
|
.action(async (name, options) => {
|
|
42
|
-
const run =
|
|
42
|
+
const run = getCliInvocation();
|
|
43
43
|
const zh = program.opts().zh || false;
|
|
44
44
|
const dense = program.opts().dense || false;
|
|
45
45
|
const lang = program.opts().lang || null;
|
|
@@ -142,7 +142,7 @@ export function registerComponent(program) {
|
|
|
142
142
|
}
|
|
143
143
|
humanLog('');
|
|
144
144
|
humanLog(`Import from the path shown (e.g. import {Button} from '@astryxdesign/core/Button')`);
|
|
145
|
-
humanLog(`Usage: ${run}
|
|
145
|
+
humanLog(`Usage: ${run} component <name>`);
|
|
146
146
|
humanLog('');
|
|
147
147
|
}
|
|
148
148
|
break;
|
|
@@ -165,7 +165,7 @@ export function registerComponent(program) {
|
|
|
165
165
|
humanLog('');
|
|
166
166
|
}
|
|
167
167
|
humanLog(`Import from the path shown (e.g. import {Button} from '@astryxdesign/core/Button')`);
|
|
168
|
-
humanLog(`Usage: ${run}
|
|
168
|
+
humanLog(`Usage: ${run} component <name>`);
|
|
169
169
|
humanLog('');
|
|
170
170
|
break;
|
|
171
171
|
}
|
|
@@ -14,6 +14,7 @@ import {formatFull, formatBrief, formatCompact} from '../lib/component-format.mj
|
|
|
14
14
|
import {jsonOut, humanLog} from '../lib/json.mjs';
|
|
15
15
|
import {cliError} from '../lib/cli-error.mjs';
|
|
16
16
|
import {discover as discoverApi} from '../api/discover.mjs';
|
|
17
|
+
import {getCliInvocation} from '../utils/package-manager.mjs';
|
|
17
18
|
|
|
18
19
|
export function registerDiscover(program) {
|
|
19
20
|
program
|
|
@@ -25,6 +26,7 @@ export function registerDiscover(program) {
|
|
|
25
26
|
const json = program.opts().json || false;
|
|
26
27
|
const lang = program.opts().lang || null;
|
|
27
28
|
const zh = program.opts().zh || false;
|
|
29
|
+
const run = getCliInvocation();
|
|
28
30
|
|
|
29
31
|
let result;
|
|
30
32
|
try {
|
|
@@ -80,9 +82,9 @@ export function registerDiscover(program) {
|
|
|
80
82
|
humanLog('');
|
|
81
83
|
}
|
|
82
84
|
humanLog('Usage:');
|
|
83
|
-
humanLog(
|
|
84
|
-
humanLog(
|
|
85
|
-
humanLog(
|
|
85
|
+
humanLog(` ${run} discover <package> Browse a package`);
|
|
86
|
+
humanLog(` ${run} discover <package>/Component View component docs`);
|
|
87
|
+
humanLog(` ${run} discover <search> Search all packages`);
|
|
86
88
|
humanLog('');
|
|
87
89
|
}
|
|
88
90
|
break;
|
|
@@ -99,7 +101,7 @@ export function registerDiscover(program) {
|
|
|
99
101
|
humanLog('');
|
|
100
102
|
for (const comp of result.data.components) humanLog(' ' + comp);
|
|
101
103
|
humanLog('');
|
|
102
|
-
humanLog(
|
|
104
|
+
humanLog(`Usage: ${run} discover ` + result.data.name + '/<ComponentName>');
|
|
103
105
|
humanLog('');
|
|
104
106
|
break;
|
|
105
107
|
}
|
|
@@ -122,7 +124,7 @@ export function registerDiscover(program) {
|
|
|
122
124
|
humanLog('Found ' + result.data.matches.length + ' matches for "' + result.data.query + '":');
|
|
123
125
|
humanLog('');
|
|
124
126
|
for (const m of result.data.matches) {
|
|
125
|
-
humanLog(
|
|
127
|
+
humanLog(` ${run} discover ` + m.package + '/' + m.component);
|
|
126
128
|
}
|
|
127
129
|
humanLog('');
|
|
128
130
|
break;
|