@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,208 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file End-to-end tests for integration-contributed doc topics: a scaffolded
5
+ * consumer project whose configured integration ships a `docs` root, read back
6
+ * through the public surfaces — `docs()`, `search()`, and the agent-docs block.
7
+ *
8
+ * The unit-level rules (what a docs root contributes, what the catalog does
9
+ * with `replaces`/`extends`) are covered in
10
+ * foundation/discovery/docs-discovery.test.mjs. What is pinned here is that a
11
+ * contributed topic is indistinguishable from a built-in one at the surfaces
12
+ * an agent actually reads.
13
+ *
14
+ * Fixtures live under a repo-local temp dir, not /tmp, because Vite refuses to
15
+ * dynamically import a module from outside the project root.
16
+ */
17
+
18
+ import {afterEach, beforeEach, describe, expect, it} from 'vitest';
19
+ import * as fs from 'node:fs';
20
+ import * as path from 'node:path';
21
+ import {docs} from './docs.mjs';
22
+ import {search} from '../search/search.mjs';
23
+ import {loadDocsCatalog} from './_adapter.mjs';
24
+ import {AstryxError} from '../error.mjs';
25
+
26
+ const SLOW = 30_000;
27
+
28
+ let tmpDir;
29
+
30
+ /** A minimal, valid topic. */
31
+ function topic(fields) {
32
+ return {
33
+ type: 'generic',
34
+ name: 'deploying',
35
+ title: 'Deploying',
36
+ description: 'How to ship an app built with Acme widgets.',
37
+ category: 'guide',
38
+ sections: [
39
+ {title: 'Overview', content: [{type: 'prose', text: 'Push the button.'}]},
40
+ ],
41
+ ...fields,
42
+ };
43
+ }
44
+
45
+ /**
46
+ * A consumer project that configures one integration, optionally with a docs
47
+ * root holding the given topics.
48
+ * @param {Record<string, object|string>} [topics] file name → doc
49
+ * @param {{config?: string}} [options]
50
+ */
51
+ function scaffold(topics, {config} = {}) {
52
+ fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify({name: 'consumer'}));
53
+ fs.writeFileSync(
54
+ path.join(tmpDir, 'astryx.config.mjs'),
55
+ config ?? "export default {integrations: ['@acme/widgets']};\n",
56
+ );
57
+
58
+ const pkgDir = path.join(tmpDir, 'node_modules', '@acme', 'widgets');
59
+ fs.mkdirSync(pkgDir, {recursive: true});
60
+ fs.writeFileSync(
61
+ path.join(pkgDir, 'package.json'),
62
+ JSON.stringify({name: '@acme/widgets', version: '1.0.0'}),
63
+ );
64
+ fs.writeFileSync(
65
+ path.join(pkgDir, 'astryx.integration.mjs'),
66
+ `export default ${JSON.stringify(topics ? {docs: './docs'} : {})};\n`,
67
+ );
68
+
69
+ if (topics) {
70
+ const docsDir = path.join(pkgDir, 'docs');
71
+ fs.mkdirSync(docsDir, {recursive: true});
72
+ for (const [file, doc] of Object.entries(topics)) {
73
+ fs.writeFileSync(
74
+ path.join(docsDir, file),
75
+ typeof doc === 'string'
76
+ ? doc
77
+ : `export const docs = ${JSON.stringify(doc, null, 2)};\n`,
78
+ );
79
+ }
80
+ }
81
+ }
82
+
83
+ beforeEach(() => {
84
+ tmpDir = fs.mkdtempSync(path.join(process.cwd(), '.astryx-integration-docs-test-'));
85
+ });
86
+
87
+ afterEach(() => {
88
+ fs.rmSync(tmpDir, {recursive: true, force: true});
89
+ });
90
+
91
+ describe('integration-contributed topics', () => {
92
+ it('lists and reads like a built-in topic, naming its owner', async () => {
93
+ scaffold({'deploying.doc.mjs': topic()});
94
+
95
+ const listed = await docs(undefined, undefined, {cwd: tmpDir});
96
+ const entry = listed.data.find(t => t.topic === 'deploying');
97
+ expect(entry).toMatchObject({
98
+ topic: 'deploying',
99
+ description: 'How to ship an app built with Acme widgets.',
100
+ package: '@acme/widgets',
101
+ });
102
+ // The built-in topics keep their own owner.
103
+ expect(listed.data.find(t => t.topic === 'tokens').package).toBe('@astryxdesign/cli');
104
+
105
+ const detail = await docs('deploying', undefined, {cwd: tmpDir});
106
+ expect(detail.type).toBe('docs.detail');
107
+ expect(detail.data.sections[0].content[0].text).toBe('Push the button.');
108
+
109
+ const section = await docs('deploying', 'overview', {cwd: tmpDir});
110
+ expect(section.data.title).toBe('Overview');
111
+ }, SLOW);
112
+
113
+ it('is invisible to a project that does not configure the integration', async () => {
114
+ scaffold({'deploying.doc.mjs': topic()}, {config: 'export default {};\n'});
115
+ const listed = await docs(undefined, undefined, {cwd: tmpDir});
116
+ expect(listed.data.some(t => t.topic === 'deploying')).toBe(false);
117
+ }, SLOW);
118
+
119
+ it('serves the replacement of a built-in topic, and says what it replaced', async () => {
120
+ scaffold({
121
+ 'getting-started.doc.mjs': topic({
122
+ name: 'getting-started',
123
+ replaces: 'getting-started',
124
+ title: 'Getting started',
125
+ description: 'Install Acme widgets.',
126
+ sections: [
127
+ {title: 'Install', content: [{type: 'prose', text: 'yarn add @acme/widgets'}]},
128
+ ],
129
+ }),
130
+ });
131
+
132
+ const detail = await docs('getting-started', undefined, {cwd: tmpDir});
133
+ expect(detail.data.sections.map(s => s.title)).toEqual(['Install']);
134
+ expect(detail.data.sections[0].content[0].text).toBe('yarn add @acme/widgets');
135
+
136
+ const listed = await docs(undefined, undefined, {cwd: tmpDir});
137
+ const entries = listed.data.filter(t => t.topic === 'getting-started');
138
+ expect(entries).toHaveLength(1);
139
+ expect(entries[0]).toMatchObject({
140
+ package: '@acme/widgets',
141
+ replaces: 'getting-started',
142
+ });
143
+ }, SLOW);
144
+
145
+ it('keeps the replaced name resolving when the replacement renames it', async () => {
146
+ scaffold({
147
+ 'setup.doc.mjs': topic({name: 'setup', replaces: 'getting-started'}),
148
+ });
149
+ const byOldName = await docs('getting-started', undefined, {cwd: tmpDir});
150
+ const byNewName = await docs('setup', undefined, {cwd: tmpDir});
151
+ expect(byOldName.data.name).toBe('setup');
152
+ expect(byNewName.data.name).toBe('setup');
153
+ }, SLOW);
154
+
155
+ it('merges an extension into the topic it extends', async () => {
156
+ const builtin = await docs('theme');
157
+ const baseSectionTitle = builtin.data.sections[0].title;
158
+ scaffold({
159
+ 'theme-internal.doc.mjs': topic({
160
+ name: 'theme-internal',
161
+ extends: 'theme',
162
+ sections: [
163
+ {title: baseSectionTitle, content: [{type: 'prose', text: 'Use the Acme theme.'}]},
164
+ {title: 'Acme themes', content: [{type: 'prose', text: 'Three of them.'}]},
165
+ ],
166
+ }),
167
+ });
168
+
169
+ const extended = await docs('theme', undefined, {cwd: tmpDir});
170
+ // The extension is not a topic of its own.
171
+ expect((await docs(undefined, undefined, {cwd: tmpDir})).data.some(
172
+ t => t.topic === 'theme-internal',
173
+ )).toBe(false);
174
+ expect(extended.data.sections[0].content[0].text).toBe('Use the Acme theme.');
175
+ expect(extended.data.sections.at(-1).title).toBe('Acme themes');
176
+ // Everything the extension did not name is still the base doc's.
177
+ expect(extended.data.sections.length).toBe(builtin.data.sections.length + 1);
178
+ }, SLOW);
179
+
180
+ it('offers the contributed topics as suggestions on an unknown one', async () => {
181
+ scaffold({'deploying.doc.mjs': topic()});
182
+ await expect(docs('nope-not-a-topic', undefined, {cwd: tmpDir})).rejects.toBeInstanceOf(
183
+ AstryxError,
184
+ );
185
+ try {
186
+ await docs('nope-not-a-topic', undefined, {cwd: tmpDir});
187
+ } catch (err) {
188
+ expect(err.suggestions.map(s => s.name)).toContain('deploying');
189
+ }
190
+ }, SLOW);
191
+
192
+ it('indexes a contributed topic in search', async () => {
193
+ scaffold({'deploying.doc.mjs': topic()});
194
+ const {data} = await search('deploying', {cwd: tmpDir, type: 'doc'});
195
+ expect(data.results[0]).toMatchObject({
196
+ domain: 'doc',
197
+ name: 'deploying',
198
+ command: 'astryx docs deploying',
199
+ });
200
+ }, SLOW);
201
+
202
+ it("falls back to the CLI's own topics when the project config is unreadable", async () => {
203
+ scaffold({'deploying.doc.mjs': topic()}, {config: 'export default {integrations: 42};\n'});
204
+ const catalog = await loadDocsCatalog(tmpDir);
205
+ expect(catalog.resolve('tokens')).toBeTruthy();
206
+ expect(catalog.resolve('deploying')).toBeUndefined();
207
+ }, SLOW);
208
+ });
@@ -2,6 +2,10 @@
2
2
  // DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
3
3
 
4
4
  /**
5
+ * @param {object} [options]
6
+ * @param {string} [options.cwd]
5
7
  * @returns {Promise<import('../docs.type.mjs').DocsListResponse>}
6
8
  */
7
- export function list(): Promise<import("../docs.type.mjs").DocsListResponse>;
9
+ export function list({ cwd }?: {
10
+ cwd?: string | undefined;
11
+ }): Promise<import("../docs.type.mjs").DocsListResponse>;
@@ -3,31 +3,47 @@
3
3
  /**
4
4
  * @file docs.list leaf — enumerate the available reference-doc topics.
5
5
  *
6
- * @input Reads packages/cli/assets/docs/{topic}.doc.mjs via the shared adapter's
7
- * topic discovery. Each topic's English `description` is read directly; the
8
- * listing never applies --dense/--zh overlays.
9
- * @output { type: 'docs.list', data: DocsListEntry[] } one entry per topic,
10
- * in filesystem-discovery order, matching `xds --json docs`.
6
+ * @input The project's doc catalog (built-in topics plus the ones configured
7
+ * integrations contribute), via the shared adapter. A built-in topic's
8
+ * English `description` is read from its file; a contributed topic already
9
+ * carries the one discovery read. The listing never applies --dense/--zh
10
+ * overlays.
11
+ * @output { type: 'docs.list', data: DocsListEntry[] } — one entry per topic in
12
+ * read order, each naming the package that owns it, matching
13
+ * `astryx --json docs`.
11
14
  * @position Leaf under api/docs. Sibling of detail; both share _adapter.mjs.
12
15
  */
13
16
 
14
17
  import {pathToFileURL} from 'node:url';
15
- import {discoverTopics} from '../_adapter.mjs';
18
+ import {loadDocsCatalog} from '../_adapter.mjs';
16
19
 
17
20
  /**
21
+ * @param {object} [options]
22
+ * @param {string} [options.cwd]
18
23
  * @returns {Promise<import('../docs.type.mjs').DocsListResponse>}
19
24
  */
20
- export async function list() {
21
- const topics = discoverTopics();
25
+ export async function list({cwd} = {}) {
26
+ const catalog = await loadDocsCatalog(cwd);
22
27
  /** @type {Array<import('../docs.type.mjs').DocsListEntry>} */
23
28
  const entries = [];
24
- for (const [name, docPath] of Object.entries(topics)) {
25
- try {
26
- const mod = await import(pathToFileURL(docPath).href);
27
- entries.push({topic: name, description: mod.docs.description});
28
- } catch {
29
- entries.push({topic: name, description: ''});
29
+ for (const entry of catalog.entries()) {
30
+ let description = entry.description ?? '';
31
+ if (entry.description == null) {
32
+ try {
33
+ const mod = await import(pathToFileURL(entry.path).href);
34
+ description = (mod.docs ?? mod.default)?.description ?? '';
35
+ } catch {
36
+ description = '';
37
+ }
30
38
  }
39
+ /** @type {import('../docs.type.mjs').DocsListEntry} */
40
+ const listed = {
41
+ topic: entry.name,
42
+ description,
43
+ package: entry.package,
44
+ };
45
+ if (entry.replaces != null) listed.replaces = entry.replaces;
46
+ entries.push(listed);
31
47
  }
32
48
  return {type: 'docs.list', data: entries};
33
49
  }
@@ -20,6 +20,7 @@ import {CLI_ROOT} from '../../../foundation/fs/paths.mjs';
20
20
  import {PathSafetyError} from '../../../foundation/fs/path-safety.mjs';
21
21
  import {getCliInvocation} from '../../../foundation/env/package-manager.mjs';
22
22
  import {installAgentDocs} from '../../../foundation/agent-docs/agent-docs.mjs';
23
+ import {loadDocsCatalog} from '../../docs/_adapter.mjs';
23
24
  import {themeTemplate} from '../../theme/template/template.mjs';
24
25
  import {listTemplates} from '../../template/template.mjs';
25
26
  import {AstryxError} from '../../error.mjs';
@@ -71,7 +72,7 @@ export function getNextSteps(invocation) {
71
72
  * @param {string} invocation
72
73
  * @param {import('../init.type.mjs').InitRunData} data
73
74
  */
74
- function applyAgents(cwd, options, invocation, data) {
75
+ async function applyAgents(cwd, options, invocation, data) {
75
76
  // Validate --agent up front (a hard error, not a swallowed install failure).
76
77
  // ERR_UNKNOWN_AGENT was defined but never wired — a typo like `--agent claud`
77
78
  // otherwise silently fell back to writing AGENTS.md. Mirrors --features.
@@ -88,7 +89,11 @@ function applyAgents(cwd, options, invocation, data) {
88
89
  ? options.agentDocsPath
89
90
  : [options.agentDocsPath]
90
91
  : undefined;
91
- const written = installAgentDocs(cwd, {agent: options.agent, paths});
92
+ // The block names the topics the agent can read, and an integration's
93
+ // topics are part of that set — resolved here rather than inside
94
+ // installAgentDocs, which is sync and cannot load a project.
95
+ const topics = (await loadDocsCatalog(cwd)).names();
96
+ const written = installAgentDocs(cwd, {agent: options.agent, paths, topics});
92
97
  data.docsWritten = written;
93
98
  logger.log(`✓ AI agent docs installed → ${written.join(', ')}`);
94
99
  } catch (err) {
@@ -248,7 +253,7 @@ export async function run(options = {}, {cwd = process.cwd()} = {}) {
248
253
  nextSteps: false,
249
254
  };
250
255
  for (const feature of features) {
251
- if (feature === 'agents') applyAgents(cwd, options, invocation, data);
256
+ if (feature === 'agents') await applyAgents(cwd, options, invocation, data);
252
257
  if (feature === 'theme') applyTheme(cwd, invocation, data);
253
258
  if (feature === 'template') {
254
259
  applyTemplate(cwd, {templateName: options.templateName}, invocation, data);
@@ -272,7 +277,7 @@ export async function run(options = {}, {cwd = process.cwd()} = {}) {
272
277
  templatePath: null,
273
278
  nextSteps: true,
274
279
  };
275
- applyAgents(cwd, options, invocation, data);
280
+ await applyAgents(cwd, options, invocation, data);
276
281
  logger.log('');
277
282
  logger.log(
278
283
  ` Tip: \`${invocation} init --all\` also points you to the theme and page-building workflows.`,
@@ -18,7 +18,7 @@
18
18
  * error) so `validate-integration` can stay exit-0 in a non-integration dir.
19
19
  *
20
20
  * The on-disk contribution validators themselves (roots + codemods/templates/
21
- * components, behind `validateLoadedIntegration`) live in
21
+ * components/docs, behind `validateLoadedIntegration`) live in
22
22
  * `foundation/integrations/validate-contributions.mjs`, because foundation also
23
23
  * runs them: `Project` collects integration issues and `integration-warnings`
24
24
  * nudges about them on ordinary commands. This file re-exports
@@ -158,6 +158,7 @@ async function validateAtPackageDir(packageDir, identity) {
158
158
  components: resolveRoot(manifest.components),
159
159
  templates: resolveRoot(manifest.templates),
160
160
  codemods: resolveRoot(manifest.codemods),
161
+ docs: resolveRoot(manifest.docs),
161
162
  issuesUrl: manifest.issuesUrl,
162
163
  __spec: identity.name,
163
164
  __packageDir: packageDir,
@@ -12,6 +12,7 @@ export type LoadedIntegration = {
12
12
  components?: string | undefined;
13
13
  templates?: string | undefined;
14
14
  codemods?: string | undefined;
15
+ docs?: string | undefined;
15
16
  issuesUrl?: string | undefined;
16
17
  __spec: string;
17
18
  __packageDir: string;
@@ -17,6 +17,7 @@
17
17
  * @property {string} [components]
18
18
  * @property {string} [templates]
19
19
  * @property {string} [codemods]
20
+ * @property {string} [docs]
20
21
  * @property {string} [issuesUrl]
21
22
  * @property {string} __spec
22
23
  * @property {string} __packageDir
@@ -30,10 +30,8 @@
30
30
  * sorts above an incidental mention.
31
31
  */
32
32
 
33
- import * as fs from 'node:fs';
34
- import * as path from 'node:path';
35
33
  import {pathToFileURL} from 'node:url';
36
- import {findCoreDir, CLI_ROOT} from '../../foundation/fs/paths.mjs';
34
+ import {findCoreDir} from '../../foundation/fs/paths.mjs';
37
35
  import {
38
36
  discoverComponents,
39
37
  findComponentReadme,
@@ -42,11 +40,10 @@ import {
42
40
  import {discoverHooks, findHookDoc} from '../../foundation/discovery/hook-discovery.mjs';
43
41
  import {levenshteinDistance} from '../../foundation/text/string-utils.mjs';
44
42
  import {discoverTemplates, extractComponents} from '../template/template.mjs';
43
+ import {loadDocsCatalog, loadTopicDoc} from '../docs/_adapter.mjs';
45
44
  import {AstryxError} from '../error.mjs';
46
45
  import {ERROR_CODES} from '../../foundation/response/error-codes.mjs';
47
46
 
48
- const DOCS_DIR = path.join(CLI_ROOT, 'assets', 'docs');
49
-
50
47
  /**
51
48
  * A search candidate gathered from one content domain. Extra underscore-
52
49
  * prefixed fields carry domain-specific payload used only by {@link toResult}.
@@ -424,17 +421,31 @@ async function gatherHooks(coreDir) {
424
421
 
425
422
  /**
426
423
  * Build doc-topic candidates: topic name + description + section prose.
424
+ *
425
+ * Reads the project's catalog rather than the CLI's own docs directory, so a
426
+ * topic an integration contributed (or replaced) is searchable exactly like a
427
+ * built-in one — otherwise the replacement is served by `astryx docs` but
428
+ * invisible to the command whose job is finding it.
429
+ * @param {string} cwd
427
430
  * @returns {Promise<Candidate[]>}
428
431
  */
429
- async function gatherDocs() {
430
- if (!fs.existsSync(DOCS_DIR)) return [];
432
+ async function gatherDocs(cwd) {
431
433
  /** @type {Candidate[]} */
432
434
  const candidates = [];
433
- for (const file of fs.readdirSync(DOCS_DIR)) {
434
- const match = file.match(/^([\w-]+)\.doc\.mjs$/);
435
- if (!match) continue;
436
- const topic = match[1];
437
- const doc = await loadModuleDoc(path.join(DOCS_DIR, file));
435
+ let entries;
436
+ try {
437
+ entries = (await loadDocsCatalog(cwd)).entries();
438
+ } catch {
439
+ return candidates;
440
+ }
441
+ for (const entry of entries) {
442
+ let doc = null;
443
+ try {
444
+ doc = await loadTopicDoc(entry);
445
+ } catch {
446
+ // A topic that cannot be loaded is reported by the commands that own
447
+ // integration issues; search just cannot index it.
448
+ }
438
449
  let description = '';
439
450
  /** @type {string[]} */
440
451
  const prose = [];
@@ -449,11 +460,11 @@ async function gatherDocs() {
449
460
  }
450
461
  candidates.push({
451
462
  domain: 'doc',
452
- name: topic,
463
+ name: entry.name,
453
464
  keywords: [],
454
465
  description,
455
466
  prose,
456
- _title: doc?.title || topic,
467
+ _title: doc?.title || entry.title || entry.name,
457
468
  });
458
469
  }
459
470
  return candidates;
@@ -602,7 +613,7 @@ export async function search(query, options = {}) {
602
613
  const [components, hooks, docTopics, templates] = await Promise.all([
603
614
  wants('component') ? gatherComponents(coreDir) : [],
604
615
  wants('hook') ? gatherHooks(coreDir) : [],
605
- wants('doc') ? gatherDocs() : [],
616
+ wants('doc') ? gatherDocs(cwd) : [],
606
617
  wants('template') ? gatherTemplates(cwd) : [],
607
618
  ]);
608
619
 
@@ -52,13 +52,13 @@ export function runPostCodemodHooks(hooks: import("../../authoring/config/type")
52
52
  * EVERY upgrade path, including the no-codemods short-circuits (#4168).
53
53
  *
54
54
  * @param {{cwd: string, installedVersion: string, apply: boolean}} ctx
55
- * @returns {import('./upgrade.type.mjs').AgentDocsSummary}
55
+ * @returns {Promise<import('./upgrade.type.mjs').AgentDocsSummary>}
56
56
  */
57
57
  export function refreshAgentDocs({ cwd, installedVersion, apply }: {
58
58
  cwd: string;
59
59
  installedVersion: string;
60
60
  apply: boolean;
61
- }): import("./upgrade.type.mjs").AgentDocsSummary;
61
+ }): Promise<import("./upgrade.type.mjs").AgentDocsSummary>;
62
62
  /**
63
63
  * Every registered codemod (oldest→newest) for `upgrade --list`. Registry walk
64
64
  * + flatten; nothing is run.
@@ -27,6 +27,7 @@ import {
27
27
  } from '../../assets/codemods/integration-discovery.mjs';
28
28
  import {runIntegrationCodemods} from '../../assets/codemods/integration-runner.mjs';
29
29
  import {installAgentDocs, inspectAgentDocs} from '../../foundation/agent-docs/agent-docs.mjs';
30
+ import {loadDocsCatalog} from '../docs/_adapter.mjs';
30
31
  import {formatCliCommand} from '../../foundation/env/package-manager.mjs';
31
32
  import {Project} from '../../foundation/config/project.mjs';
32
33
  import {loadIntegrations} from '../../foundation/integrations/integrations.mjs';
@@ -145,9 +146,9 @@ export async function runPostCodemodHooks(hooks, context) {
145
146
  * EVERY upgrade path, including the no-codemods short-circuits (#4168).
146
147
  *
147
148
  * @param {{cwd: string, installedVersion: string, apply: boolean}} ctx
148
- * @returns {import('./upgrade.type.mjs').AgentDocsSummary}
149
+ * @returns {Promise<import('./upgrade.type.mjs').AgentDocsSummary>}
149
150
  */
150
- export function refreshAgentDocs({cwd, installedVersion, apply}) {
151
+ export async function refreshAgentDocs({cwd, installedVersion, apply}) {
151
152
  const inspection = inspectAgentDocs(cwd, installedVersion);
152
153
  /** @type {import('./upgrade.type.mjs').AgentDocsSummary} */
153
154
  const summary = {
@@ -186,7 +187,10 @@ export function refreshAgentDocs({cwd, installedVersion, apply}) {
186
187
 
187
188
  // Apply: rewrite only files that already carry a marker (onlyReplace).
188
189
  try {
189
- const written = installAgentDocs(cwd, {onlyReplace: true});
190
+ const written = installAgentDocs(cwd, {
191
+ onlyReplace: true,
192
+ topics: (await loadDocsCatalog(cwd)).names(),
193
+ });
190
194
  summary.refreshed = written.length > 0;
191
195
  summary.files = written;
192
196
  if (summary.refreshed) {
@@ -90,7 +90,7 @@ export async function run(options = {}, {cwd = process.cwd()} = {}) {
90
90
 
91
91
  // Sync the managed agent-docs block FIRST — it documents the installed library
92
92
  // independent of codemods, so refresh on every path (issue #4168).
93
- const agentDocs = refreshAgentDocs({cwd, installedVersion: targetVersion, apply: apply || false});
93
+ const agentDocs = await refreshAgentDocs({cwd, installedVersion: targetVersion, apply: apply || false});
94
94
 
95
95
  if (!options.force && semverGte(currentVersion, targetVersion)) {
96
96
  return statusUpToDate({from: currentVersion, to: targetVersion, agentDocs});