@astryxdesign/cli 0.1.2-canary.ae2d146 → 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 (102) hide show
  1. package/CHANGELOG.md +1 -0
  2. package/README.md +55 -49
  3. package/package.json +7 -19
  4. package/src/api/component.mjs +12 -253
  5. package/src/api/discover.mjs +7 -14
  6. package/src/api/doctor.mjs +25 -10
  7. package/src/api/layout.mjs +10 -11
  8. package/src/api/layout.test.mjs +1 -4
  9. package/src/api/template.mjs +84 -242
  10. package/src/codemods/__tests__/registry.test.mjs +0 -1
  11. package/src/codemods/registry.mjs +0 -1
  12. package/src/codemods/runner.mjs +96 -73
  13. package/src/codemods/transforms/{v0.1.0 → v0.0.15}/drop-xds-prefix-imports.mjs +4 -15
  14. package/src/codemods/transforms/v0.0.15/index.mjs +13 -0
  15. package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-config-surfaces.test.mjs +116 -0
  16. package/src/codemods/transforms/v0.1.0/index.mjs +6 -43
  17. package/src/codemods/transforms/v0.1.0/migrate-xds-config-surfaces.mjs +230 -0
  18. package/src/commands/agent-docs.mjs +1 -1
  19. package/src/commands/component/index.mjs +10 -44
  20. package/src/commands/discover.mjs +16 -5
  21. package/src/commands/doctor.test.mjs +3 -3
  22. package/src/commands/gap-report.mjs +464 -0
  23. package/src/commands/gap-report.test.mjs +168 -0
  24. package/src/commands/json-contract.test.mjs +32 -0
  25. package/src/commands/swizzle-gap-safety.test.mjs +273 -0
  26. package/src/commands/swizzle.mjs +235 -224
  27. package/src/commands/swizzle.path-safety.test.mjs +3 -3
  28. package/src/commands/template.mjs +37 -33
  29. package/src/commands/upgrade.mjs +170 -349
  30. package/src/config.mjs +20 -7
  31. package/src/config.test.mjs +13 -89
  32. package/src/index.mjs +3 -7
  33. package/src/lib/component-discovery.mjs +0 -177
  34. package/src/lib/config-schema.mjs +55 -41
  35. package/src/lib/config.mjs +113 -0
  36. package/src/lib/config.test.mjs +91 -0
  37. package/src/lib/error-codes.mjs +12 -7
  38. package/src/lib/integrations.mjs +115 -92
  39. package/src/lib/integrations.test.mjs +107 -88
  40. package/src/lib/manifest.mjs +2 -5
  41. package/src/types/api.d.ts +6 -2
  42. package/src/types/base.d.ts +9 -3
  43. package/src/types/component.d.ts +2 -27
  44. package/src/types/config.d.ts +85 -56
  45. package/src/types/error-codes.d.ts +3 -5
  46. package/src/types/gap-report.d.ts +29 -0
  47. package/src/types/index.d.ts +1 -0
  48. package/src/types/swizzle.d.ts +2 -9
  49. package/src/types/template.d.ts +7 -10
  50. package/src/types/upgrade.d.ts +0 -29
  51. package/src/utils/github.mjs +237 -0
  52. package/src/utils/interactive.mjs +2 -2
  53. package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.doc.mjs +1 -1
  54. package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.tsx +4 -9
  55. package/src/api/template-integration.test.mjs +0 -225
  56. package/src/api/validate-integration.mjs +0 -370
  57. package/src/api/validate-integration.test.mjs +0 -222
  58. package/src/codemod.mjs +0 -93
  59. package/src/codemod.test.mjs +0 -134
  60. package/src/codemods/__tests__/runner.test.mjs +0 -103
  61. package/src/codemods/integration-discovery.mjs +0 -168
  62. package/src/codemods/integration-discovery.test.mjs +0 -234
  63. package/src/codemods/integration-runner.mjs +0 -109
  64. package/src/codemods/run-codemod.mjs +0 -207
  65. package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-css-surfaces.test.mjs +0 -67
  66. package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-declare-module.test.mjs +0 -61
  67. package/src/codemods/transforms/v0.1.0/__tests__/v0.1.0-ordering.test.mjs +0 -104
  68. package/src/codemods/transforms/v0.1.0/migrate-xds-css-surfaces.mjs +0 -77
  69. package/src/codemods/transforms/v0.1.0/migrate-xds-declare-module.mjs +0 -78
  70. package/src/codemods/transforms/v0.1.3/__tests__/migrate-layout-components-to-experimental.test.mjs +0 -360
  71. package/src/codemods/transforms/v0.1.3/index.mjs +0 -19
  72. package/src/codemods/transforms/v0.1.3/migrate-layout-components-to-experimental.mjs +0 -260
  73. package/src/commands/component-ownership.test.mjs +0 -227
  74. package/src/commands/swizzle.routing.test.mjs +0 -279
  75. package/src/commands/upgrade.config-ordering.test.mjs +0 -273
  76. package/src/commands/upgrade.integration-policy.test.mjs +0 -204
  77. package/src/commands/validate-integration.mjs +0 -110
  78. package/src/commands/validate-integration.test.mjs +0 -124
  79. package/src/integration.mjs +0 -19
  80. package/src/lib/config-cache.mjs +0 -91
  81. package/src/lib/config-cache.test.mjs +0 -83
  82. package/src/lib/integration-warnings.mjs +0 -62
  83. package/src/lib/integration-warnings.test.mjs +0 -102
  84. package/src/lib/module-loader.mjs +0 -80
  85. package/src/lib/module-loader.test.mjs +0 -106
  86. package/src/lib/project.mjs +0 -502
  87. package/src/lib/project.test.mjs +0 -308
  88. package/src/template.mjs +0 -73
  89. package/src/template.test.mjs +0 -127
  90. package/src/types/codemod.d.ts +0 -81
  91. package/src/types/integration.d.ts +0 -29
  92. package/src/types/template-api.d.ts +0 -54
  93. package/src/types/validate-integration.d.ts +0 -24
  94. package/templates/blocks/components/DateRangeInput/DateRangeInputWithPresets.doc.mjs +0 -14
  95. package/templates/blocks/components/DateRangeInput/DateRangeInputWithPresets.tsx +0 -46
  96. package/templates/blocks/components/DateRangeInput/DateRangeInputWithValidation.doc.mjs +0 -14
  97. package/templates/blocks/components/DateRangeInput/DateRangeInputWithValidation.tsx +0 -52
  98. package/templates/blocks/components/DateTimeInput/DateTimeInputWithValidation.doc.mjs +0 -14
  99. package/templates/blocks/components/DateTimeInput/DateTimeInputWithValidation.tsx +0 -43
  100. package/templates/blocks/components/Tab/TabWithSelectedIcon.doc.mjs +0 -13
  101. package/templates/blocks/components/Tab/TabWithSelectedIcon.tsx +0 -39
  102. /package/src/codemods/transforms/{v0.1.0 → v0.0.15}/__tests__/drop-xds-prefix-imports.test.mjs +0 -0
package/CHANGELOG.md CHANGED
@@ -110,6 +110,7 @@ Thanks to everyone who contributed to this release:
110
110
  Thanks to everyone who contributed to this release:
111
111
 
112
112
  - @ejhammond
113
+ - @joeyfarina
113
114
  - @josephfarina
114
115
  - @nynexman4464
115
116
 
package/README.md CHANGED
@@ -62,7 +62,8 @@ 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
- | `doctor` | Diagnose your Astryx setup and report problems with fixes (CI-friendly via exit code) |
65
+ | `gap-report` | Report a gap when a component doesn't meet your needs |
66
+ | `doctor` | Diagnose your XDS setup and report problems with fixes (CI-friendly via exit code) |
66
67
 
67
68
  ### Global options
68
69
 
@@ -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
 
@@ -170,7 +172,7 @@ discriminators each command can emit. Think of it as an OpenAPI spec for the CLI
170
172
 
171
173
  ```bash
172
174
  astryx manifest --json # dedicated surface — type: "manifest"
173
- astryx --json # bare invocation — embeds the same payload under data.manifest
175
+ xds --json # bare invocation — embeds the same payload under data.manifest
174
176
  ```
175
177
 
176
178
  Shape:
@@ -180,7 +182,7 @@ Shape:
180
182
  "apiVersion": 1,
181
183
  "type": "manifest",
182
184
  "data": {
183
- "name": "astryx",
185
+ "name": "xds",
184
186
  "version": "0.0.14",
185
187
  "description": "Design system CLI — components, themes, and tooling",
186
188
  "globalOptions": [
@@ -247,14 +249,14 @@ the `JSON_SUPPORTED` allowlist and a small declarative `RESPONSE_TYPES` map in
247
249
  `src/lib/manifest.mjs`, guarded by a drift test (`manifest.test.mjs`) so adding a
248
250
  command without describing it fails CI.
249
251
 
250
- **Backwards-compat:** the bare `astryx --json` envelope keeps `type: "help"` and its
252
+ **Backwards-compat:** the bare `xds --json` envelope keeps `type: "help"` and its
251
253
  original shallow fields (`name`, `version`, `commands` as a `string[]` of names,
252
254
  `jsonSupported`); the full structured manifest is additive under `data.manifest`.
253
255
  For the standalone manifest envelope (`type: "manifest"`), use `astryx manifest --json`.
254
256
 
255
257
  ## Programmatic API
256
258
 
257
- The same logic that powers `astryx --json` is available as importable, type-safe functions:
259
+ The same logic that powers `xds --json` is available as importable, type-safe functions:
258
260
 
259
261
  ```typescript
260
262
  import {
@@ -267,20 +269,20 @@ import {
267
269
  AstryxError,
268
270
  } from '@astryxdesign/cli/api';
269
271
 
270
- // Same result as: astryx --json component Button
272
+ // Same result as: xds --json component Button
271
273
  const btn = await component('Button');
272
274
  btn.type; // 'component.detail'
273
275
  btn.data.name; // 'Button' (typed as ComponentDoc)
274
276
 
275
- // Same result as: astryx --json component --list
277
+ // Same result as: xds --json component --list
276
278
  const list = await component(undefined, {list: true});
277
279
  list.data; // Record<string, string[]>
278
280
 
279
- // Same result as: astryx --json docs principles
281
+ // Same result as: xds --json docs principles
280
282
  const principles = await docs('principles');
281
- principles.data.title; // 'Principles'
283
+ principles.data.title; // 'XDS Principles'
282
284
 
283
- // Same result as: astryx --json hook useMediaQuery
285
+ // Same result as: xds --json hook useMediaQuery
284
286
  const useMediaQuery = await hook('useMediaQuery');
285
287
  useMediaQuery.data.params; // typed as HookParamDoc[]
286
288
 
@@ -294,7 +296,7 @@ try {
294
296
  }
295
297
  ```
296
298
 
297
- The CLI command handlers are thin wrappers around these functions: they parse args, call the API, then format the output (JSON or text). This guarantees that `@astryxdesign/cli/api` and `astryx --json` always return identical data.
299
+ The CLI command handlers are thin wrappers around these functions: they parse args, call the API, then format the output (JSON or text). This guarantees that `@astryxdesign/cli/api` and `xds --json` always return identical data.
298
300
 
299
301
  ### Consumer utilities
300
302
 
@@ -324,41 +326,43 @@ detail.data.name; // already narrowed
324
326
 
325
327
  Every response has a `type` string that uniquely identifies it:
326
328
 
327
- | Command | Type | Response |
328
- | ------------------------------------------------- | --------------------------- | --------------------------------- |
329
- | `astryx --json component [--list]` | `component.list` | `ComponentListResponse` |
330
- | `astryx --json component --list --detail compact` | `component.brief` | `ComponentBriefResponse` |
331
- | `astryx --json component --list --detail full` | `component.full` | `ComponentFullResponse` |
332
- | `astryx --json component <name>` | `component.detail` | `ComponentDetailResponse` |
333
- | `astryx --json component <name> --props` | `component.detail.props` | `ComponentDetailPropsResponse` |
334
- | `astryx --json component <name> --source` | `component.detail.source` | `ComponentDetailSourceResponse` |
335
- | `astryx --json component <name> --showcase` | `component.detail.showcase` | `ComponentDetailShowcaseResponse` |
336
- | `astryx --json component <name> --blocks` | `component.detail.blocks` | `ComponentDetailBlocksResponse` |
337
- | `astryx --json discover` | `discover.list` | `DiscoverListResponse` |
338
- | `astryx --json discover @scope/name` | `discover.detail` | `DiscoverDetailResponse` |
339
- | `astryx --json discover @scope/name/Comp` | `discover.detail.doc` | `DiscoverDetailDocResponse` |
340
- | `astryx --json discover <search>` | `discover.search` | `DiscoverSearchResponse` |
341
- | `astryx --json docs` | `docs.list` | `DocsListResponse` |
342
- | `astryx --json docs <topic>` | `docs.detail` | `DocsDetailResponse` |
343
- | `astryx --json docs <topic> <section>` | `docs.detail.section` | `DocsDetailSectionResponse` |
344
- | `astryx --json template [--list]` | `template.list` | `TemplateListResponse` |
345
- | `astryx --json template <name>` | `template.show` | `TemplateShowResponse` |
346
- | `astryx --json template <name> --skeleton` | `template.skeleton` | `TemplateSkeletonResponse` |
347
- | `astryx --json template <name> [path]` | `template.copy` | `TemplateCopyResponse` |
348
- | `astryx --json hook [--list]` | `hook.list` | `HookListResponse` |
349
- | `astryx --json hook --list --detail compact` | `hook.brief` | `HookBriefResponse` |
350
- | `astryx --json hook --list --detail full` | `hook.full` | `HookFullResponse` |
351
- | `astryx --json hook <name>` | `hook.detail` | `HookDetailResponse` |
352
- | `astryx --json hook <name> --params` | `hook.detail.params` | `HookDetailParamsResponse` |
353
- | `astryx --json search <query>` | `search` | `SearchResponse` |
354
- | `astryx --json swizzle [--list]` | `swizzle.list` | `SwizzleListResponse` |
355
- | `astryx --json swizzle <component>` | `swizzle.copy` | `SwizzleCopyResponse` |
356
- | `astryx --json theme build <file>` | `theme.build` | `ThemeBuildResponse` |
357
- | `astryx --json upgrade --list` | `upgrade.list` | `UpgradeListResponse` |
358
- | `astryx --json upgrade [--apply]` | `upgrade.run` | `UpgradeRunResponse` |
359
- | `astryx --json doctor` | `doctor` | `DoctorResponse` |
360
- | any error | | `CLIError` |
361
- | unsupported command | | `CLIUnsupportedError` |
329
+ | Command | Type | Response |
330
+ | ---------------------------------------------- | --------------------------- | --------------------------------- |
331
+ | `xds --json component [--list]` | `component.list` | `ComponentListResponse` |
332
+ | `xds --json component --list --detail compact` | `component.brief` | `ComponentBriefResponse` |
333
+ | `xds --json component --list --detail full` | `component.full` | `ComponentFullResponse` |
334
+ | `xds --json component <name>` | `component.detail` | `ComponentDetailResponse` |
335
+ | `xds --json component <name> --props` | `component.detail.props` | `ComponentDetailPropsResponse` |
336
+ | `xds --json component <name> --source` | `component.detail.source` | `ComponentDetailSourceResponse` |
337
+ | `xds --json component <name> --showcase` | `component.detail.showcase` | `ComponentDetailShowcaseResponse` |
338
+ | `xds --json component <name> --blocks` | `component.detail.blocks` | `ComponentDetailBlocksResponse` |
339
+ | `xds --json discover` | `discover.list` | `DiscoverListResponse` |
340
+ | `xds --json discover @scope/name` | `discover.detail` | `DiscoverDetailResponse` |
341
+ | `xds --json discover @scope/name/Comp` | `discover.detail.doc` | `DiscoverDetailDocResponse` |
342
+ | `xds --json discover <search>` | `discover.search` | `DiscoverSearchResponse` |
343
+ | `xds --json docs` | `docs.list` | `DocsListResponse` |
344
+ | `xds --json docs <topic>` | `docs.detail` | `DocsDetailResponse` |
345
+ | `xds --json docs <topic> <section>` | `docs.detail.section` | `DocsDetailSectionResponse` |
346
+ | `xds --json template [--list]` | `template.list` | `TemplateListResponse` |
347
+ | `xds --json template <name>` | `template.show` | `TemplateShowResponse` |
348
+ | `xds --json template <name> --skeleton` | `template.skeleton` | `TemplateSkeletonResponse` |
349
+ | `xds --json template <name> [path]` | `template.copy` | `TemplateCopyResponse` |
350
+ | `xds --json hook [--list]` | `hook.list` | `HookListResponse` |
351
+ | `xds --json hook --list --detail compact` | `hook.brief` | `HookBriefResponse` |
352
+ | `xds --json hook --list --detail full` | `hook.full` | `HookFullResponse` |
353
+ | `xds --json hook <name>` | `hook.detail` | `HookDetailResponse` |
354
+ | `xds --json hook <name> --params` | `hook.detail.params` | `HookDetailParamsResponse` |
355
+ | `xds --json search <query>` | `search` | `SearchResponse` |
356
+ | `xds --json swizzle [--list]` | `swizzle.list` | `SwizzleListResponse` |
357
+ | `xds --json swizzle <component>` | `swizzle.copy` | `SwizzleCopyResponse` |
358
+ | `xds --json theme build <file>` | `theme.build` | `ThemeBuildResponse` |
359
+ | `xds --json upgrade --list` | `upgrade.list` | `UpgradeListResponse` |
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` |
363
+ | `xds --json doctor` | `doctor` | `DoctorResponse` |
364
+ | any error | — | `CLIError` |
365
+ | unsupported command | — | `CLIUnsupportedError` |
362
366
 
363
367
  ## Doctor
364
368
 
@@ -379,7 +383,7 @@ astryx doctor — diagnosing your setup
379
383
  @astryxdesign/core v0.0.14 is in step with @astryxdesign/cli v0.0.14.
380
384
  ⚠ Theme packages
381
385
  No @astryxdesign/theme-* packages are installed.
382
- → fix: Install a theme, e.g. `npm install @astryxdesign/theme-neutral`, then import its CSS or set astryx.theme.
386
+ → fix: Install a theme, e.g. `npm install @astryxdesign/theme-neutral`, then import its CSS or set xds.theme.
383
387
  ℹ astryx.config.mjs
384
388
  No astryx.config.mjs found — using defaults.
385
389
  ℹ AI agent docs
@@ -404,7 +408,7 @@ No failures — but review the ⚠ warnings above when you can.
404
408
  | Version alignment | pass / warn / info | Installed `@astryxdesign/core` is in step with `@astryxdesign/cli` |
405
409
  | Theme packages | pass / warn | An `@astryxdesign/theme-*` package is installed and a theme is wired |
406
410
  | astryx.config.mjs | pass / fail / info | Config (if present) loads cleanly with a valid shape |
407
- | AI agent docs | pass / warn / info | Agent docs exist and contain the Astryx section markers |
411
+ | AI agent docs | pass / warn / info | Agent docs exist and contain the XDS section markers |
408
412
  | Peer dependencies | pass / warn / info | `@astryxdesign/core`'s peer deps (react, …) are installed |
409
413
  | Package manager | info | Reports the detected package manager |
410
414
 
@@ -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.ae2d146",
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.ae2d146",
79
- "@astryxdesign/lab": "0.1.2-canary.ae2d146",
80
- "@astryxdesign/theme-neutral": "0.1.2-canary.ae2d146",
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.ae2d146",
96
- "@astryxdesign/lab": "0.1.2-canary.ae2d146",
97
- "@astryxdesign/theme-neutral": "0.1.2-canary.ae2d146",
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
  }