@astryxdesign/cli 0.1.1-canary.a514b99 → 0.1.1-canary.b6827c4

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.
Files changed (45) hide show
  1. package/package.json +19 -8
  2. package/src/api/discover.mjs +78 -26
  3. package/src/api/layout.mjs +301 -0
  4. package/src/api/layout.test.mjs +238 -0
  5. package/src/api/template.mjs +191 -50
  6. package/src/api/template.test.mjs +2 -0
  7. package/src/commands/gap-report.mjs +17 -9
  8. package/src/commands/gap-report.test.mjs +21 -16
  9. package/src/commands/init.mjs +34 -8
  10. package/src/commands/init.next-steps.test.mjs +46 -0
  11. package/src/commands/layout.mjs +139 -0
  12. package/src/commands/swizzle.mjs +51 -23
  13. package/src/commands/upgrade.mjs +1 -70
  14. package/src/config.mjs +31 -0
  15. package/src/config.test.mjs +24 -0
  16. package/src/index.mjs +4 -0
  17. package/src/lib/config-schema.mjs +119 -0
  18. package/src/lib/config.mjs +34 -7
  19. package/src/lib/config.test.mjs +51 -2
  20. package/src/lib/error-codes.mjs +8 -0
  21. package/src/lib/integrations.mjs +155 -0
  22. package/src/lib/integrations.test.mjs +154 -0
  23. package/src/lib/levenshtein.mjs +29 -0
  24. package/src/lib/manifest.mjs +6 -0
  25. package/src/lib/package-scanner.mjs +31 -7
  26. package/src/lib/string-utils.mjs +5 -14
  27. package/src/lib/xle/browser.d.ts +91 -0
  28. package/src/lib/xle/browser.mjs +120 -0
  29. package/src/lib/xle/expand.mjs +622 -0
  30. package/src/lib/xle/parse.mjs +581 -0
  31. package/src/lib/xle/print.mjs +174 -0
  32. package/src/lib/xle/registry-core.mjs +170 -0
  33. package/src/lib/xle/registry.mjs +237 -0
  34. package/src/lib/xle/splice.mjs +137 -0
  35. package/src/lib/xle/validate.mjs +356 -0
  36. package/src/lib/xle/xle.test.mjs +333 -0
  37. package/src/types/config.d.ts +99 -0
  38. package/src/utils/github.mjs +12 -27
  39. package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyShowcase.doc.mjs +15 -0
  40. package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyShowcase.tsx +26 -0
  41. package/templates/blocks/components/DateInput/DateInputDateRange.doc.mjs +2 -2
  42. package/templates/blocks/components/Slider/SliderShowcase.tsx +10 -1
  43. package/templates/blocks/components/ToggleButton/ToggleButtonGroup.doc.mjs +1 -1
  44. package/templates/blocks/components/MoreMenu/MoreMenuInToolbar.doc.mjs +0 -14
  45. package/templates/blocks/components/MoreMenu/MoreMenuInToolbar.tsx +0 -57
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/cli",
3
- "version": "0.1.1-canary.a514b99",
3
+ "version": "0.1.1-canary.b6827c4",
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",
@@ -38,6 +38,14 @@
38
38
  "./api": {
39
39
  "types": "./src/types/api.d.ts",
40
40
  "import": "./src/api/index.mjs"
41
+ },
42
+ "./config": {
43
+ "types": "./src/types/config.d.ts",
44
+ "import": "./src/config.mjs"
45
+ },
46
+ "./xle": {
47
+ "types": "./src/lib/xle/browser.d.ts",
48
+ "import": "./src/lib/xle/browser.mjs"
41
49
  }
42
50
  },
43
51
  "files": [
@@ -51,12 +59,14 @@
51
59
  "@clack/prompts": "^1.5.1",
52
60
  "commander": "^12.1.0",
53
61
  "jiti": "^2.7.0",
54
- "jscodeshift": "^17.3.0"
62
+ "jscodeshift": "^17.3.0",
63
+ "zod": "^4.4.3"
55
64
  },
56
65
  "peerDependencies": {
57
- "@astryxdesign/core": "0.1.1-canary.a514b99",
58
- "@astryxdesign/lab": "0.1.1-canary.a514b99",
59
- "@astryxdesign/theme-neutral": "0.1.1-canary.a514b99"
66
+ "@astryxdesign/core": "0.1.1-canary.b6827c4",
67
+ "@astryxdesign/lab": "0.1.1-canary.b6827c4",
68
+ "@astryxdesign/theme-neutral": "0.1.1-canary.b6827c4",
69
+ "gpt-tokenizer": "^2.0.0"
60
70
  },
61
71
  "peerDependenciesMeta": {
62
72
  "@astryxdesign/core": {
@@ -70,9 +80,10 @@
70
80
  }
71
81
  },
72
82
  "devDependencies": {
73
- "@astryxdesign/core": "0.1.1-canary.a514b99",
74
- "@astryxdesign/lab": "0.1.1-canary.a514b99",
75
- "@astryxdesign/theme-neutral": "0.1.1-canary.a514b99"
83
+ "@astryxdesign/core": "0.1.1-canary.b6827c4",
84
+ "@astryxdesign/lab": "0.1.1-canary.b6827c4",
85
+ "@astryxdesign/theme-neutral": "0.1.1-canary.b6827c4",
86
+ "gpt-tokenizer": "^2.0.0"
76
87
  },
77
88
  "scripts": {
78
89
  "astryx": "node bin/astryx.mjs",
@@ -5,19 +5,28 @@
5
5
  */
6
6
 
7
7
  import {loadConfig} from '../lib/config.mjs';
8
- import {scanAllPackages, findComponentInPackages} from '../lib/package-scanner.mjs';
8
+ import {
9
+ scanAllPackages,
10
+ findComponentInPackages,
11
+ } from '../lib/package-scanner.mjs';
9
12
  import {loadDocs} from '../lib/component-loader.mjs';
10
13
  import {levenshteinDistance} from '../lib/string-utils.mjs';
11
14
  import {AstryxError} from './error.mjs';
12
15
  import {ERROR_CODES} from '../lib/error-codes.mjs';
13
16
 
14
17
  function validateDocs(docs) {
15
- if (!docs || typeof docs !== 'object') return 'docs export is missing or not an object';
16
- if (typeof docs.name !== 'string' || !docs.name) return 'docs.name is missing or not a string';
17
- if (!docs.usage || typeof docs.usage.description !== 'string') return 'docs.usage.description is missing or not a string';
18
- if (docs.props && !Array.isArray(docs.props)) return 'docs.props must be an array';
19
- if (docs.components && !Array.isArray(docs.components)) return 'docs.components must be an array';
20
- if (docs.usage?.bestPractices && !Array.isArray(docs.usage.bestPractices)) return 'docs.usage.bestPractices must be an array';
18
+ if (!docs || typeof docs !== 'object')
19
+ return 'docs export is missing or not an object';
20
+ if (typeof docs.name !== 'string' || !docs.name)
21
+ return 'docs.name is missing or not a string';
22
+ if (!docs.usage || typeof docs.usage.description !== 'string')
23
+ return 'docs.usage.description is missing or not a string';
24
+ if (docs.props && !Array.isArray(docs.props))
25
+ return 'docs.props must be an array';
26
+ if (docs.components && !Array.isArray(docs.components))
27
+ return 'docs.components must be an array';
28
+ if (docs.usage?.bestPractices && !Array.isArray(docs.usage.bestPractices))
29
+ return 'docs.usage.bestPractices must be an array';
21
30
  return null;
22
31
  }
23
32
 
@@ -32,7 +41,7 @@ function validateDocs(docs) {
32
41
  export async function discover(query, options = {}) {
33
42
  const {lang = null, zh = false} = options;
34
43
  const config = await loadConfig();
35
- const toEntry = (pkg) => ({
44
+ const toEntry = pkg => ({
36
45
  name: pkg.name,
37
46
  category: pkg.category,
38
47
  components: pkg.components,
@@ -41,11 +50,14 @@ export async function discover(query, options = {}) {
41
50
  displayName: pkg.displayName,
42
51
  });
43
52
 
44
- if (config.packages.length === 0) {
53
+ const explicitPackages = (config.loadedIntegrations ?? [])
54
+ .map(integration => integration.package)
55
+ .filter(Boolean);
56
+ if (config.packages.length === 0 && explicitPackages.length === 0) {
45
57
  return {type: 'discover.list', data: [], meta: {configured: false}};
46
58
  }
47
59
 
48
- const packages = scanAllPackages(config.packages);
60
+ const packages = scanAllPackages(config.packages, explicitPackages);
49
61
 
50
62
  if (packages.length === 0) {
51
63
  return {type: 'discover.list', data: [], meta: {configured: true}};
@@ -61,7 +73,10 @@ export async function discover(query, options = {}) {
61
73
  if (slashIdx > 0) {
62
74
  const pkgName = query.slice(0, slashIdx);
63
75
  const compName = query.slice(slashIdx + 1);
64
- return await resolveComponentDocs(packages, compName, pkgName, {lang, zh});
76
+ return await resolveComponentDocs(packages, compName, pkgName, {
77
+ lang,
78
+ zh,
79
+ });
65
80
  }
66
81
 
67
82
  const pkg = packages.find(p => p.name === query);
@@ -99,7 +114,16 @@ export async function discover(query, options = {}) {
99
114
  }
100
115
 
101
116
  if (substringMatches.length > 1) {
102
- return {type: 'discover.search', data: {query, matches: substringMatches.map(m => ({package: m.pkg.name, component: m.comp}))}};
117
+ return {
118
+ type: 'discover.search',
119
+ data: {
120
+ query,
121
+ matches: substringMatches.map(m => ({
122
+ package: m.pkg.name,
123
+ component: m.comp,
124
+ })),
125
+ },
126
+ };
103
127
  }
104
128
 
105
129
  // Fuzzy fallback
@@ -110,7 +134,10 @@ export async function discover(query, options = {}) {
110
134
  }
111
135
  }
112
136
  const fuzzyMatches = allComponents
113
- .map(item => ({...item, distance: levenshteinDistance(lower, item.comp.toLowerCase())}))
137
+ .map(item => ({
138
+ ...item,
139
+ distance: levenshteinDistance(lower, item.comp.toLowerCase()),
140
+ }))
114
141
  .filter(m => m.distance <= 3)
115
142
  .sort((a, b) => a.distance - b.distance)
116
143
  .slice(0, 5);
@@ -118,30 +145,46 @@ export async function discover(query, options = {}) {
118
145
  if (fuzzyMatches.length > 0) {
119
146
  throw new AstryxError(
120
147
  `"${query}" not found`,
121
- fuzzyMatches.map(m => ({name: m.pkg.name + '/' + m.comp, reason: 'similar name'})),
148
+ fuzzyMatches.map(m => ({
149
+ name: m.pkg.name + '/' + m.comp,
150
+ reason: 'similar name',
151
+ })),
122
152
  ERROR_CODES.ERR_NOT_FOUND,
123
153
  );
124
154
  }
125
155
 
126
- throw new AstryxError(`"${query}" not found in any package`, undefined, ERROR_CODES.ERR_NOT_FOUND);
156
+ throw new AstryxError(
157
+ `"${query}" not found in any package`,
158
+ undefined,
159
+ ERROR_CODES.ERR_NOT_FOUND,
160
+ );
127
161
  }
128
162
 
129
163
  async function resolveComponentDocs(packages, compName, pkgName, {lang, zh}) {
130
164
  const pkg = packages.find(p => p.name === pkgName);
131
- if (!pkg) throw new AstryxError(`Package "${pkgName}" not found`, undefined, ERROR_CODES.ERR_UNKNOWN_PACKAGE);
165
+ if (!pkg)
166
+ throw new AstryxError(
167
+ `Package "${pkgName}" not found`,
168
+ undefined,
169
+ ERROR_CODES.ERR_UNKNOWN_PACKAGE,
170
+ );
132
171
 
133
172
  const result = findComponentInPackages([pkg], compName);
134
173
  if (!result) {
135
174
  const lower = compName.toLowerCase();
136
175
  const hits = pkg.components.filter(c => c.toLowerCase().includes(lower));
137
- const suggestions = hits.length > 0
138
- ? hits
139
- : pkg.components
140
- .map(c => ({name: c, distance: levenshteinDistance(lower, c.toLowerCase())}))
141
- .filter(m => m.distance <= 3)
142
- .sort((a, b) => a.distance - b.distance)
143
- .slice(0, 5)
144
- .map(m => m.name);
176
+ const suggestions =
177
+ hits.length > 0
178
+ ? hits
179
+ : pkg.components
180
+ .map(c => ({
181
+ name: c,
182
+ distance: levenshteinDistance(lower, c.toLowerCase()),
183
+ }))
184
+ .filter(m => m.distance <= 3)
185
+ .sort((a, b) => a.distance - b.distance)
186
+ .slice(0, 5)
187
+ .map(m => m.name);
145
188
  throw new AstryxError(
146
189
  `Component "${compName}" not found in ${pkgName}`,
147
190
  suggestions.map(s => ({name: s, reason: 'similar name'})),
@@ -157,9 +200,18 @@ async function loadAndValidate(result, {lang, zh}) {
157
200
  try {
158
201
  docs = await loadDocs(result.docPath, {zh, lang});
159
202
  } catch (e) {
160
- throw new AstryxError(`Failed to load docs for ${result.componentName}: ${e.message}`, undefined, ERROR_CODES.ERR_INVALID_DOC);
203
+ throw new AstryxError(
204
+ `Failed to load docs for ${result.componentName}: ${e.message}`,
205
+ undefined,
206
+ ERROR_CODES.ERR_INVALID_DOC,
207
+ );
161
208
  }
162
209
  const err = validateDocs(docs);
163
- if (err) throw new AstryxError(`Invalid docs for ${result.componentName}: ${err}`, undefined, ERROR_CODES.ERR_INVALID_DOC);
210
+ if (err)
211
+ throw new AstryxError(
212
+ `Invalid docs for ${result.componentName}: ${err}`,
213
+ undefined,
214
+ ERROR_CODES.ERR_INVALID_DOC,
215
+ );
164
216
  return {type: 'discover.detail.doc', data: docs};
165
217
  }
@@ -0,0 +1,301 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Programmatic API for the layout command (XLE/XLO).
5
+ *
6
+ * `astryx layout` turns token-compressed layout expressions into validated
7
+ * XDS TSX. Two input surfaces — compact (Emmet-derived XLE) and outline
8
+ * (indentation-based XLO) — share one AST, one validator, and one
9
+ * expander. See the research: pastes P2376666892 (spec) and P2376717669
10
+ * (outline surface).
11
+ *
12
+ * @input expression string (+ options)
13
+ * @output {type, data} envelopes: layout.expand / layout.check / layout.grammar
14
+ * @position api — pure orchestration over lib/xle; command wrapper in commands/layout.mjs
15
+ */
16
+
17
+ import * as fs from 'node:fs';
18
+ import * as path from 'node:path';
19
+ import {AstryxError} from './error.mjs';
20
+ import {ERROR_CODES} from '../lib/error-codes.mjs';
21
+ import {assertWithin, isFilePathArg, PathSafetyError} from '../utils/path-safety.mjs';
22
+ import {parse, detectForm, XLEParseError} from '../lib/xle/parse.mjs';
23
+ import {validate} from '../lib/xle/validate.mjs';
24
+ import {expand} from '../lib/xle/expand.mjs';
25
+ import {toCompact, toOutline} from '../lib/xle/print.mjs';
26
+ import {buildRegistry, ALIAS_TABLE} from '../lib/xle/registry.mjs';
27
+ import {discoverTemplates, stripTemplateAssetRefs} from './template.mjs';
28
+ import {loadConfig} from '../lib/config.mjs';
29
+
30
+ /**
31
+ * The catalog a `{hint}` can resolve to: template blocks (spliced inline) plus
32
+ * any app-registered local components from astryx.config.mjs `layout.components`
33
+ * (imported by name). App components are how XLE reaches domain pieces — the
34
+ * KpiCard/chart/drawer set that the @astryxdesign/core registry can't see.
35
+ */
36
+ async function loadBlocks(cwd) {
37
+ const blocks = [];
38
+ try {
39
+ const all = await discoverTemplates(cwd);
40
+ for (const t of all) if (t.type === 'block') blocks.push({...t, kind: 'template'});
41
+ } catch {
42
+ // discovery is best-effort
43
+ }
44
+ try {
45
+ const config = await loadConfig(cwd);
46
+ const components = config.layout?.components || {};
47
+ for (const [name, spec] of Object.entries(components)) {
48
+ const importPath = typeof spec === 'string' ? spec : spec.from;
49
+ if (!importPath) continue;
50
+ blocks.push({
51
+ type: 'block',
52
+ kind: 'component',
53
+ dirName: name,
54
+ name,
55
+ description: typeof spec === 'object' ? spec.description || '' : '',
56
+ category: 'app',
57
+ importPath,
58
+ isDefault: typeof spec === 'object' ? Boolean(spec.default) : false,
59
+ });
60
+ }
61
+ } catch {
62
+ // config is optional
63
+ }
64
+ return blocks;
65
+ }
66
+
67
+ const normKey = (name) => name.toLowerCase().replace(/[^a-z0-9]/g, '');
68
+
69
+ /** Collect the block names referenced by {hints} anywhere in the doc. */
70
+ function collectHintNames(doc) {
71
+ const names = new Set();
72
+ const visitNode = (node) => {
73
+ if (!node || node.kind === 'group') {
74
+ (node?.children || []).forEach(visit);
75
+ return;
76
+ }
77
+ if (node.hint?.block) names.add(node.hint.block.name);
78
+ for (const slot of node.slots || []) {
79
+ if (slot.value?.hint?.block) names.add(slot.value.hint.block.name);
80
+ (slot.value?.subexpr || []).forEach(visit);
81
+ }
82
+ (node.children || []).forEach(visit);
83
+ };
84
+ const visit = (item) => (item?.kind === 'group' ? item.children.forEach(visit) : visitNode(item));
85
+ doc.roots.forEach(visit);
86
+ doc.overlays.forEach(visit);
87
+ return names;
88
+ }
89
+
90
+ /**
91
+ * Build the blockModules map expand() needs: import-mode for app components,
92
+ * splice-mode (reading + asset-stripping the block source) for template blocks.
93
+ * Only blocks actually referenced are read.
94
+ */
95
+ function buildBlockModules(doc, blocks) {
96
+ const referenced = collectHintNames(doc);
97
+ if (referenced.size === 0) return new Map();
98
+ const byKey = new Map(blocks.map(b => [normKey(b.dirName), b]));
99
+ const modules = new Map();
100
+ for (const name of referenced) {
101
+ const block = byKey.get(normKey(name));
102
+ if (!block) continue;
103
+ if (block.kind === 'component') {
104
+ modules.set(name, {mode: 'import', componentName: block.name, importPath: block.importPath, isDefault: block.isDefault});
105
+ } else if (block.filePath && fs.existsSync(block.filePath)) {
106
+ modules.set(name, {mode: 'splice', componentName: block.dirName, source: stripTemplateAssetRefs(fs.readFileSync(block.filePath, 'utf-8'))});
107
+ }
108
+ }
109
+ return modules;
110
+ }
111
+
112
+ function formatIssue(issue) {
113
+ const where = issue.line != null ? `line ${issue.line}: ` : '';
114
+ return `${where}${issue.message}`;
115
+ }
116
+
117
+ /**
118
+ * Parse + validate, throwing structured XDSErrors on failure.
119
+ * Returns {doc, registry, blocks, warnings}.
120
+ */
121
+ async function analyze(expression, {form = 'auto', loose = false, cwd = process.cwd()} = {}) {
122
+ const registry = await buildRegistry({cwd});
123
+ const blocks = await loadBlocks(cwd);
124
+
125
+ let doc;
126
+ try {
127
+ doc = parse(expression, {form});
128
+ } catch (e) {
129
+ if (e instanceof XLEParseError) {
130
+ throw new AstryxError(
131
+ `Layout expression syntax error at line ${e.line}, col ${e.col}: ${e.message}`,
132
+ undefined,
133
+ ERROR_CODES.ERR_LAYOUT_PARSE,
134
+ );
135
+ }
136
+ throw e;
137
+ }
138
+
139
+ const {errors, warnings} = validate(doc, registry, blocks, {loose});
140
+ return {doc, registry, blocks, errors, warnings};
141
+ }
142
+
143
+ /**
144
+ * `astryx layout expand "<expr>" [path]`
145
+ *
146
+ * @param {string} expression
147
+ * @param {object} [options]
148
+ * @param {string} [options.targetPath] - write TSX here (validated against cwd)
149
+ * @param {'compact'|'outline'|'auto'} [options.form]
150
+ * @param {boolean} [options.loose] - downgrade unknown {hints} to TODO warnings
151
+ * @param {string} [options.name] - generated component name
152
+ * @param {string} [options.cwd]
153
+ */
154
+ export async function layoutExpand(expression, options = {}) {
155
+ const {targetPath, form = 'auto', loose = false, name, cwd = process.cwd()} = options;
156
+ const {doc, registry, blocks, errors, warnings} = await analyze(expression, {form, loose, cwd});
157
+
158
+ if (errors.length > 0) {
159
+ throw new AstryxError(
160
+ `Layout expression is invalid:\n` + errors.map(e => ` - ${formatIssue(e)}`).join('\n'),
161
+ errors.flatMap(e => (e.suggestions || []).map(s => ({name: s, reason: 'did you mean this?'}))),
162
+ ERROR_CODES.ERR_LAYOUT_INVALID,
163
+ );
164
+ }
165
+
166
+ const componentName = name || 'GeneratedLayout';
167
+ if (!/^[A-Z][A-Za-z0-9]*$/.test(componentName)) {
168
+ throw new AstryxError(
169
+ `--name must be a PascalCase component name, got '${componentName}'`,
170
+ undefined,
171
+ ERROR_CODES.ERR_INVALID_ARGUMENT,
172
+ );
173
+ }
174
+ const blockModules = buildBlockModules(doc, blocks);
175
+ const result = expand(doc, registry, {componentName, blockModules});
176
+
177
+ let written = null;
178
+ if (targetPath) {
179
+ let resolved;
180
+ try {
181
+ resolved = assertWithin(targetPath, cwd, {label: 'layout target path'});
182
+ } catch (err) {
183
+ if (err instanceof PathSafetyError) {
184
+ throw new AstryxError(err.message, undefined, ERROR_CODES.ERR_PATH_TRAVERSAL);
185
+ }
186
+ throw err;
187
+ }
188
+ const filePath = isFilePathArg(targetPath)
189
+ ? resolved
190
+ : path.join(resolved, `${componentName}.tsx`);
191
+ fs.mkdirSync(path.dirname(filePath), {recursive: true});
192
+ fs.writeFileSync(filePath, result.code);
193
+ written = path.relative(cwd, filePath);
194
+ }
195
+
196
+ return {
197
+ type: 'layout.expand',
198
+ data: {
199
+ form: form === 'auto' ? detectForm(expression) : form,
200
+ code: result.code,
201
+ componentsUsed: result.componentsUsed,
202
+ states: result.states,
203
+ todos: result.todos,
204
+ blocksReferenced: [...blockModules.entries()].map(([name, m]) => ({name, mode: m.mode})),
205
+ warnings: warnings.map(formatIssue),
206
+ written,
207
+ },
208
+ };
209
+ }
210
+
211
+ /**
212
+ * `astryx layout check "<expr>" [--form compact|outline]`
213
+ * Validates without expanding; echoes both canonical surfaces.
214
+ */
215
+ export async function layoutCheck(expression, options = {}) {
216
+ const {form = 'auto', loose = false, cwd = process.cwd()} = options;
217
+ const {doc, errors, warnings} = await analyze(expression, {form, loose, cwd});
218
+
219
+ return {
220
+ type: 'layout.check',
221
+ data: {
222
+ valid: errors.length === 0,
223
+ form: doc.form,
224
+ errors: errors.map(e => ({...e, formatted: formatIssue(e)})),
225
+ warnings: warnings.map(formatIssue),
226
+ compact: toCompact(doc),
227
+ outline: toOutline(doc),
228
+ },
229
+ };
230
+ }
231
+
232
+ /**
233
+ * `astryx layout grammar` — the agent cheatsheet, with the alias table
234
+ * generated from this branch's registry (never hand-maintained).
235
+ */
236
+ export async function layoutGrammar(options = {}) {
237
+ const {cwd = process.cwd()} = options;
238
+ const registry = await buildRegistry({cwd});
239
+
240
+ const aliasLines = [];
241
+ const byTarget = new Map();
242
+ for (const [alias, target] of registry.aliases) {
243
+ if (!byTarget.has(target)) byTarget.set(target, []);
244
+ byTarget.get(target).push(alias);
245
+ }
246
+ for (const [target, aliases] of [...byTarget.entries()].sort(([a], [b]) => a.localeCompare(b))) {
247
+ aliasLines.push(`${aliases.join('/')}=${target}`);
248
+ }
249
+
250
+ const text = `XLE/XLO — XDS layout expressions (branch-generated; aliases reflect this install)
251
+
252
+ WORKFLOW
253
+ astryx layout check "<expr>" validate; echoes canonical compact + outline forms
254
+ astryx layout expand "<expr>" [path] emit validated TSX (path optional; --name <Pascal>)
255
+ Errors carry line/col + suggestions. Fix and resubmit; nothing is guessed.
256
+
257
+ TWO SURFACES, ONE LANGUAGE (autodetected; --form to force)
258
+ compact: A[cp6 @topNav=TN] > L > LC > S[p6] > (C{card-callout}*4) + T
259
+ outline: indentation = nesting · same-indent = siblings · "repeat N:" block = (...)*N
260
+ slot lines: topNav: TN (or a block: topNav:\\n TN ...)
261
+
262
+ NODE ANATOMY Name#id.enum"payload"[attrs]{hint}*N > children
263
+ .enum unique enum value of any prop: Bd.success Tx.lg B.primary
264
+ "payload" primary text prop (label/title/heading) or text child: TI"Email" B"Save"
265
+ {hint} kebab-case template/component reference (see TEMPLATE REFERENCING) — NEVER text
266
+ *N / xN repeat (use $ for the counter: Tk"item-$"*3)
267
+ trailing ! initial selection for scaffolded state: Tab"Overview"!
268
+
269
+ ATTRS [...] (outline: bare tokens after the name, no brackets)
270
+ fused p6 g4 c4 w240 h2 cp2 mw960 rg2 cg2 (per-component: padding lives on Card/Section/AppShell.cp — p6 on AppShell/Layout/VStack errors with a correction)
271
+ key=value t=email href='/x' c{min:340} dv=[top,bottom] — keys validated per component
272
+ flags req opt dis striped hover divider … (isX/hasX props) · negate: !scroll
273
+ align j= main axis, a= cross axis — expander picks hAlign/vAlign per stack direction
274
+ slots @slotName=Node | @slotName=(sub > expr) | @slotName='text' | @slotName=#id
275
+ trigger opens=#id (a plain attr, no @ — binds an onClick that opens the overlay)
276
+ fill on a stack child → wraps in <StackItem size="fill">
277
+
278
+ TEMPLATE REFERENCING ({hint} pulls in real content — this is how XLE reaches past the @astryxdesign/core shell)
279
+ C{card-callout} splice a template block (astryx template --list --type block):
280
+ the block is co-defined once in the file, referenced, imports merged
281
+ {kpi-card} standalone reference (no wrapper element) — place a component directly
282
+ {kpi-card}*4 repeat a reference; the definition/import is emitted once
283
+ app components register local ones in astryx.config.mjs to import them by name:
284
+ export default {layout: {components: {KpiCard: '@/components/KpiCard'}}}
285
+ then {kpi-card} → import {KpiCard} + <KpiCard /> (kebab ↔ Pascal)
286
+
287
+ STRUCTURE THE EXPANDER HANDLES
288
+ Layout > LH + LC + LF + LP children auto-route into header/content/footer/start slots
289
+ T > (TR>THC*4) + (TR>TC*4)*6 rows partition into TableHeader/TableBody automatically
290
+ TabList/inputs required value+onChange scaffold typed useState automatically
291
+ overlays compact: tree ;; Dlg#confirm[...] · outline: overlays: section
292
+ trigger: B"Delete"[opens=#confirm]
293
+
294
+ ALIASES (full component names always valid; XDS prefix optional)
295
+ ${aliasLines.join(' ')}
296
+ `;
297
+
298
+ return {type: 'layout.grammar', data: {text, aliases: Object.fromEntries(registry.aliases)}};
299
+ }
300
+
301
+ export {ALIAS_TABLE};