@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
package/api/docs/_adapter.mjs
CHANGED
|
@@ -1146,6 +1146,14 @@ function packageCatalog(integration, discovered) {
|
|
|
1146
1146
|
* @returns {Promise<Array<{severity: 'warning', message: string}>>}
|
|
1147
1147
|
*/
|
|
1148
1148
|
export async function packageComponentLinkProblems(integration, discovered) {
|
|
1149
|
+
// A package with no provider id (its manifest's, else its name) links
|
|
1150
|
+
// nothing, as on the read path: its text reads as written, so there is
|
|
1151
|
+
// nothing to report.
|
|
1152
|
+
try {
|
|
1153
|
+
normalizeProviderId(integration.providerId ?? integration.name);
|
|
1154
|
+
} catch {
|
|
1155
|
+
return [];
|
|
1156
|
+
}
|
|
1149
1157
|
const catalog = packageCatalog(integration, discovered);
|
|
1150
1158
|
return (await componentLinkProblems(catalog, integration.name)).map(
|
|
1151
1159
|
message => ({severity: /** @type {const} */ ('warning'), message}),
|
|
@@ -1176,11 +1184,15 @@ async function componentLinkProblems(catalog, owner) {
|
|
|
1176
1184
|
// The components check reports a doc that does not load.
|
|
1177
1185
|
continue;
|
|
1178
1186
|
}
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
componentLinkProvider(record.package, record.providerId)
|
|
1182
|
-
|
|
1183
|
-
|
|
1187
|
+
let provider;
|
|
1188
|
+
try {
|
|
1189
|
+
provider = componentLinkProvider(record.package, record.providerId);
|
|
1190
|
+
} catch {
|
|
1191
|
+
// An owner that is not a provider id links nothing (as on the read
|
|
1192
|
+
// path); its text reads as written.
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
const linked = await linkComponentDoc(catalog, provider, doc);
|
|
1184
1196
|
const name = typeof doc?.name === 'string' ? doc.name : record.name;
|
|
1185
1197
|
for (const problem of linked.problems) {
|
|
1186
1198
|
problems.push(
|
package/api/docs/docs.test.mjs
CHANGED
|
@@ -123,7 +123,13 @@ describe('docs() dispatcher routing', () => {
|
|
|
123
123
|
kind: 'namespace',
|
|
124
124
|
package: '@astryxdesign/cli',
|
|
125
125
|
breadcrumb: [{route: 'cli', title: 'Astryx CLI'}],
|
|
126
|
-
|
|
126
|
+
// Its one block points to the host entry's guide.
|
|
127
|
+
content: [
|
|
128
|
+
{
|
|
129
|
+
type: 'prose',
|
|
130
|
+
text: expect.stringContaining('`astryx docs cli/host-entry`'),
|
|
131
|
+
},
|
|
132
|
+
],
|
|
127
133
|
});
|
|
128
134
|
expect(r.data.slots.flatMap(slot => slot.children.map(c => c.route))).toEqual([
|
|
129
135
|
'cli/api/functions',
|
package/api/doctor/doctor.mjs
CHANGED
|
@@ -28,7 +28,15 @@ import * as fs from 'node:fs';
|
|
|
28
28
|
import * as path from 'node:path';
|
|
29
29
|
|
|
30
30
|
import {MIN_NODE_VERSION, isNodeVersionSupported} from '../../foundation/env/node-version.mjs';
|
|
31
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
AGENT_DOC_PATHS,
|
|
33
|
+
holdsAstryxBlock,
|
|
34
|
+
} from '../../foundation/agent-docs/agent-doc-state.mjs';
|
|
35
|
+
import {
|
|
36
|
+
agentDocsTargetDir,
|
|
37
|
+
assertAgentDocsTarget,
|
|
38
|
+
resolveAgentDocsRoot,
|
|
39
|
+
} from '../../foundation/agent-docs/agent-docs.mjs';
|
|
32
40
|
import {CLI_ROOT, findProjectCoreDir, findInstalledPackage} from '../../foundation/fs/paths.mjs';
|
|
33
41
|
import {explainPackageManager, formatCliCommand, getCliInvocation, guessCliInvocation} from '../../foundation/env/package-manager.mjs';
|
|
34
42
|
import {chooseDeclaredCliCommand, declaredCliCommandsOf} from '../../foundation/env/cli-command.mjs';
|
|
@@ -610,6 +618,21 @@ export function checkConfiguredIntegrations(ctx) {
|
|
|
610
618
|
};
|
|
611
619
|
}
|
|
612
620
|
|
|
621
|
+
/**
|
|
622
|
+
* Why init writes no agent docs for `cwd`, or null when it does: the folder
|
|
623
|
+
* it would write to is the home folder or the top of a settings folder in it.
|
|
624
|
+
* @param {string} cwd
|
|
625
|
+
* @returns {string | null}
|
|
626
|
+
*/
|
|
627
|
+
function agentDocsRefusal(cwd) {
|
|
628
|
+
try {
|
|
629
|
+
assertAgentDocsTarget(agentDocsTargetDir(cwd));
|
|
630
|
+
return null;
|
|
631
|
+
} catch (err) {
|
|
632
|
+
return err instanceof Error && err.name === 'AgentDocsHomeError' ? err.message : null;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
613
636
|
/**
|
|
614
637
|
* Check 7 — agent docs exist and contain the Astryx section markers.
|
|
615
638
|
* @param {DoctorContext} ctx
|
|
@@ -617,8 +640,15 @@ export function checkConfiguredIntegrations(ctx) {
|
|
|
617
640
|
*/
|
|
618
641
|
export function checkAgentDocs(ctx) {
|
|
619
642
|
// Every file init can write (one shared list), looked for in the working
|
|
620
|
-
// directory
|
|
621
|
-
|
|
643
|
+
// directory, the project root, and the folder init writes them to: run from
|
|
644
|
+
// src/ or a nested package, the docs sit at the root.
|
|
645
|
+
const dirs = [
|
|
646
|
+
...new Set(
|
|
647
|
+
[ctx.cwd, projectRootOf(ctx), resolveAgentDocsRoot(ctx.cwd) ?? ctx.cwd].map(dir =>
|
|
648
|
+
path.resolve(dir),
|
|
649
|
+
),
|
|
650
|
+
),
|
|
651
|
+
];
|
|
622
652
|
const present = dirs.flatMap(dir =>
|
|
623
653
|
AGENT_DOC_PATHS.map(rel => path.join(dir, rel)).filter(abs =>
|
|
624
654
|
fs.existsSync(abs),
|
|
@@ -626,6 +656,15 @@ export function checkAgentDocs(ctx) {
|
|
|
626
656
|
);
|
|
627
657
|
/** @param {string} abs */
|
|
628
658
|
const shown = abs => path.relative(ctx.cwd, abs) || path.basename(abs);
|
|
659
|
+
// In the home folder, or at the top of a settings folder in it, init
|
|
660
|
+
// writes no agent docs (INV31), so the fix is moving the project or a hand
|
|
661
|
+
// edit, never init.
|
|
662
|
+
const refusal = agentDocsRefusal(ctx.cwd);
|
|
663
|
+
/** @param {string} initFix */
|
|
664
|
+
const fixFor = initFix =>
|
|
665
|
+
refusal
|
|
666
|
+
? `${refusal} Move the project into a folder of its own, or edit the Astryx block in its agent file by hand.`
|
|
667
|
+
: initFix;
|
|
629
668
|
|
|
630
669
|
if (present.length === 0) {
|
|
631
670
|
return {
|
|
@@ -633,29 +672,64 @@ export function checkAgentDocs(ctx) {
|
|
|
633
672
|
label: 'AI agent docs',
|
|
634
673
|
status: 'info',
|
|
635
674
|
message: `No agent docs found: looked for ${AGENT_DOC_PATHS.join(', ')} in ${dirs.map(dir => path.relative(ctx.cwd, dir) || '.').join(' and ')}.`,
|
|
636
|
-
fix:
|
|
675
|
+
fix: fixFor(
|
|
676
|
+
`Generate agent docs with \`${getCliInvocation(ctx.cwd)} init --features agents\`.`,
|
|
677
|
+
),
|
|
637
678
|
};
|
|
638
679
|
}
|
|
639
680
|
|
|
640
|
-
|
|
681
|
+
/** @param {string} abs */
|
|
682
|
+
const read = abs => {
|
|
641
683
|
try {
|
|
642
|
-
|
|
643
|
-
return (
|
|
644
|
-
(content.includes('<!-- ASTRYX:START -->') || content.includes('<!-- XDS:START -->')) &&
|
|
645
|
-
(content.includes('<!-- ASTRYX:END -->') || content.includes('<!-- XDS:END -->'))
|
|
646
|
-
);
|
|
684
|
+
return fs.readFileSync(abs, 'utf-8');
|
|
647
685
|
} catch {
|
|
648
|
-
return
|
|
686
|
+
return '';
|
|
649
687
|
}
|
|
688
|
+
};
|
|
689
|
+
const withMarkers = present.filter(abs => {
|
|
690
|
+
const content = read(abs);
|
|
691
|
+
return (
|
|
692
|
+
(content.includes('<!-- ASTRYX:START -->') || content.includes('<!-- XDS:START -->')) &&
|
|
693
|
+
(content.includes('<!-- ASTRYX:END -->') || content.includes('<!-- XDS:END -->')) &&
|
|
694
|
+
holdsAstryxBlock(content)
|
|
695
|
+
);
|
|
650
696
|
});
|
|
651
697
|
|
|
698
|
+
// A managed block that only imports another file (init's GEMINI.md import
|
|
699
|
+
// of AGENTS.md) is broken when that file holds no block: the agents that
|
|
700
|
+
// read either file get no Astryx docs.
|
|
701
|
+
const dangling = present.flatMap(abs => {
|
|
702
|
+
const match = /<!-- ASTRYX:START -->\s*@(\S+)\s*<!-- ASTRYX:END -->/.exec(read(abs));
|
|
703
|
+
if (match == null) return [];
|
|
704
|
+
const target = path.resolve(path.dirname(abs), match[1]);
|
|
705
|
+
return holdsAstryxBlock(read(target)) ? [] : [{abs, target}];
|
|
706
|
+
});
|
|
707
|
+
if (dangling.length > 0) {
|
|
708
|
+
return {
|
|
709
|
+
id: 'agent-docs',
|
|
710
|
+
label: 'AI agent docs',
|
|
711
|
+
status: 'fail',
|
|
712
|
+
message: dangling
|
|
713
|
+
.map(
|
|
714
|
+
({abs, target}) =>
|
|
715
|
+
`${shown(abs)} imports ${shown(target)}, which holds no Astryx block, so agents that read them get no Astryx docs.`,
|
|
716
|
+
)
|
|
717
|
+
.join(' '),
|
|
718
|
+
fix: fixFor(
|
|
719
|
+
`Write the block again with \`${getCliInvocation(ctx.cwd)} init --features agents\`.`,
|
|
720
|
+
),
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
|
|
652
724
|
if (withMarkers.length === 0) {
|
|
653
725
|
return {
|
|
654
726
|
id: 'agent-docs',
|
|
655
727
|
label: 'AI agent docs',
|
|
656
728
|
status: 'warn',
|
|
657
729
|
message: `Agent docs present (${present.map(shown).join(', ')}) but no Astryx section markers found.`,
|
|
658
|
-
fix:
|
|
730
|
+
fix: fixFor(
|
|
731
|
+
`Add the Astryx section to your agent docs with \`${getCliInvocation(ctx.cwd)} init --features agents\`.`,
|
|
732
|
+
),
|
|
659
733
|
};
|
|
660
734
|
}
|
|
661
735
|
|
|
@@ -1317,8 +1317,12 @@ describe('a configured integration that cannot load fails the gate', () => {
|
|
|
1317
1317
|
'astryx.config.mjs': "export default { integrations: ['@acme/widgets'] };\n",
|
|
1318
1318
|
};
|
|
1319
1319
|
|
|
1320
|
-
/**
|
|
1321
|
-
|
|
1320
|
+
/**
|
|
1321
|
+
* Run doctor on a project under the working directory, in its folder `at`.
|
|
1322
|
+
* @param {Record<string, string>} files
|
|
1323
|
+
* @param {string} [at]
|
|
1324
|
+
*/
|
|
1325
|
+
async function report(files, at = '.') {
|
|
1322
1326
|
const dir = fs.mkdtempSync(path.join(process.cwd(), '.astryx-doctor-gate-'));
|
|
1323
1327
|
tmpDirs.push(dir);
|
|
1324
1328
|
for (const [rel, content] of Object.entries(files)) {
|
|
@@ -1326,7 +1330,7 @@ describe('a configured integration that cannot load fails the gate', () => {
|
|
|
1326
1330
|
fs.mkdirSync(path.dirname(abs), {recursive: true});
|
|
1327
1331
|
fs.writeFileSync(abs, content);
|
|
1328
1332
|
}
|
|
1329
|
-
const {checks, summary} = (await doctor({cwd: dir})).data;
|
|
1333
|
+
const {checks, summary} = (await doctor({cwd: path.join(dir, at)})).data;
|
|
1330
1334
|
return {by: Object.fromEntries(checks.map(c => [c.id, c])), summary};
|
|
1331
1335
|
}
|
|
1332
1336
|
|
|
@@ -1354,6 +1358,27 @@ describe('a configured integration that cannot load fails the gate', () => {
|
|
|
1354
1358
|
SLOW,
|
|
1355
1359
|
);
|
|
1356
1360
|
|
|
1361
|
+
it(
|
|
1362
|
+
'passes in a workspace member whose configured integration is hoisted to the root',
|
|
1363
|
+
async () => {
|
|
1364
|
+
const {by} = await report(
|
|
1365
|
+
{
|
|
1366
|
+
'package.json': JSON.stringify({name: 'root', private: true, workspaces: ['apps/*']}),
|
|
1367
|
+
...CORE_STUB,
|
|
1368
|
+
...widgets(),
|
|
1369
|
+
'apps/web/package.json': '{"name":"web"}',
|
|
1370
|
+
'apps/web/astryx.config.mjs': "export default { integrations: ['@acme/widgets'] };\n",
|
|
1371
|
+
},
|
|
1372
|
+
'apps/web',
|
|
1373
|
+
);
|
|
1374
|
+
expect(by['configured-integrations'].status).toBe('pass');
|
|
1375
|
+
expect(by['configured-integrations'].message).toBe(
|
|
1376
|
+
'1 integration named in astryx.config loaded: @acme/widgets.',
|
|
1377
|
+
);
|
|
1378
|
+
},
|
|
1379
|
+
SLOW,
|
|
1380
|
+
);
|
|
1381
|
+
|
|
1357
1382
|
it(
|
|
1358
1383
|
'counts the package being authored when its own config names it',
|
|
1359
1384
|
async () => {
|
package/api/init/init.doc.mjs
CHANGED
|
@@ -17,11 +17,22 @@ export const doc = {
|
|
|
17
17
|
'Non-interactive project setup: install agent docs and point at the theme + build workflows.',
|
|
18
18
|
description:
|
|
19
19
|
'Sets a project up with NO prompts, so it behaves identically for humans, ' +
|
|
20
|
-
'agents, CI, and piped I/O. By default it
|
|
21
|
-
'
|
|
20
|
+
'agents, CI, and piped I/O. By default it writes the agent-docs cheat sheet, ' +
|
|
21
|
+
'including guidance from configured integrations, at the project root (the nearest ' +
|
|
22
|
+
'package.json that declares an @astryxdesign package, else the nearest package.json, ' +
|
|
23
|
+
'searching no higher than the repository root): to AGENTS.md, ' +
|
|
24
|
+
"to the project's Claude Code file (CLAUDE.md, else .claude/CLAUDE.md; when CLAUDE.md " +
|
|
25
|
+
'exists, a block in .claude/CLAUDE.md is removed, unless the project ignores CLAUDE.md, in which case a ' +
|
|
26
|
+
'.claude/CLAUDE.md the project commits keeps a copy), and as a ' +
|
|
27
|
+
'one-line import of AGENTS.md in GEMINI.md unless GEMINI.md already imports it. ' +
|
|
28
|
+
'In the home folder, or in a settings folder in it outside a project, it writes none: every ' +
|
|
29
|
+
'folder of the home folder whose name starts with a dot is a settings folder, and such a folder itself never ' +
|
|
30
|
+
'counts as a project, even with a package.json; a folder below it with a package.json does. ' +
|
|
31
|
+
'The managed block is Astryx-owned: it is written, refreshed, and removed whatever ignore rules ' +
|
|
32
|
+
'or generated markers say, and the text around it is never changed. It then ' +
|
|
22
33
|
"prints getting-started guidance; `features` / `all` print only the chosen features' " +
|
|
23
34
|
'guidance instead (the theme feature also writes the annotated theme template). ' +
|
|
24
|
-
'With `removeAgents` it removes the managed agent-docs block instead of installing. ' +
|
|
35
|
+
'With `removeAgents` it removes the managed agent-docs block, at the project root and in `ctx.cwd`, instead of installing. ' +
|
|
25
36
|
'Outside a project it writes the same files but prints no getting-started steps and no step that adds a theme to an app, since those set up a project the folder does not have. ' +
|
|
26
37
|
"With `user` it sets up the computer instead of the folder: it writes the computer-level agent text, one skill file at `~/.agents/skills/astryx/SKILL.md`, linked into the skill folders of agents that read only their own (`~/.claude/skills` always; Kiro, Windsurf, Letta, Xcode's coding assistant, and `~/.config/agents/skills` when that folder exists), with a managed copy for Continue, whose scanner skips links,, telling agents anywhere on the computer that Astryx is there and when to use it; with `user` and `removeAgents` it removes the file and the links it made and records that the text is turned off. " +
|
|
27
38
|
'The CLI installed for the computer refreshes the file in the background after each upgrade; package install scripts, project setup, and upgrade never write it.' +
|
|
@@ -46,7 +57,7 @@ export const doc = {
|
|
|
46
57
|
name: 'options.removeAgents',
|
|
47
58
|
type: 'boolean',
|
|
48
59
|
description:
|
|
49
|
-
'Remove the managed agent-docs block instead of installing. With `user`, remove the computer-level agent text instead.',
|
|
60
|
+
'Remove the managed agent-docs block, at the project root and in `ctx.cwd`, instead of installing. With `user`, remove the computer-level agent text instead.',
|
|
50
61
|
},
|
|
51
62
|
{
|
|
52
63
|
name: 'options.user',
|
|
@@ -62,7 +73,10 @@ export const doc = {
|
|
|
62
73
|
name: 'options.agent',
|
|
63
74
|
type: 'string',
|
|
64
75
|
description:
|
|
65
|
-
'Agent preset: claude, cursor, codex, hermes, muse, all.
|
|
76
|
+
'Agent preset: claude, cursor, codex, hermes, muse, all. `all` writes every existing agent doc except GEMINI.md, ' +
|
|
77
|
+
'which keeps its one-line import of AGENTS.md. With claude and all, when CLAUDE.md gets the block, a block in ' +
|
|
78
|
+
'.claude/CLAUDE.md is removed, unless the project ignores CLAUDE.md, in which case a .claude/CLAUDE.md the ' +
|
|
79
|
+
'project commits gets a copy too. Used only when the agents feature runs. Refused with `user`.',
|
|
66
80
|
},
|
|
67
81
|
{
|
|
68
82
|
name: 'options.agentDocsPath',
|
|
@@ -122,7 +136,7 @@ export const doc = {
|
|
|
122
136
|
},
|
|
123
137
|
{
|
|
124
138
|
code: 'ERR_PATH_TRAVERSAL',
|
|
125
|
-
when: 'the template output path resolves outside cwd (for example through a symlinked src directory), or, with removeAgents, a managed agent-docs file resolves outside
|
|
139
|
+
when: 'the template output path resolves outside cwd (for example through a symlinked src directory), or, with removeAgents, a managed agent-docs file in cwd, or at the project root when run there, resolves outside that folder through a symlink (run below the root, a project-root file is left as it is with a note)',
|
|
126
140
|
},
|
|
127
141
|
],
|
|
128
142
|
examples: [
|
package/api/init/init.test.mjs
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import {describe, it, expect, beforeEach, afterEach, vi} from 'vitest';
|
|
12
12
|
import * as fs from 'node:fs';
|
|
13
|
+
import * as os from 'node:os';
|
|
13
14
|
import * as path from 'node:path';
|
|
14
15
|
import {init, getNextSteps} from './init.mjs';
|
|
15
16
|
import {logger} from '../logger.mjs';
|
|
@@ -21,7 +22,8 @@ const MARKER_START = '<!-- ASTRYX:START -->';
|
|
|
21
22
|
let tmpDir;
|
|
22
23
|
|
|
23
24
|
beforeEach(() => {
|
|
24
|
-
|
|
25
|
+
// Outside any package: init writes agent docs at the nearest package.json.
|
|
26
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-init-api-'));
|
|
25
27
|
});
|
|
26
28
|
|
|
27
29
|
afterEach(() => {
|
|
@@ -30,6 +32,18 @@ afterEach(() => {
|
|
|
30
32
|
|
|
31
33
|
/** @param {string} rel */
|
|
32
34
|
const read = rel => fs.readFileSync(path.join(tmpDir, rel), 'utf8');
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Make tmpDir a project that declares Astryx: a folder that declares nothing
|
|
38
|
+
* is outside a project, where init prints no getting-started steps (spec
|
|
39
|
+
* AST-081 FR3).
|
|
40
|
+
*/
|
|
41
|
+
function asProject() {
|
|
42
|
+
fs.writeFileSync(
|
|
43
|
+
path.join(tmpDir, 'package.json'),
|
|
44
|
+
JSON.stringify({name: 'app', dependencies: {'@astryxdesign/core': '*'}}),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
33
47
|
/** @param {string} rel */
|
|
34
48
|
const exists = rel => fs.existsSync(path.join(tmpDir, rel));
|
|
35
49
|
|
|
@@ -58,6 +72,7 @@ function writeIntegration() {
|
|
|
58
72
|
|
|
59
73
|
describe('init() — receipts + side effects', () => {
|
|
60
74
|
it('default mode installs AGENTS.md and returns an init.run receipt', async () => {
|
|
75
|
+
asProject();
|
|
61
76
|
const res = await init({}, {cwd: tmpDir});
|
|
62
77
|
expect(res.type).toBe('init.run');
|
|
63
78
|
if (res.type !== 'init.run') return;
|
|
@@ -217,6 +232,7 @@ describe('init() — logger', () => {
|
|
|
217
232
|
});
|
|
218
233
|
|
|
219
234
|
it('emits the install line + full next-steps through the shared logger', async () => {
|
|
235
|
+
asProject();
|
|
220
236
|
/** @type {string[]} */
|
|
221
237
|
const lines = [];
|
|
222
238
|
const logSpy = vi.spyOn(console, 'log').mockImplementation((...a) => lines.push(a.join(' ')));
|
|
@@ -243,7 +259,7 @@ describe('init() — ASCII output', () => {
|
|
|
243
259
|
const lines = [];
|
|
244
260
|
const logSpy = vi.spyOn(console, 'log').mockImplementation((...a) => lines.push(a.join(' ')));
|
|
245
261
|
const errSpy = vi.spyOn(console, 'error').mockImplementation((...a) => lines.push(a.join(' ')));
|
|
246
|
-
const empty = fs.mkdtempSync(path.join(
|
|
262
|
+
const empty = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-init-remove-'));
|
|
247
263
|
logger.setSilent(false);
|
|
248
264
|
try {
|
|
249
265
|
await init({}, {cwd: tmpDir});
|
package/api/init/init.type.d.mts
CHANGED
|
@@ -11,11 +11,11 @@ export type InitRunData = {
|
|
|
11
11
|
*/
|
|
12
12
|
features: string[];
|
|
13
13
|
/**
|
|
14
|
-
* Agent-doc files written (empty if agents weren't run or install failed).
|
|
14
|
+
* Agent-doc files written, relative to the directory init ran in (empty if agents weren't run or install failed). Without --agent-docs-path they sit at the project root: the nearest folder at or above it whose package.json declares an Astryx package, else the nearest with any package.json, within its repository and never the home folder; with none, the directory init ran in.
|
|
15
15
|
*/
|
|
16
16
|
docsWritten: string[];
|
|
17
17
|
/**
|
|
18
|
-
* Soft agent-docs failure, if any. `path-safety` also implies a non-zero exit.
|
|
18
|
+
* Soft agent-docs failure, if any. `path-safety` also implies a non-zero exit. `install-failed` carries a message when init wrote no agent docs (the home folder, or a settings folder in it outside a project) or skipped a file that resolves outside the project; docsWritten then lists what was written.
|
|
19
19
|
*/
|
|
20
20
|
docsError: {
|
|
21
21
|
kind: "path-safety" | "install-failed";
|
|
@@ -113,7 +113,7 @@ export type InitOptions = {
|
|
|
113
113
|
*/
|
|
114
114
|
all?: boolean | undefined;
|
|
115
115
|
/**
|
|
116
|
-
* Remove the managed agent-docs block instead of installing; with `user`, remove the computer-level agent text.
|
|
116
|
+
* Remove the managed agent-docs block, at the project root and in the folder init runs in, instead of installing; with `user`, remove the computer-level agent text.
|
|
117
117
|
*/
|
|
118
118
|
removeAgents?: boolean | undefined;
|
|
119
119
|
/**
|
package/api/init/init.type.mjs
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* @typedef {object} InitRunData
|
|
11
11
|
* @property {'default' | 'features'} mode `default` (no flags) or `features` (--features/--all).
|
|
12
12
|
* @property {string[]} features Features that were run, in order.
|
|
13
|
-
* @property {string[]} docsWritten Agent-doc files written (empty if agents weren't run or install failed).
|
|
14
|
-
* @property {{kind: 'path-safety' | 'install-failed', message?: string} | null} docsError Soft agent-docs failure, if any. `path-safety` also implies a non-zero exit.
|
|
13
|
+
* @property {string[]} docsWritten Agent-doc files written, relative to the directory init ran in (empty if agents weren't run or install failed). Without --agent-docs-path they sit at the project root: the nearest folder at or above it whose package.json declares an Astryx package, else the nearest with any package.json, within its repository and never the home folder; with none, the directory init ran in.
|
|
14
|
+
* @property {{kind: 'path-safety' | 'install-failed', message?: string} | null} docsError Soft agent-docs failure, if any. `path-safety` also implies a non-zero exit. `install-failed` carries a message when init wrote no agent docs (the home folder, or a settings folder in it outside a project) or skipped a file that resolves outside the project; docsWritten then lists what was written.
|
|
15
15
|
* @property {boolean} theme Whether the theme feature ran.
|
|
16
16
|
* @property {'created' | 'skipped' | 'failed' | null} themeTemplate Outcome of writing the annotated theme template (`skipped` = a file was already there).
|
|
17
17
|
* @property {string | null} themeTemplatePath Relative output path when `themeTemplate === 'created'`.
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
* @typedef {object} InitOptions
|
|
71
71
|
* @property {string} [features] Comma-separated features to install (agents, theme, template).
|
|
72
72
|
* @property {boolean} [all] Install all features.
|
|
73
|
-
* @property {boolean} [removeAgents] Remove the managed agent-docs block instead of installing; with `user`, remove the computer-level agent text.
|
|
73
|
+
* @property {boolean} [removeAgents] Remove the managed agent-docs block, at the project root and in the folder init runs in, instead of installing; with `user`, remove the computer-level agent text.
|
|
74
74
|
* @property {boolean} [user] Write the computer-level agent text (`~/.agents/skills/astryx/SKILL.md`, linked into `~/.claude/skills` and the skill folders of other installed tools, with a copy for Continue) instead of setting up the folder.
|
|
75
75
|
* @property {string} [agent] Agent preset: claude, cursor, codex, hermes, muse, all. Refused with `user`.
|
|
76
76
|
* @property {string | string[]} [agentDocsPath] Explicit agent-docs file path(s).
|
|
@@ -4,15 +4,16 @@
|
|
|
4
4
|
* @file init.remove leaf — the `--remove-agents` path behind `astryx init`.
|
|
5
5
|
*
|
|
6
6
|
* `remove(ctx)` deletes the managed agent-docs block from every agent-doc file
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* at the project root, where init writes it, and in the working folder, and
|
|
8
|
+
* returns an `init.remove` receipt. The removal engine lives in
|
|
9
|
+
* foundation/agent-docs; this leaf only wires the cwd.
|
|
9
10
|
*
|
|
10
11
|
* Note: removeAgentDocs() still logs its own per-file lines via humanLog
|
|
11
12
|
* (shared lib behavior) — so a programmatic remove is not perfectly silent yet.
|
|
12
13
|
* Threading a logger through agent-docs is a separate cleanup.
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
|
-
import {
|
|
16
|
+
import {removeProjectAgentDocs} from '../../../foundation/agent-docs/agent-docs.mjs';
|
|
16
17
|
import {logger} from '../../logger.mjs';
|
|
17
18
|
|
|
18
19
|
/**
|
|
@@ -23,7 +24,19 @@ import {logger} from '../../logger.mjs';
|
|
|
23
24
|
* @returns {Promise<import('../init.type.mjs').InitRemoveResponse>}
|
|
24
25
|
*/
|
|
25
26
|
export async function remove({cwd = process.cwd()} = {}) {
|
|
26
|
-
|
|
27
|
+
/** @type {string[]} */
|
|
28
|
+
const skipped = [];
|
|
29
|
+
const removedFrom = removeProjectAgentDocs(cwd, {
|
|
30
|
+
onSkipped: (rel, reason) => skipped.push(`${rel} (${reason})`),
|
|
31
|
+
});
|
|
32
|
+
if (skipped.length > 0) {
|
|
33
|
+
// Below the project root, as install does there (0.7.x touched only the
|
|
34
|
+
// working folder): a root agent file outside the project is left alone.
|
|
35
|
+
logger.warn(
|
|
36
|
+
`Note: Left as ${skipped.length === 1 ? 'it is' : 'they are'}: ${skipped.join('; ')}. ` +
|
|
37
|
+
'To remove the Astryx block there, delete it by hand.',
|
|
38
|
+
);
|
|
39
|
+
}
|
|
27
40
|
const removed = removedFrom.length > 0;
|
|
28
41
|
logger.log(
|
|
29
42
|
removed
|
|
@@ -1,12 +1,124 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
2
|
import {describe, it, expect, beforeEach, afterEach, vi} from 'vitest';
|
|
3
|
-
import * as fs from 'node:fs'; import * as path from 'node:path';
|
|
3
|
+
import * as fs from 'node:fs'; import * as path from 'node:path'; import * as os from 'node:os';
|
|
4
4
|
import {init} from '../init.mjs'; import {remove} from './remove.mjs'; import {logger} from '../../logger.mjs';
|
|
5
5
|
let dir;
|
|
6
|
-
beforeEach(() => { dir = fs.mkdtempSync(path.join(
|
|
6
|
+
beforeEach(() => { dir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-rm-')); fs.writeFileSync(path.join(dir,'package.json'),JSON.stringify({name:'c',version:'1.0.0'})); });
|
|
7
7
|
afterEach(() => { fs.rmSync(dir,{recursive:true,force:true}); logger.setSilent(true); });
|
|
8
8
|
describe('init.remove says whether anything was removed', () => {
|
|
9
9
|
it('false on clean', async () => { const r=await remove({cwd:dir}); expect(r.type).toBe('init.remove'); expect(r.data.removed).toBe(false); });
|
|
10
10
|
it('true then false', async () => { await init({features:['agents']},{cwd:dir}); expect(fs.readFileSync(path.join(dir,'AGENTS.md'),'utf8')).toContain('<!-- ASTRYX:START -->'); const a=await remove({cwd:dir}); expect(a.data.removed).toBe(true); const b=await remove({cwd:dir}); expect(b.data.removed).toBe(false); });
|
|
11
11
|
it('human line', async () => { const l=[]; const s=vi.spyOn(console,'log').mockImplementation((...a)=>l.push(a.join(' '))); logger.setSilent(false); try{await remove({cwd:dir});}finally{logger.setSilent(true);s.mockRestore();} expect(l.join('\n')).toContain('Nothing to remove'); });
|
|
12
12
|
});
|
|
13
|
+
|
|
14
|
+
describe('init.remove acts where init writes, the project root, and in the folder it runs in', () => {
|
|
15
|
+
/** @type {string} */
|
|
16
|
+
let project;
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
project = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-rm-root-'));
|
|
19
|
+
fs.writeFileSync(
|
|
20
|
+
path.join(project, 'package.json'),
|
|
21
|
+
JSON.stringify({name: 'app', dependencies: {'@astryxdesign/core': '1.0.0'}}),
|
|
22
|
+
);
|
|
23
|
+
fs.mkdirSync(path.join(project, 'src', 'app'), {recursive: true});
|
|
24
|
+
});
|
|
25
|
+
afterEach(() => { fs.rmSync(project, {recursive: true, force: true}); });
|
|
26
|
+
const block = '<!-- ASTRYX:START -->\nAstryx v0.7.0 · old\n<!-- ASTRYX:END -->\n';
|
|
27
|
+
|
|
28
|
+
it('removes from a subfolder what init wrote at the project root', async () => {
|
|
29
|
+
const sub = path.join(project, 'src', 'app');
|
|
30
|
+
const written = await init({features: ['agents']}, {cwd: sub});
|
|
31
|
+
expect(written.data.docsWritten).toEqual(['../../AGENTS.md', '../../GEMINI.md']);
|
|
32
|
+
|
|
33
|
+
const r = await remove({cwd: sub});
|
|
34
|
+
expect(r).toEqual({type: 'init.remove', data: {removed: true}});
|
|
35
|
+
// Both files held only what init wrote, so they are deleted.
|
|
36
|
+
expect(fs.existsSync(path.join(project, 'AGENTS.md'))).toBe(false);
|
|
37
|
+
expect(fs.existsSync(path.join(project, 'GEMINI.md'))).toBe(false);
|
|
38
|
+
expect((await remove({cwd: sub})).data.removed).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('removes a block an earlier version left in the subfolder, and keeps the files\' own text', async () => {
|
|
42
|
+
const sub = path.join(project, 'src', 'app');
|
|
43
|
+
fs.writeFileSync(path.join(project, 'AGENTS.md'), `# Agents\n\nRoot rules.\n\n${block}`);
|
|
44
|
+
fs.writeFileSync(path.join(sub, 'AGENTS.md'), `# App\n\nApp rules.\n\n${block}`);
|
|
45
|
+
|
|
46
|
+
expect((await remove({cwd: sub})).data.removed).toBe(true);
|
|
47
|
+
expect(fs.readFileSync(path.join(project, 'AGENTS.md'), 'utf8').trimEnd()).toBe('# Agents\n\nRoot rules.');
|
|
48
|
+
expect(fs.readFileSync(path.join(sub, 'AGENTS.md'), 'utf8').trimEnd()).toBe('# App\n\nApp rules.');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('run at the project root, leaves a subfolder alone', async () => {
|
|
52
|
+
const sub = path.join(project, 'src', 'app');
|
|
53
|
+
fs.writeFileSync(path.join(project, 'AGENTS.md'), `# Agents\n\n${block}`);
|
|
54
|
+
fs.writeFileSync(path.join(sub, 'AGENTS.md'), `# App\n\n${block}`);
|
|
55
|
+
|
|
56
|
+
expect((await remove({cwd: project})).data.removed).toBe(true);
|
|
57
|
+
expect(fs.readFileSync(path.join(sub, 'AGENTS.md'), 'utf8')).toContain('<!-- ASTRYX:START -->');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('below the root, leaves a root agent file linked outside the project with a note, and removes the rest (0.7.x exited 0)', async () => {
|
|
61
|
+
// A workspace member whose CLAUDE.md links to the monorepo's.
|
|
62
|
+
const monorepo = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-rm-mono-'));
|
|
63
|
+
try {
|
|
64
|
+
const web = path.join(monorepo, 'apps', 'web');
|
|
65
|
+
const sub = path.join(web, 'src');
|
|
66
|
+
fs.mkdirSync(sub, {recursive: true});
|
|
67
|
+
fs.writeFileSync(
|
|
68
|
+
path.join(web, 'package.json'),
|
|
69
|
+
JSON.stringify({name: 'web', dependencies: {'@astryxdesign/core': '1.0.0'}}),
|
|
70
|
+
);
|
|
71
|
+
fs.writeFileSync(path.join(monorepo, 'CLAUDE.md'), `# Shared\n\n${block}`);
|
|
72
|
+
fs.symlinkSync('../../CLAUDE.md', path.join(web, 'CLAUDE.md'));
|
|
73
|
+
fs.writeFileSync(path.join(web, 'AGENTS.md'), `# Web\n\n${block}`);
|
|
74
|
+
fs.writeFileSync(path.join(sub, 'AGENTS.md'), `# Src\n\n${block}`);
|
|
75
|
+
const warned = [];
|
|
76
|
+
const warn = vi.spyOn(console, 'error').mockImplementation((...a) => warned.push(a.join(' ')));
|
|
77
|
+
logger.setSilent(false);
|
|
78
|
+
let r;
|
|
79
|
+
try {
|
|
80
|
+
r = await remove({cwd: sub});
|
|
81
|
+
} finally {
|
|
82
|
+
logger.setSilent(true);
|
|
83
|
+
warn.mockRestore();
|
|
84
|
+
}
|
|
85
|
+
expect(r).toEqual({type: 'init.remove', data: {removed: true}});
|
|
86
|
+
expect(fs.readFileSync(path.join(web, 'AGENTS.md'), 'utf8').trimEnd()).toBe('# Web');
|
|
87
|
+
expect(fs.readFileSync(path.join(sub, 'AGENTS.md'), 'utf8').trimEnd()).toBe('# Src');
|
|
88
|
+
// The file outside the project keeps its block.
|
|
89
|
+
expect(fs.readFileSync(path.join(monorepo, 'CLAUDE.md'), 'utf8')).toContain('<!-- ASTRYX:START -->');
|
|
90
|
+
expect(warned.join('\n')).toMatch(/Note: Left as it is: \.\.\/CLAUDE\.md \(/u);
|
|
91
|
+
} finally {
|
|
92
|
+
fs.rmSync(monorepo, {recursive: true, force: true});
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('at the root, a root agent file linked outside the project still refuses the run', async () => {
|
|
97
|
+
const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-rm-outside-'));
|
|
98
|
+
try {
|
|
99
|
+
fs.writeFileSync(path.join(outside, 'CLAUDE.md'), `# Shared\n\n${block}`);
|
|
100
|
+
fs.symlinkSync(path.join(outside, 'CLAUDE.md'), path.join(project, 'CLAUDE.md'));
|
|
101
|
+
fs.writeFileSync(path.join(project, 'AGENTS.md'), `# Agents\n\n${block}`);
|
|
102
|
+
await expect(remove({cwd: project})).rejects.toMatchObject({code: 'ERR_PATH_TRAVERSAL'});
|
|
103
|
+
expect(fs.readFileSync(path.join(project, 'AGENTS.md'), 'utf8')).toContain('<!-- ASTRYX:START -->');
|
|
104
|
+
} finally {
|
|
105
|
+
fs.rmSync(outside, {recursive: true, force: true});
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('a link in the subfolder that escapes it refuses the run before anything is written', async () => {
|
|
110
|
+
const sub = path.join(project, 'src', 'app');
|
|
111
|
+
const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-rm-outside-'));
|
|
112
|
+
try {
|
|
113
|
+
fs.writeFileSync(path.join(project, 'AGENTS.md'), `# Agents\n\n${block}`);
|
|
114
|
+
fs.writeFileSync(path.join(outside, 'CLAUDE.md'), `# Shared\n\n${block}`);
|
|
115
|
+
fs.symlinkSync(path.join(outside, 'CLAUDE.md'), path.join(sub, 'CLAUDE.md'));
|
|
116
|
+
|
|
117
|
+
await expect(remove({cwd: sub})).rejects.toMatchObject({code: 'ERR_PATH_TRAVERSAL'});
|
|
118
|
+
expect(fs.readFileSync(path.join(project, 'AGENTS.md'), 'utf8')).toContain('<!-- ASTRYX:START -->');
|
|
119
|
+
expect(fs.readFileSync(path.join(outside, 'CLAUDE.md'), 'utf8')).toContain('<!-- ASTRYX:START -->');
|
|
120
|
+
} finally {
|
|
121
|
+
fs.rmSync(outside, {recursive: true, force: true});
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
});
|