@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
@@ -6,15 +6,14 @@ import * as os from 'node:os';
6
6
  import * as path from 'node:path';
7
7
  import {themeAdd} from './add.mjs';
8
8
  import {listThemes} from '../_adapter.mjs';
9
+ import {isErrorCode} from '../../../foundation/response/error-codes.mjs';
9
10
 
10
11
  let tmpDir;
11
12
  let outsideDir;
12
13
 
13
14
  beforeEach(() => {
14
15
  tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-themeadd-staging-'));
15
- outsideDir = fs.mkdtempSync(
16
- path.join(os.tmpdir(), 'astryx-themeadd-outside-'),
17
- );
16
+ outsideDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-themeadd-outside-'));
18
17
  });
19
18
 
20
19
  afterEach(() => {
@@ -23,61 +22,45 @@ afterEach(() => {
23
22
  });
24
23
 
25
24
  /**
26
- * Where `theme add` writes the first file of `slug`.
25
+ * Put a symlink where `theme add` stages the first file of `slug`.
27
26
  * @param {string} slug
27
+ * @param {string} target
28
28
  */
29
- function firstDestination(slug) {
29
+ function plantStagingLink(slug, target) {
30
30
  const theme = listThemes().find(entry => entry.slug === slug);
31
31
  if (!theme) throw new Error(`missing bundled theme ${slug}`);
32
- const dest = path.join(tmpDir, 'src', 'themes', slug, theme.files[0]);
32
+ const first = theme.files[0];
33
+ const dest = path.join(tmpDir, 'src', 'themes', slug, first);
33
34
  fs.mkdirSync(path.dirname(dest), {recursive: true});
35
+ fs.symlinkSync(target, `${dest}.${process.pid}.tmp`);
34
36
  return dest;
35
37
  }
36
38
 
37
- // Staging names are unpredictable and created exclusively, so an entry planted
38
- // at a predictable name beside the destination is never written through.
39
39
  describe('themeAdd staging writes stay inside the project', () => {
40
- it('never writes through a link planted at a predictable staging name', async () => {
40
+ it('refuses a staging path that links outside the project', async () => {
41
41
  const victim = path.join(outsideDir, 'victim.txt');
42
42
  fs.writeFileSync(victim, 'outside\n');
43
- const dest = firstDestination('stone');
44
- fs.symlinkSync(victim, `${dest}.${process.pid}.tmp`);
45
-
46
- await themeAdd('stone', {cwd: tmpDir});
43
+ const dest = plantStagingLink('stone', victim);
47
44
 
45
+ await expect(themeAdd('stone', {cwd: tmpDir})).rejects.toMatchObject({
46
+ code: 'ERR_PATH_TRAVERSAL',
47
+ });
48
48
  expect(fs.readFileSync(victim, 'utf-8')).toBe('outside\n');
49
- expect(fs.lstatSync(dest).isFile()).toBe(true);
49
+ expect(fs.existsSync(dest)).toBe(false);
50
50
  });
51
51
 
52
- it('never creates a file through a dangling link at a predictable staging name', async () => {
52
+ it('never creates a file through a dangling staging link', async () => {
53
53
  const victim = path.join(outsideDir, 'created.txt');
54
- fs.symlinkSync(victim, `${firstDestination('stone')}.${process.pid}.tmp`);
55
-
56
- await themeAdd('stone', {cwd: tmpDir});
57
-
58
- expect(fs.existsSync(victim)).toBe(false);
59
- });
60
-
61
- it('refuses to replace a destination that links outside the project', async () => {
62
- const victim = path.join(outsideDir, 'victim.txt');
63
- fs.writeFileSync(victim, 'outside\n');
64
- const dest = firstDestination('stone');
65
- fs.symlinkSync(victim, dest);
54
+ plantStagingLink('stone', victim);
66
55
 
67
- await expect(
68
- themeAdd('stone', {cwd: tmpDir, overwrite: true}),
69
- ).rejects.toMatchObject({code: 'ERR_PATH_TRAVERSAL'});
70
- expect(fs.readFileSync(victim, 'utf-8')).toBe('outside\n');
71
- expect(fs.lstatSync(dest).isSymbolicLink()).toBe(true);
72
- });
56
+ let error;
57
+ try {
58
+ await themeAdd('stone', {cwd: tmpDir});
59
+ } catch (caught) {
60
+ error = caught;
61
+ }
73
62
 
74
- it('never creates a file through a dangling destination link', async () => {
75
- const victim = path.join(outsideDir, 'created.txt');
76
- fs.symlinkSync(victim, firstDestination('stone'));
77
-
78
- await expect(themeAdd('stone', {cwd: tmpDir})).rejects.toMatchObject({
79
- code: 'ERR_PATH_TRAVERSAL',
80
- });
63
+ expect(isErrorCode(error?.code)).toBe(true);
81
64
  expect(fs.existsSync(victim)).toBe(false);
82
65
  });
83
66
  });
@@ -251,18 +251,23 @@ describe('themeBuildFamily()', () => {
251
251
  const dir = makeDir();
252
252
  const files = writeFamily(dir);
253
253
  const outputDir = path.join(dir, 'themes');
254
- // A directory where the JS output goes fails its staging after the CSS
255
- // output was already staged.
256
- fs.mkdirSync(path.join(outputDir, `${FAMILY_KEY}.js`));
254
+ const blockedTmp = path.join(
255
+ outputDir,
256
+ `${FAMILY_KEY}.js.${process.pid}.tmp`,
257
+ );
258
+ fs.mkdirSync(blockedTmp);
257
259
 
258
260
  await expect(build(dir, files)).rejects.toThrow(
259
261
  /Failed to write theme outputs/,
260
262
  );
261
- expect(fs.existsSync(path.join(outputDir, `${FAMILY_KEY}.css`))).toBe(false);
262
- expect(fs.existsSync(path.join(outputDir, `${FAMILY_KEY}.d.ts`))).toBe(false);
263
+ for (const output of OUTPUTS) {
264
+ expect(fs.existsSync(path.join(outputDir, output))).toBe(false);
265
+ }
263
266
  expect(
264
- fs.readdirSync(outputDir).filter(name => name.includes('.tmp-')),
265
- ).toEqual([]);
267
+ fs.existsSync(
268
+ path.join(outputDir, `${FAMILY_KEY}.css.${process.pid}.tmp`),
269
+ ),
270
+ ).toBe(false);
266
271
  });
267
272
 
268
273
  it('rejects invalid graphs and keys before touching an existing trio', async () => {
@@ -53,7 +53,6 @@ import {
53
53
  } from '../../../foundation/fs/path-safety.mjs';
54
54
  import {ERROR_CODES} from '../../../foundation/response/error-codes.mjs';
55
55
  import {AstryxError} from '../../error.mjs';
56
- import {applyWrites} from '../../integration/add-helpers.mjs';
57
56
  import {logger} from '../../logger.mjs';
58
57
  import {loadComponentDoc} from '../../../foundation/discovery/component-loader.mjs';
59
58
  import {
@@ -225,15 +224,26 @@ function staleBuildOutputs(writes, cwd) {
225
224
  /** @param {Array<{dest: string, content: string}>} writes */
226
225
  function writeBuildOutputs(writes) {
227
226
  if (writes.length === 0) return;
227
+ /** @type {Array<{tmp: string, dest: string}>} */
228
+ const staged = [];
228
229
  try {
229
- applyWrites(
230
- writes.map(write => ({
231
- path: write.dest,
232
- contents: write.content,
233
- createOnly: false,
234
- })),
235
- );
230
+ fs.mkdirSync(path.dirname(writes[0].dest), {recursive: true});
231
+ for (const write of writes) {
232
+ const tmp = `${write.dest}.${process.pid}.tmp`;
233
+ fs.writeFileSync(tmp, write.content);
234
+ staged.push({tmp, dest: write.dest});
235
+ }
236
+ for (const stagedWrite of staged) {
237
+ fs.renameSync(stagedWrite.tmp, stagedWrite.dest);
238
+ }
236
239
  } catch (error) {
240
+ for (const stagedWrite of staged) {
241
+ try {
242
+ fs.rmSync(stagedWrite.tmp, {force: true});
243
+ } catch {
244
+ // Best effort: the command still fails and never reports success.
245
+ }
246
+ }
237
247
  const message = `Failed to write theme outputs: ${/** @type {Error} */ (error).message}`;
238
248
  throw new AstryxError(message, undefined, ERROR_CODES.ERR_WRITE_FAILED);
239
249
  }
@@ -114,7 +114,7 @@ export async function run(options = {}, {cwd = process.cwd()} = {}) {
114
114
  // Resolve the source dir against the API's cwd (not process.cwd()) so a
115
115
  // programmatic caller in another directory scans the right tree. Confine it to
116
116
  // cwd: --apply rewrites files in place, so a `..`-escaping or out-of-tree
117
- // absolute --path must be rejected (parity with template/theme/swizzle,
117
+ // absolute --path must be rejected (parity with template/theme/swizzle/layout,
118
118
  // and this is the most destructive command). allowAbsolute permits an absolute
119
119
  // path that still resolves inside cwd.
120
120
  let path_;
@@ -437,11 +437,9 @@ export async function run(options = {}, {cwd = process.cwd()} = {}) {
437
437
 
438
438
  const registryResult = await reconcileCompositions();
439
439
 
440
- // A file a core codemod AND an integration codemod both changed is one file.
441
- const mergedFilesChanged = new Set([
442
- ...(coreResult?.changedFiles ?? []),
443
- ...(integrationResult?.changedFiles ?? []),
444
- ]).size;
440
+ const mergedFilesChanged =
441
+ (coreResult?.totalFilesChanged ?? 0) +
442
+ (integrationResult?.totalFilesChanged ?? 0);
445
443
  const mergedTransformsApplied =
446
444
  (coreResult?.totalTransformsApplied ?? 0) +
447
445
  (integrationResult?.totalTransformsApplied ?? 0);
@@ -119,11 +119,11 @@
119
119
  * @property {RegistryCompositionSummary} [data.registryCompositions]
120
120
  * @property {boolean} [data.complete] False when protected required changes remain.
121
121
  * @property {'ERR_CODEMOD_PROTECTED'} [data.errorCode] Stable incomplete-result code when complete is false.
122
- * @property {number} [data.filesChanged] Distinct files changed across core + integration codemods (apply mode). One file that four codemods each changed counts once.
122
+ * @property {number} [data.filesChanged] Total files changed across core + integration codemods (apply mode).
123
123
  * @property {string[]} [data.modifiedFiles] Project-relative files changed or previewed.
124
124
  * @property {ProtectedCodemodFile[]} [data.protectedFiles] Protected files that still require a codemod change after regeneration.
125
125
  * @property {Array<{file: string, location?: string, reason: string}>} [data.declinedCandidates] Candidates left unchanged because proof was insufficient.
126
- * @property {number} [data.transformsApplied] Total codemod changes. A code or config codemod counts once for each file it changed, so one file changed by four of them counts four times; a project codemod counts once, however many files it writes.
126
+ * @property {number} [data.transformsApplied] Total transforms that reported a change.
127
127
  * @property {Array<{file: string, codemod: string, error: string}>} [data.errors] Per-codemod errors, when any codemod failed.
128
128
  */
129
129
 
@@ -46,9 +46,7 @@ describe('runCodemods — ordered dry-run state', () => {
46
46
  silent: true,
47
47
  });
48
48
 
49
- // One file that two transforms changed is one file and two changes.
50
- expect(preview.totalFilesChanged).toBe(1);
51
- expect(preview.totalTransformsApplied).toBe(2);
49
+ expect(preview.totalFilesChanged).toBe(2);
52
50
  expect(preview.changedFiles).toEqual([
53
51
  path.join(srcDir, 'a.ts'),
54
52
  path.join(srcDir, 'a.ts'),
@@ -67,6 +67,7 @@ export function runIntegrationCodemods(
67
67
  /** In-memory pipeline state keeps ordered dry-runs equivalent to apply. */
68
68
  const virtualContents = new Map(providedContents ?? []);
69
69
 
70
+ let totalFilesChanged = 0;
70
71
  let totalTransformsApplied = 0;
71
72
  /** @type {string[]} */
72
73
  const changedFiles = [];
@@ -114,6 +115,7 @@ export function runIntegrationCodemods(
114
115
  protection,
115
116
  contents: virtualContents,
116
117
  });
118
+ totalFilesChanged += r.filesChanged;
117
119
  totalTransformsApplied += r.filesChanged;
118
120
  changedFiles.push(...r.changedFiles);
119
121
  writtenFiles.push(...r.writtenFiles);
@@ -143,6 +145,7 @@ export function runIntegrationCodemods(
143
145
  protection,
144
146
  contents: virtualContents,
145
147
  });
148
+ totalFilesChanged += r.filesChanged;
146
149
  totalTransformsApplied += r.filesChanged;
147
150
  changedFiles.push(...r.changedFiles);
148
151
  writtenFiles.push(...r.writtenFiles);
@@ -151,9 +154,6 @@ export function runIntegrationCodemods(
151
154
  }
152
155
  }
153
156
 
154
- // A file several codemods changed is one file; transforms count each change.
155
- const totalFilesChanged = new Set(changedFiles).size;
156
-
157
157
  return {
158
158
  totalFilesChanged,
159
159
  totalTransformsApplied,
@@ -486,6 +486,7 @@ export async function runCodemods(
486
486
  (await import('jscodeshift')).default
487
487
  );
488
488
 
489
+ let totalFilesChanged = 0;
489
490
  let totalTransformsApplied = 0;
490
491
  let totalValidationBlocked = 0;
491
492
  /** @type {Array<{file: string, codemod: string, error: string}>} */
@@ -553,6 +554,7 @@ export async function runCodemods(
553
554
  protectionWriteCount = writtenFiles.length;
554
555
  }
555
556
  if (result.filesChanged > 0) {
557
+ totalFilesChanged += result.filesChanged;
556
558
  totalTransformsApplied += 1;
557
559
  }
558
560
  continue;
@@ -575,6 +577,7 @@ export async function runCodemods(
575
577
  changedFiles.push(...result.changedFiles);
576
578
  writtenFiles.push(...result.writtenFiles);
577
579
  if (result.filesChanged > 0) {
580
+ totalFilesChanged += result.filesChanged;
578
581
  totalTransformsApplied += result.filesChanged;
579
582
  }
580
583
  continue;
@@ -589,6 +592,7 @@ export async function runCodemods(
589
592
  protectedFiles.push(...result.protectedFiles);
590
593
  changedFiles.push(...result.changedFiles);
591
594
  writtenFiles.push(...result.writtenFiles);
595
+ totalFilesChanged += result.filesChanged;
592
596
  totalTransformsApplied += result.filesChanged;
593
597
  totalValidationBlocked += result.errors.filter(
594
598
  error =>
@@ -626,11 +630,6 @@ export async function runCodemods(
626
630
  );
627
631
  }
628
632
 
629
- // A file several codemods changed is one file. `totalTransformsApplied` is
630
- // unchanged: a code or config codemod counts each file it changed, and a
631
- // project codemod counts once. The two answer different questions.
632
- const totalFilesChanged = new Set(changedFiles).size;
633
-
634
633
  if (protectedFiles.length > 0) {
635
634
  const files = [...new Set(protectedFiles.map(item => item.file))];
636
635
  log.warn(
@@ -41,17 +41,15 @@ function App() {
41
41
  lang: 'tsx',
42
42
  label: 'Load an astryx locale catalog',
43
43
  code: `import {InternationalizationProvider} from '@astryxdesign/core/i18n';
44
- import frFR from '@astryxdesign/core/locales/fr-FR.generated.js';
44
+ import fr from '@astryxdesign/core/locales/fr.json';
45
45
 
46
- <InternationalizationProvider
47
- locale="fr-FR"
48
- messages={{'fr-FR': frFR}}>
46
+ <InternationalizationProvider locale="fr" messages={{fr}}>
49
47
  <App />
50
48
  </InternationalizationProvider>;`,
51
49
  },
52
50
  {
53
51
  type: 'prose',
54
- text: 'Astryx ships English and first-party translations for supported locales. Compact runtime modules from `@astryxdesign/core/locales/*.generated.js` contain only the messages apps need; the existing `@astryxdesign/core/locales/*.json` files retain translator context. Until a locale is available, apps can pass a local catalog in either shape. Missing keys fall back through the locale chain to English (for example, `pt-BR` walks to `pt`, then to shipped `en`).',
52
+ text: 'Astryx ships English today, with first-party translations for other locales on the roadmap. Until a locale is available from `@astryxdesign/core/locales/*`, apps can pass a local catalog with the same shape. See `@astryxdesign/core/locales/en.json` for the current key inventory. Missing keys fall back through the locale chain to English (for example, `pt-BR` walks to `pt`, then to shipped `en`).',
55
53
  },
56
54
  {
57
55
  type: 'prose',
@@ -292,7 +290,7 @@ export default function App() {
292
290
  },
293
291
  {
294
292
  type: 'prose',
295
- text: '`Catalog` types the rich `{defaultMessage, description?}` authoring shape. `RuntimeCatalog` types the generated key-to-message string map. `ProviderMessagesByLocale` accepts either shape for the provider, while `MessagesByLocale` keeps the original rich-only context shape.',
293
+ text: '`Catalog` types a single locale file; `MessagesByLocale` types the map passed to `messages`. A catalog entry uses the same `{defaultMessage, description?}` shape as `@astryxdesign/core/locales/en.json`.',
296
294
  },
297
295
  ],
298
296
  },
@@ -309,7 +307,7 @@ export default function App() {
309
307
  lang: 'tsx',
310
308
  label: 'Turn on pseudo-localization',
311
309
  code: `import {InternationalizationProvider} from '@astryxdesign/core/i18n';
312
- import pseudo from '@astryxdesign/core/locales/pseudo.generated.js';
310
+ import pseudo from '@astryxdesign/core/locales/pseudo.json';
313
311
 
314
312
  <InternationalizationProvider locale="pseudo" messages={{pseudo}}>
315
313
  <App />
@@ -440,25 +440,6 @@ export const docs = {
440
440
  },
441
441
  ],
442
442
  },
443
- {
444
- title: 'Discover source',
445
- category: 'guide',
446
- content: [
447
- {
448
- type: 'prose',
449
- text: '`astryx discover` lists the integrations an app has and, through discover sources, the ones it could add, with every version and what each one adds. An integration can supply a source by exporting an async function named `discover` from its integration module. Like `debug`, it is a named export, not a manifest field, so a CLI that predates it simply does not read it.',
450
- },
451
- {
452
- type: 'code',
453
- lang: 'typescript',
454
- code: "// astryx.integration.ts\nimport type {DiscoverSource} from '@astryxdesign/cli/authoring';\n\nexport const discover: DiscoverSource = async ({signal, package: name, version}) => {\n // Every package you know about, each with every version and what the\n // requested (else latest) version adds.\n return readCatalog({signal, name, version});\n};\n\nexport default {\n components: './components',\n};",
455
- },
456
- {
457
- type: 'prose',
458
- text: 'A project can set the same function as `discover` in `astryx.config`. Discover calls every source, the project one first and then each integration in load order, and one that throws, runs past 30 seconds, or returns an invalid catalog never hides the others. It keeps the last good answer from each source in the per-user cache and uses it, with its date, when the source cannot be reached. Discover only reads: it prints the command that adds a package and never runs it. `astryx docs authoring discover-source` has the catalog shape.',
459
- },
460
- ],
461
- },
462
443
  {
463
444
  title: 'How It Works',
464
445
  category: 'guide',
@@ -469,7 +450,7 @@ export const docs = {
469
450
  },
470
451
  {
471
452
  type: 'prose',
472
- text: 'Runtime integration features — `debug`, `gapReport`, and `discover` — use named exports from the integration module rather than fields in the default manifest. The CLI discovers them alongside the manifest but loads them through the composition rules in `spec:AST-031`: every configured handler runs additively, each in isolation with its own copy of the event.',
453
+ text: 'Runtime integration features — `debug` and `gapReport` — use named exports from the integration module rather than fields in the default manifest. The CLI discovers them alongside the manifest but loads them through the composition rules in `spec:AST-031`: every configured handler runs additively, each in isolation with its own copy of the event.',
473
454
  },
474
455
  {
475
456
  type: 'prose',
@@ -4,7 +4,7 @@
4
4
 
5
5
  import {useState} from 'react';
6
6
  import {InternationalizationProvider} from '@astryxdesign/core/i18n';
7
- import frFR from '@astryxdesign/core/locales/fr-FR.generated.js';
7
+ import frFR from '@astryxdesign/core/locales/fr-FR.json';
8
8
  import {Stack} from '@astryxdesign/core/Layout';
9
9
  import {
10
10
  SegmentedControl,
@@ -60,14 +60,6 @@ export const doc = {
60
60
  example:
61
61
  "{ audience: 'internal', async handle(report, {signal}) { return sendGap(report, {signal}); } }",
62
62
  },
63
- {
64
- name: 'discover',
65
- type: 'DiscoverSource',
66
- description:
67
- 'Tell `astryx discover` which integrations this project could add: an async function that returns a catalog. An integration can provide one too, as a `discover` named export from its manifest. Discover calls every source, yours first, and one that fails never hides the others. Discover only reads; your package manager installs.',
68
- example:
69
- "async ({signal, package: name, version}) => fetchCatalog({signal, name, version})",
70
- },
71
63
  {
72
64
  name: 'experimental',
73
65
  type: '{ xle?: { components?: Record<string, XleComponent> } }',
@@ -77,7 +69,7 @@ export const doc = {
77
69
  name: 'experimental.xle.components',
78
70
  type: 'Record<string, XleComponent>',
79
71
  description:
80
- 'No effect. Its only reader was the removed `layout` command. The key is still accepted so existing configs keep loading; delete it.',
72
+ 'Custom components the layout expander (XLE) may emit, keyed by tag.',
81
73
  },
82
74
  ],
83
75
  },
@@ -30,7 +30,6 @@ export type XleComponent = import("./type.js").XleComponent;
30
30
  export type DebugConfig = import("./type.js").DebugConfig;
31
31
  export type DebugEventHandler = import("../debug/type.js").DebugEventHandler;
32
32
  export type GapReportHandler = import("../gap-report/type.js").GapReportHandler;
33
- export type DiscoverSource = import("../discover/type.js").DiscoverSource;
34
33
  import { z } from 'zod';
35
34
  declare const configSchema: z.ZodObject<{
36
35
  integrations: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -49,7 +48,6 @@ declare const configSchema: z.ZodObject<{
49
48
  }, z.core.$strict>>;
50
49
  debug: z.ZodOptional<z.ZodType<import("../debug/type.js").DebugEventHandler, any, z.core.$ZodTypeInternals<import("../debug/type.js").DebugEventHandler, any>>>;
51
50
  gapReport: z.ZodOptional<z.ZodType<import("../gap-report/type.js").GapReportHandler, any, z.core.$ZodTypeInternals<import("../gap-report/type.js").GapReportHandler, any>>>;
52
- discover: z.ZodOptional<z.ZodType<import("../discover/type.js").DiscoverSource, any, z.core.$ZodTypeInternals<import("../discover/type.js").DiscoverSource, any>>>;
53
51
  experimental: z.ZodOptional<z.ZodObject<{
54
52
  xle: z.ZodOptional<z.ZodObject<{
55
53
  components: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
@@ -12,7 +12,6 @@
12
12
  import {z} from 'zod';
13
13
  import {formatZodError} from '../_shared/errors.mjs';
14
14
  import {parseGapReportHandler} from '../gap-report/parse.mjs';
15
- import {parseDiscoverSource} from '../discover/parse.mjs';
16
15
 
17
16
  /** @typedef {import('./type.js').AstryxConfig} AstryxConfig */
18
17
  /** @typedef {import('./type.js').PostCodemodHook} PostCodemodHook */
@@ -20,7 +19,6 @@ import {parseDiscoverSource} from '../discover/parse.mjs';
20
19
  /** @typedef {import('./type.js').DebugConfig} DebugConfig */
21
20
  /** @typedef {import('../debug/type.js').DebugEventHandler} DebugEventHandler */
22
21
  /** @typedef {import('../gap-report/type.js').GapReportHandler} GapReportHandler */
23
- /** @typedef {import('../discover/type.js').DiscoverSource} DiscoverSource */
24
22
 
25
23
  // Typed `z.custom` so `z.infer` reproduces the real function type (not `unknown`).
26
24
  const buildCommand = /** @type {z.ZodType<PostCodemodHook['buildCommand']>} */ (
@@ -68,22 +66,6 @@ const gapReportHandlerSchema = /** @type {z.ZodType<GapReportHandler>} */ (
68
66
  )
69
67
  );
70
68
 
71
- // The same check an integration's `discover` named export passes. Typed
72
- // z.custom preserves the public function type.
73
- const discoverSourceSchema = /** @type {z.ZodType<DiscoverSource>} */ (
74
- z.custom(
75
- value => {
76
- try {
77
- parseDiscoverSource(value, 'discover');
78
- return true;
79
- } catch {
80
- return false;
81
- }
82
- },
83
- {message: 'Expected a discover source function'},
84
- )
85
- );
86
-
87
69
  const configSchema = z
88
70
  .object({
89
71
  integrations: z.array(z.string()).optional(),
@@ -94,7 +76,6 @@ const configSchema = z
94
76
  .optional(),
95
77
  debug: debugSchema.optional(),
96
78
  gapReport: gapReportHandlerSchema.optional(),
97
- discover: discoverSourceSchema.optional(),
98
79
  experimental: z
99
80
  .object({
100
81
  xle: z
@@ -63,14 +63,6 @@ describe('parseConfig (load boundary)', () => {
63
63
  ).toEqual({audience: 'internal', handle});
64
64
  });
65
65
 
66
- it('accepts a discover source function and refuses anything else', () => {
67
- const discover = async () => ({});
68
- expect(parseConfig({discover}).discover).toBe(discover);
69
- expect(reason({discover: 'https://example.com/catalog.json'})).toContain(
70
- 'discover',
71
- );
72
- });
73
-
74
66
  it('rejects obsolete or extended gap-report handler shapes', () => {
75
67
  expect(reason({gapReport: {command: './report.mjs'}})).toContain(
76
68
  'gapReport',
@@ -11,7 +11,6 @@
11
11
 
12
12
  import type {DebugEventHandler} from '../debug/type.js';
13
13
  import type {GapReportHandler} from '../gap-report/type.js';
14
- import type {DiscoverSource} from '../discover/type.js';
15
14
 
16
15
  /**
17
16
  * A command to run as part of a post-codemod hook. Returned by a hook's
@@ -97,16 +96,6 @@ export interface AstryxConfig {
97
96
  debug?: DebugConfig;
98
97
  /** Route gap reports through a project-owned handler. See {@link GapReportHandler}. */
99
98
  gapReport?: GapReportHandler;
100
- /**
101
- * Tell `astryx discover` about integrations this project could add. See
102
- * {@link DiscoverSource}.
103
- *
104
- * An integration can provide a source too, as a `discover` named export from
105
- * its `astryx.integration.*` module. Discover calls every source: this one
106
- * first, then each integration's in load order, and one that fails never
107
- * hides the others.
108
- */
109
- discover?: DiscoverSource;
110
99
  /**
111
100
  * EXPERIMENTAL — shape may change and is not part of the stable config
112
101
  * contract. Provisional home for features still being proven out.
@@ -115,8 +104,8 @@ export interface AstryxConfig {
115
104
  /** Experimental XLE (layout expression) configuration. */
116
105
  xle?: {
117
106
  /**
118
- * No effect. Its only reader was the removed `layout` command. Still
119
- * accepted so existing configs keep loading; delete it.
107
+ * Register app-local components so XLE layout expressions can
108
+ * reference them by name via {hint}. Keyed by component name.
120
109
  */
121
110
  components?: Record<string, XleComponent>;
122
111
  };
@@ -151,7 +151,7 @@ export const doc = {
151
151
  name: 'subcommands',
152
152
  type: 'string[]',
153
153
  description:
154
- 'Subcommand names (for command groups like `theme`).',
154
+ 'Subcommand names (for command groups like `theme` / `layout`).',
155
155
  },
156
156
  {
157
157
  name: 'examples',
@@ -75,7 +75,7 @@ export interface CommandDoc extends AuthoredDocGraphFields {
75
75
  args?: CommandArgDoc[];
76
76
  /** Flags/options. */
77
77
  options?: CommandOptionDoc[];
78
- /** Subcommand names (for command groups like `theme`). */
78
+ /** Subcommand names (for command groups like `theme` / `layout`). */
79
79
  subcommands?: string[];
80
80
  /** Terminal examples. */
81
81
  examples?: CommandExampleDoc[];
@@ -3,7 +3,6 @@
3
3
 
4
4
  export { parseConfig } from "./config/parse.mjs";
5
5
  export { parseIntegration } from "./integration/parse.mjs";
6
- export { parseDiscoverCatalog } from "./discover/parse.mjs";
7
6
  export { parseCodemod } from "./codemod/parse.mjs";
8
7
  export { parseDebugEvent } from "./debug/parse.mjs";
9
8
  export { parseDoc } from "./doctypes/parse.mjs";
@@ -44,15 +44,6 @@ export type {
44
44
  GapReportTarget,
45
45
  GapReportHandlerReceipt,
46
46
  } from './gap-report/type.js'; // gap-report handler contract
47
- export type {
48
- DiscoverSource,
49
- DiscoverSourceContext,
50
- DiscoverCatalog,
51
- DiscoverPackage,
52
- DiscoverVersion,
53
- DiscoverContribution,
54
- DiscoverKind,
55
- } from './discover/type.js'; // discover source contract
56
47
  export type {AstryxCodemod, AstryxConfigCodemod} from './codemod/type.js'; // codemods/*
57
48
 
58
49
  // ═══════════════════════════════════════════════════════════════════════
@@ -76,7 +67,6 @@ export {
76
67
  parseGapReportHandler,
77
68
  parseGapReportReceipt,
78
69
  } from './gap-report/parse.mjs';
79
- export {parseDiscoverCatalog} from './discover/parse.mjs';
80
70
  export {parseCodemod} from './codemod/parse.mjs';
81
71
  export {parseDebugEvent} from './debug/parse.mjs';
82
72
 
@@ -20,7 +20,6 @@ export {
20
20
  parseGapReportHandler,
21
21
  parseGapReportReceipt,
22
22
  } from './gap-report/parse.mjs';
23
- export {parseDiscoverCatalog} from './discover/parse.mjs';
24
23
  export {parseCodemod} from './codemod/parse.mjs';
25
24
  export {parseDebugEvent} from './debug/parse.mjs';
26
25
  export {parseDoc} from './doctypes/parse.mjs';
@@ -29,17 +29,17 @@ import {program} from './index.mjs';
29
29
  import {reportsResult, reportsResultVia} from './lib/define-command.mjs';
30
30
 
31
31
  /**
32
- * Commands with no action of their own: a group that prints its subcommand
33
- * list and does nothing else has no run to report on.
32
+ * Commands with no action of their own: bare `astryx layout` prints its
33
+ * subcommand list and does nothing else, so there is no run to report on.
34
34
  *
35
35
  * Pinned as a SET rather than skipped silently — if a command joins this list,
36
36
  * that is a real change to what the CLI does and it should be read, not
37
- * absorbed. Every group ships an action today: `theme`, for instance, HAS one
38
- * — the one that rejects an unknown subcommand — so it goes through the
39
- * converter like any other. Its bare form prints help and never returns from
40
- * that action, which the recorder covers where help is recorded, not here.
37
+ * absorbed. (`theme` is deliberately NOT here: it HAS an action — the one that
38
+ * rejects an unknown subcommand — so it goes through the converter like any
39
+ * other. Its bare form prints help and never returns from that action, which
40
+ * the recorder covers where help is recorded, not here.)
41
41
  */
42
- const NO_ACTION_OF_THEIR_OWN = [];
42
+ const NO_ACTION_OF_THEIR_OWN = ['layout'];
43
43
 
44
44
  /**
45
45
  * Commands that record for themselves instead of returning a descriptor.