@astryxdesign/cli 0.6.4-canary.06c8fa3 → 0.6.4-canary.0e1fbdb

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 (254) hide show
  1. package/README.md +49 -40
  2. package/api/build/build.doc.mjs +6 -1
  3. package/api/build/build.test.mjs +22 -0
  4. package/api/build/kit/kit.mjs +44 -5
  5. package/api/component/component.doc.mjs +14 -7
  6. package/api/docs/_adapter.d.mts +8 -3
  7. package/api/docs/_adapter.mjs +14 -6
  8. package/api/docs/docOverlays.test.mjs +27 -1
  9. package/api/docs/docs.doc.mjs +2 -2
  10. package/api/doctor/doctor.doc.mjs +17 -8
  11. package/api/doctor/doctor.type.d.mts +1 -1
  12. package/api/doctor/doctor.type.mjs +1 -1
  13. package/api/gap-report/gap-report.doc.mjs +19 -10
  14. package/api/hook/hook.doc.mjs +6 -3
  15. package/api/index.d.mts +2 -0
  16. package/api/index.mjs +3 -1
  17. package/api/init/init.doc.mjs +17 -12
  18. package/api/integration/add-theme.mjs +22 -1
  19. package/api/integration/add-theme.test.mjs +34 -0
  20. package/api/integration/authoring-checks.mjs +2 -2
  21. package/api/integration/integrationPackCheck.doc.mjs +3 -3
  22. package/api/integration/pack-check.lifecycle-output.test.mjs +2 -0
  23. package/api/integration/pack-check.mjs +54 -6
  24. package/api/integration/pack-check.test.mjs +90 -0
  25. package/api/integration/pack-check.type.mjs +1 -1
  26. package/api/json/assertResponse.doc.mjs +1 -1
  27. package/api/json/index.ts +1 -0
  28. package/api/json/isError.doc.mjs +1 -1
  29. package/api/layout/_adapter.d.mts +34 -0
  30. package/api/layout/_adapter.mjs +148 -0
  31. package/api/layout/check/check.d.mts +16 -0
  32. package/api/layout/check/check.mjs +40 -0
  33. package/api/layout/expand/expand.d.mts +22 -0
  34. package/api/layout/expand/expand.mjs +155 -0
  35. package/api/layout/expand/expand.path-safety.test.mjs +53 -0
  36. package/api/layout/grammar/grammar.d.mts +13 -0
  37. package/api/layout/grammar/grammar.mjs +87 -0
  38. package/api/layout/layout.d.mts +6 -0
  39. package/api/layout/layout.mjs +17 -0
  40. package/api/layout/layout.test.mjs +297 -0
  41. package/api/layout/layout.type.d.mts +89 -0
  42. package/api/layout/layout.type.mjs +103 -0
  43. package/api/layout/layoutCheck.doc.d.mts +11 -0
  44. package/api/layout/layoutCheck.doc.mjs +85 -0
  45. package/api/layout/layoutExpand.doc.d.mts +11 -0
  46. package/api/layout/layoutExpand.doc.mjs +107 -0
  47. package/api/layout/layoutGrammar.doc.d.mts +11 -0
  48. package/api/layout/layoutGrammar.doc.mjs +57 -0
  49. package/api/search/search.d.mts +27 -1
  50. package/api/search/search.doc.mjs +2 -2
  51. package/api/search/search.mjs +228 -16
  52. package/api/swizzle/swizzle.doc.mjs +7 -5
  53. package/api/template/copy/copy.mjs +1 -1
  54. package/api/template/copy/copy.test.mjs +9 -0
  55. package/api/template/template-integration.test.mjs +65 -1
  56. package/api/template/template.doc.mjs +2 -1
  57. package/api/template/template.mjs +1 -1
  58. package/api/theme/generateTonalPalette.doc.mjs +1 -2
  59. package/api/theme/listThemes.doc.mjs +1 -1
  60. package/api/theme/themeAdd.doc.mjs +9 -10
  61. package/api/theme/themeBuild.doc.mjs +13 -13
  62. package/api/theme/themeList.doc.mjs +1 -1
  63. package/api/theme/themeListAvailable.doc.mjs +2 -1
  64. package/api/theme/themePaletteGenerate.doc.mjs +15 -8
  65. package/api/theme/themeTargets.doc.mjs +3 -2
  66. package/api/theme/themeTemplate.doc.mjs +2 -1
  67. package/api/upgrade/run/run.mjs +1 -1
  68. package/api/upgrade/upgrade.doc.mjs +24 -22
  69. package/assets/docs/README.md +4 -2
  70. package/assets/docs/browser-support.doc.mjs +11 -11
  71. package/assets/docs/color.doc.mjs +8 -2
  72. package/assets/docs/elevation.doc.mjs +6 -4
  73. package/assets/docs/getting-started.doc.mjs +5 -16
  74. package/assets/docs/icons.doc.mjs +2 -21
  75. package/assets/docs/illustrations.doc.mjs +7 -15
  76. package/assets/docs/layout.doc.dense.mjs +130 -82
  77. package/assets/docs/layout.doc.mjs +133 -77
  78. package/assets/docs/migration.doc.mjs +19 -21
  79. package/assets/docs/motion.doc.mjs +16 -3
  80. package/assets/docs/principles.doc.dense.mjs +5 -5
  81. package/assets/docs/principles.doc.mjs +8 -0
  82. package/assets/docs/principles.doc.zh.mjs +6 -6
  83. package/assets/docs/shape.doc.mjs +8 -3
  84. package/assets/docs/spacing.doc.mjs +7 -2
  85. package/assets/docs/styling-libraries.doc.mjs +6 -2
  86. package/assets/docs/styling.doc.mjs +19 -23
  87. package/assets/docs/theme.doc.dense.mjs +58 -18
  88. package/assets/docs/theme.doc.mjs +56 -46
  89. package/assets/docs/theme.doc.zh.mjs +9 -8
  90. package/assets/docs/tokens.doc.dense.mjs +2 -2
  91. package/assets/docs/tokens.doc.mjs +389 -8
  92. package/assets/docs/tokens.doc.zh.mjs +2 -2
  93. package/assets/docs/tree/add-a-component.doc.mjs +75 -0
  94. package/assets/docs/tree/add-a-theme.doc.mjs +85 -0
  95. package/assets/docs/tree/add-a-topic.doc.mjs +144 -0
  96. package/assets/docs/tree/agent-guidance.doc.mjs +138 -0
  97. package/assets/docs/tree/block-template.doc.mjs +130 -0
  98. package/assets/docs/tree/build-the-template.doc.mjs +28 -0
  99. package/assets/docs/tree/building-blocks.doc.mjs +46 -0
  100. package/assets/docs/tree/check-your-docs.doc.mjs +137 -0
  101. package/assets/docs/tree/checks.doc.mjs +119 -0
  102. package/assets/docs/tree/codemods.doc.mjs +147 -0
  103. package/assets/docs/tree/component-family.doc.mjs +113 -0
  104. package/assets/docs/tree/component-imports.doc.mjs +69 -0
  105. package/assets/docs/tree/components.doc.mjs +23 -0
  106. package/assets/docs/tree/configuration.doc.mjs +23 -0
  107. package/assets/docs/tree/debug-and-gap-reports.doc.mjs +182 -0
  108. package/assets/docs/tree/define-the-theme.doc.mjs +118 -0
  109. package/assets/docs/tree/describe-the-component.doc.mjs +57 -0
  110. package/assets/docs/tree/docs.doc.mjs +21 -0
  111. package/assets/docs/tree/document-the-template.doc.mjs +28 -0
  112. package/assets/docs/tree/document-the-theme.doc.mjs +68 -0
  113. package/assets/docs/tree/export-template-assets.doc.mjs +147 -0
  114. package/assets/docs/tree/extend-or-replace.doc.mjs +103 -0
  115. package/assets/docs/tree/fonts-and-assets.doc.mjs +106 -0
  116. package/assets/docs/tree/generate-a-palette.doc.mjs +66 -0
  117. package/assets/docs/tree/grade-template-with-agent.doc.mjs +105 -0
  118. package/assets/docs/tree/help.doc.mjs +16 -0
  119. package/assets/docs/tree/integrations.doc.mjs +25 -470
  120. package/assets/docs/tree/links.doc.mjs +98 -0
  121. package/assets/docs/tree/package-and-test.doc.mjs +32 -0
  122. package/assets/docs/tree/page-template.doc.mjs +71 -0
  123. package/assets/docs/tree/publishing.doc.mjs +111 -0
  124. package/assets/docs/tree/quick-start.doc.mjs +272 -0
  125. package/assets/docs/tree/replace-a-core-component.doc.mjs +104 -0
  126. package/assets/docs/tree/replace-a-core-template.doc.mjs +172 -0
  127. package/assets/docs/tree/sections-and-placement.doc.mjs +108 -0
  128. package/assets/docs/tree/see-it-in-an-app.doc.mjs +59 -0
  129. package/assets/docs/tree/ship.doc.mjs +16 -0
  130. package/assets/docs/tree/short-and-findable.doc.mjs +108 -0
  131. package/assets/docs/tree/single-component.doc.mjs +165 -0
  132. package/assets/docs/tree/start-a-template.doc.mjs +143 -0
  133. package/assets/docs/tree/subcomponent.doc.mjs +115 -0
  134. package/assets/docs/tree/template-assets.doc.mjs +64 -0
  135. package/assets/docs/tree/template-doc-overview.doc.mjs +109 -0
  136. package/assets/docs/tree/template-fonts.doc.mjs +102 -0
  137. package/assets/docs/tree/template-grading-rubric.doc.mjs +452 -0
  138. package/assets/docs/tree/template-icons.doc.mjs +97 -0
  139. package/assets/docs/tree/template-images-media.doc.mjs +127 -0
  140. package/assets/docs/tree/template-styles.doc.mjs +93 -0
  141. package/assets/docs/tree/templates.doc.mjs +34 -0
  142. package/assets/docs/tree/test-in-an-app.doc.mjs +115 -0
  143. package/assets/docs/tree/test-template-in-app.doc.mjs +128 -0
  144. package/assets/docs/tree/themes.doc.mjs +39 -0
  145. package/assets/docs/tree/troubleshooting.doc.mjs +149 -0
  146. package/assets/docs/tree/upgrading.doc.mjs +103 -0
  147. package/assets/docs/tree/use-a-theme-in-an-app.doc.mjs +51 -0
  148. package/assets/docs/tree/verify-packed-template.doc.mjs +77 -0
  149. package/assets/docs/tree/versioning.doc.mjs +161 -0
  150. package/assets/docs/tree/write-good-templates.doc.mjs +64 -0
  151. package/assets/docs/tree/write-the-template-file.doc.mjs +154 -0
  152. package/assets/docs/typography.doc.mjs +24 -4
  153. package/assets/docs/working-with-ai.doc.mjs +30 -22
  154. package/authoring/config/config.doc.mjs +2 -2
  155. package/authoring/config/type.ts +2 -2
  156. package/authoring/doctypes/_schema.d.mts +3 -2
  157. package/authoring/doctypes/_schema.mjs +6 -0
  158. package/authoring/doctypes/base/graph-fields.doc.mjs +3 -3
  159. package/authoring/doctypes/base/type.ts +4 -2
  160. package/authoring/doctypes/command/command.doc.mjs +1 -1
  161. package/authoring/doctypes/command/type.ts +1 -1
  162. package/authoring/doctypes/component/component.doc.mjs +6 -0
  163. package/authoring/doctypes/component/type.ts +8 -0
  164. package/authoring/doctypes/reference/reference.doc.mjs +7 -0
  165. package/authoring/doctypes/reference/type.ts +5 -0
  166. package/authoring/doctypes/schema/schema.doc.mjs +2 -2
  167. package/authoring/doctypes/template/template.doc.mjs +1 -1
  168. package/authoring/doctypes/template/type.ts +2 -2
  169. package/authoring/integration/integration.doc.mjs +12 -10
  170. package/clients/cli/command-result-coverage.test.mjs +7 -7
  171. package/clients/cli/commands/component.doc.mjs +4 -3
  172. package/clients/cli/commands/debug-result-summary.test.mjs +2 -2
  173. package/clients/cli/commands/docs.doc.mjs +1 -1
  174. package/clients/cli/commands/docs.mjs +60 -17
  175. package/clients/cli/commands/doctor-integration-docs.doc.mjs +3 -2
  176. package/clients/cli/commands/doctor-integration.test.mjs +53 -0
  177. package/clients/cli/commands/doctor.doc.mjs +3 -1
  178. package/clients/cli/commands/doctor.mjs +49 -5
  179. package/clients/cli/commands/gap-report.doc.mjs +10 -9
  180. package/clients/cli/commands/init.doc.mjs +9 -6
  181. package/clients/cli/commands/integration-add.doc.mjs +9 -9
  182. package/clients/cli/commands/integration-authoring.test.mjs +61 -10
  183. package/clients/cli/commands/integration-pack.doc.mjs +5 -9
  184. package/clients/cli/commands/integration-real-world.test.mjs +1 -1
  185. package/clients/cli/commands/integration-verify.doc.mjs +22 -0
  186. package/clients/cli/commands/integration.doc.mjs +4 -4
  187. package/clients/cli/commands/integration.mjs +74 -43
  188. package/clients/cli/commands/layout-check.doc.mjs +65 -0
  189. package/clients/cli/commands/layout-expand.doc.mjs +83 -0
  190. package/clients/cli/commands/layout-grammar.doc.mjs +30 -0
  191. package/clients/cli/commands/layout.doc.mjs +34 -0
  192. package/clients/cli/commands/layout.error-codes.test.mjs +66 -0
  193. package/clients/cli/commands/layout.exit-parity.test.mjs +41 -0
  194. package/clients/cli/commands/layout.mjs +275 -0
  195. package/clients/cli/commands/layout.path-help.test.mjs +33 -0
  196. package/clients/cli/commands/layout.stdin-cap.test.mjs +47 -0
  197. package/clients/cli/commands/layout.text-fields.test.mjs +39 -0
  198. package/clients/cli/commands/manifest.doc.mjs +1 -1
  199. package/clients/cli/commands/search.doc.mjs +10 -3
  200. package/clients/cli/commands/search.mjs +21 -2
  201. package/clients/cli/commands/search.test.mjs +21 -4
  202. package/clients/cli/commands/swizzle.doc.mjs +1 -1
  203. package/clients/cli/commands/template.doc.mjs +1 -1
  204. package/clients/cli/commands/text-json-parity.test.mjs +24 -1
  205. package/clients/cli/commands/theme-add.doc.mjs +1 -1
  206. package/clients/cli/commands/theme-palette-generate.doc.mjs +3 -2
  207. package/clients/cli/commands/theme-palette.doc.mjs +1 -2
  208. package/clients/cli/commands/theme-targets.doc.mjs +2 -2
  209. package/clients/cli/commands/theme.doc.mjs +2 -1
  210. package/clients/cli/commands/upgrade.doc.mjs +62 -3
  211. package/clients/cli/index.mjs +32 -6
  212. package/clients/cli/lib/define-command.mjs +28 -4
  213. package/clients/cli/lib/define-command.test.mjs +54 -0
  214. package/clients/cli/lib/exit-codes.test.mjs +25 -2
  215. package/clients/cli/lib/json-shim.test.mjs +20 -6
  216. package/clients/cli/lib/manifest.mjs +23 -5
  217. package/foundation/agent-docs/agent-docs.mjs +1 -1
  218. package/foundation/discovery/authoring-self-docs.test.mjs +6 -2
  219. package/foundation/discovery/cli-self-docs.mjs +16 -2
  220. package/foundation/discovery/cli-self-docs.test.mjs +20 -0
  221. package/foundation/discovery/docs-discovery.mjs +5 -1
  222. package/foundation/discovery/docs-discovery.test.mjs +21 -0
  223. package/foundation/discovery/docs-section-key.d.mts +1 -1
  224. package/foundation/discovery/docs-section-key.mjs +1 -1
  225. package/foundation/discovery/template-adapter.mjs +1 -1
  226. package/foundation/doc-compiler/doc-loads.test.mjs +15 -2
  227. package/foundation/doc-compiler/inputs.test.mjs +0 -1
  228. package/foundation/doc-compiler/tree.d.mts +4 -0
  229. package/foundation/doc-compiler/tree.mjs +6 -1
  230. package/foundation/integrations/cli-requirement.d.mts +26 -6
  231. package/foundation/integrations/cli-requirement.mjs +46 -11
  232. package/foundation/integrations/cli-requirement.test.mjs +7 -2
  233. package/foundation/integrations/contribution-inventory.mjs +1 -1
  234. package/foundation/response/error-codes.doc.mjs +6 -8
  235. package/foundation/response/error-codes.test.mjs +30 -5
  236. package/foundation/response/response-types.doc.d.mts +4 -3
  237. package/foundation/response/response-types.doc.mjs +42 -6
  238. package/foundation/response/response.doc.mjs +11 -10
  239. package/foundation/xle/browser.d.mts +3 -3
  240. package/foundation/xle/browser.mjs +3 -3
  241. package/foundation/xle/expand.mjs +2 -2
  242. package/foundation/xle/parse.mjs +1 -1
  243. package/foundation/xle/print.mjs +2 -2
  244. package/foundation/xle/splice.mjs +1 -1
  245. package/package.json +9 -9
  246. package/api/docs/docs.test.mjs +0 -245
  247. package/api/docs/integration-tree.test.mjs +0 -555
  248. package/api/docs/integrationDocs.test.mjs +0 -314
  249. package/api/search/search.test.mjs +0 -530
  250. package/assets/docs/tree/integrations.test.mjs +0 -62
  251. package/assets/docs/tree/writing-docs.doc.mjs +0 -286
  252. package/clients/cli/commands/docs.test.mjs +0 -323
  253. package/foundation/agent-docs/agent-docs.test.mjs +0 -1159
  254. package/foundation/doc-compiler/tree.test.mjs +0 -606
@@ -1,1159 +0,0 @@
1
- // Copyright (c) Meta Platforms, Inc. and affiliates.
2
-
3
- import {describe, it, expect, beforeEach, afterEach, vi} from 'vitest';
4
- import * as fs from 'node:fs';
5
- import * as path from 'node:path';
6
- import * as os from 'node:os';
7
- import {
8
- generateCompressedIndex,
9
- detectStylingSystem,
10
- getXdsVersion,
11
- installAgentDocs,
12
- injectAgentsMd,
13
- injectClaudeMd,
14
- injectXdsBlock,
15
- removeAgentDocs,
16
- removeXdsBlock,
17
- discoverAgentDocs,
18
- resolveAgentPaths,
19
- parseBlockVersion,
20
- renderAgentDocsBlock,
21
- inspectAgentDocs,
22
- isAstryxInitialized,
23
- } from './agent-docs.mjs';
24
-
25
- let tmpDir;
26
-
27
- beforeEach(() => {
28
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-agent-docs-test-'));
29
- });
30
-
31
- afterEach(() => {
32
- fs.rmSync(tmpDir, {recursive: true, force: true});
33
- vi.restoreAllMocks();
34
- });
35
-
36
- describe('generateCompressedIndex', () => {
37
- it('includes the version number', () => {
38
- const result = generateCompressedIndex('1.2.3');
39
- expect(result).toContain('Astryx v1.2.3');
40
- expect(result).toContain('<!-- ASTRYX:START -->');
41
- expect(result).toContain('<!-- ASTRYX:END -->');
42
- });
43
-
44
- it('includes theme nudge rule', () => {
45
- const result = generateCompressedIndex('1.0.0');
46
- expect(result).toMatch(/astryx theme/);
47
- expect(result).toMatch(/never override --color-/);
48
- });
49
-
50
- it('sends frame choice to the layout doc rather than naming a shell', () => {
51
- const result = generateCompressedIndex('1.0.0');
52
- const frameRule = result.split('\n').find(l => l.includes('Frame first'));
53
- expect(frameRule).toContain('astryx docs layout');
54
- expect(result).not.toMatch(/AppShell/);
55
- expect(frameRule).not.toMatch(/https?:/);
56
- });
57
-
58
- it('starts every page from a template before any component', () => {
59
- const lines = generateCompressedIndex('1.0.0').split('\n');
60
- const workflow = lines.findIndex(l => l.startsWith('WORKFLOW'));
61
- expect(lines[workflow]).toMatch(/start every page from a template/);
62
- expect(lines[workflow + 1]).toMatch(/^1\. `astryx build /);
63
- expect(lines[workflow + 2]).toMatch(/^2\. `astryx template <name> <path>`/);
64
- expect(lines.join('\n')).not.toMatch(/reference code/);
65
- });
66
-
67
- it('includes the post-generation self-check rule', () => {
68
- const result = generateCompressedIndex('1.0.0');
69
- expect(result).toContain('SELF-CHECK before you finish');
70
- expect(result).toMatch(/re-read the file/);
71
- expect(result).toMatch(/don't hand-roll CSS/);
72
- });
73
-
74
- it('tailors the self-check to the styling system (xstyle for StyleX, not className for Tailwind)', () => {
75
- // StyleX path: className/inline style are veers; the fix is the xstyle prop + a token
76
- const stylex = generateCompressedIndex('1.0.0', {stylingSystem: 'stylex'});
77
- const stylexSelfCheck = stylex.split('\n').find(l => l.includes('SELF-CHECK'));
78
- expect(stylexSelfCheck).toMatch(/xstyle/);
79
- expect(stylexSelfCheck).toMatch(/className=/);
80
- // className IS the system in Tailwind — it must NOT be flagged
81
- const tailwind = generateCompressedIndex('1.0.0', {stylingSystem: 'tailwind'});
82
- const tailwindSelfCheck = tailwind.split('\n').find(l => l.includes('SELF-CHECK'));
83
- expect(tailwindSelfCheck).toBeDefined();
84
- expect(tailwindSelfCheck).not.toMatch(/className=/);
85
- });
86
-
87
- it('defaults to the CSS-variable styling path (no compiler)', () => {
88
- const result = generateCompressedIndex('1.0.0');
89
- expect(result).toMatch(/style\/className with tokens/);
90
- expect(result).toMatch(/var\(--color-\*/);
91
- // Must NOT push xstyle when no StyleX compiler is present.
92
- expect(result).not.toMatch(/xstyle prop/);
93
- });
94
-
95
- it('recommends xstyle when StyleX is configured', () => {
96
- const result = generateCompressedIndex('1.0.0', {stylingSystem: 'stylex'});
97
- expect(result).toMatch(/xstyle prop \/ StyleX tokens/);
98
- });
99
-
100
- it('recommends Tailwind utilities when Tailwind is configured', () => {
101
- const result = generateCompressedIndex('1.0.0', {stylingSystem: 'tailwind'});
102
- expect(result).toMatch(/Tailwind utilities backed by tokens/);
103
- expect(result).toMatch(/tailwind-theme\.css/);
104
- });
105
-
106
- it('includes upgrade command and migration rule', () => {
107
- const result = generateCompressedIndex('1.0.0');
108
- expect(result).toContain('upgrade --apply');
109
- expect(result).toMatch(/after any Astryx or integration dependency bump/);
110
- });
111
-
112
- it('states the invocation once in the CLI header (yarn)', () => {
113
- const result = generateCompressedIndex('1.0.0', {invocation: 'yarn astryx'});
114
- expect(result).toContain('yarn astryx <cmd>');
115
- expect(result).not.toContain('npx astryx');
116
- });
117
-
118
- it('uses the pnpm exec invocation', () => {
119
- const result = generateCompressedIndex('1.0.0', {invocation: 'pnpm exec astryx'});
120
- expect(result).toContain('pnpm exec astryx <cmd>');
121
- expect(result).not.toContain('npx astryx');
122
- });
123
-
124
- it('uses the scoped package for one-off (uninstalled) runs so agents never hit the bare name', () => {
125
- const result = generateCompressedIndex('1.0.0', {invocation: 'npx @astryxdesign/cli'});
126
- expect(result).toContain('npx @astryxdesign/cli <cmd>');
127
- // The header defines the mapping; the bare "run every command as `npx astryx`" footgun must be absent.
128
- expect(result).not.toContain('npx astryx <cmd>');
129
- });
130
-
131
- /** The `docs <topic> a, b, c` line of the block. */
132
- function topicLine(block) {
133
- return (
134
- block.split('\n').find(line => line.trimStart().startsWith('docs <topic>')) ?? ''
135
- );
136
- }
137
-
138
- it('names the hyphenated topics, which the scan used to drop', () => {
139
- // The fallback scan matched `\w+`, which does not match `-`, so five real
140
- // topics were missing from every block ever written. An agent cannot ask
141
- // for a topic it was never told about, and `getting-started` — the one it
142
- // should reach for first — was one of them.
143
- const line = topicLine(generateCompressedIndex('1.0.0'));
144
- for (const topic of [
145
- 'getting-started',
146
- 'browser-support',
147
- 'styling-libraries',
148
- 'working-with-ai',
149
- ]) {
150
- expect(line).toContain(topic);
151
- }
152
- });
153
-
154
- it('points to the CLI docs tree on a line of its own', () => {
155
- // The integration guide lives in the docs tree now (cli/integrations), so
156
- // the topic line no longer names it; the tree's entry point does.
157
- const block = generateCompressedIndex('1.0.0');
158
- expect(block).toContain(
159
- ' docs cli commands, API reference, integration authoring (one level at a time)',
160
- );
161
- expect(topicLine(block)).not.toContain('cli-integrations');
162
- });
163
-
164
- it('lists the topics it is given, so an integration’s reach the agent', () => {
165
- const line = topicLine(
166
- generateCompressedIndex('1.0.0', {topics: ['tokens', 'deploying']}),
167
- );
168
- expect(line).toContain('tokens, deploying');
169
- // The project's catalog replaces the built-in scan rather than adding to it:
170
- // a topic an integration replaced must not also be listed under its old name.
171
- expect(line).not.toContain('typography');
172
- });
173
-
174
- it('omits the line entirely when the project has no topics', () => {
175
- expect(topicLine(generateCompressedIndex('1.0.0', {topics: []}))).toBe('');
176
- });
177
-
178
- it('is byte-identical when integrations contribute no lines', () => {
179
- const options = {
180
- invocation: 'npx @astryxdesign/cli',
181
- stylingSystem: 'css',
182
- topics: ['tokens', 'working-with-ai'],
183
- };
184
- expect(generateCompressedIndex('1.0.0', {...options, agentDocs: []})).toBe(
185
- generateCompressedIndex('1.0.0', options),
186
- );
187
- });
188
-
189
- it('appends package-labeled lines after core guidance in config order', () => {
190
- const result = generateCompressedIndex('1.0.0', {
191
- topics: [],
192
- agentDocs: [
193
- {
194
- package: '@acme/second',
195
- append: ['second one', 'second two'],
196
- },
197
- {
198
- package: '@acme/first',
199
- append: ['first one'],
200
- },
201
- ],
202
- });
203
-
204
- expect(result.indexOf('upgrade --apply')).toBeLessThan(
205
- result.indexOf('INTEGRATIONS:'),
206
- );
207
- expect(result.indexOf('INTEGRATIONS:')).toBeLessThan(
208
- result.indexOf('<!-- ASTRYX:END -->'),
209
- );
210
- expect(result).toContain(
211
- 'INTEGRATIONS:\n' +
212
- '- `@acme/second`: second one\n' +
213
- '- `@acme/second`: second two\n' +
214
- '- `@acme/first`: first one',
215
- );
216
- });
217
-
218
- it('caps the configured project at 32 integration lines', () => {
219
- const agentDocs = Array.from({length: 5}, (_, packageIndex) => ({
220
- package: `@acme/package-${packageIndex}`,
221
- append: Array.from(
222
- {length: packageIndex === 4 ? 1 : 8},
223
- (_, lineIndex) => `line ${lineIndex}`,
224
- ),
225
- }));
226
- expect(() => generateCompressedIndex('1.0.0', {agentDocs})).toThrow(
227
- /32-line project limit/,
228
- );
229
- });
230
- });
231
-
232
- describe('renderAgentDocsBlock', () => {
233
- it('produces repeatable target bytes from configured manifests', async () => {
234
- const projectDir = fs.mkdtempSync(
235
- path.join(process.cwd(), '.astryx-agent-doc-render-'),
236
- );
237
- try {
238
- fs.writeFileSync(
239
- path.join(projectDir, 'package.json'),
240
- JSON.stringify({name: 'consumer'}),
241
- );
242
- fs.writeFileSync(
243
- path.join(projectDir, 'astryx.config.mjs'),
244
- `export default {integrations: ['@acme/second', '@acme/first']};\n`,
245
- );
246
- const coreDir = path.join(
247
- projectDir,
248
- 'node_modules',
249
- '@astryxdesign',
250
- 'core',
251
- );
252
- fs.mkdirSync(coreDir, {recursive: true});
253
- fs.writeFileSync(
254
- path.join(coreDir, 'package.json'),
255
- JSON.stringify({name: '@astryxdesign/core', version: '1.0.0'}),
256
- );
257
- for (const [name, agentDocs] of [
258
- ['@acme/second', {append: ['second one', 'second two']}],
259
- ['@acme/first', {append: ['first one']}],
260
- ]) {
261
- const packageDir = path.join(
262
- projectDir,
263
- 'node_modules',
264
- ...name.split('/'),
265
- );
266
- fs.mkdirSync(packageDir, {recursive: true});
267
- fs.writeFileSync(
268
- path.join(packageDir, 'package.json'),
269
- JSON.stringify({name}),
270
- );
271
- fs.writeFileSync(
272
- path.join(packageDir, 'astryx.integration.mjs'),
273
- `export default {agentDocs: ${JSON.stringify(agentDocs)}};\n`,
274
- );
275
- }
276
- fs.writeFileSync(path.join(projectDir, 'AGENTS.md'), '# Agents\n');
277
- fs.writeFileSync(path.join(projectDir, 'CLAUDE.md'), '# Claude\n');
278
-
279
- const renderedBlock = await renderAgentDocsBlock(projectDir);
280
- expect(renderedBlock.indexOf('second one')).toBeLessThan(
281
- renderedBlock.indexOf('second two'),
282
- );
283
- expect(renderedBlock.indexOf('second two')).toBeLessThan(
284
- renderedBlock.indexOf('first one'),
285
- );
286
-
287
- installAgentDocs(projectDir, {renderedBlock});
288
- const firstAgents = fs.readFileSync(
289
- path.join(projectDir, 'AGENTS.md'),
290
- 'utf-8',
291
- );
292
- const firstClaude = fs.readFileSync(
293
- path.join(projectDir, 'CLAUDE.md'),
294
- 'utf-8',
295
- );
296
- expect(firstAgents).toContain(renderedBlock);
297
- expect(firstClaude).toContain(renderedBlock);
298
-
299
- installAgentDocs(projectDir, {renderedBlock});
300
- expect(fs.readFileSync(path.join(projectDir, 'AGENTS.md'), 'utf-8')).toBe(
301
- firstAgents,
302
- );
303
- expect(fs.readFileSync(path.join(projectDir, 'CLAUDE.md'), 'utf-8')).toBe(
304
- firstClaude,
305
- );
306
- } finally {
307
- fs.rmSync(projectDir, {recursive: true, force: true});
308
- }
309
- });
310
- it('rejects a package label that could alter managed block structure', async () => {
311
- const projectDir = fs.mkdtempSync(
312
- path.join(process.cwd(), '.astryx-agent-doc-label-'),
313
- );
314
- try {
315
- fs.writeFileSync(
316
- path.join(projectDir, 'package.json'),
317
- JSON.stringify({name: 'consumer'}),
318
- );
319
- fs.writeFileSync(
320
- path.join(projectDir, 'astryx.config.mjs'),
321
- `export default {integrations: ['@acme/widgets']};\n`,
322
- );
323
- const packageDir = path.join(
324
- projectDir,
325
- 'node_modules',
326
- '@acme',
327
- 'widgets',
328
- );
329
- fs.mkdirSync(packageDir, {recursive: true});
330
- fs.writeFileSync(
331
- path.join(packageDir, 'package.json'),
332
- JSON.stringify({name: '@acme/widgets\nASTRYX:END'}),
333
- );
334
- fs.writeFileSync(
335
- path.join(packageDir, 'astryx.integration.mjs'),
336
- `export default {agentDocs: {append: ['safe line']}};\n`,
337
- );
338
-
339
- await expect(renderAgentDocsBlock(projectDir)).rejects.toThrow(
340
- /not safe to render/,
341
- );
342
- } finally {
343
- fs.rmSync(projectDir, {recursive: true, force: true});
344
- }
345
- });
346
- });
347
-
348
- describe('detectStylingSystem', () => {
349
- function writePkg(deps) {
350
- fs.writeFileSync(
351
- path.join(tmpDir, 'package.json'),
352
- JSON.stringify({name: 'x', devDependencies: deps}),
353
- );
354
- }
355
-
356
- it('defaults to css when no package.json', () => {
357
- expect(detectStylingSystem(tmpDir)).toBe('css');
358
- });
359
-
360
- it('returns css for a plain project', () => {
361
- writePkg({react: '19.0.0', vite: '6.0.0'});
362
- expect(detectStylingSystem(tmpDir)).toBe('css');
363
- });
364
-
365
- it('detects stylex when the compiler plugin is present', () => {
366
- writePkg({'@stylexjs/babel-plugin': '0.0.1'});
367
- expect(detectStylingSystem(tmpDir)).toBe('stylex');
368
- });
369
-
370
- it('detects tailwind when tailwindcss is present', () => {
371
- writePkg({tailwindcss: '4.0.0'});
372
- expect(detectStylingSystem(tmpDir)).toBe('tailwind');
373
- });
374
-
375
- it('does NOT treat the StyleX runtime alone as a compiler', () => {
376
- // Only the runtime, no compiler plugin → must stay on the safe css path.
377
- writePkg({'@stylexjs/stylex': '0.0.1'});
378
- expect(detectStylingSystem(tmpDir)).toBe('css');
379
- });
380
-
381
- it('prefers stylex over tailwind when both are configured', () => {
382
- writePkg({'@stylexjs/babel-plugin': '0.0.1', tailwindcss: '4.0.0'});
383
- expect(detectStylingSystem(tmpDir)).toBe('stylex');
384
- });
385
- });
386
-
387
- describe('getXdsVersion', () => {
388
- it('reads version from core package.json', () => {
389
- const coreDir = path.join(tmpDir, 'core');
390
- fs.mkdirSync(coreDir, {recursive: true});
391
- fs.writeFileSync(
392
- path.join(coreDir, 'package.json'),
393
- JSON.stringify({version: '3.4.5'}),
394
- );
395
-
396
- expect(getXdsVersion(coreDir)).toBe('3.4.5');
397
- });
398
- });
399
-
400
- describe('injectXdsBlock', () => {
401
- it('injects into an existing file without markers', () => {
402
- const filePath = path.join(tmpDir, 'test.md');
403
- fs.writeFileSync(filePath, '# Existing content\n');
404
-
405
- const result = injectXdsBlock(filePath, '<!-- ASTRYX:START -->\nnew\n<!-- ASTRYX:END -->');
406
-
407
- expect(result).toBe(true);
408
- const content = fs.readFileSync(filePath, 'utf-8');
409
- expect(content).toContain('# Existing content');
410
- expect(content).toContain('<!-- ASTRYX:START -->');
411
- });
412
-
413
- it('replaces existing markers', () => {
414
- const filePath = path.join(tmpDir, 'test.md');
415
- fs.writeFileSync(filePath, 'before\n<!-- XDS:START -->\nold\n<!-- XDS:END -->\nafter\n');
416
-
417
- injectXdsBlock(filePath, '<!-- ASTRYX:START -->\nnew\n<!-- ASTRYX:END -->');
418
-
419
- const content = fs.readFileSync(filePath, 'utf-8');
420
- expect(content).toContain('new');
421
- expect(content).not.toContain('old');
422
- expect(content).toContain('before');
423
- expect(content).toContain('after');
424
- });
425
-
426
- it('returns false and does not create file when createIfMissing is false', () => {
427
- const filePath = path.join(tmpDir, 'nonexistent.md');
428
-
429
- const result = injectXdsBlock(filePath, '<!-- ASTRYX:START -->\ncontent\n<!-- ASTRYX:END -->');
430
-
431
- expect(result).toBe(false);
432
- expect(fs.existsSync(filePath)).toBe(false);
433
- });
434
-
435
- it('skips files without markers when onlyReplace is true', () => {
436
- const filePath = path.join(tmpDir, 'test.md');
437
- fs.writeFileSync(filePath, '# Existing content\n\nNo XDS markers here.\n');
438
-
439
- const result = injectXdsBlock(filePath, '<!-- ASTRYX:START -->\nnew\n<!-- ASTRYX:END -->', {onlyReplace: true});
440
-
441
- expect(result).toBe(false);
442
- const content = fs.readFileSync(filePath, 'utf-8');
443
- expect(content).not.toContain('<!-- ASTRYX:START -->');
444
- expect(content).toBe('# Existing content\n\nNo XDS markers here.\n');
445
- });
446
-
447
- it('replaces existing markers even when onlyReplace is true', () => {
448
- const filePath = path.join(tmpDir, 'test.md');
449
- fs.writeFileSync(filePath, 'before\n<!-- XDS:START -->\nold\n<!-- XDS:END -->\nafter\n');
450
-
451
- const result = injectXdsBlock(filePath, '<!-- ASTRYX:START -->\nnew\n<!-- ASTRYX:END -->', {onlyReplace: true});
452
-
453
- expect(result).toBe(true);
454
- const content = fs.readFileSync(filePath, 'utf-8');
455
- expect(content).toContain('new');
456
- expect(content).not.toContain('old');
457
- });
458
-
459
- it('creates file when createIfMissing is true', () => {
460
- const filePath = path.join(tmpDir, 'new.md');
461
-
462
- const result = injectXdsBlock(filePath, '<!-- ASTRYX:START -->\ncontent\n<!-- ASTRYX:END -->', {
463
- createIfMissing: true,
464
- header: '# Header',
465
- });
466
-
467
- expect(result).toBe(true);
468
- const content = fs.readFileSync(filePath, 'utf-8');
469
- expect(content).toContain('# Header');
470
- expect(content).toContain('<!-- ASTRYX:START -->');
471
- });
472
- });
473
-
474
- describe('injectAgentsMd', () => {
475
- it('creates new AGENTS.md when none exists', () => {
476
- injectAgentsMd(tmpDir, '1.0.0');
477
-
478
- const content = fs.readFileSync(path.join(tmpDir, 'AGENTS.md'), 'utf-8');
479
- expect(content).toContain('# AGENTS.md');
480
- expect(content).toContain('<!-- ASTRYX:START -->');
481
- expect(content).toContain('Astryx v1.0.0');
482
- expect(content).toContain('<!-- ASTRYX:END -->');
483
- });
484
-
485
- it('updates existing AGENTS.md by replacing XDS markers', () => {
486
- const existing = `# My Project
487
-
488
- Some content.
489
-
490
- <!-- XDS:START -->
491
- old content
492
- <!-- XDS:END -->
493
-
494
- More stuff.
495
- `;
496
- fs.writeFileSync(path.join(tmpDir, 'AGENTS.md'), existing);
497
-
498
- injectAgentsMd(tmpDir, '2.0.0');
499
-
500
- const content = fs.readFileSync(path.join(tmpDir, 'AGENTS.md'), 'utf-8');
501
- expect(content).toContain('Astryx v2.0.0');
502
- expect(content).not.toContain('old content');
503
- expect(content).toContain('Some content.');
504
- expect(content).toContain('More stuff.');
505
- });
506
-
507
- it('appends to existing AGENTS.md without markers', () => {
508
- const existing = `# My Project
509
-
510
- Existing agent docs.
511
- `;
512
- fs.writeFileSync(path.join(tmpDir, 'AGENTS.md'), existing);
513
-
514
- injectAgentsMd(tmpDir, '1.0.0');
515
-
516
- const content = fs.readFileSync(path.join(tmpDir, 'AGENTS.md'), 'utf-8');
517
- expect(content).toContain('Existing agent docs.');
518
- expect(content).toContain('<!-- ASTRYX:START -->');
519
- expect(content).toContain('Astryx v1.0.0');
520
- });
521
- });
522
-
523
- describe('injectClaudeMd', () => {
524
- it('injects into existing CLAUDE.md', () => {
525
- fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), '# Claude Config\n\nExisting rules.\n');
526
-
527
- const result = injectClaudeMd(tmpDir, '1.0.0');
528
-
529
- expect(result).toBe(true);
530
- const content = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8');
531
- expect(content).toContain('# Claude Config');
532
- expect(content).toContain('Existing rules.');
533
- expect(content).toContain('<!-- ASTRYX:START -->');
534
- expect(content).toContain('Astryx v1.0.0');
535
- });
536
-
537
- it('does not create CLAUDE.md when it does not exist', () => {
538
- const result = injectClaudeMd(tmpDir, '1.0.0');
539
-
540
- expect(result).toBe(false);
541
- expect(fs.existsSync(path.join(tmpDir, 'CLAUDE.md'))).toBe(false);
542
- });
543
-
544
- it('updates existing markers in CLAUDE.md', () => {
545
- const existing = `# Claude Config
546
-
547
- <!-- XDS:START -->
548
- old content
549
- <!-- XDS:END -->
550
-
551
- Other rules.
552
- `;
553
- fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), existing);
554
-
555
- injectClaudeMd(tmpDir, '2.0.0');
556
-
557
- const content = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8');
558
- expect(content).toContain('Astryx v2.0.0');
559
- expect(content).not.toContain('old content');
560
- expect(content).toContain('Other rules.');
561
- });
562
- });
563
-
564
- describe('removeAgentDocs', () => {
565
- it('removes XDS section from AGENTS.md', () => {
566
- const content = `# My Project
567
-
568
- Custom content here.
569
-
570
- <!-- XDS:START -->
571
- XDS index stuff
572
- <!-- XDS:END -->
573
-
574
- More custom content.
575
- `;
576
- fs.writeFileSync(path.join(tmpDir, 'AGENTS.md'), content);
577
- vi.spyOn(console, 'log').mockImplementation(() => {});
578
-
579
- removeAgentDocs(tmpDir);
580
-
581
- const result = fs.readFileSync(path.join(tmpDir, 'AGENTS.md'), 'utf-8');
582
- expect(result).toContain('Custom content here.');
583
- expect(result).toContain('More custom content.');
584
- expect(result).not.toContain('<!-- XDS:START -->');
585
- expect(result).not.toContain('XDS index stuff');
586
- });
587
-
588
- it('removes the file entirely when only XDS content remains', () => {
589
- const content = `# AGENTS.md
590
-
591
- Project-specific guidance for AI coding agents.
592
-
593
- <!-- XDS:START -->
594
- XDS index stuff
595
- <!-- XDS:END -->
596
- `;
597
- fs.writeFileSync(path.join(tmpDir, 'AGENTS.md'), content);
598
- vi.spyOn(console, 'log').mockImplementation(() => {});
599
-
600
- removeAgentDocs(tmpDir);
601
-
602
- expect(fs.existsSync(path.join(tmpDir, 'AGENTS.md'))).toBe(false);
603
- });
604
-
605
- it('removes XDS section from CLAUDE.md when present', () => {
606
- fs.writeFileSync(
607
- path.join(tmpDir, 'AGENTS.md'),
608
- '# AGENTS.md\n\n<!-- XDS:START -->\nstuff\n<!-- XDS:END -->\n',
609
- );
610
- fs.writeFileSync(
611
- path.join(tmpDir, 'CLAUDE.md'),
612
- '# Claude\n\nRules.\n\n<!-- XDS:START -->\nstuff\n<!-- XDS:END -->\n\nMore rules.\n',
613
- );
614
- vi.spyOn(console, 'log').mockImplementation(() => {});
615
-
616
- removeAgentDocs(tmpDir);
617
-
618
- const claudeContent = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8');
619
- expect(claudeContent).toContain('Rules.');
620
- expect(claudeContent).toContain('More rules.');
621
- expect(claudeContent).not.toContain('<!-- XDS:START -->');
622
- });
623
- });
624
-
625
- describe('installAgentDocs', () => {
626
- function setupCorePackage(dir, version = '1.0.0') {
627
- // Create a minimal @astryxdesign/core so getXdsVersion works
628
- const coreDir = path.join(dir, 'node_modules', '@astryxdesign', 'core');
629
- fs.mkdirSync(coreDir, {recursive: true});
630
- fs.writeFileSync(
631
- path.join(coreDir, 'package.json'),
632
- JSON.stringify({version}),
633
- );
634
- }
635
-
636
- it('creates AGENTS.md when no agent docs exist (tool-agnostic default)', () => {
637
- setupCorePackage(tmpDir);
638
-
639
- const written = installAgentDocs(tmpDir);
640
-
641
- expect(written).toEqual(['AGENTS.md']);
642
- expect(fs.existsSync(path.join(tmpDir, 'AGENTS.md'))).toBe(true);
643
- // Must NOT create the Claude-specific file by default.
644
- expect(fs.existsSync(path.join(tmpDir, '.claude', 'CLAUDE.md'))).toBe(false);
645
- const content = fs.readFileSync(path.join(tmpDir, 'AGENTS.md'), 'utf-8');
646
- expect(content).toContain('# AGENTS.md');
647
- expect(content).toContain('<!-- ASTRYX:START -->');
648
- });
649
-
650
- it('defaults to AGENTS.md but writes .claude/CLAUDE.md only when --agent claude is explicit', () => {
651
- // Default (no agent): tool-agnostic AGENTS.md, never the Claude file.
652
- setupCorePackage(tmpDir);
653
- expect(installAgentDocs(tmpDir)).toEqual(['AGENTS.md']);
654
- expect(fs.existsSync(path.join(tmpDir, '.claude', 'CLAUDE.md'))).toBe(false);
655
-
656
- // Explicit Claude: the Claude-specific file, in a fresh project.
657
- const claudeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-agent-docs-claude-'));
658
- setupCorePackage(claudeDir);
659
- try {
660
- expect(installAgentDocs(claudeDir, {agent: 'claude'})).toEqual(['.claude/CLAUDE.md']);
661
- expect(fs.existsSync(path.join(claudeDir, 'AGENTS.md'))).toBe(false);
662
- } finally {
663
- fs.rmSync(claudeDir, {recursive: true, force: true});
664
- }
665
- });
666
-
667
- it('injects into CLAUDE.md at root when it exists', () => {
668
- setupCorePackage(tmpDir);
669
- fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), '# Claude\n\nProject rules.\n');
670
-
671
- const written = installAgentDocs(tmpDir);
672
-
673
- expect(written).toEqual(['CLAUDE.md']);
674
- expect(fs.existsSync(path.join(tmpDir, 'AGENTS.md'))).toBe(false);
675
- const claudeContent = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8');
676
- expect(claudeContent).toContain('<!-- ASTRYX:START -->');
677
- expect(claudeContent).toContain('Project rules.');
678
- });
679
-
680
- it('injects into all existing agent doc files', () => {
681
- setupCorePackage(tmpDir);
682
- fs.writeFileSync(path.join(tmpDir, 'AGENTS.md'), '# Agents\n\nAgent rules.\n');
683
- fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), '# Claude\n\nClaude rules.\n');
684
-
685
- const written = installAgentDocs(tmpDir);
686
-
687
- expect(written).toContain('AGENTS.md');
688
- expect(written).toContain('CLAUDE.md');
689
- const agentsContent = fs.readFileSync(path.join(tmpDir, 'AGENTS.md'), 'utf-8');
690
- const claudeContent = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8');
691
- expect(agentsContent).toContain('<!-- ASTRYX:START -->');
692
- expect(claudeContent).toContain('<!-- ASTRYX:START -->');
693
- });
694
-
695
- it('preserves import wrappers while initializing standalone files', () => {
696
- setupCorePackage(tmpDir);
697
- fs.writeFileSync(path.join(tmpDir, 'AGENTS.md'), '# Agents\n');
698
- fs.mkdirSync(path.join(tmpDir, '.claude'), {recursive: true});
699
- const wrapperPath = path.join(tmpDir, '.claude', 'CLAUDE.md');
700
- const wrapper = '@../AGENTS.md\n';
701
- fs.writeFileSync(wrapperPath, wrapper);
702
- fs.writeFileSync(path.join(tmpDir, '.cursorrules'), 'Cursor rules.\n');
703
-
704
- const written = installAgentDocs(tmpDir);
705
-
706
- expect(written).toEqual(['AGENTS.md', '.cursorrules']);
707
- expect(fs.readFileSync(wrapperPath, 'utf-8')).toBe(wrapper);
708
- expect(fs.readFileSync(path.join(tmpDir, 'AGENTS.md'), 'utf-8')).toContain(
709
- '<!-- ASTRYX:START -->',
710
- );
711
- expect(fs.readFileSync(path.join(tmpDir, '.cursorrules'), 'utf-8')).toContain(
712
- '<!-- ASTRYX:START -->',
713
- );
714
- });
715
-
716
- it('keeps cyclic imports standalone when another file exists', () => {
717
- setupCorePackage(tmpDir);
718
- fs.writeFileSync(path.join(tmpDir, 'AGENTS.md'), '@CLAUDE.md\n');
719
- fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), '@AGENTS.md\n');
720
- fs.writeFileSync(path.join(tmpDir, '.cursorrules'), 'Cursor rules.\n');
721
-
722
- const written = installAgentDocs(tmpDir);
723
-
724
- expect(written).toEqual(['AGENTS.md', 'CLAUDE.md', '.cursorrules']);
725
- for (const rel of written) {
726
- expect(fs.readFileSync(path.join(tmpDir, rel), 'utf-8')).toContain(
727
- '<!-- ASTRYX:START -->',
728
- );
729
- }
730
- });
731
-
732
- it('removes a managed block previously expanded into an import wrapper', () => {
733
- setupCorePackage(tmpDir);
734
- installAgentDocs(tmpDir, {agent: 'codex'});
735
- fs.mkdirSync(path.join(tmpDir, '.claude'), {recursive: true});
736
- const wrapperPath = path.join(tmpDir, '.claude', 'CLAUDE.md');
737
- const wrapper = '@../AGENTS.md\n';
738
- fs.writeFileSync(wrapperPath, wrapper);
739
- installAgentDocs(tmpDir, {agent: 'claude'});
740
- expect(fs.readFileSync(wrapperPath, 'utf-8')).toContain(
741
- '<!-- ASTRYX:START -->',
742
- );
743
-
744
- const written = installAgentDocs(tmpDir, {onlyReplace: true});
745
-
746
- expect(written).toContain('AGENTS.md');
747
- expect(written).toContain('.claude/CLAUDE.md');
748
- expect(fs.readFileSync(wrapperPath, 'utf-8')).toBe(wrapper);
749
- });
750
-
751
- it('refuses a malformed managed block inside an import wrapper', () => {
752
- setupCorePackage(tmpDir);
753
- installAgentDocs(tmpDir, {agent: 'codex'});
754
- fs.mkdirSync(path.join(tmpDir, '.claude'), {recursive: true});
755
- fs.writeFileSync(
756
- path.join(tmpDir, '.claude', 'CLAUDE.md'),
757
- '@../AGENTS.md\n<!-- ASTRYX:START -->\nincomplete\n',
758
- );
759
-
760
- expect(() => installAgentDocs(tmpDir)).toThrow(/malformed|no matching/i);
761
- });
762
-
763
- it('updates existing .claude/CLAUDE.md', () => {
764
- setupCorePackage(tmpDir);
765
- fs.mkdirSync(path.join(tmpDir, '.claude'), {recursive: true});
766
- fs.writeFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), '# Project\n\nExisting content.\n');
767
-
768
- const written = installAgentDocs(tmpDir);
769
-
770
- expect(written).toEqual(['.claude/CLAUDE.md']);
771
- const content = fs.readFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), 'utf-8');
772
- expect(content).toContain('Existing content.');
773
- expect(content).toContain('<!-- ASTRYX:START -->');
774
- });
775
-
776
- it('respects --agent claude preset: finds existing CLAUDE.md', () => {
777
- setupCorePackage(tmpDir);
778
- fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), '# Claude\n\nRules.\n');
779
-
780
- const written = installAgentDocs(tmpDir, {agent: 'claude'});
781
-
782
- expect(written).toEqual(['CLAUDE.md']);
783
- });
784
-
785
- it('respects --agent claude preset: creates .claude/CLAUDE.md when nothing exists', () => {
786
- setupCorePackage(tmpDir);
787
-
788
- const written = installAgentDocs(tmpDir, {agent: 'claude'});
789
-
790
- expect(written).toEqual(['.claude/CLAUDE.md']);
791
- expect(fs.existsSync(path.join(tmpDir, '.claude', 'CLAUDE.md'))).toBe(true);
792
- });
793
-
794
- it('respects --agent codex preset: creates AGENTS.md', () => {
795
- setupCorePackage(tmpDir);
796
-
797
- const written = installAgentDocs(tmpDir, {agent: 'codex'});
798
-
799
- expect(written).toEqual(['AGENTS.md']);
800
- expect(fs.existsSync(path.join(tmpDir, 'AGENTS.md'))).toBe(true);
801
- });
802
-
803
- it('respects --agent hermes preset: creates AGENTS.md', () => {
804
- setupCorePackage(tmpDir);
805
-
806
- const written = installAgentDocs(tmpDir, {agent: 'hermes'});
807
-
808
- expect(written).toEqual(['AGENTS.md']);
809
- const content = fs.readFileSync(path.join(tmpDir, 'AGENTS.md'), 'utf-8');
810
- expect(content).toContain('<!-- ASTRYX:START -->');
811
- expect(fs.existsSync(path.join(tmpDir, '.claude'))).toBe(false);
812
- });
813
-
814
- it('respects --agent muse preset: creates AGENTS.md', () => {
815
- setupCorePackage(tmpDir);
816
-
817
- const written = installAgentDocs(tmpDir, {agent: 'muse'});
818
-
819
- expect(written).toEqual(['AGENTS.md']);
820
- expect(fs.existsSync(path.join(tmpDir, 'AGENTS.md'))).toBe(true);
821
- });
822
-
823
- it('respects explicit --paths', () => {
824
- setupCorePackage(tmpDir);
825
-
826
- const written = installAgentDocs(tmpDir, {paths: ['custom/AGENT.md']});
827
-
828
- expect(written).toEqual(['custom/AGENT.md']);
829
- expect(fs.existsSync(path.join(tmpDir, 'custom', 'AGENT.md'))).toBe(true);
830
- });
831
-
832
- it('onlyReplace: skips files without XDS markers', () => {
833
- setupCorePackage(tmpDir);
834
- fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), '# Claude\n\nProject rules only.\n');
835
-
836
- const written = installAgentDocs(tmpDir, {onlyReplace: true});
837
-
838
- expect(written).toEqual([]);
839
- const content = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8');
840
- expect(content).not.toContain('<!-- ASTRYX:START -->');
841
- expect(content).toBe('# Claude\n\nProject rules only.\n');
842
- });
843
-
844
- it('onlyReplace: updates files that have XDS markers', () => {
845
- setupCorePackage(tmpDir, '2.0.0');
846
- fs.writeFileSync(
847
- path.join(tmpDir, 'CLAUDE.md'),
848
- '# Claude\n\n<!-- XDS:START -->\nPLACEHOLDER_STALE_CONTENT\n<!-- XDS:END -->\n\nOther rules.\n',
849
- );
850
-
851
- const written = installAgentDocs(tmpDir, {onlyReplace: true});
852
-
853
- expect(written).toEqual(['CLAUDE.md']);
854
- const content = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8');
855
- expect(content).toContain('Astryx v2.0.0');
856
- expect(content).not.toContain('PLACEHOLDER_STALE_CONTENT');
857
- expect(content).toContain('Other rules.');
858
- });
859
-
860
- it('onlyReplace: does not create the default AGENTS.md when nothing exists', () => {
861
- setupCorePackage(tmpDir);
862
-
863
- const written = installAgentDocs(tmpDir, {onlyReplace: true});
864
-
865
- expect(written).toEqual([]);
866
- expect(fs.existsSync(path.join(tmpDir, 'AGENTS.md'))).toBe(false);
867
- expect(fs.existsSync(path.join(tmpDir, '.claude', 'CLAUDE.md'))).toBe(false);
868
- });
869
- });
870
-
871
- describe('discoverAgentDocs', () => {
872
- it('finds AGENTS.md and CLAUDE.md at root', () => {
873
- fs.writeFileSync(path.join(tmpDir, 'AGENTS.md'), '');
874
- fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), '');
875
-
876
- const found = discoverAgentDocs(tmpDir);
877
-
878
- expect(found).toContain('AGENTS.md');
879
- expect(found).toContain('CLAUDE.md');
880
- });
881
-
882
- it('finds .claude/CLAUDE.md', () => {
883
- fs.mkdirSync(path.join(tmpDir, '.claude'), {recursive: true});
884
- fs.writeFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), '');
885
-
886
- const found = discoverAgentDocs(tmpDir);
887
-
888
- expect(found).toContain('.claude/CLAUDE.md');
889
- });
890
-
891
- it('returns empty when nothing exists', () => {
892
- expect(discoverAgentDocs(tmpDir)).toEqual([]);
893
- });
894
- });
895
-
896
- describe('resolveAgentPaths', () => {
897
- it('claude preset finds existing CLAUDE.md at root', () => {
898
- fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), '');
899
- const result = resolveAgentPaths(tmpDir, 'claude');
900
- expect(result).toEqual({inject: ['CLAUDE.md'], create: []});
901
- });
902
-
903
- it('claude preset falls back to .claude/CLAUDE.md', () => {
904
- const result = resolveAgentPaths(tmpDir, 'claude');
905
- expect(result).toEqual({inject: [], create: ['.claude/CLAUDE.md']});
906
- });
907
-
908
- it('all preset discovers existing files', () => {
909
- fs.writeFileSync(path.join(tmpDir, 'AGENTS.md'), '');
910
- fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), '');
911
- const result = resolveAgentPaths(tmpDir, 'all');
912
- expect(result.inject).toContain('AGENTS.md');
913
- expect(result.inject).toContain('CLAUDE.md');
914
- expect(result.create).toEqual([]);
915
- });
916
-
917
- it('all preset creates defaults when nothing exists', () => {
918
- const result = resolveAgentPaths(tmpDir, 'all');
919
- expect(result.inject).toEqual([]);
920
- expect(result.create).toContain('AGENTS.md');
921
- expect(result.create).toContain('.claude/CLAUDE.md');
922
- });
923
-
924
- it('hermes preset creates AGENTS.md when nothing exists', () => {
925
- const result = resolveAgentPaths(tmpDir, 'hermes');
926
- expect(result).toEqual({inject: [], create: ['AGENTS.md']});
927
- });
928
-
929
- it('hermes preset finds existing .hermes.md', () => {
930
- fs.writeFileSync(path.join(tmpDir, '.hermes.md'), '');
931
- const result = resolveAgentPaths(tmpDir, 'hermes');
932
- expect(result).toEqual({inject: ['.hermes.md'], create: []});
933
- });
934
-
935
- it('hermes preset finds existing HERMES.md when no .hermes.md', () => {
936
- fs.writeFileSync(path.join(tmpDir, 'HERMES.md'), '');
937
- const result = resolveAgentPaths(tmpDir, 'hermes');
938
- expect(result).toEqual({inject: ['HERMES.md'], create: []});
939
- });
940
-
941
- it('muse preset creates AGENTS.md when nothing exists', () => {
942
- const result = resolveAgentPaths(tmpDir, 'muse');
943
- expect(result).toEqual({inject: [], create: ['AGENTS.md']});
944
- });
945
-
946
- it('claude preset still creates .claude/CLAUDE.md when nothing exists (hermes is additive)', () => {
947
- const result = resolveAgentPaths(tmpDir, 'claude');
948
- expect(result).toEqual({inject: [], create: ['.claude/CLAUDE.md']});
949
- });
950
- });
951
-
952
- describe('parseBlockVersion', () => {
953
- it('reads the version from the header of a generated block', () => {
954
- expect(parseBlockVersion(generateCompressedIndex('1.2.3'))).toBe('1.2.3');
955
- });
956
-
957
- it('reads prerelease versions', () => {
958
- expect(parseBlockVersion('Astryx v1.2.3-beta.4 · 10 components')).toBe(
959
- '1.2.3-beta.4',
960
- );
961
- });
962
-
963
- it('returns null when no versioned header is present', () => {
964
- expect(parseBlockVersion('<!-- XDS:START -->\nold\n<!-- XDS:END -->')).toBeNull();
965
- expect(parseBlockVersion('')).toBeNull();
966
- expect(parseBlockVersion(undefined)).toBeNull();
967
- });
968
- });
969
-
970
- describe('inspectAgentDocs', () => {
971
- function writeBlock(rel, version) {
972
- const filePath = path.join(tmpDir, rel);
973
- fs.mkdirSync(path.dirname(filePath), {recursive: true});
974
- fs.writeFileSync(filePath, `# Doc\n\n${generateCompressedIndex(version)}\n`);
975
- }
976
-
977
- it('reports missing when no agent docs exist at all', () => {
978
- const res = inspectAgentDocs(tmpDir, '1.0.0');
979
- expect(res.status).toBe('missing');
980
- expect(res.files).toEqual([]);
981
- expect(res.staleFiles).toEqual([]);
982
- });
983
-
984
- it('reports missing when agent files exist but carry no Astryx marker', () => {
985
- fs.writeFileSync(path.join(tmpDir, 'AGENTS.md'), '# Agents\n\nHand-written notes.\n');
986
- expect(inspectAgentDocs(tmpDir, '1.0.0').status).toBe('missing');
987
- });
988
-
989
- it('reports current when the only block matches the installed version', () => {
990
- writeBlock('AGENTS.md', '1.0.0');
991
- const res = inspectAgentDocs(tmpDir, '1.0.0');
992
- expect(res.status).toBe('current');
993
- expect(res.staleFiles).toEqual([]);
994
- expect(res.blockVersions).toEqual([]);
995
- });
996
-
997
- it.each([
998
- ['append addition', [], [{package: '@acme/a', append: ['added']}]],
999
- ['append removal', [{package: '@acme/a', append: ['removed']}], []],
1000
- [
1001
- 'integration reorder',
1002
- [
1003
- {package: '@acme/a', append: ['a']},
1004
- {package: '@acme/b', append: ['b']},
1005
- ],
1006
- [
1007
- {package: '@acme/b', append: ['b']},
1008
- {package: '@acme/a', append: ['a']},
1009
- ],
1010
- ],
1011
- [
1012
- 'line content change',
1013
- [{package: '@acme/a', append: ['old']}],
1014
- [{package: '@acme/a', append: ['new']}],
1015
- ],
1016
- ])(
1017
- 'reports stale at the same Core version after %s',
1018
- (_label, current, expected) => {
1019
- const actualBlock = generateCompressedIndex('1.0.0', {
1020
- agentDocs: current,
1021
- });
1022
- fs.writeFileSync(
1023
- path.join(tmpDir, 'AGENTS.md'),
1024
- `# Doc\n\n${actualBlock}\n`,
1025
- );
1026
- const expectedBlock = generateCompressedIndex('1.0.0', {
1027
- agentDocs: expected,
1028
- });
1029
-
1030
- expect(inspectAgentDocs(tmpDir, '1.0.0', expectedBlock)).toMatchObject({
1031
- status: 'stale',
1032
- staleFiles: ['AGENTS.md'],
1033
- });
1034
- },
1035
- );
1036
-
1037
- it('reports stale (with the old version) when the block is behind', () => {
1038
- writeBlock('AGENTS.md', '1.0.0');
1039
- const res = inspectAgentDocs(tmpDir, '2.0.0');
1040
- expect(res.status).toBe('stale');
1041
- expect(res.staleFiles).toEqual(['AGENTS.md']);
1042
- expect(res.blockVersions).toEqual(['1.0.0']);
1043
- expect(res.installedVersion).toBe('2.0.0');
1044
- });
1045
-
1046
- it('treats a legacy XDS block as stale even with no parseable version', () => {
1047
- fs.writeFileSync(
1048
- path.join(tmpDir, 'CLAUDE.md'),
1049
- '# Claude\n\n<!-- XDS:START -->\nlegacy index\n<!-- XDS:END -->\n',
1050
- );
1051
- const res = inspectAgentDocs(tmpDir, '1.0.0');
1052
- expect(res.status).toBe('stale');
1053
- expect(res.files[0].legacy).toBe(true);
1054
- expect(res.blockVersions).toEqual([]);
1055
- });
1056
-
1057
- it('is stale if ANY marked file is behind (mixed current + stale)', () => {
1058
- writeBlock('AGENTS.md', '2.0.0');
1059
- writeBlock('CLAUDE.md', '1.0.0');
1060
- const res = inspectAgentDocs(tmpDir, '2.0.0');
1061
- expect(res.status).toBe('stale');
1062
- expect(res.staleFiles).toEqual(['CLAUDE.md']);
1063
- });
1064
-
1065
- it('defaults the installed version to the core package when omitted', () => {
1066
- const coreDir = path.join(tmpDir, 'node_modules', '@astryxdesign', 'core');
1067
- fs.mkdirSync(coreDir, {recursive: true});
1068
- fs.writeFileSync(path.join(coreDir, 'package.json'), JSON.stringify({version: '3.0.0'}));
1069
- writeBlock('AGENTS.md', '3.0.0');
1070
- const res = inspectAgentDocs(tmpDir);
1071
- expect(res.installedVersion).toBe('3.0.0');
1072
- expect(res.status).toBe('current');
1073
- });
1074
- });
1075
-
1076
- describe('injectXdsBlock / removeXdsBlock — malformed managed blocks (no user-content loss)', () => {
1077
- const S = '<!-- ASTRYX:START -->';
1078
- const E = '<!-- ASTRYX:END -->';
1079
- const BLOCK = `${S}\nNEW\n${E}`;
1080
-
1081
- it('does not duplicate user content when END precedes START', () => {
1082
- const f = path.join(tmpDir, 'a.md');
1083
- fs.writeFileSync(f, `# Doc\nUSER-A\n${E}\nUSER-B\n${S}\nUSER-C\n`);
1084
- removeXdsBlock(f);
1085
- const c = fs.readFileSync(f, 'utf-8');
1086
- expect((c.match(/USER-B/g) || []).length).toBeLessThanOrEqual(1);
1087
- });
1088
-
1089
- it('refuses (throws) instead of orphaning a second block when duplicates exist', () => {
1090
- const f = path.join(tmpDir, 'b.md');
1091
- fs.writeFileSync(f, `# Doc\n${S}\nB1\n${E}\nMID\n${S}\nB2\n${E}\nTAIL\n`);
1092
- expect(() => injectXdsBlock(f, BLOCK)).toThrow(/multiple|malformed/i);
1093
- // file left untouched for the user to fix
1094
- expect(fs.readFileSync(f, 'utf-8')).toContain('MID');
1095
- expect(fs.readFileSync(f, 'utf-8')).toContain('TAIL');
1096
- });
1097
-
1098
- it('refuses to append a second block when a START has no matching END', () => {
1099
- const f = path.join(tmpDir, 'c.md');
1100
- fs.writeFileSync(f, `# Doc\nUSER1\n${S}\nOLD (no end)\nUSER2\n`);
1101
- expect(() => injectXdsBlock(f, BLOCK)).toThrow(/end marker|malformed|no matching/i);
1102
- expect((fs.readFileSync(f, 'utf-8').match(/ASTRYX:START/g) || []).length).toBe(1);
1103
- });
1104
-
1105
- it('replaces a single well-formed block idempotently, preserving surrounding content', () => {
1106
- const f = path.join(tmpDir, 'ok.md');
1107
- fs.writeFileSync(f, `# Doc\nUSER\n${S}\nOLD\n${E}\nTAIL\n`);
1108
- injectXdsBlock(f, BLOCK);
1109
- const c = fs.readFileSync(f, 'utf-8');
1110
- expect((c.match(/ASTRYX:START/g) || []).length).toBe(1);
1111
- expect(c).toContain('NEW');
1112
- expect(c).toContain('USER');
1113
- expect(c).toContain('TAIL');
1114
- expect(c).not.toContain('OLD');
1115
- });
1116
- });
1117
-
1118
- describe('isAstryxInitialized — resilient to malformed markers (postinstall hot path)', () => {
1119
- // isAstryxInitialized runs on every consumer `npm install` (via postinstall).
1120
- // It must never throw — even on a half-written or garbage-marker file — so it
1121
- // uses a plain `.includes(MARKER_START)`, never the strict findManagedBlock.
1122
- const START = '<!-- ASTRYX:START -->';
1123
- const END = '<!-- ASTRYX:END -->';
1124
-
1125
- it('returns true (never throws) on duplicate START markers', () => {
1126
- fs.writeFileSync(
1127
- path.join(tmpDir, 'AGENTS.md'),
1128
- `${START}\na\n${START}\nb\n${END}\n`,
1129
- );
1130
- expect(() => isAstryxInitialized(tmpDir)).not.toThrow();
1131
- expect(isAstryxInitialized(tmpDir)).toBe(true);
1132
- });
1133
-
1134
- it('returns true on an END-before-START file without throwing', () => {
1135
- fs.writeFileSync(
1136
- path.join(tmpDir, 'AGENTS.md'),
1137
- `${END}\nuser\n${START}\nblock\n`,
1138
- );
1139
- expect(() => isAstryxInitialized(tmpDir)).not.toThrow();
1140
- expect(isAstryxInitialized(tmpDir)).toBe(true);
1141
- });
1142
-
1143
- it('returns true on a START with no END (half-written block)', () => {
1144
- fs.writeFileSync(
1145
- path.join(tmpDir, 'AGENTS.md'),
1146
- `# Doc\nuser\n${START}\ntruncated block, no end\n`,
1147
- );
1148
- expect(() => isAstryxInitialized(tmpDir)).not.toThrow();
1149
- expect(isAstryxInitialized(tmpDir)).toBe(true);
1150
- });
1151
-
1152
- it('returns false for a directory with no agent docs', () => {
1153
- expect(isAstryxInitialized(tmpDir)).toBe(false);
1154
- });
1155
-
1156
- it('returns false for a nonexistent directory', () => {
1157
- expect(isAstryxInitialized(path.join(tmpDir, 'does-not-exist'))).toBe(false);
1158
- });
1159
- });