@astryxdesign/cli 0.6.4-canary.ed2e54e → 0.6.4

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 (145) hide show
  1. package/README.md +66 -64
  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/discover/_adapter.d.mts +6 -114
  10. package/api/discover/_adapter.mjs +17 -372
  11. package/api/discover/detail/detail.d.mts +6 -18
  12. package/api/discover/detail/detail.mjs +13 -67
  13. package/api/discover/detail/detail.test.mjs +0 -85
  14. package/api/discover/discover.d.mts +9 -3
  15. package/api/discover/discover.doc.mjs +18 -61
  16. package/api/discover/discover.mjs +36 -220
  17. package/api/discover/discover.test.mjs +2 -11
  18. package/api/discover/discover.type.d.mts +8 -147
  19. package/api/discover/discover.type.mjs +12 -102
  20. package/api/discover/list/list.d.mts +6 -20
  21. package/api/discover/list/list.mjs +12 -45
  22. package/api/discover/list/list.test.mjs +0 -46
  23. package/api/discover/search/search.d.mts +16 -18
  24. package/api/discover/search/search.mjs +56 -102
  25. package/api/discover/search/search.test.mjs +10 -144
  26. package/api/docs/docs.test.mjs +0 -2
  27. package/api/doctor/doctor.d.mts +3 -8
  28. package/api/doctor/doctor.mjs +9 -90
  29. package/api/doctor/doctor.test.mjs +10 -122
  30. package/api/index.d.mts +2 -1
  31. package/api/index.mjs +4 -4
  32. package/api/integration/add-helpers.d.mts +2 -5
  33. package/api/integration/add-helpers.mjs +9 -36
  34. package/api/integration/pack-check.mjs +3 -28
  35. package/api/json/index.ts +1 -0
  36. package/api/layout/_adapter.d.mts +34 -0
  37. package/api/layout/_adapter.mjs +148 -0
  38. package/api/layout/check/check.d.mts +16 -0
  39. package/api/layout/check/check.mjs +40 -0
  40. package/api/layout/expand/expand.d.mts +22 -0
  41. package/api/layout/expand/expand.mjs +155 -0
  42. package/api/layout/expand/expand.path-safety.test.mjs +53 -0
  43. package/api/layout/grammar/grammar.d.mts +13 -0
  44. package/api/layout/grammar/grammar.mjs +87 -0
  45. package/api/layout/layout.d.mts +6 -0
  46. package/api/layout/layout.mjs +17 -0
  47. package/api/layout/layout.test.mjs +297 -0
  48. package/api/layout/layout.type.d.mts +89 -0
  49. package/api/layout/layout.type.mjs +103 -0
  50. package/api/layout/layoutCheck.doc.d.mts +11 -0
  51. package/api/layout/layoutCheck.doc.mjs +85 -0
  52. package/api/layout/layoutExpand.doc.d.mts +11 -0
  53. package/api/layout/layoutExpand.doc.mjs +107 -0
  54. package/api/layout/layoutGrammar.doc.d.mts +11 -0
  55. package/api/layout/layoutGrammar.doc.mjs +57 -0
  56. package/api/search/search.test.mjs +0 -18
  57. package/api/template/template-integration.test.mjs +65 -1
  58. package/api/template/template.mjs +1 -1
  59. package/api/theme/add/add.mjs +25 -17
  60. package/api/theme/add/add.staging.test.mjs +23 -40
  61. package/api/theme/build/build.family.test.mjs +12 -7
  62. package/api/theme/build/build.mjs +18 -8
  63. package/api/upgrade/run/run.mjs +4 -6
  64. package/api/upgrade/upgrade.type.mjs +2 -2
  65. package/assets/codemods/__tests__/runner.test.mjs +1 -3
  66. package/assets/codemods/integration-runner.mjs +3 -3
  67. package/assets/codemods/runner.mjs +4 -5
  68. package/assets/docs/internationalization.doc.mjs +5 -7
  69. package/assets/docs/tree/integrations.doc.mjs +1 -20
  70. package/assets/templates/blocks/components/InternationalizationProvider/InternationalizationProvider01ShippedLocale.tsx +1 -1
  71. package/authoring/config/config.doc.mjs +1 -9
  72. package/authoring/config/parse.d.mts +0 -2
  73. package/authoring/config/parse.mjs +0 -19
  74. package/authoring/config/parse.test.mjs +0 -8
  75. package/authoring/config/type.ts +2 -13
  76. package/authoring/doctypes/command/command.doc.mjs +1 -1
  77. package/authoring/doctypes/command/type.ts +1 -1
  78. package/authoring/index.d.mts +0 -1
  79. package/authoring/index.d.ts +0 -10
  80. package/authoring/index.mjs +0 -1
  81. package/clients/cli/command-result-coverage.test.mjs +7 -7
  82. package/clients/cli/commands/component/index.mjs +55 -152
  83. package/clients/cli/commands/component-ownership.test.mjs +0 -89
  84. package/clients/cli/commands/component.doc.mjs +6 -23
  85. package/clients/cli/commands/debug-result-summary.test.mjs +2 -2
  86. package/clients/cli/commands/discover.doc.mjs +9 -53
  87. package/clients/cli/commands/discover.mjs +118 -393
  88. package/clients/cli/commands/docs.test.mjs +0 -29
  89. package/clients/cli/commands/layout-check.doc.mjs +65 -0
  90. package/clients/cli/commands/layout-expand.doc.mjs +83 -0
  91. package/clients/cli/commands/layout-grammar.doc.mjs +30 -0
  92. package/clients/cli/commands/layout.doc.mjs +34 -0
  93. package/clients/cli/commands/layout.error-codes.test.mjs +66 -0
  94. package/clients/cli/commands/layout.exit-parity.test.mjs +41 -0
  95. package/clients/cli/commands/layout.mjs +275 -0
  96. package/clients/cli/commands/layout.path-help.test.mjs +33 -0
  97. package/clients/cli/commands/layout.stdin-cap.test.mjs +47 -0
  98. package/clients/cli/commands/layout.text-fields.test.mjs +39 -0
  99. package/clients/cli/commands/text-json-parity.test.mjs +17 -0
  100. package/clients/cli/index.mjs +4 -0
  101. package/clients/cli/lib/exit-codes.test.mjs +8 -1
  102. package/clients/cli/lib/json-shim.mjs +14 -24
  103. package/clients/cli/lib/json-shim.test.mjs +20 -6
  104. package/clients/cli/lib/manifest.mjs +8 -3
  105. package/clients/cli/lib/manifest.test.mjs +2 -5
  106. package/foundation/discovery/authoring-self-docs.mjs +0 -1
  107. package/foundation/discovery/template-adapter.mjs +1 -1
  108. package/foundation/doc-compiler/doc-loads.test.mjs +12 -0
  109. package/foundation/doc-compiler/tree.test.mjs +1 -9
  110. package/foundation/integrations/integrations.d.mts +1 -14
  111. package/foundation/integrations/integrations.mjs +1 -41
  112. package/foundation/integrations/integrations.test.mjs +0 -31
  113. package/foundation/response/response-types.doc.mjs +21 -15
  114. package/foundation/response/response-types.doc.test.mjs +0 -23
  115. package/foundation/xle/browser.d.mts +3 -3
  116. package/foundation/xle/browser.mjs +3 -3
  117. package/foundation/xle/expand.mjs +2 -2
  118. package/foundation/xle/parse.mjs +1 -1
  119. package/foundation/xle/print.mjs +2 -2
  120. package/foundation/xle/splice.mjs +1 -1
  121. package/package.json +9 -9
  122. package/api/discover/_adapter.test.mjs +0 -215
  123. package/api/discover/_catalog-view.d.mts +0 -115
  124. package/api/discover/_catalog-view.mjs +0 -203
  125. package/api/discover/_catalog-view.test.mjs +0 -128
  126. package/api/discover/detail/item/item.d.mts +0 -26
  127. package/api/discover/detail/item/item.mjs +0 -78
  128. package/api/discover/detail/item/item.test.mjs +0 -73
  129. package/api/integration/pack-check.lifecycle-output.test.mjs +0 -105
  130. package/api/theme/add/add.rollback.test.mjs +0 -158
  131. package/api/theme/build/build.rollback.test.mjs +0 -148
  132. package/api/upgrade/run/files-changed.test.mjs +0 -111
  133. package/assets/codemods/file-count.test.mjs +0 -163
  134. package/assets/docs/tree/component-lookups.doc.mjs +0 -149
  135. package/authoring/discover/discover.doc.d.mts +0 -13
  136. package/authoring/discover/discover.doc.mjs +0 -138
  137. package/authoring/discover/parse.d.mts +0 -24
  138. package/authoring/discover/parse.mjs +0 -128
  139. package/authoring/discover/parse.test.mjs +0 -124
  140. package/authoring/discover/type.ts +0 -87
  141. package/clients/cli/commands/component-batch.test.mjs +0 -341
  142. package/clients/cli/commands/discover.sources.test.mjs +0 -267
  143. package/clients/cli/lib/parse-error-format.test.mjs +0 -81
  144. package/foundation/response/batch.type.d.mts +0 -33
  145. package/foundation/response/batch.type.mjs +0 -34
@@ -0,0 +1,39 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file `layout expand` text output names its fields after the JSON keys, so
5
+ * the two views stay greppable against each other.
6
+ */
7
+
8
+ import {describe, it, expect, beforeEach, afterEach} from 'vitest';
9
+ import * as fs from 'node:fs';
10
+ import * as path from 'node:path';
11
+ import {runCli} from '../../../test-utils/run-cli.mjs';
12
+
13
+ const SLOW = 60_000;
14
+
15
+ describe('layout expand text fields mirror the JSON keys', () => {
16
+ let cwd;
17
+ beforeEach(() => {
18
+ // Inside the workspace so @astryxdesign/core resolves.
19
+ cwd = fs.mkdtempSync(path.join(process.cwd(), '.xle-text-fields-'));
20
+ });
21
+ afterEach(() => fs.rmSync(cwd, {recursive: true, force: true}));
22
+
23
+ it('labels every record field with a key of the layout.expand data', async () => {
24
+ const expr = 'V > B + {not-a-real-block}';
25
+ const json = await runCli(['layout', 'expand', expr, './a', '--loose', '--json'], cwd);
26
+ expect(json.status).toBe(0);
27
+ const {data} = JSON.parse(json.stdout);
28
+ expect(data.todos.length).toBeGreaterThan(0);
29
+
30
+ const human = await runCli(['layout', 'expand', expr, './b', '--loose'], cwd);
31
+ expect(human.status).toBe(0);
32
+ const fields = human.stdout
33
+ .split('\n')
34
+ .map(line => /^([A-Za-z]+):\s/.exec(line)?.[1])
35
+ .filter(Boolean);
36
+ expect(fields).toEqual(['componentsUsed', 'todos']);
37
+ for (const field of fields) expect(Object.keys(data)).toContain(field);
38
+ }, SLOW);
39
+ });
@@ -377,6 +377,23 @@ const CASES = [
377
377
  // Needs a packable project; pack itself may exit differently.
378
378
  skipFieldChecks: true,
379
379
  },
380
+ // ── Layout command: remove these cases when the layout command is deleted ──
381
+ {
382
+ name: 'layout check',
383
+ args: ['layout', 'check', 'Button'],
384
+ skipFieldChecks: true,
385
+ },
386
+ {
387
+ name: 'layout expand',
388
+ args: ['layout', 'expand', 'Button'],
389
+ skipFieldChecks: true,
390
+ },
391
+ {
392
+ name: 'layout grammar',
393
+ args: ['layout', 'grammar'],
394
+ skipFieldChecks: true,
395
+ },
396
+ // ── End layout cases ──
380
397
  {
381
398
  name: 'manifest',
382
399
  args: ['manifest'],
@@ -103,6 +103,9 @@ export const JSON_SUPPORTED = new Set([
103
103
  'doctor integration templates',
104
104
  'doctor integration components',
105
105
  'doctor integration docs',
106
+ 'layout expand',
107
+ 'layout check',
108
+ 'layout grammar',
106
109
  ]);
107
110
 
108
111
  /**
@@ -261,6 +264,7 @@ const commands = [
261
264
  {name: 'gap-report', path: './commands/gap-report.mjs', register: 'registerGapReport'},
262
265
  // agent-docs folded into init — functions still importable from agent-docs.mjs
263
266
  {name: 'template', path: './commands/template.mjs', register: 'registerTemplate'},
267
+ {name: 'layout', path: './commands/layout.mjs', register: 'registerLayout'},
264
268
  {name: 'upgrade', path: './commands/upgrade.mjs', register: 'registerUpgrade'},
265
269
  {name: 'theme', path: './commands/build-theme.mjs', register: 'registerTheme'},
266
270
  {name: 'integration', path: './commands/integration.mjs', register: 'registerIntegration'},
@@ -34,7 +34,7 @@ manifest.commands.forEach(walk);
34
34
 
35
35
  describe('command exit codes', () => {
36
36
  it('every CommandDoc documents its exit codes', () => {
37
- expect(commandDocs.length).toBeGreaterThan(25);
37
+ expect(commandDocs.length).toBeGreaterThan(30);
38
38
  for (const doc of commandDocs) {
39
39
  expect(doc.exitCodes?.length, doc.name).toBeGreaterThan(0);
40
40
  }
@@ -54,6 +54,13 @@ describe('command exit codes', () => {
54
54
  },
55
55
  );
56
56
 
57
+ it('bare `astryx layout` exits 1 in both modes, as documented', async () => {
58
+ const doc = commandDocs.find((d) => d.name === 'layout');
59
+ expect(doc.exitCodes.find((e) => e.code === 1)?.when).toMatch(/^no subcommand/);
60
+ expect((await runCli(['layout'])).status).toBe(1);
61
+ expect((await runCli(['layout', '--json'])).status).toBe(1);
62
+ });
63
+
57
64
  it('`astryx discover` with a blank query exits 1 only when packages are discovered', async () => {
58
65
  const doc = commandDocs.find((d) => d.name === 'discover');
59
66
  expect(doc.exitCodes.find((e) => e.code === 1)?.when).toMatch(/blank query when packages are discovered/);
@@ -33,12 +33,10 @@
33
33
  * shows help because the invocation failed (`help <unknown>`, or a
34
34
  * command group with no subcommand), which exits 1.
35
35
  *
36
- * Commander writes its own "error: ..." line via configureOutput.writeErr.
37
- * The shim drops that line in both modes. Under --json the error envelope
38
- * replaces it; in text mode `handleCommanderError` writes the Astryx line
39
- * instead (`Error: <message>`, the same message the envelope carries), so
40
- * a parse failure reads like every other CLI error. Other stderr output,
41
- * such as help printed as the failure report, still passes through.
36
+ * Non-JSON behavior is preserved exactly: every code path that printed
37
+ * to stderr before still prints to stderr. Commander writes its
38
+ * "error: ..." line via configureOutput.writeErr, which we pass
39
+ * through verbatim outside of --json mode.
42
40
  */
43
41
 
44
42
  import {API_VERSION, isJsonMode, toErrorEnvelope} from '../../../foundation/response/json.mjs';
@@ -251,20 +249,11 @@ function applyShimRecursively(cmd) {
251
249
  });
252
250
  cmd.configureOutput({
253
251
  writeOut: (str) => process.stdout.write(str),
254
- // Commander's own "error: ..." line never reaches the user. Under --json a
255
- // consumer parsing both streams must not see it alongside the envelope;
256
- // in text mode it is Commander's format, not Astryx's, so an invalid
257
- // global option (`--lang zh-Hans`) printed `error: option '--lang
258
- // <locale>' argument 'zh-Hans' is invalid…` where every other CLI error
259
- // prints `Error: …`. handleCommanderError writes the Astryx line below,
260
- // for both modes, from the same message.
261
- //
262
- // ONLY that line. Commander also writes HELP through this channel when it
263
- // shows help because the invocation failed (a command group with no
264
- // subcommand), and that output is still wanted in text mode.
265
252
  writeErr: (str) => {
253
+ // Suppress Commander's "error: ..." stderr line when --json is
254
+ // active, so a JSON consumer parsing both streams doesn't see
255
+ // it alongside the envelope. Non-JSON callers are unaffected.
266
256
  if (jsonActive()) return;
267
- if (/^error:\s/i.test(str)) return;
268
257
  process.stderr.write(str);
269
258
  },
270
259
  });
@@ -443,15 +432,16 @@ export function handleCommanderError(err) {
443
432
  code: commanderCodeToErrorCode(code, message),
444
433
  });
445
434
 
446
- // Real error paths. Strip Commander's "error: " prefix once: in the envelope
447
- // the key is already `error`, and in text mode the Astryx prefix replaces it.
448
- const cleaned = message.replace(/^error:\s*/i, '');
435
+ // Real error paths.
449
436
  if (jsonActive()) {
437
+ // Strip Commander's "error: " prefix — the envelope key is `error`
438
+ // already, doubled "error" is noise.
439
+ const cleaned = message.replace(/^error:\s*/i, '');
450
440
  emitJsonError(cleaned, undefined, commanderCodeToErrorCode(code, cleaned));
451
441
  } else {
452
- // Commander's own line was suppressed above, so a parse failure reads the
453
- // same as every other CLI error — the `Error: …` line cliError prints.
454
- process.stderr.write(`Error: ${cleaned}\n`);
442
+ // Non-JSON mode: Commander already wrote the "error: ..." line
443
+ // to stderr via configureOutput.writeErr before throwing the
444
+ // CommanderError. Nothing to do — exit with the original code.
455
445
  }
456
446
  process.exit(exitCode || 1);
457
447
  }
@@ -225,6 +225,23 @@ describe('--json shim: help shown for a failed invocation is an error envelope',
225
225
  expect(parsed.error).toMatch(/bogus/);
226
226
  });
227
227
 
228
+ it('astryx layout --json (group, no subcommand) emits ERR_MISSING_ARGUMENT, exit 1', async () => {
229
+ const {status, stdout, stderr} = await runCli(['layout', '--json']);
230
+ expect(status).toBe(1);
231
+ expect(stderr).toBe('');
232
+ const parsed = parseJson(stdout);
233
+ expect(parsed).not.toHaveProperty('type');
234
+ expect(parsed.code).toBe('ERR_MISSING_ARGUMENT');
235
+ expect(parsed.suggestions.map((s) => s.name)).toContain('layout expand');
236
+ });
237
+
238
+ it('astryx layout (no --json) still prints help to stderr, exit 1', async () => {
239
+ const {status, stdout, stderr} = await runCli(['layout']);
240
+ expect(status).toBe(1);
241
+ expect(stdout).toBe('');
242
+ expect(stderr).toMatch(/Usage: astryx layout/);
243
+ });
244
+
228
245
  it('the real binary emits the same envelope for help bogus --json', () => {
229
246
  // One program per process, so the root's own outputHelp patch is used here.
230
247
  const bin = fileURLToPath(new URL('../bin/astryx.mjs', import.meta.url));
@@ -240,9 +257,6 @@ describe('--json shim: help shown for a failed invocation is an error envelope',
240
257
  });
241
258
 
242
259
  it('a group added after install gets the same error envelope', () => {
243
- // This is the only coverage of the no-subcommand path: every group the CLI
244
- // ships has an action of its own (see command-result-coverage.test.mjs), so
245
- // a group without one has to be built here.
246
260
  const program = new Command('astryx');
247
261
  installJsonShim(program);
248
262
  // Added later, so only the prototype-level patch covers its outputHelp.
@@ -268,11 +282,11 @@ describe('--json shim: help shown for a failed invocation is an error envelope',
268
282
  expect(parsed.suggestions).toEqual([{name: 'late child', reason: 'available subcommand'}]);
269
283
  });
270
284
 
271
- it('astryx help theme --json still emits the help envelope, exit 0', async () => {
272
- const {status, stdout} = await runCli(['help', 'theme', '--json']);
285
+ it('astryx help layout --json still emits the help envelope, exit 0', async () => {
286
+ const {status, stdout} = await runCli(['help', 'layout', '--json']);
273
287
  expect(status).toBe(0);
274
288
  const parsed = parseJson(stdout);
275
289
  expect(parsed.type).toBe('help');
276
- expect(parsed.data.command).toBe('astryx theme');
290
+ expect(parsed.data.command).toBe('astryx layout');
277
291
  });
278
292
  });
@@ -51,7 +51,6 @@ export const RESPONSE_TYPES = {
51
51
  init: ['init.run', 'init.remove'],
52
52
  component: [
53
53
  'component.list',
54
- 'component.batch',
55
54
  'component.detail',
56
55
  'component.detail.props',
57
56
  'component.detail.source',
@@ -70,7 +69,6 @@ export const RESPONSE_TYPES = {
70
69
  'discover.list',
71
70
  'discover.detail',
72
71
  'discover.detail.doc',
73
- 'discover.item',
74
72
  'discover.search',
75
73
  ],
76
74
  search: ['search'],
@@ -100,6 +98,9 @@ export const RESPONSE_TYPES = {
100
98
  'doctor integration templates': ['integration.template-conflicts'],
101
99
  'doctor integration components': ['integration.component-conflicts'],
102
100
  'doctor integration docs': ['integration.doc-conflicts'],
101
+ 'layout expand': ['layout.expand'],
102
+ 'layout check': ['layout.check'],
103
+ 'layout grammar': ['layout.grammar'],
103
104
  };
104
105
 
105
106
  /**
@@ -110,7 +111,6 @@ const EXAMPLES = {
110
111
  component: [
111
112
  'astryx component',
112
113
  'astryx component XDSButton',
113
- 'astryx component Button Badge Text',
114
114
  'astryx component XDSButton --props --json',
115
115
  ],
116
116
  docs: [
@@ -180,6 +180,11 @@ const EXAMPLES = {
180
180
  'astryx doctor integration docs @acme/widgets --json',
181
181
  ],
182
182
  init: ['astryx init', 'astryx init --all --json'],
183
+ 'layout expand': [
184
+ `astryx layout expand 'V[g6] > C{card-callout}*4' ./src/Page.tsx`,
185
+ ],
186
+ 'layout check': [`astryx layout check 'A[cp6] > L > LC > S[p6]' --json`],
187
+ 'layout grammar': ['astryx layout grammar'],
183
188
  };
184
189
 
185
190
  /**
@@ -156,10 +156,7 @@ describe('manifest: shape', () => {
156
156
 
157
157
  it('derives arguments from Commander metadata', () => {
158
158
  const component = allEntries.find((c) => c.name === 'component');
159
- const names = component.arguments.find((a) => a.name === 'names');
160
- expect(names.required).toBe(false);
161
- expect(names.variadic).toBe(true);
162
- expect(names.description).toContain('Two or more return one ordered batch');
159
+ expect(component.arguments.map((a) => a.name)).toContain('name');
163
160
  const themeBuild = allEntries.find((c) => c.name === 'theme build');
164
161
  expect(themeBuild.arguments.map((a) => a.name)).toContain('files');
165
162
  const files = themeBuild.arguments.find((a) => a.name === 'files');
@@ -196,7 +193,7 @@ describe('manifest: e2e', () => {
196
193
  // Enriched: the full structured manifest is embedded.
197
194
  expect(parsed.data.manifest).toBeDefined();
198
195
  expect(parsed.data.manifest.commands.find((c) => c.name === 'component').responseTypes)
199
- .toEqual(expect.arrayContaining(['component.list', 'component.batch']));
196
+ .toContain('component.list');
200
197
  });
201
198
  });
202
199
 
@@ -33,7 +33,6 @@ export const AUTHORING_SELF_DOCS = [
33
33
  'config/config.doc.mjs',
34
34
  'debug/debug.doc.mjs',
35
35
  'gap-report/gap-report.doc.mjs',
36
- 'discover/discover.doc.mjs',
37
36
  'codemod/codemod.doc.mjs',
38
37
  'identity/identity.doc.mjs',
39
38
  'doctypes/base/graph-fields.doc.mjs',
@@ -4,7 +4,7 @@
4
4
  * @file Shared template discovery + IO.
5
5
  *
6
6
  * Owns everything the template leaves (list/show/skeleton/copy) AND other
7
- * commands (component, search, init, discover, Doctor integration)
7
+ * commands (component, layout, search, init, discover, Doctor integration)
8
8
  * share: template discovery across core/external/integration sources, the
9
9
  * template-spec loaders, and the cross-command helpers (stripTemplateAssetRefs,
10
10
  * findShowcase, findRelatedBlocks, extractComponents, listTemplates). The
@@ -191,6 +191,18 @@ const RUNNERS = {
191
191
  'import ./integration.doc.mjs',
192
192
  ],
193
193
  },
194
+ 'clients/cli/commands/layout.mjs': {
195
+ runs: SELF_DOCS,
196
+ sites: [
197
+ 'import ../../../api/layout/layoutCheck.doc.mjs',
198
+ 'import ../../../api/layout/layoutExpand.doc.mjs',
199
+ 'import ../../../api/layout/layoutGrammar.doc.mjs',
200
+ 'import ./layout-check.doc.mjs',
201
+ 'import ./layout-expand.doc.mjs',
202
+ 'import ./layout-grammar.doc.mjs',
203
+ 'import ./layout.doc.mjs',
204
+ ],
205
+ },
194
206
  'clients/cli/commands/search.mjs': {
195
207
  runs: SELF_DOCS,
196
208
  sites: [
@@ -497,14 +497,7 @@ describe("the CLI's own docs tree", () => {
497
497
  expect(
498
498
  tree.get('cli')?.slots.map(slot => [slot.name, slot.children]),
499
499
  ).toEqual([
500
- [
501
- 'guides',
502
- [
503
- 'cli/component-lookups',
504
- 'cli/integrations',
505
- 'cli/writing-docs',
506
- ],
507
- ],
500
+ ['guides', ['cli/integrations', 'cli/writing-docs']],
508
501
  ['reference', ['cli/commands', 'cli/api']],
509
502
  ]);
510
503
  expect(tree.get('cli/api')?.slots[0].children).toEqual([
@@ -573,7 +566,6 @@ describe("the CLI's own docs tree", () => {
573
566
  'commands',
574
567
  ]);
575
568
  expect(inputs.docs.map(d => [d.name, d.placement?.parent])).toEqual([
576
- ['component-lookups', 'namespace:cli'],
577
569
  ['integrations', 'namespace:cli'],
578
570
  ['writing-docs', 'namespace:cli'],
579
571
  ]);
@@ -45,7 +45,7 @@ export function findManifestPaths(dir: string): string[];
45
45
  * @param {string} file absolute manifest path
46
46
  * @param {string} [label] used in error messages
47
47
  * @param {{fresh?: boolean}} [options]
48
- * @returns {Promise<{manifest: import('../../authoring/integration/type').AstryxIntegration, unknownKeys: string[], debug?: import('../../authoring/debug/type').DebugEventHandler, gapReport?: import('../../authoring/gap-report/type').GapReportHandler, gapReportError?: string, discover?: import('../../authoring/discover/type').DiscoverSource, discoverError?: string, agentDocsError?: string}>}
48
+ * @returns {Promise<{manifest: import('../../authoring/integration/type').AstryxIntegration, unknownKeys: string[], debug?: import('../../authoring/debug/type').DebugEventHandler, gapReport?: import('../../authoring/gap-report/type').GapReportHandler, gapReportError?: string, agentDocsError?: string}>}
49
49
  */
50
50
  export function loadManifest(file: string, label?: string, { fresh }?: {
51
51
  fresh?: boolean;
@@ -55,8 +55,6 @@ export function loadManifest(file: string, label?: string, { fresh }?: {
55
55
  debug?: import("../../authoring/debug/type").DebugEventHandler;
56
56
  gapReport?: import("../../authoring/gap-report/type").GapReportHandler;
57
57
  gapReportError?: string;
58
- discover?: import("../../authoring/discover/type").DiscoverSource;
59
- discoverError?: string;
60
58
  agentDocsError?: string;
61
59
  }>;
62
60
  /**
@@ -156,9 +154,6 @@ export function loadIntegrations(specs?: string[], { cwd, fresh, resolveProvider
156
154
  * validated package-owned handler from the `gapReport` NAMED export.
157
155
  * @property {string} [__gapReportError] isolated named-handler validation error.
158
156
  * Named exports are not manifest keys — see {@link loadManifest}.
159
- * @property {import('../../authoring/discover/type').DiscoverSource} [__discover]
160
- * validated catalog source from the `discover` NAMED export.
161
- * @property {string} [__discoverError] isolated discover-source validation error.
162
157
  */
163
158
  /** Conventional manifest basenames, in load-precedence order. */
164
159
  export const MANIFEST_BASENAMES: string[];
@@ -250,12 +245,4 @@ export type LoadedIntegration = {
250
245
  * Named exports are not manifest keys — see {@link loadManifest}.
251
246
  */
252
247
  __gapReportError?: string | undefined;
253
- /**
254
- * validated catalog source from the `discover` NAMED export.
255
- */
256
- __discover?: import("../../authoring/discover/type").DiscoverSource | undefined;
257
- /**
258
- * isolated discover-source validation error.
259
- */
260
- __discoverError?: string | undefined;
261
248
  };
@@ -29,7 +29,6 @@ import {
29
29
  } from '../../authoring/integration/schema.mjs';
30
30
  import {importUserModule, findPresentFiles} from '../fs/module-loader.mjs';
31
31
  import {parseGapReportHandler} from '../../authoring/gap-report/parse.mjs';
32
- import {parseDiscoverSource} from '../../authoring/discover/parse.mjs';
33
32
  import {resolveProviders} from './provider-resolution.mjs';
34
33
 
35
34
  /**
@@ -81,9 +80,6 @@ import {resolveProviders} from './provider-resolution.mjs';
81
80
  * validated package-owned handler from the `gapReport` NAMED export.
82
81
  * @property {string} [__gapReportError] isolated named-handler validation error.
83
82
  * Named exports are not manifest keys — see {@link loadManifest}.
84
- * @property {import('../../authoring/discover/type').DiscoverSource} [__discover]
85
- * validated catalog source from the `discover` NAMED export.
86
- * @property {string} [__discoverError] isolated discover-source validation error.
87
83
  */
88
84
 
89
85
  /** Conventional manifest basenames, in load-precedence order. */
@@ -171,23 +167,6 @@ function parseGapReportHandlerExport(value, label) {
171
167
  }
172
168
  }
173
169
 
174
- /**
175
- * Parse the optional named discover source the same way: a malformed one is
176
- * isolated from every other contribution and reported by `astryx discover`.
177
- *
178
- * @param {unknown} value
179
- * @param {string} label
180
- * @returns {{source?: import('../../authoring/discover/type').DiscoverSource, error?: string}}
181
- */
182
- function parseDiscoverSourceExport(value, label) {
183
- if (value === undefined) return {};
184
- try {
185
- return {source: parseDiscoverSource(value, `${label} named export "discover"`)};
186
- } catch (error) {
187
- return {error: error instanceof Error ? error.message : String(error)};
188
- }
189
- }
190
-
191
170
  /**
192
171
  * Load and validate a manifest module's default export, while isolating the
193
172
  * optional `agentDocs` contribution from the manifest's other fields.
@@ -204,7 +183,7 @@ function parseDiscoverSourceExport(value, label) {
204
183
  * @param {string} file absolute manifest path
205
184
  * @param {string} [label] used in error messages
206
185
  * @param {{fresh?: boolean}} [options]
207
- * @returns {Promise<{manifest: import('../../authoring/integration/type').AstryxIntegration, unknownKeys: string[], debug?: import('../../authoring/debug/type').DebugEventHandler, gapReport?: import('../../authoring/gap-report/type').GapReportHandler, gapReportError?: string, discover?: import('../../authoring/discover/type').DiscoverSource, discoverError?: string, agentDocsError?: string}>}
186
+ * @returns {Promise<{manifest: import('../../authoring/integration/type').AstryxIntegration, unknownKeys: string[], debug?: import('../../authoring/debug/type').DebugEventHandler, gapReport?: import('../../authoring/gap-report/type').GapReportHandler, gapReportError?: string, agentDocsError?: string}>}
208
187
  */
209
188
  export async function loadManifest(
210
189
  file,
@@ -215,7 +194,6 @@ export async function loadManifest(
215
194
  const raw = mod?.default;
216
195
  const baseManifest = parseIntegrationBase(raw, label);
217
196
  const gapReport = parseGapReportHandlerExport(mod?.gapReport, label);
218
- const discover = parseDiscoverSourceExport(mod?.discover, label);
219
197
  const hasAgentDocs =
220
198
  raw != null &&
221
199
  typeof raw === 'object' &&
@@ -250,8 +228,6 @@ export async function loadManifest(
250
228
  : undefined,
251
229
  gapReport: gapReport.handler,
252
230
  gapReportError: gapReport.error,
253
- discover: discover.source,
254
- discoverError: discover.error,
255
231
  agentDocsError,
256
232
  };
257
233
  }
@@ -369,10 +345,6 @@ export async function loadLocalIntegration(packageDir, {fresh = false} = {}) {
369
345
  let gapReportHandler;
370
346
  /** @type {string | undefined} */
371
347
  let gapReportError;
372
- /** @type {import('../../authoring/discover/type').DiscoverSource | undefined} */
373
- let discoverSource;
374
- /** @type {string | undefined} */
375
- let discoverError;
376
348
  /** @type {string | undefined} */
377
349
  let agentDocsError;
378
350
  try {
@@ -382,8 +354,6 @@ export async function loadLocalIntegration(packageDir, {fresh = false} = {}) {
382
354
  debug: debugHandler,
383
355
  gapReport: gapReportHandler,
384
356
  gapReportError,
385
- discover: discoverSource,
386
- discoverError,
387
357
  agentDocsError,
388
358
  } = await loadManifest(manifestFile, `Integration ${spec}`, {fresh}));
389
359
  } catch (err) {
@@ -427,8 +397,6 @@ export async function loadLocalIntegration(packageDir, {fresh = false} = {}) {
427
397
  __debug: debugHandler,
428
398
  __gapReport: gapReportHandler,
429
399
  __gapReportError: gapReportError,
430
- __discover: discoverSource,
431
- __discoverError: discoverError,
432
400
  __spec: spec,
433
401
  __packageDir: packageDir,
434
402
  __packageExports: pkg.exports ?? null,
@@ -480,10 +448,6 @@ export async function loadIntegrations(
480
448
  let gapReportHandler;
481
449
  /** @type {string | undefined} */
482
450
  let gapReportError;
483
- /** @type {import('../../authoring/discover/type').DiscoverSource | undefined} */
484
- let discoverSource;
485
- /** @type {string | undefined} */
486
- let discoverError;
487
451
  /** @type {string | undefined} */
488
452
  let agentDocsError;
489
453
  try {
@@ -493,8 +457,6 @@ export async function loadIntegrations(
493
457
  debug: debugHandler,
494
458
  gapReport: gapReportHandler,
495
459
  gapReportError,
496
- discover: discoverSource,
497
- discoverError,
498
460
  agentDocsError,
499
461
  } = await loadManifest(manifestFile, `Integration ${spec}`, {fresh}));
500
462
  } catch (err) {
@@ -545,8 +507,6 @@ export async function loadIntegrations(
545
507
  __debug: debugHandler,
546
508
  __gapReport: gapReportHandler,
547
509
  __gapReportError: gapReportError,
548
- __discover: discoverSource,
549
- __discoverError: discoverError,
550
510
  __spec: spec,
551
511
  __packageDir: packageDir,
552
512
  __packageExports: pkg.exports ?? null,
@@ -785,34 +785,3 @@ describe('the `gapReport` named export', () => {
785
785
  expect(loaded.__unknownKeys).toEqual(['gapReport']);
786
786
  });
787
787
  });
788
-
789
- describe('the `discover` named export', () => {
790
- it('is carried out of the manifest module as __discover', async () => {
791
- writeManifestPackage(tmpDir, {
792
- body:
793
- `export async function discover() { return {schemaVersion: 1}; }\n` +
794
- `export default {issuesUrl: 'https://example.com/i'};\n`,
795
- });
796
-
797
- const [loaded] = await loadIntegrations(['@acme/widgets'], {cwd: tmpDir});
798
-
799
- expect(typeof loaded.__discover).toBe('function');
800
- expect(loaded.__discoverError).toBeUndefined();
801
- expect(loaded.__unknownKeys).toEqual([]);
802
- expect(loaded.issuesUrl).toBe('https://example.com/i');
803
- });
804
-
805
- it('keeps the manifest and records why when the export is not a function', async () => {
806
- writeManifestPackage(tmpDir, {
807
- body:
808
- `export const discover = {url: 'https://example.com/catalog.json'};\n` +
809
- `export default {issuesUrl: 'https://example.com/i'};\n`,
810
- });
811
-
812
- const [loaded] = await loadIntegrations(['@acme/widgets'], {cwd: tmpDir});
813
-
814
- expect(loaded.__discover).toBeUndefined();
815
- expect(loaded.__discoverError).toContain('must be a function');
816
- expect(loaded.issuesUrl).toBe('https://example.com/i');
817
- });
818
- });
@@ -37,11 +37,6 @@ export const doc = {
37
37
  description:
38
38
  'The component catalog grouped by category: `detail` (the level: names | compact | full) and `components`, the grouped map of names entries ({name, package, and optional canonical import for integrations}), brief entries, or a full ComponentDoc per entry.',
39
39
  },
40
- {
41
- value: 'component.batch',
42
- description:
43
- 'The component specialization of the shared `BatchResponse` and `BatchRow` types. An explicit programmatic selector array, or two or more CLI selectors, returns one ordered receipt: `count` plus `results`, one row per selector including duplicates. Every row carries `selector` and `status` (found | not_found | ambiguous | error). A found row carries `result`, the same {type, data} response as one selector. An ambiguous row carries `code`, `error`, and `candidates` ({package, component, kind, installed}). Other failed rows carry `code`, `error`, and optional `suggestions` ({name, reason}).',
44
- },
45
40
  {
46
41
  value: 'component.detail',
47
42
  description:
@@ -109,27 +104,21 @@ export const doc = {
109
104
  {
110
105
  value: 'discover.list',
111
106
  description:
112
- 'The integrations the project loads (name, category, components, version, a list per other kind they add, and latest when a source knows it); with a discover source, meta.available lists what the project could add and meta.sources reports each source; when empty it carries meta.configured to tell "nothing configured" from "nothing discovered".',
107
+ 'The configured external packages (name, category, components, version, description); when empty it carries meta.configured to tell "nothing configured" from "nothing discovered".',
113
108
  },
114
109
  {
115
110
  value: 'discover.detail',
116
- description:
117
- 'One package, for an @scope/name or @scope/name@version query: what the shown version adds, whether the project has it, and, when a source knows the package, its versions, latest release, and the command that adds it.',
111
+ description: 'A single external package entry, for an @scope/name query.',
118
112
  },
119
113
  {
120
114
  value: 'discover.detail.doc',
121
115
  description:
122
- 'The validated ComponentDoc for one installed component, for an @scope/name/Component query.',
123
- },
124
- {
125
- value: 'discover.item',
126
- description:
127
- 'One item that is not an installed component, for an @scope/name/<item> query: its kind, name, package, version, and whether the project has the package.',
116
+ 'The validated ComponentDoc for one external component: an @scope/name/Component query, or a free-text term resolving to exactly one component.',
128
117
  },
129
118
  {
130
119
  value: 'discover.search',
131
120
  description:
132
- 'The echoed query plus every matching item and package across all packages, each with its kind and whether the project has it, even when only one matches; total is set when --limit cut the list.',
121
+ 'The echoed query plus the matching {package, component} pairs, when a free-text term matches several components.',
133
122
  },
134
123
 
135
124
  // search
@@ -318,5 +307,22 @@ export const doc = {
318
307
  description:
319
308
  'The integration identity, structural issues, and Core doc overlaps. Each finding includes `severity` (`info` | `error`) and `relationship` (`replaces` | `extends` | `accidental`).',
320
309
  },
310
+
311
+ // layout (XLE/XLO)
312
+ {
313
+ value: 'layout.expand',
314
+ description:
315
+ 'The expansion: parsed form, generated TSX code, componentsUsed, states (count of useState hooks scaffolded), todos, blocksReferenced (each {name, mode}), warnings, and written (the output path, or null when nothing was written).',
316
+ },
317
+ {
318
+ value: 'layout.check',
319
+ description:
320
+ 'The validation result: a valid flag, the detected form, errors (each with line/col, message, formatted text, and suggestions), warnings, and the expression re-printed in both canonical surfaces (compact and outline).',
321
+ },
322
+ {
323
+ value: 'layout.grammar',
324
+ description:
325
+ "The XLE/XLO grammar cheatsheet: a text field with the full reference plus an aliases map (short name → canonical component) generated from this install's registry.",
326
+ },
321
327
  ],
322
328
  };
@@ -53,29 +53,6 @@ function expectNamed(type, fields) {
53
53
  }
54
54
 
55
55
  describe('response-types EnumDoc names every field', () => {
56
- it('component.batch names every row field and state', async () => {
57
- const res = await data(['component', 'Button', 'Badge']);
58
- expect(res.type).toBe('component.batch');
59
- expect(describedAs('component.batch')).toMatch(/BatchResponse.*BatchRow/);
60
- expectNamed('component.batch', [
61
- 'count',
62
- 'results',
63
- 'selector',
64
- 'status',
65
- 'result',
66
- 'code',
67
- 'error',
68
- 'candidates',
69
- 'package',
70
- 'component',
71
- 'kind',
72
- 'installed',
73
- 'suggestions',
74
- 'name',
75
- 'reason',
76
- ]);
77
- });
78
-
79
56
  it('component.detail: the ownership fields and parentDoc', async () => {
80
57
  const res = await data(['component', 'HStack']);
81
58
  expect(res.type).toBe('component.detail');