@astryxdesign/cli 0.1.2-canary.ac17c7f → 0.1.2-canary.b6820d0

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 (97) hide show
  1. package/README.md +7 -1
  2. package/package.json +7 -19
  3. package/src/api/component.mjs +12 -253
  4. package/src/api/discover.mjs +7 -14
  5. package/src/api/doctor.mjs +22 -7
  6. package/src/api/layout.mjs +10 -11
  7. package/src/api/layout.test.mjs +1 -4
  8. package/src/api/template.mjs +84 -242
  9. package/src/codemods/__tests__/registry.test.mjs +0 -1
  10. package/src/codemods/registry.mjs +0 -1
  11. package/src/codemods/runner.mjs +96 -73
  12. package/src/codemods/transforms/{v0.1.0 → v0.0.15}/drop-xds-prefix-imports.mjs +4 -15
  13. package/src/codemods/transforms/v0.0.15/index.mjs +13 -0
  14. package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-config-surfaces.test.mjs +116 -0
  15. package/src/codemods/transforms/v0.1.0/index.mjs +6 -15
  16. package/src/codemods/transforms/v0.1.0/migrate-xds-config-surfaces.mjs +230 -0
  17. package/src/commands/agent-docs.mjs +1 -1
  18. package/src/commands/component/index.mjs +10 -44
  19. package/src/commands/discover.mjs +16 -5
  20. package/src/commands/doctor.test.mjs +3 -3
  21. package/src/commands/gap-report.mjs +464 -0
  22. package/src/commands/gap-report.test.mjs +168 -0
  23. package/src/commands/json-contract.test.mjs +32 -0
  24. package/src/commands/swizzle-gap-safety.test.mjs +273 -0
  25. package/src/commands/swizzle.mjs +235 -224
  26. package/src/commands/swizzle.path-safety.test.mjs +3 -3
  27. package/src/commands/template.mjs +37 -33
  28. package/src/commands/upgrade.mjs +170 -349
  29. package/src/config.mjs +20 -7
  30. package/src/config.test.mjs +13 -89
  31. package/src/index.mjs +3 -7
  32. package/src/lib/component-discovery.mjs +0 -177
  33. package/src/lib/config-schema.mjs +55 -41
  34. package/src/lib/config.mjs +113 -0
  35. package/src/lib/config.test.mjs +91 -0
  36. package/src/lib/error-codes.mjs +12 -7
  37. package/src/lib/integrations.mjs +115 -92
  38. package/src/lib/integrations.test.mjs +107 -88
  39. package/src/lib/manifest.mjs +2 -5
  40. package/src/types/api.d.ts +6 -2
  41. package/src/types/base.d.ts +9 -3
  42. package/src/types/component.d.ts +2 -27
  43. package/src/types/config.d.ts +85 -56
  44. package/src/types/error-codes.d.ts +3 -5
  45. package/src/types/gap-report.d.ts +29 -0
  46. package/src/types/index.d.ts +1 -0
  47. package/src/types/swizzle.d.ts +2 -9
  48. package/src/types/template.d.ts +7 -10
  49. package/src/types/upgrade.d.ts +0 -29
  50. package/src/utils/github.mjs +237 -0
  51. package/src/utils/interactive.mjs +2 -2
  52. package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.doc.mjs +1 -1
  53. package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.tsx +4 -9
  54. package/src/api/template-integration.test.mjs +0 -225
  55. package/src/api/validate-integration.mjs +0 -370
  56. package/src/api/validate-integration.test.mjs +0 -222
  57. package/src/codemod.mjs +0 -93
  58. package/src/codemod.test.mjs +0 -134
  59. package/src/codemods/__tests__/runner.test.mjs +0 -103
  60. package/src/codemods/integration-discovery.mjs +0 -168
  61. package/src/codemods/integration-discovery.test.mjs +0 -234
  62. package/src/codemods/integration-runner.mjs +0 -109
  63. package/src/codemods/run-codemod.mjs +0 -207
  64. package/src/codemods/transforms/v0.1.0/__tests__/v0.1.0-ordering.test.mjs +0 -81
  65. package/src/codemods/transforms/v0.1.3/__tests__/migrate-layout-components-to-experimental.test.mjs +0 -360
  66. package/src/codemods/transforms/v0.1.3/index.mjs +0 -19
  67. package/src/codemods/transforms/v0.1.3/migrate-layout-components-to-experimental.mjs +0 -260
  68. package/src/commands/component-ownership.test.mjs +0 -227
  69. package/src/commands/swizzle.routing.test.mjs +0 -279
  70. package/src/commands/upgrade.config-ordering.test.mjs +0 -273
  71. package/src/commands/upgrade.integration-policy.test.mjs +0 -204
  72. package/src/commands/validate-integration.mjs +0 -110
  73. package/src/commands/validate-integration.test.mjs +0 -124
  74. package/src/integration.mjs +0 -19
  75. package/src/lib/config-cache.mjs +0 -91
  76. package/src/lib/config-cache.test.mjs +0 -83
  77. package/src/lib/integration-warnings.mjs +0 -62
  78. package/src/lib/integration-warnings.test.mjs +0 -102
  79. package/src/lib/module-loader.mjs +0 -80
  80. package/src/lib/module-loader.test.mjs +0 -106
  81. package/src/lib/project.mjs +0 -502
  82. package/src/lib/project.test.mjs +0 -308
  83. package/src/template.mjs +0 -73
  84. package/src/template.test.mjs +0 -127
  85. package/src/types/codemod.d.ts +0 -81
  86. package/src/types/integration.d.ts +0 -29
  87. package/src/types/template-api.d.ts +0 -54
  88. package/src/types/validate-integration.d.ts +0 -24
  89. package/templates/blocks/components/DateRangeInput/DateRangeInputWithPresets.doc.mjs +0 -14
  90. package/templates/blocks/components/DateRangeInput/DateRangeInputWithPresets.tsx +0 -46
  91. package/templates/blocks/components/DateRangeInput/DateRangeInputWithValidation.doc.mjs +0 -14
  92. package/templates/blocks/components/DateRangeInput/DateRangeInputWithValidation.tsx +0 -52
  93. package/templates/blocks/components/DateTimeInput/DateTimeInputWithValidation.doc.mjs +0 -14
  94. package/templates/blocks/components/DateTimeInput/DateTimeInputWithValidation.tsx +0 -43
  95. package/templates/blocks/components/Tab/TabWithSelectedIcon.doc.mjs +0 -13
  96. package/templates/blocks/components/Tab/TabWithSelectedIcon.tsx +0 -39
  97. /package/src/codemods/transforms/{v0.1.0 → v0.0.15}/__tests__/drop-xds-prefix-imports.test.mjs +0 -0
package/README.md CHANGED
@@ -62,6 +62,7 @@ Options:
62
62
  | `upgrade` | Run codemods to migrate between versions |
63
63
  | `theme build` | Compile a defineTheme file to production CSS and JS |
64
64
  | `discover` | Discover external packages and components |
65
+ | `gap-report` | Report a gap when a component doesn't meet your needs |
65
66
  | `doctor` | Diagnose your XDS setup and report problems with fixes (CI-friendly via exit code) |
66
67
 
67
68
  ### Global options
@@ -160,6 +161,7 @@ if (isError(result)) {
160
161
  | `ERR_INVALID_VERSION` | A `--from`/`--to` value was not a valid semver string. |
161
162
  | `ERR_DEP_MISSING` | A required external dependency (e.g. jscodeshift) is missing. |
162
163
  | `ERR_GH_CLI` | GitHub CLI (`gh`) is not installed or not authenticated. |
164
+ | `ERR_GAP_REPORT_FAILED` | Filing a gap report failed (disabled, or the integration errored). |
163
165
 
164
166
  ## Capability manifest (agent discovery)
165
167
 
@@ -356,6 +358,8 @@ Every response has a `type` string that uniquely identifies it:
356
358
  | `xds --json theme build <file>` | `theme.build` | `ThemeBuildResponse` |
357
359
  | `xds --json upgrade --list` | `upgrade.list` | `UpgradeListResponse` |
358
360
  | `xds --json upgrade [--apply]` | `upgrade.run` | `UpgradeRunResponse` |
361
+ | `xds --json gap-report --list-categories` | `gap-report.categories` | `GapReportCategoriesResponse` |
362
+ | `xds --json gap-report --component X ...` | `gap-report.file` | `GapReportFileResponse` |
359
363
  | `xds --json doctor` | `doctor` | `DoctorResponse` |
360
364
  | any error | — | `CLIError` |
361
365
  | unsupported command | — | `CLIUnsupportedError` |
@@ -430,6 +434,8 @@ export default {
430
434
  templates: {
431
435
  get: async id => fetchTemplateFromAPI(id),
432
436
  },
433
- issuesUrl: 'https://github.com/your-org/your-repo/issues',
437
+ gapReport: {
438
+ url: 'https://your-api.com/gaps',
439
+ },
434
440
  };
435
441
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/cli",
3
- "version": "0.1.2-canary.ac17c7f",
3
+ "version": "0.1.2-canary.b6820d0",
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",
@@ -43,18 +43,6 @@
43
43
  "types": "./src/types/config.d.ts",
44
44
  "import": "./src/config.mjs"
45
45
  },
46
- "./integration": {
47
- "types": "./src/types/integration.d.ts",
48
- "import": "./src/integration.mjs"
49
- },
50
- "./template": {
51
- "types": "./src/types/template-api.d.ts",
52
- "import": "./src/template.mjs"
53
- },
54
- "./codemod": {
55
- "types": "./src/types/codemod.d.ts",
56
- "import": "./src/codemod.mjs"
57
- },
58
46
  "./xle": {
59
47
  "types": "./src/lib/xle/browser.d.ts",
60
48
  "import": "./src/lib/xle/browser.mjs"
@@ -75,9 +63,9 @@
75
63
  "zod": "^4.4.3"
76
64
  },
77
65
  "peerDependencies": {
78
- "@astryxdesign/core": "0.1.2-canary.ac17c7f",
79
- "@astryxdesign/lab": "0.1.2-canary.ac17c7f",
80
- "@astryxdesign/theme-neutral": "0.1.2-canary.ac17c7f",
66
+ "@astryxdesign/core": "0.1.2-canary.b6820d0",
67
+ "@astryxdesign/lab": "0.1.2-canary.b6820d0",
68
+ "@astryxdesign/theme-neutral": "0.1.2-canary.b6820d0",
81
69
  "gpt-tokenizer": "^2.0.0"
82
70
  },
83
71
  "peerDependenciesMeta": {
@@ -92,9 +80,9 @@
92
80
  }
93
81
  },
94
82
  "devDependencies": {
95
- "@astryxdesign/core": "0.1.2-canary.ac17c7f",
96
- "@astryxdesign/lab": "0.1.2-canary.ac17c7f",
97
- "@astryxdesign/theme-neutral": "0.1.2-canary.ac17c7f",
83
+ "@astryxdesign/core": "0.1.2-canary.b6820d0",
84
+ "@astryxdesign/lab": "0.1.2-canary.b6820d0",
85
+ "@astryxdesign/theme-neutral": "0.1.2-canary.b6820d0",
98
86
  "gpt-tokenizer": "^2.0.0"
99
87
  },
100
88
  "scripts": {
@@ -11,48 +11,18 @@ import * as fs from 'node:fs';
11
11
  import {ERROR_CODES} from '../lib/error-codes.mjs';
12
12
  import {findCoreDir, discoverExternalPackages} from '../utils/paths.mjs';
13
13
  import {
14
- CORE_PACKAGE,
15
14
  discoverComponents,
16
15
  discoverExternalComponentsGrouped,
17
- discoverIntegrationComponents,
18
16
  findComponentReadme,
19
17
  findComponentSource,
20
18
  findExternalComponentDoc,
21
- findIntegrationComponentDoc,
22
- findIntegrationComponentSource,
23
19
  resolveImportPath,
24
20
  } from '../lib/component-discovery.mjs';
25
- import {Project} from '../lib/project.mjs';
26
21
  import {loadDocs} from '../lib/component-loader.mjs';
27
22
  import {searchComponents} from '../lib/string-utils.mjs';
28
23
  import {AstryxError} from './error.mjs';
29
24
  import {findShowcase, findRelatedBlocks} from './template.mjs';
30
25
 
31
- /**
32
- * Load the configured integrations for `cwd`, swallowing any config errors so
33
- * component discovery never hard-fails on a malformed/absent integration. An
34
- * empty list means "core only".
35
- * @param {string} cwd
36
- * @returns {Promise<Array<{name: string, components?: string, issuesUrl?: string}>>}
37
- */
38
- async function loadIntegrationsSafely(cwd) {
39
- try {
40
- const project = await Project.load(cwd);
41
- return project.loadedIntegrations;
42
- } catch {
43
- return [];
44
- }
45
- }
46
-
47
- /**
48
- * Resolve a loaded integration by package name.
49
- * @param {Array<{name: string}>} loadedIntegrations
50
- * @param {string} packageName
51
- */
52
- function findLoadedIntegration(loadedIntegrations, packageName) {
53
- return loadedIntegrations.find(i => i.name === packageName) ?? null;
54
- }
55
-
56
26
  /**
57
27
  * Resolve an external package by name from the discovered externals list.
58
28
  * @param {string} packageName - e.g. '@acme/xds-widgets'
@@ -161,13 +131,8 @@ export async function component(name, options = {}) {
161
131
  return {type: 'component.full', data: {[match[0]]: entries}};
162
132
  }
163
133
 
164
- // Default: brief — package-qualified object list for the category.
165
- // Pre-1.0 JSON contract: members are {name, package} objects, not bare
166
- // strings, so consumers can disambiguate ownership.
167
- return {
168
- type: 'component.list',
169
- data: {[match[0]]: match[1].map(n => ({name: n, package: CORE_PACKAGE}))},
170
- };
134
+ // Default: brief — names only
135
+ return {type: 'component.list', data: {[match[0]]: match[1]}};
171
136
  }
172
137
 
173
138
  // All components — merge core + external packages with grouped subcategories
@@ -214,189 +179,43 @@ export async function component(name, options = {}) {
214
179
  return {type: 'component.full', data: result};
215
180
  }
216
181
 
217
- // Default: brief — package-qualified object list (core + integrations).
218
- // Pre-1.0 JSON contract: each group's members are {name, package} objects.
219
- /** @type {Record<string, Array<{name: string, package: string}>>} */
220
- const listData = {};
221
- for (const [cat, comps] of Object.entries(components)) {
222
- listData[cat] = comps.map(n => ({name: n, package: CORE_PACKAGE}));
223
- }
224
-
225
- // Integration components (authoritative source: loadedIntegrations).
226
- const loadedIntegrations = await loadIntegrationsSafely(cwd);
227
- const seenIntegration = new Set();
228
- for (const integration of loadedIntegrations) {
229
- seenIntegration.add(integration.name);
230
- const owned = discoverIntegrationComponents(integration);
231
- // Group integration components by their doc `group`, falling back to the
232
- // package name. Keys are package-qualified so they never collide with
233
- // core groups or each other.
234
- /** @type {Map<string, Array<{name: string, package: string}>>} */
235
- const byGroup = new Map();
236
- for (const rec of owned) {
237
- const groupLabel = rec.group ?? integration.name;
238
- const key = `${groupLabel} (${integration.name})`;
239
- if (!byGroup.has(key)) byGroup.set(key, []);
240
- byGroup.get(key).push({name: rec.name, package: integration.name});
241
- }
242
- for (const [key, members] of byGroup) {
243
- members.sort((a, b) => a.name.localeCompare(b.name));
244
- listData[key] = members;
245
- }
246
- }
247
-
248
- // Back-compat: node_modules-scanned external packages (pkg.astryx.docs)
249
- // that are NOT configured integrations. Preserves existing discovery for
250
- // consumers that haven't adopted the config-integration flow.
182
+ // Default: brief — names only (with externals merged in)
251
183
  const externals = discoverExternalPackages(cwd);
252
184
  for (const ext of externals) {
253
- if (seenIntegration.has(ext.name)) continue;
254
185
  const grouped = discoverExternalComponentsGrouped(ext.docsDir);
255
186
  const groupKeys = Object.keys(grouped);
256
187
  if (groupKeys.length === 0) continue;
257
188
 
189
+ // If the package has subcategories (groups), emit each as a separate key.
190
+ // If no groups exist, fall back to the flat list under one key.
258
191
  const hasGroups = groupKeys.some(
259
192
  k => grouped[k].length > 1 || grouped[k][0] !== k,
260
193
  );
261
194
 
262
195
  if (hasGroups) {
263
196
  for (const [group, members] of Object.entries(grouped)) {
264
- listData[`${group} (${ext.name})`] = members.map(n => ({
265
- name: n,
266
- package: ext.name,
267
- }));
197
+ components[`${group} (${ext.name})`] = members;
268
198
  }
269
199
  } else {
200
+ // All ungrouped — single flat list under the package category
270
201
  const allComps = Object.values(grouped).flat().sort();
271
202
  if (allComps.length > 0) {
272
- listData[`${ext.category} (${ext.name})`] = allComps.map(n => ({
273
- name: n,
274
- package: ext.name,
275
- }));
203
+ components[`${ext.category} (${ext.name})`] = allComps;
276
204
  }
277
205
  }
278
206
  }
279
- return {type: 'component.list', data: listData};
207
+ return {type: 'component.list', data: components};
280
208
  }
281
209
 
282
210
  // ── Single component ───────────────────────────────────────────
283
211
 
284
- if (typeof name !== 'string') {
285
- throw new AstryxError(
286
- `No component named "${String(name)}"`,
287
- undefined,
288
- ERROR_CODES.ERR_UNKNOWN_COMPONENT,
289
- );
290
- }
291
-
292
212
  const dirName = name.replace(/^XDS/, '');
293
213
 
294
- // Ownership-aware resolution. Build the set of OWNER packages that provide a
295
- // component with this name across core + every loaded integration. This is
296
- // what lets the CLI disambiguate by package and expose the owner's source +
297
- // issuesUrl (the inputs the future integration-component swizzle needs).
298
- const loadedIntegrations = await loadIntegrationsSafely(cwd);
299
- const coreDocPath = findComponentReadme(coreDir, dirName);
300
- /**
301
- * @type {Array<{
302
- * package: string,
303
- * docPath: string,
304
- * sourcePath: string|null,
305
- * issuesUrl: string|undefined,
306
- * integration: object|null,
307
- * }>}
308
- */
309
- const owners = [];
310
- if (coreDocPath) {
311
- owners.push({
312
- package: CORE_PACKAGE,
313
- docPath: coreDocPath,
314
- sourcePath: findComponentSource(coreDir, dirName),
315
- issuesUrl: undefined,
316
- integration: null,
317
- });
318
- }
319
- for (const integration of loadedIntegrations) {
320
- const docPath = findIntegrationComponentDoc(integration, dirName);
321
- if (!docPath) continue;
322
- owners.push({
323
- package: integration.name,
324
- docPath,
325
- sourcePath: findIntegrationComponentSource(integration, dirName),
326
- issuesUrl: integration.issuesUrl,
327
- integration,
328
- });
329
- }
330
-
331
- /**
332
- * Augment a loaded `component.detail` doc with ownership metadata. Adds
333
- * `package`, the resolved `import` specifier, and `sourceAvailable` (whether
334
- * a swizzleable source file exists for the owner). Existing doc fields
335
- * (name, usage, props, …) are preserved.
336
- * @param {object} docs
337
- * @param {{package: string, sourcePath: string|null}} owner
338
- * @param {string} componentName
339
- */
340
- function withOwnership(docs, owner, componentName) {
341
- const importSpec =
342
- owner.package === CORE_PACKAGE
343
- ? resolveImportPath(coreDir, componentName)
344
- : `${owner.package}/${componentName}`;
345
- return {
346
- ...docs,
347
- package: owner.package,
348
- import: importSpec,
349
- sourceAvailable: owner.sourcePath != null,
350
- };
351
- }
352
-
353
214
  // When scoped to a specific package, search that package first.
354
215
  // This is critical for components that exist in both core and an external
355
216
  // package (e.g. AppShell, Button, SideNav) — the package scope ensures
356
217
  // the external package's docs are returned, not core's.
357
218
  if (packageScope) {
358
- // Core scope: resolve from core directly.
359
- if (packageScope === CORE_PACKAGE) {
360
- const owner = owners.find(o => o.package === CORE_PACKAGE);
361
- if (!owner) {
362
- throw new AstryxError(`No component "${name}" in package "${packageScope}"`, undefined, ERROR_CODES.ERR_UNKNOWN_COMPONENT);
363
- }
364
- if (source) {
365
- if (!owner.sourcePath) {
366
- throw new AstryxError(`Source for "${name}" not found`, undefined, ERROR_CODES.ERR_NO_SOURCE);
367
- }
368
- return {type: 'component.detail.source', data: {component: dirName, source: fs.readFileSync(owner.sourcePath, 'utf-8')}};
369
- }
370
- const docs = await loadDocs(owner.docPath, {zh, dense, lang});
371
- if (props) {
372
- const p = docs.props || (docs.components ? docs.components.flatMap(c => c.props || []) : []);
373
- return {type: 'component.detail.props', data: p};
374
- }
375
- return {type: 'component.detail', data: withOwnership(docs, owner, dirName)};
376
- }
377
-
378
- // Integration scope (authoritative): resolve from the loaded integration.
379
- const integration = findLoadedIntegration(loadedIntegrations, packageScope);
380
- if (integration) {
381
- const owner = owners.find(o => o.package === packageScope);
382
- if (!owner) {
383
- throw new AstryxError(`No component "${name}" in package "${packageScope}"`, undefined, ERROR_CODES.ERR_UNKNOWN_COMPONENT);
384
- }
385
- if (source) {
386
- if (!owner.sourcePath) {
387
- throw new AstryxError(`Source for "${name}" not found in package "${packageScope}"`, undefined, ERROR_CODES.ERR_NO_SOURCE);
388
- }
389
- return {type: 'component.detail.source', data: {component: dirName, source: fs.readFileSync(owner.sourcePath, 'utf-8')}};
390
- }
391
- const docs = await loadDocs(owner.docPath, {zh, dense, lang});
392
- if (props) {
393
- const p = docs.props || (docs.components ? docs.components.flatMap(c => c.props || []) : []);
394
- return {type: 'component.detail.props', data: p};
395
- }
396
- return {type: 'component.detail', data: withOwnership(docs, owner, dirName)};
397
- }
398
-
399
- // Legacy fallback: node_modules `pkg.astryx.docs` external package.
400
219
  const ext = resolveExternalPackage(packageScope, cwd);
401
220
  if (!ext) {
402
221
  throw new AstryxError(`External package "${packageScope}" not found`, undefined, ERROR_CODES.ERR_UNKNOWN_PACKAGE);
@@ -425,49 +244,11 @@ export async function component(name, options = {}) {
425
244
  const p = docs.props || (docs.components ? docs.components.flatMap(c => c.props || []) : []);
426
245
  return {type: 'component.detail.props', data: p};
427
246
  }
428
- return {
429
- type: 'component.detail',
430
- data: withOwnership(docs, {package: ext.name, sourcePath: null}, dirName),
431
- };
247
+ return {type: 'component.detail', data: docs};
432
248
  }
433
249
  throw new AstryxError(`No component "${name}" in package "${packageScope}"`, undefined, ERROR_CODES.ERR_UNKNOWN_COMPONENT);
434
250
  }
435
251
 
436
- // Ambiguity: when the name is owned by MORE THAN ONE package (core and/or
437
- // integrations) and the caller did not scope with --package, refuse to guess.
438
- // NOTE: legacy `pkg.astryx.docs` externals are intentionally NOT part of this
439
- // ambiguity set — they retain their historical core-first fallback below so
440
- // existing consumers (and tests) keep working. Only config-driven integration
441
- // ownership participates here.
442
- if (owners.length > 1) {
443
- throw new AstryxError(
444
- `Component "${dirName}" is provided by multiple packages. Re-run with --package <pkg> to choose one.`,
445
- owners.map(o => ({name: o.package, reason: 'provides this component'})),
446
- ERROR_CODES.ERR_UNKNOWN_COMPONENT,
447
- );
448
- }
449
-
450
- // Single non-core owner (an integration provides it, core does not) — resolve
451
- // from that integration so the integration component is authoritative.
452
- if (owners.length === 1 && owners[0].package !== CORE_PACKAGE) {
453
- const owner = owners[0];
454
- if (source) {
455
- if (!owner.sourcePath) {
456
- throw new AstryxError(`Source for "${name}" not found`, undefined, ERROR_CODES.ERR_NO_SOURCE);
457
- }
458
- return {type: 'component.detail.source', data: {component: dirName, source: fs.readFileSync(owner.sourcePath, 'utf-8')}};
459
- }
460
- if (showcase) {
461
- throw new AstryxError(`No showcase found for "${name}"`, undefined, ERROR_CODES.ERR_NO_SHOWCASE);
462
- }
463
- const docs = await loadDocs(owner.docPath, {zh, dense, lang});
464
- if (props) {
465
- const p = docs.props || (docs.components ? docs.components.flatMap(c => c.props || []) : []);
466
- return {type: 'component.detail.props', data: p};
467
- }
468
- return {type: 'component.detail', data: withOwnership(docs, owner, dirName)};
469
- }
470
-
471
252
  if (source) {
472
253
  const sourcePath = findComponentSource(coreDir, dirName);
473
254
  if (!sourcePath) {
@@ -493,10 +274,6 @@ export async function component(name, options = {}) {
493
274
 
494
275
  let readmePath = findComponentReadme(coreDir, dirName);
495
276
  let resolvedName = dirName;
496
- // Track the resolving owner so the detail payload can carry ownership info.
497
- // Defaults to core; the legacy-external fallback below may reassign it.
498
- let resolvedOwnerPackage = CORE_PACKAGE;
499
- let resolvedSourcePath = readmePath ? findComponentSource(coreDir, dirName) : null;
500
277
 
501
278
  if (!readmePath) {
502
279
  const externals = discoverExternalPackages(cwd);
@@ -504,8 +281,6 @@ export async function component(name, options = {}) {
504
281
  const extDocPath = findExternalComponentDoc(ext.docsDir, dirName);
505
282
  if (extDocPath) {
506
283
  readmePath = extDocPath;
507
- resolvedOwnerPackage = ext.name;
508
- resolvedSourcePath = null;
509
284
  break;
510
285
  }
511
286
  }
@@ -524,8 +299,6 @@ export async function component(name, options = {}) {
524
299
  if (topScore >= 90 && topTied.length === 1 && gap >= 20) {
525
300
  resolvedName = topTied[0].name;
526
301
  readmePath = findComponentReadme(coreDir, resolvedName);
527
- resolvedOwnerPackage = CORE_PACKAGE;
528
- resolvedSourcePath = findComponentSource(coreDir, resolvedName);
529
302
  } else {
530
303
  const threshold = Math.max(topScore - 20, 1);
531
304
  const candidates = results.filter(r => r.score >= threshold).slice(0, 5);
@@ -609,14 +382,7 @@ export async function component(name, options = {}) {
609
382
  if (props) {
610
383
  return {type: 'component.detail.props', data: matchingComponent.props || []};
611
384
  }
612
- return {
613
- type: 'component.detail',
614
- data: withOwnership(
615
- scoped,
616
- {package: resolvedOwnerPackage, sourcePath: resolvedSourcePath},
617
- dirName,
618
- ),
619
- };
385
+ return {type: 'component.detail', data: scoped};
620
386
  }
621
387
 
622
388
  if (props) {
@@ -624,12 +390,5 @@ export async function component(name, options = {}) {
624
390
  return {type: 'component.detail.props', data: p};
625
391
  }
626
392
 
627
- return {
628
- type: 'component.detail',
629
- data: withOwnership(
630
- docs,
631
- {package: resolvedOwnerPackage, sourcePath: resolvedSourcePath},
632
- resolvedName,
633
- ),
634
- };
393
+ return {type: 'component.detail', data: docs};
635
394
  }
@@ -4,7 +4,7 @@
4
4
  * @file Programmatic API for the discover command.
5
5
  */
6
6
 
7
- import {Project} from '../lib/project.mjs';
7
+ import {loadConfig} from '../lib/config.mjs';
8
8
  import {
9
9
  scanAllPackages,
10
10
  findComponentInPackages,
@@ -40,7 +40,7 @@ function validateDocs(docs) {
40
40
  */
41
41
  export async function discover(query, options = {}) {
42
42
  const {lang = null, zh = false} = options;
43
- const project = await Project.load();
43
+ const config = await loadConfig();
44
44
  const toEntry = pkg => ({
45
45
  name: pkg.name,
46
46
  category: pkg.category,
@@ -50,21 +50,14 @@ export async function discover(query, options = {}) {
50
50
  displayName: pkg.displayName,
51
51
  });
52
52
 
53
- // External packages come from configured integrations that declare a
54
- // components root. Each becomes a scannable package keyed by its docsDir.
55
- const explicitPackages = project.loadedIntegrations
56
- .filter(integration => integration.components)
57
- .map(integration => ({
58
- name: integration.name,
59
- version: integration.version,
60
- category: integration.name,
61
- docsDir: integration.components,
62
- }));
63
- if (explicitPackages.length === 0) {
53
+ const explicitPackages = (config.loadedIntegrations ?? [])
54
+ .map(integration => integration.package)
55
+ .filter(Boolean);
56
+ if (config.packages.length === 0 && explicitPackages.length === 0) {
64
57
  return {type: 'discover.list', data: [], meta: {configured: false}};
65
58
  }
66
59
 
67
- const packages = scanAllPackages([], explicitPackages);
60
+ const packages = scanAllPackages(config.packages, explicitPackages);
68
61
 
69
62
  if (packages.length === 0) {
70
63
  return {type: 'discover.list', data: [], meta: {configured: true}};
@@ -27,7 +27,7 @@ import {createRequire} from 'node:module';
27
27
  import {MIN_NODE_VERSION, isNodeVersionSupported} from '../lib/node-version.mjs';
28
28
  import {CLI_ROOT, findCoreDir} from '../utils/paths.mjs';
29
29
  import {detectPackageManager} from '../utils/package-manager.mjs';
30
- import {findConfigPath, Project} from '../lib/project.mjs';
30
+ import {findConfigPath, loadConfig} from '../lib/config.mjs';
31
31
  import {semverCompare} from '../utils/semver.mjs';
32
32
 
33
33
  const _require = createRequire(import.meta.url);
@@ -279,9 +279,8 @@ export async function checkConfig(ctx) {
279
279
  };
280
280
  }
281
281
 
282
- // Project.load swallows nothing it surfaces a genuine load failure — but
283
- // the config check wants to report a bad default export precisely, so we
284
- // re-import directly to surface a genuine load failure as a FAIL.
282
+ // loadConfig swallows errors and returns defaults, so re-import directly to
283
+ // surface a genuine load failure as a FAIL.
285
284
  try {
286
285
  const {pathToFileURL} = await import('node:url');
287
286
  const mod = await import(pathToFileURL(ctx.configPath).href);
@@ -292,9 +291,25 @@ export async function checkConfig(ctx) {
292
291
  label: 'astryx.config.mjs',
293
292
  status: 'fail',
294
293
  message: `astryx.config.mjs default export is not an object (got ${typeof config}).`,
295
- fix: 'Export a default object from astryx.config.mjs, e.g. `export default { integrations: [] };`.',
294
+ fix: 'Export a default object from astryx.config.mjs, e.g. `export default { theme: "default" };`.',
296
295
  };
297
296
  }
297
+ // Validate that `packages`, if present, is a string or array of strings.
298
+ if (config?.packages !== undefined) {
299
+ const arr = Array.isArray(config.packages)
300
+ ? config.packages
301
+ : [config.packages];
302
+ const bad = arr.some(p => typeof p !== 'string');
303
+ if (bad) {
304
+ return {
305
+ id: 'config',
306
+ label: 'astryx.config.mjs',
307
+ status: 'fail',
308
+ message: 'astryx.config.mjs `packages` must be a string or array of strings.',
309
+ fix: 'Set `packages` to a path string or array of path strings.',
310
+ };
311
+ }
312
+ }
298
313
  return {
299
314
  id: 'config',
300
315
  label: 'astryx.config.mjs',
@@ -478,8 +493,8 @@ export async function runChecks(options = {}) {
478
493
  // Resolve a possible theme key from config (best-effort; never throws).
479
494
  let configTheme = null;
480
495
  try {
481
- const project = await Project.load(cwd);
482
- configTheme = project.config?.theme ?? null;
496
+ const loaded = await loadConfig(cwd);
497
+ configTheme = loaded?.theme ?? null;
483
498
  } catch {
484
499
  // Best-effort: a missing/invalid config leaves configTheme null.
485
500
  }
@@ -25,14 +25,13 @@ import {expand} from '../lib/xle/expand.mjs';
25
25
  import {toCompact, toOutline} from '../lib/xle/print.mjs';
26
26
  import {buildRegistry, ALIAS_TABLE} from '../lib/xle/registry.mjs';
27
27
  import {discoverTemplates, stripTemplateAssetRefs} from './template.mjs';
28
- import {Project} from '../lib/project.mjs';
28
+ import {loadConfig} from '../lib/config.mjs';
29
29
 
30
30
  /**
31
31
  * The catalog a `{hint}` can resolve to: template blocks (spliced inline) plus
32
- * any app-registered local components from astryx.config.mjs
33
- * `experimental.xle.components` (imported by name). App components are how XLE
34
- * reaches domain pieces — the KpiCard/chart/drawer set that the
35
- * @astryxdesign/core registry can't see.
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.
36
35
  */
37
36
  async function loadBlocks(cwd) {
38
37
  const blocks = [];
@@ -43,20 +42,20 @@ async function loadBlocks(cwd) {
43
42
  // discovery is best-effort
44
43
  }
45
44
  try {
46
- const project = await Project.load(cwd);
47
- const components = project.config.experimental?.xle?.components ?? {};
45
+ const config = await loadConfig(cwd);
46
+ const components = config.layout?.components || {};
48
47
  for (const [name, spec] of Object.entries(components)) {
49
- const importPath = spec.from;
48
+ const importPath = typeof spec === 'string' ? spec : spec.from;
50
49
  if (!importPath) continue;
51
50
  blocks.push({
52
51
  type: 'block',
53
52
  kind: 'component',
54
53
  dirName: name,
55
54
  name,
56
- description: spec.description ?? '',
55
+ description: typeof spec === 'object' ? spec.description || '' : '',
57
56
  category: 'app',
58
57
  importPath,
59
- isDefault: Boolean(spec.default),
58
+ isDefault: typeof spec === 'object' ? Boolean(spec.default) : false,
60
59
  });
61
60
  }
62
61
  } catch {
@@ -282,7 +281,7 @@ TEMPLATE REFERENCING ({hint} pulls in real content — this is how XLE reaches
282
281
  {kpi-card} standalone reference (no wrapper element) — place a component directly
283
282
  {kpi-card}*4 repeat a reference; the definition/import is emitted once
284
283
  app components register local ones in astryx.config.mjs to import them by name:
285
- export default {experimental: {xle: {components: {KpiCard: {from: '@/components/KpiCard'}}}}}
284
+ export default {layout: {components: {KpiCard: '@/components/KpiCard'}}}
286
285
  then {kpi-card} → import {KpiCard} + <KpiCard /> (kebab ↔ Pascal)
287
286
 
288
287
  STRUCTURE THE EXPANDER HANDLES
@@ -196,14 +196,11 @@ describe('template referencing', () => {
196
196
 
197
197
  it('imports app-registered local components (the local-component bridge)', async () => {
198
198
  // Inside the workspace so @astryxdesign/core resolves; cleaned up after.
199
- // A package.json beside the config makes Project.load resolve it as the
200
- // sibling-of-nearest-package.json (the standard config resolution).
201
199
  const cwd = mkdtempSync(join(process.cwd(), '.xle-imp-test-'));
202
200
  try {
203
- writeFileSync(join(cwd, 'package.json'), '{"name": "xle-imp-fixture"}\n');
204
201
  writeFileSync(
205
202
  join(cwd, 'astryx.config.mjs'),
206
- `export default {experimental: {xle: {components: {KpiCard: {from: '@/components/KpiCard'}, TimeRangePicker: {from: '@/components/TimeRangePicker'}}}}};\n`,
203
+ `export default {layout: {components: {KpiCard: '@/components/KpiCard', TimeRangePicker: {from: '@/components/TimeRangePicker'}}}};\n`,
207
204
  );
208
205
  const result = await layoutExpand('S[p6] > (G[c4 g4] > {kpi-card}*4) + {time-range-picker}', {
209
206
  name: 'Demo',