@astryxdesign/cli 0.7.2 → 0.7.3-canary.04c7468
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.
- package/README.md +104 -71
- package/api/build/_adapter.d.mts +58 -0
- package/api/build/_adapter.mjs +127 -3
- package/api/build/blocks/blocks.d.mts +92 -0
- package/api/build/blocks/blocks.mjs +528 -0
- package/api/build/blocks/blocks.test.mjs +238 -0
- package/api/build/blocks/blocks.weights.json +1 -0
- package/api/build/build.mjs +25 -6
- package/api/build/build.type.mjs +1 -1
- package/api/build/kit/answer.d.mts +18 -0
- package/api/build/kit/answer.mjs +195 -0
- package/api/build/kit/answer.test.mjs +108 -0
- package/api/build/kit/kind.d.mts +25 -0
- package/api/build/kit/kind.mjs +170 -0
- package/api/build/kit/kit.d.mts +2 -4
- package/api/build/kit/kit.mjs +8 -398
- package/api/build/kit/rank.d.mts +3 -32
- package/api/build/kit/rank.mjs +25 -342
- package/api/build/kit/rank.test.mjs +2 -1
- package/api/build/kit/signals.d.mts +79 -0
- package/api/build/kit/signals.mjs +295 -0
- package/api/build/kit/start.d.mts +32 -0
- package/api/build/kit/start.mjs +163 -0
- package/api/build/kit/words.d.mts +72 -0
- package/api/build/kit/words.mjs +202 -0
- package/api/build/tree.d.mts +20 -0
- package/api/build/tree.mjs +53 -0
- package/api/build/tree.test.mjs +185 -0
- package/api/build/tree.weights.json +1 -0
- package/api/component/_adapter.mjs +39 -5
- package/api/component/component-replacement.test.mjs +1 -0
- package/api/component/component.mjs +17 -3
- package/api/component/component.test.mjs +15 -0
- package/api/component/doc-links.test.mjs +2 -2
- package/api/discover/_adapter.d.mts +1 -2
- package/api/discover/_adapter.mjs +42 -17
- package/api/discover/_adapter.test.mjs +42 -0
- package/api/discover/discover.mjs +0 -1
- package/api/docs/_adapter.d.mts +2 -2
- package/api/docs/_adapter.mjs +82 -23
- package/api/docs/docs-homes.test.mjs +127 -0
- package/api/docs/docs.doc.mjs +3 -3
- package/api/docs/docs.test.mjs +35 -19
- package/api/docs/integration-tree.test.mjs +20 -20
- package/api/docs/integrationDocs.test.mjs +1 -1
- package/api/docs/namespace-section.test.mjs +6 -6
- package/api/docs/reference-blocks.test.mjs +2 -2
- package/api/doctor/_adapter.d.mts +33 -13
- package/api/doctor/_adapter.mjs +55 -25
- package/api/doctor/doctor.d.mts +23 -5
- package/api/doctor/doctor.mjs +183 -40
- package/api/doctor/doctor.test.mjs +95 -12
- package/api/hook/hook.mjs +1 -1
- package/api/init/init.doc.mjs +20 -6
- package/api/init/init.test.mjs +18 -2
- package/api/init/init.type.d.mts +3 -3
- package/api/init/init.type.mjs +3 -3
- package/api/init/remove/remove.mjs +17 -4
- package/api/init/remove/remove.test.mjs +114 -2
- package/api/init/run/run.mjs +69 -6
- package/api/integration/_adapter.d.mts +8 -0
- package/api/integration/_adapter.mjs +21 -1
- package/api/integration/authoring-checks.mjs +83 -9
- package/api/integration/authoring-checks.test.mjs +220 -0
- package/api/integration/pack-check.mjs +123 -4
- package/api/integration/pack-check.test.mjs +87 -0
- package/api/integration/validate-integration-case-disk.test.mjs +111 -0
- package/api/integration/validate-integration-fixes.test.mjs +6 -5
- package/api/integration/validate-integration-gaps.test.mjs +1975 -0
- package/api/integration/validate-integration.d.mts +15 -2
- package/api/integration/validate-integration.mjs +298 -39
- package/api/integration/validate-integration.type.d.mts +90 -1
- package/api/integration/validate-integration.type.mjs +67 -2
- package/api/integration/validateIntegration.doc.mjs +1 -1
- package/api/search/ORDERING.md +28 -0
- package/api/search/_adapter.d.mts +82 -2
- package/api/search/_adapter.mjs +157 -10
- package/api/search/_adapter.test.mjs +173 -0
- package/api/search/_candidate-score.d.mts +40 -0
- package/api/search/_candidate-score.mjs +238 -0
- package/api/search/_cards.json +6098 -0
- package/api/search/_compound.d.mts +50 -0
- package/api/search/_compound.mjs +170 -0
- package/api/search/_doc-searcher.d.mts +4 -0
- package/api/search/_doc-searcher.mjs +23 -0
- package/api/search/_doc-searcher.test.mjs +36 -0
- package/api/search/_doc-searcher.weights.json +1 -0
- package/api/search/_doc-text.d.mts +30 -0
- package/api/search/_doc-text.mjs +87 -0
- package/api/search/_entries.d.mts +63 -0
- package/api/search/_entries.mjs +232 -0
- package/api/search/_entries.test.mjs +48 -0
- package/api/search/_features.d.mts +35 -0
- package/api/search/_features.mjs +155 -0
- package/api/search/_gather-components.d.mts +26 -0
- package/api/search/_gather-components.mjs +284 -0
- package/api/search/_gather-docs.d.mts +20 -0
- package/api/search/_gather-docs.mjs +254 -0
- package/api/search/_gather-hooks.d.mts +12 -0
- package/api/search/_gather-hooks.mjs +52 -0
- package/api/search/_gather-templates.d.mts +25 -0
- package/api/search/_gather-templates.mjs +107 -0
- package/api/search/_lexical.d.mts +135 -0
- package/api/search/_lexical.mjs +351 -0
- package/api/search/_main-searcher.d.mts +19 -0
- package/api/search/_main-searcher.mjs +115 -0
- package/api/search/_main-searcher.test.mjs +106 -0
- package/api/search/_main-searcher.weights.json +1 -0
- package/api/search/_module-doc.d.mts +32 -0
- package/api/search/_module-doc.mjs +78 -0
- package/api/search/_order.d.mts +32 -0
- package/api/search/_order.mjs +162 -0
- package/api/search/_order.test.mjs +72 -0
- package/api/search/_priority.d.mts +50 -0
- package/api/search/_priority.mjs +139 -0
- package/api/search/_query-words.d.mts +25 -0
- package/api/search/_query-words.mjs +92 -0
- package/api/search/_query.d.mts +65 -0
- package/api/search/_query.mjs +176 -0
- package/api/search/_result.d.mts +97 -0
- package/api/search/_result.mjs +150 -0
- package/api/search/_score.d.mts +60 -0
- package/api/search/_score.mjs +283 -0
- package/api/search/_synonyms.d.mts +21 -0
- package/api/search/_synonyms.mjs +67 -0
- package/api/search/_template-searcher.d.mts +4 -0
- package/api/search/_template-searcher.mjs +23 -0
- package/api/search/_template-searcher.test.mjs +40 -0
- package/api/search/_template-searcher.weights.json +1 -0
- package/api/search/_titles.d.mts +33 -0
- package/api/search/_titles.mjs +65 -0
- package/api/search/_words.d.mts +64 -0
- package/api/search/_words.mjs +157 -0
- package/api/search/search.d.mts +22 -305
- package/api/search/search.doc.mjs +8 -6
- package/api/search/search.mjs +56 -2482
- package/api/search/search.test.mjs +14 -14
- package/api/search/search.type.d.mts +6 -1
- package/api/search/search.type.mjs +6 -1
- package/api/search/tree.d.mts +15 -0
- package/api/search/tree.mjs +40 -0
- package/api/search/tree.test.mjs +66 -0
- package/api/search/tree.weights.json +1 -0
- package/api/template/template.mjs +5 -1
- package/api/theme/build/build.packed-old-core.test.mjs +1 -0
- package/api/upgrade/_adapter.d.mts +57 -9
- package/api/upgrade/_adapter.mjs +198 -28
- package/api/upgrade/registry/registry.d.mts +10 -4
- package/api/upgrade/registry/registry.mjs +33 -16
- package/api/upgrade/registry/registry.test.mjs +85 -0
- package/api/upgrade/run/run.d.mts +4 -2
- package/api/upgrade/run/run.mjs +13 -2
- package/api/upgrade/run/run.test.mjs +417 -3
- package/api/upgrade/upgrade.doc.mjs +10 -5
- package/api/upgrade/upgrade.mjs +13 -2
- package/api/upgrade/upgrade.test.mjs +194 -0
- package/api/upgrade/upgrade.type.d.mts +5 -3
- package/api/upgrade/upgrade.type.mjs +8 -6
- package/assets/codemods/transforms/v0.7.0/__tests__/validate-data-token-ownership.test.mjs +11 -0
- package/assets/codemods/transforms/v0.7.0/validate-data-token-ownership.mjs +3 -3
- package/assets/docs/author-a-theme.doc.mjs +2 -2
- package/assets/docs/is-my-code-good.doc.mjs +287 -0
- package/assets/docs/tree/add-a-component.doc.mjs +1 -1
- package/assets/docs/tree/add-a-theme.doc.mjs +1 -1
- package/assets/docs/tree/add-a-topic.doc.mjs +2 -2
- package/assets/docs/tree/agent-guidance.doc.mjs +1 -1
- package/assets/docs/tree/api.doc.mjs +6 -0
- package/assets/docs/tree/{integrations.doc.mjs → authoring.doc.mjs} +15 -6
- package/assets/docs/tree/block-template.doc.mjs +1 -1
- package/assets/docs/tree/build-the-template.doc.mjs +1 -1
- package/assets/docs/tree/building-blocks.doc.mjs +2 -2
- package/assets/docs/tree/check-the-props.doc.mjs +1 -1
- package/assets/docs/tree/check-your-docs.doc.mjs +2 -2
- package/assets/docs/tree/checks.doc.mjs +3 -3
- package/assets/docs/tree/ci-and-containers.doc.mjs +13 -0
- package/assets/docs/tree/cli-command.doc.mjs +1 -1
- package/assets/docs/tree/cli.doc.mjs +1 -1
- package/assets/docs/tree/codemods.doc.mjs +1 -1
- package/assets/docs/tree/component-family.doc.mjs +1 -1
- package/assets/docs/tree/component-imports.doc.mjs +1 -1
- package/assets/docs/tree/components.doc.mjs +1 -1
- package/assets/docs/tree/configuration.doc.mjs +1 -1
- package/assets/docs/tree/debug-and-gap-reports.doc.mjs +1 -1
- package/assets/docs/tree/define-the-theme.doc.mjs +1 -1
- package/assets/docs/tree/describe-the-component.doc.mjs +1 -1
- package/assets/docs/tree/docs.doc.mjs +1 -1
- package/assets/docs/tree/document-the-template.doc.mjs +1 -1
- package/assets/docs/tree/document-the-theme.doc.mjs +1 -1
- package/assets/docs/tree/export-template-assets.doc.mjs +1 -1
- package/assets/docs/tree/extend-or-replace.doc.mjs +1 -1
- package/assets/docs/{authoring.doc.mjs → tree/field-reference.doc.mjs} +4 -3
- package/assets/docs/tree/fonts-and-assets.doc.mjs +1 -1
- package/assets/docs/tree/generate-a-palette.doc.mjs +1 -1
- package/assets/docs/tree/global-integrations.doc.mjs +2 -2
- package/assets/docs/tree/grade-template-with-agent.doc.mjs +4 -4
- package/assets/docs/tree/help.doc.mjs +2 -2
- package/assets/docs/tree/host-entry.doc.mjs +128 -0
- package/assets/docs/tree/links.doc.mjs +1 -1
- package/assets/docs/tree/package-and-test.doc.mjs +1 -1
- package/assets/docs/tree/page-template.doc.mjs +1 -1
- package/assets/docs/tree/publishing.doc.mjs +1 -1
- package/assets/docs/tree/quick-start.doc.mjs +1 -1
- package/assets/docs/tree/replace-a-core-component.doc.mjs +1 -1
- package/assets/docs/tree/replace-a-core-template.doc.mjs +1 -1
- package/assets/docs/tree/sections-and-placement.doc.mjs +1 -1
- package/assets/docs/tree/see-it-in-an-app.doc.mjs +1 -1
- package/assets/docs/tree/ship.doc.mjs +2 -2
- package/assets/docs/tree/short-and-findable.doc.mjs +1 -1
- package/assets/docs/tree/single-component.doc.mjs +1 -1
- package/assets/docs/tree/start-a-template.doc.mjs +1 -1
- package/assets/docs/tree/subcomponent.doc.mjs +1 -1
- package/assets/docs/tree/template-assets.doc.mjs +1 -1
- package/assets/docs/tree/template-doc-overview.doc.mjs +2 -2
- package/assets/docs/tree/template-fonts.doc.mjs +1 -1
- package/assets/docs/tree/template-grading-rubric.doc.mjs +17 -38
- package/assets/docs/tree/template-icons.doc.mjs +1 -1
- package/assets/docs/tree/template-images-media.doc.mjs +1 -1
- package/assets/docs/tree/template-styles.doc.mjs +1 -1
- package/assets/docs/tree/templates.doc.mjs +1 -1
- package/assets/docs/tree/test-in-an-app.doc.mjs +1 -1
- package/assets/docs/tree/test-template-in-app.doc.mjs +1 -1
- package/assets/docs/tree/themes.doc.mjs +1 -1
- package/assets/docs/tree/tokens-data.doc.mjs +2 -2
- package/assets/docs/tree/troubleshooting.doc.mjs +1 -1
- package/assets/docs/tree/upgrading.doc.mjs +1 -1
- package/assets/docs/tree/use-a-theme-in-an-app.doc.mjs +1 -1
- package/assets/docs/tree/verify-packed-template.doc.mjs +1 -1
- package/assets/docs/tree/versioning.doc.mjs +5 -1
- package/assets/docs/tree/write-good-templates.doc.mjs +1 -1
- package/assets/docs/tree/write-the-template-file.doc.mjs +1 -1
- package/assets/docs/use-integrations.doc.mjs +120 -0
- package/assets/docs/working-with-ai.doc.mjs +6 -2
- package/assets/templates/themes/butter/butterTheme.ts +4 -2
- package/assets/templates/themes/stone/stoneTheme.ts +4 -2
- package/assets/templates/themes/y2k/y2kTheme.ts +4 -2
- package/authoring/debug/parse.d.mts +1 -1
- package/authoring/doctypes/schema/schema.doc.mjs +1 -1
- package/authoring/integration/integration.doc.mjs +1 -1
- package/authoring/integration/type.ts +1 -1
- package/clients/cli/bin/astryx.mjs +7 -78
- package/clients/cli/command-result-coverage.test.mjs +6 -5
- package/clients/cli/commands/build-theme.color-scheme.test.mjs +1 -1
- package/clients/cli/commands/{component/index.mjs → component.mjs} +37 -37
- package/clients/cli/commands/component.test.mjs +6 -6
- package/clients/cli/commands/docs.doc.mjs +2 -2
- package/clients/cli/commands/docs.mjs +1 -1
- package/clients/cli/commands/docs.test.mjs +14 -14
- package/clients/cli/commands/doctor-integration.test.mjs +49 -1
- package/clients/cli/commands/doctor.doc.mjs +1 -1
- package/clients/cli/commands/doctor.mjs +40 -0
- package/clients/cli/commands/doctor.test.mjs +65 -0
- package/clients/cli/commands/{hook/index.mjs → hook.mjs} +22 -22
- package/clients/cli/commands/import-hint-correctness.test.mjs +1 -1
- package/clients/cli/commands/init.behavior.test.mjs +230 -1
- package/clients/cli/commands/init.doc.mjs +28 -9
- package/clients/cli/commands/init.flag-help.test.mjs +1 -0
- package/clients/cli/commands/init.mjs +2 -2
- package/clients/cli/commands/integration-check.test.mjs +48 -0
- package/clients/cli/commands/integration.doc.mjs +1 -1
- package/clients/cli/commands/manifest.doc.mjs +1 -1
- package/clients/cli/commands/manifest.mjs +67 -0
- package/clients/cli/commands/outside-project.test.mjs +101 -1
- package/clients/cli/commands/postinstall.doc.mjs +23 -0
- package/clients/cli/commands/postinstall.mjs +31 -0
- package/clients/cli/commands/setup-nudge.test.mjs +78 -0
- package/clients/cli/commands/{build-theme.mjs → theme.mjs} +1 -1
- package/clients/cli/commands/upgrade.doc.mjs +4 -2
- package/clients/cli/commands/upgrade.mjs +9 -2
- package/clients/cli/commands/upgrade.subfolder-path.test.mjs +97 -0
- package/clients/cli/index.mjs +54 -94
- package/clients/cli/lib/component-format.mjs +1 -1
- package/clients/cli/lib/define-command.mjs +12 -7
- package/clients/cli/lib/exit-codes.test.mjs +5 -0
- package/clients/cli/lib/manifest.test.mjs +12 -0
- package/clients/cli/lib/welcome-box.mjs +50 -0
- package/clients/cli/run.mjs +108 -0
- package/foundation/agent-docs/agent-doc-state.d.mts +32 -7
- package/foundation/agent-docs/agent-doc-state.mjs +58 -9
- package/foundation/agent-docs/agent-docs.d.mts +100 -12
- package/foundation/agent-docs/agent-docs.mjs +1017 -105
- package/foundation/agent-docs/agent-docs.path-safety.test.mjs +1239 -17
- package/foundation/agent-docs/agent-docs.test.mjs +683 -39
- package/foundation/agent-docs/computer-agent-text.d.mts +39 -2
- package/foundation/agent-docs/computer-agent-text.mjs +135 -8
- package/foundation/agent-docs/computer-agent-text.test.mjs +99 -2
- package/foundation/config/project.mjs +20 -22
- package/foundation/config/project.test.mjs +56 -0
- package/foundation/discovery/authoring-self-docs.d.mts +7 -0
- package/foundation/discovery/authoring-self-docs.mjs +16 -4
- package/foundation/discovery/authoring-self-docs.test.mjs +15 -3
- package/foundation/discovery/authoring-surface.d.mts +1 -1
- package/foundation/discovery/authoring-surface.mjs +2 -2
- package/foundation/discovery/authoring-surface.test.mjs +1 -1
- package/foundation/discovery/cli-self-docs.d.mts +2 -1
- package/foundation/discovery/cli-self-docs.mjs +4 -3
- package/foundation/discovery/cli-self-docs.test.mjs +2 -2
- package/foundation/discovery/component-discovery.d.mts +37 -0
- package/foundation/discovery/component-discovery.mjs +80 -5
- package/foundation/discovery/component-loader.d.mts +1 -1
- package/foundation/discovery/docs-discovery.mjs +14 -1
- package/foundation/discovery/module-exports.d.mts +41 -0
- package/foundation/discovery/module-exports.mjs +253 -0
- package/foundation/discovery/runner-allowlist.d.mts +2 -4
- package/foundation/discovery/runner-allowlist.mjs +2 -4
- package/foundation/discovery/runner-commands.d.mts +77 -0
- package/foundation/discovery/runner-commands.mjs +294 -0
- package/foundation/discovery/runner-commands.test.mjs +128 -0
- package/foundation/discovery/stray-pointers.test.mjs +1 -1
- package/foundation/discovery/template-adapter.mjs +18 -1
- package/foundation/discovery/template-needs.mjs +1 -44
- package/foundation/discovery/theme-discovery.mjs +147 -3
- package/foundation/doc-compiler/doc-loads.test.mjs +33 -9
- package/foundation/doc-compiler/read.mjs +59 -1
- package/foundation/doc-compiler/read.test.mjs +11 -2
- package/foundation/doc-compiler/split-topic-keywords.test.mjs +3 -2
- package/foundation/doc-compiler/tree.d.mts +32 -6
- package/foundation/doc-compiler/tree.mjs +77 -43
- package/foundation/doc-compiler/tree.test.mjs +7 -7
- package/foundation/fs/paths.d.mts +70 -8
- package/foundation/fs/paths.mjs +556 -38
- package/foundation/fs/paths.test.mjs +430 -54
- package/foundation/fs/read-record.d.mts +18 -0
- package/foundation/fs/read-record.mjs +131 -0
- package/foundation/fs/read-record.test.mjs +77 -0
- package/foundation/integrations/autolink.d.mts +2 -1
- package/foundation/integrations/autolink.mjs +4 -13
- package/foundation/integrations/cli-peer-version.d.mts +36 -0
- package/foundation/integrations/cli-peer-version.mjs +132 -0
- package/foundation/integrations/cli-peer-version.test.mjs +354 -0
- package/foundation/integrations/computer-integrations.d.mts +6 -3
- package/foundation/integrations/computer-integrations.mjs +38 -9
- package/foundation/integrations/contribution-inventory.d.mts +15 -20
- package/foundation/integrations/contribution-inventory.mjs +41 -5
- package/foundation/integrations/exports-subpaths.d.mts +57 -0
- package/foundation/integrations/exports-subpaths.mjs +113 -0
- package/foundation/integrations/integrations.d.mts +27 -1
- package/foundation/integrations/integrations.mjs +61 -2
- package/foundation/integrations/public-imports.d.mts +118 -0
- package/foundation/integrations/public-imports.mjs +1769 -0
- package/foundation/integrations/public-imports.test.mjs +1793 -0
- package/foundation/integrations/validate-contributions.d.mts +115 -0
- package/foundation/integrations/validate-contributions.mjs +1168 -13
- package/foundation/release/cache.test.mjs +24 -21
- package/foundation/release/global-state.d.mts +8 -4
- package/foundation/release/global-state.mjs +4 -2
- package/foundation/release/handoff.d.mts +23 -4
- package/foundation/release/handoff.mjs +175 -8
- package/foundation/release/notices.d.mts +62 -8
- package/foundation/release/notices.mjs +136 -25
- package/foundation/release/notices.test.mjs +21 -0
- package/foundation/release/run-context.mjs +14 -0
- package/foundation/response/response-types.doc.mjs +1 -1
- package/foundation/route/route.d.mts +285 -0
- package/foundation/route/route.mjs +324 -0
- package/foundation/route/route.test.mjs +153 -0
- package/foundation/runtime/hosted-runtime.test.mjs +403 -0
- package/foundation/runtime/locked-versions.d.mts +32 -0
- package/foundation/runtime/locked-versions.mjs +65 -0
- package/foundation/runtime/package-root.d.mts +91 -0
- package/foundation/runtime/package-root.mjs +174 -0
- package/foundation/runtime/package-set.d.mts +62 -0
- package/foundation/runtime/package-set.mjs +130 -0
- package/foundation/runtime/package-set.test.mjs +432 -0
- package/foundation/text/string-utils.d.mts +2 -1
- package/foundation/text/string-utils.mjs +16 -5
- package/foundation/text/suggest-order.d.mts +72 -0
- package/foundation/text/suggest-order.mjs +363 -0
- package/foundation/text/suggest-order.test.mjs +259 -0
- package/host/door-conformance.test.mjs +274 -0
- package/host/index.d.ts +116 -0
- package/host/index.mjs +295 -0
- package/host/index.test.mjs +302 -0
- package/package.json +14 -9
|
@@ -20,10 +20,64 @@ import {
|
|
|
20
20
|
renderAgentDocsBlock,
|
|
21
21
|
inspectAgentDocs,
|
|
22
22
|
isAstryxInitialized,
|
|
23
|
+
AgentDocsHomeError,
|
|
24
|
+
resolveAgentDocsRoot,
|
|
23
25
|
} from './agent-docs.mjs';
|
|
24
26
|
|
|
25
27
|
let tmpDir;
|
|
26
28
|
|
|
29
|
+
/** The managed import of AGENTS.md the default writes to GEMINI.md. */
|
|
30
|
+
const GEMINI_IMPORT = '<!-- ASTRYX:START -->\n@./AGENTS.md\n<!-- ASTRYX:END -->\n';
|
|
31
|
+
/** The heading of a .claude/CLAUDE.md the default creates. */
|
|
32
|
+
const CLAUDE_HEADING = '# CLAUDE\n\nProject-specific guidance for AI coding agents.\n\n';
|
|
33
|
+
|
|
34
|
+
/** Every file the default layout can write. */
|
|
35
|
+
const LAYOUT_FILES = ['AGENTS.md', 'CLAUDE.md', '.claude/CLAUDE.md', 'GEMINI.md'];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* How many full managed blocks the files hold, by the header every block
|
|
39
|
+
* starts with. An import of AGENTS.md carries no header.
|
|
40
|
+
* @param {string} dir
|
|
41
|
+
* @param {string[]} files
|
|
42
|
+
*/
|
|
43
|
+
function countBlocks(dir, files) {
|
|
44
|
+
return files
|
|
45
|
+
.filter(f => fs.existsSync(path.join(dir, f)))
|
|
46
|
+
.map(f => fs.readFileSync(path.join(dir, f), 'utf-8').split(/Astryx v\d/).length - 1)
|
|
47
|
+
.reduce((a, b) => a + b, 0);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Full blocks in the files each agent reads at the project root, as measured:
|
|
52
|
+
* Codex reads AGENTS.md; Copilot CLI reads AGENTS.md, CLAUDE.md and GEMINI.md
|
|
53
|
+
* and does not expand an import in GEMINI.md; Claude Code reads CLAUDE.md and
|
|
54
|
+
* .claude/CLAUDE.md, or AGENTS.md when neither exists; Gemini CLI reads
|
|
55
|
+
* GEMINI.md and expands its import of AGENTS.md.
|
|
56
|
+
* @param {string} dir
|
|
57
|
+
*/
|
|
58
|
+
function blocksSeen(dir) {
|
|
59
|
+
const claudeFiles = ['CLAUDE.md', '.claude/CLAUDE.md'].filter(f =>
|
|
60
|
+
fs.existsSync(path.join(dir, f)),
|
|
61
|
+
);
|
|
62
|
+
const gemini = fs.existsSync(path.join(dir, 'GEMINI.md'))
|
|
63
|
+
? fs.readFileSync(path.join(dir, 'GEMINI.md'), 'utf-8')
|
|
64
|
+
: null;
|
|
65
|
+
return {
|
|
66
|
+
claude: countBlocks(dir, claudeFiles.length > 0 ? claudeFiles : ['AGENTS.md']),
|
|
67
|
+
codex: countBlocks(dir, ['AGENTS.md']),
|
|
68
|
+
gemini:
|
|
69
|
+
gemini == null
|
|
70
|
+
? 0
|
|
71
|
+
: countBlocks(dir, ['GEMINI.md']) +
|
|
72
|
+
(/(^|\s)@(\.\/)?AGENTS\.md(\s|$)/.test(gemini) ? countBlocks(dir, ['AGENTS.md']) : 0),
|
|
73
|
+
copilot: countBlocks(dir, ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md']),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/** Every agent reads one copy. */
|
|
77
|
+
const ONCE_EACH = {claude: 1, codex: 1, gemini: 1, copilot: 1};
|
|
78
|
+
/** Copilot CLI reads AGENTS.md and a root CLAUDE.md together: the accepted duplicate. */
|
|
79
|
+
const COPILOT_TWICE = {claude: 1, codex: 1, gemini: 1, copilot: 2};
|
|
80
|
+
|
|
27
81
|
beforeEach(() => {
|
|
28
82
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-agent-docs-test-'));
|
|
29
83
|
});
|
|
@@ -155,12 +209,13 @@ describe('generateCompressedIndex', () => {
|
|
|
155
209
|
expect(line).toContain('astryx docs');
|
|
156
210
|
});
|
|
157
211
|
|
|
158
|
-
it('points to the CLI docs
|
|
159
|
-
// The integration
|
|
160
|
-
//
|
|
212
|
+
it('points to the CLI docs and the authoring docs on lines of their own', () => {
|
|
213
|
+
// The integration guides live under `authoring` in the docs tree, so the
|
|
214
|
+
// topic line does not name them; the tree's entry points do.
|
|
161
215
|
const block = generateCompressedIndex('1.0.0');
|
|
216
|
+
expect(block).toContain(' docs cli commands and API reference');
|
|
162
217
|
expect(block).toContain(
|
|
163
|
-
' docs
|
|
218
|
+
' docs authoring build an integration package, and every field it holds',
|
|
164
219
|
);
|
|
165
220
|
expect(topicLine(block)).not.toContain('cli-integrations');
|
|
166
221
|
});
|
|
@@ -301,7 +356,8 @@ describe('renderAgentDocsBlock', () => {
|
|
|
301
356
|
'utf-8',
|
|
302
357
|
);
|
|
303
358
|
expect(firstAgents).toContain(renderedBlock);
|
|
304
|
-
expect(firstClaude).
|
|
359
|
+
expect(firstClaude).toBe(`# Claude\n\n${renderedBlock}\n`);
|
|
360
|
+
expect(fs.existsSync(path.join(projectDir, '.claude'))).toBe(false);
|
|
305
361
|
|
|
306
362
|
installAgentDocs(projectDir, {renderedBlock});
|
|
307
363
|
expect(fs.readFileSync(path.join(projectDir, 'AGENTS.md'), 'utf-8')).toBe(
|
|
@@ -655,24 +711,27 @@ describe('installAgentDocs', () => {
|
|
|
655
711
|
);
|
|
656
712
|
}
|
|
657
713
|
|
|
658
|
-
it('creates AGENTS.md when no agent docs exist
|
|
714
|
+
it('creates AGENTS.md, plus the GEMINI.md import, when no agent docs exist', () => {
|
|
659
715
|
setupCorePackage(tmpDir);
|
|
660
716
|
|
|
661
717
|
const written = installAgentDocs(tmpDir);
|
|
662
718
|
|
|
663
|
-
expect(written).toEqual(['AGENTS.md']);
|
|
719
|
+
expect(written).toEqual(['AGENTS.md', 'GEMINI.md']);
|
|
664
720
|
expect(fs.existsSync(path.join(tmpDir, 'AGENTS.md'))).toBe(true);
|
|
665
|
-
//
|
|
721
|
+
// Claude Code reads AGENTS.md when the project has no CLAUDE.md, so no
|
|
722
|
+
// Claude file is created.
|
|
666
723
|
expect(fs.existsSync(path.join(tmpDir, '.claude', 'CLAUDE.md'))).toBe(false);
|
|
724
|
+
expect(fs.existsSync(path.join(tmpDir, 'CLAUDE.md'))).toBe(false);
|
|
667
725
|
const content = fs.readFileSync(path.join(tmpDir, 'AGENTS.md'), 'utf-8');
|
|
668
726
|
expect(content).toContain('# AGENTS.md');
|
|
669
727
|
expect(content).toContain('<!-- ASTRYX:START -->');
|
|
728
|
+
expect(fs.readFileSync(path.join(tmpDir, 'GEMINI.md'), 'utf-8')).toBe(GEMINI_IMPORT);
|
|
670
729
|
});
|
|
671
730
|
|
|
672
731
|
it('defaults to AGENTS.md but writes .claude/CLAUDE.md only when --agent claude is explicit', () => {
|
|
673
732
|
// Default (no agent): tool-agnostic AGENTS.md, never the Claude file.
|
|
674
733
|
setupCorePackage(tmpDir);
|
|
675
|
-
expect(installAgentDocs(tmpDir)).toEqual(['AGENTS.md']);
|
|
734
|
+
expect(installAgentDocs(tmpDir)).toEqual(['AGENTS.md', 'GEMINI.md']);
|
|
676
735
|
expect(fs.existsSync(path.join(tmpDir, '.claude', 'CLAUDE.md'))).toBe(false);
|
|
677
736
|
|
|
678
737
|
// Explicit Claude: the Claude-specific file, in a fresh project.
|
|
@@ -686,46 +745,193 @@ describe('installAgentDocs', () => {
|
|
|
686
745
|
}
|
|
687
746
|
});
|
|
688
747
|
|
|
689
|
-
it('
|
|
748
|
+
it('writes the block to AGENTS.md and to root CLAUDE.md when the project has only CLAUDE.md', () => {
|
|
749
|
+
// Codex reads only AGENTS.md, and Claude Code skips AGENTS.md beside a
|
|
750
|
+
// CLAUDE.md. The copy goes in the committed root CLAUDE.md; init creates
|
|
751
|
+
// no .claude/ folder.
|
|
690
752
|
setupCorePackage(tmpDir);
|
|
691
753
|
fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), '# Claude\n\nProject rules.\n');
|
|
692
754
|
|
|
693
755
|
const written = installAgentDocs(tmpDir);
|
|
694
756
|
|
|
695
|
-
expect(written).toEqual(['CLAUDE.md']);
|
|
696
|
-
expect(
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
757
|
+
expect(written).toEqual(['AGENTS.md', 'CLAUDE.md', 'GEMINI.md']);
|
|
758
|
+
expect(
|
|
759
|
+
fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8').startsWith(
|
|
760
|
+
'# Claude\n\nProject rules.\n\n<!-- ASTRYX:START -->',
|
|
761
|
+
),
|
|
762
|
+
).toBe(true);
|
|
763
|
+
expect(fs.existsSync(path.join(tmpDir, '.claude'))).toBe(false);
|
|
764
|
+
expect(fs.readFileSync(path.join(tmpDir, 'GEMINI.md'), 'utf-8')).toBe(GEMINI_IMPORT);
|
|
765
|
+
expect(blocksSeen(tmpDir)).toEqual(COPILOT_TWICE);
|
|
700
766
|
});
|
|
701
767
|
|
|
702
|
-
it('
|
|
768
|
+
it('keeps the block in root CLAUDE.md when AGENTS.md and CLAUDE.md both exist', () => {
|
|
703
769
|
setupCorePackage(tmpDir);
|
|
704
770
|
fs.writeFileSync(path.join(tmpDir, 'AGENTS.md'), '# Agents\n\nAgent rules.\n');
|
|
705
771
|
fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), '# Claude\n\nClaude rules.\n');
|
|
706
772
|
|
|
707
773
|
const written = installAgentDocs(tmpDir);
|
|
708
774
|
|
|
709
|
-
expect(written).
|
|
710
|
-
expect(written).toContain('CLAUDE.md');
|
|
775
|
+
expect(written).toEqual(['AGENTS.md', 'CLAUDE.md', 'GEMINI.md']);
|
|
711
776
|
const agentsContent = fs.readFileSync(path.join(tmpDir, 'AGENTS.md'), 'utf-8');
|
|
712
|
-
|
|
777
|
+
expect(agentsContent).toContain('Agent rules.');
|
|
713
778
|
expect(agentsContent).toContain('<!-- ASTRYX:START -->');
|
|
714
|
-
expect(
|
|
779
|
+
expect(fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8')).toContain(
|
|
780
|
+
'Claude rules.',
|
|
781
|
+
);
|
|
782
|
+
expect(blocksSeen(tmpDir)).toEqual(COPILOT_TWICE);
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
it('refreshes a block an older run wrote into CLAUDE.md and adds one to AGENTS.md', () => {
|
|
786
|
+
setupCorePackage(tmpDir, '2.0.0');
|
|
787
|
+
fs.writeFileSync(
|
|
788
|
+
path.join(tmpDir, 'CLAUDE.md'),
|
|
789
|
+
'# Claude\n\nRules.\n\n<!-- ASTRYX:START -->\nAstryx v1.0.0 · old\n<!-- ASTRYX:END -->\n\nMore rules.\n',
|
|
790
|
+
);
|
|
791
|
+
|
|
792
|
+
const written = installAgentDocs(tmpDir);
|
|
793
|
+
|
|
794
|
+
expect(written).toEqual(['AGENTS.md', 'CLAUDE.md', 'GEMINI.md']);
|
|
795
|
+
const claude = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8');
|
|
796
|
+
expect(claude).toContain('Astryx v2.0.0');
|
|
797
|
+
expect(claude).not.toContain('Astryx v1.0.0');
|
|
798
|
+
expect(claude).toContain('More rules.');
|
|
799
|
+
expect(fs.readFileSync(path.join(tmpDir, 'AGENTS.md'), 'utf-8')).toContain(
|
|
800
|
+
'Astryx v2.0.0',
|
|
801
|
+
);
|
|
802
|
+
expect(blocksSeen(tmpDir)).toEqual(COPILOT_TWICE);
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
it('gives Claude Code one copy when CLAUDE.md and .claude/CLAUDE.md both exist', () => {
|
|
806
|
+
// The copy goes in root CLAUDE.md, which projects commit even when they
|
|
807
|
+
// ignore .claude/; a block an older run left in .claude/CLAUDE.md goes.
|
|
808
|
+
setupCorePackage(tmpDir, '2.0.0');
|
|
809
|
+
fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), '# Claude\n');
|
|
810
|
+
fs.mkdirSync(path.join(tmpDir, '.claude'));
|
|
811
|
+
fs.writeFileSync(
|
|
812
|
+
path.join(tmpDir, '.claude', 'CLAUDE.md'),
|
|
813
|
+
'# Local\n\n<!-- ASTRYX:START -->\nAstryx v1.0.0 · old\n<!-- ASTRYX:END -->\n',
|
|
814
|
+
);
|
|
815
|
+
|
|
816
|
+
const written = installAgentDocs(tmpDir);
|
|
817
|
+
|
|
818
|
+
expect(written).toEqual(['AGENTS.md', 'CLAUDE.md', '.claude/CLAUDE.md', 'GEMINI.md']);
|
|
819
|
+
expect(fs.readFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), 'utf-8')).toBe(
|
|
820
|
+
'# Local\n',
|
|
821
|
+
);
|
|
822
|
+
expect(blocksSeen(tmpDir)).toEqual(COPILOT_TWICE);
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
it('writes the same bytes when run again', () => {
|
|
826
|
+
setupCorePackage(tmpDir);
|
|
827
|
+
fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), '# Claude\n');
|
|
828
|
+
installAgentDocs(tmpDir);
|
|
829
|
+
const files = ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md'];
|
|
830
|
+
const first = files.map(f => fs.readFileSync(path.join(tmpDir, f), 'utf-8'));
|
|
831
|
+
|
|
832
|
+
expect(installAgentDocs(tmpDir)).toEqual(files);
|
|
833
|
+
expect(files.map(f => fs.readFileSync(path.join(tmpDir, f), 'utf-8'))).toEqual(first);
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
it('adds no import when GEMINI.md already imports AGENTS.md inline', () => {
|
|
837
|
+
setupCorePackage(tmpDir);
|
|
838
|
+
const gemini = '# Gemini\n\nFollow @./AGENTS.md always.\n';
|
|
839
|
+
fs.writeFileSync(path.join(tmpDir, 'GEMINI.md'), gemini);
|
|
840
|
+
|
|
841
|
+
expect(installAgentDocs(tmpDir)).toEqual(['AGENTS.md']);
|
|
842
|
+
expect(fs.readFileSync(path.join(tmpDir, 'GEMINI.md'), 'utf-8')).toBe(gemini);
|
|
843
|
+
expect(blocksSeen(tmpDir)).toEqual(ONCE_EACH);
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
it('removes its own import when GEMINI.md also imports AGENTS.md', () => {
|
|
847
|
+
setupCorePackage(tmpDir);
|
|
848
|
+
fs.writeFileSync(
|
|
849
|
+
path.join(tmpDir, 'GEMINI.md'),
|
|
850
|
+
`# Gemini\n\nSee @AGENTS.md first.\n\n${GEMINI_IMPORT}`,
|
|
851
|
+
);
|
|
852
|
+
|
|
853
|
+
expect(installAgentDocs(tmpDir)).toEqual(['AGENTS.md', 'GEMINI.md']);
|
|
854
|
+
expect(fs.readFileSync(path.join(tmpDir, 'GEMINI.md'), 'utf-8')).toBe(
|
|
855
|
+
'# Gemini\n\nSee @AGENTS.md first.\n',
|
|
856
|
+
);
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
it('adds the import when GEMINI.md mentions AGENTS.md only in code or not as an import', () => {
|
|
860
|
+
setupCorePackage(tmpDir);
|
|
861
|
+
// Gemini CLI follows no `@` inside backticks, none glued to a word, and
|
|
862
|
+
// none whose path does not name AGENTS.md exactly.
|
|
863
|
+
const gemini = 'Write `@./AGENTS.md` to import.\nmail@AGENTS.md\nRead @./AGENTS.md.\n';
|
|
864
|
+
fs.writeFileSync(path.join(tmpDir, 'GEMINI.md'), gemini);
|
|
865
|
+
|
|
866
|
+
expect(installAgentDocs(tmpDir)).toEqual(['AGENTS.md', 'GEMINI.md']);
|
|
867
|
+
expect(fs.readFileSync(path.join(tmpDir, 'GEMINI.md'), 'utf-8')).toBe(
|
|
868
|
+
`${gemini}\n${GEMINI_IMPORT}`,
|
|
869
|
+
);
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
it.each([
|
|
873
|
+
['a list', '{"context": {"fileName": ["AGENTS.md", "GEMINI.md"]}}'],
|
|
874
|
+
['a name', '{"context": {"fileName": "AGENTS.md"}}'],
|
|
875
|
+
['comments', '{\n // Gemini reads AGENTS.md\n "context": {/* both */ "fileName": ["AGENTS.md", "GEMINI.md"]}\n}\n'],
|
|
876
|
+
['another file', '{"context": {"fileName": "GEMINI.md"}}'],
|
|
877
|
+
])('writes the GEMINI.md import whatever the project Gemini settings read (%s)', (_label, settings) => {
|
|
878
|
+
// Gemini loads project settings only when it starts at the project root;
|
|
879
|
+
// started in a subfolder, the import is its only copy.
|
|
880
|
+
setupCorePackage(tmpDir);
|
|
881
|
+
fs.mkdirSync(path.join(tmpDir, '.gemini'));
|
|
882
|
+
fs.writeFileSync(path.join(tmpDir, '.gemini', 'settings.json'), settings);
|
|
883
|
+
|
|
884
|
+
expect(installAgentDocs(tmpDir)).toEqual(['AGENTS.md', 'GEMINI.md']);
|
|
885
|
+
expect(fs.readFileSync(path.join(tmpDir, 'GEMINI.md'), 'utf-8')).toBe(GEMINI_IMPORT);
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
it('keeps its GEMINI.md import when the project Gemini settings read AGENTS.md', () => {
|
|
889
|
+
setupCorePackage(tmpDir);
|
|
890
|
+
fs.writeFileSync(path.join(tmpDir, 'GEMINI.md'), `# Gemini\n\n${GEMINI_IMPORT}`);
|
|
891
|
+
fs.mkdirSync(path.join(tmpDir, '.gemini'));
|
|
892
|
+
fs.writeFileSync(
|
|
893
|
+
path.join(tmpDir, '.gemini', 'settings.json'),
|
|
894
|
+
'{"context": {"fileName": ["AGENTS.md", "GEMINI.md"]}}',
|
|
895
|
+
);
|
|
896
|
+
|
|
897
|
+
expect(installAgentDocs(tmpDir)).toEqual(['AGENTS.md', 'GEMINI.md']);
|
|
898
|
+
expect(fs.readFileSync(path.join(tmpDir, 'GEMINI.md'), 'utf-8')).toBe(
|
|
899
|
+
`# Gemini\n\n${GEMINI_IMPORT}`,
|
|
900
|
+
);
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
it('leaves a file that is AGENTS.md under another name alone', () => {
|
|
904
|
+
setupCorePackage(tmpDir);
|
|
905
|
+
fs.writeFileSync(path.join(tmpDir, 'AGENTS.md'), '# Agents\n');
|
|
906
|
+
fs.symlinkSync('AGENTS.md', path.join(tmpDir, 'CLAUDE.md'));
|
|
907
|
+
|
|
908
|
+
// Claude Code reads the block through CLAUDE.md, so no copy is added.
|
|
909
|
+
expect(installAgentDocs(tmpDir)).toEqual(['AGENTS.md', 'GEMINI.md']);
|
|
910
|
+
expect(fs.existsSync(path.join(tmpDir, '.claude', 'CLAUDE.md'))).toBe(false);
|
|
911
|
+
expect(countBlocks(tmpDir, ['AGENTS.md'])).toBe(1);
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
it('keeps an existing GEMINI.md and adds the import to it', () => {
|
|
915
|
+
setupCorePackage(tmpDir);
|
|
916
|
+
fs.writeFileSync(path.join(tmpDir, 'GEMINI.md'), '# Gemini\n\nUse tabs.\n');
|
|
917
|
+
|
|
918
|
+
expect(installAgentDocs(tmpDir)).toEqual(['AGENTS.md', 'GEMINI.md']);
|
|
919
|
+
expect(fs.readFileSync(path.join(tmpDir, 'GEMINI.md'), 'utf-8')).toBe(
|
|
920
|
+
`# Gemini\n\nUse tabs.\n\n${GEMINI_IMPORT}`,
|
|
921
|
+
);
|
|
715
922
|
});
|
|
716
923
|
|
|
717
924
|
it('preserves import wrappers while initializing standalone files', () => {
|
|
718
925
|
setupCorePackage(tmpDir);
|
|
719
926
|
fs.writeFileSync(path.join(tmpDir, 'AGENTS.md'), '# Agents\n');
|
|
720
|
-
|
|
721
|
-
const
|
|
722
|
-
const wrapper = '@../AGENTS.md\n';
|
|
927
|
+
const wrapperPath = path.join(tmpDir, '.hermes.md');
|
|
928
|
+
const wrapper = '@AGENTS.md\n';
|
|
723
929
|
fs.writeFileSync(wrapperPath, wrapper);
|
|
724
930
|
fs.writeFileSync(path.join(tmpDir, '.cursorrules'), 'Cursor rules.\n');
|
|
725
931
|
|
|
726
932
|
const written = installAgentDocs(tmpDir);
|
|
727
933
|
|
|
728
|
-
expect(written).toEqual(['AGENTS.md', '.cursorrules']);
|
|
934
|
+
expect(written).toEqual(['AGENTS.md', 'GEMINI.md', '.cursorrules']);
|
|
729
935
|
expect(fs.readFileSync(wrapperPath, 'utf-8')).toBe(wrapper);
|
|
730
936
|
expect(fs.readFileSync(path.join(tmpDir, 'AGENTS.md'), 'utf-8')).toContain(
|
|
731
937
|
'<!-- ASTRYX:START -->',
|
|
@@ -735,7 +941,7 @@ describe('installAgentDocs', () => {
|
|
|
735
941
|
);
|
|
736
942
|
});
|
|
737
943
|
|
|
738
|
-
it('
|
|
944
|
+
it('treats cyclic imports as standalone files', () => {
|
|
739
945
|
setupCorePackage(tmpDir);
|
|
740
946
|
fs.writeFileSync(path.join(tmpDir, 'AGENTS.md'), '@CLAUDE.md\n');
|
|
741
947
|
fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), '@AGENTS.md\n');
|
|
@@ -743,22 +949,30 @@ describe('installAgentDocs', () => {
|
|
|
743
949
|
|
|
744
950
|
const written = installAgentDocs(tmpDir);
|
|
745
951
|
|
|
746
|
-
expect(written).toEqual(['AGENTS.md', 'CLAUDE.md', '.cursorrules']);
|
|
747
|
-
for (const rel of
|
|
748
|
-
expect(
|
|
749
|
-
'<!-- ASTRYX:START -->',
|
|
750
|
-
);
|
|
952
|
+
expect(written).toEqual(['AGENTS.md', 'CLAUDE.md', 'GEMINI.md', '.cursorrules']);
|
|
953
|
+
for (const rel of ['AGENTS.md', 'CLAUDE.md', '.cursorrules']) {
|
|
954
|
+
expect(countBlocks(tmpDir, [rel])).toBe(1);
|
|
751
955
|
}
|
|
752
956
|
});
|
|
753
957
|
|
|
958
|
+
it('keeps the per-file layout when a wrapper imports a file other than AGENTS.md', () => {
|
|
959
|
+
setupCorePackage(tmpDir);
|
|
960
|
+
fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), '# Claude\n');
|
|
961
|
+
fs.writeFileSync(path.join(tmpDir, 'AGENTS.md'), '@CLAUDE.md\n');
|
|
962
|
+
|
|
963
|
+
expect(installAgentDocs(tmpDir)).toEqual(['CLAUDE.md']);
|
|
964
|
+
expect(fs.readFileSync(path.join(tmpDir, 'AGENTS.md'), 'utf-8')).toBe('@CLAUDE.md\n');
|
|
965
|
+
expect(countBlocks(tmpDir, ['CLAUDE.md'])).toBe(1);
|
|
966
|
+
expect(fs.existsSync(path.join(tmpDir, 'GEMINI.md'))).toBe(false);
|
|
967
|
+
});
|
|
968
|
+
|
|
754
969
|
it('removes a managed block previously expanded into an import wrapper', () => {
|
|
755
970
|
setupCorePackage(tmpDir);
|
|
756
971
|
installAgentDocs(tmpDir, {agent: 'codex'});
|
|
757
|
-
|
|
758
|
-
const
|
|
759
|
-
const wrapper = '@../AGENTS.md\n';
|
|
972
|
+
const wrapperPath = path.join(tmpDir, '.hermes.md');
|
|
973
|
+
const wrapper = '@AGENTS.md\n';
|
|
760
974
|
fs.writeFileSync(wrapperPath, wrapper);
|
|
761
|
-
installAgentDocs(tmpDir, {agent: '
|
|
975
|
+
installAgentDocs(tmpDir, {agent: 'hermes'});
|
|
762
976
|
expect(fs.readFileSync(wrapperPath, 'utf-8')).toContain(
|
|
763
977
|
'<!-- ASTRYX:START -->',
|
|
764
978
|
);
|
|
@@ -766,7 +980,7 @@ describe('installAgentDocs', () => {
|
|
|
766
980
|
const written = installAgentDocs(tmpDir, {onlyReplace: true});
|
|
767
981
|
|
|
768
982
|
expect(written).toContain('AGENTS.md');
|
|
769
|
-
expect(written).toContain('.
|
|
983
|
+
expect(written).toContain('.hermes.md');
|
|
770
984
|
expect(fs.readFileSync(wrapperPath, 'utf-8')).toBe(wrapper);
|
|
771
985
|
});
|
|
772
986
|
|
|
@@ -782,17 +996,123 @@ describe('installAgentDocs', () => {
|
|
|
782
996
|
expect(() => installAgentDocs(tmpDir)).toThrow(/malformed|no matching/i);
|
|
783
997
|
});
|
|
784
998
|
|
|
785
|
-
it('
|
|
999
|
+
it('adds the block to an existing .claude/CLAUDE.md and to AGENTS.md', () => {
|
|
786
1000
|
setupCorePackage(tmpDir);
|
|
787
1001
|
fs.mkdirSync(path.join(tmpDir, '.claude'), {recursive: true});
|
|
788
1002
|
fs.writeFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), '# Project\n\nExisting content.\n');
|
|
789
1003
|
|
|
790
1004
|
const written = installAgentDocs(tmpDir);
|
|
791
1005
|
|
|
792
|
-
expect(written).toEqual(['.claude/CLAUDE.md']);
|
|
1006
|
+
expect(written).toEqual(['AGENTS.md', '.claude/CLAUDE.md', 'GEMINI.md']);
|
|
793
1007
|
const content = fs.readFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), 'utf-8');
|
|
794
|
-
expect(content
|
|
795
|
-
|
|
1008
|
+
expect(content.startsWith('# Project\n\nExisting content.\n\n<!-- ASTRYX:START -->')).toBe(
|
|
1009
|
+
true,
|
|
1010
|
+
);
|
|
1011
|
+
expect(blocksSeen(tmpDir)).toEqual(ONCE_EACH);
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
it('gives a root CLAUDE.md that imports AGENTS.md a copy too', () => {
|
|
1015
|
+
// Claude Code does not follow an import of a file above the folder it
|
|
1016
|
+
// starts in, so a session in a subfolder reads only the copy.
|
|
1017
|
+
setupCorePackage(tmpDir);
|
|
1018
|
+
const claude = '# Claude\n\n@AGENTS.md\n';
|
|
1019
|
+
fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), claude);
|
|
1020
|
+
|
|
1021
|
+
expect(installAgentDocs(tmpDir)).toEqual(['AGENTS.md', 'CLAUDE.md', 'GEMINI.md']);
|
|
1022
|
+
const content = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8');
|
|
1023
|
+
expect(content.startsWith(`${claude}\n<!-- ASTRYX:START -->`)).toBe(true);
|
|
1024
|
+
expect(countBlocks(tmpDir, ['AGENTS.md', 'CLAUDE.md'])).toBe(2);
|
|
1025
|
+
expect(fs.existsSync(path.join(tmpDir, '.claude'))).toBe(false);
|
|
1026
|
+
|
|
1027
|
+
// Running again keeps the copy where it is.
|
|
1028
|
+
expect(installAgentDocs(tmpDir)).toEqual(['AGENTS.md', 'CLAUDE.md', 'GEMINI.md']);
|
|
1029
|
+
expect(fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8')).toBe(content);
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
it('keeps the copy in root CLAUDE.md, not .claude/CLAUDE.md, when root CLAUDE.md imports AGENTS.md', () => {
|
|
1033
|
+
// Root CLAUDE.md is committed even where .claude/ is ignored, so a fresh
|
|
1034
|
+
// clone still gives a session in a subfolder its copy.
|
|
1035
|
+
setupCorePackage(tmpDir, '2.0.0');
|
|
1036
|
+
fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), '# Claude\n\n@AGENTS.md\n');
|
|
1037
|
+
fs.mkdirSync(path.join(tmpDir, '.claude'));
|
|
1038
|
+
fs.writeFileSync(
|
|
1039
|
+
path.join(tmpDir, '.claude', 'CLAUDE.md'),
|
|
1040
|
+
'# Local\n\n<!-- ASTRYX:START -->\nAstryx v1.0.0 · old\n<!-- ASTRYX:END -->\n',
|
|
1041
|
+
);
|
|
1042
|
+
|
|
1043
|
+
expect(installAgentDocs(tmpDir)).toEqual([
|
|
1044
|
+
'AGENTS.md',
|
|
1045
|
+
'CLAUDE.md',
|
|
1046
|
+
'.claude/CLAUDE.md',
|
|
1047
|
+
'GEMINI.md',
|
|
1048
|
+
]);
|
|
1049
|
+
expect(countBlocks(tmpDir, ['CLAUDE.md'])).toBe(1);
|
|
1050
|
+
expect(fs.readFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), 'utf-8')).toBe(
|
|
1051
|
+
'# Local\n',
|
|
1052
|
+
);
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
it('gives a .claude/CLAUDE.md that imports AGENTS.md a copy when there is no root CLAUDE.md', () => {
|
|
1056
|
+
setupCorePackage(tmpDir);
|
|
1057
|
+
fs.writeFileSync(path.join(tmpDir, 'AGENTS.md'), '# Agents\n');
|
|
1058
|
+
fs.mkdirSync(path.join(tmpDir, '.claude'));
|
|
1059
|
+
fs.writeFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), '@../AGENTS.md\n');
|
|
1060
|
+
|
|
1061
|
+
expect(installAgentDocs(tmpDir)).toEqual(['AGENTS.md', '.claude/CLAUDE.md', 'GEMINI.md']);
|
|
1062
|
+
const content = fs.readFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), 'utf-8');
|
|
1063
|
+
expect(content.startsWith('@../AGENTS.md\n\n<!-- ASTRYX:START -->')).toBe(true);
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
it('onlyReplace: refreshes the copy in a Claude file that imports AGENTS.md instead of removing it', () => {
|
|
1067
|
+
setupCorePackage(tmpDir, '1.0.0');
|
|
1068
|
+
fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), '# Claude\n\n@AGENTS.md\n');
|
|
1069
|
+
installAgentDocs(tmpDir);
|
|
1070
|
+
setupCorePackage(tmpDir, '2.0.0');
|
|
1071
|
+
|
|
1072
|
+
expect(installAgentDocs(tmpDir, {onlyReplace: true})).toEqual(['AGENTS.md', 'CLAUDE.md']);
|
|
1073
|
+
const content = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8');
|
|
1074
|
+
expect(content).toContain('@AGENTS.md');
|
|
1075
|
+
expect(content).toContain('Astryx v2.0.0');
|
|
1076
|
+
expect(content).not.toContain('Astryx v1.0.0');
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
it('--agent all after the default keeps the GEMINI.md import and one Claude copy', () => {
|
|
1080
|
+
setupCorePackage(tmpDir);
|
|
1081
|
+
fs.writeFileSync(path.join(tmpDir, 'AGENTS.md'), '# Agents\n');
|
|
1082
|
+
fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), '# Claude\n');
|
|
1083
|
+
fs.mkdirSync(path.join(tmpDir, '.claude'));
|
|
1084
|
+
fs.writeFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), '# Local\n');
|
|
1085
|
+
installAgentDocs(tmpDir);
|
|
1086
|
+
|
|
1087
|
+
expect(installAgentDocs(tmpDir, {agent: 'all'})).toEqual(['AGENTS.md', 'CLAUDE.md']);
|
|
1088
|
+
expect(fs.readFileSync(path.join(tmpDir, 'GEMINI.md'), 'utf-8')).toBe(GEMINI_IMPORT);
|
|
1089
|
+
expect(fs.readFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), 'utf-8')).toBe(
|
|
1090
|
+
'# Local\n',
|
|
1091
|
+
);
|
|
1092
|
+
expect(blocksSeen(tmpDir)).toEqual(COPILOT_TWICE);
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
it('--agent claude writes CLAUDE.md and removes a block from .claude/CLAUDE.md', () => {
|
|
1096
|
+
setupCorePackage(tmpDir);
|
|
1097
|
+
fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), '# Claude\n');
|
|
1098
|
+
fs.mkdirSync(path.join(tmpDir, '.claude'));
|
|
1099
|
+
fs.writeFileSync(
|
|
1100
|
+
path.join(tmpDir, '.claude', 'CLAUDE.md'),
|
|
1101
|
+
'# Local\n\n<!-- ASTRYX:START -->\nAstryx v1.0.0 · old\n<!-- ASTRYX:END -->\n',
|
|
1102
|
+
);
|
|
1103
|
+
installAgentDocs(tmpDir);
|
|
1104
|
+
|
|
1105
|
+
expect(installAgentDocs(tmpDir, {agent: 'claude'})).toEqual(['CLAUDE.md']);
|
|
1106
|
+
expect(installAgentDocs(tmpDir, {agent: 'all'})).toEqual(['AGENTS.md', 'CLAUDE.md']);
|
|
1107
|
+
fs.writeFileSync(
|
|
1108
|
+
path.join(tmpDir, '.claude', 'CLAUDE.md'),
|
|
1109
|
+
'# Local\n\n<!-- ASTRYX:START -->\nAstryx v1.0.0 · old\n<!-- ASTRYX:END -->\n',
|
|
1110
|
+
);
|
|
1111
|
+
expect(installAgentDocs(tmpDir, {agent: 'claude'})).toEqual(['CLAUDE.md', '.claude/CLAUDE.md']);
|
|
1112
|
+
expect(fs.readFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), 'utf-8')).toBe(
|
|
1113
|
+
'# Local\n',
|
|
1114
|
+
);
|
|
1115
|
+
expect(blocksSeen(tmpDir).claude).toBe(1);
|
|
796
1116
|
});
|
|
797
1117
|
|
|
798
1118
|
it('respects --agent claude preset: finds existing CLAUDE.md', () => {
|
|
@@ -879,6 +1199,39 @@ describe('installAgentDocs', () => {
|
|
|
879
1199
|
expect(content).toContain('Other rules.');
|
|
880
1200
|
});
|
|
881
1201
|
|
|
1202
|
+
it('onlyReplace: refreshes both copies of the block and leaves the GEMINI.md import alone', () => {
|
|
1203
|
+
setupCorePackage(tmpDir, '1.0.0');
|
|
1204
|
+
fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), '# Claude\n');
|
|
1205
|
+
installAgentDocs(tmpDir);
|
|
1206
|
+
setupCorePackage(tmpDir, '2.0.0');
|
|
1207
|
+
|
|
1208
|
+
expect(installAgentDocs(tmpDir, {onlyReplace: true})).toEqual([
|
|
1209
|
+
'AGENTS.md',
|
|
1210
|
+
'CLAUDE.md',
|
|
1211
|
+
]);
|
|
1212
|
+
for (const rel of ['AGENTS.md', 'CLAUDE.md']) {
|
|
1213
|
+
const content = fs.readFileSync(path.join(tmpDir, rel), 'utf-8');
|
|
1214
|
+
expect(content).toContain('Astryx v2.0.0');
|
|
1215
|
+
expect(content).not.toContain('Astryx v1.0.0');
|
|
1216
|
+
}
|
|
1217
|
+
expect(fs.readFileSync(path.join(tmpDir, 'GEMINI.md'), 'utf-8')).toBe(GEMINI_IMPORT);
|
|
1218
|
+
expect(blocksSeen(tmpDir)).toEqual(COPILOT_TWICE);
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
it('onlyReplace: refreshes a block beside an import of a missing AGENTS.md', () => {
|
|
1222
|
+
setupCorePackage(tmpDir, '2.0.0');
|
|
1223
|
+
fs.writeFileSync(
|
|
1224
|
+
path.join(tmpDir, 'CLAUDE.md'),
|
|
1225
|
+
'@AGENTS.md\n\n<!-- ASTRYX:START -->\nAstryx v1.0.0\n<!-- ASTRYX:END -->\n',
|
|
1226
|
+
);
|
|
1227
|
+
|
|
1228
|
+
expect(installAgentDocs(tmpDir, {onlyReplace: true})).toEqual(['CLAUDE.md']);
|
|
1229
|
+
expect(fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8')).toContain(
|
|
1230
|
+
'Astryx v2.0.0',
|
|
1231
|
+
);
|
|
1232
|
+
expect(fs.existsSync(path.join(tmpDir, 'AGENTS.md'))).toBe(false);
|
|
1233
|
+
});
|
|
1234
|
+
|
|
882
1235
|
it('onlyReplace: does not create the default AGENTS.md when nothing exists', () => {
|
|
883
1236
|
setupCorePackage(tmpDir);
|
|
884
1237
|
|
|
@@ -1084,6 +1437,25 @@ describe('inspectAgentDocs', () => {
|
|
|
1084
1437
|
expect(res.staleFiles).toEqual(['CLAUDE.md']);
|
|
1085
1438
|
});
|
|
1086
1439
|
|
|
1440
|
+
it('checks both copies of the block and does not count the GEMINI.md import', () => {
|
|
1441
|
+
const core = path.join(tmpDir, 'node_modules', '@astryxdesign', 'core');
|
|
1442
|
+
fs.mkdirSync(core, {recursive: true});
|
|
1443
|
+
fs.writeFileSync(path.join(core, 'package.json'), JSON.stringify({version: '3.0.0'}));
|
|
1444
|
+
fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), '# Claude\n');
|
|
1445
|
+
installAgentDocs(tmpDir);
|
|
1446
|
+
|
|
1447
|
+
const current = inspectAgentDocs(tmpDir, '3.0.0');
|
|
1448
|
+
expect(current.status).toBe('current');
|
|
1449
|
+
expect(current.files.map(f => f.path)).toEqual(['AGENTS.md', 'CLAUDE.md']);
|
|
1450
|
+
expect(inspectAgentDocs(tmpDir, '4.0.0').staleFiles).toEqual(['AGENTS.md', 'CLAUDE.md']);
|
|
1451
|
+
|
|
1452
|
+
// Without the blocks, the import alone reads as not set up.
|
|
1453
|
+
fs.rmSync(path.join(tmpDir, 'AGENTS.md'));
|
|
1454
|
+
fs.rmSync(path.join(tmpDir, 'CLAUDE.md'));
|
|
1455
|
+
expect(inspectAgentDocs(tmpDir, '3.0.0').status).toBe('missing');
|
|
1456
|
+
expect(isAstryxInitialized(tmpDir)).toBe(false);
|
|
1457
|
+
});
|
|
1458
|
+
|
|
1087
1459
|
it('defaults the installed version to the core package when omitted', () => {
|
|
1088
1460
|
const coreDir = path.join(tmpDir, 'node_modules', '@astryxdesign', 'core');
|
|
1089
1461
|
fs.mkdirSync(coreDir, {recursive: true});
|
|
@@ -1179,3 +1551,275 @@ describe('isAstryxInitialized — resilient to malformed markers (postinstall ho
|
|
|
1179
1551
|
expect(isAstryxInitialized(path.join(tmpDir, 'does-not-exist'))).toBe(false);
|
|
1180
1552
|
});
|
|
1181
1553
|
});
|
|
1554
|
+
|
|
1555
|
+
describe('removeAgentDocs after the default layout', () => {
|
|
1556
|
+
beforeEach(() => {
|
|
1557
|
+
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
1558
|
+
});
|
|
1559
|
+
|
|
1560
|
+
it('deletes the files the default created and keeps the project files', () => {
|
|
1561
|
+
const core = path.join(tmpDir, 'node_modules', '@astryxdesign', 'core');
|
|
1562
|
+
fs.mkdirSync(core, {recursive: true});
|
|
1563
|
+
fs.writeFileSync(path.join(core, 'package.json'), JSON.stringify({version: '1.0.0'}));
|
|
1564
|
+
fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), '# Claude\n');
|
|
1565
|
+
installAgentDocs(tmpDir);
|
|
1566
|
+
|
|
1567
|
+
expect(removeAgentDocs(tmpDir)).toEqual(['AGENTS.md', 'CLAUDE.md', 'GEMINI.md']);
|
|
1568
|
+
expect(fs.existsSync(path.join(tmpDir, 'AGENTS.md'))).toBe(false);
|
|
1569
|
+
expect(fs.existsSync(path.join(tmpDir, 'GEMINI.md'))).toBe(false);
|
|
1570
|
+
expect(fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8')).toBe('# Claude\n');
|
|
1571
|
+
});
|
|
1572
|
+
|
|
1573
|
+
it('keeps a GEMINI.md that has the project\'s own content', () => {
|
|
1574
|
+
fs.writeFileSync(
|
|
1575
|
+
path.join(tmpDir, 'GEMINI.md'),
|
|
1576
|
+
`# Gemini\n\nUse tabs.\n\n${GEMINI_IMPORT}`,
|
|
1577
|
+
);
|
|
1578
|
+
|
|
1579
|
+
expect(removeAgentDocs(tmpDir)).toEqual(['GEMINI.md']);
|
|
1580
|
+
expect(fs.readFileSync(path.join(tmpDir, 'GEMINI.md'), 'utf-8')).toBe(
|
|
1581
|
+
'# Gemini\n\nUse tabs.\n',
|
|
1582
|
+
);
|
|
1583
|
+
});
|
|
1584
|
+
});
|
|
1585
|
+
|
|
1586
|
+
describe('agent docs never go into the home folder', () => {
|
|
1587
|
+
const saved = {HOME: process.env.HOME, USERPROFILE: process.env.USERPROFILE};
|
|
1588
|
+
const personal = '# My notes\n\nAlways use tabs.\n';
|
|
1589
|
+
|
|
1590
|
+
beforeEach(() => {
|
|
1591
|
+
process.env.HOME = tmpDir;
|
|
1592
|
+
process.env.USERPROFILE = tmpDir;
|
|
1593
|
+
fs.mkdirSync(path.join(tmpDir, '.claude'), {recursive: true});
|
|
1594
|
+
fs.writeFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), personal);
|
|
1595
|
+
});
|
|
1596
|
+
afterEach(() => {
|
|
1597
|
+
for (const [key, value] of Object.entries(saved)) {
|
|
1598
|
+
if (value === undefined) delete process.env[key];
|
|
1599
|
+
else process.env[key] = value;
|
|
1600
|
+
}
|
|
1601
|
+
});
|
|
1602
|
+
|
|
1603
|
+
it.each([
|
|
1604
|
+
['the default', {}],
|
|
1605
|
+
['a preset', {agent: 'claude'}],
|
|
1606
|
+
['an explicit path', {paths: ['AGENTS.md']}],
|
|
1607
|
+
['a refresh', {onlyReplace: true}],
|
|
1608
|
+
])('refuses %s and writes nothing', (_label, options) => {
|
|
1609
|
+
expect(() => installAgentDocs(tmpDir, options)).toThrow(AgentDocsHomeError);
|
|
1610
|
+
expect(fs.readFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), 'utf-8')).toBe(
|
|
1611
|
+
personal,
|
|
1612
|
+
);
|
|
1613
|
+
expect(fs.readdirSync(tmpDir).sort()).toEqual(['.claude']);
|
|
1614
|
+
});
|
|
1615
|
+
|
|
1616
|
+
it('writes an explicit path into a plain folder of the home folder, as 0.7.x did', () => {
|
|
1617
|
+
expect(installAgentDocs(tmpDir, {paths: ['notes/AI.md']})).toEqual(['notes/AI.md']);
|
|
1618
|
+
expect(fs.readFileSync(path.join(tmpDir, 'notes', 'AI.md'), 'utf-8')).toContain(
|
|
1619
|
+
'<!-- ASTRYX:START -->',
|
|
1620
|
+
);
|
|
1621
|
+
});
|
|
1622
|
+
|
|
1623
|
+
it.each([
|
|
1624
|
+
['a file in the home folder', ['AI.md']],
|
|
1625
|
+
['a file at the top of a settings folder', ['.claude/CLAUDE.md']],
|
|
1626
|
+
['one of several paths', ['notes/AI.md', 'AI.md']],
|
|
1627
|
+
])('refuses an explicit path to %s, and writes nothing', (_label, paths) => {
|
|
1628
|
+
expect(() => installAgentDocs(tmpDir, {paths})).toThrow(AgentDocsHomeError);
|
|
1629
|
+
expect(fs.readFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), 'utf-8')).toBe(
|
|
1630
|
+
personal,
|
|
1631
|
+
);
|
|
1632
|
+
expect(fs.readdirSync(tmpDir).sort()).toEqual(['.claude']);
|
|
1633
|
+
});
|
|
1634
|
+
|
|
1635
|
+
it('init with an explicit path from the home folder writes it, and refuses one in the home folder', async () => {
|
|
1636
|
+
const {init} = await import('../../api/init/init.mjs');
|
|
1637
|
+
const written = await init(
|
|
1638
|
+
{features: ['agents'], agentDocsPath: 'notes/AI.md'},
|
|
1639
|
+
{cwd: tmpDir},
|
|
1640
|
+
);
|
|
1641
|
+
expect(written.data.docsWritten).toEqual(['notes/AI.md']);
|
|
1642
|
+
expect(written.data.docsError ?? null).toBeNull();
|
|
1643
|
+
const refused = await init(
|
|
1644
|
+
{features: ['agents'], agentDocsPath: 'AI.md'},
|
|
1645
|
+
{cwd: tmpDir},
|
|
1646
|
+
);
|
|
1647
|
+
expect(refused.data.docsError).toMatchObject({kind: 'install-failed'});
|
|
1648
|
+
expect(fs.existsSync(path.join(tmpDir, 'AI.md'))).toBe(false);
|
|
1649
|
+
});
|
|
1650
|
+
|
|
1651
|
+
it('refuses through a symlink to the home folder', () => {
|
|
1652
|
+
const link = `${tmpDir}-link`;
|
|
1653
|
+
fs.symlinkSync(tmpDir, link);
|
|
1654
|
+
try {
|
|
1655
|
+
expect(() => installAgentDocs(link)).toThrow(AgentDocsHomeError);
|
|
1656
|
+
} finally {
|
|
1657
|
+
fs.rmSync(link);
|
|
1658
|
+
}
|
|
1659
|
+
});
|
|
1660
|
+
|
|
1661
|
+
it('still removes a block an older run wrote there', () => {
|
|
1662
|
+
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
1663
|
+
fs.writeFileSync(
|
|
1664
|
+
path.join(tmpDir, '.claude', 'CLAUDE.md'),
|
|
1665
|
+
`${personal}\n<!-- ASTRYX:START -->\nAstryx v1.0.0\n<!-- ASTRYX:END -->\n`,
|
|
1666
|
+
);
|
|
1667
|
+
|
|
1668
|
+
expect(removeAgentDocs(tmpDir)).toEqual(['.claude/CLAUDE.md']);
|
|
1669
|
+
expect(fs.readFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), 'utf-8')).toBe(
|
|
1670
|
+
personal,
|
|
1671
|
+
);
|
|
1672
|
+
});
|
|
1673
|
+
|
|
1674
|
+
it.each(['.gemini', '.claude', '.codex', path.join('.gemini', 'extensions', 'x')])(
|
|
1675
|
+
'refuses the settings folder ~/%s',
|
|
1676
|
+
folder => {
|
|
1677
|
+
const dir = path.join(tmpDir, folder);
|
|
1678
|
+
fs.mkdirSync(dir, {recursive: true});
|
|
1679
|
+
const before = fs.readdirSync(dir);
|
|
1680
|
+
expect(() => installAgentDocs(dir)).toThrow(AgentDocsHomeError);
|
|
1681
|
+
expect(fs.readdirSync(dir)).toEqual(before);
|
|
1682
|
+
expect(fs.readFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), 'utf-8')).toBe(
|
|
1683
|
+
personal,
|
|
1684
|
+
);
|
|
1685
|
+
},
|
|
1686
|
+
);
|
|
1687
|
+
|
|
1688
|
+
it('writes in a project folder inside the home folder', () => {
|
|
1689
|
+
const project = path.join(tmpDir, 'app');
|
|
1690
|
+
fs.mkdirSync(project);
|
|
1691
|
+
expect(installAgentDocs(project)).toEqual(['AGENTS.md', 'GEMINI.md']);
|
|
1692
|
+
});
|
|
1693
|
+
|
|
1694
|
+
/**
|
|
1695
|
+
* A git project that declares Astryx, at `rel` under the home folder, with
|
|
1696
|
+
* Core installed at `version`.
|
|
1697
|
+
* @param {string} rel
|
|
1698
|
+
* @param {string} version
|
|
1699
|
+
*/
|
|
1700
|
+
function writeProject(rel, version) {
|
|
1701
|
+
const project = path.join(tmpDir, rel);
|
|
1702
|
+
fs.mkdirSync(path.join(project, 'src'), {recursive: true});
|
|
1703
|
+
fs.writeFileSync(path.join(project, '.git'), 'gitdir: /elsewhere/.git/worktrees/web\n');
|
|
1704
|
+
fs.writeFileSync(
|
|
1705
|
+
path.join(project, 'package.json'),
|
|
1706
|
+
JSON.stringify({name: 'web', dependencies: {'@astryxdesign/core': version}}),
|
|
1707
|
+
);
|
|
1708
|
+
const core = path.join(project, 'node_modules', '@astryxdesign', 'core');
|
|
1709
|
+
fs.mkdirSync(core, {recursive: true});
|
|
1710
|
+
fs.writeFileSync(path.join(core, 'package.json'), JSON.stringify({version}));
|
|
1711
|
+
return project;
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
it.each([
|
|
1715
|
+
['a Codex worktree', path.join('.codex', 'worktrees', 'a1b2c3', 'web')],
|
|
1716
|
+
['a Cursor worktree', path.join('.cursor', 'worktrees', 'web', 'x9y8')],
|
|
1717
|
+
['a project with its package.json one level in', path.join('.config', 'web')],
|
|
1718
|
+
])('writes and refreshes the docs of a project inside a settings folder (%s)', (_label, rel) => {
|
|
1719
|
+
const project = writeProject(rel, '1.0.0');
|
|
1720
|
+
|
|
1721
|
+
expect(resolveAgentDocsRoot(path.join(project, 'src'))).toBe(project);
|
|
1722
|
+
expect(installAgentDocs(project)).toEqual(['AGENTS.md', 'GEMINI.md']);
|
|
1723
|
+
|
|
1724
|
+
// upgrade refreshes the block in place.
|
|
1725
|
+
writeProject(rel, '2.0.0');
|
|
1726
|
+
expect(installAgentDocs(project, {onlyReplace: true})).toEqual(['AGENTS.md']);
|
|
1727
|
+
expect(fs.readFileSync(path.join(project, 'AGENTS.md'), 'utf-8')).toContain(
|
|
1728
|
+
'Astryx v2.0.0',
|
|
1729
|
+
);
|
|
1730
|
+
|
|
1731
|
+
// Nothing reaches the settings folder or the home folder.
|
|
1732
|
+
const settings = path.join(tmpDir, rel.split(path.sep)[0]);
|
|
1733
|
+
expect(fs.readdirSync(settings)).toEqual([rel.split(path.sep)[1]]);
|
|
1734
|
+
expect(fs.readFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), 'utf-8')).toBe(
|
|
1735
|
+
personal,
|
|
1736
|
+
);
|
|
1737
|
+
});
|
|
1738
|
+
|
|
1739
|
+
it('refuses the top of a settings folder even with a package.json, and the folders in it outside a project', () => {
|
|
1740
|
+
// A package.json in ~/.claude (for hook scripts, say) would otherwise put the
|
|
1741
|
+
// block in ~/.claude/CLAUDE.md, which Claude Code reads in every project.
|
|
1742
|
+
fs.writeFileSync(path.join(tmpDir, '.claude', 'package.json'), '{"name":"hooks"}\n');
|
|
1743
|
+
fs.mkdirSync(path.join(tmpDir, '.claude', 'hooks'));
|
|
1744
|
+
|
|
1745
|
+
expect(() => installAgentDocs(path.join(tmpDir, '.claude'))).toThrow(AgentDocsHomeError);
|
|
1746
|
+
expect(() => installAgentDocs(path.join(tmpDir, '.claude', 'hooks'))).toThrow(
|
|
1747
|
+
AgentDocsHomeError,
|
|
1748
|
+
);
|
|
1749
|
+
expect(resolveAgentDocsRoot(path.join(tmpDir, '.claude', 'hooks'))).toBe(
|
|
1750
|
+
path.join(tmpDir, '.claude'),
|
|
1751
|
+
);
|
|
1752
|
+
expect(fs.readFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), 'utf-8')).toBe(
|
|
1753
|
+
personal,
|
|
1754
|
+
);
|
|
1755
|
+
|
|
1756
|
+
// A worktree folder whose project has no package.json is not a project.
|
|
1757
|
+
const bare = path.join(tmpDir, '.codex', 'worktrees', 'a1b2c3', 'notes');
|
|
1758
|
+
fs.mkdirSync(bare, {recursive: true});
|
|
1759
|
+
expect(() => installAgentDocs(bare)).toThrow(AgentDocsHomeError);
|
|
1760
|
+
expect(fs.readdirSync(bare)).toEqual([]);
|
|
1761
|
+
});
|
|
1762
|
+
});
|
|
1763
|
+
|
|
1764
|
+
describe('resolveAgentDocsRoot', () => {
|
|
1765
|
+
/** @param {string} dir @param {object} pkg */
|
|
1766
|
+
const writePkg = (dir, pkg) => {
|
|
1767
|
+
fs.mkdirSync(dir, {recursive: true});
|
|
1768
|
+
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify(pkg));
|
|
1769
|
+
};
|
|
1770
|
+
const astryx = {devDependencies: {'@astryxdesign/cli': '0.7.0'}};
|
|
1771
|
+
|
|
1772
|
+
it('finds the nearest folder whose package.json declares Astryx', () => {
|
|
1773
|
+
writePkg(tmpDir, astryx);
|
|
1774
|
+
const sub = path.join(tmpDir, 'src', 'app');
|
|
1775
|
+
fs.mkdirSync(sub, {recursive: true});
|
|
1776
|
+
expect(resolveAgentDocsRoot(sub)).toBe(tmpDir);
|
|
1777
|
+
expect(resolveAgentDocsRoot(tmpDir)).toBe(tmpDir);
|
|
1778
|
+
|
|
1779
|
+
// A nested package that does not declare Astryx is passed over.
|
|
1780
|
+
const functions = path.join(tmpDir, 'functions');
|
|
1781
|
+
writePkg(functions, {name: 'functions'});
|
|
1782
|
+
fs.mkdirSync(path.join(functions, 'src'));
|
|
1783
|
+
expect(resolveAgentDocsRoot(path.join(functions, 'src'))).toBe(tmpDir);
|
|
1784
|
+
|
|
1785
|
+
// A nested package that declares Astryx is its own project.
|
|
1786
|
+
const web = path.join(tmpDir, 'apps', 'web');
|
|
1787
|
+
writePkg(web, {dependencies: {'@astryxdesign/core': '0.7.0'}});
|
|
1788
|
+
expect(resolveAgentDocsRoot(web)).toBe(web);
|
|
1789
|
+
});
|
|
1790
|
+
|
|
1791
|
+
it('falls back to the nearest package.json when none declares Astryx', () => {
|
|
1792
|
+
writePkg(tmpDir, {name: 'app'});
|
|
1793
|
+
const pkg = path.join(tmpDir, 'packages', 'web');
|
|
1794
|
+
writePkg(pkg, {name: 'web'});
|
|
1795
|
+
fs.mkdirSync(path.join(pkg, 'src'));
|
|
1796
|
+
expect(resolveAgentDocsRoot(path.join(pkg, 'src'))).toBe(pkg);
|
|
1797
|
+
});
|
|
1798
|
+
|
|
1799
|
+
it('stops at a repository root, and finds no project without a package.json', () => {
|
|
1800
|
+
writePkg(tmpDir, astryx);
|
|
1801
|
+
const repo = path.join(tmpDir, 'repo');
|
|
1802
|
+
fs.mkdirSync(path.join(repo, '.git'), {recursive: true});
|
|
1803
|
+
const sub = path.join(repo, 'docs');
|
|
1804
|
+
fs.mkdirSync(sub);
|
|
1805
|
+
expect(resolveAgentDocsRoot(sub)).toBeNull();
|
|
1806
|
+
});
|
|
1807
|
+
|
|
1808
|
+
it('never returns the home folder', () => {
|
|
1809
|
+
const saved = {HOME: process.env.HOME, USERPROFILE: process.env.USERPROFILE};
|
|
1810
|
+
process.env.HOME = tmpDir;
|
|
1811
|
+
process.env.USERPROFILE = tmpDir;
|
|
1812
|
+
try {
|
|
1813
|
+
writePkg(tmpDir, astryx);
|
|
1814
|
+
const scratch = path.join(tmpDir, 'scratch');
|
|
1815
|
+
fs.mkdirSync(scratch);
|
|
1816
|
+
expect(resolveAgentDocsRoot(scratch)).toBeNull();
|
|
1817
|
+
expect(resolveAgentDocsRoot(tmpDir)).toBeNull();
|
|
1818
|
+
} finally {
|
|
1819
|
+
for (const [key, value] of Object.entries(saved)) {
|
|
1820
|
+
if (value === undefined) delete process.env[key];
|
|
1821
|
+
else process.env[key] = value;
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
});
|
|
1825
|
+
});
|