@astryxdesign/cli 0.1.2-canary.b712653 → 0.1.2-canary.bbf77ea

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 (111) hide show
  1. package/CHANGELOG.md +0 -1
  2. package/README.md +49 -55
  3. package/docs/theme.doc.mjs +2 -2
  4. package/package.json +19 -7
  5. package/src/api/component.mjs +253 -12
  6. package/src/api/discover.mjs +14 -7
  7. package/src/api/doctor.mjs +10 -25
  8. package/src/api/layout.mjs +11 -10
  9. package/src/api/layout.test.mjs +4 -1
  10. package/src/api/template-integration.test.mjs +225 -0
  11. package/src/api/template.mjs +242 -84
  12. package/src/api/validate-integration.mjs +370 -0
  13. package/src/api/validate-integration.test.mjs +222 -0
  14. package/src/codemod.mjs +93 -0
  15. package/src/codemod.test.mjs +134 -0
  16. package/src/codemods/__tests__/registry.test.mjs +1 -0
  17. package/src/codemods/__tests__/runner.test.mjs +103 -0
  18. package/src/codemods/integration-discovery.mjs +168 -0
  19. package/src/codemods/integration-discovery.test.mjs +234 -0
  20. package/src/codemods/integration-runner.mjs +109 -0
  21. package/src/codemods/registry.mjs +1 -0
  22. package/src/codemods/run-codemod.mjs +207 -0
  23. package/src/codemods/runner.mjs +73 -96
  24. package/src/codemods/transforms/v0.0.15/index.mjs +0 -13
  25. package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-css-surfaces.test.mjs +67 -0
  26. package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-declare-module.test.mjs +61 -0
  27. package/src/codemods/transforms/v0.1.0/__tests__/v0.1.0-ordering.test.mjs +104 -0
  28. package/src/codemods/transforms/{v0.0.15 → v0.1.0}/drop-xds-prefix-imports.mjs +15 -4
  29. package/src/codemods/transforms/v0.1.0/index.mjs +43 -6
  30. package/src/codemods/transforms/v0.1.0/migrate-xds-css-surfaces.mjs +77 -0
  31. package/src/codemods/transforms/v0.1.0/migrate-xds-declare-module.mjs +78 -0
  32. package/src/codemods/transforms/v0.1.3/__tests__/migrate-layout-components-to-experimental.test.mjs +360 -0
  33. package/src/codemods/transforms/v0.1.3/index.mjs +19 -0
  34. package/src/codemods/transforms/v0.1.3/migrate-layout-components-to-experimental.mjs +260 -0
  35. package/src/commands/agent-docs.mjs +1 -1
  36. package/src/commands/component/index.mjs +44 -10
  37. package/src/commands/component-ownership.test.mjs +227 -0
  38. package/src/commands/discover.mjs +5 -16
  39. package/src/commands/doctor.test.mjs +3 -3
  40. package/src/commands/json-contract.test.mjs +0 -32
  41. package/src/commands/swizzle.mjs +224 -235
  42. package/src/commands/swizzle.path-safety.test.mjs +3 -3
  43. package/src/commands/swizzle.routing.test.mjs +279 -0
  44. package/src/commands/template.mjs +33 -37
  45. package/src/commands/upgrade.config-ordering.test.mjs +273 -0
  46. package/src/commands/upgrade.integration-policy.test.mjs +204 -0
  47. package/src/commands/upgrade.mjs +349 -170
  48. package/src/commands/validate-integration.mjs +110 -0
  49. package/src/commands/validate-integration.test.mjs +124 -0
  50. package/src/config.mjs +7 -20
  51. package/src/config.test.mjs +89 -13
  52. package/src/index.mjs +7 -3
  53. package/src/integration.mjs +19 -0
  54. package/src/lib/component-discovery.mjs +177 -0
  55. package/src/lib/config-cache.mjs +91 -0
  56. package/src/lib/config-cache.test.mjs +83 -0
  57. package/src/lib/config-schema.mjs +41 -55
  58. package/src/lib/error-codes.mjs +7 -12
  59. package/src/lib/integration-warnings.mjs +62 -0
  60. package/src/lib/integration-warnings.test.mjs +102 -0
  61. package/src/lib/integrations.mjs +92 -115
  62. package/src/lib/integrations.test.mjs +88 -107
  63. package/src/lib/manifest.mjs +5 -2
  64. package/src/lib/module-loader.mjs +80 -0
  65. package/src/lib/module-loader.test.mjs +106 -0
  66. package/src/lib/project.mjs +502 -0
  67. package/src/lib/project.test.mjs +308 -0
  68. package/src/template.mjs +73 -0
  69. package/src/template.test.mjs +127 -0
  70. package/src/types/api.d.ts +2 -6
  71. package/src/types/base.d.ts +3 -9
  72. package/src/types/codemod.d.ts +81 -0
  73. package/src/types/component.d.ts +27 -2
  74. package/src/types/config.d.ts +56 -85
  75. package/src/types/error-codes.d.ts +5 -3
  76. package/src/types/index.d.ts +0 -1
  77. package/src/types/integration.d.ts +29 -0
  78. package/src/types/swizzle.d.ts +9 -2
  79. package/src/types/template-api.d.ts +54 -0
  80. package/src/types/template.d.ts +10 -7
  81. package/src/types/upgrade.d.ts +29 -0
  82. package/src/types/validate-integration.d.ts +24 -0
  83. package/src/utils/github.mjs +0 -237
  84. package/src/utils/interactive.mjs +2 -2
  85. package/templates/blocks/components/DateRangeInput/DateRangeInputWithPresets.doc.mjs +14 -0
  86. package/templates/blocks/components/DateRangeInput/DateRangeInputWithPresets.tsx +46 -0
  87. package/templates/blocks/components/DateRangeInput/DateRangeInputWithValidation.doc.mjs +14 -0
  88. package/templates/blocks/components/DateRangeInput/DateRangeInputWithValidation.tsx +52 -0
  89. package/templates/blocks/components/DateTimeInput/DateTimeInputWithValidation.doc.mjs +14 -0
  90. package/templates/blocks/components/DateTimeInput/DateTimeInputWithValidation.tsx +43 -0
  91. package/templates/blocks/components/DropdownMenu/DropdownMenuShowcase.tsx +0 -1
  92. package/templates/blocks/components/DropdownMenuItem/DropdownMenuItemShowcase.tsx +0 -1
  93. package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.doc.mjs +1 -1
  94. package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.tsx +9 -4
  95. package/templates/blocks/components/LinkProvider/LinkProviderCustomLink.doc.mjs +1 -1
  96. package/templates/blocks/components/MoreMenu/MoreMenuShowcase.tsx +0 -1
  97. package/templates/blocks/components/Tab/TabWithSelectedIcon.doc.mjs +13 -0
  98. package/templates/blocks/components/Tab/TabWithSelectedIcon.tsx +39 -0
  99. package/templates/blocks/components/Table/StickyColumnsHookUsage.doc.mjs +1 -1
  100. package/templates/pages/shell-side-nav/page.tsx +0 -1
  101. package/templates/pages/shell-side-nav/template.doc.mjs +1 -1
  102. package/templates/pages/shell-top-nav/template.doc.mjs +1 -1
  103. package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-config-surfaces.test.mjs +0 -116
  104. package/src/codemods/transforms/v0.1.0/migrate-xds-config-surfaces.mjs +0 -230
  105. package/src/commands/gap-report.mjs +0 -464
  106. package/src/commands/gap-report.test.mjs +0 -168
  107. package/src/commands/swizzle-gap-safety.test.mjs +0 -273
  108. package/src/lib/config.mjs +0 -113
  109. package/src/lib/config.test.mjs +0 -91
  110. package/src/types/gap-report.d.ts +0 -29
  111. /package/src/codemods/transforms/{v0.0.15 → v0.1.0}/__tests__/drop-xds-prefix-imports.test.mjs +0 -0
package/CHANGELOG.md CHANGED
@@ -110,7 +110,6 @@ 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
114
113
  - @josephfarina
115
114
  - @nynexman4464
116
115
 
package/README.md CHANGED
@@ -62,8 +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 |
66
- | `doctor` | Diagnose your XDS setup and report problems with fixes (CI-friendly via exit code) |
65
+ | `doctor` | Diagnose your Astryx setup and report problems with fixes (CI-friendly via exit code) |
67
66
 
68
67
  ### Global options
69
68
 
@@ -161,7 +160,6 @@ if (isError(result)) {
161
160
  | `ERR_INVALID_VERSION` | A `--from`/`--to` value was not a valid semver string. |
162
161
  | `ERR_DEP_MISSING` | A required external dependency (e.g. jscodeshift) is missing. |
163
162
  | `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). |
165
163
 
166
164
  ## Capability manifest (agent discovery)
167
165
 
@@ -172,7 +170,7 @@ discriminators each command can emit. Think of it as an OpenAPI spec for the CLI
172
170
 
173
171
  ```bash
174
172
  astryx manifest --json # dedicated surface — type: "manifest"
175
- xds --json # bare invocation — embeds the same payload under data.manifest
173
+ astryx --json # bare invocation — embeds the same payload under data.manifest
176
174
  ```
177
175
 
178
176
  Shape:
@@ -182,7 +180,7 @@ Shape:
182
180
  "apiVersion": 1,
183
181
  "type": "manifest",
184
182
  "data": {
185
- "name": "xds",
183
+ "name": "astryx",
186
184
  "version": "0.0.14",
187
185
  "description": "Design system CLI — components, themes, and tooling",
188
186
  "globalOptions": [
@@ -249,14 +247,14 @@ the `JSON_SUPPORTED` allowlist and a small declarative `RESPONSE_TYPES` map in
249
247
  `src/lib/manifest.mjs`, guarded by a drift test (`manifest.test.mjs`) so adding a
250
248
  command without describing it fails CI.
251
249
 
252
- **Backwards-compat:** the bare `xds --json` envelope keeps `type: "help"` and its
250
+ **Backwards-compat:** the bare `astryx --json` envelope keeps `type: "help"` and its
253
251
  original shallow fields (`name`, `version`, `commands` as a `string[]` of names,
254
252
  `jsonSupported`); the full structured manifest is additive under `data.manifest`.
255
253
  For the standalone manifest envelope (`type: "manifest"`), use `astryx manifest --json`.
256
254
 
257
255
  ## Programmatic API
258
256
 
259
- The same logic that powers `xds --json` is available as importable, type-safe functions:
257
+ The same logic that powers `astryx --json` is available as importable, type-safe functions:
260
258
 
261
259
  ```typescript
262
260
  import {
@@ -269,20 +267,20 @@ import {
269
267
  AstryxError,
270
268
  } from '@astryxdesign/cli/api';
271
269
 
272
- // Same result as: xds --json component Button
270
+ // Same result as: astryx --json component Button
273
271
  const btn = await component('Button');
274
272
  btn.type; // 'component.detail'
275
273
  btn.data.name; // 'Button' (typed as ComponentDoc)
276
274
 
277
- // Same result as: xds --json component --list
275
+ // Same result as: astryx --json component --list
278
276
  const list = await component(undefined, {list: true});
279
277
  list.data; // Record<string, string[]>
280
278
 
281
- // Same result as: xds --json docs principles
279
+ // Same result as: astryx --json docs principles
282
280
  const principles = await docs('principles');
283
- principles.data.title; // 'XDS Principles'
281
+ principles.data.title; // 'Principles'
284
282
 
285
- // Same result as: xds --json hook useMediaQuery
283
+ // Same result as: astryx --json hook useMediaQuery
286
284
  const useMediaQuery = await hook('useMediaQuery');
287
285
  useMediaQuery.data.params; // typed as HookParamDoc[]
288
286
 
@@ -296,7 +294,7 @@ try {
296
294
  }
297
295
  ```
298
296
 
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.
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.
300
298
 
301
299
  ### Consumer utilities
302
300
 
@@ -326,43 +324,41 @@ detail.data.name; // already narrowed
326
324
 
327
325
  Every response has a `type` string that uniquely identifies it:
328
326
 
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` |
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` |
366
362
 
367
363
  ## Doctor
368
364
 
@@ -383,7 +379,7 @@ astryx doctor — diagnosing your setup
383
379
  @astryxdesign/core v0.0.14 is in step with @astryxdesign/cli v0.0.14.
384
380
  ⚠ Theme packages
385
381
  No @astryxdesign/theme-* packages are installed.
386
- → fix: Install a theme, e.g. `npm install @astryxdesign/theme-neutral`, then import its CSS or set xds.theme.
382
+ → fix: Install a theme, e.g. `npm install @astryxdesign/theme-neutral`, then import its CSS or set astryx.theme.
387
383
  ℹ astryx.config.mjs
388
384
  No astryx.config.mjs found — using defaults.
389
385
  ℹ AI agent docs
@@ -408,7 +404,7 @@ No failures — but review the ⚠ warnings above when you can.
408
404
  | Version alignment | pass / warn / info | Installed `@astryxdesign/core` is in step with `@astryxdesign/cli` |
409
405
  | Theme packages | pass / warn | An `@astryxdesign/theme-*` package is installed and a theme is wired |
410
406
  | astryx.config.mjs | pass / fail / info | Config (if present) loads cleanly with a valid shape |
411
- | AI agent docs | pass / warn / info | Agent docs exist and contain the XDS section markers |
407
+ | AI agent docs | pass / warn / info | Agent docs exist and contain the Astryx section markers |
412
408
  | Peer dependencies | pass / warn / info | `@astryxdesign/core`'s peer deps (react, …) are installed |
413
409
  | Package manager | info | Reports the detected package manager |
414
410
 
@@ -434,8 +430,6 @@ export default {
434
430
  templates: {
435
431
  get: async id => fetchTemplateFromAPI(id),
436
432
  },
437
- gapReport: {
438
- url: 'https://your-api.com/gaps',
439
- },
433
+ issuesUrl: 'https://github.com/your-org/your-repo/issues',
440
434
  };
441
435
  ```
@@ -53,7 +53,7 @@ function App() {
53
53
  },
54
54
  {
55
55
  type: 'prose',
56
- text: 'Each theme ships as its own npm package. Install the one you want, then wrap your app in `<Theme>` the same pattern works for every theme; just swap the package and import name.',
56
+ text: 'Each theme ships as its own npm package. Install the one you want, then wrap your app in `<Theme>`. The same pattern works for every theme; just swap the package and import name.',
57
57
  },
58
58
  {
59
59
  type: 'prose',
@@ -261,7 +261,7 @@ const brandTheme = defineTheme({
261
261
  content: [
262
262
  {
263
263
  type: 'prose',
264
- text: 'The `components` field in defineTheme uses semantic component keys and style keys not raw CSS selectors. Use `base` for all instances, `variant:value` or `stateName` for specific props/states, and let the theme pipeline choose the underlying selector. For raw external CSS escape hatches, prefer the data-attribute selector surface documented in `astryx docs styling`.',
264
+ text: 'The `components` field in defineTheme uses semantic component keys and style keys, not raw CSS selectors. Use `base` for all instances, `variant:value` or `stateName` for specific props/states, and let the theme pipeline choose the underlying selector. For raw external CSS escape hatches, prefer the data-attribute selector surface documented in `astryx docs styling`.',
265
265
  },
266
266
  {
267
267
  type: 'code',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/cli",
3
- "version": "0.1.2-canary.b712653",
3
+ "version": "0.1.2-canary.bbf77ea",
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,6 +43,18 @@
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
+ },
46
58
  "./xle": {
47
59
  "types": "./src/lib/xle/browser.d.ts",
48
60
  "import": "./src/lib/xle/browser.mjs"
@@ -63,9 +75,9 @@
63
75
  "zod": "^4.4.3"
64
76
  },
65
77
  "peerDependencies": {
66
- "@astryxdesign/core": "0.1.2-canary.b712653",
67
- "@astryxdesign/lab": "0.1.2-canary.b712653",
68
- "@astryxdesign/theme-neutral": "0.1.2-canary.b712653",
78
+ "@astryxdesign/core": "0.1.2-canary.bbf77ea",
79
+ "@astryxdesign/lab": "0.1.2-canary.bbf77ea",
80
+ "@astryxdesign/theme-neutral": "0.1.2-canary.bbf77ea",
69
81
  "gpt-tokenizer": "^2.0.0"
70
82
  },
71
83
  "peerDependenciesMeta": {
@@ -80,9 +92,9 @@
80
92
  }
81
93
  },
82
94
  "devDependencies": {
83
- "@astryxdesign/core": "0.1.2-canary.b712653",
84
- "@astryxdesign/lab": "0.1.2-canary.b712653",
85
- "@astryxdesign/theme-neutral": "0.1.2-canary.b712653",
95
+ "@astryxdesign/core": "0.1.2-canary.bbf77ea",
96
+ "@astryxdesign/lab": "0.1.2-canary.bbf77ea",
97
+ "@astryxdesign/theme-neutral": "0.1.2-canary.bbf77ea",
86
98
  "gpt-tokenizer": "^2.0.0"
87
99
  },
88
100
  "scripts": {
@@ -11,18 +11,48 @@ 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,
14
15
  discoverComponents,
15
16
  discoverExternalComponentsGrouped,
17
+ discoverIntegrationComponents,
16
18
  findComponentReadme,
17
19
  findComponentSource,
18
20
  findExternalComponentDoc,
21
+ findIntegrationComponentDoc,
22
+ findIntegrationComponentSource,
19
23
  resolveImportPath,
20
24
  } from '../lib/component-discovery.mjs';
25
+ import {Project} from '../lib/project.mjs';
21
26
  import {loadDocs} from '../lib/component-loader.mjs';
22
27
  import {searchComponents} from '../lib/string-utils.mjs';
23
28
  import {AstryxError} from './error.mjs';
24
29
  import {findShowcase, findRelatedBlocks} from './template.mjs';
25
30
 
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
+
26
56
  /**
27
57
  * Resolve an external package by name from the discovered externals list.
28
58
  * @param {string} packageName - e.g. '@acme/xds-widgets'
@@ -131,8 +161,13 @@ export async function component(name, options = {}) {
131
161
  return {type: 'component.full', data: {[match[0]]: entries}};
132
162
  }
133
163
 
134
- // Default: brief — names only
135
- return {type: 'component.list', data: {[match[0]]: match[1]}};
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
+ };
136
171
  }
137
172
 
138
173
  // All components — merge core + external packages with grouped subcategories
@@ -179,43 +214,189 @@ export async function component(name, options = {}) {
179
214
  return {type: 'component.full', data: result};
180
215
  }
181
216
 
182
- // Default: brief — names only (with externals merged in)
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.
183
251
  const externals = discoverExternalPackages(cwd);
184
252
  for (const ext of externals) {
253
+ if (seenIntegration.has(ext.name)) continue;
185
254
  const grouped = discoverExternalComponentsGrouped(ext.docsDir);
186
255
  const groupKeys = Object.keys(grouped);
187
256
  if (groupKeys.length === 0) continue;
188
257
 
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.
191
258
  const hasGroups = groupKeys.some(
192
259
  k => grouped[k].length > 1 || grouped[k][0] !== k,
193
260
  );
194
261
 
195
262
  if (hasGroups) {
196
263
  for (const [group, members] of Object.entries(grouped)) {
197
- components[`${group} (${ext.name})`] = members;
264
+ listData[`${group} (${ext.name})`] = members.map(n => ({
265
+ name: n,
266
+ package: ext.name,
267
+ }));
198
268
  }
199
269
  } else {
200
- // All ungrouped — single flat list under the package category
201
270
  const allComps = Object.values(grouped).flat().sort();
202
271
  if (allComps.length > 0) {
203
- components[`${ext.category} (${ext.name})`] = allComps;
272
+ listData[`${ext.category} (${ext.name})`] = allComps.map(n => ({
273
+ name: n,
274
+ package: ext.name,
275
+ }));
204
276
  }
205
277
  }
206
278
  }
207
- return {type: 'component.list', data: components};
279
+ return {type: 'component.list', data: listData};
208
280
  }
209
281
 
210
282
  // ── Single component ───────────────────────────────────────────
211
283
 
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
+
212
292
  const dirName = name.replace(/^XDS/, '');
213
293
 
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
+
214
353
  // When scoped to a specific package, search that package first.
215
354
  // This is critical for components that exist in both core and an external
216
355
  // package (e.g. AppShell, Button, SideNav) — the package scope ensures
217
356
  // the external package's docs are returned, not core's.
218
357
  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.
219
400
  const ext = resolveExternalPackage(packageScope, cwd);
220
401
  if (!ext) {
221
402
  throw new AstryxError(`External package "${packageScope}" not found`, undefined, ERROR_CODES.ERR_UNKNOWN_PACKAGE);
@@ -244,11 +425,49 @@ export async function component(name, options = {}) {
244
425
  const p = docs.props || (docs.components ? docs.components.flatMap(c => c.props || []) : []);
245
426
  return {type: 'component.detail.props', data: p};
246
427
  }
247
- return {type: 'component.detail', data: docs};
428
+ return {
429
+ type: 'component.detail',
430
+ data: withOwnership(docs, {package: ext.name, sourcePath: null}, dirName),
431
+ };
248
432
  }
249
433
  throw new AstryxError(`No component "${name}" in package "${packageScope}"`, undefined, ERROR_CODES.ERR_UNKNOWN_COMPONENT);
250
434
  }
251
435
 
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
+
252
471
  if (source) {
253
472
  const sourcePath = findComponentSource(coreDir, dirName);
254
473
  if (!sourcePath) {
@@ -274,6 +493,10 @@ export async function component(name, options = {}) {
274
493
 
275
494
  let readmePath = findComponentReadme(coreDir, dirName);
276
495
  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;
277
500
 
278
501
  if (!readmePath) {
279
502
  const externals = discoverExternalPackages(cwd);
@@ -281,6 +504,8 @@ export async function component(name, options = {}) {
281
504
  const extDocPath = findExternalComponentDoc(ext.docsDir, dirName);
282
505
  if (extDocPath) {
283
506
  readmePath = extDocPath;
507
+ resolvedOwnerPackage = ext.name;
508
+ resolvedSourcePath = null;
284
509
  break;
285
510
  }
286
511
  }
@@ -299,6 +524,8 @@ export async function component(name, options = {}) {
299
524
  if (topScore >= 90 && topTied.length === 1 && gap >= 20) {
300
525
  resolvedName = topTied[0].name;
301
526
  readmePath = findComponentReadme(coreDir, resolvedName);
527
+ resolvedOwnerPackage = CORE_PACKAGE;
528
+ resolvedSourcePath = findComponentSource(coreDir, resolvedName);
302
529
  } else {
303
530
  const threshold = Math.max(topScore - 20, 1);
304
531
  const candidates = results.filter(r => r.score >= threshold).slice(0, 5);
@@ -382,7 +609,14 @@ export async function component(name, options = {}) {
382
609
  if (props) {
383
610
  return {type: 'component.detail.props', data: matchingComponent.props || []};
384
611
  }
385
- return {type: 'component.detail', data: scoped};
612
+ return {
613
+ type: 'component.detail',
614
+ data: withOwnership(
615
+ scoped,
616
+ {package: resolvedOwnerPackage, sourcePath: resolvedSourcePath},
617
+ dirName,
618
+ ),
619
+ };
386
620
  }
387
621
 
388
622
  if (props) {
@@ -390,5 +624,12 @@ export async function component(name, options = {}) {
390
624
  return {type: 'component.detail.props', data: p};
391
625
  }
392
626
 
393
- return {type: 'component.detail', data: docs};
627
+ return {
628
+ type: 'component.detail',
629
+ data: withOwnership(
630
+ docs,
631
+ {package: resolvedOwnerPackage, sourcePath: resolvedSourcePath},
632
+ resolvedName,
633
+ ),
634
+ };
394
635
  }