@astryxdesign/cli 0.7.2-canary.fe2c655 → 0.7.3-canary.02f5fe4

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 (694) hide show
  1. package/CHANGELOG.md +119 -0
  2. package/README.md +216 -162
  3. package/api/build/_adapter.d.mts +66 -0
  4. package/api/build/_adapter.mjs +139 -4
  5. package/api/build/blocks/blocks.d.mts +92 -0
  6. package/api/build/blocks/blocks.mjs +528 -0
  7. package/api/build/blocks/blocks.test.mjs +238 -0
  8. package/api/build/blocks/blocks.weights.json +1 -0
  9. package/api/build/build.doc.mjs +4 -0
  10. package/api/build/build.mjs +25 -6
  11. package/api/build/build.type.mjs +1 -1
  12. package/api/build/help/help.mjs +1 -1
  13. package/api/build/kit/answer.d.mts +18 -0
  14. package/api/build/kit/answer.mjs +195 -0
  15. package/api/build/kit/answer.test.mjs +108 -0
  16. package/api/build/kit/kind.d.mts +25 -0
  17. package/api/build/kit/kind.mjs +170 -0
  18. package/api/build/kit/kit.d.mts +2 -4
  19. package/api/build/kit/kit.mjs +8 -406
  20. package/api/build/kit/rank.d.mts +3 -32
  21. package/api/build/kit/rank.mjs +25 -342
  22. package/api/build/kit/rank.test.mjs +2 -1
  23. package/api/build/kit/signals.d.mts +79 -0
  24. package/api/build/kit/signals.mjs +295 -0
  25. package/api/build/kit/start.d.mts +32 -0
  26. package/api/build/kit/start.mjs +163 -0
  27. package/api/build/kit/words.d.mts +72 -0
  28. package/api/build/kit/words.mjs +202 -0
  29. package/api/build/tree.d.mts +20 -0
  30. package/api/build/tree.mjs +53 -0
  31. package/api/build/tree.test.mjs +185 -0
  32. package/api/build/tree.weights.json +1 -0
  33. package/api/component/_adapter.d.mts +72 -1
  34. package/api/component/_adapter.mjs +251 -23
  35. package/api/component/component-replacement.test.mjs +1 -0
  36. package/api/component/component.doc.mjs +6 -1
  37. package/api/component/component.mjs +97 -16
  38. package/api/component/component.test.mjs +95 -0
  39. package/api/component/doc-links.test.mjs +386 -0
  40. package/api/component/integration-export-name.test.mjs +644 -0
  41. package/api/component/list/list.mjs +42 -4
  42. package/api/discover/_adapter.d.mts +131 -12
  43. package/api/discover/_adapter.mjs +520 -34
  44. package/api/discover/_adapter.test.mjs +641 -17
  45. package/api/discover/_catalog-view.d.mts +16 -5
  46. package/api/discover/_catalog-view.mjs +32 -8
  47. package/api/discover/detail/doc/doc.mjs +9 -1
  48. package/api/discover/discover.d.mts +11 -0
  49. package/api/discover/discover.doc.mjs +9 -7
  50. package/api/discover/discover.mjs +120 -19
  51. package/api/discover/discover.type.d.mts +20 -7
  52. package/api/discover/discover.type.mjs +17 -7
  53. package/api/discover/list/list.d.mts +4 -3
  54. package/api/discover/list/list.mjs +17 -10
  55. package/api/docs/_adapter.d.mts +76 -2
  56. package/api/docs/_adapter.mjs +630 -34
  57. package/api/docs/docs-homes.test.mjs +127 -0
  58. package/api/docs/docs.doc.mjs +5 -4
  59. package/api/docs/docs.mjs +2 -1
  60. package/api/docs/docs.test.mjs +39 -20
  61. package/api/docs/integration-tree.test.mjs +42 -20
  62. package/api/docs/integrationDocs.test.mjs +125 -1
  63. package/api/docs/list/list.mjs +12 -2
  64. package/api/docs/namespace-section.test.mjs +6 -6
  65. package/api/docs/reference-blocks.test.mjs +3 -5
  66. package/api/doctor/_adapter.d.mts +128 -0
  67. package/api/doctor/_adapter.mjs +253 -4
  68. package/api/doctor/doctor.d.mts +217 -0
  69. package/api/doctor/doctor.doc.mjs +3 -2
  70. package/api/doctor/doctor.mjs +691 -52
  71. package/api/doctor/doctor.test.mjs +267 -18
  72. package/api/doctor/release-checks.test.mjs +156 -0
  73. package/api/doctor/theme-checks.mjs +17 -4
  74. package/api/doctor/theme-checks.test.mjs +223 -7
  75. package/api/error.d.mts +10 -0
  76. package/api/error.mjs +25 -0
  77. package/api/gap-report/gap-report.d.mts +3 -0
  78. package/api/gap-report/gap-report.doc.mjs +11 -3
  79. package/api/gap-report/gap-report.mjs +219 -52
  80. package/api/gap-report/gap-report.test.mjs +233 -0
  81. package/api/gap-report/gap-report.type.d.mts +41 -0
  82. package/api/gap-report/gap-report.type.mjs +18 -0
  83. package/api/hook/_adapter.mjs +3 -11
  84. package/api/hook/hook.doc.mjs +4 -0
  85. package/api/hook/hook.mjs +1 -1
  86. package/api/hook/list/list.d.mts +1 -1
  87. package/api/index.d.mts +2 -0
  88. package/api/index.mjs +2 -0
  89. package/api/init/_adapter.d.mts +22 -0
  90. package/api/init/_adapter.mjs +59 -0
  91. package/api/init/init.d.mts +7 -5
  92. package/api/init/init.doc.mjs +45 -10
  93. package/api/init/init.mjs +28 -4
  94. package/api/init/init.test.mjs +18 -2
  95. package/api/init/init.type.d.mts +50 -5
  96. package/api/init/init.type.mjs +39 -5
  97. package/api/init/remove/remove.mjs +17 -4
  98. package/api/init/remove/remove.test.mjs +114 -2
  99. package/api/init/run/run.d.mts +1 -1
  100. package/api/init/run/run.mjs +86 -12
  101. package/api/init/user/user.d.mts +14 -0
  102. package/api/init/user/user.mjs +87 -0
  103. package/api/init/user/user.test.mjs +232 -0
  104. package/api/integration/_adapter.d.mts +53 -0
  105. package/api/integration/_adapter.mjs +132 -0
  106. package/api/integration/add-contribution.component-names.test.mjs +66 -0
  107. package/api/integration/add-contribution.mjs +8 -4
  108. package/api/integration/add-contribution.test.mjs +4 -2
  109. package/api/integration/authoring-checks.mjs +192 -14
  110. package/api/integration/authoring-checks.test.mjs +1028 -0
  111. package/api/integration/authoring-checks.type.d.mts +143 -0
  112. package/api/integration/authoring-checks.type.mjs +74 -2
  113. package/api/integration/check/check.d.mts +96 -0
  114. package/api/integration/check/check.mjs +357 -0
  115. package/api/integration/check/check.test.mjs +489 -0
  116. package/api/integration/integration-check.type.d.mts +118 -0
  117. package/api/integration/integration-check.type.mjs +84 -0
  118. package/api/integration/integrationCheck.doc.d.mts +10 -0
  119. package/api/integration/integrationCheck.doc.mjs +73 -0
  120. package/api/integration/integrationComponentConflicts.doc.mjs +45 -6
  121. package/api/integration/integrationDocConflicts.doc.mjs +22 -5
  122. package/api/integration/pack-check.mjs +486 -123
  123. package/api/integration/pack-check.test.mjs +817 -10
  124. package/api/integration/validate-integration-case-disk.test.mjs +111 -0
  125. package/api/integration/validate-integration-fixes.test.mjs +73 -5
  126. package/api/integration/validate-integration-gaps.test.mjs +1975 -0
  127. package/api/integration/validate-integration.d.mts +23 -2
  128. package/api/integration/validate-integration.mjs +358 -45
  129. package/api/integration/validate-integration.test.mjs +88 -8
  130. package/api/integration/validate-integration.type.d.mts +105 -0
  131. package/api/integration/validate-integration.type.mjs +79 -1
  132. package/api/integration/validate-unread-theme-folders.test.mjs +3 -2
  133. package/api/integration/validateIntegration.doc.mjs +19 -3
  134. package/api/search/ORDERING.md +28 -0
  135. package/api/search/_adapter.d.mts +91 -0
  136. package/api/search/_adapter.mjs +172 -0
  137. package/api/search/_adapter.test.mjs +173 -0
  138. package/api/search/_candidate-score.d.mts +40 -0
  139. package/api/search/_candidate-score.mjs +238 -0
  140. package/api/search/_cards.json +6098 -0
  141. package/api/search/_compound.d.mts +50 -0
  142. package/api/search/_compound.mjs +170 -0
  143. package/api/search/_doc-searcher.d.mts +4 -0
  144. package/api/search/_doc-searcher.mjs +23 -0
  145. package/api/search/_doc-searcher.test.mjs +36 -0
  146. package/api/search/_doc-searcher.weights.json +1 -0
  147. package/api/search/_doc-text.d.mts +30 -0
  148. package/api/search/_doc-text.mjs +87 -0
  149. package/api/search/_entries.d.mts +63 -0
  150. package/api/search/_entries.mjs +232 -0
  151. package/api/search/_entries.test.mjs +48 -0
  152. package/api/search/_features.d.mts +35 -0
  153. package/api/search/_features.mjs +155 -0
  154. package/api/search/_gather-components.d.mts +26 -0
  155. package/api/search/_gather-components.mjs +284 -0
  156. package/api/search/_gather-docs.d.mts +20 -0
  157. package/api/search/_gather-docs.mjs +254 -0
  158. package/api/search/_gather-hooks.d.mts +12 -0
  159. package/api/search/_gather-hooks.mjs +52 -0
  160. package/api/search/_gather-templates.d.mts +25 -0
  161. package/api/search/_gather-templates.mjs +107 -0
  162. package/api/search/_lexical.d.mts +135 -0
  163. package/api/search/_lexical.mjs +351 -0
  164. package/api/search/_main-searcher.d.mts +19 -0
  165. package/api/search/_main-searcher.mjs +115 -0
  166. package/api/search/_main-searcher.test.mjs +106 -0
  167. package/api/search/_main-searcher.weights.json +1 -0
  168. package/api/search/_module-doc.d.mts +32 -0
  169. package/api/search/_module-doc.mjs +78 -0
  170. package/api/search/_order.d.mts +32 -0
  171. package/api/search/_order.mjs +162 -0
  172. package/api/search/_order.test.mjs +72 -0
  173. package/api/search/_priority.d.mts +50 -0
  174. package/api/search/_priority.mjs +139 -0
  175. package/api/search/_query-words.d.mts +25 -0
  176. package/api/search/_query-words.mjs +92 -0
  177. package/api/search/_query.d.mts +65 -0
  178. package/api/search/_query.mjs +176 -0
  179. package/api/search/_result.d.mts +97 -0
  180. package/api/search/_result.mjs +150 -0
  181. package/api/search/_score.d.mts +60 -0
  182. package/api/search/_score.mjs +283 -0
  183. package/api/search/_synonyms.d.mts +21 -0
  184. package/api/search/_synonyms.mjs +67 -0
  185. package/api/search/_template-searcher.d.mts +4 -0
  186. package/api/search/_template-searcher.mjs +23 -0
  187. package/api/search/_template-searcher.test.mjs +40 -0
  188. package/api/search/_template-searcher.weights.json +1 -0
  189. package/api/search/_titles.d.mts +33 -0
  190. package/api/search/_titles.mjs +65 -0
  191. package/api/search/_words.d.mts +64 -0
  192. package/api/search/_words.mjs +157 -0
  193. package/api/search/search.d.mts +22 -236
  194. package/api/search/search.doc.mjs +14 -7
  195. package/api/search/search.domain-priority.test.mjs +108 -18
  196. package/api/search/search.integration-check.test.mjs +24 -0
  197. package/api/search/search.mjs +59 -1904
  198. package/api/search/search.test.mjs +193 -15
  199. package/api/search/search.type.d.mts +20 -2
  200. package/api/search/search.type.mjs +12 -2
  201. package/api/search/tree.d.mts +15 -0
  202. package/api/search/tree.mjs +40 -0
  203. package/api/search/tree.test.mjs +66 -0
  204. package/api/search/tree.weights.json +1 -0
  205. package/api/swizzle/_adapter.mjs +2 -2
  206. package/api/swizzle/copy/copy.mjs +29 -15
  207. package/api/template/template-integration.test.mjs +1 -1
  208. package/api/template/template.mjs +5 -1
  209. package/api/template/template.test.mjs +120 -3
  210. package/api/theme/_adapter.d.mts +16 -0
  211. package/api/theme/_adapter.mjs +32 -5
  212. package/api/theme/build/build.d.mts +2 -1
  213. package/api/theme/build/build.family.test.mjs +64 -0
  214. package/api/theme/build/build.mjs +250 -38
  215. package/api/theme/build/build.packed-old-core.test.mjs +21 -3
  216. package/api/theme/build/font-warning.mjs +5 -3
  217. package/api/theme/build/font-warning.test.mjs +2 -1
  218. package/api/theme/remove/remove.test.mjs +2 -2
  219. package/api/theme/targets/targets.mjs +3 -10
  220. package/api/theme/themeBuild.doc.mjs +4 -4
  221. package/api/theme/themeTargets.doc.mjs +4 -0
  222. package/api/theme/use/use.test.mjs +1 -1
  223. package/api/upgrade/_adapter.d.mts +97 -11
  224. package/api/upgrade/_adapter.mjs +315 -32
  225. package/api/upgrade/registry/registry.d.mts +10 -4
  226. package/api/upgrade/registry/registry.mjs +33 -16
  227. package/api/upgrade/registry/registry.test.mjs +85 -0
  228. package/api/upgrade/run/cli-behind-core.test.mjs +438 -0
  229. package/api/upgrade/run/run.d.mts +4 -2
  230. package/api/upgrade/run/run.mjs +48 -4
  231. package/api/upgrade/run/run.test.mjs +417 -3
  232. package/api/upgrade/status/status.d.mts +21 -5
  233. package/api/upgrade/status/status.mjs +45 -8
  234. package/api/upgrade/upgrade.doc.mjs +16 -8
  235. package/api/upgrade/upgrade.mjs +18 -3
  236. package/api/upgrade/upgrade.test.mjs +194 -0
  237. package/api/upgrade/upgrade.type.d.mts +28 -3
  238. package/api/upgrade/upgrade.type.mjs +24 -7
  239. package/assets/codemods/__tests__/registry.test.mjs +1 -0
  240. package/assets/codemods/registry.mjs +1 -0
  241. package/assets/codemods/runner.mjs +7 -1
  242. package/assets/codemods/transforms/v0.7.0/__tests__/validate-data-token-ownership.test.mjs +11 -0
  243. package/assets/codemods/transforms/v0.7.0/validate-data-token-ownership.mjs +3 -3
  244. package/assets/codemods/transforms/v0.7.2/__tests__/migrate-list-edge-compensation-to-edge.test.mjs +120 -0
  245. package/assets/codemods/transforms/v0.7.2/index.mjs +20 -0
  246. package/assets/codemods/transforms/v0.7.2/migrate-list-edge-compensation-to-edge.mjs +198 -0
  247. package/assets/discover/official-packages.json +367 -0
  248. package/assets/docs/README.md +10 -0
  249. package/assets/docs/author-a-theme.doc.mjs +5 -5
  250. package/assets/docs/browser-support.doc.mjs +1 -1
  251. package/assets/docs/getting-started.doc.mjs +26 -6
  252. package/assets/docs/is-my-code-good.doc.mjs +287 -0
  253. package/assets/docs/styling-overview.doc.mjs +1 -1
  254. package/assets/docs/theme.doc.dense.mjs +1 -1
  255. package/assets/docs/theme.doc.mjs +2 -2
  256. package/assets/docs/theme.doc.zh.mjs +1 -1
  257. package/assets/docs/tree/add-a-component.doc.mjs +22 -2
  258. package/assets/docs/tree/add-a-theme.doc.mjs +14 -4
  259. package/assets/docs/tree/add-a-topic.doc.mjs +54 -9
  260. package/assets/docs/tree/agent-guidance.doc.mjs +19 -5
  261. package/assets/docs/tree/api.doc.mjs +6 -0
  262. package/assets/docs/tree/authoring.doc.mjs +61 -0
  263. package/assets/docs/tree/block-template.doc.mjs +8 -20
  264. package/assets/docs/tree/build-the-template.doc.mjs +1 -1
  265. package/assets/docs/tree/building-blocks.doc.mjs +6 -2
  266. package/assets/docs/tree/check-the-props.doc.mjs +201 -0
  267. package/assets/docs/tree/check-your-docs.doc.mjs +69 -11
  268. package/assets/docs/tree/checks.doc.mjs +32 -24
  269. package/assets/docs/tree/ci-and-containers.doc.mjs +101 -0
  270. package/assets/docs/tree/cli-command.doc.mjs +142 -0
  271. package/assets/docs/tree/cli.doc.mjs +7 -1
  272. package/assets/docs/tree/codemods.doc.mjs +17 -7
  273. package/assets/docs/tree/commands.doc.mjs +7 -1
  274. package/assets/docs/tree/component-family.doc.mjs +8 -6
  275. package/assets/docs/tree/component-imports.doc.mjs +21 -3
  276. package/assets/docs/tree/components.doc.mjs +7 -1
  277. package/assets/docs/tree/computer-agent-guide.doc.mjs +160 -0
  278. package/assets/docs/tree/computer-integrations.doc.mjs +93 -0
  279. package/assets/docs/tree/configuration.doc.mjs +8 -4
  280. package/assets/docs/tree/debug-and-gap-reports.doc.mjs +41 -22
  281. package/assets/docs/tree/define-the-theme.doc.mjs +7 -6
  282. package/assets/docs/tree/describe-the-component.doc.mjs +3 -3
  283. package/assets/docs/tree/docs.doc.mjs +9 -3
  284. package/assets/docs/tree/document-the-template.doc.mjs +1 -1
  285. package/assets/docs/tree/document-the-theme.doc.mjs +4 -4
  286. package/assets/docs/tree/export-template-assets.doc.mjs +1 -1
  287. package/assets/docs/tree/extend-or-replace.doc.mjs +19 -9
  288. package/assets/docs/{authoring.doc.mjs → tree/field-reference.doc.mjs} +4 -3
  289. package/assets/docs/tree/fonts-and-assets.doc.mjs +3 -3
  290. package/assets/docs/tree/generate-a-palette.doc.mjs +8 -6
  291. package/assets/docs/tree/global-integrations.doc.mjs +124 -0
  292. package/assets/docs/tree/global.doc.mjs +131 -0
  293. package/assets/docs/tree/grade-template-with-agent.doc.mjs +5 -5
  294. package/assets/docs/tree/help.doc.mjs +2 -2
  295. package/assets/docs/tree/host-entry.doc.mjs +128 -0
  296. package/assets/docs/tree/install-once.doc.mjs +105 -0
  297. package/assets/docs/tree/layout-spacing.doc.mjs +2 -3
  298. package/assets/docs/tree/layout.doc.mjs +6 -6
  299. package/assets/docs/tree/links.doc.mjs +7 -6
  300. package/assets/docs/tree/migration-setup.doc.mjs +3 -2
  301. package/assets/docs/tree/migration-verification.doc.mjs +0 -1
  302. package/assets/docs/tree/migration.doc.mjs +0 -1
  303. package/assets/docs/tree/monorepos.doc.mjs +84 -0
  304. package/assets/docs/tree/package-and-test.doc.mjs +1 -1
  305. package/assets/docs/tree/page-template.doc.mjs +4 -8
  306. package/assets/docs/tree/problems.doc.mjs +205 -0
  307. package/assets/docs/tree/project-global-or-both.doc.mjs +131 -0
  308. package/assets/docs/tree/publishing.doc.mjs +6 -6
  309. package/assets/docs/tree/quick-start.doc.mjs +54 -28
  310. package/assets/docs/tree/reference.doc.mjs +192 -0
  311. package/assets/docs/tree/replace-a-core-component.doc.mjs +15 -7
  312. package/assets/docs/tree/replace-a-core-template.doc.mjs +10 -12
  313. package/assets/docs/tree/scaffold.doc.mjs +1 -1
  314. package/assets/docs/tree/sections-and-placement.doc.mjs +9 -11
  315. package/assets/docs/tree/see-it-in-an-app.doc.mjs +17 -5
  316. package/assets/docs/tree/ship.doc.mjs +2 -2
  317. package/assets/docs/tree/short-and-findable.doc.mjs +9 -8
  318. package/assets/docs/tree/single-component.doc.mjs +15 -6
  319. package/assets/docs/tree/start-a-template.doc.mjs +18 -8
  320. package/assets/docs/tree/subcomponent.doc.mjs +6 -6
  321. package/assets/docs/tree/template-assets.doc.mjs +2 -2
  322. package/assets/docs/tree/template-doc-overview.doc.mjs +6 -17
  323. package/assets/docs/tree/template-fonts.doc.mjs +1 -1
  324. package/assets/docs/tree/template-grading-rubric.doc.mjs +17 -38
  325. package/assets/docs/tree/template-icons.doc.mjs +1 -1
  326. package/assets/docs/tree/template-images-media.doc.mjs +1 -1
  327. package/assets/docs/tree/template-styles.doc.mjs +2 -2
  328. package/assets/docs/tree/templates.doc.mjs +7 -1
  329. package/assets/docs/tree/test-in-an-app.doc.mjs +7 -7
  330. package/assets/docs/tree/test-template-in-app.doc.mjs +5 -5
  331. package/assets/docs/tree/themes.doc.mjs +5 -1
  332. package/assets/docs/tree/tokens-and-setup.doc.mjs +19 -15
  333. package/assets/docs/tree/tokens-data.doc.mjs +2 -2
  334. package/assets/docs/tree/troubleshooting.doc.mjs +29 -13
  335. package/assets/docs/tree/type-scale.doc.mjs +3 -3
  336. package/assets/docs/tree/uninstall.doc.mjs +109 -0
  337. package/assets/docs/tree/updates.doc.mjs +92 -0
  338. package/assets/docs/tree/upgrading.doc.mjs +6 -6
  339. package/assets/docs/tree/use-a-theme-in-an-app.doc.mjs +15 -3
  340. package/assets/docs/tree/verify-packed-template.doc.mjs +3 -3
  341. package/assets/docs/tree/versioning.doc.mjs +14 -9
  342. package/assets/docs/tree/who-answers.doc.mjs +119 -0
  343. package/assets/docs/tree/write-good-templates.doc.mjs +1 -1
  344. package/assets/docs/tree/write-the-template-file.doc.mjs +7 -6
  345. package/assets/docs/use-a-theme.doc.mjs +2 -2
  346. package/assets/docs/use-integrations.doc.mjs +120 -0
  347. package/assets/docs/working-with-ai.doc.mjs +70 -13
  348. package/assets/templates/themes/butter/butterTheme.ts +4 -2
  349. package/assets/templates/themes/stone/stoneTheme.ts +4 -2
  350. package/assets/templates/themes/y2k/y2kTheme.ts +4 -2
  351. package/assets/theme.template.ts +1 -1
  352. package/authoring/codemod/codemod.doc.mjs +7 -3
  353. package/authoring/config/config.doc.mjs +9 -6
  354. package/authoring/config/type.ts +4 -2
  355. package/authoring/debug/parse.d.mts +5 -5
  356. package/authoring/discover/discover.doc.mjs +7 -1
  357. package/authoring/doctypes/_schema.d.mts +8 -3
  358. package/authoring/doctypes/_schema.mjs +1 -0
  359. package/authoring/doctypes/base/graph-fields.doc.mjs +6 -4
  360. package/authoring/doctypes/command/command.doc.mjs +20 -12
  361. package/authoring/doctypes/command/type.ts +14 -0
  362. package/authoring/doctypes/component/component.doc.mjs +30 -27
  363. package/authoring/doctypes/component/type.ts +21 -8
  364. package/authoring/doctypes/enum/enum.doc.mjs +7 -4
  365. package/authoring/doctypes/function/function.doc.mjs +9 -8
  366. package/authoring/doctypes/hook/hook.doc.mjs +5 -4
  367. package/authoring/doctypes/hook/type.ts +4 -0
  368. package/authoring/doctypes/namespace/namespace.doc.mjs +7 -6
  369. package/authoring/doctypes/reference/reference.doc.mjs +12 -11
  370. package/authoring/doctypes/schema/schema.doc.mjs +10 -10
  371. package/authoring/doctypes/template/integration-template.doc.d.mts +11 -0
  372. package/authoring/doctypes/template/integration-template.doc.mjs +49 -0
  373. package/authoring/doctypes/template/parse.d.mts +6 -2
  374. package/authoring/doctypes/template/parse.mjs +3 -1
  375. package/authoring/doctypes/template/template.doc.mjs +3 -3
  376. package/authoring/doctypes/template/type.ts +22 -0
  377. package/authoring/doctypes/theme/theme.doc.mjs +2 -2
  378. package/authoring/gap-report/gap-report.doc.mjs +2 -2
  379. package/authoring/identity/identity.doc.mjs +3 -3
  380. package/authoring/index.d.ts +2 -0
  381. package/authoring/integration/integration.doc.mjs +30 -6
  382. package/authoring/integration/parse.test.mjs +21 -0
  383. package/authoring/integration/schema.d.mts +16 -3
  384. package/authoring/integration/schema.mjs +34 -2
  385. package/authoring/integration/type.ts +13 -1
  386. package/clients/cli/bin/astryx.mjs +11 -65
  387. package/clients/cli/cli-command.test.mjs +166 -0
  388. package/clients/cli/command-result-coverage.test.mjs +6 -5
  389. package/clients/cli/commands/blog.doc.mjs +1 -0
  390. package/clients/cli/commands/build-theme.batch-report.test.mjs +17 -3
  391. package/clients/cli/commands/build-theme.color-scheme.test.mjs +1 -1
  392. package/clients/cli/commands/build-theme.font-warning.test.mjs +3 -3
  393. package/clients/cli/commands/build-theme.path-safety.test.mjs +224 -6
  394. package/clients/cli/commands/build-theme.registry.test.mjs +5 -2
  395. package/clients/cli/commands/build.doc.mjs +1 -0
  396. package/clients/cli/commands/build.exit-codes-doc.test.mjs +4 -0
  397. package/clients/cli/commands/cli-postinstall.test.mjs +49 -1
  398. package/clients/cli/commands/component.doc.mjs +1 -0
  399. package/clients/cli/commands/{component/index.mjs → component.mjs} +37 -37
  400. package/clients/cli/commands/component.test.mjs +6 -6
  401. package/clients/cli/commands/discover.broken-integration.test.mjs +11 -7
  402. package/clients/cli/commands/discover.doc.mjs +9 -1
  403. package/clients/cli/commands/discover.mjs +144 -93
  404. package/clients/cli/commands/discover.no-source.test.mjs +448 -33
  405. package/clients/cli/commands/discover.sources.test.mjs +20 -1
  406. package/clients/cli/commands/docs.doc.mjs +3 -2
  407. package/clients/cli/commands/docs.mjs +50 -20
  408. package/clients/cli/commands/docs.test.mjs +16 -16
  409. package/clients/cli/commands/doctor-integration-components.doc.mjs +19 -4
  410. package/clients/cli/commands/doctor-integration-docs.doc.mjs +20 -5
  411. package/clients/cli/commands/doctor-integration-templates.doc.mjs +1 -0
  412. package/clients/cli/commands/doctor-integration-validate.doc.mjs +22 -2
  413. package/clients/cli/commands/doctor-integration.test.mjs +936 -3
  414. package/clients/cli/commands/doctor.doc.mjs +12 -2
  415. package/clients/cli/commands/doctor.mjs +332 -7
  416. package/clients/cli/commands/doctor.test.mjs +75 -4
  417. package/clients/cli/commands/gap-report.doc.mjs +15 -2
  418. package/clients/cli/commands/gap-report.mjs +24 -2
  419. package/clients/cli/commands/gap-report.test.mjs +79 -0
  420. package/clients/cli/commands/hook.doc.mjs +1 -0
  421. package/clients/cli/commands/{hook/index.mjs → hook.mjs} +22 -22
  422. package/clients/cli/commands/import-hint-correctness.test.mjs +1 -1
  423. package/clients/cli/commands/init.behavior.test.mjs +235 -1
  424. package/clients/cli/commands/init.doc.mjs +55 -10
  425. package/clients/cli/commands/init.flag-help.test.mjs +1 -0
  426. package/clients/cli/commands/init.mjs +2 -2
  427. package/clients/cli/commands/integration-add.doc.mjs +7 -0
  428. package/clients/cli/commands/integration-authoring.test.mjs +49 -3
  429. package/clients/cli/commands/integration-check.doc.mjs +93 -0
  430. package/clients/cli/commands/integration-check.test.mjs +489 -0
  431. package/clients/cli/commands/integration-pack.doc.mjs +8 -1
  432. package/clients/cli/commands/integration-verify.doc.mjs +8 -1
  433. package/clients/cli/commands/integration.doc.mjs +6 -2
  434. package/clients/cli/commands/integration.mjs +119 -7
  435. package/clients/cli/commands/json-contract.test.mjs +11 -2
  436. package/clients/cli/commands/manifest.doc.mjs +2 -1
  437. package/clients/cli/commands/manifest.mjs +67 -0
  438. package/clients/cli/commands/outside-project.test.mjs +874 -0
  439. package/clients/cli/commands/postinstall.doc.mjs +23 -0
  440. package/clients/cli/commands/postinstall.mjs +31 -0
  441. package/clients/cli/commands/search.doc.mjs +1 -0
  442. package/clients/cli/commands/search.mjs +16 -11
  443. package/clients/cli/commands/search.test.mjs +429 -296
  444. package/clients/cli/commands/setup-nudge.test.mjs +180 -2
  445. package/clients/cli/commands/swizzle.doc.mjs +7 -0
  446. package/clients/cli/commands/swizzle.mjs +5 -3
  447. package/clients/cli/commands/swizzle.routing.test.mjs +19 -2
  448. package/clients/cli/commands/template.doc.mjs +1 -0
  449. package/clients/cli/commands/text-json-parity.test.mjs +5 -0
  450. package/clients/cli/commands/theme-add-lifecycle.test.mjs +3 -3
  451. package/clients/cli/commands/theme-add.doc.mjs +8 -1
  452. package/clients/cli/commands/theme-build.doc.mjs +3 -2
  453. package/clients/cli/commands/theme-eject.doc.mjs +1 -0
  454. package/clients/cli/commands/theme-list.behavior.test.mjs +6 -0
  455. package/clients/cli/commands/theme-list.doc.mjs +1 -0
  456. package/clients/cli/commands/theme-palette-generate.doc.mjs +1 -0
  457. package/clients/cli/commands/theme-remove.doc.mjs +7 -0
  458. package/clients/cli/commands/theme-targets.doc.mjs +1 -0
  459. package/clients/cli/commands/theme-template.doc.mjs +1 -0
  460. package/clients/cli/commands/theme-use.doc.mjs +7 -0
  461. package/clients/cli/commands/{build-theme.mjs → theme.mjs} +27 -14
  462. package/clients/cli/commands/upgrade.config-ordering.test.mjs +2 -1
  463. package/clients/cli/commands/upgrade.doc.mjs +26 -3
  464. package/clients/cli/commands/upgrade.file-protection.test.mjs +3 -2
  465. package/clients/cli/commands/upgrade.hook-output.test.mjs +2 -1
  466. package/clients/cli/commands/upgrade.mjs +9 -2
  467. package/clients/cli/commands/upgrade.older-cli.test.mjs +160 -0
  468. package/clients/cli/commands/upgrade.subfolder-path.test.mjs +97 -0
  469. package/clients/cli/index.mjs +156 -100
  470. package/clients/cli/lib/cli-error.mjs +4 -1
  471. package/clients/cli/lib/component-format.mjs +1 -1
  472. package/clients/cli/lib/component-format.test.mjs +1 -1
  473. package/clients/cli/lib/define-command.mjs +14 -9
  474. package/clients/cli/lib/exit-codes.test.mjs +5 -0
  475. package/clients/cli/lib/json-shim.mjs +3 -1
  476. package/clients/cli/lib/manifest.d.ts +9 -0
  477. package/clients/cli/lib/manifest.mjs +17 -1
  478. package/clients/cli/lib/manifest.test.mjs +37 -0
  479. package/clients/cli/lib/welcome-box.mjs +50 -0
  480. package/clients/cli/run.mjs +108 -0
  481. package/foundation/agent-docs/agent-doc-state.d.mts +67 -13
  482. package/foundation/agent-docs/agent-doc-state.mjs +123 -14
  483. package/foundation/agent-docs/agent-docs.d.mts +104 -15
  484. package/foundation/agent-docs/agent-docs.mjs +1028 -114
  485. package/foundation/agent-docs/agent-docs.path-safety.test.mjs +1239 -17
  486. package/foundation/agent-docs/agent-docs.test.mjs +683 -39
  487. package/foundation/agent-docs/computer-agent-text.d.mts +170 -0
  488. package/foundation/agent-docs/computer-agent-text.mjs +750 -0
  489. package/foundation/agent-docs/computer-agent-text.test.mjs +699 -0
  490. package/foundation/config/project.d.mts +35 -3
  491. package/foundation/config/project.mjs +243 -26
  492. package/foundation/config/project.test.mjs +122 -0
  493. package/foundation/config/theme-state.mjs +5 -4
  494. package/foundation/discovery/authoring-self-docs.d.mts +7 -0
  495. package/foundation/discovery/authoring-self-docs.mjs +17 -4
  496. package/foundation/discovery/authoring-self-docs.test.mjs +15 -3
  497. package/foundation/discovery/authoring-surface.d.mts +1 -1
  498. package/foundation/discovery/authoring-surface.mjs +2 -2
  499. package/foundation/discovery/authoring-surface.test.mjs +1 -1
  500. package/foundation/discovery/cli-self-docs.d.mts +2 -1
  501. package/foundation/discovery/cli-self-docs.mjs +22 -4
  502. package/foundation/discovery/cli-self-docs.test.mjs +3 -2
  503. package/foundation/discovery/component-discovery.d.mts +227 -10
  504. package/foundation/discovery/component-discovery.export-names.test.mjs +760 -0
  505. package/foundation/discovery/component-discovery.mjs +940 -57
  506. package/foundation/discovery/component-discovery.test.mjs +96 -0
  507. package/foundation/discovery/component-loader.d.mts +1 -1
  508. package/foundation/discovery/component-replacement.mjs +20 -1
  509. package/foundation/discovery/component-replacement.test.mjs +40 -0
  510. package/foundation/discovery/copied-passages.d.mts +216 -0
  511. package/foundation/discovery/copied-passages.mjs +720 -0
  512. package/foundation/discovery/copied-passages.test.mjs +124 -0
  513. package/foundation/discovery/doc-consistency.d.mts +34 -0
  514. package/foundation/discovery/doc-consistency.mjs +513 -0
  515. package/foundation/discovery/doc-consistency.test.mjs +347 -0
  516. package/foundation/discovery/doc-fluff.d.mts +112 -0
  517. package/foundation/discovery/doc-fluff.mjs +328 -0
  518. package/foundation/discovery/doc-fluff.test.mjs +142 -0
  519. package/foundation/discovery/doc-types.d.mts +157 -0
  520. package/foundation/discovery/doc-types.mjs +1231 -0
  521. package/foundation/discovery/doc-types.test.mjs +744 -0
  522. package/foundation/discovery/docs-discovery.d.mts +88 -0
  523. package/foundation/discovery/docs-discovery.mjs +97 -3
  524. package/foundation/discovery/integration-doc-files.d.mts +22 -0
  525. package/foundation/discovery/integration-doc-files.mjs +231 -0
  526. package/foundation/discovery/integration-doc-files.test.mjs +246 -0
  527. package/foundation/discovery/module-exports.d.mts +68 -0
  528. package/foundation/discovery/module-exports.mjs +609 -31
  529. package/foundation/discovery/module-exports.test.mjs +172 -0
  530. package/foundation/discovery/package-tsconfig.d.mts +14 -0
  531. package/foundation/discovery/package-tsconfig.mjs +228 -0
  532. package/foundation/discovery/package-tsconfig.test.mjs +210 -0
  533. package/foundation/discovery/project-typescript.d.mts +32 -0
  534. package/foundation/discovery/project-typescript.mjs +89 -0
  535. package/foundation/discovery/prop-drift.d.mts +297 -0
  536. package/foundation/discovery/prop-drift.mjs +2428 -0
  537. package/foundation/discovery/prop-drift.test.mjs +2337 -0
  538. package/foundation/discovery/runner-allowlist.d.mts +38 -0
  539. package/foundation/discovery/runner-allowlist.mjs +51 -0
  540. package/foundation/discovery/runner-commands.d.mts +77 -0
  541. package/foundation/discovery/runner-commands.mjs +294 -0
  542. package/foundation/discovery/runner-commands.test.mjs +128 -0
  543. package/foundation/discovery/stray-pointers.d.mts +52 -0
  544. package/foundation/discovery/stray-pointers.mjs +293 -0
  545. package/foundation/discovery/stray-pointers.test.mjs +86 -0
  546. package/foundation/discovery/template-adapter.mjs +92 -6
  547. package/foundation/discovery/template-needs.mjs +6 -46
  548. package/foundation/discovery/theme-discovery.mjs +147 -3
  549. package/foundation/discovery/theme-imports.mjs +8 -4
  550. package/foundation/discovery/theme-imports.test.mjs +1 -1
  551. package/foundation/discovery/type-match.d.mts +53 -0
  552. package/foundation/discovery/type-match.mjs +660 -0
  553. package/foundation/discovery/type-match.test.mjs +169 -0
  554. package/foundation/discovery/type-text.d.mts +68 -0
  555. package/foundation/discovery/type-text.mjs +658 -0
  556. package/foundation/discovery/type-text.test.mjs +196 -0
  557. package/foundation/discovery/unresolved-references.d.mts +62 -0
  558. package/foundation/discovery/unresolved-references.mjs +790 -0
  559. package/foundation/discovery/unresolved-references.test.mjs +567 -0
  560. package/foundation/doc-compiler/doc-loads.test.mjs +84 -10
  561. package/foundation/doc-compiler/inputs.mjs +27 -1
  562. package/foundation/doc-compiler/inputs.test.mjs +0 -1
  563. package/foundation/doc-compiler/links.d.mts +41 -5
  564. package/foundation/doc-compiler/links.mjs +167 -28
  565. package/foundation/doc-compiler/links.test.mjs +97 -4
  566. package/foundation/doc-compiler/read.d.mts +1 -1
  567. package/foundation/doc-compiler/read.mjs +59 -1
  568. package/foundation/doc-compiler/read.test.mjs +11 -2
  569. package/foundation/doc-compiler/split-topic-keywords.test.mjs +3 -2
  570. package/foundation/doc-compiler/tree.d.mts +32 -6
  571. package/foundation/doc-compiler/tree.mjs +107 -44
  572. package/foundation/doc-compiler/tree.test.mjs +14 -7
  573. package/foundation/env/cli-command.d.mts +83 -0
  574. package/foundation/env/cli-command.mjs +152 -0
  575. package/foundation/env/cli-command.test.mjs +129 -0
  576. package/foundation/env/node-version.test.mjs +15 -3
  577. package/foundation/env/package-manager.d.mts +93 -9
  578. package/foundation/env/package-manager.global.test.mjs +66 -0
  579. package/foundation/env/package-manager.mjs +184 -13
  580. package/foundation/env/package-manager.test.mjs +341 -15
  581. package/foundation/env/project-scope.d.mts +9 -0
  582. package/foundation/env/project-scope.mjs +33 -0
  583. package/foundation/env/search-path.d.mts +42 -0
  584. package/foundation/env/search-path.mjs +205 -0
  585. package/foundation/env/search-path.test.mjs +122 -0
  586. package/foundation/env/user-dirs.d.mts +39 -0
  587. package/foundation/env/user-dirs.mjs +93 -0
  588. package/foundation/env/user-dirs.test.mjs +72 -0
  589. package/foundation/env/user-settings.d.mts +46 -0
  590. package/foundation/env/user-settings.mjs +106 -0
  591. package/foundation/env/user-settings.test.mjs +64 -0
  592. package/foundation/fs/file-protection.mjs +15 -1
  593. package/foundation/fs/module-loader.d.mts +2 -1
  594. package/foundation/fs/module-loader.mjs +60 -4
  595. package/foundation/fs/module-loader.stdout.test.mjs +3 -2
  596. package/foundation/fs/module-loader.test.mjs +194 -0
  597. package/foundation/fs/path-compare.d.mts +33 -0
  598. package/foundation/fs/path-compare.mjs +73 -0
  599. package/foundation/fs/paths.d.mts +122 -4
  600. package/foundation/fs/paths.mjs +675 -29
  601. package/foundation/fs/paths.test.mjs +439 -54
  602. package/foundation/fs/read-record.d.mts +18 -0
  603. package/foundation/fs/read-record.mjs +131 -0
  604. package/foundation/fs/read-record.test.mjs +77 -0
  605. package/foundation/integrations/autolink.d.mts +2 -1
  606. package/foundation/integrations/autolink.mjs +4 -13
  607. package/foundation/integrations/cli-peer-version.d.mts +36 -0
  608. package/foundation/integrations/cli-peer-version.mjs +132 -0
  609. package/foundation/integrations/cli-peer-version.test.mjs +354 -0
  610. package/foundation/integrations/computer-integrations.d.mts +81 -0
  611. package/foundation/integrations/computer-integrations.mjs +345 -0
  612. package/foundation/integrations/computer-integrations.test.mjs +292 -0
  613. package/foundation/integrations/contribution-inventory.d.mts +26 -20
  614. package/foundation/integrations/contribution-inventory.mjs +97 -8
  615. package/foundation/integrations/exports-subpaths.d.mts +57 -0
  616. package/foundation/integrations/exports-subpaths.mjs +113 -0
  617. package/foundation/integrations/integration-warnings.mjs +3 -2
  618. package/foundation/integrations/integration-warnings.test.mjs +8 -9
  619. package/foundation/integrations/integrations.d.mts +51 -1
  620. package/foundation/integrations/integrations.mjs +106 -4
  621. package/foundation/integrations/issue.ts +6 -0
  622. package/foundation/integrations/public-imports.d.mts +118 -0
  623. package/foundation/integrations/public-imports.mjs +1769 -0
  624. package/foundation/integrations/public-imports.test.mjs +1793 -0
  625. package/foundation/integrations/validate-contributions.d.mts +117 -0
  626. package/foundation/integrations/validate-contributions.mjs +1209 -14
  627. package/foundation/release/agent-text-worker.d.mts +4 -0
  628. package/foundation/release/agent-text-worker.mjs +52 -0
  629. package/foundation/release/cache.d.mts +310 -0
  630. package/foundation/release/cache.mjs +1216 -0
  631. package/foundation/release/cache.test.mjs +923 -0
  632. package/foundation/release/choose.d.mts +207 -0
  633. package/foundation/release/choose.mjs +677 -0
  634. package/foundation/release/choose.test.mjs +427 -0
  635. package/foundation/release/core-worker.d.mts +4 -0
  636. package/foundation/release/core-worker.mjs +38 -0
  637. package/foundation/release/global-state.d.mts +263 -0
  638. package/foundation/release/global-state.mjs +474 -0
  639. package/foundation/release/global-state.test.mjs +381 -0
  640. package/foundation/release/handoff.d.mts +79 -0
  641. package/foundation/release/handoff.e2e.test.mjs +278 -0
  642. package/foundation/release/handoff.mjs +524 -0
  643. package/foundation/release/lock-lease.d.mts +23 -0
  644. package/foundation/release/lock-lease.mjs +80 -0
  645. package/foundation/release/notices.d.mts +307 -0
  646. package/foundation/release/notices.mjs +707 -0
  647. package/foundation/release/notices.test.mjs +586 -0
  648. package/foundation/release/npm-guard.d.mts +4 -0
  649. package/foundation/release/npm-guard.mjs +126 -0
  650. package/foundation/release/project.d.mts +141 -0
  651. package/foundation/release/project.mjs +466 -0
  652. package/foundation/release/project.test.mjs +288 -0
  653. package/foundation/release/refresh-worker.d.mts +4 -0
  654. package/foundation/release/refresh-worker.mjs +27 -0
  655. package/foundation/release/run-context.d.mts +47 -0
  656. package/foundation/release/run-context.mjs +163 -0
  657. package/foundation/release/update-notice-env.d.mts +28 -0
  658. package/foundation/release/update-notice-env.mjs +39 -0
  659. package/foundation/response/base.d.ts +26 -3
  660. package/foundation/response/error-codes.d.mts +3 -1
  661. package/foundation/response/error-codes.doc.mjs +12 -0
  662. package/foundation/response/error-codes.mjs +17 -0
  663. package/foundation/response/error-codes.test.mjs +2 -0
  664. package/foundation/response/json-contract.test.mjs +7 -2
  665. package/foundation/response/json.mjs +4 -1
  666. package/foundation/response/response-types.doc.mjs +22 -11
  667. package/foundation/response/response-types.doc.test.mjs +46 -0
  668. package/foundation/response/response.doc.mjs +3 -3
  669. package/foundation/route/route.d.mts +285 -0
  670. package/foundation/route/route.mjs +324 -0
  671. package/foundation/route/route.test.mjs +153 -0
  672. package/foundation/runtime/hosted-runtime.test.mjs +403 -0
  673. package/foundation/runtime/locked-versions.d.mts +32 -0
  674. package/foundation/runtime/locked-versions.mjs +65 -0
  675. package/foundation/runtime/package-root.d.mts +91 -0
  676. package/foundation/runtime/package-root.mjs +174 -0
  677. package/foundation/runtime/package-set.d.mts +62 -0
  678. package/foundation/runtime/package-set.mjs +130 -0
  679. package/foundation/runtime/package-set.test.mjs +432 -0
  680. package/foundation/text/string-utils.d.mts +2 -1
  681. package/foundation/text/string-utils.mjs +16 -5
  682. package/foundation/text/suggest-order.d.mts +72 -0
  683. package/foundation/text/suggest-order.mjs +363 -0
  684. package/foundation/text/suggest-order.test.mjs +259 -0
  685. package/foundation/xle/browser-imports.test.mjs +74 -0
  686. package/foundation/xle/registry.mjs +4 -0
  687. package/foundation/xle/registry.test.mjs +28 -0
  688. package/host/door-conformance.test.mjs +274 -0
  689. package/host/index.d.ts +116 -0
  690. package/host/index.mjs +295 -0
  691. package/host/index.test.mjs +302 -0
  692. package/package.json +16 -11
  693. package/scripts/postinstall.mjs +24 -5
  694. package/assets/docs/tree/integrations.doc.mjs +0 -40
@@ -3,26 +3,39 @@
3
3
  /**
4
4
  * @file agent-docs — Install/update agent docs for AI coding tools
5
5
  *
6
- * Generates a CLI cheat sheet from actual command metadata and injects
7
- * it into agent doc files. Supports multiple tools with auto-detection:
6
+ * Generates a CLI cheat sheet from actual command metadata and writes it, as
7
+ * one managed block, into the files AI coding tools read.
8
8
  *
9
- * - Claude Code: CLAUDE.md (root) or .claude/CLAUDE.md
10
- * - Cursor: .cursorrules
11
- * - Codex/generic: AGENTS.md
12
- * - Muse: AGENTS.md
13
- * - Hermes Agent: .hermes.md or HERMES.md (existing), else AGENTS.md
9
+ * Default (no --agent or --agent-docs-path): every agent reads the block, in a
10
+ * file the project commits.
14
11
  *
15
- * Auto-detect: discovers existing files and updates them in place.
16
- * Default (no existing files): creates AGENTS.md (the tool-agnostic standard).
12
+ * - AGENTS.md holds the block. Most agents read it; Claude Code reads it when
13
+ * the folder has no CLAUDE.md or .claude/CLAUDE.md.
14
+ * - The Claude Code file the project already has holds a copy: root CLAUDE.md,
15
+ * else .claude/CLAUDE.md, even when it imports AGENTS.md, because Claude Code
16
+ * does not follow that import from a subfolder. Init creates no Claude file.
17
+ * - Gemini CLI reads only GEMINI.md: it gets a managed import, `@./AGENTS.md`,
18
+ * created when missing, unless GEMINI.md already imports AGENTS.md. Project
19
+ * settings that point Gemini at AGENTS.md don't replace it: Gemini loads them
20
+ * only when it starts at the project root.
21
+ * - Existing Cursor (.cursorrules) and Hermes (.hermes.md, HERMES.md) files
22
+ * keep a full block.
23
+ *
24
+ * No mode writes into the home folder, or into a settings folder in it outside a
25
+ * project: agents read the files there in every project. A project inside a
26
+ * settings folder, such as an agent worktree, is written normally.
17
27
  *
18
28
  * --agent <tool>: target a specific tool preset (claude, cursor, codex, hermes, muse, all)
19
29
  * --agent-docs-path <path>: explicit file path(s)
20
30
  */
21
31
 
32
+ import {spawnSync} from 'node:child_process';
22
33
  import * as fs from 'node:fs';
34
+ import * as os from 'node:os';
23
35
  import * as path from 'node:path';
24
36
  import {findCoreDir, CLI_ROOT} from '../fs/paths.mjs';
25
37
  import {assertWithin, PathSafetyError} from '../fs/path-safety.mjs';
38
+ import {createFileProtectionResolver} from '../fs/file-protection.mjs';
26
39
  import {getCliInvocation} from '../env/package-manager.mjs';
27
40
  import {discoverComponents} from '../discovery/component-discovery.mjs';
28
41
  import {Project} from '../config/project.mjs';
@@ -31,9 +44,11 @@ import {humanLog} from '../response/json.mjs';
31
44
  import {ERROR_CODES} from '../response/error-codes.mjs';
32
45
  import {
33
46
  AGENTS_MD,
47
+ AGENT_DOC_PATHS,
34
48
  CLAUDE_MD,
35
49
  CLAUDE_DIR_MD,
36
50
  CURSOR_RULES,
51
+ GEMINI_MD,
37
52
  HERMES_DOT_MD,
38
53
  HERMES_MD,
39
54
  MARKER_START,
@@ -42,15 +57,20 @@ import {
42
57
  LEGACY_MARKER_END,
43
58
  discoverAgentDocs,
44
59
  isAstryxInitialized,
60
+ isAstryxPromptInReach,
61
+ SELF_CHECK_CSS,
62
+ WORKFLOW_STEPS,
63
+ WORKFLOW_TITLE,
45
64
  } from './agent-doc-state.mjs';
46
65
 
47
66
  // The agent-doc locations, markers, and the setup-state predicates
48
- // (discoverAgentDocs, isAstryxInitialized) are the ONE canonical contract. They
49
- // live in the dependency-free leaf ./agent-doc-state.mjs so the postinstall
50
- // nudge (enforcement layer 2) can load them safely at install time. Re-exported
51
- // here so existing importers (init/upgrade commands, the layer-3 nudge in
52
- // clients/cli/index.mjs, tests) keep their `from './agent-docs.mjs'` paths.
53
- export {discoverAgentDocs, isAstryxInitialized};
67
+ // (discoverAgentDocs, isAstryxInitialized, isAstryxPromptInReach) are the ONE
68
+ // canonical contract. They live in the dependency-free leaf
69
+ // ./agent-doc-state.mjs so the postinstall nudge (enforcement layer 2) can load
70
+ // them safely at install time. Re-exported here so existing importers
71
+ // (init/upgrade commands, the layer-3 nudge in clients/cli/index.mjs, tests)
72
+ // keep their `from './agent-docs.mjs'` paths.
73
+ export {discoverAgentDocs, isAstryxInitialized, isAstryxPromptInReach};
54
74
 
55
75
  const MAX_PROJECT_AGENT_DOC_LINES = 32;
56
76
  const MANAGED_MARKER_TEXT = /(?:ASTRYX|XDS):(START|END)/u;
@@ -78,27 +98,26 @@ function validateIntegrationLabel(value) {
78
98
  }
79
99
 
80
100
  /**
81
- * Find tool-specific files that import another detected agent doc.
82
- *
83
- * Claude's `@path` directive is a real include, so injecting the full managed
84
- * block beside it duplicates the same instructions. Only an import whose
85
- * normalized target is another known agent doc counts; prose mentions and
86
- * standalone files keep the normal initialization behavior. Import cycles have
87
- * no canonical owner, so their members also stay standalone.
101
+ * The `@path` imports of other agent docs in each agent doc. Only an import
102
+ * whose normalized target is another known agent doc counts; prose mentions do
103
+ * not. Lines inside the managed block are Astryx's own (the default writes an
104
+ * import of AGENTS.md there), so they never count as the project's import.
88
105
  *
89
106
  * @param {string} targetDir
90
107
  * @param {string[]} agentDocs
91
- * @returns {Set<string>}
108
+ * @param {string[]} [extraTargets] Import targets that count even though they
109
+ * do not exist yet (the default always writes AGENTS.md).
110
+ * @returns {Map<string, Set<string>>} each importing doc and the docs it imports.
92
111
  */
93
- function discoverAgentDocWrappers(targetDir, agentDocs) {
94
- const known = new Set(agentDocs.map(p => path.normalize(p)));
112
+ function readAgentDocImports(targetDir, agentDocs, extraTargets = []) {
113
+ const known = new Set([...agentDocs, ...extraTargets].map(p => path.normalize(p)));
95
114
  /** @type {Map<string, Set<string>>} */
96
115
  const imports = new Map();
97
116
 
98
117
  for (const rel of agentDocs) {
99
118
  let content;
100
119
  try {
101
- content = fs.readFileSync(path.join(targetDir, rel), 'utf-8');
120
+ content = withoutManagedBlock(fs.readFileSync(path.join(targetDir, rel), 'utf-8'));
102
121
  } catch {
103
122
  continue;
104
123
  }
@@ -114,7 +133,21 @@ function discoverAgentDocWrappers(targetDir, agentDocs) {
114
133
  }
115
134
  if (targets.size > 0) imports.set(rel, targets);
116
135
  }
136
+ return imports;
137
+ }
117
138
 
139
+ /**
140
+ * Find tool-specific files that import another detected agent doc.
141
+ *
142
+ * Claude's `@path` directive is a real include, so injecting the full managed
143
+ * block beside it duplicates the same instructions. Standalone files keep the
144
+ * normal initialization behavior. Import cycles have no canonical owner, so
145
+ * their members also stay standalone.
146
+ *
147
+ * @param {Map<string, Set<string>>} imports From {@link readAgentDocImports}.
148
+ * @returns {Map<string, Set<string>>} each wrapper and the agent docs it imports.
149
+ */
150
+ function findAgentDocWrappers(imports) {
118
151
  /** @param {string} start */
119
152
  const isCyclic = start => {
120
153
  /**
@@ -133,7 +166,7 @@ function discoverAgentDocWrappers(targetDir, agentDocs) {
133
166
  return visit(start, new Set([start]));
134
167
  };
135
168
 
136
- return new Set([...imports.keys()].filter(rel => !isCyclic(rel)));
169
+ return new Map([...imports].filter(([rel]) => !isCyclic(rel)));
137
170
  }
138
171
 
139
172
  /**
@@ -175,6 +208,295 @@ function findManagedBlock(content) {
175
208
  return null;
176
209
  }
177
210
 
211
+ /**
212
+ * `content` with its managed block cut out. A malformed block is left in
213
+ * place; the writers refuse it later.
214
+ * @param {string} content
215
+ * @returns {string}
216
+ */
217
+ function withoutManagedBlock(content) {
218
+ try {
219
+ const block = findManagedBlock(content);
220
+ return block ? content.slice(0, block.start) + content.slice(block.end) : content;
221
+ } catch {
222
+ return content;
223
+ }
224
+ }
225
+
226
+ /**
227
+ * The managed one-line import of AGENTS.md that the default writes, keyed by
228
+ * file. Gemini CLI reads only GEMINI.md, and expands `@./AGENTS.md` there.
229
+ *
230
+ * @type {Readonly<Record<string, string>>}
231
+ */
232
+ const AGENTS_IMPORTS = Object.freeze({
233
+ [GEMINI_MD]: '@./AGENTS.md',
234
+ });
235
+
236
+ /**
237
+ * Agent docs 0.7.0 already wrote. A path-safety refusal on one of them stays an
238
+ * error, as it was; a file only the current layout writes is skipped instead.
239
+ */
240
+ const LEGACY_DOC_PATHS = AGENT_DOC_PATHS.filter(p => p !== GEMINI_MD);
241
+
242
+ /**
243
+ * The managed block holding the import of AGENTS.md for `rel`.
244
+ * @param {string} rel A key of {@link AGENTS_IMPORTS}.
245
+ * @returns {string}
246
+ */
247
+ function renderAgentsImportBlock(rel) {
248
+ return `${MARKER_START}\n${AGENTS_IMPORTS[rel]}\n${MARKER_END}`;
249
+ }
250
+
251
+ /**
252
+ * Whether the managed block in `content` holds only an import of the root
253
+ * AGENTS.md, resolved from the folder of `rel`. Such a block names no version,
254
+ * so it stays current while it points at AGENTS.md.
255
+ * @param {string} rel Project-relative path of the file `content` came from.
256
+ * @param {string} content
257
+ * @returns {boolean}
258
+ */
259
+ function isAgentsImportBlock(rel, content) {
260
+ let block;
261
+ try {
262
+ block = findManagedBlock(content);
263
+ } catch {
264
+ return false;
265
+ }
266
+ if (block == null) return false;
267
+ const text = content.slice(block.start, block.end);
268
+ const inner = text.slice(text.indexOf('-->') + 3, text.lastIndexOf('<!--')).trim();
269
+ const match = /^@(\S+)$/.exec(inner);
270
+ if (match == null || path.isAbsolute(match[1])) return false;
271
+ return path.normalize(path.join(path.dirname(rel), match[1])) === AGENTS_MD;
272
+ }
273
+
274
+ /**
275
+ * The paths Gemini CLI imports from `content`, in order. Gemini reads `@path`
276
+ * where the `@` starts the text or follows whitespace, and the path runs to the
277
+ * next whitespace and starts with `.`, `/` or a letter. An `@` inside backticks
278
+ * (inline code or a fenced block) is not an import.
279
+ * @param {string} content
280
+ * @returns {string[]}
281
+ */
282
+ function findGeminiImports(content) {
283
+ /** @type {Array<[number, number]>} */
284
+ const code = [];
285
+ for (const match of content.matchAll(/(`+)[\s\S]*?\1/g)) {
286
+ code.push([match.index, match.index + match[0].length]);
287
+ }
288
+ /** @type {string[]} */
289
+ const found = [];
290
+ for (const match of content.matchAll(/(?<=^|[ \t\r\n])@([^ \t\r\n]+)/g)) {
291
+ if (!/^[./A-Za-z]/.test(match[1])) continue;
292
+ if (code.some(([start, end]) => match.index >= start && match.index < end)) {
293
+ continue;
294
+ }
295
+ found.push(match[1]);
296
+ }
297
+ return found;
298
+ }
299
+
300
+ /**
301
+ * Whether GEMINI.md, outside Astryx's managed block, already has Gemini CLI
302
+ * import AGENTS.md, on a line of its own or inline in a sentence.
303
+ * @param {string} targetDir
304
+ * @returns {boolean}
305
+ */
306
+ function geminiImportsAgentsMd(targetDir) {
307
+ let content;
308
+ try {
309
+ content = fs.readFileSync(path.join(targetDir, GEMINI_MD), 'utf-8');
310
+ } catch {
311
+ return false;
312
+ }
313
+ const agents = path.resolve(targetDir, AGENTS_MD);
314
+ return findGeminiImports(withoutManagedBlock(content)).some(
315
+ rel => path.resolve(targetDir, rel) === agents,
316
+ );
317
+ }
318
+
319
+ /**
320
+ * Thrown when agent docs would be written into the home folder, or into a
321
+ * settings folder in it such as `~/.gemini` outside a project. Agents read the
322
+ * files there (for example `~/.claude/CLAUDE.md` or `~/.gemini/GEMINI.md`) in
323
+ * every project, so a project's version-pinned block never goes there. Removal
324
+ * stays allowed.
325
+ */
326
+ export class AgentDocsHomeError extends Error {
327
+ /** @param {string} dir */
328
+ constructor(dir) {
329
+ super(
330
+ `Agent docs are not written into the home folder, or into a settings folder in it outside a project (${dir}): agents read the files there in every project. Run init inside a project folder.`,
331
+ );
332
+ this.name = 'AgentDocsHomeError';
333
+ /** The refused folder. */
334
+ this.dir = dir;
335
+ }
336
+ }
337
+
338
+ /**
339
+ * `p` resolved, through symlinks when it exists.
340
+ * @param {string} p
341
+ * @returns {string}
342
+ */
343
+ function realDir(p) {
344
+ try {
345
+ return fs.realpathSync(path.resolve(p));
346
+ } catch {
347
+ return path.resolve(p);
348
+ }
349
+ }
350
+
351
+ /**
352
+ * `dir` relative to the home folder, compared without symlinks (and without
353
+ * case on Windows). null when there is no home folder.
354
+ * @param {string} dir
355
+ * @returns {string | null}
356
+ */
357
+ function relativeToHome(dir) {
358
+ let home;
359
+ try {
360
+ home = os.homedir();
361
+ } catch {
362
+ return null;
363
+ }
364
+ if (!home) return null;
365
+ /** @param {string} p */
366
+ const fold = p => (process.platform === 'win32' ? p.toLowerCase() : p);
367
+ return path.relative(fold(realDir(home)), fold(realDir(dir)));
368
+ }
369
+
370
+ /**
371
+ * Whether `dir` is the home folder.
372
+ * @param {string} dir
373
+ * @returns {boolean}
374
+ */
375
+ function isHomeFolder(dir) {
376
+ return relativeToHome(dir) === '';
377
+ }
378
+
379
+ /**
380
+ * Whether agent docs stay out of `dir`: the home folder, or a folder in a
381
+ * settings folder of it (a folder of the home folder whose name starts with a
382
+ * dot, such as `~/.gemini` or `~/.claude`) that belongs to no project there.
383
+ * Agents read the files at the top of a settings folder in every project, so
384
+ * the settings folder itself is never a project, even with a package.json. A
385
+ * folder below it with a package.json at or above it is a project, such as an
386
+ * agent worktree in `~/.codex/worktrees/<id>/<repo>`.
387
+ * @param {string} dir
388
+ * @returns {boolean}
389
+ */
390
+ function isHomeOrSettingsFolder(dir) {
391
+ const rel = relativeToHome(dir);
392
+ if (rel == null) return false;
393
+ if (rel === '') return true;
394
+ if (rel === '..' || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) {
395
+ return false;
396
+ }
397
+ const parts = rel.split(path.sep);
398
+ if (!parts[0].startsWith('.')) return false;
399
+ // Look for a package.json from `dir` up to, not including, the settings folder.
400
+ let current = realDir(dir);
401
+ for (let depth = parts.length; depth > 1; depth--) {
402
+ if (fs.existsSync(path.join(current, 'package.json'))) return false;
403
+ current = path.dirname(current);
404
+ }
405
+ return true;
406
+ }
407
+
408
+ /**
409
+ * Throw {@link AgentDocsHomeError} when `targetDir` is the home folder, or a
410
+ * settings folder in it outside a project.
411
+ * @param {string} targetDir
412
+ */
413
+ export function assertAgentDocsTarget(targetDir) {
414
+ if (isHomeOrSettingsFolder(targetDir)) throw new AgentDocsHomeError(realDir(targetDir));
415
+ }
416
+
417
+ /** Folders that mark a repository root; the project search stops there. */
418
+ const REPOSITORY_MARKERS = ['.git', '.hg', '.sl'];
419
+
420
+ /** Dependency fields where a project declares the Astryx packages it uses. */
421
+ const DEPENDENCY_FIELDS = [
422
+ 'dependencies',
423
+ 'devDependencies',
424
+ 'peerDependencies',
425
+ 'optionalDependencies',
426
+ ];
427
+
428
+ /**
429
+ * The package.json in `dir`: its parsed content, `null` when it exists but
430
+ * does not parse, or `undefined` when there is none.
431
+ * @param {string} dir
432
+ * @returns {Record<string, unknown> | null | undefined}
433
+ */
434
+ function readPackageJson(dir) {
435
+ const file = path.join(dir, 'package.json');
436
+ if (!fs.existsSync(file)) return undefined;
437
+ try {
438
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf-8'));
439
+ return parsed && typeof parsed === 'object' ? parsed : null;
440
+ } catch {
441
+ return null;
442
+ }
443
+ }
444
+
445
+ /**
446
+ * Whether a parsed package.json declares an `@astryxdesign` package.
447
+ * @param {Record<string, unknown> | null} manifest
448
+ * @returns {boolean}
449
+ */
450
+ function declaresAstryx(manifest) {
451
+ return DEPENDENCY_FIELDS.some(field => {
452
+ const deps = manifest?.[field];
453
+ return (
454
+ deps != null &&
455
+ typeof deps === 'object' &&
456
+ Object.keys(deps).some(name => name.startsWith('@astryxdesign/'))
457
+ );
458
+ });
459
+ }
460
+
461
+ /**
462
+ * The folder a run started in `cwd` writes agent docs to: the nearest folder at
463
+ * or above it whose package.json declares an `@astryxdesign` package, else the
464
+ * nearest folder with any package.json. The search stops at a repository root
465
+ * and never reaches the home folder, so a stray `~/package.json` cannot pull
466
+ * the docs there.
467
+ * @param {string} cwd
468
+ * @returns {string | null} null when no folder it searches has a package.json;
469
+ * init then writes where it runs, as before.
470
+ */
471
+ export function resolveAgentDocsRoot(cwd) {
472
+ let dir = path.resolve(cwd);
473
+ /** @type {string | null} */
474
+ let nearest = null;
475
+ for (let i = 0; i < 50 && !isHomeFolder(dir); i++) {
476
+ const manifest = readPackageJson(dir);
477
+ if (manifest !== undefined) {
478
+ if (declaresAstryx(manifest)) return dir;
479
+ nearest ??= dir;
480
+ }
481
+ if (REPOSITORY_MARKERS.some(marker => fs.existsSync(path.join(dir, marker)))) break;
482
+ const parent = path.dirname(dir);
483
+ if (parent === dir) break;
484
+ dir = parent;
485
+ }
486
+ return nearest;
487
+ }
488
+
489
+ /**
490
+ * The folder `init` run in `cwd` writes the agent docs to: the project root
491
+ * {@link resolveAgentDocsRoot} finds, else `cwd` itself. The one definition
492
+ * that init, its removal, and the per-command setup nudge all use.
493
+ * @param {string} cwd
494
+ * @returns {string}
495
+ */
496
+ export function agentDocsTargetDir(cwd) {
497
+ return resolveAgentDocsRoot(cwd) ?? cwd;
498
+ }
499
+
178
500
  /**
179
501
  * Agent tool presets — maps tool names to their file search paths.
180
502
  * Order matters: first existing file wins, last entry is the default (created if none exist).
@@ -213,6 +535,8 @@ export function parseBlockVersion(content) {
213
535
  * - `current` — every managed block already matches the installed version.
214
536
  * And the project is `missing` when no managed block exists anywhere (the repo
215
537
  * has agent-doc files without our markers, or none at all — i.e. never `init`ed).
538
+ * A managed import of AGENTS.md is not a block here: it names no version and
539
+ * stays current while AGENTS.md holds the block.
216
540
  *
217
541
  * @param {string} targetDir
218
542
  * @param {string} [installedVersion] Defaults to the installed core version.
@@ -242,6 +566,8 @@ export function inspectAgentDocs(targetDir, installedVersion, expectedBlock) {
242
566
  const hasNew = content.includes(MARKER_START);
243
567
  const hasLegacy = content.includes(LEGACY_MARKER_START);
244
568
  if (!hasNew && !hasLegacy) continue; // Not a block we manage.
569
+ // An import of AGENTS.md names no version and never needs a refresh.
570
+ if (isAgentsImportBlock(rel, content)) continue;
245
571
 
246
572
  const legacy = !hasNew && hasLegacy;
247
573
  const blockVersion = parseBlockVersion(content);
@@ -289,8 +615,11 @@ export function inspectAgentDocs(targetDir, installedVersion, expectedBlock) {
289
615
  */
290
616
  export function resolveAgentPaths(targetDir, agent) {
291
617
  if (agent === 'all') {
292
- // Inject into all existing files, create defaults for each tool
293
- const existing = discoverAgentDocs(targetDir);
618
+ // Every existing agent doc gets the block, except GEMINI.md, which keeps
619
+ // the one-line import of AGENTS.md the default writes. When CLAUDE.md is
620
+ // among them, installAgentDocs then removes the block from
621
+ // .claude/CLAUDE.md, unless the project ignores CLAUDE.md (INV31).
622
+ const existing = discoverAgentDocs(targetDir).filter(p => p !== GEMINI_MD);
294
623
  if (existing.length > 0) {
295
624
  return {inject: existing, create: []};
296
625
  }
@@ -377,7 +706,7 @@ export function detectStylingSystem(targetDir) {
377
706
  * among them. An agent cannot ask for a topic it was never told about, and
378
707
  * `getting-started` is the one it should reach for first.
379
708
  *
380
- * @param {string} version
709
+ * @param {string | null} version the Core version
381
710
  * @param {{coreDir?: string|null, invocation?: string, stylingSystem?: 'stylex'|'tailwind'|'css', zh?: boolean, lang?: string, topics?: string[], agentDocs?: Array<{package: string, append: readonly string[]}>}} [options]
382
711
  * @returns {string}
383
712
  */
@@ -418,9 +747,9 @@ export function generateCompressedIndex(
418
747
  // Best-effort: component count is cosmetic; fall back to the default.
419
748
  }
420
749
  }
421
-
422
- // Header — state the CLI prefix once; commands below are shown as `astryx <cmd>`.
423
750
  lines.push(`Astryx v${version} · ${componentCount} components`);
751
+
752
+ // State the CLI prefix once; commands below are shown as `astryx <cmd>`.
424
753
  lines.push(
425
754
  `CLI: run every command as \`${run} <cmd>\` (shown below as \`astryx ...\`).`,
426
755
  );
@@ -436,10 +765,7 @@ export function generateCompressedIndex(
436
765
  // Workflow — `build` is the front door, and every page starts from the
437
766
  // template it names. build/template/component are covered here, so they are
438
767
  // intentionally omitted from the command reference below.
439
- lines.push('WORKFLOW — start every page from a template, never from scratch:');
440
- lines.push('1. `astryx build "<idea>"` — names the template to scaffold from and the parts it lacks.');
441
- lines.push('2. `astryx template <name> <path>` — scaffold it; keep its frame, gap and padding; replace the content.');
442
- lines.push('3. `astryx template <Block>` for parts it lacks; `astryx component <Name>` to read props before using one.');
768
+ lines.push(WORKFLOW_TITLE, ...WORKFLOW_STEPS);
443
769
  lines.push('Changing an existing page? Keep it: skip step 2 and add blocks/components inside its sections.');
444
770
  lines.push('');
445
771
 
@@ -467,7 +793,7 @@ export function generateCompressedIndex(
467
793
  'replace any className=, style={{…}}, raw <div>/<span>, imported .css/@apply, or hardcoded #hex/px with the component or the xstyle prop + a token',
468
794
  tailwind:
469
795
  'replace any style={{…}}, raw <div>/<span>, imported .css/@apply, or hardcoded/arbitrary value (bg-[#fff], p-[13px]) with the component or a token-backed utility',
470
- css: 'replace any raw <div>/<span>, imported .css/@apply, or hardcoded #hex/px with the component or a token',
796
+ css: SELF_CHECK_CSS,
471
797
  };
472
798
  lines.push(
473
799
  `- SELF-CHECK: re-read the file; ${selfCheckFix[stylingSystem] ?? selfCheckFix.css}.`,
@@ -498,7 +824,8 @@ export function generateCompressedIndex(
498
824
  const suffix = resolvedTopics.length > shown.length ? ' …' : '';
499
825
  lines.push(` docs <topic> ${shown.join(', ')}${suffix} (\`astryx docs\` lists all)`);
500
826
  }
501
- lines.push(' docs cli commands, API reference, integration authoring');
827
+ lines.push(' docs cli commands and API reference');
828
+ lines.push(' docs authoring build an integration package, and every field it holds');
502
829
  lines.push(' upgrade --from <old version> --apply run after a dependency bump');
503
830
  const appendCount = agentDocs.reduce(
504
831
  (count, contribution) => count + contribution.append.length,
@@ -698,14 +1025,16 @@ export function injectClaudeMd(targetDir, version) {
698
1025
  }
699
1026
 
700
1027
  /**
701
- * Remove Astryx section from a file.
702
- * If the file becomes empty (only boilerplate header remains), deletes it.
1028
+ * Remove the Astryx-managed block, between its markers, from a file, leaving
1029
+ * every other line as it is. With `deleteIfEmpty`, the file is deleted when
1030
+ * nothing is left but whitespace or the exact heading init writes into a file
1031
+ * it creates; with `deleteIfBlank`, only when nothing but whitespace is left.
703
1032
  *
704
1033
  * @param {string} filePath
705
- * @param {{deleteIfEmpty?: boolean}} [options]
1034
+ * @param {{deleteIfEmpty?: boolean, deleteIfBlank?: boolean}} [options]
706
1035
  * @returns {boolean} Whether the Astryx section was found and removed
707
1036
  */
708
- export function removeXdsBlock(filePath, {deleteIfEmpty = false} = {}) {
1037
+ export function removeXdsBlock(filePath, {deleteIfEmpty = false, deleteIfBlank = false} = {}) {
709
1038
  if (!fs.existsSync(filePath)) return false;
710
1039
 
711
1040
  let content = fs.readFileSync(filePath, 'utf-8');
@@ -717,18 +1046,198 @@ export function removeXdsBlock(filePath, {deleteIfEmpty = false} = {}) {
717
1046
  const after = content.slice(block.end).trimStart();
718
1047
  content = before + (after ? '\n\n' + after : '') + '\n';
719
1048
 
720
- if (deleteIfEmpty) {
721
- const stripped = content.replace(/^#.*\n+.*guidance.*\n*/m, '').trim();
722
- if (!stripped) {
723
- fs.unlinkSync(filePath);
724
- return true;
725
- }
1049
+ const rest = content.trim();
1050
+ if (
1051
+ (deleteIfBlank && rest === '') ||
1052
+ (deleteIfEmpty && (rest === '' || CREATED_HEADING.test(rest)))
1053
+ ) {
1054
+ fs.unlinkSync(filePath);
1055
+ return true;
726
1056
  }
727
1057
 
728
1058
  fs.writeFileSync(filePath, content);
729
1059
  return true;
730
1060
  }
731
1061
 
1062
+ /**
1063
+ * The whole of a file init created, once its block is gone: the heading init
1064
+ * writes, `# <name>` and its one line, and nothing else.
1065
+ */
1066
+ const CREATED_HEADING =
1067
+ /^# [^\n]+\n\nProject-specific guidance for AI coding agents\.$/u;
1068
+
1069
+ /**
1070
+ * Whether the project's ignore rules exclude root CLAUDE.md, read with the
1071
+ * working-tree ignore reader upgrade uses, from the repository root down to
1072
+ * the project root. It decides only where the copy
1073
+ * goes (a fresh clone lacks an ignored file); the managed block is written
1074
+ * whatever the ignore rules say (INV31). An ignore file that cannot be read
1075
+ * counts as excluding root CLAUDE.md, so the copy in .claude/CLAUDE.md stays.
1076
+ * @param {string} targetDir
1077
+ * @returns {boolean}
1078
+ */
1079
+ function rootClaudeIgnored(targetDir) {
1080
+ return uncommittedHere(targetDir, CLAUDE_MD, true);
1081
+ }
1082
+
1083
+ /**
1084
+ * Whether a fresh clone lacks `rel` in `targetDir`: it is untracked and
1085
+ * ignored. Inside a repository the repository answers, with every ignore
1086
+ * source it reads (its ignore files, its local exclude file and configured
1087
+ * excludes), and a tracked file is committed whatever they say, as git and
1088
+ * Mercurial ignore only untracked files. Outside one, the project's ignore
1089
+ * files answer. `unreadable` is the answer when the repository, or an ignore
1090
+ * file, cannot answer.
1091
+ * @param {string} targetDir
1092
+ * @param {string} rel
1093
+ * @param {boolean} unreadable
1094
+ * @returns {boolean}
1095
+ */
1096
+ function uncommittedHere(targetDir, rel, unreadable) {
1097
+ const root = repositoryRootOf(targetDir);
1098
+ if (!root) return ignoredHere(targetDir, rel, unreadable);
1099
+ return untrackedIgnored(root, targetDir, rel) ?? unreadable;
1100
+ }
1101
+
1102
+ /**
1103
+ * How each repository kind lists `rel` when it is untracked and ignored:
1104
+ * `git check-ignore` exits 0 for such a file and 1 otherwise (it never
1105
+ * reports a tracked file); `status -i -n` prints it, or nothing.
1106
+ */
1107
+ const IGNORED_QUERIES = [
1108
+ {marker: '.git', command: 'git', args: ['check-ignore', '-q', '--'], by: 'exit'},
1109
+ {marker: '.sl', command: 'sl', args: ['status', '-i', '-n', '--'], by: 'output'},
1110
+ {marker: '.hg', command: 'hg', args: ['status', '-i', '-n', '--'], by: 'output'},
1111
+ ];
1112
+
1113
+ /**
1114
+ * Whether the repository at `root` holds `rel` in `targetDir` as untracked
1115
+ * and ignored; null, which keeps the copy, when it cannot answer: its tool is
1116
+ * not installed or cannot start, runs past the time limit, or exits with a
1117
+ * code other than its documented answers.
1118
+ * @param {string} root
1119
+ * @param {string} targetDir
1120
+ * @param {string} rel
1121
+ * @returns {boolean | null}
1122
+ */
1123
+ function untrackedIgnored(root, targetDir, rel) {
1124
+ const query = IGNORED_QUERIES.find(q => fs.existsSync(path.join(root, q.marker)));
1125
+ if (!query) return null;
1126
+ const result = spawnSync(query.command, [...query.args, rel], {
1127
+ cwd: targetDir,
1128
+ encoding: 'utf-8',
1129
+ stdio: ['ignore', 'pipe', 'ignore'],
1130
+ timeout: 10_000,
1131
+ windowsHide: true,
1132
+ shell: false,
1133
+ });
1134
+ if (result.error) return null;
1135
+ if (query.by === 'exit') {
1136
+ if (result.status === 0) return true;
1137
+ if (result.status === 1) return false;
1138
+ return null;
1139
+ }
1140
+ if (result.status !== 0) return null;
1141
+ return result.stdout.trim() !== '';
1142
+ }
1143
+
1144
+ /**
1145
+ * Whether the ignore files in `targetDir` exclude `rel`, read with the
1146
+ * working-tree ignore reader upgrade uses; for a folder outside any
1147
+ * repository. `unreadable` when an ignore file cannot be read.
1148
+ * @param {string} targetDir
1149
+ * @param {string} rel
1150
+ * @param {boolean} unreadable
1151
+ * @returns {boolean}
1152
+ */
1153
+ function ignoredHere(targetDir, rel, unreadable) {
1154
+ try {
1155
+ return createFileProtectionResolver(targetDir)
1156
+ .classify(path.join(targetDir, rel))
1157
+ .some(item => item.reason === 'ignored');
1158
+ } catch {
1159
+ return unreadable;
1160
+ }
1161
+ }
1162
+
1163
+ /**
1164
+ * The repository root at or above `dir`: the nearest folder holding `.git`,
1165
+ * `.hg`, or `.sl`. Null outside a repository.
1166
+ * @param {string} dir
1167
+ * @returns {string | null}
1168
+ */
1169
+ function repositoryRootOf(dir) {
1170
+ for (let current = path.resolve(dir); ; ) {
1171
+ if (REPOSITORY_MARKERS.some(marker => fs.existsSync(path.join(current, marker)))) {
1172
+ return current;
1173
+ }
1174
+ const parent = path.dirname(current);
1175
+ if (parent === current) return null;
1176
+ current = parent;
1177
+ }
1178
+ }
1179
+
1180
+ /**
1181
+ * Whether .claude/CLAUDE.md keeps a copy beside root CLAUDE.md (INV31): while
1182
+ * a fresh clone lacks root CLAUDE.md (the project ignores it and does not
1183
+ * track it), a .claude/CLAUDE.md the project commits (tracked, or not
1184
+ * ignored) gets the copy too (DEC-2: an extra copy beats none). One the
1185
+ * project ignores and does not track follows the usual rule: its block goes
1186
+ * once root CLAUDE.md holds one. An ignore file, or tracking, that cannot be
1187
+ * read keeps the copy.
1188
+ * @param {string} targetDir
1189
+ * @returns {boolean}
1190
+ */
1191
+ function claudeDirKeepsCopy(targetDir) {
1192
+ if (!rootClaudeIgnored(targetDir)) return false;
1193
+ return !uncommittedHere(targetDir, CLAUDE_DIR_MD, false);
1194
+ }
1195
+
1196
+ /**
1197
+ * `rels` without the files that resolve outside `targetDir`, each reported
1198
+ * through `onSkipped`.
1199
+ * @param {string} targetDir
1200
+ * @param {readonly string[]} rels
1201
+ * @param {((rel: string, reason: string) => void) | undefined} onSkipped
1202
+ * @returns {Set<string>} the files skipped
1203
+ */
1204
+ function skipEscaping(targetDir, rels, onSkipped) {
1205
+ /** @type {Set<string>} */
1206
+ const skipped = new Set();
1207
+ for (const p of rels) {
1208
+ try {
1209
+ assertWithin(p, targetDir, {label: 'agent docs path'});
1210
+ } catch (err) {
1211
+ if (!(err instanceof PathSafetyError)) throw err;
1212
+ skipped.add(p);
1213
+ onSkipped?.(p, err.message);
1214
+ }
1215
+ }
1216
+ return skipped;
1217
+ }
1218
+
1219
+ /**
1220
+ * Whether root CLAUDE.md holds `block` as its managed block, so a copy in
1221
+ * .claude/CLAUDE.md may go.
1222
+ * @param {string} targetDir
1223
+ * @param {string} block
1224
+ * @returns {boolean}
1225
+ */
1226
+ function rootClaudeHolds(targetDir, block) {
1227
+ // A root CLAUDE.md the project ignores is not in a fresh clone, so it never
1228
+ // stands in for a committed copy in .claude/CLAUDE.md (an extra copy beats
1229
+ // none, DEC-2). An ignored .claude/CLAUDE.md is not in one either, so the
1230
+ // usual rule holds for it.
1231
+ if (claudeDirKeepsCopy(targetDir)) return false;
1232
+ try {
1233
+ const content = fs.readFileSync(path.join(targetDir, CLAUDE_MD), 'utf-8');
1234
+ const found = findManagedBlock(content);
1235
+ return found != null && content.slice(found.start, found.end) === block.trim();
1236
+ } catch {
1237
+ return false;
1238
+ }
1239
+ }
1240
+
732
1241
  /**
733
1242
  * Whether an agent-doc file already carries a managed-block marker.
734
1243
  * @param {string} filePath
@@ -771,24 +1280,40 @@ function assertTargetsWithin(targetDir, relPaths) {
771
1280
  }
772
1281
 
773
1282
  /**
774
- * Remove Astryx section from all known agent doc files.
1283
+ * Throw `ERR_PATH_TRAVERSAL` when an agent doc in `targetDir` that holds a
1284
+ * managed block resolves outside `targetDir`.
775
1285
  * @param {string} targetDir
776
- * @returns {string[]} the project-relative files a block was removed from.
777
- * @throws {PathSafetyError} `ERR_PATH_TRAVERSAL` when a file it would change
778
- * resolves outside `targetDir`; nothing is changed.
779
1286
  */
780
- export function removeAgentDocs(targetDir) {
1287
+ function assertRemovableWithin(targetDir, skip = new Set()) {
781
1288
  const allPaths = discoverAgentDocs(targetDir);
782
1289
  try {
783
1290
  assertTargetsWithin(
784
1291
  targetDir,
785
- allPaths.filter(p => hasRemovableBlock(path.join(targetDir, p))),
1292
+ allPaths.filter(
1293
+ p => !skip.has(p) && hasRemovableBlock(path.join(targetDir, p)),
1294
+ ),
786
1295
  );
787
1296
  } catch (err) {
788
1297
  // The code reaches the error envelope as is, so it must be registered.
789
1298
  if (!(err instanceof PathSafetyError)) throw err;
790
1299
  throw new PathSafetyError(err.message, ERROR_CODES.ERR_PATH_TRAVERSAL);
791
1300
  }
1301
+ }
1302
+
1303
+ /**
1304
+ * Remove Astryx section from all known agent doc files.
1305
+ * @param {string} targetDir
1306
+ * @param {object} [options]
1307
+ * @param {string} [options.shownFrom] - Folder the logged paths are shown
1308
+ * relative to; defaults to `targetDir`.
1309
+ * @param {Set<string>} [options.skip] - Files left as they are.
1310
+ * @returns {string[]} the project-relative files a block was removed from.
1311
+ * @throws {PathSafetyError} `ERR_PATH_TRAVERSAL` when a file it would change
1312
+ * resolves outside `targetDir`; nothing is changed.
1313
+ */
1314
+ export function removeAgentDocs(targetDir, {shownFrom = targetDir, skip = new Set()} = {}) {
1315
+ assertRemovableWithin(targetDir, skip);
1316
+ const allPaths = discoverAgentDocs(targetDir).filter(p => !skip.has(p));
792
1317
 
793
1318
  /** @type {string[]} */
794
1319
  const removedFrom = [];
@@ -797,27 +1322,374 @@ export function removeAgentDocs(targetDir) {
797
1322
  // Delete if empty for files we created (AGENTS.md, .claude/CLAUDE.md)
798
1323
  const deleteIfEmpty = p === AGENTS_MD || p === CLAUDE_DIR_MD;
799
1324
  if (removeXdsBlock(filePath, {deleteIfEmpty})) {
1325
+ // The default creates GEMINI.md holding only the import of AGENTS.md.
1326
+ if (p === GEMINI_MD && fs.readFileSync(filePath, 'utf-8').trim() === '') {
1327
+ fs.unlinkSync(filePath);
1328
+ }
800
1329
  removedFrom.push(p);
1330
+ const shown = path.relative(shownFrom, filePath).split(path.sep).join('/') || p;
801
1331
  if (!fs.existsSync(filePath)) {
802
- humanLog(`[ok] Removed empty ${p}`);
1332
+ humanLog(`[ok] Removed empty ${shown}`);
803
1333
  } else {
804
- humanLog(`[ok] Removed design system section from ${p}`);
1334
+ humanLog(`[ok] Removed design system section from ${shown}`);
805
1335
  }
806
1336
  }
807
1337
  }
808
1338
  return removedFrom;
809
1339
  }
810
1340
 
1341
+ /**
1342
+ * Remove the managed block where init writes it, the project root (see
1343
+ * {@link resolveAgentDocsRoot}), and in `cwd`, so a block an earlier version
1344
+ * wrote in that folder goes too. Each folder is checked before anything is
1345
+ * changed. Run below the project root, a project-root agent file holding a
1346
+ * block that resolves outside the project is left as it is and reported to
1347
+ * `onSkipped`, as install does there; 0.7.x touched only `cwd`. A file in
1348
+ * `cwd`, or at the root when run there, that resolves outside refuses the run.
1349
+ *
1350
+ * @param {string} cwd
1351
+ * @param {{onSkipped?: (rel: string, reason: string) => void}} [options]
1352
+ * @returns {string[]} the files a block was removed from, relative to `cwd`
1353
+ * @throws {PathSafetyError} `ERR_PATH_TRAVERSAL`; nothing is changed
1354
+ */
1355
+ export function removeProjectAgentDocs(cwd, {onSkipped} = {}) {
1356
+ const root = agentDocsTargetDir(cwd);
1357
+ const below = realDir(root) !== realDir(cwd);
1358
+ /** @type {Set<string>} */
1359
+ const rootSkip = below
1360
+ ? skipEscaping(
1361
+ root,
1362
+ discoverAgentDocs(root).filter(p => hasRemovableBlock(path.join(root, p))),
1363
+ (rel, reason) =>
1364
+ onSkipped?.(path.relative(cwd, path.join(root, rel)).split(path.sep).join('/'), reason),
1365
+ )
1366
+ : new Set();
1367
+ const dirs = below ? [root, cwd] : [cwd];
1368
+ for (const dir of dirs) assertRemovableWithin(dir, dir === root ? rootSkip : undefined);
1369
+ return dirs.flatMap(dir =>
1370
+ removeAgentDocs(dir, {shownFrom: cwd, skip: dir === root ? rootSkip : undefined}).map(rel =>
1371
+ path.relative(cwd, path.join(dir, rel)).split(path.sep).join('/'),
1372
+ ),
1373
+ );
1374
+ }
1375
+
1376
+ /**
1377
+ * Write the block into each standalone agent doc and remove a managed block
1378
+ * from each import wrapper. A refresh (`onlyReplace`) skips files without a
1379
+ * block and leaves a managed import of AGENTS.md as it is.
1380
+ *
1381
+ * @param {string} targetDir
1382
+ * @param {string[]} existing
1383
+ * @param {Map<string, Set<string>>} givenWrappers
1384
+ * @param {string} compressedIndex
1385
+ * @param {boolean} onlyReplace
1386
+ * @param {((rel: string, reason: string) => void) | undefined} onSkipped
1387
+ * @param {boolean} [refuseEscapes] whether a file 0.7.1 wrote that escapes
1388
+ * `targetDir` refuses the run
1389
+ * @returns {string[]} files written
1390
+ */
1391
+ function writeEachAgentDoc(targetDir, existing, givenWrappers, compressedIndex, onlyReplace, onSkipped, refuseEscapes = true) {
1392
+ /** @param {string} p */
1393
+ const marked = p => hasManagedMarker(path.join(targetDir, p));
1394
+ // A committed .claude/CLAUDE.md beside an ignored root CLAUDE.md keeps a
1395
+ // copy (INV31): one that already holds a block is refreshed where it is,
1396
+ // even when it imports another agent file, rather than left stale.
1397
+ const keptCopy =
1398
+ givenWrappers.has(CLAUDE_DIR_MD) && marked(CLAUDE_DIR_MD) && claudeDirKeepsCopy(targetDir);
1399
+ const wrappers = new Map(givenWrappers);
1400
+ if (keptCopy) wrappers.delete(CLAUDE_DIR_MD);
1401
+ /** @param {string} p */
1402
+ const isImport = p => {
1403
+ try {
1404
+ return isAgentsImportBlock(p, fs.readFileSync(path.join(targetDir, p), 'utf-8'));
1405
+ } catch {
1406
+ return false;
1407
+ }
1408
+ };
1409
+ // Every agent file that imports no other agent file holds a copy, GEMINI.md
1410
+ // included (INV31); a managed import of AGENTS.md is left as it is.
1411
+ const targets = existing.filter(p => !wrappers.has(p) && !isImport(p));
1412
+ const writes = targets.filter(p => !onlyReplace || marked(p));
1413
+ const strips = [...wrappers.keys()].filter(marked);
1414
+ // A file 0.7.1 did not write here (GEMINI.md, or any file when init runs
1415
+ // below the project root) that resolves outside the project is skipped and
1416
+ // reported, as in the default layout, and the rest is written. A path of a
1417
+ // file 0.7.1 wrote that escapes the project refuses the run, as in 0.7.1.
1418
+ const skipped = skipEscaping(
1419
+ targetDir,
1420
+ // A kept copy here already holds a block, which 0.7.x wrote, so its
1421
+ // escape refuses the run like any file 0.7.x wrote.
1422
+ [...writes, ...strips].filter(p => !refuseEscapes || p === GEMINI_MD),
1423
+ onSkipped,
1424
+ );
1425
+ assertTargetsWithin(
1426
+ targetDir,
1427
+ [...writes, ...strips].filter(p => !skipped.has(p)),
1428
+ );
1429
+ // A refresh skips unmarked files and a wrapper is only written when it
1430
+ // carries a block, so only the files this run writes are checked.
1431
+ /** @type {string[]} */
1432
+ const written = [];
1433
+ for (const p of targets) {
1434
+ if (skipped.has(p)) continue;
1435
+ const didWrite = injectXdsBlock(path.join(targetDir, p), compressedIndex, {onlyReplace});
1436
+ if (didWrite) written.push(p);
1437
+ }
1438
+ for (const p of wrappers.keys()) {
1439
+ if (skipped.has(p)) continue;
1440
+ // The copy in .claude/CLAUDE.md goes only once root CLAUDE.md holds it.
1441
+ if (
1442
+ p === CLAUDE_DIR_MD &&
1443
+ (skipped.has(CLAUDE_MD) || !rootClaudeHolds(targetDir, compressedIndex))
1444
+ ) {
1445
+ continue;
1446
+ }
1447
+ if (stripManagedBlock(path.join(targetDir, p), compressedIndex)) written.push(p);
1448
+ }
1449
+ return written;
1450
+ }
1451
+
1452
+ /**
1453
+ * The Claude Code file that holds the copy of the block: root CLAUDE.md, which
1454
+ * projects commit even when they ignore .claude/, else .claude/CLAUDE.md. It
1455
+ * holds the copy even when it imports AGENTS.md: Claude Code does not follow an
1456
+ * import of a file above the folder it starts in, so a session started in a
1457
+ * subfolder reads only the copy.
1458
+ *
1459
+ * @param {Set<string>} present Existing agent docs that are not AGENTS.md under
1460
+ * another name.
1461
+ * @param {Set<string>} aliases Files that are AGENTS.md under another name.
1462
+ * @returns {string | null} null when the project has no Claude file, or when
1463
+ * root CLAUDE.md is AGENTS.md under another name and so holds the block.
1464
+ */
1465
+ function claudeCopyFile(present, aliases) {
1466
+ if (aliases.has(CLAUDE_MD)) return null;
1467
+ return [CLAUDE_MD, CLAUDE_DIR_MD].find(p => present.has(p)) ?? null;
1468
+ }
1469
+
1470
+ /**
1471
+ * Plan the default layout. AGENTS.md holds the block. The Claude Code file the
1472
+ * project already has holds a copy (see {@link claudeCopyFile}), so the block
1473
+ * sits in a file the project commits; init creates no Claude file. GEMINI.md
1474
+ * imports AGENTS.md unless it already does. Every other wrapper here imports
1475
+ * AGENTS.md, so it already reaches the block.
1476
+ *
1477
+ * @param {string} targetDir
1478
+ * @param {string[]} existing
1479
+ * @param {Map<string, Set<string>>} wrappers
1480
+ * @param {Set<string>} aliases Files that are AGENTS.md under another name;
1481
+ * they are never written.
1482
+ * @returns {Map<string, 'block' | 'import' | 'strip'>}
1483
+ */
1484
+ function planDefaultAgentDocs(targetDir, existing, wrappers, aliases) {
1485
+ const present = new Set(existing.filter(p => !aliases.has(p)));
1486
+ /** @type {Map<string, 'block' | 'import' | 'strip'>} */
1487
+ const plan = new Map([[AGENTS_MD, 'block']]);
1488
+
1489
+ // Claude Code reads CLAUDE.md and .claude/CLAUDE.md, and skips AGENTS.md
1490
+ // when either exists. One Claude file holds the copy, so Claude Code reads
1491
+ // it once in a subfolder and at most twice at the root, where it also
1492
+ // follows that file's import of AGENTS.md.
1493
+ const claudeCopy = claudeCopyFile(present, aliases);
1494
+ for (const p of [CLAUDE_MD, CLAUDE_DIR_MD]) {
1495
+ if (present.has(p)) plan.set(p, p === claudeCopy ? 'block' : 'strip');
1496
+ }
1497
+
1498
+ // Gemini CLI reads GEMINI.md. Project settings can point it at AGENTS.md
1499
+ // too, but Gemini loads them only when it starts at the project root, so the
1500
+ // import is written anyway: two copies at the root beat none in a subfolder.
1501
+ // An import GEMINI.md already has, even inline, is enough.
1502
+ if (!aliases.has(GEMINI_MD)) {
1503
+ if (geminiImportsAgentsMd(targetDir)) {
1504
+ if (present.has(GEMINI_MD)) plan.set(GEMINI_MD, 'strip');
1505
+ } else {
1506
+ plan.set(GEMINI_MD, 'import');
1507
+ }
1508
+ }
1509
+
1510
+ // Every other file: a full block when standalone, none in a wrapper.
1511
+ for (const p of present) {
1512
+ if (!plan.has(p)) plan.set(p, wrappers.has(p) ? 'strip' : 'block');
1513
+ }
1514
+ return plan;
1515
+ }
1516
+
1517
+ /**
1518
+ * Write the default layout (see {@link planDefaultAgentDocs}).
1519
+ *
1520
+ * Path safety follows 0.7.0: an agent doc it would have written that resolves
1521
+ * outside `targetDir` refuses the whole run before any write. A file only this
1522
+ * layout writes, such as GEMINI.md, is skipped instead, reported through
1523
+ * `onSkipped`, and the rest is written.
1524
+ *
1525
+ * @param {string} targetDir
1526
+ * @param {string[]} existing
1527
+ * @param {Map<string, Set<string>>} wrappers
1528
+ * @param {Set<string>} aliases
1529
+ * @param {string} compressedIndex
1530
+ * @param {((rel: string, reason: string) => void) | undefined} onSkipped
1531
+ * @param {boolean} [refuseEscapes] whether a file 0.7.1 wrote that escapes
1532
+ * `targetDir` refuses the run
1533
+ * @returns {string[]} files written, in {@link AGENT_DOC_PATHS} order
1534
+ */
1535
+ function writeDefaultAgentDocs(targetDir, existing, wrappers, aliases, compressedIndex, onSkipped, refuseEscapes = true) {
1536
+ const plan = planDefaultAgentDocs(targetDir, existing, wrappers, aliases);
1537
+ // While root CLAUDE.md is one the project ignores, .claude/CLAUDE.md gets
1538
+ // the copy too, first run included: a fresh clone has only that file.
1539
+ if (plan.get(CLAUDE_DIR_MD) === 'strip' && claudeDirKeepsCopy(targetDir)) {
1540
+ plan.set(CLAUDE_DIR_MD, 'block');
1541
+ }
1542
+ // 0.7.x left a block-less import of an existing AGENTS.md alone, so a Claude
1543
+ // file of that shape that now gets the copy and resolves outside the project
1544
+ // is skipped and reported. Any other agent file 0.7.x wrote, so its escape
1545
+ // still refuses the run.
1546
+ /** @param {string} p */
1547
+ const leftAlone = p =>
1548
+ existing.includes(AGENTS_MD) &&
1549
+ wrappers.get(p)?.has(AGENTS_MD) === true &&
1550
+ !hasManagedMarker(path.join(targetDir, p));
1551
+ const foundLegacy = existing.filter(p => LEGACY_DOC_PATHS.includes(p));
1552
+ /** @param {string} p */
1553
+ const legacy = p =>
1554
+ !leftAlone(p) &&
1555
+ (foundLegacy.includes(p) || (p === AGENTS_MD && foundLegacy.length === 0));
1556
+
1557
+ /** @type {Set<string>} */
1558
+ const escaped = new Set();
1559
+ for (const [p, action] of [...plan]) {
1560
+ // A strip writes only a file that carries a block.
1561
+ if (action === 'strip' && !hasManagedMarker(path.join(targetDir, p))) {
1562
+ plan.delete(p);
1563
+ continue;
1564
+ }
1565
+ try {
1566
+ assertWithin(p, targetDir, {label: 'agent docs path'});
1567
+ } catch (err) {
1568
+ if (!(err instanceof PathSafetyError) || (refuseEscapes && legacy(p))) throw err;
1569
+ plan.delete(p);
1570
+ escaped.add(p);
1571
+ onSkipped?.(p, err.message);
1572
+ }
1573
+ }
1574
+ // An import of an AGENTS.md left as it is would point at no block, so the
1575
+ // GEMINI.md import is not written either.
1576
+ if (escaped.has(AGENTS_MD) && plan.get(GEMINI_MD) === 'import') {
1577
+ plan.delete(GEMINI_MD);
1578
+ onSkipped?.(GEMINI_MD, 'its import of AGENTS.md would point at a file left as it is');
1579
+ }
1580
+
1581
+ /** @type {string[]} */
1582
+ const written = [];
1583
+ for (const p of AGENT_DOC_PATHS) {
1584
+ const action = plan.get(p);
1585
+ if (action == null) continue;
1586
+ const filePath = path.join(targetDir, p);
1587
+ if (action === 'strip') {
1588
+ // The copy in .claude/CLAUDE.md goes only once root CLAUDE.md holds it.
1589
+ if (
1590
+ p === CLAUDE_DIR_MD &&
1591
+ (escaped.has(CLAUDE_MD) || !rootClaudeHolds(targetDir, compressedIndex))
1592
+ ) {
1593
+ continue;
1594
+ }
1595
+ if (stripManagedBlock(filePath, compressedIndex)) written.push(p);
1596
+ continue;
1597
+ }
1598
+ fs.mkdirSync(path.dirname(filePath), {recursive: true});
1599
+ injectXdsBlock(
1600
+ filePath,
1601
+ action === 'block' ? compressedIndex : renderAgentsImportBlock(p),
1602
+ {createIfMissing: true, header: CREATED_HEADERS[p] ?? ''},
1603
+ );
1604
+ written.push(p);
1605
+ }
1606
+ return written;
1607
+ }
1608
+
1609
+ /**
1610
+ * Heading of the AGENTS.md the default creates; the same text the presets
1611
+ * write, so `--remove-agents` recognizes and deletes it.
1612
+ * @type {Readonly<Record<string, string>>}
1613
+ */
1614
+ const CREATED_HEADERS = Object.freeze({
1615
+ [AGENTS_MD]: '# AGENTS.md\n\nProject-specific guidance for AI coding agents.',
1616
+ });
1617
+
1618
+ /**
1619
+ * Agent docs that are one file under several names (symlinks), resolved inside
1620
+ * or outside the project.
1621
+ * @param {string} targetDir
1622
+ * @param {string[]} existing
1623
+ * @returns {{agentsAliases: Set<string>, otherShared: boolean}} the files that
1624
+ * are AGENTS.md under another name, and whether any other two docs share a
1625
+ * file.
1626
+ */
1627
+ function findSharedAgentDocs(targetDir, existing) {
1628
+ /** @type {Map<string, string[]>} */
1629
+ const byFile = new Map();
1630
+ for (const p of existing) {
1631
+ let real;
1632
+ try {
1633
+ real = fs.realpathSync(path.join(targetDir, p));
1634
+ } catch {
1635
+ real = path.resolve(targetDir, p);
1636
+ }
1637
+ byFile.set(real, [...(byFile.get(real) ?? []), p]);
1638
+ }
1639
+ /** @type {Set<string>} */
1640
+ const agentsAliases = new Set();
1641
+ let otherShared = false;
1642
+ for (const names of byFile.values()) {
1643
+ if (names.length < 2) continue;
1644
+ if (!names.includes(AGENTS_MD)) {
1645
+ otherShared = true;
1646
+ continue;
1647
+ }
1648
+ for (const name of names) if (name !== AGENTS_MD) agentsAliases.add(name);
1649
+ }
1650
+ return {agentsAliases, otherShared};
1651
+ }
1652
+
1653
+ /**
1654
+ * Remove the managed block from a file that should not carry one, leaving the
1655
+ * rest of the file as it is. A malformed block is refused (throws) rather than
1656
+ * guessed at. `.claude/CLAUDE.md` is deleted when nothing but whitespace is
1657
+ * left.
1658
+ * @param {string} filePath
1659
+ * @param {string} compressedIndex
1660
+ * @returns {boolean} whether the file changed
1661
+ */
1662
+ function stripManagedBlock(filePath, compressedIndex) {
1663
+ const deleteIfBlank =
1664
+ path.basename(filePath) === path.basename(CLAUDE_DIR_MD) &&
1665
+ path.basename(path.dirname(filePath)) === path.dirname(CLAUDE_DIR_MD);
1666
+ if (removeXdsBlock(filePath, {deleteIfBlank})) return true;
1667
+ if (hasManagedMarker(filePath)) {
1668
+ // Preserve the existing fail-closed behavior for malformed blocks.
1669
+ injectXdsBlock(filePath, compressedIndex, {onlyReplace: true});
1670
+ }
1671
+ return false;
1672
+ }
1673
+
811
1674
  /**
812
1675
  * Programmatic entry point for installing agent docs.
813
1676
  * Used by the init command, upgrade command, and agent-docs command.
814
1677
  *
815
- * Strategy (when no agent/paths specified):
816
- * - Discover all existing agent doc files.
817
- * - Leave `@path` import wrappers untouched; if an older run expanded a block
818
- * into one, remove that duplicate block.
819
- * - Initialize or refresh every standalone file.
820
- * - If nothing exists, create AGENTS.md as the tool-agnostic default.
1678
+ * Default strategy (no agent/paths specified), so every agent reads the block:
1679
+ * - Write the block to AGENTS.md (created when missing).
1680
+ * - Write the block to the project's Claude Code file: root CLAUDE.md when it
1681
+ * exists, else an existing .claude/CLAUDE.md, even when it imports AGENTS.md.
1682
+ * Remove a block from the other one. Create no Claude file.
1683
+ * - Write a managed import of AGENTS.md to GEMINI.md (created when missing),
1684
+ * unless GEMINI.md already imports AGENTS.md.
1685
+ * - Keep a full block in every other existing standalone file.
1686
+ * - Leave every other `@path` import wrapper untouched; if an older run
1687
+ * expanded a block into one, remove that duplicate block. When a wrapper
1688
+ * imports a file other than AGENTS.md, the project keeps its own layout:
1689
+ * every standalone file gets the block.
1690
+ * A refresh (`onlyReplace`) rewrites existing blocks in place, the Claude
1691
+ * file's copy included, leaves imports of AGENTS.md as they are, and creates
1692
+ * nothing.
821
1693
  *
822
1694
  * @param {string} targetDir
823
1695
  * @param {object} [options]
@@ -831,9 +1703,19 @@ export function removeAgentDocs(targetDir) {
831
1703
  * so an integration's topics reach the agent.
832
1704
  * @param {string} [options.renderedBlock] - Fully rendered expected block. Init
833
1705
  * and upgrade pass one shared block to every target.
1706
+ * @param {(rel: string, reason: string) => void} [options.onSkipped] - Called
1707
+ * for each file skipped because it resolves outside `targetDir` and 0.7.1
1708
+ * never wrote it: GEMINI.md, or any file when `refuseEscapes` is false.
1709
+ * @param {boolean} [options.refuseEscapes] - Whether a file 0.7.1 wrote that
1710
+ * resolves outside `targetDir` refuses the run (default). Init passes false
1711
+ * when it runs below the project root, where 0.7.1 wrote in the folder it
1712
+ * ran in instead.
834
1713
  * @returns {string[]} List of files written
835
1714
  * @throws {import('../fs/path-safety.mjs').PathSafetyError} when a file it would
836
1715
  * write resolves outside `targetDir`; nothing is written.
1716
+ * @throws {AgentDocsHomeError} when `targetDir` is the home folder, or a settings
1717
+ * folder in it outside a project, or, with `paths`, when a file's own folder
1718
+ * is; nothing is written.
837
1719
  */
838
1720
  export function installAgentDocs(
839
1721
  targetDir,
@@ -845,8 +1727,14 @@ export function installAgentDocs(
845
1727
  onlyReplace = false,
846
1728
  topics,
847
1729
  renderedBlock,
1730
+ onSkipped,
1731
+ refuseEscapes = true,
848
1732
  } = {},
849
1733
  ) {
1734
+ const explicit = paths != null && paths.length > 0;
1735
+ // Explicit paths are checked file by file below: each file's own folder
1736
+ // is what agents would read in every project.
1737
+ if (!explicit) assertAgentDocsTarget(targetDir);
850
1738
  const coreDir = findCoreDir(targetDir);
851
1739
  const version = getXdsVersion(coreDir);
852
1740
  const invocation = getCliInvocation(targetDir);
@@ -865,13 +1753,20 @@ export function installAgentDocs(
865
1753
  const written = [];
866
1754
 
867
1755
  // Explicit paths override everything
868
- if (paths && paths.length > 0) {
1756
+ if (explicit) {
869
1757
  // Path-safety: each --agent-docs-path entry must resolve inside the
870
1758
  // target directory. Reject absolute paths (silent re-rooting via
871
1759
  // path.join hides intent) and `..` traversal.
872
1760
  for (const p of paths) {
873
1761
  assertWithin(p, targetDir, {label: 'agent docs path'});
874
1762
  }
1763
+ // A file directly in the home folder, or at the top of a settings folder
1764
+ // in it, is refused, as 0.7.x never wrote there unasked; a file in a
1765
+ // plain folder of the home folder, such as ~/notes/AI.md, is written.
1766
+ // Every path is checked before anything is written.
1767
+ for (const p of paths) {
1768
+ assertAgentDocsTarget(path.dirname(path.join(targetDir, p)));
1769
+ }
875
1770
  for (const p of paths) {
876
1771
  const filePath = path.join(targetDir, p);
877
1772
  const dir = path.dirname(filePath);
@@ -890,12 +1785,43 @@ export function installAgentDocs(
890
1785
  // Agent preset
891
1786
  if (agent) {
892
1787
  const {inject, create} = resolveAgentPaths(targetDir, agent);
893
- assertTargetsWithin(targetDir, [...inject, ...create]);
894
- for (const p of inject) {
1788
+ // Claude Code reads CLAUDE.md and .claude/CLAUDE.md together: when the
1789
+ // block goes in CLAUDE.md, .claude/CLAUDE.md gets none, and a block there
1790
+ // is removed.
1791
+ const claudeRoot = inject.includes(CLAUDE_MD);
1792
+ // While root CLAUDE.md is ignored, a committed .claude/CLAUDE.md holds a
1793
+ // copy too, with `claude` and `all` alike (INV31), first run included.
1794
+ const keepDirCopy = claudeRoot && claudeDirKeepsCopy(targetDir);
1795
+ const injectAll = !claudeRoot
1796
+ ? inject
1797
+ : keepDirCopy
1798
+ ? [...new Set([...inject, CLAUDE_DIR_MD])].filter(p =>
1799
+ p !== CLAUDE_DIR_MD || fs.existsSync(path.join(targetDir, CLAUDE_DIR_MD)),
1800
+ )
1801
+ : inject.filter(p => p !== CLAUDE_DIR_MD);
1802
+ const dirHasBlock = hasManagedMarker(path.join(targetDir, CLAUDE_DIR_MD));
1803
+ const strip = claudeRoot && !keepDirCopy && dirHasBlock ? [CLAUDE_DIR_MD] : [];
1804
+ // A path that escapes the project refuses the run, as in 0.7.0; below the
1805
+ // project root, where 0.7.1 wrote nothing, it is skipped and reported. A
1806
+ // .claude/CLAUDE.md the preset adds beside an ignored root, which 0.7.1
1807
+ // did not write, is skipped and reported wherever init runs.
1808
+ // So is removing a block from .claude/CLAUDE.md with `claude`, whose
1809
+ // 0.7.1 preset wrote only root CLAUDE.md there; `all` wrote both, so its
1810
+ // escape still refuses.
1811
+ const added = injectAll.filter(p => !inject.includes(p));
1812
+ const unrefused = agent === 'claude' ? [...added, ...strip] : added;
1813
+ const escaping = refuseEscapes
1814
+ ? skipEscaping(targetDir, unrefused, onSkipped)
1815
+ : skipEscaping(targetDir, [...injectAll, ...create, ...strip], onSkipped);
1816
+ const injectHere = injectAll.filter(p => !escaping.has(p));
1817
+ const createHere = create.filter(p => !escaping.has(p));
1818
+ const stripHere = escaping.has(CLAUDE_MD) ? [] : strip.filter(p => !escaping.has(p));
1819
+ assertTargetsWithin(targetDir, [...injectHere, ...createHere, ...stripHere]);
1820
+ for (const p of injectHere) {
895
1821
  injectXdsBlock(path.join(targetDir, p), compressedIndex);
896
1822
  written.push(p);
897
1823
  }
898
- for (const p of create) {
1824
+ for (const p of createHere) {
899
1825
  const filePath = path.join(targetDir, p);
900
1826
  const dir = path.dirname(filePath);
901
1827
  if (dir !== targetDir) {
@@ -907,57 +1833,45 @@ export function installAgentDocs(
907
1833
  });
908
1834
  written.push(p);
909
1835
  }
1836
+ for (const p of stripHere) {
1837
+ if (!rootClaudeHolds(targetDir, compressedIndex)) continue;
1838
+ if (stripManagedBlock(path.join(targetDir, p), compressedIndex)) written.push(p);
1839
+ }
910
1840
  return written;
911
1841
  }
912
1842
 
913
- // Auto-detect: initialize standalone files, but do not expand the managed
914
- // block beside an `@path` import of another agent doc. Remove a block from a
915
- // wrapper if an older run already duplicated it there.
1843
+ // Auto-detect. A refresh rewrites each existing block where it is.
916
1844
  const existing = discoverAgentDocs(targetDir);
917
-
918
- if (existing.length > 0) {
919
- const wrappers = discoverAgentDocWrappers(targetDir, existing);
920
- const targets = existing.filter(p => !wrappers.has(p));
921
- // A refresh skips unmarked files and a wrapper is only written when it
922
- // carries a block, so only the files this run writes are checked.
923
- /** @param {string} p */
924
- const marked = p => hasManagedMarker(path.join(targetDir, p));
925
- assertTargetsWithin(targetDir, [
926
- ...targets.filter(p => !onlyReplace || marked(p)),
927
- ...[...wrappers].filter(marked),
928
- ]);
929
-
930
- for (const p of targets) {
931
- const didWrite = injectXdsBlock(path.join(targetDir, p), compressedIndex, {onlyReplace});
932
- if (didWrite) written.push(p);
933
- }
934
- for (const p of wrappers) {
935
- const filePath = path.join(targetDir, p);
936
- if (removeXdsBlock(filePath)) {
937
- written.push(p);
938
- } else {
939
- const content = fs.readFileSync(filePath, 'utf-8');
940
- if (content.includes(MARKER_START) || content.includes(LEGACY_MARKER_START)) {
941
- // Preserve the existing fail-closed behavior for malformed blocks.
942
- injectXdsBlock(filePath, compressedIndex, {onlyReplace: true});
943
- }
944
- }
945
- }
946
- return written;
1845
+ if (onlyReplace) {
1846
+ const wrappers = findAgentDocWrappers(readAgentDocImports(targetDir, existing));
1847
+ // The Claude file keeps its copy even when it imports AGENTS.md, so a
1848
+ // refresh rewrites that block rather than removing it.
1849
+ const {agentsAliases} = findSharedAgentDocs(targetDir, existing);
1850
+ const claudeCopy = claudeCopyFile(
1851
+ new Set(existing.filter(p => !agentsAliases.has(p))),
1852
+ agentsAliases,
1853
+ );
1854
+ if (claudeCopy != null) wrappers.delete(claudeCopy);
1855
+ // A committed .claude/CLAUDE.md kept beside an ignored root CLAUDE.md is
1856
+ // refreshed even when it imports AGENTS.md (writeEachAgentDoc).
1857
+ return writeEachAgentDoc(targetDir, existing, wrappers, compressedIndex, true, onSkipped, refuseEscapes);
947
1858
  }
948
-
949
- // Nothing exists — create root AGENTS.md as the default (skip if onlyReplace).
950
- // AGENTS.md is the tool-agnostic standard (Codex/Copilot, Cursor, Muse, and
951
- // most agents read it), so it's the safe default. Claude-specific output is
952
- // opt-in via `--agent claude` (→ .claude/CLAUDE.md); `--agent all` writes both.
953
- if (onlyReplace) return written;
954
-
955
- const defaultPath = AGENTS_MD;
956
- assertTargetsWithin(targetDir, [defaultPath]);
957
- injectXdsBlock(path.join(targetDir, defaultPath), compressedIndex, {
958
- createIfMissing: true,
959
- header: `# AGENTS.md\n\nProject-specific guidance for AI coding agents.`,
960
- });
961
- written.push(defaultPath);
962
- return written;
1859
+ // A wrapper that imports something other than AGENTS.md, or two agent docs
1860
+ // that are one file, mean the project chose its own layout: keep the
1861
+ // per-file layout then. AGENTS.md under another name is fine.
1862
+ const wrappers = findAgentDocWrappers(readAgentDocImports(targetDir, existing, [AGENTS_MD]));
1863
+ const {agentsAliases, otherShared} = findSharedAgentDocs(targetDir, existing);
1864
+ if (otherShared || [...wrappers.values()].some(targets => !targets.has(AGENTS_MD))) {
1865
+ const ownWrappers = findAgentDocWrappers(readAgentDocImports(targetDir, existing));
1866
+ return writeEachAgentDoc(targetDir, existing, ownWrappers, compressedIndex, false, onSkipped, refuseEscapes);
1867
+ }
1868
+ return writeDefaultAgentDocs(
1869
+ targetDir,
1870
+ existing,
1871
+ wrappers,
1872
+ agentsAliases,
1873
+ compressedIndex,
1874
+ onSkipped,
1875
+ refuseEscapes,
1876
+ );
963
1877
  }