@astryxdesign/cli 0.6.3-canary.7ada42e → 0.6.3-canary.7ec7be0

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 (221) hide show
  1. package/README.md +21 -19
  2. package/api/build/_adapter.d.mts +50 -0
  3. package/api/build/_adapter.mjs +60 -0
  4. package/api/build/build.doc.mjs +13 -7
  5. package/api/build/build.test.mjs +184 -6
  6. package/api/build/build.type.d.mts +57 -2
  7. package/api/build/build.type.mjs +30 -7
  8. package/api/build/help/help.d.mts +4 -0
  9. package/api/build/help/help.mjs +24 -11
  10. package/api/build/kit/kit.d.mts +4 -1
  11. package/api/build/kit/kit.mjs +165 -49
  12. package/api/build/kit/rank.d.mts +44 -0
  13. package/api/build/kit/rank.mjs +432 -0
  14. package/api/build/kit/rank.test.mjs +196 -0
  15. package/api/component/_adapter.d.mts +6 -12
  16. package/api/component/_adapter.mjs +20 -10
  17. package/api/component/component.mjs +60 -12
  18. package/api/component/list/list.mjs +3 -2
  19. package/api/docs/_adapter.d.mts +251 -28
  20. package/api/docs/_adapter.mjs +918 -35
  21. package/api/docs/detail/detail.mjs +11 -3
  22. package/api/docs/detail/section/section.mjs +24 -13
  23. package/api/docs/detail/section/section.test.mjs +15 -6
  24. package/api/docs/docs.d.mts +5 -3
  25. package/api/docs/docs.doc.mjs +38 -17
  26. package/api/docs/docs.mjs +44 -8
  27. package/api/docs/docs.test.mjs +158 -4
  28. package/api/docs/docs.type.d.mts +181 -2
  29. package/api/docs/docs.type.mjs +117 -3
  30. package/api/docs/index/index.mjs +11 -3
  31. package/api/docs/index/index.test.mjs +1 -1
  32. package/api/docs/integration-tree.test.mjs +555 -0
  33. package/api/docs/integrationDocs.test.mjs +14 -14
  34. package/api/docs/list/list.mjs +25 -5
  35. package/api/docs/node/node.d.mts +43 -0
  36. package/api/docs/node/node.mjs +192 -0
  37. package/api/docs/reference-blocks.test.mjs +406 -0
  38. package/api/doctor/doctor.d.mts +30 -2
  39. package/api/doctor/doctor.mjs +151 -8
  40. package/api/doctor/doctor.test.mjs +95 -7
  41. package/api/index.d.mts +1 -1
  42. package/api/index.mjs +1 -0
  43. package/api/init/init.doc.mjs +1 -1
  44. package/api/integration/add-contribution.d.mts +2 -1
  45. package/api/integration/add-contribution.mjs +107 -5
  46. package/api/integration/add-contribution.test.mjs +137 -0
  47. package/api/integration/authoring-checks.mjs +138 -28
  48. package/api/integration/authoring-checks.test.mjs +171 -3
  49. package/api/integration/authoring-checks.type.mjs +6 -1
  50. package/api/integration/integration-authoring.type.d.mts +2 -0
  51. package/api/integration/integration-authoring.type.mjs +2 -0
  52. package/api/integration/integrationAdd.doc.mjs +6 -0
  53. package/api/integration/integrationAddDoc.doc.mjs +7 -1
  54. package/api/integration/integrationDocConflicts.doc.mjs +1 -1
  55. package/api/integration/integrationPackCheck.doc.mjs +1 -1
  56. package/api/integration/integrationTemplateConflicts.doc.d.mts +3 -0
  57. package/api/integration/integrationTemplateConflicts.doc.mjs +3 -0
  58. package/api/integration/pack-check.mjs +34 -0
  59. package/api/integration/pack-check.test.mjs +121 -0
  60. package/api/integration/pack-check.type.d.mts +26 -2
  61. package/api/integration/pack-check.type.mjs +14 -1
  62. package/api/integration/template-conflict-compatibility.test.mjs +73 -0
  63. package/api/integration/validate-integration.test.mjs +3 -3
  64. package/api/integration/validateIntegration.doc.mjs +1 -1
  65. package/api/json/index.ts +2 -0
  66. package/api/layout/_adapter.mjs +20 -5
  67. package/api/search/search.d.mts +59 -1
  68. package/api/search/search.doc.mjs +4 -3
  69. package/api/search/search.mjs +442 -71
  70. package/api/search/search.test.mjs +91 -2
  71. package/api/search/search.type.d.mts +13 -1
  72. package/api/search/search.type.mjs +4 -1
  73. package/api/template/list/list.mjs +1 -0
  74. package/api/template/table-floating-bulk-actions.test.mjs +66 -0
  75. package/api/template/template-integration.test.mjs +1072 -3
  76. package/api/template/template.doc.mjs +27 -7
  77. package/api/template/template.mjs +45 -8
  78. package/api/template/template.type.d.mts +6 -8
  79. package/api/template/template.type.mjs +3 -2
  80. package/api/theme/template/template.test.mjs +5 -0
  81. package/api/theme/themeTemplate.doc.mjs +1 -1
  82. package/assets/docs/getting-started.doc.mjs +2 -2
  83. package/assets/docs/layout.doc.dense.mjs +2 -2
  84. package/assets/docs/layout.doc.mjs +1 -1
  85. package/assets/docs/principles.doc.mjs +6 -6
  86. package/assets/docs/styling-libraries.doc.mjs +3 -3
  87. package/assets/docs/styling.doc.mjs +4 -4
  88. package/assets/docs/theme.doc.mjs +4 -4
  89. package/assets/docs/tokens.doc.mjs +1 -1
  90. package/assets/docs/tree/api.doc.mjs +30 -0
  91. package/assets/docs/tree/cli.doc.mjs +23 -0
  92. package/assets/docs/tree/commands.doc.mjs +25 -0
  93. package/assets/docs/{cli-integrations.doc.mjs → tree/integrations.doc.mjs} +37 -9
  94. package/assets/docs/tree/integrations.test.mjs +62 -0
  95. package/assets/docs/tree/writing-docs.doc.mjs +286 -0
  96. package/assets/docs/working-with-ai.doc.mjs +3 -3
  97. package/assets/templates/blocks/components/CheckboxList/CheckboxListSelectAllPattern.doc.mjs +1 -1
  98. package/assets/templates/blocks/components/CheckboxList/CheckboxListSelectAllPattern.tsx +1 -3
  99. package/assets/templates/blocks/components/Table/TableBulkActionsTable.doc.mjs +14 -0
  100. package/assets/templates/blocks/components/Table/TableBulkActionsTable.tsx +71 -0
  101. package/assets/templates/blocks/components/Table/TableFloatingBulkActionsTable.doc.mjs +19 -0
  102. package/assets/templates/blocks/components/Table/TableFloatingBulkActionsTable.tsx +139 -0
  103. package/authoring/debug/parse.d.mts +3 -3
  104. package/authoring/doctypes/_schema.d.mts +119 -117
  105. package/authoring/doctypes/_schema.mjs +54 -3
  106. package/authoring/doctypes/base/graph-fields.doc.mjs +7 -5
  107. package/authoring/doctypes/base/type.ts +6 -5
  108. package/authoring/doctypes/command/command.doc.mjs +1 -1
  109. package/authoring/doctypes/command/type.ts +2 -2
  110. package/authoring/doctypes/enum/enum.doc.mjs +1 -1
  111. package/authoring/doctypes/enum/type.ts +1 -1
  112. package/authoring/doctypes/function/function.doc.mjs +3 -2
  113. package/authoring/doctypes/function/type.ts +3 -2
  114. package/authoring/doctypes/load-contract.test.mjs +26 -1
  115. package/authoring/doctypes/namespace/namespace.doc.mjs +6 -10
  116. package/authoring/doctypes/namespace/parse.test.mjs +23 -25
  117. package/authoring/doctypes/namespace/type.ts +5 -2
  118. package/authoring/doctypes/parse.mjs +3 -2
  119. package/authoring/doctypes/reference/reference.doc.mjs +34 -5
  120. package/authoring/doctypes/reference/type.ts +28 -11
  121. package/authoring/doctypes/schema/schema.doc.mjs +1 -1
  122. package/authoring/doctypes/schema/type.ts +1 -1
  123. package/authoring/doctypes/template/parse.d.mts +2 -0
  124. package/authoring/doctypes/template/parse.mjs +4 -0
  125. package/authoring/doctypes/template/parse.test.mjs +18 -0
  126. package/authoring/doctypes/template/template.doc.mjs +6 -0
  127. package/authoring/doctypes/template/type.ts +8 -0
  128. package/authoring/identity/identity.doc.mjs +2 -2
  129. package/authoring/index.d.ts +1 -0
  130. package/authoring/integration/type.ts +1 -1
  131. package/clients/cli/commands/build-theme.adaptations.test.mjs +100 -1
  132. package/clients/cli/commands/build-theme.mjs +1 -1
  133. package/clients/cli/commands/build.doc.mjs +11 -6
  134. package/clients/cli/commands/build.mjs +105 -67
  135. package/clients/cli/commands/build.text-fields.test.mjs +41 -0
  136. package/clients/cli/commands/component/index.mjs +1 -1
  137. package/clients/cli/commands/component-ownership.test.mjs +3 -3
  138. package/clients/cli/commands/discover.broken-integration.test.mjs +7 -8
  139. package/clients/cli/commands/discover.mjs +1 -1
  140. package/clients/cli/commands/docs.doc.mjs +20 -8
  141. package/clients/cli/commands/docs.mjs +183 -69
  142. package/clients/cli/commands/docs.test.mjs +120 -16
  143. package/clients/cli/commands/doctor-integration-docs.doc.mjs +2 -2
  144. package/clients/cli/commands/doctor-integration-templates.doc.mjs +14 -7
  145. package/clients/cli/commands/doctor-integration.test.mjs +75 -1
  146. package/clients/cli/commands/doctor.mjs +53 -16
  147. package/clients/cli/commands/integration-add.doc.mjs +10 -0
  148. package/clients/cli/commands/integration.mjs +1 -0
  149. package/clients/cli/commands/search.mjs +11 -2
  150. package/clients/cli/commands/setup-nudge.test.mjs +6 -0
  151. package/clients/cli/commands/template.doc.mjs +23 -6
  152. package/clients/cli/commands/template.mjs +4 -91
  153. package/clients/cli/index.mjs +8 -0
  154. package/clients/cli/lib/manifest.mjs +9 -2
  155. package/foundation/agent-docs/agent-docs.d.mts +3 -2
  156. package/foundation/agent-docs/agent-docs.mjs +13 -9
  157. package/foundation/agent-docs/agent-docs.test.mjs +19 -1
  158. package/foundation/config/project.d.mts +12 -11
  159. package/foundation/config/project.mjs +73 -59
  160. package/foundation/config/project.test.mjs +141 -5
  161. package/foundation/discovery/authoring-self-docs.d.mts +12 -0
  162. package/foundation/discovery/authoring-self-docs.mjs +20 -8
  163. package/foundation/discovery/authoring-self-docs.test.mjs +25 -9
  164. package/foundation/discovery/cli-self-docs.d.mts +40 -34
  165. package/foundation/discovery/cli-self-docs.mjs +91 -115
  166. package/foundation/discovery/cli-self-docs.test.mjs +104 -166
  167. package/foundation/discovery/component-discovery.d.mts +38 -0
  168. package/foundation/discovery/component-discovery.mjs +48 -0
  169. package/foundation/discovery/docs-discovery.d.mts +109 -9
  170. package/foundation/discovery/docs-discovery.mjs +223 -24
  171. package/foundation/discovery/docs-discovery.test.mjs +103 -27
  172. package/foundation/discovery/docs-output-budget.d.mts +2 -2
  173. package/foundation/discovery/docs-output-budget.mjs +1 -1
  174. package/foundation/discovery/docs-section-key.d.mts +28 -10
  175. package/foundation/discovery/docs-section-key.mjs +134 -33
  176. package/foundation/discovery/docs-section-key.test.mjs +41 -19
  177. package/foundation/discovery/template-adapter.d.mts +92 -6
  178. package/foundation/discovery/template-adapter.mjs +453 -17
  179. package/foundation/discovery/template-adapter.test.mjs +42 -0
  180. package/foundation/discovery/template-conflict-release.d.mts +13 -0
  181. package/foundation/discovery/template-conflict-release.mjs +40 -0
  182. package/foundation/discovery/template-conflict-release.test.mjs +40 -0
  183. package/foundation/doc-compiler/bundle.mjs +61 -1
  184. package/foundation/doc-compiler/bundle.test.mjs +21 -10
  185. package/foundation/doc-compiler/compile.d.mts +113 -132
  186. package/foundation/doc-compiler/compile.mjs +57 -10
  187. package/foundation/doc-compiler/diagnostics.mjs +28 -0
  188. package/foundation/doc-compiler/doc-loads.test.mjs +2 -2
  189. package/foundation/doc-compiler/inputs.d.mts +2 -2
  190. package/foundation/doc-compiler/inputs.mjs +6 -1
  191. package/foundation/doc-compiler/ir.mjs +7 -0
  192. package/foundation/doc-compiler/lenses.d.mts +8 -5
  193. package/foundation/doc-compiler/lenses.mjs +50 -4
  194. package/foundation/doc-compiler/links.d.mts +162 -0
  195. package/foundation/doc-compiler/links.mjs +294 -0
  196. package/foundation/doc-compiler/links.test.mjs +192 -0
  197. package/foundation/doc-compiler/lower-doc.test.mjs +5 -2
  198. package/foundation/doc-compiler/read.d.mts +3 -2
  199. package/foundation/doc-compiler/read.mjs +6 -1
  200. package/foundation/doc-compiler/tree.d.mts +288 -0
  201. package/foundation/doc-compiler/tree.mjs +876 -0
  202. package/foundation/doc-compiler/tree.test.mjs +598 -0
  203. package/foundation/integrations/autolink.test.mjs +1 -1
  204. package/foundation/integrations/cli-requirement.d.mts +45 -0
  205. package/foundation/integrations/cli-requirement.mjs +154 -0
  206. package/foundation/integrations/cli-requirement.test.mjs +84 -0
  207. package/foundation/integrations/contribution-inventory.d.mts +2 -1
  208. package/foundation/integrations/contribution-inventory.mjs +10 -4
  209. package/foundation/integrations/contribution-inventory.test.mjs +19 -0
  210. package/foundation/integrations/integration-warnings.d.mts +9 -2
  211. package/foundation/integrations/integration-warnings.mjs +51 -26
  212. package/foundation/integrations/integration-warnings.test.mjs +74 -1
  213. package/foundation/integrations/validate-contributions.mjs +25 -27
  214. package/foundation/response/response-types.doc.d.mts +5 -1
  215. package/foundation/response/response-types.doc.mjs +19 -10
  216. package/foundation/text/string-utils.d.mts +8 -0
  217. package/foundation/text/string-utils.mjs +18 -0
  218. package/foundation/xle/expand.mjs +1 -1
  219. package/foundation/xle/xle.test.mjs +13 -0
  220. package/package.json +9 -9
  221. package/assets/docs/cli.doc.mjs +0 -15
@@ -0,0 +1,154 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file The CLI an integration's namespace docs need (spec:AST-046 FR11).
5
+ *
6
+ * @input A package.json object.
7
+ * @output Whether its `peerDependencies` range for `@astryxdesign/cli` admits
8
+ * only CLIs that read namespace docs, and the package.json that declares it.
9
+ * @position foundation/integrations; read by `integration add doc --parent`,
10
+ * which declares the peer, and `integration pack --check`, which requires it.
11
+ */
12
+
13
+ import {semverCompare} from '../env/semver.mjs';
14
+
15
+ export const CLI_PACKAGE = '@astryxdesign/cli';
16
+
17
+ /**
18
+ * The first CLI release that reads an integration's docs tree (namespace docs
19
+ * and placed guides) and its templates' `replaces`. A release before it can
20
+ * hide every doc topic a package with a namespace doc or a placed guide
21
+ * ships, with no warning, and it rejects `replaces` and withholds the
22
+ * package's templates and doc topics.
23
+ */
24
+ export const DOCS_TREE_CLI = '0.7.0';
25
+
26
+ const VERSION_RE = /^v?(\d+|[xX*])(?:\.(\d+|[xX*]))?(?:\.(\d+|[xX*]))?(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
27
+
28
+ /**
29
+ * A version or partial version as MAJOR.MINOR.PATCH, a wildcard part as 0.
30
+ * @param {string} text
31
+ * @returns {string | null}
32
+ */
33
+ function lowerVersion(text) {
34
+ const m = VERSION_RE.exec(text.trim());
35
+ if (!m) return null;
36
+ return [m[1], m[2], m[3]]
37
+ .map(part => (part == null || /^[xX*]$/.test(part) ? '0' : part))
38
+ .join('.');
39
+ }
40
+
41
+ /**
42
+ * The lowest version one space-joined comparator set admits, or null when it
43
+ * has no lower bound this can read.
44
+ * @param {string} set
45
+ * @returns {string | null}
46
+ */
47
+ function lowestOfSet(set) {
48
+ if (set === '' || set === '*' || /^[xX]$/.test(set)) return null;
49
+ const hyphen = /^(\S+)\s+-\s+\S+$/.exec(set);
50
+ if (hyphen) return lowerVersion(hyphen[1]);
51
+ /** @type {string | null} */
52
+ let lowest = null;
53
+ for (const comparator of set.split(/\s+/)) {
54
+ const m = /^(>=|<=|>|<|=|\^|~)?(.+)$/.exec(comparator);
55
+ if (!m) return null;
56
+ if (m[1] === '<' || m[1] === '<=') continue;
57
+ const version = lowerVersion(m[2]);
58
+ if (version == null) return null;
59
+ if (lowest == null || semverCompare(version, lowest) > 0) lowest = version;
60
+ }
61
+ return lowest;
62
+ }
63
+
64
+ /**
65
+ * The lowest version a semver range admits: the least of its `||`
66
+ * alternatives. Null when an alternative has no lower bound, or the range is
67
+ * not one this can read, so the caller treats it as admitting every version.
68
+ * @param {unknown} range
69
+ * @returns {string | null}
70
+ */
71
+ export function lowestAdmitted(range) {
72
+ if (typeof range !== 'string') return null;
73
+ /** @type {string | null} */
74
+ let lowest = null;
75
+ for (const alternative of range.split('||')) {
76
+ const low = lowestOfSet(alternative.trim());
77
+ if (low == null) return null;
78
+ if (lowest == null || semverCompare(low, lowest) < 0) lowest = low;
79
+ }
80
+ return lowest;
81
+ }
82
+
83
+ /**
84
+ * Why a package that uses a feature an older CLI cannot read would lose it,
85
+ * or null when its declared CLI range admits only CLIs that read it.
86
+ * @param {any} pkg package.json
87
+ * @param {string} feature what the package does, e.g. "ships a namespace doc"
88
+ * @param {string} loss what an older CLI does with it
89
+ * @returns {string | null}
90
+ */
91
+ function cliRangeProblem(pkg, feature, loss) {
92
+ const range = pkg?.peerDependencies?.[CLI_PACKAGE];
93
+ const fix = `"${CLI_PACKAGE}": ">=${DOCS_TREE_CLI}" in peerDependencies (optional in peerDependenciesMeta, if the CLI is not required)`;
94
+ if (typeof range !== 'string') {
95
+ return `The package ${feature} but declares no ${CLI_PACKAGE} peer. A CLI older than ${DOCS_TREE_CLI} ${loss}. Declare ${fix}.`;
96
+ }
97
+ const lowest = lowestAdmitted(range);
98
+ if (lowest == null || semverCompare(lowest, DOCS_TREE_CLI) < 0) {
99
+ return `The package ${feature}, but its ${CLI_PACKAGE} peer range "${range}" admits a CLI older than ${DOCS_TREE_CLI}, which ${loss}. Declare ${fix}.`;
100
+ }
101
+ return null;
102
+ }
103
+
104
+ /**
105
+ * Why a package that ships a namespace doc or a placed guide would lose its
106
+ * docs on an older CLI, or null when its declared CLI range admits only CLIs
107
+ * that read the docs tree. Published 0.6.3 hides every topic of a package
108
+ * with a namespace doc; builds of main before the docs tree also do so for a
109
+ * placed guide.
110
+ * @param {any} pkg package.json
111
+ * @returns {string | null}
112
+ */
113
+ export function docsTreeCliProblem(pkg) {
114
+ return cliRangeProblem(
115
+ pkg,
116
+ 'ships a namespace doc or a placed guide',
117
+ 'does not read the docs tree, and can hide every doc topic the package ships',
118
+ );
119
+ }
120
+
121
+ /**
122
+ * Why a package with a template that sets `replaces` would lose its templates
123
+ * and doc topics on an older CLI (spec:AST-035), or null when its declared CLI
124
+ * range admits only CLIs that read the field.
125
+ * @param {any} pkg package.json
126
+ * @returns {string | null}
127
+ */
128
+ export function replacesCliProblem(pkg) {
129
+ return cliRangeProblem(
130
+ pkg,
131
+ 'has a template that sets `replaces`',
132
+ "rejects the field, and withholds the package's templates and doc topics",
133
+ );
134
+ }
135
+
136
+ /**
137
+ * The package.json with a CLI peer that reads namespace docs: optional, unless
138
+ * the package already says otherwise.
139
+ * @param {any} pkg package.json
140
+ * @returns {any}
141
+ */
142
+ export function withDocsTreeCli(pkg) {
143
+ const meta = pkg.peerDependenciesMeta ?? {};
144
+ return {
145
+ ...pkg,
146
+ peerDependencies: {
147
+ ...(pkg.peerDependencies ?? {}),
148
+ [CLI_PACKAGE]: `>=${DOCS_TREE_CLI}`,
149
+ },
150
+ peerDependenciesMeta: meta[CLI_PACKAGE]
151
+ ? meta
152
+ : {...meta, [CLI_PACKAGE]: {optional: true}},
153
+ };
154
+ }
@@ -0,0 +1,84 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ import {describe, expect, it} from 'vitest';
4
+ import {
5
+ DOCS_TREE_CLI,
6
+ lowestAdmitted,
7
+ docsTreeCliProblem,
8
+ replacesCliProblem,
9
+ withDocsTreeCli,
10
+ } from './cli-requirement.mjs';
11
+
12
+ describe('lowestAdmitted', () => {
13
+ it.each([
14
+ ['>=0.7.0', '0.7.0'],
15
+ ['^0.6.0', '0.6.0'],
16
+ ['~0.7.1', '0.7.1'],
17
+ ['0.7.x', '0.7.0'],
18
+ ['>=0.7.0 <0.8.0', '0.7.0'],
19
+ ['^0.6.0 || >=0.7.0', '0.6.0'],
20
+ ['0.8.0 - 0.9.0', '0.8.0'],
21
+ ['*', null],
22
+ ['<1.0.0', null],
23
+ ['latest', null],
24
+ ['workspace:*', null],
25
+ ])('%s admits %s at the lowest', (range, lowest) => {
26
+ expect(lowestAdmitted(range)).toBe(lowest);
27
+ });
28
+ });
29
+
30
+ describe('docsTreeCliProblem', () => {
31
+ it('asks for a CLI peer when there is none', () => {
32
+ expect(docsTreeCliProblem({name: '@acme/kit'})).toContain(
33
+ 'declares no @astryxdesign/cli peer',
34
+ );
35
+ });
36
+
37
+ it.each(['^0.6.0', '^0.6.0 || >=0.7.0', '*', '>=0.6.3'])(
38
+ 'refuses %s, which admits a CLI that cannot read a namespace doc',
39
+ range => {
40
+ expect(
41
+ docsTreeCliProblem({peerDependencies: {'@astryxdesign/cli': range}}),
42
+ ).toContain('admits a CLI older than');
43
+ },
44
+ );
45
+
46
+ it.each(['>=0.7.0', '^0.7.2', '>=0.7.0 <2'])('accepts %s', range => {
47
+ expect(
48
+ docsTreeCliProblem({peerDependencies: {'@astryxdesign/cli': range}}),
49
+ ).toBeNull();
50
+ });
51
+
52
+ it('declares the peer as optional, and keeps what the package says of it', () => {
53
+ const declared = withDocsTreeCli({name: '@acme/kit'});
54
+ expect(declared.peerDependencies).toEqual({
55
+ '@astryxdesign/cli': `>=${DOCS_TREE_CLI}`,
56
+ });
57
+ expect(declared.peerDependenciesMeta).toEqual({
58
+ '@astryxdesign/cli': {optional: true},
59
+ });
60
+ expect(docsTreeCliProblem(declared)).toBeNull();
61
+ const required = withDocsTreeCli({
62
+ peerDependencies: {react: '^19.0.0', '@astryxdesign/cli': '^0.6.0'},
63
+ peerDependenciesMeta: {'@astryxdesign/cli': {optional: false}},
64
+ });
65
+ expect(required.peerDependencies.react).toBe('^19.0.0');
66
+ expect(required.peerDependenciesMeta['@astryxdesign/cli']).toEqual({
67
+ optional: false,
68
+ });
69
+ });
70
+ });
71
+
72
+ describe('replacesCliProblem', () => {
73
+ it('asks a package that sets replaces for a CLI that reads the field', () => {
74
+ expect(replacesCliProblem({name: '@acme/kit'})).toContain(
75
+ 'sets `replaces`',
76
+ );
77
+ expect(
78
+ replacesCliProblem({peerDependencies: {'@astryxdesign/cli': '^0.6.0'}}),
79
+ ).toContain('admits a CLI older than');
80
+ expect(
81
+ replacesCliProblem({peerDependencies: {'@astryxdesign/cli': '>=0.7.0'}}),
82
+ ).toBeNull();
83
+ });
84
+ });
@@ -26,7 +26,7 @@ export function computeRequiredFiles(loaded: import("./integrations.mjs").Loaded
26
26
  * @typedef {object} ContributionIdentities
27
27
  * @property {{slug: string, exportName: string}[]} themes
28
28
  * @property {string[]} components
29
- * @property {{id: string, type: string, name: string}[]} templates
29
+ * @property {{id: string, type: string, name: string, replaces?: string}[]} templates
30
30
  * @property {{version: string, id: string}[]} codemods
31
31
  * @property {string[]} docs
32
32
  * @property {string[]} agentDocsAppend
@@ -127,6 +127,7 @@ export type ContributionIdentities = {
127
127
  id: string;
128
128
  type: string;
129
129
  name: string;
130
+ replaces?: string;
130
131
  }[];
131
132
  codemods: {
132
133
  version: string;
@@ -189,7 +189,7 @@ export function computeRequiredFiles(loaded) {
189
189
  * @typedef {object} ContributionIdentities
190
190
  * @property {{slug: string, exportName: string}[]} themes
191
191
  * @property {string[]} components
192
- * @property {{id: string, type: string, name: string}[]} templates
192
+ * @property {{id: string, type: string, name: string, replaces?: string}[]} templates
193
193
  * @property {{version: string, id: string}[]} codemods
194
194
  * @property {string[]} docs
195
195
  * @property {string[]} agentDocsAppend
@@ -278,7 +278,12 @@ export async function collectIdentities(loaded) {
278
278
  errors.push({kind: 'templates', message: templateError.message});
279
279
  }
280
280
  identities.templates = templates
281
- .map(t => ({id: t.dirName, type: t.type, name: t.name}))
281
+ .map(t => ({
282
+ id: t.dirName,
283
+ type: t.type,
284
+ name: t.name,
285
+ ...(t.replaces == null ? {} : {replaces: t.replaces}),
286
+ }))
282
287
  .sort((a, b) => a.id.localeCompare(b.id));
283
288
  } catch (err) {
284
289
  errors.push({
@@ -309,12 +314,13 @@ export async function collectIdentities(loaded) {
309
314
 
310
315
  if (loaded.docs && fs.existsSync(loaded.docs)) {
311
316
  try {
312
- const {records, errors: docErrors} =
317
+ const {records, guides, errors: docErrors} =
313
318
  await discoverIntegrationDocs(loaded);
314
319
  for (const docError of docErrors) {
315
320
  errors.push({kind: 'docs', message: docError.message});
316
321
  }
317
- identities.docs = records.map(r => r.name).sort();
322
+ // A guide placed in the docs tree is a doc contribution too.
323
+ identities.docs = [...records, ...guides].map(r => r.name).sort();
318
324
  } catch (err) {
319
325
  errors.push({kind: 'docs', message: /** @type {Error} */ (err).message});
320
326
  }
@@ -304,6 +304,25 @@ describe('compareIdentities', () => {
304
304
  expect(issues[0].message).toContain('dash');
305
305
  });
306
306
 
307
+ it('errors when a packed template replaces a different target', () => {
308
+ const template = {id: 'acme-shell', type: 'page', name: 'Acme shell'};
309
+ const local = {
310
+ ...base(),
311
+ templates: [{...template, replaces: 'shell-side-nav'}],
312
+ };
313
+ const packed = {
314
+ ...base(),
315
+ templates: [{...template, replaces: 'shell-top-nav'}],
316
+ };
317
+
318
+ expect(compareIdentities(local, packed)).toEqual([
319
+ expect.objectContaining({
320
+ code: 'identity_mismatch',
321
+ message: expect.stringContaining('acme-shell'),
322
+ }),
323
+ ]);
324
+ });
325
+
307
326
  it('errors when a codemod is missing from packed', () => {
308
327
  const local = {
309
328
  ...base(),
@@ -8,10 +8,17 @@
8
8
  *
9
9
  * Warning: <pkg> has N integration issue(s). Run: astryx doctor integration validate <pkg>
10
10
  *
11
- * @param {Array<import('./integrations.mjs').LoadedIntegration>} loadedIntegrations the Project's loaded integrations
11
+ * @param {Array<import('./integrations.mjs').LoadedIntegration> | {issues: () => Promise<Array<{package: string, code: string, severity: string, message: string}>>}} source loaded integrations or their Project owner
12
12
  * @param {{json?: boolean}} [options]
13
13
  * @returns {Promise<void>}
14
14
  */
15
- export function warnOnIntegrationIssues(loadedIntegrations: Array<import("./integrations.mjs").LoadedIntegration>, { json }?: {
15
+ export function warnOnIntegrationIssues(source: Array<import("./integrations.mjs").LoadedIntegration> | {
16
+ issues: () => Promise<Array<{
17
+ package: string;
18
+ code: string;
19
+ severity: string;
20
+ message: string;
21
+ }>>;
22
+ }, { json }?: {
16
23
  json?: boolean;
17
24
  }): Promise<void>;
@@ -10,8 +10,9 @@
10
10
  * contributions or spamming per-contribution diagnostics.
11
11
  *
12
12
  * Design constraints (all enforced here):
13
- * - Reuses the Doctor integration validators (validateLoadedIntegration);
14
- * no validation logic is duplicated.
13
+ * - Reuses Project.issues() when a Project is available, so cross-package
14
+ * replacement warnings are visible; accepts loaded integrations for legacy
15
+ * internal callers and tests.
15
16
  * - Writes to STDERR only, so it never corrupts a --json stdout envelope.
16
17
  * - Suppressed entirely in --json mode.
17
18
  * - Best-effort: never throws, never changes the exit code. Broken
@@ -28,38 +29,62 @@ import {validateLoadedIntegration} from './validate-contributions.mjs';
28
29
  *
29
30
  * Warning: <pkg> has N integration issue(s). Run: astryx doctor integration validate <pkg>
30
31
  *
31
- * @param {Array<import('./integrations.mjs').LoadedIntegration>} loadedIntegrations the Project's loaded integrations
32
+ * @param {Array<import('./integrations.mjs').LoadedIntegration> | {issues: () => Promise<Array<{package: string, code: string, severity: string, message: string}>>}} source loaded integrations or their Project owner
32
33
  * @param {{json?: boolean}} [options]
33
34
  * @returns {Promise<void>}
34
35
  */
35
- export async function warnOnIntegrationIssues(loadedIntegrations, {json = false} = {}) {
36
+ export async function warnOnIntegrationIssues(source, {json = false} = {}) {
36
37
  try {
37
- // Cheap guard: only do work when integrations are actually configured.
38
38
  if (json) return;
39
- if (!Array.isArray(loadedIntegrations) || loadedIntegrations.length === 0) {
40
- return;
41
- }
42
- for (const integration of loadedIntegrations) {
43
- if (!integration || typeof integration !== 'object') continue;
44
- // A package set aside for a provider-ID conflict is fine in isolation,
45
- // so `doctor integration validate` would not explain it. Say it here.
46
- if (integration.__providerConflict) {
47
- console.error(`Warning: ${integration.__providerConflict.message}`);
48
- continue;
39
+
40
+ /** @type {Map<string, number>} */
41
+ const issueCounts = new Map();
42
+ const fromProject =
43
+ source != null &&
44
+ !Array.isArray(source) &&
45
+ typeof source.issues === 'function';
46
+
47
+ if (fromProject) {
48
+ const issues = await source.issues();
49
+ for (const issue of issues) {
50
+ // A provider-conflict issue carries the specific message that
51
+ // `doctor integration validate` would not explain. Print it directly.
52
+ if (issue.code === 'duplicate_provider') {
53
+ console.error(`Warning: ${issue.message}`);
54
+ continue;
55
+ }
56
+ issueCounts.set(
57
+ issue.package,
58
+ (issueCounts.get(issue.package) ?? 0) + 1,
59
+ );
49
60
  }
50
- let issues;
51
- try {
52
- issues = await validateLoadedIntegration(integration);
53
- } catch {
54
- // Best-effort: a validator throwing must not break the host command.
55
- continue;
61
+ } else if (Array.isArray(source)) {
62
+ for (const integration of source) {
63
+ if (!integration || typeof integration !== 'object') continue;
64
+ // A package set aside for a provider-ID conflict is fine in isolation,
65
+ // so `doctor integration validate` would not explain it. Say it here.
66
+ if (integration.__providerConflict) {
67
+ console.error(`Warning: ${integration.__providerConflict.message}`);
68
+ continue;
69
+ }
70
+ let issues;
71
+ try {
72
+ issues = await validateLoadedIntegration(integration);
73
+ } catch {
74
+ continue;
75
+ }
76
+ if (!Array.isArray(issues) || issues.length === 0) continue;
77
+ const pkg = integration.name ?? integration.__spec ?? '(integration)';
78
+ issueCounts.set(pkg, issues.length);
56
79
  }
57
- if (!Array.isArray(issues) || issues.length === 0) continue;
58
- const pkg = integration.name ?? integration.__spec ?? '(integration)';
59
- // Stderr only — keeps stdout (and any --json envelope) clean.
80
+ }
81
+
82
+ for (const [pkg, count] of issueCounts) {
83
+ const command = fromProject
84
+ ? 'astryx doctor'
85
+ : `astryx doctor integration validate ${pkg}`;
60
86
  console.error(
61
- `Warning: ${pkg} has ${issues.length} integration issue(s). ` +
62
- `Run: astryx doctor integration validate ${pkg}`,
87
+ `Warning: ${pkg} has ${count} integration issue(s). Run: ${command}`,
63
88
  );
64
89
  }
65
90
  } catch {
@@ -37,7 +37,12 @@ afterEach(() => {
37
37
  * Build a loaded-integration-shaped object. Roots are absolute (mirrors
38
38
  * loadIntegrations' resolveRoot).
39
39
  */
40
- function loaded({name = '@acme/widgets', components, templates, codemods} = {}) {
40
+ function loaded({
41
+ name = '@acme/widgets',
42
+ components,
43
+ templates,
44
+ codemods,
45
+ } = {}) {
41
46
  return {
42
47
  name,
43
48
  version: '1.0.0',
@@ -62,6 +67,25 @@ describe('warnOnIntegrationIssues', () => {
62
67
  );
63
68
  });
64
69
 
70
+ it('surfaces cross-package issues collected by Project', async () => {
71
+ const project = {
72
+ issues: async () => [
73
+ {
74
+ package: '@acme/later',
75
+ code: 'ambiguous_template_replacement',
76
+ severity: 'warning',
77
+ message: 'Later configured replacement wins.',
78
+ },
79
+ ],
80
+ };
81
+
82
+ await warnOnIntegrationIssues(project, {json: false});
83
+
84
+ expect(errLines).toEqual([
85
+ 'Warning: @acme/later has 1 integration issue(s). Run: astryx doctor',
86
+ ]);
87
+ });
88
+
65
89
  it('emits nothing in --json mode (keeps stdout/JSON clean)', async () => {
66
90
  const integration = loaded({components: 'does-not-exist'});
67
91
  await warnOnIntegrationIssues([integration], {json: true});
@@ -116,4 +140,53 @@ describe('warnOnIntegrationIssues', () => {
116
140
  ).resolves.toBeUndefined();
117
141
  expect(errLines).toHaveLength(0);
118
142
  });
143
+
144
+ it('prints the specific provider-conflict message via the array path', async () => {
145
+ const conflict = {
146
+ name: '@acme/charts',
147
+ version: '2.0.0',
148
+ __spec: '@acme/charts',
149
+ __packageDir: tmpDir,
150
+ __providerConflict: {
151
+ providerId: '@acme/lab',
152
+ claimedBy: '@acme/lab',
153
+ message:
154
+ '@acme/charts@2.0.0 and @acme/lab@1.0.0 both claim provider ID ' +
155
+ '"@acme/lab". @acme/lab@1.0.0 loads first and is used; ' +
156
+ '@acme/charts@2.0.0 contributes nothing until one package changes ' +
157
+ 'its providerId.',
158
+ },
159
+ };
160
+ await warnOnIntegrationIssues([conflict], {json: false});
161
+
162
+ expect(errLines).toHaveLength(1);
163
+ expect(errLines[0]).toBe(`Warning: ${conflict.__providerConflict.message}`);
164
+ // Must NOT print the generic validate pointer for a provider conflict.
165
+ expect(errLines[0]).not.toContain('integration issue(s)');
166
+ });
167
+
168
+ it('prints the specific provider-conflict message via the Project path', async () => {
169
+ const conflictMessage =
170
+ '@acme/charts@2.0.0 and @acme/lab@1.0.0 both claim provider ID ' +
171
+ '"@acme/lab". @acme/lab@1.0.0 loads first and is used; ' +
172
+ '@acme/charts@2.0.0 contributes nothing until one package changes ' +
173
+ 'its providerId.';
174
+ const project = {
175
+ issues: async () => [
176
+ {
177
+ package: '@acme/charts@2.0.0',
178
+ code: 'duplicate_provider',
179
+ severity: 'warning',
180
+ message: conflictMessage,
181
+ },
182
+ ],
183
+ };
184
+
185
+ await warnOnIntegrationIssues(project, {json: false});
186
+
187
+ expect(errLines).toHaveLength(1);
188
+ expect(errLines[0]).toBe(`Warning: ${conflictMessage}`);
189
+ // Must NOT print the generic validate pointer for a provider conflict.
190
+ expect(errLines[0]).not.toContain('integration issue(s)');
191
+ });
119
192
  });
@@ -236,7 +236,11 @@ async function checkTemplates(integration, issues) {
236
236
  try {
237
237
  const {errors} = await discoverIntegrationTemplatesForOne(integration);
238
238
  for (const e of errors) {
239
- issues.push(issueError('invalid_template', e.message));
239
+ issues.push({
240
+ code: e.code ?? 'invalid_template',
241
+ severity: e.severity ?? 'error',
242
+ message: e.message,
243
+ });
240
244
  }
241
245
  } catch (err) {
242
246
  issues.push(
@@ -246,41 +250,35 @@ async function checkTemplates(integration, issues) {
246
250
  }
247
251
 
248
252
  /**
249
- * Validate the integration's components via the landed ownership discovery.
250
- * Feature-detected: if the component-ownership export isn't present in this
251
- * build (sibling PR not yet merged), component validation is skipped rather
252
- * than hard-failing.
253
- *
254
- * `discoverIntegrationComponents` returns ownership records and does not throw
255
- * on a missing same-stem source — it records `sourcePath: null`. We surface
256
- * each such record as an `invalid_component` error.
253
+ * Validate the integration's components through the same load-and-parse boundary
254
+ * that component detail uses. Missing source and invalid metadata are reported
255
+ * per component so valid siblings remain available.
257
256
  * @param {LoadedIntegration} integration loaded-integration-shaped object
258
257
  * @param {Issue[]} issues
259
258
  */
260
259
  async function checkComponents(integration, issues) {
261
260
  if (!integration.components || !fs.existsSync(integration.components)) return;
262
- const discover = componentDiscovery.discoverIntegrationComponents;
261
+ const discover = componentDiscovery.discoverValidIntegrationComponents;
263
262
  if (typeof discover !== 'function') return; // feature not present yet
264
263
  try {
265
- const records = (await discover(integration)) ?? [];
264
+ const {discovered, errors} = await discover(integration);
266
265
  /** @type {import('./contribution-fixes.mjs').FixContext | undefined} */
267
266
  let context;
268
- for (const record of records) {
269
- if (record?.sourcePath == null) {
270
- const docPath = record?.docPath;
271
- let fix = `Fix: add ${record?.name}.tsx beside ${docPath ? path.basename(docPath) : `${record?.name}.doc.mjs`}.`;
272
- const stamp = docPath ? readContributionStamp(docPath, false) : null;
273
- if (stamp && stamp.type !== 'component' && integration.__packageDir) {
274
- context ??= createFixContext(integration.__packageDir, integration);
275
- fix = notAComponentFix(context, docPath, stamp);
276
- }
277
- issues.push(
278
- issueError(
279
- 'invalid_component',
280
- `Component "${record?.name}" is missing its same-stem source file ${record?.name}.tsx. ${fix}`,
281
- ),
282
- );
267
+ const byName = new Map(discovered.map(record => [record.name, record]));
268
+ for (const error of errors) {
269
+ const record = byName.get(error.name);
270
+ if (record == null || record.sourcePath != null) {
271
+ issues.push(issueError('invalid_component', error.message));
272
+ continue;
273
+ }
274
+ const docPath = record.docPath;
275
+ let fix = `Fix: add ${record.name}.tsx beside ${docPath ? path.basename(docPath) : `${record.name}.doc.mjs`}.`;
276
+ const stamp = docPath ? readContributionStamp(docPath, false) : null;
277
+ if (stamp && stamp.type !== 'component' && integration.__packageDir) {
278
+ context ??= createFixContext(integration.__packageDir, integration);
279
+ fix = notAComponentFix(context, docPath, stamp);
283
280
  }
281
+ issues.push(issueError('invalid_component', `${error.message} ${fix}`));
284
282
  }
285
283
  const overlap = integration.__packageDir
286
284
  ? newComponentDocNote(
@@ -289,7 +287,7 @@ async function checkComponents(integration, issues) {
289
287
  : '';
290
288
  for (const name of findSourceOnlyCandidates(
291
289
  integration.components,
292
- records.map(record => record.name),
290
+ discovered.map(record => record.name),
293
291
  )) {
294
292
  // A hidden doc still pairs with its source; discovery just skips it.
295
293
  if (
@@ -5,7 +5,11 @@
5
5
  * @file EnumDoc for the `type` discriminant carried on every --json success
6
6
  * envelope. The vocabulary mirrors the RESPONSE_TYPES map (each command's
7
7
  * `jsonOut(...)` call sites) in `clients/cli/lib/manifest.mjs`; a consumer
8
- * switches on `type` to narrow the `data` payload.
8
+ * switches on `type` to narrow the `data` payload. Descriptions follow the
9
+ * currently published response projection, not a future package-version boundary.
10
+ *
11
+ * @input Public response discriminants and their currently released payloads.
12
+ * @output Generated consumer reference for every typed JSON success response.
9
13
  * @position packages/cli/foundation/response — enum documentation
10
14
  */
11
15
  /** @type {import('@astryxdesign/cli/authoring').EnumDoc} */