@astryxdesign/cli 0.7.2 → 0.7.3-canary.06872b4
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 +99 -66
- package/api/build/_adapter.d.mts +58 -0
- package/api/build/_adapter.mjs +115 -3
- package/api/build/blocks/blocks.d.mts +42 -0
- package/api/build/blocks/blocks.mjs +169 -0
- package/api/build/blocks/blocks.test.mjs +108 -0
- package/api/build/blocks/blocks.weights.json +1 -0
- package/api/build/build.mjs +25 -6
- package/api/build/kit/answer.d.mts +14 -0
- package/api/build/kit/answer.mjs +85 -0
- package/api/build/kit/answer.test.mjs +38 -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 +60 -0
- package/api/build/kit/signals.mjs +284 -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 +52 -0
- package/api/build/tree.test.mjs +177 -0
- package/api/build/tree.weights.json +1 -0
- package/api/component/component.mjs +16 -2
- package/api/component/component.test.mjs +15 -0
- package/api/discover/_adapter.mjs +36 -9
- package/api/discover/_adapter.test.mjs +42 -0
- package/api/docs/_adapter.mjs +17 -5
- package/api/docs/docs.test.mjs +7 -1
- package/api/doctor/doctor.mjs +86 -12
- package/api/doctor/doctor.test.mjs +28 -3
- 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/authoring-checks.mjs +83 -9
- 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-gaps.test.mjs +1964 -0
- package/api/integration/validate-integration.d.mts +11 -2
- package/api/integration/validate-integration.mjs +226 -32
- package/api/integration/validate-integration.type.d.mts +58 -1
- package/api/integration/validate-integration.type.mjs +45 -2
- package/api/integration/validateIntegration.doc.mjs +1 -1
- package/api/search/ORDERING.md +28 -0
- package/api/search/_adapter.d.mts +66 -2
- package/api/search/_adapter.mjs +118 -10
- 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 +52 -0
- package/api/search/_entries.mjs +158 -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 +7 -5
- package/api/search/search.mjs +53 -2483
- 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/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/is-my-code-good.doc.mjs +287 -0
- package/assets/docs/tree/api.doc.mjs +6 -0
- package/assets/docs/tree/host-entry.doc.mjs +128 -0
- package/assets/docs/tree/template-grading-rubric.doc.mjs +16 -37
- package/assets/docs/tree/tokens-data.doc.mjs +2 -2
- 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/clients/cli/bin/astryx.mjs +7 -78
- package/clients/cli/commands/doctor.mjs +30 -0
- package/clients/cli/commands/doctor.test.mjs +65 -0
- 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/outside-project.test.mjs +2 -1
- package/clients/cli/commands/setup-nudge.test.mjs +61 -0
- 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 +19 -1
- package/clients/cli/run.mjs +108 -0
- package/foundation/agent-docs/agent-doc-state.d.mts +20 -7
- package/foundation/agent-docs/agent-doc-state.mjs +35 -8
- package/foundation/agent-docs/agent-docs.d.mts +100 -12
- package/foundation/agent-docs/agent-docs.mjs +1010 -99
- package/foundation/agent-docs/agent-docs.path-safety.test.mjs +1239 -17
- package/foundation/agent-docs/agent-docs.test.mjs +678 -35
- package/foundation/config/project.mjs +20 -22
- package/foundation/config/project.test.mjs +56 -0
- 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/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 +8 -0
- package/foundation/doc-compiler/read.mjs +59 -1
- package/foundation/doc-compiler/read.test.mjs +11 -2
- 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/integrations/autolink.d.mts +2 -1
- package/foundation/integrations/autolink.mjs +4 -13
- 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/integrations.d.mts +27 -1
- package/foundation/integrations/integrations.mjs +61 -2
- package/foundation/integrations/validate-contributions.d.mts +115 -0
- package/foundation/integrations/validate-contributions.mjs +1168 -13
- package/foundation/release/handoff.d.mts +23 -4
- package/foundation/release/handoff.mjs +175 -8
- 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/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
|
});
|
|
@@ -301,7 +355,8 @@ describe('renderAgentDocsBlock', () => {
|
|
|
301
355
|
'utf-8',
|
|
302
356
|
);
|
|
303
357
|
expect(firstAgents).toContain(renderedBlock);
|
|
304
|
-
expect(firstClaude).
|
|
358
|
+
expect(firstClaude).toBe(`# Claude\n\n${renderedBlock}\n`);
|
|
359
|
+
expect(fs.existsSync(path.join(projectDir, '.claude'))).toBe(false);
|
|
305
360
|
|
|
306
361
|
installAgentDocs(projectDir, {renderedBlock});
|
|
307
362
|
expect(fs.readFileSync(path.join(projectDir, 'AGENTS.md'), 'utf-8')).toBe(
|
|
@@ -655,24 +710,27 @@ describe('installAgentDocs', () => {
|
|
|
655
710
|
);
|
|
656
711
|
}
|
|
657
712
|
|
|
658
|
-
it('creates AGENTS.md when no agent docs exist
|
|
713
|
+
it('creates AGENTS.md, plus the GEMINI.md import, when no agent docs exist', () => {
|
|
659
714
|
setupCorePackage(tmpDir);
|
|
660
715
|
|
|
661
716
|
const written = installAgentDocs(tmpDir);
|
|
662
717
|
|
|
663
|
-
expect(written).toEqual(['AGENTS.md']);
|
|
718
|
+
expect(written).toEqual(['AGENTS.md', 'GEMINI.md']);
|
|
664
719
|
expect(fs.existsSync(path.join(tmpDir, 'AGENTS.md'))).toBe(true);
|
|
665
|
-
//
|
|
720
|
+
// Claude Code reads AGENTS.md when the project has no CLAUDE.md, so no
|
|
721
|
+
// Claude file is created.
|
|
666
722
|
expect(fs.existsSync(path.join(tmpDir, '.claude', 'CLAUDE.md'))).toBe(false);
|
|
723
|
+
expect(fs.existsSync(path.join(tmpDir, 'CLAUDE.md'))).toBe(false);
|
|
667
724
|
const content = fs.readFileSync(path.join(tmpDir, 'AGENTS.md'), 'utf-8');
|
|
668
725
|
expect(content).toContain('# AGENTS.md');
|
|
669
726
|
expect(content).toContain('<!-- ASTRYX:START -->');
|
|
727
|
+
expect(fs.readFileSync(path.join(tmpDir, 'GEMINI.md'), 'utf-8')).toBe(GEMINI_IMPORT);
|
|
670
728
|
});
|
|
671
729
|
|
|
672
730
|
it('defaults to AGENTS.md but writes .claude/CLAUDE.md only when --agent claude is explicit', () => {
|
|
673
731
|
// Default (no agent): tool-agnostic AGENTS.md, never the Claude file.
|
|
674
732
|
setupCorePackage(tmpDir);
|
|
675
|
-
expect(installAgentDocs(tmpDir)).toEqual(['AGENTS.md']);
|
|
733
|
+
expect(installAgentDocs(tmpDir)).toEqual(['AGENTS.md', 'GEMINI.md']);
|
|
676
734
|
expect(fs.existsSync(path.join(tmpDir, '.claude', 'CLAUDE.md'))).toBe(false);
|
|
677
735
|
|
|
678
736
|
// Explicit Claude: the Claude-specific file, in a fresh project.
|
|
@@ -686,46 +744,193 @@ describe('installAgentDocs', () => {
|
|
|
686
744
|
}
|
|
687
745
|
});
|
|
688
746
|
|
|
689
|
-
it('
|
|
747
|
+
it('writes the block to AGENTS.md and to root CLAUDE.md when the project has only CLAUDE.md', () => {
|
|
748
|
+
// Codex reads only AGENTS.md, and Claude Code skips AGENTS.md beside a
|
|
749
|
+
// CLAUDE.md. The copy goes in the committed root CLAUDE.md; init creates
|
|
750
|
+
// no .claude/ folder.
|
|
690
751
|
setupCorePackage(tmpDir);
|
|
691
752
|
fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), '# Claude\n\nProject rules.\n');
|
|
692
753
|
|
|
693
754
|
const written = installAgentDocs(tmpDir);
|
|
694
755
|
|
|
695
|
-
expect(written).toEqual(['CLAUDE.md']);
|
|
696
|
-
expect(
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
756
|
+
expect(written).toEqual(['AGENTS.md', 'CLAUDE.md', 'GEMINI.md']);
|
|
757
|
+
expect(
|
|
758
|
+
fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8').startsWith(
|
|
759
|
+
'# Claude\n\nProject rules.\n\n<!-- ASTRYX:START -->',
|
|
760
|
+
),
|
|
761
|
+
).toBe(true);
|
|
762
|
+
expect(fs.existsSync(path.join(tmpDir, '.claude'))).toBe(false);
|
|
763
|
+
expect(fs.readFileSync(path.join(tmpDir, 'GEMINI.md'), 'utf-8')).toBe(GEMINI_IMPORT);
|
|
764
|
+
expect(blocksSeen(tmpDir)).toEqual(COPILOT_TWICE);
|
|
700
765
|
});
|
|
701
766
|
|
|
702
|
-
it('
|
|
767
|
+
it('keeps the block in root CLAUDE.md when AGENTS.md and CLAUDE.md both exist', () => {
|
|
703
768
|
setupCorePackage(tmpDir);
|
|
704
769
|
fs.writeFileSync(path.join(tmpDir, 'AGENTS.md'), '# Agents\n\nAgent rules.\n');
|
|
705
770
|
fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), '# Claude\n\nClaude rules.\n');
|
|
706
771
|
|
|
707
772
|
const written = installAgentDocs(tmpDir);
|
|
708
773
|
|
|
709
|
-
expect(written).
|
|
710
|
-
expect(written).toContain('CLAUDE.md');
|
|
774
|
+
expect(written).toEqual(['AGENTS.md', 'CLAUDE.md', 'GEMINI.md']);
|
|
711
775
|
const agentsContent = fs.readFileSync(path.join(tmpDir, 'AGENTS.md'), 'utf-8');
|
|
712
|
-
|
|
776
|
+
expect(agentsContent).toContain('Agent rules.');
|
|
713
777
|
expect(agentsContent).toContain('<!-- ASTRYX:START -->');
|
|
714
|
-
expect(
|
|
778
|
+
expect(fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8')).toContain(
|
|
779
|
+
'Claude rules.',
|
|
780
|
+
);
|
|
781
|
+
expect(blocksSeen(tmpDir)).toEqual(COPILOT_TWICE);
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
it('refreshes a block an older run wrote into CLAUDE.md and adds one to AGENTS.md', () => {
|
|
785
|
+
setupCorePackage(tmpDir, '2.0.0');
|
|
786
|
+
fs.writeFileSync(
|
|
787
|
+
path.join(tmpDir, 'CLAUDE.md'),
|
|
788
|
+
'# Claude\n\nRules.\n\n<!-- ASTRYX:START -->\nAstryx v1.0.0 · old\n<!-- ASTRYX:END -->\n\nMore rules.\n',
|
|
789
|
+
);
|
|
790
|
+
|
|
791
|
+
const written = installAgentDocs(tmpDir);
|
|
792
|
+
|
|
793
|
+
expect(written).toEqual(['AGENTS.md', 'CLAUDE.md', 'GEMINI.md']);
|
|
794
|
+
const claude = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8');
|
|
795
|
+
expect(claude).toContain('Astryx v2.0.0');
|
|
796
|
+
expect(claude).not.toContain('Astryx v1.0.0');
|
|
797
|
+
expect(claude).toContain('More rules.');
|
|
798
|
+
expect(fs.readFileSync(path.join(tmpDir, 'AGENTS.md'), 'utf-8')).toContain(
|
|
799
|
+
'Astryx v2.0.0',
|
|
800
|
+
);
|
|
801
|
+
expect(blocksSeen(tmpDir)).toEqual(COPILOT_TWICE);
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
it('gives Claude Code one copy when CLAUDE.md and .claude/CLAUDE.md both exist', () => {
|
|
805
|
+
// The copy goes in root CLAUDE.md, which projects commit even when they
|
|
806
|
+
// ignore .claude/; a block an older run left in .claude/CLAUDE.md goes.
|
|
807
|
+
setupCorePackage(tmpDir, '2.0.0');
|
|
808
|
+
fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), '# Claude\n');
|
|
809
|
+
fs.mkdirSync(path.join(tmpDir, '.claude'));
|
|
810
|
+
fs.writeFileSync(
|
|
811
|
+
path.join(tmpDir, '.claude', 'CLAUDE.md'),
|
|
812
|
+
'# Local\n\n<!-- ASTRYX:START -->\nAstryx v1.0.0 · old\n<!-- ASTRYX:END -->\n',
|
|
813
|
+
);
|
|
814
|
+
|
|
815
|
+
const written = installAgentDocs(tmpDir);
|
|
816
|
+
|
|
817
|
+
expect(written).toEqual(['AGENTS.md', 'CLAUDE.md', '.claude/CLAUDE.md', 'GEMINI.md']);
|
|
818
|
+
expect(fs.readFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), 'utf-8')).toBe(
|
|
819
|
+
'# Local\n',
|
|
820
|
+
);
|
|
821
|
+
expect(blocksSeen(tmpDir)).toEqual(COPILOT_TWICE);
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
it('writes the same bytes when run again', () => {
|
|
825
|
+
setupCorePackage(tmpDir);
|
|
826
|
+
fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), '# Claude\n');
|
|
827
|
+
installAgentDocs(tmpDir);
|
|
828
|
+
const files = ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md'];
|
|
829
|
+
const first = files.map(f => fs.readFileSync(path.join(tmpDir, f), 'utf-8'));
|
|
830
|
+
|
|
831
|
+
expect(installAgentDocs(tmpDir)).toEqual(files);
|
|
832
|
+
expect(files.map(f => fs.readFileSync(path.join(tmpDir, f), 'utf-8'))).toEqual(first);
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
it('adds no import when GEMINI.md already imports AGENTS.md inline', () => {
|
|
836
|
+
setupCorePackage(tmpDir);
|
|
837
|
+
const gemini = '# Gemini\n\nFollow @./AGENTS.md always.\n';
|
|
838
|
+
fs.writeFileSync(path.join(tmpDir, 'GEMINI.md'), gemini);
|
|
839
|
+
|
|
840
|
+
expect(installAgentDocs(tmpDir)).toEqual(['AGENTS.md']);
|
|
841
|
+
expect(fs.readFileSync(path.join(tmpDir, 'GEMINI.md'), 'utf-8')).toBe(gemini);
|
|
842
|
+
expect(blocksSeen(tmpDir)).toEqual(ONCE_EACH);
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
it('removes its own import when GEMINI.md also imports AGENTS.md', () => {
|
|
846
|
+
setupCorePackage(tmpDir);
|
|
847
|
+
fs.writeFileSync(
|
|
848
|
+
path.join(tmpDir, 'GEMINI.md'),
|
|
849
|
+
`# Gemini\n\nSee @AGENTS.md first.\n\n${GEMINI_IMPORT}`,
|
|
850
|
+
);
|
|
851
|
+
|
|
852
|
+
expect(installAgentDocs(tmpDir)).toEqual(['AGENTS.md', 'GEMINI.md']);
|
|
853
|
+
expect(fs.readFileSync(path.join(tmpDir, 'GEMINI.md'), 'utf-8')).toBe(
|
|
854
|
+
'# Gemini\n\nSee @AGENTS.md first.\n',
|
|
855
|
+
);
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
it('adds the import when GEMINI.md mentions AGENTS.md only in code or not as an import', () => {
|
|
859
|
+
setupCorePackage(tmpDir);
|
|
860
|
+
// Gemini CLI follows no `@` inside backticks, none glued to a word, and
|
|
861
|
+
// none whose path does not name AGENTS.md exactly.
|
|
862
|
+
const gemini = 'Write `@./AGENTS.md` to import.\nmail@AGENTS.md\nRead @./AGENTS.md.\n';
|
|
863
|
+
fs.writeFileSync(path.join(tmpDir, 'GEMINI.md'), gemini);
|
|
864
|
+
|
|
865
|
+
expect(installAgentDocs(tmpDir)).toEqual(['AGENTS.md', 'GEMINI.md']);
|
|
866
|
+
expect(fs.readFileSync(path.join(tmpDir, 'GEMINI.md'), 'utf-8')).toBe(
|
|
867
|
+
`${gemini}\n${GEMINI_IMPORT}`,
|
|
868
|
+
);
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
it.each([
|
|
872
|
+
['a list', '{"context": {"fileName": ["AGENTS.md", "GEMINI.md"]}}'],
|
|
873
|
+
['a name', '{"context": {"fileName": "AGENTS.md"}}'],
|
|
874
|
+
['comments', '{\n // Gemini reads AGENTS.md\n "context": {/* both */ "fileName": ["AGENTS.md", "GEMINI.md"]}\n}\n'],
|
|
875
|
+
['another file', '{"context": {"fileName": "GEMINI.md"}}'],
|
|
876
|
+
])('writes the GEMINI.md import whatever the project Gemini settings read (%s)', (_label, settings) => {
|
|
877
|
+
// Gemini loads project settings only when it starts at the project root;
|
|
878
|
+
// started in a subfolder, the import is its only copy.
|
|
879
|
+
setupCorePackage(tmpDir);
|
|
880
|
+
fs.mkdirSync(path.join(tmpDir, '.gemini'));
|
|
881
|
+
fs.writeFileSync(path.join(tmpDir, '.gemini', 'settings.json'), settings);
|
|
882
|
+
|
|
883
|
+
expect(installAgentDocs(tmpDir)).toEqual(['AGENTS.md', 'GEMINI.md']);
|
|
884
|
+
expect(fs.readFileSync(path.join(tmpDir, 'GEMINI.md'), 'utf-8')).toBe(GEMINI_IMPORT);
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
it('keeps its GEMINI.md import when the project Gemini settings read AGENTS.md', () => {
|
|
888
|
+
setupCorePackage(tmpDir);
|
|
889
|
+
fs.writeFileSync(path.join(tmpDir, 'GEMINI.md'), `# Gemini\n\n${GEMINI_IMPORT}`);
|
|
890
|
+
fs.mkdirSync(path.join(tmpDir, '.gemini'));
|
|
891
|
+
fs.writeFileSync(
|
|
892
|
+
path.join(tmpDir, '.gemini', 'settings.json'),
|
|
893
|
+
'{"context": {"fileName": ["AGENTS.md", "GEMINI.md"]}}',
|
|
894
|
+
);
|
|
895
|
+
|
|
896
|
+
expect(installAgentDocs(tmpDir)).toEqual(['AGENTS.md', 'GEMINI.md']);
|
|
897
|
+
expect(fs.readFileSync(path.join(tmpDir, 'GEMINI.md'), 'utf-8')).toBe(
|
|
898
|
+
`# Gemini\n\n${GEMINI_IMPORT}`,
|
|
899
|
+
);
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
it('leaves a file that is AGENTS.md under another name alone', () => {
|
|
903
|
+
setupCorePackage(tmpDir);
|
|
904
|
+
fs.writeFileSync(path.join(tmpDir, 'AGENTS.md'), '# Agents\n');
|
|
905
|
+
fs.symlinkSync('AGENTS.md', path.join(tmpDir, 'CLAUDE.md'));
|
|
906
|
+
|
|
907
|
+
// Claude Code reads the block through CLAUDE.md, so no copy is added.
|
|
908
|
+
expect(installAgentDocs(tmpDir)).toEqual(['AGENTS.md', 'GEMINI.md']);
|
|
909
|
+
expect(fs.existsSync(path.join(tmpDir, '.claude', 'CLAUDE.md'))).toBe(false);
|
|
910
|
+
expect(countBlocks(tmpDir, ['AGENTS.md'])).toBe(1);
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
it('keeps an existing GEMINI.md and adds the import to it', () => {
|
|
914
|
+
setupCorePackage(tmpDir);
|
|
915
|
+
fs.writeFileSync(path.join(tmpDir, 'GEMINI.md'), '# Gemini\n\nUse tabs.\n');
|
|
916
|
+
|
|
917
|
+
expect(installAgentDocs(tmpDir)).toEqual(['AGENTS.md', 'GEMINI.md']);
|
|
918
|
+
expect(fs.readFileSync(path.join(tmpDir, 'GEMINI.md'), 'utf-8')).toBe(
|
|
919
|
+
`# Gemini\n\nUse tabs.\n\n${GEMINI_IMPORT}`,
|
|
920
|
+
);
|
|
715
921
|
});
|
|
716
922
|
|
|
717
923
|
it('preserves import wrappers while initializing standalone files', () => {
|
|
718
924
|
setupCorePackage(tmpDir);
|
|
719
925
|
fs.writeFileSync(path.join(tmpDir, 'AGENTS.md'), '# Agents\n');
|
|
720
|
-
|
|
721
|
-
const
|
|
722
|
-
const wrapper = '@../AGENTS.md\n';
|
|
926
|
+
const wrapperPath = path.join(tmpDir, '.hermes.md');
|
|
927
|
+
const wrapper = '@AGENTS.md\n';
|
|
723
928
|
fs.writeFileSync(wrapperPath, wrapper);
|
|
724
929
|
fs.writeFileSync(path.join(tmpDir, '.cursorrules'), 'Cursor rules.\n');
|
|
725
930
|
|
|
726
931
|
const written = installAgentDocs(tmpDir);
|
|
727
932
|
|
|
728
|
-
expect(written).toEqual(['AGENTS.md', '.cursorrules']);
|
|
933
|
+
expect(written).toEqual(['AGENTS.md', 'GEMINI.md', '.cursorrules']);
|
|
729
934
|
expect(fs.readFileSync(wrapperPath, 'utf-8')).toBe(wrapper);
|
|
730
935
|
expect(fs.readFileSync(path.join(tmpDir, 'AGENTS.md'), 'utf-8')).toContain(
|
|
731
936
|
'<!-- ASTRYX:START -->',
|
|
@@ -735,7 +940,7 @@ describe('installAgentDocs', () => {
|
|
|
735
940
|
);
|
|
736
941
|
});
|
|
737
942
|
|
|
738
|
-
it('
|
|
943
|
+
it('treats cyclic imports as standalone files', () => {
|
|
739
944
|
setupCorePackage(tmpDir);
|
|
740
945
|
fs.writeFileSync(path.join(tmpDir, 'AGENTS.md'), '@CLAUDE.md\n');
|
|
741
946
|
fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), '@AGENTS.md\n');
|
|
@@ -743,22 +948,30 @@ describe('installAgentDocs', () => {
|
|
|
743
948
|
|
|
744
949
|
const written = installAgentDocs(tmpDir);
|
|
745
950
|
|
|
746
|
-
expect(written).toEqual(['AGENTS.md', 'CLAUDE.md', '.cursorrules']);
|
|
747
|
-
for (const rel of
|
|
748
|
-
expect(
|
|
749
|
-
'<!-- ASTRYX:START -->',
|
|
750
|
-
);
|
|
951
|
+
expect(written).toEqual(['AGENTS.md', 'CLAUDE.md', 'GEMINI.md', '.cursorrules']);
|
|
952
|
+
for (const rel of ['AGENTS.md', 'CLAUDE.md', '.cursorrules']) {
|
|
953
|
+
expect(countBlocks(tmpDir, [rel])).toBe(1);
|
|
751
954
|
}
|
|
752
955
|
});
|
|
753
956
|
|
|
957
|
+
it('keeps the per-file layout when a wrapper imports a file other than AGENTS.md', () => {
|
|
958
|
+
setupCorePackage(tmpDir);
|
|
959
|
+
fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), '# Claude\n');
|
|
960
|
+
fs.writeFileSync(path.join(tmpDir, 'AGENTS.md'), '@CLAUDE.md\n');
|
|
961
|
+
|
|
962
|
+
expect(installAgentDocs(tmpDir)).toEqual(['CLAUDE.md']);
|
|
963
|
+
expect(fs.readFileSync(path.join(tmpDir, 'AGENTS.md'), 'utf-8')).toBe('@CLAUDE.md\n');
|
|
964
|
+
expect(countBlocks(tmpDir, ['CLAUDE.md'])).toBe(1);
|
|
965
|
+
expect(fs.existsSync(path.join(tmpDir, 'GEMINI.md'))).toBe(false);
|
|
966
|
+
});
|
|
967
|
+
|
|
754
968
|
it('removes a managed block previously expanded into an import wrapper', () => {
|
|
755
969
|
setupCorePackage(tmpDir);
|
|
756
970
|
installAgentDocs(tmpDir, {agent: 'codex'});
|
|
757
|
-
|
|
758
|
-
const
|
|
759
|
-
const wrapper = '@../AGENTS.md\n';
|
|
971
|
+
const wrapperPath = path.join(tmpDir, '.hermes.md');
|
|
972
|
+
const wrapper = '@AGENTS.md\n';
|
|
760
973
|
fs.writeFileSync(wrapperPath, wrapper);
|
|
761
|
-
installAgentDocs(tmpDir, {agent: '
|
|
974
|
+
installAgentDocs(tmpDir, {agent: 'hermes'});
|
|
762
975
|
expect(fs.readFileSync(wrapperPath, 'utf-8')).toContain(
|
|
763
976
|
'<!-- ASTRYX:START -->',
|
|
764
977
|
);
|
|
@@ -766,7 +979,7 @@ describe('installAgentDocs', () => {
|
|
|
766
979
|
const written = installAgentDocs(tmpDir, {onlyReplace: true});
|
|
767
980
|
|
|
768
981
|
expect(written).toContain('AGENTS.md');
|
|
769
|
-
expect(written).toContain('.
|
|
982
|
+
expect(written).toContain('.hermes.md');
|
|
770
983
|
expect(fs.readFileSync(wrapperPath, 'utf-8')).toBe(wrapper);
|
|
771
984
|
});
|
|
772
985
|
|
|
@@ -782,17 +995,123 @@ describe('installAgentDocs', () => {
|
|
|
782
995
|
expect(() => installAgentDocs(tmpDir)).toThrow(/malformed|no matching/i);
|
|
783
996
|
});
|
|
784
997
|
|
|
785
|
-
it('
|
|
998
|
+
it('adds the block to an existing .claude/CLAUDE.md and to AGENTS.md', () => {
|
|
786
999
|
setupCorePackage(tmpDir);
|
|
787
1000
|
fs.mkdirSync(path.join(tmpDir, '.claude'), {recursive: true});
|
|
788
1001
|
fs.writeFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), '# Project\n\nExisting content.\n');
|
|
789
1002
|
|
|
790
1003
|
const written = installAgentDocs(tmpDir);
|
|
791
1004
|
|
|
792
|
-
expect(written).toEqual(['.claude/CLAUDE.md']);
|
|
1005
|
+
expect(written).toEqual(['AGENTS.md', '.claude/CLAUDE.md', 'GEMINI.md']);
|
|
793
1006
|
const content = fs.readFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), 'utf-8');
|
|
794
|
-
expect(content
|
|
795
|
-
|
|
1007
|
+
expect(content.startsWith('# Project\n\nExisting content.\n\n<!-- ASTRYX:START -->')).toBe(
|
|
1008
|
+
true,
|
|
1009
|
+
);
|
|
1010
|
+
expect(blocksSeen(tmpDir)).toEqual(ONCE_EACH);
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
it('gives a root CLAUDE.md that imports AGENTS.md a copy too', () => {
|
|
1014
|
+
// Claude Code does not follow an import of a file above the folder it
|
|
1015
|
+
// starts in, so a session in a subfolder reads only the copy.
|
|
1016
|
+
setupCorePackage(tmpDir);
|
|
1017
|
+
const claude = '# Claude\n\n@AGENTS.md\n';
|
|
1018
|
+
fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), claude);
|
|
1019
|
+
|
|
1020
|
+
expect(installAgentDocs(tmpDir)).toEqual(['AGENTS.md', 'CLAUDE.md', 'GEMINI.md']);
|
|
1021
|
+
const content = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8');
|
|
1022
|
+
expect(content.startsWith(`${claude}\n<!-- ASTRYX:START -->`)).toBe(true);
|
|
1023
|
+
expect(countBlocks(tmpDir, ['AGENTS.md', 'CLAUDE.md'])).toBe(2);
|
|
1024
|
+
expect(fs.existsSync(path.join(tmpDir, '.claude'))).toBe(false);
|
|
1025
|
+
|
|
1026
|
+
// Running again keeps the copy where it is.
|
|
1027
|
+
expect(installAgentDocs(tmpDir)).toEqual(['AGENTS.md', 'CLAUDE.md', 'GEMINI.md']);
|
|
1028
|
+
expect(fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8')).toBe(content);
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
it('keeps the copy in root CLAUDE.md, not .claude/CLAUDE.md, when root CLAUDE.md imports AGENTS.md', () => {
|
|
1032
|
+
// Root CLAUDE.md is committed even where .claude/ is ignored, so a fresh
|
|
1033
|
+
// clone still gives a session in a subfolder its copy.
|
|
1034
|
+
setupCorePackage(tmpDir, '2.0.0');
|
|
1035
|
+
fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), '# Claude\n\n@AGENTS.md\n');
|
|
1036
|
+
fs.mkdirSync(path.join(tmpDir, '.claude'));
|
|
1037
|
+
fs.writeFileSync(
|
|
1038
|
+
path.join(tmpDir, '.claude', 'CLAUDE.md'),
|
|
1039
|
+
'# Local\n\n<!-- ASTRYX:START -->\nAstryx v1.0.0 · old\n<!-- ASTRYX:END -->\n',
|
|
1040
|
+
);
|
|
1041
|
+
|
|
1042
|
+
expect(installAgentDocs(tmpDir)).toEqual([
|
|
1043
|
+
'AGENTS.md',
|
|
1044
|
+
'CLAUDE.md',
|
|
1045
|
+
'.claude/CLAUDE.md',
|
|
1046
|
+
'GEMINI.md',
|
|
1047
|
+
]);
|
|
1048
|
+
expect(countBlocks(tmpDir, ['CLAUDE.md'])).toBe(1);
|
|
1049
|
+
expect(fs.readFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), 'utf-8')).toBe(
|
|
1050
|
+
'# Local\n',
|
|
1051
|
+
);
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
it('gives a .claude/CLAUDE.md that imports AGENTS.md a copy when there is no root CLAUDE.md', () => {
|
|
1055
|
+
setupCorePackage(tmpDir);
|
|
1056
|
+
fs.writeFileSync(path.join(tmpDir, 'AGENTS.md'), '# Agents\n');
|
|
1057
|
+
fs.mkdirSync(path.join(tmpDir, '.claude'));
|
|
1058
|
+
fs.writeFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), '@../AGENTS.md\n');
|
|
1059
|
+
|
|
1060
|
+
expect(installAgentDocs(tmpDir)).toEqual(['AGENTS.md', '.claude/CLAUDE.md', 'GEMINI.md']);
|
|
1061
|
+
const content = fs.readFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), 'utf-8');
|
|
1062
|
+
expect(content.startsWith('@../AGENTS.md\n\n<!-- ASTRYX:START -->')).toBe(true);
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
it('onlyReplace: refreshes the copy in a Claude file that imports AGENTS.md instead of removing it', () => {
|
|
1066
|
+
setupCorePackage(tmpDir, '1.0.0');
|
|
1067
|
+
fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), '# Claude\n\n@AGENTS.md\n');
|
|
1068
|
+
installAgentDocs(tmpDir);
|
|
1069
|
+
setupCorePackage(tmpDir, '2.0.0');
|
|
1070
|
+
|
|
1071
|
+
expect(installAgentDocs(tmpDir, {onlyReplace: true})).toEqual(['AGENTS.md', 'CLAUDE.md']);
|
|
1072
|
+
const content = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8');
|
|
1073
|
+
expect(content).toContain('@AGENTS.md');
|
|
1074
|
+
expect(content).toContain('Astryx v2.0.0');
|
|
1075
|
+
expect(content).not.toContain('Astryx v1.0.0');
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
it('--agent all after the default keeps the GEMINI.md import and one Claude copy', () => {
|
|
1079
|
+
setupCorePackage(tmpDir);
|
|
1080
|
+
fs.writeFileSync(path.join(tmpDir, 'AGENTS.md'), '# Agents\n');
|
|
1081
|
+
fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), '# Claude\n');
|
|
1082
|
+
fs.mkdirSync(path.join(tmpDir, '.claude'));
|
|
1083
|
+
fs.writeFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), '# Local\n');
|
|
1084
|
+
installAgentDocs(tmpDir);
|
|
1085
|
+
|
|
1086
|
+
expect(installAgentDocs(tmpDir, {agent: 'all'})).toEqual(['AGENTS.md', 'CLAUDE.md']);
|
|
1087
|
+
expect(fs.readFileSync(path.join(tmpDir, 'GEMINI.md'), 'utf-8')).toBe(GEMINI_IMPORT);
|
|
1088
|
+
expect(fs.readFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), 'utf-8')).toBe(
|
|
1089
|
+
'# Local\n',
|
|
1090
|
+
);
|
|
1091
|
+
expect(blocksSeen(tmpDir)).toEqual(COPILOT_TWICE);
|
|
1092
|
+
});
|
|
1093
|
+
|
|
1094
|
+
it('--agent claude writes CLAUDE.md and removes a block from .claude/CLAUDE.md', () => {
|
|
1095
|
+
setupCorePackage(tmpDir);
|
|
1096
|
+
fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), '# Claude\n');
|
|
1097
|
+
fs.mkdirSync(path.join(tmpDir, '.claude'));
|
|
1098
|
+
fs.writeFileSync(
|
|
1099
|
+
path.join(tmpDir, '.claude', 'CLAUDE.md'),
|
|
1100
|
+
'# Local\n\n<!-- ASTRYX:START -->\nAstryx v1.0.0 · old\n<!-- ASTRYX:END -->\n',
|
|
1101
|
+
);
|
|
1102
|
+
installAgentDocs(tmpDir);
|
|
1103
|
+
|
|
1104
|
+
expect(installAgentDocs(tmpDir, {agent: 'claude'})).toEqual(['CLAUDE.md']);
|
|
1105
|
+
expect(installAgentDocs(tmpDir, {agent: 'all'})).toEqual(['AGENTS.md', 'CLAUDE.md']);
|
|
1106
|
+
fs.writeFileSync(
|
|
1107
|
+
path.join(tmpDir, '.claude', 'CLAUDE.md'),
|
|
1108
|
+
'# Local\n\n<!-- ASTRYX:START -->\nAstryx v1.0.0 · old\n<!-- ASTRYX:END -->\n',
|
|
1109
|
+
);
|
|
1110
|
+
expect(installAgentDocs(tmpDir, {agent: 'claude'})).toEqual(['CLAUDE.md', '.claude/CLAUDE.md']);
|
|
1111
|
+
expect(fs.readFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), 'utf-8')).toBe(
|
|
1112
|
+
'# Local\n',
|
|
1113
|
+
);
|
|
1114
|
+
expect(blocksSeen(tmpDir).claude).toBe(1);
|
|
796
1115
|
});
|
|
797
1116
|
|
|
798
1117
|
it('respects --agent claude preset: finds existing CLAUDE.md', () => {
|
|
@@ -879,6 +1198,39 @@ describe('installAgentDocs', () => {
|
|
|
879
1198
|
expect(content).toContain('Other rules.');
|
|
880
1199
|
});
|
|
881
1200
|
|
|
1201
|
+
it('onlyReplace: refreshes both copies of the block and leaves the GEMINI.md import alone', () => {
|
|
1202
|
+
setupCorePackage(tmpDir, '1.0.0');
|
|
1203
|
+
fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), '# Claude\n');
|
|
1204
|
+
installAgentDocs(tmpDir);
|
|
1205
|
+
setupCorePackage(tmpDir, '2.0.0');
|
|
1206
|
+
|
|
1207
|
+
expect(installAgentDocs(tmpDir, {onlyReplace: true})).toEqual([
|
|
1208
|
+
'AGENTS.md',
|
|
1209
|
+
'CLAUDE.md',
|
|
1210
|
+
]);
|
|
1211
|
+
for (const rel of ['AGENTS.md', 'CLAUDE.md']) {
|
|
1212
|
+
const content = fs.readFileSync(path.join(tmpDir, rel), 'utf-8');
|
|
1213
|
+
expect(content).toContain('Astryx v2.0.0');
|
|
1214
|
+
expect(content).not.toContain('Astryx v1.0.0');
|
|
1215
|
+
}
|
|
1216
|
+
expect(fs.readFileSync(path.join(tmpDir, 'GEMINI.md'), 'utf-8')).toBe(GEMINI_IMPORT);
|
|
1217
|
+
expect(blocksSeen(tmpDir)).toEqual(COPILOT_TWICE);
|
|
1218
|
+
});
|
|
1219
|
+
|
|
1220
|
+
it('onlyReplace: refreshes a block beside an import of a missing AGENTS.md', () => {
|
|
1221
|
+
setupCorePackage(tmpDir, '2.0.0');
|
|
1222
|
+
fs.writeFileSync(
|
|
1223
|
+
path.join(tmpDir, 'CLAUDE.md'),
|
|
1224
|
+
'@AGENTS.md\n\n<!-- ASTRYX:START -->\nAstryx v1.0.0\n<!-- ASTRYX:END -->\n',
|
|
1225
|
+
);
|
|
1226
|
+
|
|
1227
|
+
expect(installAgentDocs(tmpDir, {onlyReplace: true})).toEqual(['CLAUDE.md']);
|
|
1228
|
+
expect(fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8')).toContain(
|
|
1229
|
+
'Astryx v2.0.0',
|
|
1230
|
+
);
|
|
1231
|
+
expect(fs.existsSync(path.join(tmpDir, 'AGENTS.md'))).toBe(false);
|
|
1232
|
+
});
|
|
1233
|
+
|
|
882
1234
|
it('onlyReplace: does not create the default AGENTS.md when nothing exists', () => {
|
|
883
1235
|
setupCorePackage(tmpDir);
|
|
884
1236
|
|
|
@@ -1084,6 +1436,25 @@ describe('inspectAgentDocs', () => {
|
|
|
1084
1436
|
expect(res.staleFiles).toEqual(['CLAUDE.md']);
|
|
1085
1437
|
});
|
|
1086
1438
|
|
|
1439
|
+
it('checks both copies of the block and does not count the GEMINI.md import', () => {
|
|
1440
|
+
const core = path.join(tmpDir, 'node_modules', '@astryxdesign', 'core');
|
|
1441
|
+
fs.mkdirSync(core, {recursive: true});
|
|
1442
|
+
fs.writeFileSync(path.join(core, 'package.json'), JSON.stringify({version: '3.0.0'}));
|
|
1443
|
+
fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), '# Claude\n');
|
|
1444
|
+
installAgentDocs(tmpDir);
|
|
1445
|
+
|
|
1446
|
+
const current = inspectAgentDocs(tmpDir, '3.0.0');
|
|
1447
|
+
expect(current.status).toBe('current');
|
|
1448
|
+
expect(current.files.map(f => f.path)).toEqual(['AGENTS.md', 'CLAUDE.md']);
|
|
1449
|
+
expect(inspectAgentDocs(tmpDir, '4.0.0').staleFiles).toEqual(['AGENTS.md', 'CLAUDE.md']);
|
|
1450
|
+
|
|
1451
|
+
// Without the blocks, the import alone reads as not set up.
|
|
1452
|
+
fs.rmSync(path.join(tmpDir, 'AGENTS.md'));
|
|
1453
|
+
fs.rmSync(path.join(tmpDir, 'CLAUDE.md'));
|
|
1454
|
+
expect(inspectAgentDocs(tmpDir, '3.0.0').status).toBe('missing');
|
|
1455
|
+
expect(isAstryxInitialized(tmpDir)).toBe(false);
|
|
1456
|
+
});
|
|
1457
|
+
|
|
1087
1458
|
it('defaults the installed version to the core package when omitted', () => {
|
|
1088
1459
|
const coreDir = path.join(tmpDir, 'node_modules', '@astryxdesign', 'core');
|
|
1089
1460
|
fs.mkdirSync(coreDir, {recursive: true});
|
|
@@ -1179,3 +1550,275 @@ describe('isAstryxInitialized — resilient to malformed markers (postinstall ho
|
|
|
1179
1550
|
expect(isAstryxInitialized(path.join(tmpDir, 'does-not-exist'))).toBe(false);
|
|
1180
1551
|
});
|
|
1181
1552
|
});
|
|
1553
|
+
|
|
1554
|
+
describe('removeAgentDocs after the default layout', () => {
|
|
1555
|
+
beforeEach(() => {
|
|
1556
|
+
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
1557
|
+
});
|
|
1558
|
+
|
|
1559
|
+
it('deletes the files the default created and keeps the project files', () => {
|
|
1560
|
+
const core = path.join(tmpDir, 'node_modules', '@astryxdesign', 'core');
|
|
1561
|
+
fs.mkdirSync(core, {recursive: true});
|
|
1562
|
+
fs.writeFileSync(path.join(core, 'package.json'), JSON.stringify({version: '1.0.0'}));
|
|
1563
|
+
fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), '# Claude\n');
|
|
1564
|
+
installAgentDocs(tmpDir);
|
|
1565
|
+
|
|
1566
|
+
expect(removeAgentDocs(tmpDir)).toEqual(['AGENTS.md', 'CLAUDE.md', 'GEMINI.md']);
|
|
1567
|
+
expect(fs.existsSync(path.join(tmpDir, 'AGENTS.md'))).toBe(false);
|
|
1568
|
+
expect(fs.existsSync(path.join(tmpDir, 'GEMINI.md'))).toBe(false);
|
|
1569
|
+
expect(fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8')).toBe('# Claude\n');
|
|
1570
|
+
});
|
|
1571
|
+
|
|
1572
|
+
it('keeps a GEMINI.md that has the project\'s own content', () => {
|
|
1573
|
+
fs.writeFileSync(
|
|
1574
|
+
path.join(tmpDir, 'GEMINI.md'),
|
|
1575
|
+
`# Gemini\n\nUse tabs.\n\n${GEMINI_IMPORT}`,
|
|
1576
|
+
);
|
|
1577
|
+
|
|
1578
|
+
expect(removeAgentDocs(tmpDir)).toEqual(['GEMINI.md']);
|
|
1579
|
+
expect(fs.readFileSync(path.join(tmpDir, 'GEMINI.md'), 'utf-8')).toBe(
|
|
1580
|
+
'# Gemini\n\nUse tabs.\n',
|
|
1581
|
+
);
|
|
1582
|
+
});
|
|
1583
|
+
});
|
|
1584
|
+
|
|
1585
|
+
describe('agent docs never go into the home folder', () => {
|
|
1586
|
+
const saved = {HOME: process.env.HOME, USERPROFILE: process.env.USERPROFILE};
|
|
1587
|
+
const personal = '# My notes\n\nAlways use tabs.\n';
|
|
1588
|
+
|
|
1589
|
+
beforeEach(() => {
|
|
1590
|
+
process.env.HOME = tmpDir;
|
|
1591
|
+
process.env.USERPROFILE = tmpDir;
|
|
1592
|
+
fs.mkdirSync(path.join(tmpDir, '.claude'), {recursive: true});
|
|
1593
|
+
fs.writeFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), personal);
|
|
1594
|
+
});
|
|
1595
|
+
afterEach(() => {
|
|
1596
|
+
for (const [key, value] of Object.entries(saved)) {
|
|
1597
|
+
if (value === undefined) delete process.env[key];
|
|
1598
|
+
else process.env[key] = value;
|
|
1599
|
+
}
|
|
1600
|
+
});
|
|
1601
|
+
|
|
1602
|
+
it.each([
|
|
1603
|
+
['the default', {}],
|
|
1604
|
+
['a preset', {agent: 'claude'}],
|
|
1605
|
+
['an explicit path', {paths: ['AGENTS.md']}],
|
|
1606
|
+
['a refresh', {onlyReplace: true}],
|
|
1607
|
+
])('refuses %s and writes nothing', (_label, options) => {
|
|
1608
|
+
expect(() => installAgentDocs(tmpDir, options)).toThrow(AgentDocsHomeError);
|
|
1609
|
+
expect(fs.readFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), 'utf-8')).toBe(
|
|
1610
|
+
personal,
|
|
1611
|
+
);
|
|
1612
|
+
expect(fs.readdirSync(tmpDir).sort()).toEqual(['.claude']);
|
|
1613
|
+
});
|
|
1614
|
+
|
|
1615
|
+
it('writes an explicit path into a plain folder of the home folder, as 0.7.x did', () => {
|
|
1616
|
+
expect(installAgentDocs(tmpDir, {paths: ['notes/AI.md']})).toEqual(['notes/AI.md']);
|
|
1617
|
+
expect(fs.readFileSync(path.join(tmpDir, 'notes', 'AI.md'), 'utf-8')).toContain(
|
|
1618
|
+
'<!-- ASTRYX:START -->',
|
|
1619
|
+
);
|
|
1620
|
+
});
|
|
1621
|
+
|
|
1622
|
+
it.each([
|
|
1623
|
+
['a file in the home folder', ['AI.md']],
|
|
1624
|
+
['a file at the top of a settings folder', ['.claude/CLAUDE.md']],
|
|
1625
|
+
['one of several paths', ['notes/AI.md', 'AI.md']],
|
|
1626
|
+
])('refuses an explicit path to %s, and writes nothing', (_label, paths) => {
|
|
1627
|
+
expect(() => installAgentDocs(tmpDir, {paths})).toThrow(AgentDocsHomeError);
|
|
1628
|
+
expect(fs.readFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), 'utf-8')).toBe(
|
|
1629
|
+
personal,
|
|
1630
|
+
);
|
|
1631
|
+
expect(fs.readdirSync(tmpDir).sort()).toEqual(['.claude']);
|
|
1632
|
+
});
|
|
1633
|
+
|
|
1634
|
+
it('init with an explicit path from the home folder writes it, and refuses one in the home folder', async () => {
|
|
1635
|
+
const {init} = await import('../../api/init/init.mjs');
|
|
1636
|
+
const written = await init(
|
|
1637
|
+
{features: ['agents'], agentDocsPath: 'notes/AI.md'},
|
|
1638
|
+
{cwd: tmpDir},
|
|
1639
|
+
);
|
|
1640
|
+
expect(written.data.docsWritten).toEqual(['notes/AI.md']);
|
|
1641
|
+
expect(written.data.docsError ?? null).toBeNull();
|
|
1642
|
+
const refused = await init(
|
|
1643
|
+
{features: ['agents'], agentDocsPath: 'AI.md'},
|
|
1644
|
+
{cwd: tmpDir},
|
|
1645
|
+
);
|
|
1646
|
+
expect(refused.data.docsError).toMatchObject({kind: 'install-failed'});
|
|
1647
|
+
expect(fs.existsSync(path.join(tmpDir, 'AI.md'))).toBe(false);
|
|
1648
|
+
});
|
|
1649
|
+
|
|
1650
|
+
it('refuses through a symlink to the home folder', () => {
|
|
1651
|
+
const link = `${tmpDir}-link`;
|
|
1652
|
+
fs.symlinkSync(tmpDir, link);
|
|
1653
|
+
try {
|
|
1654
|
+
expect(() => installAgentDocs(link)).toThrow(AgentDocsHomeError);
|
|
1655
|
+
} finally {
|
|
1656
|
+
fs.rmSync(link);
|
|
1657
|
+
}
|
|
1658
|
+
});
|
|
1659
|
+
|
|
1660
|
+
it('still removes a block an older run wrote there', () => {
|
|
1661
|
+
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
1662
|
+
fs.writeFileSync(
|
|
1663
|
+
path.join(tmpDir, '.claude', 'CLAUDE.md'),
|
|
1664
|
+
`${personal}\n<!-- ASTRYX:START -->\nAstryx v1.0.0\n<!-- ASTRYX:END -->\n`,
|
|
1665
|
+
);
|
|
1666
|
+
|
|
1667
|
+
expect(removeAgentDocs(tmpDir)).toEqual(['.claude/CLAUDE.md']);
|
|
1668
|
+
expect(fs.readFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), 'utf-8')).toBe(
|
|
1669
|
+
personal,
|
|
1670
|
+
);
|
|
1671
|
+
});
|
|
1672
|
+
|
|
1673
|
+
it.each(['.gemini', '.claude', '.codex', path.join('.gemini', 'extensions', 'x')])(
|
|
1674
|
+
'refuses the settings folder ~/%s',
|
|
1675
|
+
folder => {
|
|
1676
|
+
const dir = path.join(tmpDir, folder);
|
|
1677
|
+
fs.mkdirSync(dir, {recursive: true});
|
|
1678
|
+
const before = fs.readdirSync(dir);
|
|
1679
|
+
expect(() => installAgentDocs(dir)).toThrow(AgentDocsHomeError);
|
|
1680
|
+
expect(fs.readdirSync(dir)).toEqual(before);
|
|
1681
|
+
expect(fs.readFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), 'utf-8')).toBe(
|
|
1682
|
+
personal,
|
|
1683
|
+
);
|
|
1684
|
+
},
|
|
1685
|
+
);
|
|
1686
|
+
|
|
1687
|
+
it('writes in a project folder inside the home folder', () => {
|
|
1688
|
+
const project = path.join(tmpDir, 'app');
|
|
1689
|
+
fs.mkdirSync(project);
|
|
1690
|
+
expect(installAgentDocs(project)).toEqual(['AGENTS.md', 'GEMINI.md']);
|
|
1691
|
+
});
|
|
1692
|
+
|
|
1693
|
+
/**
|
|
1694
|
+
* A git project that declares Astryx, at `rel` under the home folder, with
|
|
1695
|
+
* Core installed at `version`.
|
|
1696
|
+
* @param {string} rel
|
|
1697
|
+
* @param {string} version
|
|
1698
|
+
*/
|
|
1699
|
+
function writeProject(rel, version) {
|
|
1700
|
+
const project = path.join(tmpDir, rel);
|
|
1701
|
+
fs.mkdirSync(path.join(project, 'src'), {recursive: true});
|
|
1702
|
+
fs.writeFileSync(path.join(project, '.git'), 'gitdir: /elsewhere/.git/worktrees/web\n');
|
|
1703
|
+
fs.writeFileSync(
|
|
1704
|
+
path.join(project, 'package.json'),
|
|
1705
|
+
JSON.stringify({name: 'web', dependencies: {'@astryxdesign/core': version}}),
|
|
1706
|
+
);
|
|
1707
|
+
const core = path.join(project, 'node_modules', '@astryxdesign', 'core');
|
|
1708
|
+
fs.mkdirSync(core, {recursive: true});
|
|
1709
|
+
fs.writeFileSync(path.join(core, 'package.json'), JSON.stringify({version}));
|
|
1710
|
+
return project;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
it.each([
|
|
1714
|
+
['a Codex worktree', path.join('.codex', 'worktrees', 'a1b2c3', 'web')],
|
|
1715
|
+
['a Cursor worktree', path.join('.cursor', 'worktrees', 'web', 'x9y8')],
|
|
1716
|
+
['a project with its package.json one level in', path.join('.config', 'web')],
|
|
1717
|
+
])('writes and refreshes the docs of a project inside a settings folder (%s)', (_label, rel) => {
|
|
1718
|
+
const project = writeProject(rel, '1.0.0');
|
|
1719
|
+
|
|
1720
|
+
expect(resolveAgentDocsRoot(path.join(project, 'src'))).toBe(project);
|
|
1721
|
+
expect(installAgentDocs(project)).toEqual(['AGENTS.md', 'GEMINI.md']);
|
|
1722
|
+
|
|
1723
|
+
// upgrade refreshes the block in place.
|
|
1724
|
+
writeProject(rel, '2.0.0');
|
|
1725
|
+
expect(installAgentDocs(project, {onlyReplace: true})).toEqual(['AGENTS.md']);
|
|
1726
|
+
expect(fs.readFileSync(path.join(project, 'AGENTS.md'), 'utf-8')).toContain(
|
|
1727
|
+
'Astryx v2.0.0',
|
|
1728
|
+
);
|
|
1729
|
+
|
|
1730
|
+
// Nothing reaches the settings folder or the home folder.
|
|
1731
|
+
const settings = path.join(tmpDir, rel.split(path.sep)[0]);
|
|
1732
|
+
expect(fs.readdirSync(settings)).toEqual([rel.split(path.sep)[1]]);
|
|
1733
|
+
expect(fs.readFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), 'utf-8')).toBe(
|
|
1734
|
+
personal,
|
|
1735
|
+
);
|
|
1736
|
+
});
|
|
1737
|
+
|
|
1738
|
+
it('refuses the top of a settings folder even with a package.json, and the folders in it outside a project', () => {
|
|
1739
|
+
// A package.json in ~/.claude (for hook scripts, say) would otherwise put the
|
|
1740
|
+
// block in ~/.claude/CLAUDE.md, which Claude Code reads in every project.
|
|
1741
|
+
fs.writeFileSync(path.join(tmpDir, '.claude', 'package.json'), '{"name":"hooks"}\n');
|
|
1742
|
+
fs.mkdirSync(path.join(tmpDir, '.claude', 'hooks'));
|
|
1743
|
+
|
|
1744
|
+
expect(() => installAgentDocs(path.join(tmpDir, '.claude'))).toThrow(AgentDocsHomeError);
|
|
1745
|
+
expect(() => installAgentDocs(path.join(tmpDir, '.claude', 'hooks'))).toThrow(
|
|
1746
|
+
AgentDocsHomeError,
|
|
1747
|
+
);
|
|
1748
|
+
expect(resolveAgentDocsRoot(path.join(tmpDir, '.claude', 'hooks'))).toBe(
|
|
1749
|
+
path.join(tmpDir, '.claude'),
|
|
1750
|
+
);
|
|
1751
|
+
expect(fs.readFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), 'utf-8')).toBe(
|
|
1752
|
+
personal,
|
|
1753
|
+
);
|
|
1754
|
+
|
|
1755
|
+
// A worktree folder whose project has no package.json is not a project.
|
|
1756
|
+
const bare = path.join(tmpDir, '.codex', 'worktrees', 'a1b2c3', 'notes');
|
|
1757
|
+
fs.mkdirSync(bare, {recursive: true});
|
|
1758
|
+
expect(() => installAgentDocs(bare)).toThrow(AgentDocsHomeError);
|
|
1759
|
+
expect(fs.readdirSync(bare)).toEqual([]);
|
|
1760
|
+
});
|
|
1761
|
+
});
|
|
1762
|
+
|
|
1763
|
+
describe('resolveAgentDocsRoot', () => {
|
|
1764
|
+
/** @param {string} dir @param {object} pkg */
|
|
1765
|
+
const writePkg = (dir, pkg) => {
|
|
1766
|
+
fs.mkdirSync(dir, {recursive: true});
|
|
1767
|
+
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify(pkg));
|
|
1768
|
+
};
|
|
1769
|
+
const astryx = {devDependencies: {'@astryxdesign/cli': '0.7.0'}};
|
|
1770
|
+
|
|
1771
|
+
it('finds the nearest folder whose package.json declares Astryx', () => {
|
|
1772
|
+
writePkg(tmpDir, astryx);
|
|
1773
|
+
const sub = path.join(tmpDir, 'src', 'app');
|
|
1774
|
+
fs.mkdirSync(sub, {recursive: true});
|
|
1775
|
+
expect(resolveAgentDocsRoot(sub)).toBe(tmpDir);
|
|
1776
|
+
expect(resolveAgentDocsRoot(tmpDir)).toBe(tmpDir);
|
|
1777
|
+
|
|
1778
|
+
// A nested package that does not declare Astryx is passed over.
|
|
1779
|
+
const functions = path.join(tmpDir, 'functions');
|
|
1780
|
+
writePkg(functions, {name: 'functions'});
|
|
1781
|
+
fs.mkdirSync(path.join(functions, 'src'));
|
|
1782
|
+
expect(resolveAgentDocsRoot(path.join(functions, 'src'))).toBe(tmpDir);
|
|
1783
|
+
|
|
1784
|
+
// A nested package that declares Astryx is its own project.
|
|
1785
|
+
const web = path.join(tmpDir, 'apps', 'web');
|
|
1786
|
+
writePkg(web, {dependencies: {'@astryxdesign/core': '0.7.0'}});
|
|
1787
|
+
expect(resolveAgentDocsRoot(web)).toBe(web);
|
|
1788
|
+
});
|
|
1789
|
+
|
|
1790
|
+
it('falls back to the nearest package.json when none declares Astryx', () => {
|
|
1791
|
+
writePkg(tmpDir, {name: 'app'});
|
|
1792
|
+
const pkg = path.join(tmpDir, 'packages', 'web');
|
|
1793
|
+
writePkg(pkg, {name: 'web'});
|
|
1794
|
+
fs.mkdirSync(path.join(pkg, 'src'));
|
|
1795
|
+
expect(resolveAgentDocsRoot(path.join(pkg, 'src'))).toBe(pkg);
|
|
1796
|
+
});
|
|
1797
|
+
|
|
1798
|
+
it('stops at a repository root, and finds no project without a package.json', () => {
|
|
1799
|
+
writePkg(tmpDir, astryx);
|
|
1800
|
+
const repo = path.join(tmpDir, 'repo');
|
|
1801
|
+
fs.mkdirSync(path.join(repo, '.git'), {recursive: true});
|
|
1802
|
+
const sub = path.join(repo, 'docs');
|
|
1803
|
+
fs.mkdirSync(sub);
|
|
1804
|
+
expect(resolveAgentDocsRoot(sub)).toBeNull();
|
|
1805
|
+
});
|
|
1806
|
+
|
|
1807
|
+
it('never returns the home folder', () => {
|
|
1808
|
+
const saved = {HOME: process.env.HOME, USERPROFILE: process.env.USERPROFILE};
|
|
1809
|
+
process.env.HOME = tmpDir;
|
|
1810
|
+
process.env.USERPROFILE = tmpDir;
|
|
1811
|
+
try {
|
|
1812
|
+
writePkg(tmpDir, astryx);
|
|
1813
|
+
const scratch = path.join(tmpDir, 'scratch');
|
|
1814
|
+
fs.mkdirSync(scratch);
|
|
1815
|
+
expect(resolveAgentDocsRoot(scratch)).toBeNull();
|
|
1816
|
+
expect(resolveAgentDocsRoot(tmpDir)).toBeNull();
|
|
1817
|
+
} finally {
|
|
1818
|
+
for (const [key, value] of Object.entries(saved)) {
|
|
1819
|
+
if (value === undefined) delete process.env[key];
|
|
1820
|
+
else process.env[key] = value;
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
});
|
|
1824
|
+
});
|