@astryxdesign/cli 0.4.5 → 0.4.6-canary.cea9ecd

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 (72) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/api/component/_adapter.d.mts +22 -5
  3. package/api/component/_adapter.mjs +51 -3
  4. package/api/docs/_adapter.d.mts +37 -8
  5. package/api/docs/_adapter.mjs +70 -41
  6. package/api/docs/detail/detail.d.mts +2 -0
  7. package/api/docs/detail/detail.mjs +12 -10
  8. package/api/docs/detail/section/section.d.mts +2 -0
  9. package/api/docs/detail/section/section.mjs +1 -0
  10. package/api/docs/docs.d.mts +2 -0
  11. package/api/docs/docs.doc.mjs +11 -2
  12. package/api/docs/docs.mjs +2 -1
  13. package/api/docs/docs.type.d.mts +15 -0
  14. package/api/docs/docs.type.mjs +6 -0
  15. package/api/docs/integrationDocs.test.mjs +208 -0
  16. package/api/docs/list/list.d.mts +5 -1
  17. package/api/docs/list/list.mjs +30 -14
  18. package/api/init/run/run.mjs +9 -4
  19. package/api/integration/validate-integration.mjs +2 -1
  20. package/api/integration/validate-integration.type.d.mts +1 -0
  21. package/api/integration/validate-integration.type.mjs +1 -0
  22. package/api/search/search.mjs +26 -15
  23. package/api/upgrade/_adapter.d.mts +2 -2
  24. package/api/upgrade/_adapter.mjs +7 -3
  25. package/api/upgrade/run/run.mjs +1 -1
  26. package/assets/codemods/transforms/v0.1.0/__tests__/drop-xds-prefix-imports.test.mjs +245 -0
  27. package/assets/codemods/transforms/v0.1.0/drop-xds-prefix-imports.mjs +139 -13
  28. package/assets/docs/cli-integrations.doc.mjs +41 -3
  29. package/assets/docs/layout.doc.dense.mjs +275 -30
  30. package/assets/docs/layout.doc.mjs +412 -78
  31. package/assets/docs/theme.doc.mjs +3 -3
  32. package/assets/templates/blocks/components/AvatarGroup/AvatarGroupInteractive.doc.mjs +15 -0
  33. package/assets/templates/blocks/components/AvatarGroup/AvatarGroupInteractive.tsx +47 -0
  34. package/assets/templates/blocks/components/AvatarGroup/AvatarGroupShowcase.doc.mjs +1 -1
  35. package/assets/templates/blocks/components/Breadcrumbs/BreadcrumbsCustomSeparator.tsx +18 -5
  36. package/assets/templates/blocks/components/Selector/SelectorOptionDescriptions.doc.mjs +14 -0
  37. package/assets/templates/blocks/components/Selector/SelectorOptionDescriptions.tsx +76 -0
  38. package/assets/templates/pages/dashboard-cohort-funnel/page.tsx +914 -0
  39. package/assets/templates/pages/dashboard-cohort-funnel/template.doc.mjs +12 -0
  40. package/assets/templates/pages/dashboard-data/page.tsx +894 -0
  41. package/assets/templates/pages/dashboard-data/template.doc.mjs +13 -0
  42. package/assets/templates/pages/dashboard-executive-summary/page.tsx +874 -0
  43. package/assets/templates/pages/dashboard-executive-summary/template.doc.mjs +13 -0
  44. package/assets/templates/pages/dashboard-project-status/page.tsx +1139 -0
  45. package/assets/templates/pages/dashboard-project-status/template.doc.mjs +13 -0
  46. package/assets/templates/pages/dashboard-service-monitoring/page.tsx +1590 -0
  47. package/assets/templates/pages/dashboard-service-monitoring/template.doc.mjs +12 -0
  48. package/authoring/doctypes/_schema.d.mts +2 -0
  49. package/authoring/doctypes/_schema.mjs +5 -0
  50. package/authoring/doctypes/reference/reference.doc.mjs +14 -0
  51. package/authoring/doctypes/reference/type.ts +12 -0
  52. package/authoring/doctypes/template/type.ts +2 -0
  53. package/authoring/integration/integration.doc.mjs +10 -1
  54. package/authoring/integration/parse.d.mts +1 -0
  55. package/authoring/integration/parse.mjs +1 -0
  56. package/authoring/integration/parse.test.mjs +1 -0
  57. package/authoring/integration/type.ts +5 -0
  58. package/clients/cli/commands/component-ownership.test.mjs +63 -3
  59. package/clients/cli/commands/theme-build.doc.mjs +1 -1
  60. package/foundation/agent-docs/agent-docs.d.mts +19 -3
  61. package/foundation/agent-docs/agent-docs.mjs +30 -11
  62. package/foundation/agent-docs/agent-docs.test.mjs +38 -0
  63. package/foundation/config/project.d.mts +16 -0
  64. package/foundation/config/project.mjs +65 -4
  65. package/foundation/config/project.test.mjs +66 -0
  66. package/foundation/discovery/docs-discovery.d.mts +185 -0
  67. package/foundation/discovery/docs-discovery.mjs +544 -0
  68. package/foundation/discovery/docs-discovery.test.mjs +341 -0
  69. package/foundation/integrations/integrations.d.mts +8 -6
  70. package/foundation/integrations/integrations.mjs +6 -4
  71. package/foundation/integrations/validate-contributions.mjs +30 -2
  72. package/package.json +9 -9
@@ -0,0 +1,12 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */
4
+ export const doc = {
5
+ type: 'page',
6
+ name: 'Service Monitoring Dashboard',
7
+ displayName: 'Service Monitoring Dashboard',
8
+ description:
9
+ 'Live-ops service-health dashboard: traffic-light KPI tiles with sparklines, and multi-line latency and request-volume charts with a 1h/1d/7d window, beside a triage rail split into two independently scrolling sections: active alerts over a worst-first per-service breakdown with inline status coloring. Global environment, region, and auto-refresh controls; the rail folds into the content column as two cards below 1024px.',
10
+ isReady: false,
11
+ category: 'Dashboard - Monitoring',
12
+ };
@@ -57,6 +57,8 @@ export const FunctionDocKindSchema: z.ZodObject<{
57
57
  /** New-format stamped generic reference/topic doc (`type: 'generic'`). */
58
58
  export const GenericDocKindSchema: z.ZodObject<{
59
59
  type: z.ZodLiteral<"generic">;
60
+ replaces: z.ZodOptional<z.ZodString>;
61
+ extends: z.ZodOptional<z.ZodString>;
60
62
  name: z.ZodString;
61
63
  displayName: z.ZodOptional<z.ZodString>;
62
64
  description: z.ZodOptional<z.ZodString>;
@@ -94,6 +94,11 @@ export const GenericDocKindSchema = z
94
94
  .object({
95
95
  ...BaseDocFields,
96
96
  type: z.literal('generic'),
97
+ // Declared rather than left to the passthrough: these two are read by
98
+ // docs discovery to resolve one topic against another, so a non-string
99
+ // should fail at the load boundary, not halfway through resolution.
100
+ replaces: z.string().optional(),
101
+ extends: z.string().optional(),
97
102
  })
98
103
  .passthrough();
99
104
 
@@ -51,6 +51,20 @@ export const doc = {
51
51
  type: 'string',
52
52
  description: "Navigation category: 'guide' or 'foundations'.",
53
53
  },
54
+ {
55
+ name: 'replaces',
56
+ type: 'string',
57
+ description:
58
+ "Name of an existing topic this doc takes the place of. Authored by an integration that serves its own guide instead of the built-in one: on a doc of the same name it swaps the content, and on a doc of another name it also leaves the old name as an alias so `astryx docs <old>` still resolves. Exclusive with `extends`.",
59
+ example: "'getting-started'",
60
+ },
61
+ {
62
+ name: 'extends',
63
+ type: 'string',
64
+ description:
65
+ 'Name of an existing topic this doc merges onto, section by section: a section whose title matches one in the base replaces it, a section the base does not have is appended. For correcting or adding to a topic rather than owning it. Exclusive with `replaces`.',
66
+ example: "'theme'",
67
+ },
54
68
  {
55
69
  name: 'sections',
56
70
  type: 'ReferenceSection[]',
@@ -68,6 +68,18 @@ export interface ReferenceDoc {
68
68
  description: string;
69
69
  /** Navigation category: 'guide' or 'foundations'. */
70
70
  category?: string;
71
+ /** Name of an existing topic this doc takes the place of. Authored by an
72
+ * integration whose guide should be served instead of the built-in one —
73
+ * `replaces: 'getting-started'` on a doc named `getting-started` swaps the
74
+ * content, and on a doc named something else swaps it and leaves the old
75
+ * name as an alias, so `astryx docs getting-started` still resolves.
76
+ * Ignored on a built-in topic (there is nothing above it to replace). */
77
+ replaces?: string;
78
+ /** Name of an existing topic this doc merges onto, section by section: a
79
+ * section whose title matches one in the base replaces it, and a section
80
+ * the base does not have is appended. For correcting or adding to a topic
81
+ * rather than owning it — `replaces` and `extends` are exclusive. */
82
+ extends?: string;
71
83
  /** Ordered sections that make up the doc. */
72
84
  sections: ReferenceSection[];
73
85
  /** Token category for foundational docs that map to a token section.
@@ -94,6 +94,8 @@ export type TemplateCategory =
94
94
  | 'Dashboard - Tabbed'
95
95
  | 'Dashboard - Filterable'
96
96
  | 'Dashboard - Portfolio'
97
+ | 'Dashboard - Project Status'
98
+ | 'Dashboard - Funnel & Cohort'
97
99
  // Table
98
100
  | 'Table - Basic'
99
101
  | 'Table - Grouped'
@@ -15,7 +15,8 @@ export const doc = {
15
15
  description:
16
16
  'The astryx.integration.* manifest that sits beside an integration ' +
17
17
  "package's package.json. Points the CLI at the package's components, " +
18
- 'templates, and codemods, and where to file issues. Every field is optional.',
18
+ 'templates, codemods, and doc topics, and where to file issues. Every ' +
19
+ 'field is optional.',
19
20
  appliesTo: 'astryx.integration.{ts,mjs,js}',
20
21
  fields: [
21
22
  {
@@ -38,6 +39,13 @@ export const doc = {
38
39
  description: 'Relative path to the codemods root (resolved to absolute).',
39
40
  example: "'./codemods'",
40
41
  },
42
+ {
43
+ name: 'docs',
44
+ type: 'string',
45
+ description:
46
+ 'Relative path to the reference-docs (topics) root (resolved to absolute). Every {topic}.doc.{ts,mjs,js} under it is served by `astryx docs` beside the built-in topics; a topic may also declare `replaces` or `extends` to take the place of a built-in one or merge onto it.',
47
+ example: "'./docs'",
48
+ },
41
49
  {
42
50
  name: 'issuesUrl',
43
51
  type: 'string',
@@ -52,6 +60,7 @@ export const doc = {
52
60
  components: './src/components',
53
61
  templates: './src/templates',
54
62
  codemods: './codemods',
63
+ docs: './docs',
55
64
  issuesUrl: 'https://github.com/acme/widgets/issues',
56
65
  };`,
57
66
  },
@@ -27,6 +27,7 @@ declare const integrationSchema: z.ZodObject<{
27
27
  components: z.ZodOptional<z.ZodString>;
28
28
  templates: z.ZodOptional<z.ZodString>;
29
29
  codemods: z.ZodOptional<z.ZodString>;
30
+ docs: z.ZodOptional<z.ZodString>;
30
31
  issuesUrl: z.ZodOptional<z.ZodString>;
31
32
  }, z.core.$strict>;
32
33
  export {};
@@ -16,6 +16,7 @@ const integrationSchema = z
16
16
  components: z.string().optional(),
17
17
  templates: z.string().optional(),
18
18
  codemods: z.string().optional(),
19
+ docs: z.string().optional(),
19
20
  issuesUrl: z.string().url().optional(),
20
21
  })
21
22
  .strict();
@@ -26,6 +26,7 @@ describe('parseIntegration (load boundary)', () => {
26
26
  expect(parseIntegration({components: './src'})).toEqual({
27
27
  components: './src',
28
28
  });
29
+ expect(parseIntegration({docs: './docs'})).toEqual({docs: './docs'});
29
30
  expect(() =>
30
31
  parseIntegration({components: './c', issuesUrl: 'https://example.com/i'}),
31
32
  ).not.toThrow();
@@ -14,6 +14,11 @@ export interface AstryxIntegration {
14
14
  templates?: string;
15
15
  /** Relative path to the codemods root (resolved to absolute). */
16
16
  codemods?: string;
17
+ /** Relative path to the reference-docs (topics) root (resolved to
18
+ * absolute). Every `{topic}.doc.{ts,mjs,js}` under it is a topic the CLI
19
+ * serves from `astryx docs`, alongside the built-in ones. A topic may also
20
+ * `replace` or `extend` a built-in topic; see the ReferenceDoc type. */
21
+ docs?: string;
17
22
  /** Where to file issues/feedback for this integration. */
18
23
  issuesUrl?: string;
19
24
  }
@@ -47,7 +47,12 @@ const INTEGRATION_ISSUES = 'https://example.com/meta/issues';
47
47
  * Returns the absolute `components` dir so the Project.load mock can hand back a
48
48
  * resolved integration entry.
49
49
  */
50
- function createFixture({withSource = true, extraComponent = null} = {}) {
50
+ function createFixture({
51
+ withSource = true,
52
+ extraComponent = null,
53
+ packageExports = null,
54
+ entryPoint = null,
55
+ } = {}) {
51
56
  const realCoreDir = path.resolve(import.meta.dirname, '..', '..', '..', '..', 'core');
52
57
  const coreDir = path.join(tmpDir, 'packages', 'core');
53
58
  fs.mkdirSync(path.dirname(coreDir), {recursive: true});
@@ -58,7 +63,11 @@ function createFixture({withSource = true, extraComponent = null} = {}) {
58
63
  fs.mkdirSync(compDir, {recursive: true});
59
64
  fs.writeFileSync(
60
65
  path.join(intDir, 'package.json'),
61
- JSON.stringify({name: INTEGRATION_NAME, version: '1.2.3'}),
66
+ JSON.stringify({
67
+ name: INTEGRATION_NAME,
68
+ version: '1.2.3',
69
+ ...(packageExports ? {exports: packageExports} : {}),
70
+ }),
62
71
  );
63
72
  fs.writeFileSync(
64
73
  path.join(compDir, 'MetaAppShell.doc.mjs'),
@@ -81,6 +90,20 @@ function createFixture({withSource = true, extraComponent = null} = {}) {
81
90
  );
82
91
  }
83
92
 
93
+ // A component whose directory is an entry point exporting several
94
+ // components, so the directory name and the component name differ.
95
+ if (entryPoint) {
96
+ const entryDir = path.join(compDir, entryPoint.directory);
97
+ fs.mkdirSync(entryDir, {recursive: true});
98
+ const ownSpecifier = entryPoint.importSpec
99
+ ? `\n import: '${entryPoint.importSpec}',`
100
+ : '';
101
+ fs.writeFileSync(
102
+ path.join(entryDir, `${entryPoint.component}.doc.mjs`),
103
+ `export const docs = {\n name: '${entryPoint.component}',${ownSpecifier}\n usage: { description: '${entryPoint.component} from an entry point.' },\n};\n`,
104
+ );
105
+ }
106
+
84
107
  const integration = {
85
108
  name: INTEGRATION_NAME,
86
109
  version: '1.2.3',
@@ -88,6 +111,7 @@ function createFixture({withSource = true, extraComponent = null} = {}) {
88
111
  templates: undefined,
89
112
  codemods: undefined,
90
113
  issuesUrl: INTEGRATION_ISSUES,
114
+ __packageDir: intDir,
91
115
  };
92
116
  projectLoadMock.mockResolvedValue({
93
117
  integrations: [INTEGRATION_NAME],
@@ -162,7 +186,9 @@ describe('component() — integration ownership via config', () => {
162
186
  expect(result.data.name).toBe('MetaAppShell');
163
187
  expect(result.data.package).toBe(INTEGRATION_NAME);
164
188
  expect(result.data.sourceAvailable).toBe(true);
165
- expect(result.data.import).toBe(`${INTEGRATION_NAME}/MetaAppShell`);
189
+ // This fixture declares no `exports`, so there is no subpath to import
190
+ // from and the specifier is the package root.
191
+ expect(result.data.import).toBe(INTEGRATION_NAME);
166
192
  });
167
193
 
168
194
  it('--package resolves the integration component', async () => {
@@ -217,6 +243,40 @@ describe('component() — integration ownership via config', () => {
217
243
  expect(result.data.package).toBe(INTEGRATION_NAME);
218
244
  });
219
245
 
246
+ it('resolves the import specifier against the package exports map', async () => {
247
+ // The doc sits in a `Toolbar` directory but the component is
248
+ // `ToolbarSearch`, so a specifier built from the component name would
249
+ // point at a subpath the package does not export.
250
+ createFixture({
251
+ packageExports: {'.': './index.js', './Toolbar': './components/Toolbar/index.js'},
252
+ entryPoint: {directory: 'Toolbar', component: 'ToolbarSearch'},
253
+ });
254
+ const result = await component('ToolbarSearch', {cwd: tmpDir});
255
+ expect(result.data.import).toBe(`${INTEGRATION_NAME}/Toolbar`);
256
+ });
257
+
258
+ it('falls back to the package root when the directory is not an exported subpath', async () => {
259
+ createFixture({
260
+ packageExports: {'.': './index.js'},
261
+ entryPoint: {directory: 'Toolbar', component: 'ToolbarSearch'},
262
+ });
263
+ const result = await component('ToolbarSearch', {cwd: tmpDir});
264
+ expect(result.data.import).toBe(INTEGRATION_NAME);
265
+ });
266
+
267
+ it('keeps a specifier the doc file states for itself', async () => {
268
+ createFixture({
269
+ packageExports: {'.': './index.js', './Toolbar': './components/Toolbar/index.js'},
270
+ entryPoint: {
271
+ directory: 'Toolbar',
272
+ component: 'ToolbarSearch',
273
+ importSpec: `${INTEGRATION_NAME}/Toolbar/Search`,
274
+ },
275
+ });
276
+ const result = await component('ToolbarSearch', {cwd: tmpDir});
277
+ expect(result.data.import).toBe(`${INTEGRATION_NAME}/Toolbar/Search`);
278
+ });
279
+
220
280
  it('JSON list includes integration components as {name, package} objects', async () => {
221
281
  createFixture();
222
282
  const result = await component(undefined, {cwd: tmpDir, list: true});
@@ -19,7 +19,7 @@ export const doc = {
19
19
  'Compiles a file that calls defineTheme() into a scoped CSS file, a JS module, and ' +
20
20
  'type declarations: the exact CSS the <Theme> runtime emits. Takes any number of theme ' +
21
21
  'files and compiles them in one process, in argument order, stopping at the first ' +
22
- 'failure an app with several themes does not need a shell loop. With --check it writes ' +
22
+ 'failure; an app with several themes does not need a shell loop. With --check it writes ' +
23
23
  'nothing and instead reports whether the committed outputs have drifted from source. ' +
24
24
  'When a separate build step emits the icon registry, --icons-specifier declares the ' +
25
25
  'fully specified module path that the generated JS should import.',
@@ -102,16 +102,28 @@ export function detectStylingSystem(targetDir: string): "stylex" | "tailwind" |
102
102
  * configured (see {@link detectStylingSystem}) so the agent never reaches for a
103
103
  * styling path that isn't compiled here.
104
104
  *
105
+ * `topics` is the project's doc-topic list. Passing it is what puts an
106
+ * integration's topics — including one it contributed in place of a built-in —
107
+ * in front of the agent by name; without it the block falls back to the CLI's
108
+ * own topics, because resolving a project's catalog is async and this is not.
109
+ *
110
+ * Either way the list now includes the hyphenated topics. The fallback scan
111
+ * matched `\w+`, which does not match `-`, so five real topics were missing
112
+ * from every block ever written — `getting-started` and `cli-integrations`
113
+ * among them. An agent cannot ask for a topic it was never told about, and
114
+ * `getting-started` is the one it should reach for first.
115
+ *
105
116
  * @param {string} version
106
- * @param {{coreDir?: string|null, invocation?: string, stylingSystem?: 'stylex'|'tailwind'|'css', zh?: boolean, lang?: string}} [options]
117
+ * @param {{coreDir?: string|null, invocation?: string, stylingSystem?: 'stylex'|'tailwind'|'css', zh?: boolean, lang?: string, topics?: string[]}} [options]
107
118
  * @returns {string}
108
119
  */
109
- export function generateCompressedIndex(version: string, { coreDir, invocation, stylingSystem }?: {
120
+ export function generateCompressedIndex(version: string, { coreDir, invocation, stylingSystem, topics }?: {
110
121
  coreDir?: string | null;
111
122
  invocation?: string;
112
123
  stylingSystem?: "stylex" | "tailwind" | "css";
113
124
  zh?: boolean;
114
125
  lang?: string;
126
+ topics?: string[];
115
127
  }): string;
116
128
  /**
117
129
  * Get Astryx version from core package.
@@ -186,12 +198,16 @@ export function removeAgentDocs(targetDir: string): void;
186
198
  * @param {string} [options.agent] - Tool preset: 'claude', 'cursor', 'codex', 'hermes', 'all'
187
199
  * @param {string[]} [options.paths] - Explicit paths (overrides agent/auto-detect)
188
200
  * @param {boolean} [options.onlyReplace] - Only update files that already have Astryx markers (for upgrades)
201
+ * @param {string[]} [options.topics] - Doc topics to list in the block; defaults
202
+ * to the CLI's own. Pass the project's catalog (`(await project.docs()).names()`)
203
+ * so an integration's topics reach the agent.
189
204
  * @returns {string[]} List of files written
190
205
  */
191
- export function installAgentDocs(targetDir: string, { zh, lang, agent, paths, onlyReplace }?: {
206
+ export function installAgentDocs(targetDir: string, { zh, lang, agent, paths, onlyReplace, topics }?: {
192
207
  zh?: boolean | undefined;
193
208
  lang?: string | undefined;
194
209
  agent?: string | undefined;
195
210
  paths?: string[] | undefined;
196
211
  onlyReplace?: boolean | undefined;
212
+ topics?: string[] | undefined;
197
213
  }): string[];
@@ -299,11 +299,22 @@ export function detectStylingSystem(targetDir) {
299
299
  * configured (see {@link detectStylingSystem}) so the agent never reaches for a
300
300
  * styling path that isn't compiled here.
301
301
  *
302
+ * `topics` is the project's doc-topic list. Passing it is what puts an
303
+ * integration's topics — including one it contributed in place of a built-in —
304
+ * in front of the agent by name; without it the block falls back to the CLI's
305
+ * own topics, because resolving a project's catalog is async and this is not.
306
+ *
307
+ * Either way the list now includes the hyphenated topics. The fallback scan
308
+ * matched `\w+`, which does not match `-`, so five real topics were missing
309
+ * from every block ever written — `getting-started` and `cli-integrations`
310
+ * among them. An agent cannot ask for a topic it was never told about, and
311
+ * `getting-started` is the one it should reach for first.
312
+ *
302
313
  * @param {string} version
303
- * @param {{coreDir?: string|null, invocation?: string, stylingSystem?: 'stylex'|'tailwind'|'css', zh?: boolean, lang?: string}} [options]
314
+ * @param {{coreDir?: string|null, invocation?: string, stylingSystem?: 'stylex'|'tailwind'|'css', zh?: boolean, lang?: string, topics?: string[]}} [options]
304
315
  * @returns {string}
305
316
  */
306
- export function generateCompressedIndex(version, {coreDir, invocation = getCliInvocation(), stylingSystem = 'css'} = {}) {
317
+ export function generateCompressedIndex(version, {coreDir, invocation = getCliInvocation(), stylingSystem = 'css', topics} = {}) {
307
318
  const run = invocation;
308
319
  const lines = [MARKER_START];
309
320
 
@@ -377,13 +388,18 @@ export function generateCompressedIndex(version, {coreDir, invocation = getCliIn
377
388
  lines.push(` component --list ${componentCount} components by category`);
378
389
  lines.push(' template --list page + block recipes');
379
390
  const docsDir = path.join(CLI_ROOT, 'assets', 'docs');
380
- if (fs.existsSync(docsDir)) {
381
- const topics = fs.readdirSync(docsDir)
382
- .map(f => f.match(/^(\w+)\.doc\.mjs$/))
383
- .filter(/** @returns {m is RegExpMatchArray} */ (m) => m != null)
384
- .map(m => m[1])
385
- .sort();
386
- if (topics.length > 0) lines.push(` docs <topic> ${topics.join(', ')}`);
391
+ const resolvedTopics =
392
+ topics ??
393
+ (fs.existsSync(docsDir)
394
+ ? fs
395
+ .readdirSync(docsDir)
396
+ .map(f => f.match(/^([\w-]+)\.doc\.mjs$/))
397
+ .filter(/** @returns {m is RegExpMatchArray} */ (m) => m != null)
398
+ .map(m => m[1])
399
+ .sort()
400
+ : []);
401
+ if (resolvedTopics.length > 0) {
402
+ lines.push(` docs <topic> ${resolvedTopics.join(', ')}`);
387
403
  }
388
404
  lines.push(' swizzle <Name> eject component source for deep customization');
389
405
  lines.push(' upgrade --apply run after any @astryxdesign/core bump');
@@ -566,14 +582,17 @@ export function removeAgentDocs(targetDir) {
566
582
  * @param {string} [options.agent] - Tool preset: 'claude', 'cursor', 'codex', 'hermes', 'all'
567
583
  * @param {string[]} [options.paths] - Explicit paths (overrides agent/auto-detect)
568
584
  * @param {boolean} [options.onlyReplace] - Only update files that already have Astryx markers (for upgrades)
585
+ * @param {string[]} [options.topics] - Doc topics to list in the block; defaults
586
+ * to the CLI's own. Pass the project's catalog (`(await project.docs()).names()`)
587
+ * so an integration's topics reach the agent.
569
588
  * @returns {string[]} List of files written
570
589
  */
571
- export function installAgentDocs(targetDir, {zh = false, lang, agent, paths, onlyReplace = false} = {}) {
590
+ export function installAgentDocs(targetDir, {zh = false, lang, agent, paths, onlyReplace = false, topics} = {}) {
572
591
  const coreDir = findCoreDir(targetDir);
573
592
  const version = getXdsVersion(coreDir);
574
593
  const invocation = getCliInvocation(targetDir);
575
594
  const stylingSystem = detectStylingSystem(targetDir);
576
- const compressedIndex = generateCompressedIndex(version, {coreDir, zh, lang, invocation, stylingSystem});
595
+ const compressedIndex = generateCompressedIndex(version, {coreDir, zh, lang, invocation, stylingSystem, topics});
577
596
  /** @type {string[]} */
578
597
  const written = [];
579
598
 
@@ -117,6 +117,44 @@ describe('generateCompressedIndex', () => {
117
117
  // The header defines the mapping; the bare "run every command as `npx astryx`" footgun must be absent.
118
118
  expect(result).not.toContain('npx astryx <cmd>');
119
119
  });
120
+
121
+ /** The `docs <topic> a, b, c` line of the block. */
122
+ function topicLine(block) {
123
+ return (
124
+ block.split('\n').find(line => line.trimStart().startsWith('docs <topic>')) ?? ''
125
+ );
126
+ }
127
+
128
+ it('names the hyphenated topics, which the scan used to drop', () => {
129
+ // The fallback scan matched `\w+`, which does not match `-`, so five real
130
+ // topics were missing from every block ever written. An agent cannot ask
131
+ // for a topic it was never told about, and `getting-started` — the one it
132
+ // should reach for first — was one of them.
133
+ const line = topicLine(generateCompressedIndex('1.0.0'));
134
+ for (const topic of [
135
+ 'getting-started',
136
+ 'cli-integrations',
137
+ 'browser-support',
138
+ 'styling-libraries',
139
+ 'working-with-ai',
140
+ ]) {
141
+ expect(line).toContain(topic);
142
+ }
143
+ });
144
+
145
+ it('lists the topics it is given, so an integration’s reach the agent', () => {
146
+ const line = topicLine(
147
+ generateCompressedIndex('1.0.0', {topics: ['tokens', 'deploying']}),
148
+ );
149
+ expect(line).toContain('tokens, deploying');
150
+ // The project's catalog replaces the built-in scan rather than adding to it:
151
+ // a topic an integration replaced must not also be listed under its old name.
152
+ expect(line).not.toContain('typography');
153
+ });
154
+
155
+ it('omits the line entirely when the project has no topics', () => {
156
+ expect(topicLine(generateCompressedIndex('1.0.0', {topics: []}))).toBe('');
157
+ });
120
158
  });
121
159
 
122
160
  describe('detectStylingSystem', () => {
@@ -99,6 +99,21 @@ export class Project {
99
99
  * @returns {Promise<Array<object>>}
100
100
  */
101
101
  templates(): Promise<Array<object>>;
102
+ /**
103
+ * The CLI's own doc topics plus the ones the configured integrations
104
+ * contribute, resolved into one catalog: additions, replacements (with the
105
+ * replaced name left as an alias), and extensions merged in configuration
106
+ * order. Same skip+warn policy as its siblings — a broken integration
107
+ * contributes no topics and its issues are collected. Memoized per instance.
108
+ *
109
+ * Two problems can only be seen with every integration in hand, so they are
110
+ * raised here rather than in the per-integration validators: a topic that
111
+ * collides with one another package already provides, and a `replaces` /
112
+ * `extends` that names a topic nothing provides.
113
+ *
114
+ * @returns {Promise<DocsCatalog>}
115
+ */
116
+ docs(): Promise<DocsCatalog>;
102
117
  /**
103
118
  * Core registry transforms + integration codemods for an upgrade range.
104
119
  * Wraps getTransformsBetween (core) and discoverIntegrationCodemods /
@@ -152,3 +167,4 @@ export class Project {
152
167
  export type ProjectIntegrationIssue = import("../integrations/issue").AstryxIntegrationIssue & {
153
168
  package: string;
154
169
  };
170
+ import { DocsCatalog } from '../discovery/docs-discovery.mjs';
@@ -6,16 +6,16 @@
6
6
  * `Project` is the one entry point a command uses to read everything it needs
7
7
  * about a consumer's project: the validated config surface, the configured
8
8
  * integrations, and the resolved discovery sets (components, templates,
9
- * codemods) — plus issue routing (issuesUrl) and the accumulated integration
10
- * issues. It replaces the old `loadConfig(cwd)` plain-object loader and the
11
- * per-command fan-out into the various discovery helpers.
9
+ * codemods, docs) — plus issue routing (issuesUrl) and the accumulated
10
+ * integration issues. It replaces the old `loadConfig(cwd)` plain-object loader
11
+ * and the per-command fan-out into the various discovery helpers.
12
12
  *
13
13
  * Design:
14
14
  * - `Project.load(cwd, {cache})` is the async factory (constructors can't be
15
15
  * async). It does what loadConfig did — find the config sibling-of
16
16
  * package.json, import + validate it, load the configured integrations —
17
17
  * and nothing more. Discovery is LAZY.
18
- * - Discovery methods (components/templates/codemods) are MEMOIZED per
18
+ * - Discovery methods (components/templates/codemods/docs) are MEMOIZED per
19
19
  * instance (via the pluggable cache) and orchestrate the EXISTING discovery
20
20
  * functions — Project never reimplements discovery.
21
21
  * - SKIP + WARN policy: as a discovery method runs, per-integration work is
@@ -45,6 +45,10 @@ import {
45
45
  discoverAll as discoverTemplates,
46
46
  discoverIntegrationTemplatesForOne,
47
47
  } from '../discovery/template-adapter.mjs';
48
+ import {
49
+ DocsCatalog,
50
+ discoverIntegrationDocs,
51
+ } from '../discovery/docs-discovery.mjs';
48
52
  import {getTransformsBetween} from '../../assets/codemods/registry.mjs';
49
53
  import {
50
54
  discoverIntegrationCodemods,
@@ -454,6 +458,63 @@ export class Project {
454
458
  });
455
459
  }
456
460
 
461
+ /**
462
+ * The CLI's own doc topics plus the ones the configured integrations
463
+ * contribute, resolved into one catalog: additions, replacements (with the
464
+ * replaced name left as an alias), and extensions merged in configuration
465
+ * order. Same skip+warn policy as its siblings — a broken integration
466
+ * contributes no topics and its issues are collected. Memoized per instance.
467
+ *
468
+ * Two problems can only be seen with every integration in hand, so they are
469
+ * raised here rather than in the per-integration validators: a topic that
470
+ * collides with one another package already provides, and a `replaces` /
471
+ * `extends` that names a topic nothing provides.
472
+ *
473
+ * @returns {Promise<DocsCatalog>}
474
+ */
475
+ async docs() {
476
+ return this.#memo('docs', async () => {
477
+ const catalog = DocsCatalog.fromBuiltins();
478
+
479
+ for (const integration of this.#loadedIntegrations) {
480
+ await this.#collectIssues(integration);
481
+ const pkg = this.#pkgLabel(integration);
482
+ const hadError = this.#issues.some(
483
+ i => i.package === pkg && i.severity === 'error',
484
+ );
485
+ if (hadError) continue;
486
+ if (!integration?.docs) continue;
487
+ try {
488
+ const {records, errors} = await discoverIntegrationDocs(integration);
489
+ for (const e of errors) {
490
+ this.#pushIssue(pkg, {
491
+ code: 'invalid_doc',
492
+ severity: 'error',
493
+ message: e.message,
494
+ });
495
+ }
496
+ // Any unusable doc withdraws the whole package's topics, matching
497
+ // how a bad template withdraws its package's templates: a partial
498
+ // docs contribution is the state where a `replaces` silently does
499
+ // nothing and a reader gets the topic it was meant to replace.
500
+ if (errors.length > 0) continue;
501
+ for (const record of records) {
502
+ const issue = catalog.add(record);
503
+ if (issue) this.#pushIssue(pkg, issue);
504
+ }
505
+ } catch (err) {
506
+ this.#pushIssue(pkg, {
507
+ code: 'invalid_doc',
508
+ severity: 'error',
509
+ message: errorMessage(err),
510
+ });
511
+ }
512
+ }
513
+
514
+ return catalog;
515
+ });
516
+ }
517
+
457
518
  /**
458
519
  * Core registry transforms + integration codemods for an upgrade range.
459
520
  * Wraps getTransformsBetween (core) and discoverIntegrationCodemods /