Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/cli-build-kit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@astryxdesign/cli': patch
---

[feat] Improve `astryx build` output into a complete composition kit.

`build "<idea>"` now returns an agent-ready kit grouped by role: a START line
(scaffold vs compose), the closest PAGE template, an always-on FRAME (page
shell) and FOUNDATION (layout/typography/action primitives), idea-specific
BLOCKS and DOMAIN COMPONENTS (with a relevance floor to cut noise), and a SETUP
reminder. The always-on FRAME/FOUNDATION groups fix low recall of the
structural primitives every page needs but that never keyword-match an idea
(measured: component recall 15% to 71% on an agent-grounded eval).
@joeyfarina
81 changes: 52 additions & 29 deletions packages/cli/src/commands/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,21 @@ import {search as searchApi} from '../api/search.mjs';
const PAGE_DIRECT = 95;
/** Below this a page is too weak to even offer as a layout reference. */
const PAGE_FLOOR = 50;
/** Below this a block/domain-component match is incidental noise, not surfaced. */
const DOMAIN_FLOOR = 55;

/**
* Always-surfaced primitives. Every page needs a shell + layout/typography/
* action atoms, but these never keyword-match an idea ("dashboard" != "Stack"),
* so search alone never returns them. We list them unconditionally so an agent
* composing from scratch has the whole kit (esp. off-template).
*/
const FRAME = ['AppShell', 'TopNav', 'SideNav', 'Layout'];
const FOUNDATION = [
'VStack', 'HStack', 'Grid', 'StackItem', 'Card', 'Section',
'Text', 'Heading', 'Button', 'Icon', 'Badge', 'Divider',
];
const ALWAYS = new Set([...FRAME, ...FOUNDATION]);

/** Print the build playbook (shown when `build` is run with no query). */
function printPlaybook(run) {
Expand Down Expand Up @@ -105,14 +120,17 @@ export function registerBuild(program) {
return;
}

// Group by role so an agent can assemble a UI from the pieces.
// ── Group results by role (the build kit) ──────────────────────
const pages = results
.filter(r => r.domain === 'template' && r.kind !== 'block' && r.score >= PAGE_FLOOR)
.slice(0, 3);
const blocks = results.filter(r => r.domain === 'template' && r.kind === 'block').slice(0, 5);
const atoms = results
.filter(r => r.domain === 'component' || r.domain === 'hook')
const blocks = results
.filter(r => r.domain === 'template' && r.kind === 'block' && r.score >= DOMAIN_FLOOR)
.slice(0, 5);
// Idea-specific atoms = matched components/hooks MINUS the always-on kit.
const domain = results
.filter(r => (r.domain === 'component' || r.domain === 'hook') && r.score >= DOMAIN_FLOOR && !ALWAYS.has(r.name))
.slice(0, 6);
const directMatch = pages.length > 0 && pages[0].score >= PAGE_DIRECT;

const printItem = (r, label) => {
Expand All @@ -130,44 +148,49 @@ export function registerBuild(program) {
humanLog('');
humanLog(`Building "${q}":`);

let frame = null;
// START β€” the single recommended path.
humanLog('');
if (directMatch) {
humanLog(`START β†’ Scaffold the \`${pages[0].name}\` page template, then adapt: ${run} astryx template ${pages[0].name} ./src/App.tsx`);
} else if (pages.length) {
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.`);
} else {
humanLog(`START β†’ No page template fits. Frame with AppShell and compose the blocks + components below.`);
}

// PAGE
if (pages.length) {
humanLog('');
humanLog(
directMatch
? 'PAGE TEMPLATE β€” looks like a direct match (scaffold this):'
: 'CLOSEST PAGE TEMPLATES β€” scaffold if one fits, or use as a layout reference:',
);
humanLog(directMatch ? 'PAGE TEMPLATE β€” direct match:' : 'CLOSEST PAGE TEMPLATES β€” layout reference:');
pages.forEach(p => printItem(p, directMatch ? 'page' : 'closest'));
humanLog(` (study structure: ${run} astryx template ${pages[0].name} --skeleton)`);
frame = pages[0];
} else {
humanLog('');
humanLog('NO MATCHING PAGE TEMPLATE β€” compose from the blocks + components below:');
}

// FRAME β€” always (the page shell).
humanLog('');
humanLog(`FRAME β€” page shell (always): ${FRAME.join(', ')}`);
humanLog(` full-page β†’ AppShell; or Layout + SideNav/TopNav. ${run} astryx component AppShell`);

// BLOCKS β€” idea-specific composed patterns.
if (blocks.length) {
humanLog('');
humanLog('BLOCKS β€” drop-in patterns that likely cover parts of it:');
humanLog('BLOCKS β€” drop-in patterns that cover parts of it:');
blocks.forEach(b => printItem(b, 'block'));
}

if (atoms.length) {
// DOMAIN COMPONENTS β€” idea-specific atoms.
if (domain.length) {
humanLog('');
humanLog('COMPONENTS β€” building blocks to fill the gaps:');
atoms.forEach(c => printItem(c, c.domain === 'hook' ? 'hook' : 'component'));
humanLog('DOMAIN COMPONENTS β€” specific to this idea:');
domain.forEach(c => printItem(c, c.domain === 'hook' ? 'hook' : 'component'));
}

const parts = [];
if (frame) {
parts.push(directMatch ? `scaffold \`${frame.name}\`` : `frame with \`${frame.name}\` (--skeleton)`);
}
if (blocks.length) parts.push(`drop in ${blocks.slice(0, 2).map(b => '`' + b.name + '`').join(', ')}`);
if (atoms.length) parts.push(`fill with ${atoms.slice(0, 2).map(c => '`' + c.name + '`').join(', ')}`);
if (parts.length) {
humanLog('');
humanLog('Compose: ' + parts.join(' β†’ '));
}
// FOUNDATION β€” always (layout/typography/actions).
humanLog('');
humanLog(`FOUNDATION β€” always available (layout/text/actions): ${FOUNDATION.join(' ')}`);

// SETUP β€” so it renders / stays on-system.
humanLog('');
humanLog('SETUP β€” import "@astryxdesign/core/reset.css" + "astryx.css". No <div>/style for layout β€” use Stack/Grid + tokens.');
humanLog('');
});
}
Loading