@astryxdesign/cli 0.6.4-canary.06c8fa3 → 0.6.4-canary.10dd683

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 (91) hide show
  1. package/README.md +66 -63
  2. package/api/component/_adapter.d.mts +0 -25
  3. package/api/component/_adapter.mjs +5 -59
  4. package/api/component/component.d.mts +3 -6
  5. package/api/component/component.doc.mjs +10 -23
  6. package/api/component/component.mjs +9 -249
  7. package/api/component/component.type.d.mts +0 -25
  8. package/api/component/component.type.mjs +0 -44
  9. package/api/docs/docs.test.mjs +0 -2
  10. package/api/doctor/doctor.d.mts +3 -8
  11. package/api/doctor/doctor.mjs +9 -90
  12. package/api/doctor/doctor.test.mjs +10 -122
  13. package/api/index.d.mts +2 -1
  14. package/api/index.mjs +4 -4
  15. package/api/integration/pack-check.mjs +3 -28
  16. package/api/json/index.ts +1 -0
  17. package/api/layout/_adapter.d.mts +34 -0
  18. package/api/layout/_adapter.mjs +148 -0
  19. package/api/layout/check/check.d.mts +16 -0
  20. package/api/layout/check/check.mjs +40 -0
  21. package/api/layout/expand/expand.d.mts +22 -0
  22. package/api/layout/expand/expand.mjs +155 -0
  23. package/api/layout/expand/expand.path-safety.test.mjs +53 -0
  24. package/api/layout/grammar/grammar.d.mts +13 -0
  25. package/api/layout/grammar/grammar.mjs +87 -0
  26. package/api/layout/layout.d.mts +6 -0
  27. package/api/layout/layout.mjs +17 -0
  28. package/api/layout/layout.test.mjs +297 -0
  29. package/api/layout/layout.type.d.mts +89 -0
  30. package/api/layout/layout.type.mjs +103 -0
  31. package/api/layout/layoutCheck.doc.d.mts +11 -0
  32. package/api/layout/layoutCheck.doc.mjs +85 -0
  33. package/api/layout/layoutExpand.doc.d.mts +11 -0
  34. package/api/layout/layoutExpand.doc.mjs +107 -0
  35. package/api/layout/layoutGrammar.doc.d.mts +11 -0
  36. package/api/layout/layoutGrammar.doc.mjs +57 -0
  37. package/api/search/search.test.mjs +0 -18
  38. package/api/template/template-integration.test.mjs +65 -1
  39. package/api/template/template.mjs +1 -1
  40. package/api/upgrade/run/run.mjs +4 -6
  41. package/api/upgrade/upgrade.type.mjs +2 -2
  42. package/assets/codemods/__tests__/runner.test.mjs +1 -3
  43. package/assets/codemods/integration-runner.mjs +3 -3
  44. package/assets/codemods/runner.mjs +4 -5
  45. package/authoring/config/config.doc.mjs +1 -1
  46. package/authoring/config/type.ts +2 -2
  47. package/authoring/doctypes/command/command.doc.mjs +1 -1
  48. package/authoring/doctypes/command/type.ts +1 -1
  49. package/clients/cli/command-result-coverage.test.mjs +7 -7
  50. package/clients/cli/commands/component/index.mjs +55 -152
  51. package/clients/cli/commands/component-ownership.test.mjs +0 -89
  52. package/clients/cli/commands/component.doc.mjs +6 -23
  53. package/clients/cli/commands/debug-result-summary.test.mjs +2 -2
  54. package/clients/cli/commands/docs.test.mjs +0 -29
  55. package/clients/cli/commands/layout-check.doc.mjs +65 -0
  56. package/clients/cli/commands/layout-expand.doc.mjs +83 -0
  57. package/clients/cli/commands/layout-grammar.doc.mjs +30 -0
  58. package/clients/cli/commands/layout.doc.mjs +34 -0
  59. package/clients/cli/commands/layout.error-codes.test.mjs +66 -0
  60. package/clients/cli/commands/layout.exit-parity.test.mjs +41 -0
  61. package/clients/cli/commands/layout.mjs +275 -0
  62. package/clients/cli/commands/layout.path-help.test.mjs +33 -0
  63. package/clients/cli/commands/layout.stdin-cap.test.mjs +47 -0
  64. package/clients/cli/commands/layout.text-fields.test.mjs +39 -0
  65. package/clients/cli/commands/text-json-parity.test.mjs +17 -0
  66. package/clients/cli/index.mjs +4 -0
  67. package/clients/cli/lib/exit-codes.test.mjs +8 -1
  68. package/clients/cli/lib/json-shim.mjs +14 -24
  69. package/clients/cli/lib/json-shim.test.mjs +20 -6
  70. package/clients/cli/lib/manifest.mjs +8 -2
  71. package/clients/cli/lib/manifest.test.mjs +2 -5
  72. package/foundation/discovery/template-adapter.mjs +1 -1
  73. package/foundation/doc-compiler/doc-loads.test.mjs +12 -0
  74. package/foundation/doc-compiler/tree.test.mjs +1 -9
  75. package/foundation/response/response-types.doc.mjs +17 -5
  76. package/foundation/response/response-types.doc.test.mjs +0 -23
  77. package/foundation/xle/browser.d.mts +3 -3
  78. package/foundation/xle/browser.mjs +3 -3
  79. package/foundation/xle/expand.mjs +2 -2
  80. package/foundation/xle/parse.mjs +1 -1
  81. package/foundation/xle/print.mjs +2 -2
  82. package/foundation/xle/splice.mjs +1 -1
  83. package/package.json +9 -9
  84. package/api/integration/pack-check.lifecycle-output.test.mjs +0 -105
  85. package/api/upgrade/run/files-changed.test.mjs +0 -111
  86. package/assets/codemods/file-count.test.mjs +0 -163
  87. package/assets/docs/tree/component-lookups.doc.mjs +0 -149
  88. package/clients/cli/commands/component-batch.test.mjs +0 -341
  89. package/clients/cli/lib/parse-error-format.test.mjs +0 -81
  90. package/foundation/response/batch.type.d.mts +0 -33
  91. package/foundation/response/batch.type.mjs +0 -34
@@ -4,10 +4,10 @@
4
4
  * @file Programmatic API for the component command — DISPATCHER + BARREL.
5
5
  *
6
6
  * Returns the same typed envelope { type, data } that `astryx --json component`
7
- * outputs. `component(name, opts)` parses one selector or an ordered selector
8
- * list, resolves each subject through `_adapter`, and routes to the correct leaf
9
- * (list · detail · detail.props/source/showcase/blocks). The CLI command handler
10
- * is a thin wrapper around this function.
7
+ * outputs. `component(name, opts)` parses the (name, options) pair, resolves the
8
+ * subject once via `_adapter`, and routes to the correct leaf
9
+ * (list · detail · detail.props/source/showcase/blocks), returning that leaf's
10
+ * envelope. The CLI command handler is a thin wrapper around this function.
11
11
  *
12
12
  * Every leaf is also re-exported for direct scripting use.
13
13
  */
@@ -26,8 +26,6 @@ import {
26
26
  resolveUnscopedDoc,
27
27
  loadComponentDoc,
28
28
  scopeSubComponent,
29
- ComponentAmbiguityError,
30
- installedComponentPackageVersion,
31
29
  } from './_adapter.mjs';
32
30
  import {componentList} from './list/list.mjs';
33
31
  import {componentDetail} from './detail/detail.mjs';
@@ -36,199 +34,13 @@ import {componentDetailSource} from './detail/source/source.mjs';
36
34
  import {componentDetailShowcase} from './detail/showcase/showcase.mjs';
37
35
  import {componentDetailBlocks} from './detail/blocks/blocks.mjs';
38
36
 
39
- /** Maximum selectors accepted before any component resolution starts. */
40
- export const COMPONENT_BATCH_SELECTOR_LIMIT = 100;
41
-
42
37
  /** @type {ReadonlyArray<string>} */
43
38
  const DETAIL_LEVELS = ['full', 'compact', 'brief'];
44
39
  /** @type {ReadonlyArray<string>} */
45
40
  const LANGS = ['en', 'zh', 'dense'];
46
- /** @type {Set<string>} */
47
- const NOT_FOUND_CODES = new Set([
48
- ERROR_CODES.ERR_UNKNOWN_COMPONENT,
49
- ERROR_CODES.ERR_UNKNOWN_PACKAGE,
50
- ERROR_CODES.ERR_NOT_FOUND,
51
- ]);
52
-
53
- /**
54
- * Discover-compatible package target grammar. Versions belong to the package
55
- * head, never the item after the package slash.
56
- * @param {string} selector
57
- * @returns {{scoped: boolean, name: string, version?: string, item?: string}}
58
- */
59
- function splitSelector(selector) {
60
- const scoped = selector.startsWith('@');
61
- const firstSlash = selector.indexOf('/');
62
- let head = selector;
63
- /** @type {string | undefined} */
64
- let rest;
65
- const split = scoped
66
- ? firstSlash < 0
67
- ? -1
68
- : selector.indexOf('/', firstSlash + 1)
69
- : firstSlash;
70
- if (split > 0) {
71
- head = selector.slice(0, split);
72
- rest = selector.slice(split + 1) || undefined;
73
- }
74
- const at = head.indexOf('@', 1);
75
- return {
76
- scoped,
77
- name: at > 0 ? head.slice(0, at) : head,
78
- ...(at > 0 && head.slice(at + 1) ? {version: head.slice(at + 1)} : {}),
79
- ...(rest ? {item: rest} : {}),
80
- };
81
- }
82
-
83
- /**
84
- * Resolve one component selector into the existing single-component API inputs.
85
- * @param {unknown} value
86
- * @param {string|undefined} packageScope
87
- * @returns {{selector: string, name: string, package?: string, version?: string}}
88
- */
89
- function parseComponentSelector(value, packageScope) {
90
- if (typeof value !== 'string' || value.length === 0) {
91
- throw new AstryxError(
92
- `Invalid component selector "${String(value)}"`,
93
- undefined,
94
- ERROR_CODES.ERR_INVALID_ARGUMENT,
95
- );
96
- }
97
- const target = splitSelector(value);
98
- const packageShaped =
99
- target.scoped || target.item != null || target.version != null;
100
- if (!packageShaped) {
101
- return {selector: value, name: target.name, package: packageScope};
102
- }
103
- if (!target.item) {
104
- throw new AstryxError(
105
- `Component selector "${value}" names a package but no component`,
106
- undefined,
107
- ERROR_CODES.ERR_INVALID_ARGUMENT,
108
- );
109
- }
110
- if (packageScope && packageScope !== target.name) {
111
- throw new AstryxError(
112
- `Component selector "${value}" conflicts with --package "${packageScope}"`,
113
- undefined,
114
- ERROR_CODES.ERR_INVALID_ARGUMENT,
115
- );
116
- }
117
- return {
118
- selector: value,
119
- name: target.item,
120
- package: target.name,
121
- ...(target.version ? {version: target.version} : {}),
122
- };
123
- }
124
-
125
- /**
126
- * A version-qualified selector never falls through to another installed
127
- * version. Its structured suggestion points at the command that can discover
128
- * the requested package release.
129
- * @param {{package?: string, version?: string}} target
130
- * @param {string} coreDir
131
- * @param {import('../../foundation/integrations/integrations.mjs').LoadedIntegration[]} loadedIntegrations
132
- * @returns {void}
133
- */
134
- function requireInstalledSelectorVersion(target, coreDir, loadedIntegrations) {
135
- if (!target.version) return;
136
- const packageName = /** @type {string} */ (target.package);
137
- const installed = installedComponentPackageVersion(
138
- coreDir,
139
- loadedIntegrations,
140
- packageName,
141
- );
142
- if (installed === target.version) return;
143
- const exact = `${packageName}@${target.version}`;
144
- throw new AstryxError(
145
- `Package "${exact}" is not installed`,
146
- [
147
- {
148
- name: `astryx discover ${exact}`,
149
- reason: 'look up this package version',
150
- },
151
- ],
152
- ERROR_CODES.ERR_UNKNOWN_PACKAGE,
153
- );
154
- }
155
-
156
- /**
157
- * Turn a single-lookup failure into one complete batch row.
158
- * @param {string} selector
159
- * @param {unknown} error
160
- * @returns {import('./component.type.mjs').ComponentBatchResult}
161
- */
162
- function batchFailure(selector, error) {
163
- const err =
164
- error instanceof AstryxError
165
- ? error
166
- : new AstryxError(
167
- error instanceof Error ? error.message : String(error),
168
- undefined,
169
- ERROR_CODES.ERR_UNKNOWN,
170
- );
171
- if (err instanceof ComponentAmbiguityError) {
172
- return {
173
- selector,
174
- status: 'ambiguous',
175
- code: err.code,
176
- error: err.message,
177
- candidates: err.candidates,
178
- };
179
- }
180
- return {
181
- selector,
182
- status: NOT_FOUND_CODES.has(err.code) ? 'not_found' : 'error',
183
- code: err.code,
184
- error: err.message,
185
- ...(err.suggestions ? {suggestions: err.suggestions} : {}),
186
- };
187
- }
188
41
 
189
42
  /**
190
- * @param {unknown[]} selectors
191
- * @param {object} options
192
- * @param {string} options.cwd
193
- * @param {string} [options.package]
194
- * @param {boolean} [options.props]
195
- * @param {boolean} [options.source]
196
- * @param {boolean} [options.showcase]
197
- * @param {boolean} [options.blocks]
198
- * @param {'full'|'compact'|'brief'} [options.detail]
199
- * @param {string|null} [options.lang]
200
- * @param {boolean} [options.zh]
201
- * @param {boolean} [options.dense]
202
- * @param {string} coreDir
203
- * @returns {Promise<import('./component.type.mjs').ComponentBatchResponse>}
204
- */
205
- async function componentBatch(selectors, options, coreDir) {
206
- const loadedIntegrations = await loadIntegrationsSafely(options.cwd);
207
- /** @type {import('./component.type.mjs').ComponentBatchResult[]} */
208
- const results = [];
209
- for (const value of selectors) {
210
- const selector = typeof value === 'string' ? value : String(value);
211
- try {
212
- const target = parseComponentSelector(value, options.package);
213
- requireInstalledSelectorVersion(target, coreDir, loadedIntegrations);
214
- const result =
215
- /** @type {import('./component.type.mjs').ComponentSingleResponse} */ (
216
- await component(target.name, {
217
- ...options,
218
- lang: options.lang ?? undefined,
219
- package: target.package,
220
- })
221
- );
222
- results.push({selector, status: 'found', result});
223
- } catch (error) {
224
- results.push(batchFailure(selector, error));
225
- }
226
- }
227
- return {type: 'component.batch', data: {count: results.length, results}};
228
- }
229
-
230
- /**
231
- * @param {string|string[]} [name]
43
+ * @param {string} [name]
232
44
  * @param {object} [options]
233
45
  * @param {string} [options.cwd]
234
46
  * @param {boolean} [options.list]
@@ -244,7 +56,6 @@ async function componentBatch(selectors, options, coreDir) {
244
56
  * @param {boolean} [options.dense]
245
57
  * @returns {Promise<(
246
58
  * import('./component.type.mjs').ComponentListResponse
247
- * | import('./component.type.mjs').ComponentBatchResponse
248
59
  * | import('./component.type.mjs').ComponentDetailResponse
249
60
  * | import('./component.type.mjs').ComponentDetailPropsResponse
250
61
  * | import('./component.type.mjs').ComponentDetailSourceResponse
@@ -268,16 +79,11 @@ export async function component(name, options = {}) {
268
79
  dense = false,
269
80
  } = options;
270
81
 
271
- const selectorList = Array.isArray(name) ? name : null;
272
- const noName = selectorList == null && !name;
273
-
274
82
  // Default detail level mirrors the CLI (see commands/component/index.mjs):
275
83
  // single-component views default to 'full', list-style views (--list,
276
84
  // --category, or no name) default to 'brief' (scannable name lists).
277
- // An explicit array remains a batch even when it is empty or a caller also
278
- // passes a list-only option.
279
- const isListView =
280
- selectorList == null && (list || category != null || noName);
85
+ // Keeping this in sync with the CLI is what the API↔CLI parity test checks.
86
+ const isListView = list || category != null || !name;
281
87
  const detail = detailOption ?? (isListView ? 'brief' : 'full');
282
88
 
283
89
  // Same accepted values and codes as the CLI's --detail and --lang, checked
@@ -297,14 +103,6 @@ export async function component(name, options = {}) {
297
103
  );
298
104
  }
299
105
 
300
- if (selectorList && selectorList.length > COMPONENT_BATCH_SELECTOR_LIMIT) {
301
- throw new AstryxError(
302
- `Component batch accepts at most ${COMPONENT_BATCH_SELECTOR_LIMIT} selectors; received ${selectorList.length}`,
303
- undefined,
304
- ERROR_CODES.ERR_INVALID_ARGUMENT,
305
- );
306
- }
307
-
308
106
  const coreDir = requireCoreDir(cwd);
309
107
 
310
108
  // A public API caller could pass a non-string category; the list leaf does
@@ -318,46 +116,11 @@ export async function component(name, options = {}) {
318
116
  );
319
117
  }
320
118
 
321
- // ── Explicit batch selector list ─────────────────────────────────
322
- // Array shape owns cardinality even if a caller also supplies a list-only
323
- // option. The aggregate limit above is checked before core or integrations
324
- // are resolved, so an oversized request can never emit a partial receipt.
325
- if (selectorList) {
326
- return componentBatch(
327
- selectorList,
328
- {
329
- cwd,
330
- package: packageScope,
331
- props,
332
- source,
333
- showcase,
334
- blocks,
335
- detail,
336
- lang,
337
- zh,
338
- dense,
339
- },
340
- coreDir,
341
- );
342
- }
343
-
344
119
  // ── List mode ──────────────────────────────────────────────────
345
- if (category || list || noName) {
120
+ if (category || list || !name) {
346
121
  return componentList(coreDir, {cwd, category, detail, zh, dense, lang});
347
122
  }
348
123
 
349
- if (typeof name === 'string') {
350
- const target = parseComponentSelector(name, packageScope);
351
- if (target.version) {
352
- const loadedIntegrations = await loadIntegrationsSafely(cwd);
353
- requireInstalledSelectorVersion(target, coreDir, loadedIntegrations);
354
- }
355
- if (target.name !== name || target.package !== packageScope) {
356
- return component(target.name, {...options, package: target.package});
357
- }
358
- name = target.name;
359
- }
360
-
361
124
  // ── Single component ───────────────────────────────────────────
362
125
  if (typeof name !== 'string') {
363
126
  throw new AstryxError(
@@ -423,10 +186,7 @@ export async function component(name, options = {}) {
423
186
  if (extDocPath) {
424
187
  // Legacy packages ship docs, never source.
425
188
  if (source) {
426
- return componentDetailSource(dirName, null, {
427
- name,
428
- notFoundInPackage: packageScope,
429
- });
189
+ return componentDetailSource(dirName, null, {name, notFoundInPackage: packageScope});
430
190
  }
431
191
  if (blocks) {
432
192
  return componentDetailBlocks(dirName, cwd);
@@ -28,31 +28,6 @@ export type ComponentListData = ({
28
28
  detail: "full";
29
29
  components: Record<string, import("@astryxdesign/cli/authoring").ComponentDoc[]>;
30
30
  });
31
- /**
32
- * `component(string[])` always returns this type, including empty and one-item
33
- * arrays. The CLI returns it for two or more positional selectors.
34
- */
35
- export type ComponentBatchResponse = import("../../foundation/response/batch.type.mjs").BatchResponse<"component.batch", ComponentSingleResponse, ComponentBatchCandidate>;
36
- /**
37
- * One installed component that makes an unqualified selector ambiguous.
38
- * Keys match a component row in `discover.search` so a caller does not learn a
39
- * second candidate shape.
40
- */
41
- export type ComponentBatchCandidate = {
42
- package: string;
43
- component: string;
44
- kind: "component";
45
- installed: true;
46
- };
47
- /**
48
- * The response a successful single selector would have returned.
49
- */
50
- export type ComponentSingleResponse = (ComponentDetailResponse | ComponentDetailPropsResponse | ComponentDetailSourceResponse | ComponentDetailShowcaseResponse | ComponentDetailBlocksResponse);
51
- /**
52
- * One row per requested selector, in argument order. Duplicate selectors keep
53
- * duplicate rows.
54
- */
55
- export type ComponentBatchResult = import("../../foundation/response/batch.type.mjs").BatchRow<ComponentSingleResponse, ComponentBatchCandidate>;
56
31
  /**
57
32
  * A single entry in a `component.list` group at `detail: 'names'`. Pre-1.0 the
58
33
  * list moved from bare strings to package-qualified objects so consumers can
@@ -18,9 +18,6 @@
18
18
  * astryx --json component --category Form -> component.list (filtered)
19
19
  * astryx --json component --list --detail compact -> component.list (data.detail='compact')
20
20
  * astryx --json component --list --detail full -> component.list (data.detail='full')
21
- * component([]) -> component.batch (data.count=0)
22
- * component(['Button']) -> component.batch (data.count=1)
23
- * astryx --json component Button Badge -> component.batch
24
21
  * astryx --json component Button -> component.detail
25
22
  * astryx --json component Button --props -> component.detail.props
26
23
  * astryx --json component Button --source -> component.detail.source
@@ -52,47 +49,6 @@
52
49
  * )} ComponentListData
53
50
  */
54
51
 
55
- /**
56
- * `component(string[])` always returns this type, including empty and one-item
57
- * arrays. The CLI returns it for two or more positional selectors.
58
- * @typedef {import('../../foundation/response/batch.type.mjs').BatchResponse<
59
- * 'component.batch',
60
- * ComponentSingleResponse,
61
- * ComponentBatchCandidate
62
- * >} ComponentBatchResponse
63
- */
64
-
65
- /**
66
- * One installed component that makes an unqualified selector ambiguous.
67
- * Keys match a component row in `discover.search` so a caller does not learn a
68
- * second candidate shape.
69
- * @typedef {object} ComponentBatchCandidate
70
- * @property {string} package
71
- * @property {string} component
72
- * @property {'component'} kind
73
- * @property {true} installed
74
- */
75
-
76
- /**
77
- * The response a successful single selector would have returned.
78
- * @typedef {(
79
- * | ComponentDetailResponse
80
- * | ComponentDetailPropsResponse
81
- * | ComponentDetailSourceResponse
82
- * | ComponentDetailShowcaseResponse
83
- * | ComponentDetailBlocksResponse
84
- * )} ComponentSingleResponse
85
- */
86
-
87
- /**
88
- * One row per requested selector, in argument order. Duplicate selectors keep
89
- * duplicate rows.
90
- * @typedef {import('../../foundation/response/batch.type.mjs').BatchRow<
91
- * ComponentSingleResponse,
92
- * ComponentBatchCandidate
93
- * >} ComponentBatchResult
94
- */
95
-
96
52
  /**
97
53
  * A single entry in a `component.list` group at `detail: 'names'`. Pre-1.0 the
98
54
  * list moved from bare strings to package-qualified objects so consumers can
@@ -146,7 +146,6 @@ describe('docs() dispatcher routing', () => {
146
146
  // A guide moves across its namespace's slot, as any tree node does.
147
147
  expect(index.data.links).toEqual({
148
148
  up: 'astryx docs cli',
149
- previous: 'astryx docs cli/component-lookups',
150
149
  next: 'astryx docs cli/writing-docs',
151
150
  });
152
151
  const first = (await docs('cli/integrations', index.data.sections[0].id)).data;
@@ -210,7 +209,6 @@ describe('docs() dispatcher routing', () => {
210
209
  expect(err).toBeInstanceOf(AstryxError);
211
210
  expect(err.code).toBe('ERR_UNKNOWN_SECTION');
212
211
  expect(err.suggestions.map(s => s.name)).toEqual([
213
- 'cli/component-lookups',
214
212
  'cli/integrations',
215
213
  'cli/writing-docs',
216
214
  'cli/commands',
@@ -257,20 +257,15 @@ export type DoctorContext = {
257
257
  * could not be built, when it could not.
258
258
  */
259
259
  docsCatalogError?: string | null | undefined;
260
+ /**
261
+ * Combined project-level integration issues, including cross-package template replacement warnings.
262
+ */
260
263
  integrationIssues?: {
261
264
  package: string;
262
265
  code: string;
263
266
  severity: "warning" | "error";
264
267
  message: string;
265
268
  }[] | null | undefined;
266
- /**
267
- * installed dependencies whose integration manifest could not be loaded
268
- * Combined project-level integration issues, including cross-package template replacement warnings.
269
- */
270
- autolinkFailures?: {
271
- spec: string;
272
- error: string;
273
- }[] | null | undefined;
274
269
  /**
275
270
  * - Error thrown while resolving the config
276
271
  * path (e.g. multiple config files present), surfaced by checkConfig as a FAIL.
@@ -26,11 +26,7 @@ import * as path from 'node:path';
26
26
  import {MIN_NODE_VERSION, isNodeVersionSupported} from '../../foundation/env/node-version.mjs';
27
27
  import {CLI_ROOT, findCoreDir, findInstalledPackage} from '../../foundation/fs/paths.mjs';
28
28
  import {explainPackageManager, getCliInvocation} from '../../foundation/env/package-manager.mjs';
29
- import {
30
- findConfigPath,
31
- Project,
32
- providerLedgerOf,
33
- } from '../../foundation/config/project.mjs';
29
+ import {findConfigPath, Project} from '../../foundation/config/project.mjs';
34
30
  import {DocsCatalog} from '../../foundation/discovery/docs-discovery.mjs';
35
31
  import {buildDocsIndexData} from '../../foundation/discovery/docs-section-key.mjs';
36
32
  import {
@@ -80,8 +76,6 @@ import {semverCompare, isValidSemver, satisfiesRange} from '../../foundation/env
80
76
  * @property {string|null} [docsCatalogError] - Why the project's docs catalog
81
77
  * could not be built, when it could not.
82
78
  * @property {Array<{package: string, code: string, severity: 'warning'|'error', message: string}>|null} [integrationIssues]
83
- * @property {Array<{spec: string, error: string}>|null} [autolinkFailures]
84
- * installed dependencies whose integration manifest could not be loaded
85
79
  * Combined project-level integration issues, including cross-package template replacement warnings.
86
80
  * @property {Error|null} [configError] - Error thrown while resolving the config
87
81
  * path (e.g. multiple config files present), surfaced by checkConfig as a FAIL.
@@ -423,9 +417,6 @@ export function checkImplicitIntegrations(ctx) {
423
417
  const implicit = ctx.integrations.filter(
424
418
  integration => integration.__autolinked,
425
419
  );
426
- // A dependency whose manifest cannot be loaded leaves no loaded record, so
427
- // without this line doctor would say no dependency ships a manifest at all.
428
- const unreadable = describeUnreadableManifests(ctx.autolinkFailures ?? []);
429
420
 
430
421
  if (implicit.length === 0) {
431
422
  return {
@@ -433,12 +424,9 @@ export function checkImplicitIntegrations(ctx) {
433
424
  label,
434
425
  status: 'info',
435
426
  message:
436
- (ctx.integrations.length > 0
427
+ ctx.integrations.length > 0
437
428
  ? 'None — every loaded integration is named in astryx.config.'
438
- : unreadable
439
- ? 'None loaded.'
440
- : 'None — no installed dependency ships an astryx.integration.* manifest.') +
441
- unreadable,
429
+ : 'None — no installed dependency ships an astryx.integration.* manifest.',
442
430
  };
443
431
  }
444
432
 
@@ -451,25 +439,11 @@ export function checkImplicitIntegrations(ctx) {
451
439
  integration.__spec && integration.__spec !== integration.name
452
440
  ? ` (declared as "${integration.__spec}")`
453
441
  : '';
454
- // A declared root counts only when it exists: the manifest's keys are a
455
- // claim, and `integration-issues` reports the ones that are not true.
456
- const declared = ['components', 'templates', 'themes', 'docs', 'codemods'].filter(
442
+ const roots = ['components', 'templates', 'themes', 'docs', 'codemods'].filter(
457
443
  root => integration[/** @type {'components'} */ (root)],
458
444
  );
459
- const missing = declared.filter(root => {
460
- const dir = integration[/** @type {'components'} */ (root)];
461
- if (typeof dir !== 'string') return false;
462
- const base =
463
- typeof integration.__packageDir === 'string'
464
- ? integration.__packageDir
465
- : (ctx.cwd ?? process.cwd());
466
- return !fs.existsSync(path.isAbsolute(dir) ? dir : path.resolve(base, dir));
467
- });
468
- const roots = declared.filter(root => !missing.includes(root));
469
445
  const contributes = roots.length > 0 ? roots.join(', ') : 'nothing';
470
- const absent =
471
- missing.length > 0 ? ` (declared ${missing.join(', ')} missing on disk)` : '';
472
- return `${integration.name}${version}${alias} from ${integration.__dependencyField}, contributing ${contributes}${absent}`;
446
+ return `${integration.name}${version}${alias} from ${integration.__dependencyField}, contributing ${contributes}`;
473
447
  });
474
448
 
475
449
  const plural = implicit.length === 1 ? '' : 's';
@@ -479,8 +453,7 @@ export function checkImplicitIntegrations(ctx) {
479
453
  status: 'info',
480
454
  message:
481
455
  `${implicit.length} integration${plural} loaded from installed ` +
482
- `dependencies with no astryx.config entry: ${described.join('; ')}.` +
483
- unreadable,
456
+ `dependencies with no astryx.config entry: ${described.join('; ')}.`,
484
457
  fix:
485
458
  'Nothing to fix. Keep these dependencies installed. The CLI links them ' +
486
459
  'from package.json, so an unused-dependency check that looks only for ' +
@@ -489,30 +462,6 @@ export function checkImplicitIntegrations(ctx) {
489
462
  };
490
463
  }
491
464
 
492
- /**
493
- * The sentence `implicit-integrations` adds for dependencies whose manifest
494
- * could not be loaded, or '' when there are none. Still informational: the
495
- * package is a dependency's own bug, which `doctor integration validate`
496
- * diagnoses, but doctor must not report it as absent.
497
- * @param {Array<{spec: string, error: string}>} failures
498
- * @returns {string}
499
- */
500
- function describeUnreadableManifests(failures) {
501
- if (failures.length === 0) return '';
502
- const one = failures.length === 1;
503
- const listed = failures
504
- .map(({spec, error}) => {
505
- const reason = String(error).split('\n')[0].slice(0, 160);
506
- return `${spec} (${reason})`;
507
- })
508
- .join('; ');
509
- return (
510
- ` ${failures.length} installed ${one ? 'dependency ships' : 'dependencies ship'} ` +
511
- `an astryx.integration.* manifest that could not be loaded, so ${one ? 'it contributes' : 'they contribute'} ` +
512
- `nothing: ${listed}. Run \`astryx doctor integration validate <package>\` for details.`
513
- );
514
- }
515
-
516
465
  /**
517
466
  * Check 7 — agent docs exist and contain the Astryx section markers.
518
467
  * @param {DoctorContext} ctx
@@ -789,26 +738,12 @@ export function checkProviderIdentity(ctx) {
789
738
  integration =>
790
739
  integration.providerId != null && integration.__loadError == null,
791
740
  ).length;
792
- // An integration that could not be read has no provider ID to check, so the
793
- // count above is not a complete survey. Say so instead of counting silently.
794
- const unread = ctx.integrations.filter(
795
- integration => integration.__loadError != null,
796
- ).length;
797
- const unreadNote =
798
- unread === 0
799
- ? ''
800
- : unread === 1
801
- ? ' 1 loaded integration could not be read, so its provider ID is unknown.'
802
- : ` ${unread} loaded integrations could not be read, so their provider IDs are unknown.`;
803
741
  if (count === 0) {
804
742
  return {
805
743
  id,
806
744
  label,
807
745
  status: 'info',
808
- message:
809
- (unread === 0
810
- ? 'None — no loaded integration has a provider identity.'
811
- : 'No readable integration has a provider identity.') + unreadNote,
746
+ message: 'None — no loaded integration has a provider identity.',
812
747
  };
813
748
  }
814
749
  return {
@@ -816,10 +751,9 @@ export function checkProviderIdentity(ctx) {
816
751
  label,
817
752
  status: 'pass',
818
753
  message:
819
- (count === 1
754
+ count === 1
820
755
  ? '1 loaded integration has its own provider ID.'
821
- : `${count} loaded integrations each have their own provider ID.`) +
822
- unreadNote,
756
+ : `${count} loaded integrations each have their own provider ID.`,
823
757
  };
824
758
  }
825
759
 
@@ -1264,25 +1198,11 @@ export async function runChecks(options = {}) {
1264
1198
  let docsCatalogError = null;
1265
1199
  /** @type {Array<{package: string, code: string, severity: 'warning'|'error', message: string}>|null} */
1266
1200
  let integrationIssues = null;
1267
- /** @type {Array<{spec: string, error: string}>|null} */
1268
- let autolinkFailures = null;
1269
1201
  try {
1270
1202
  const project = await Project.load(cwd);
1271
1203
  configTheme =
1272
1204
  /** @type {{theme?: string}} */ (project.config ?? {}).theme ?? null;
1273
1205
  integrations = project.loadedIntegrations;
1274
- // An installed dependency whose manifest cannot be loaded is kept out of
1275
- // loadedIntegrations on purpose. The provider ledger still records it.
1276
- autolinkFailures = [...providerLedgerOf(project).values()]
1277
- .filter(
1278
- entry =>
1279
- entry.outcome === 'load-failed' &&
1280
- entry.candidate.source === 'autolinked',
1281
- )
1282
- .map(entry => ({
1283
- spec: entry.candidate.spec ?? entry.label,
1284
- error: entry.error ?? 'its manifest could not be loaded',
1285
- }));
1286
1206
  try {
1287
1207
  docsCatalog = await project.docs();
1288
1208
  docsCatalogIssues = (await project.issues()).filter(
@@ -1309,7 +1229,6 @@ export async function runChecks(options = {}) {
1309
1229
  docsCatalogIssues,
1310
1230
  docsCatalogError,
1311
1231
  integrationIssues,
1312
- autolinkFailures,
1313
1232
  configError,
1314
1233
  };
1315
1234