compose-agentsmd 1.1.2 → 2.0.1
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 +16 -48
- package/agent-ruleset.schema.json +9 -13
- package/dist/compose-agents.js +184 -48
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,10 +6,7 @@ This repository contains CLI tooling for composing per-project `AGENTS.md` files
|
|
|
6
6
|
|
|
7
7
|
See `CHANGELOG.md` for release notes.
|
|
8
8
|
|
|
9
|
-
It is intended to be used together with shared rule modules such as
|
|
10
|
-
|
|
11
|
-
- `agent-rules/` (public rule modules)
|
|
12
|
-
- `agent-rules-private/` (optional, private-only rule modules)
|
|
9
|
+
It is intended to be used together with shared rule modules such as the public `agent-rules` repository.
|
|
13
10
|
|
|
14
11
|
## Install (global CLI)
|
|
15
12
|
|
|
@@ -23,17 +20,7 @@ This provides the `compose-agentsmd` command.
|
|
|
23
20
|
|
|
24
21
|
## Rules setup (this repository)
|
|
25
22
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
Initialize submodules and compose the rules:
|
|
29
|
-
|
|
30
|
-
```sh
|
|
31
|
-
git submodule update --init --recursive
|
|
32
|
-
npm install
|
|
33
|
-
npm run compose
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
The default ruleset for this repository is `agent-ruleset.json` and currently composes the `node` domain into `AGENTS.md`.
|
|
23
|
+
The default ruleset for this repository is `agent-ruleset.json` and currently composes the `node` domain into `AGENTS.md` from the shared GitHub source.
|
|
37
24
|
|
|
38
25
|
## Compose
|
|
39
26
|
|
|
@@ -47,59 +34,40 @@ The tool searches for `agent-ruleset.json` under the given root directory (defau
|
|
|
47
34
|
|
|
48
35
|
The tool prepends a small "Tool Rules" block to every generated `AGENTS.md` so agents know how to regenerate or update rules.
|
|
49
36
|
|
|
50
|
-
### Rules root resolution (important for global installs)
|
|
51
|
-
|
|
52
|
-
When installed globally, the rules directory is usually outside the project. You can point to it in either of the following ways:
|
|
53
|
-
|
|
54
|
-
```sh
|
|
55
|
-
compose-agentsmd --rules-root "C:/path/to/agent-rules/rules"
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
Or via environment variable:
|
|
59
|
-
|
|
60
|
-
```sh
|
|
61
|
-
set AGENT_RULES_ROOT=C:/path/to/agent-rules/rules
|
|
62
|
-
compose-agentsmd
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
Rules root resolution precedence is:
|
|
66
|
-
|
|
67
|
-
- `--rules-root` CLI option
|
|
68
|
-
- `AGENT_RULES_ROOT` environment variable
|
|
69
|
-
- `rulesRoot` in the ruleset file
|
|
70
|
-
- Default: `agent-rules/rules` relative to the ruleset file
|
|
71
|
-
|
|
72
37
|
## Project ruleset format
|
|
73
38
|
|
|
74
39
|
```json
|
|
75
40
|
{
|
|
76
|
-
"
|
|
41
|
+
"source": "github:org/agent-rules@latest",
|
|
77
42
|
"domains": ["node", "unreal"],
|
|
78
|
-
"
|
|
43
|
+
"extra": ["agent-rules-local/custom.md"],
|
|
44
|
+
"output": "AGENTS.md"
|
|
79
45
|
}
|
|
80
46
|
```
|
|
81
47
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
- `
|
|
85
|
-
- `rules`
|
|
48
|
+
Ruleset keys:
|
|
49
|
+
|
|
50
|
+
- `source` (required): rules source. Use `github:owner/repo@ref` or a local path.
|
|
51
|
+
- `global` (optional): include `rules/global` (defaults to true). Omit this unless you want to disable globals.
|
|
52
|
+
- `domains` (optional): domain folders under `rules/domains/<domain>`.
|
|
53
|
+
- `extra` (optional): additional local rule files to append.
|
|
54
|
+
- `output` (optional): output file name (defaults to `AGENTS.md`).
|
|
86
55
|
|
|
87
56
|
### Ruleset schema validation
|
|
88
57
|
|
|
89
58
|
`compose-agentsmd` validates rulesets against `agent-ruleset.schema.json` on every run. If the ruleset does not conform to the schema, the tool exits with a schema error.
|
|
90
59
|
|
|
91
|
-
|
|
60
|
+
### Cache
|
|
92
61
|
|
|
93
|
-
- `
|
|
94
|
-
- `globalDir`: override `global` (relative to `rulesRoot`).
|
|
95
|
-
- `domainsDir`: override `domains` (relative to `rulesRoot`).
|
|
62
|
+
Remote sources are cached under `~/.agentsmd/<owner>/<repo>/<ref>/`. Use `--refresh` to re-fetch or `--clear-cache` to remove cached rules.
|
|
96
63
|
|
|
97
64
|
### Optional arguments
|
|
98
65
|
|
|
99
66
|
- `--root <path>`: project root (defaults to current working directory)
|
|
100
67
|
- `--ruleset <path>`: only compose a single ruleset file
|
|
101
68
|
- `--ruleset-name <name>`: override the ruleset filename (default: `agent-ruleset.json`)
|
|
102
|
-
- `--
|
|
69
|
+
- `--refresh`: refresh cached remote rules
|
|
70
|
+
- `--clear-cache`: remove cached remote rules and exit
|
|
103
71
|
|
|
104
72
|
## Development
|
|
105
73
|
|
|
@@ -3,7 +3,15 @@
|
|
|
3
3
|
"title": "Compose AGENTS.md ruleset",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"additionalProperties": false,
|
|
6
|
+
"required": ["source"],
|
|
6
7
|
"properties": {
|
|
8
|
+
"source": {
|
|
9
|
+
"type": "string",
|
|
10
|
+
"minLength": 1
|
|
11
|
+
},
|
|
12
|
+
"global": {
|
|
13
|
+
"type": "boolean"
|
|
14
|
+
},
|
|
7
15
|
"output": {
|
|
8
16
|
"type": "string",
|
|
9
17
|
"minLength": 1
|
|
@@ -15,24 +23,12 @@
|
|
|
15
23
|
"minLength": 1
|
|
16
24
|
}
|
|
17
25
|
},
|
|
18
|
-
"
|
|
26
|
+
"extra": {
|
|
19
27
|
"type": "array",
|
|
20
28
|
"items": {
|
|
21
29
|
"type": "string",
|
|
22
30
|
"minLength": 1
|
|
23
31
|
}
|
|
24
|
-
},
|
|
25
|
-
"rulesRoot": {
|
|
26
|
-
"type": "string",
|
|
27
|
-
"minLength": 1
|
|
28
|
-
},
|
|
29
|
-
"globalDir": {
|
|
30
|
-
"type": "string",
|
|
31
|
-
"minLength": 1
|
|
32
|
-
},
|
|
33
|
-
"domainsDir": {
|
|
34
|
-
"type": "string",
|
|
35
|
-
"minLength": 1
|
|
36
32
|
}
|
|
37
33
|
}
|
|
38
34
|
}
|
package/dist/compose-agents.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
4
6
|
import { Ajv } from "ajv";
|
|
5
7
|
const DEFAULT_RULESET_NAME = "agent-ruleset.json";
|
|
6
|
-
const DEFAULT_RULES_ROOT = "agent-rules/rules";
|
|
7
|
-
const DEFAULT_GLOBAL_DIR = "global";
|
|
8
|
-
const DEFAULT_DOMAINS_DIR = "domains";
|
|
9
|
-
const RULES_ROOT_ENV_VAR = "AGENT_RULES_ROOT";
|
|
10
8
|
const DEFAULT_OUTPUT = "AGENTS.md";
|
|
9
|
+
const DEFAULT_CACHE_ROOT = path.join(os.homedir(), ".agentsmd");
|
|
11
10
|
const RULESET_SCHEMA_PATH = new URL("../agent-ruleset.schema.json", import.meta.url);
|
|
12
11
|
const TOOL_RULES = [
|
|
13
12
|
"# Tool Rules (compose-agentsmd)",
|
|
@@ -30,13 +29,14 @@ const DEFAULT_IGNORE_DIRS = new Set([
|
|
|
30
29
|
".turbo",
|
|
31
30
|
"coverage"
|
|
32
31
|
]);
|
|
33
|
-
const usage = `Usage: compose-agentsmd [--root <path>] [--ruleset <path>] [--ruleset-name <name>] [--
|
|
32
|
+
const usage = `Usage: compose-agentsmd [--root <path>] [--ruleset <path>] [--ruleset-name <name>] [--refresh] [--clear-cache]
|
|
34
33
|
|
|
35
34
|
Options:
|
|
36
35
|
--root <path> Project root directory (default: current working directory)
|
|
37
36
|
--ruleset <path> Only compose a single ruleset file
|
|
38
37
|
--ruleset-name <name> Ruleset filename to search for (default: agent-ruleset.json)
|
|
39
|
-
--
|
|
38
|
+
--refresh Refresh cached remote rules
|
|
39
|
+
--clear-cache Remove cached remote rules and exit
|
|
40
40
|
`;
|
|
41
41
|
const parseArgs = (argv) => {
|
|
42
42
|
const args = {};
|
|
@@ -73,13 +73,12 @@ const parseArgs = (argv) => {
|
|
|
73
73
|
i += 1;
|
|
74
74
|
continue;
|
|
75
75
|
}
|
|
76
|
-
if (arg === "--
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
args.
|
|
82
|
-
i += 1;
|
|
76
|
+
if (arg === "--refresh") {
|
|
77
|
+
args.refresh = true;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (arg === "--clear-cache") {
|
|
81
|
+
args.clearCache = true;
|
|
83
82
|
continue;
|
|
84
83
|
}
|
|
85
84
|
throw new Error(`Unknown argument: ${arg}`);
|
|
@@ -109,6 +108,11 @@ const resolveFrom = (baseDir, targetPath) => {
|
|
|
109
108
|
}
|
|
110
109
|
return path.resolve(baseDir, targetPath);
|
|
111
110
|
};
|
|
111
|
+
const clearCache = () => {
|
|
112
|
+
if (fs.existsSync(DEFAULT_CACHE_ROOT)) {
|
|
113
|
+
fs.rmSync(DEFAULT_CACHE_ROOT, { recursive: true, force: true });
|
|
114
|
+
}
|
|
115
|
+
};
|
|
112
116
|
const ensureFileExists = (filePath) => {
|
|
113
117
|
if (!fs.existsSync(filePath)) {
|
|
114
118
|
throw new Error(`Missing file: ${filePath}`);
|
|
@@ -138,32 +142,10 @@ const readProjectRuleset = (rulesetPath) => {
|
|
|
138
142
|
if (ruleset.output === undefined) {
|
|
139
143
|
ruleset.output = DEFAULT_OUTPUT;
|
|
140
144
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const resolveRulesRoot = (rulesetDir, projectRuleset, options) => {
|
|
144
|
-
if (isNonEmptyString(options.cliRulesRoot)) {
|
|
145
|
-
return resolveFrom(rulesetDir, options.cliRulesRoot);
|
|
146
|
-
}
|
|
147
|
-
const envRulesRoot = process.env[RULES_ROOT_ENV_VAR];
|
|
148
|
-
if (isNonEmptyString(envRulesRoot)) {
|
|
149
|
-
return resolveFrom(rulesetDir, envRulesRoot);
|
|
145
|
+
if (ruleset.global === undefined) {
|
|
146
|
+
ruleset.global = true;
|
|
150
147
|
}
|
|
151
|
-
|
|
152
|
-
return resolveFrom(rulesetDir, projectRuleset.rulesRoot);
|
|
153
|
-
}
|
|
154
|
-
return path.resolve(rulesetDir, DEFAULT_RULES_ROOT);
|
|
155
|
-
};
|
|
156
|
-
const resolveGlobalRoot = (rulesRoot, projectRuleset) => {
|
|
157
|
-
const globalDirName = isNonEmptyString(projectRuleset.globalDir)
|
|
158
|
-
? projectRuleset.globalDir
|
|
159
|
-
: DEFAULT_GLOBAL_DIR;
|
|
160
|
-
return path.resolve(rulesRoot, globalDirName);
|
|
161
|
-
};
|
|
162
|
-
const resolveDomainsRoot = (rulesRoot, projectRuleset) => {
|
|
163
|
-
const domainsDirName = isNonEmptyString(projectRuleset.domainsDir)
|
|
164
|
-
? projectRuleset.domainsDir
|
|
165
|
-
: DEFAULT_DOMAINS_DIR;
|
|
166
|
-
return path.resolve(rulesRoot, domainsDirName);
|
|
148
|
+
return ruleset;
|
|
167
149
|
};
|
|
168
150
|
const collectMarkdownFiles = (rootDir) => {
|
|
169
151
|
ensureDirectoryExists(rootDir);
|
|
@@ -203,27 +185,176 @@ const addRulePaths = (rulePaths, resolvedRules, seenRules) => {
|
|
|
203
185
|
seenRules.add(resolvedRulePath);
|
|
204
186
|
}
|
|
205
187
|
};
|
|
188
|
+
const sanitizeCacheSegment = (value) => value.replace(/[\\/]/gu, "__");
|
|
189
|
+
const looksLikeCommitHash = (value) => /^[a-f0-9]{7,40}$/iu.test(value);
|
|
190
|
+
const execGit = (args, cwd) => execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
191
|
+
const parseGithubSource = (source) => {
|
|
192
|
+
const trimmed = source.trim();
|
|
193
|
+
if (!trimmed.startsWith("github:")) {
|
|
194
|
+
throw new Error(`Unsupported source: ${source}`);
|
|
195
|
+
}
|
|
196
|
+
const withoutPrefix = trimmed.slice("github:".length);
|
|
197
|
+
const [repoPart, refPart] = withoutPrefix.split("@");
|
|
198
|
+
const [owner, repo] = repoPart.split("/");
|
|
199
|
+
if (!isNonEmptyString(owner) || !isNonEmptyString(repo)) {
|
|
200
|
+
throw new Error(`Invalid GitHub source (expected github:owner/repo@ref): ${source}`);
|
|
201
|
+
}
|
|
202
|
+
const ref = isNonEmptyString(refPart) ? refPart : "latest";
|
|
203
|
+
return { owner, repo, ref, url: `https://github.com/${owner}/${repo}.git` };
|
|
204
|
+
};
|
|
205
|
+
const parseSemver = (tag) => {
|
|
206
|
+
const cleaned = tag.startsWith("v") ? tag.slice(1) : tag;
|
|
207
|
+
const parts = cleaned.split(".");
|
|
208
|
+
if (parts.length < 2 || parts.length > 3) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
const numbers = parts.map((part) => Number(part));
|
|
212
|
+
if (numbers.some((value) => Number.isNaN(value))) {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
return numbers;
|
|
216
|
+
};
|
|
217
|
+
const compareSemver = (a, b) => {
|
|
218
|
+
const maxLength = Math.max(a.length, b.length);
|
|
219
|
+
for (let i = 0; i < maxLength; i += 1) {
|
|
220
|
+
const left = a[i] ?? 0;
|
|
221
|
+
const right = b[i] ?? 0;
|
|
222
|
+
if (left !== right) {
|
|
223
|
+
return left - right;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return 0;
|
|
227
|
+
};
|
|
228
|
+
const resolveLatestTag = (repoUrl) => {
|
|
229
|
+
const raw = execGit(["ls-remote", "--tags", "--refs", repoUrl]);
|
|
230
|
+
if (!raw) {
|
|
231
|
+
return {};
|
|
232
|
+
}
|
|
233
|
+
const candidates = raw
|
|
234
|
+
.split("\n")
|
|
235
|
+
.map((line) => line.trim())
|
|
236
|
+
.filter(Boolean)
|
|
237
|
+
.map((line) => {
|
|
238
|
+
const [hash, ref] = line.split(/\s+/u);
|
|
239
|
+
const tag = ref?.replace("refs/tags/", "");
|
|
240
|
+
if (!hash || !tag) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
const semver = parseSemver(tag);
|
|
244
|
+
if (!semver) {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
return { hash, tag, semver };
|
|
248
|
+
})
|
|
249
|
+
.filter((item) => Boolean(item));
|
|
250
|
+
if (candidates.length === 0) {
|
|
251
|
+
return {};
|
|
252
|
+
}
|
|
253
|
+
candidates.sort((a, b) => compareSemver(a.semver, b.semver));
|
|
254
|
+
const latest = candidates[candidates.length - 1];
|
|
255
|
+
return { tag: latest.tag, hash: latest.hash };
|
|
256
|
+
};
|
|
257
|
+
const resolveHeadHash = (repoUrl) => {
|
|
258
|
+
const raw = execGit(["ls-remote", repoUrl, "HEAD"]);
|
|
259
|
+
const [hash] = raw.split(/\s+/u);
|
|
260
|
+
if (!hash) {
|
|
261
|
+
throw new Error(`Unable to resolve HEAD for ${repoUrl}`);
|
|
262
|
+
}
|
|
263
|
+
return hash;
|
|
264
|
+
};
|
|
265
|
+
const resolveRefHash = (repoUrl, ref) => {
|
|
266
|
+
const raw = execGit(["ls-remote", repoUrl, ref, `refs/tags/${ref}`, `refs/heads/${ref}`]);
|
|
267
|
+
if (!raw) {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
const [hash] = raw.split(/\s+/u);
|
|
271
|
+
return hash ?? null;
|
|
272
|
+
};
|
|
273
|
+
const ensureDir = (dirPath) => {
|
|
274
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
275
|
+
};
|
|
276
|
+
const cloneAtRef = (repoUrl, ref, destination) => {
|
|
277
|
+
execGit(["clone", "--depth", "1", "--branch", ref, repoUrl, destination]);
|
|
278
|
+
};
|
|
279
|
+
const fetchCommit = (repoUrl, commitHash, destination) => {
|
|
280
|
+
ensureDir(destination);
|
|
281
|
+
execGit(["init"], destination);
|
|
282
|
+
execGit(["remote", "add", "origin", repoUrl], destination);
|
|
283
|
+
execGit(["fetch", "--depth", "1", "origin", commitHash], destination);
|
|
284
|
+
execGit(["checkout", "FETCH_HEAD"], destination);
|
|
285
|
+
};
|
|
286
|
+
const resolveGithubRulesRoot = (source, refresh) => {
|
|
287
|
+
const parsed = parseGithubSource(source);
|
|
288
|
+
const resolved = parsed.ref === "latest" ? resolveLatestTag(parsed.url) : null;
|
|
289
|
+
const resolvedRef = resolved?.tag ?? (parsed.ref === "latest" ? "HEAD" : parsed.ref);
|
|
290
|
+
const resolvedHash = resolved?.hash ??
|
|
291
|
+
(resolvedRef === "HEAD"
|
|
292
|
+
? resolveHeadHash(parsed.url)
|
|
293
|
+
: resolveRefHash(parsed.url, resolvedRef));
|
|
294
|
+
if (!resolvedHash && !looksLikeCommitHash(resolvedRef)) {
|
|
295
|
+
throw new Error(`Unable to resolve ref ${resolvedRef} for ${parsed.url}`);
|
|
296
|
+
}
|
|
297
|
+
const cacheSegment = resolvedRef === "HEAD" ? sanitizeCacheSegment(resolvedHash ?? resolvedRef) : sanitizeCacheSegment(resolvedRef);
|
|
298
|
+
const cacheDir = path.join(DEFAULT_CACHE_ROOT, parsed.owner, parsed.repo, cacheSegment);
|
|
299
|
+
if (refresh && fs.existsSync(cacheDir)) {
|
|
300
|
+
fs.rmSync(cacheDir, { recursive: true, force: true });
|
|
301
|
+
}
|
|
302
|
+
if (!fs.existsSync(cacheDir)) {
|
|
303
|
+
ensureDir(path.dirname(cacheDir));
|
|
304
|
+
try {
|
|
305
|
+
cloneAtRef(parsed.url, resolvedRef, cacheDir);
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
if (resolvedHash && looksLikeCommitHash(resolvedHash)) {
|
|
309
|
+
fetchCommit(parsed.url, resolvedHash, cacheDir);
|
|
310
|
+
}
|
|
311
|
+
else if (looksLikeCommitHash(resolvedRef)) {
|
|
312
|
+
fetchCommit(parsed.url, resolvedRef, cacheDir);
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
throw error;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
const rulesRoot = path.join(cacheDir, "rules");
|
|
320
|
+
ensureDirectoryExists(rulesRoot);
|
|
321
|
+
return { rulesRoot, resolvedRef };
|
|
322
|
+
};
|
|
323
|
+
const resolveLocalRulesRoot = (rulesetDir, source) => {
|
|
324
|
+
const resolvedSource = resolveFrom(rulesetDir, source);
|
|
325
|
+
if (!fs.existsSync(resolvedSource)) {
|
|
326
|
+
throw new Error(`Missing source path: ${resolvedSource}`);
|
|
327
|
+
}
|
|
328
|
+
const candidate = path.basename(resolvedSource) === "rules" ? resolvedSource : path.join(resolvedSource, "rules");
|
|
329
|
+
ensureDirectoryExists(candidate);
|
|
330
|
+
return candidate;
|
|
331
|
+
};
|
|
332
|
+
const resolveRulesRoot = (rulesetDir, source, refresh) => {
|
|
333
|
+
if (source.startsWith("github:")) {
|
|
334
|
+
return resolveGithubRulesRoot(source, refresh);
|
|
335
|
+
}
|
|
336
|
+
return { rulesRoot: resolveLocalRulesRoot(rulesetDir, source) };
|
|
337
|
+
};
|
|
206
338
|
const composeRuleset = (rulesetPath, rootDir, options) => {
|
|
207
339
|
const rulesetDir = path.dirname(rulesetPath);
|
|
208
340
|
const projectRuleset = readProjectRuleset(rulesetPath);
|
|
209
341
|
const outputFileName = projectRuleset.output ?? DEFAULT_OUTPUT;
|
|
210
342
|
const outputPath = resolveFrom(rulesetDir, outputFileName);
|
|
211
|
-
const rulesRoot = resolveRulesRoot(rulesetDir, projectRuleset,
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const globalRoot = resolveGlobalRoot(rulesRoot, projectRuleset);
|
|
215
|
-
const domainsRoot = resolveDomainsRoot(rulesRoot, projectRuleset);
|
|
343
|
+
const { rulesRoot } = resolveRulesRoot(rulesetDir, projectRuleset.source, options.refresh ?? false);
|
|
344
|
+
const globalRoot = path.join(rulesRoot, "global");
|
|
345
|
+
const domainsRoot = path.join(rulesRoot, "domains");
|
|
216
346
|
const resolvedRules = [];
|
|
217
347
|
const seenRules = new Set();
|
|
218
|
-
|
|
219
|
-
|
|
348
|
+
if (projectRuleset.global !== false) {
|
|
349
|
+
addRulePaths(collectMarkdownFiles(globalRoot), resolvedRules, seenRules);
|
|
350
|
+
}
|
|
220
351
|
const domains = Array.isArray(projectRuleset.domains) ? projectRuleset.domains : [];
|
|
221
352
|
for (const domain of domains) {
|
|
222
353
|
const domainRoot = path.resolve(domainsRoot, domain);
|
|
223
354
|
addRulePaths(collectMarkdownFiles(domainRoot), resolvedRules, seenRules);
|
|
224
355
|
}
|
|
225
|
-
const
|
|
226
|
-
const directRulePaths =
|
|
356
|
+
const extraRules = Array.isArray(projectRuleset.extra) ? projectRuleset.extra : [];
|
|
357
|
+
const directRulePaths = extraRules.map((rulePath) => resolveFrom(rulesetDir, rulePath));
|
|
227
358
|
addRulePaths(directRulePaths, resolvedRules, seenRules);
|
|
228
359
|
const parts = resolvedRules.map((rulePath) => normalizeTrailingWhitespace(fs.readFileSync(rulePath, "utf8")));
|
|
229
360
|
const lintHeader = "<!-- markdownlint-disable MD025 -->";
|
|
@@ -272,6 +403,11 @@ const main = () => {
|
|
|
272
403
|
process.stdout.write(`${usage}\n`);
|
|
273
404
|
return;
|
|
274
405
|
}
|
|
406
|
+
if (args.clearCache) {
|
|
407
|
+
clearCache();
|
|
408
|
+
process.stdout.write("Cache cleared.\n");
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
275
411
|
const rootDir = args.root ? path.resolve(args.root) : process.cwd();
|
|
276
412
|
const rulesetName = args.rulesetName || DEFAULT_RULESET_NAME;
|
|
277
413
|
const rulesetFiles = getRulesetFiles(rootDir, args.ruleset, rulesetName);
|
|
@@ -280,7 +416,7 @@ const main = () => {
|
|
|
280
416
|
}
|
|
281
417
|
const outputs = rulesetFiles
|
|
282
418
|
.sort()
|
|
283
|
-
.map((rulesetPath) => composeRuleset(rulesetPath, rootDir, {
|
|
419
|
+
.map((rulesetPath) => composeRuleset(rulesetPath, rootDir, { refresh: args.refresh }));
|
|
284
420
|
process.stdout.write(`Composed AGENTS.md:\n${outputs.map((file) => `- ${file}`).join("\n")}\n`);
|
|
285
421
|
};
|
|
286
422
|
try {
|