compose-agentsmd 2.0.0 → 2.0.2

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 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
- This repository expects the public rules to be available at `agent-rules/rules` via the `agent-rules/` submodule.
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,6 +34,23 @@ 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
 
37
+ ## Updating shared rules
38
+
39
+ For GitHub sources, the tool keeps two locations:
40
+
41
+ - Cache: `~/.agentsmd/cache/<owner>/<repo>/<ref>/` (read-only, used for compose)
42
+ - Workspace: `~/.agentsmd/workspace/<owner>/<repo>/` (editable)
43
+
44
+ Update flow:
45
+
46
+ ```sh
47
+ compose-agentsmd edit-rules
48
+ # edit files under rules/ in the workspace
49
+ compose-agentsmd apply-rules
50
+ ```
51
+
52
+ `edit-rules` clones the GitHub source into the workspace (or reuses it). `apply-rules` pushes the workspace (if clean) and regenerates `AGENTS.md` by refreshing the cache. If your `source` is a local path, `edit-rules` will just print that path and `apply-rules` will skip the push.
53
+
50
54
  ## Project ruleset format
51
55
 
52
56
  ```json
@@ -72,7 +76,7 @@ Ruleset keys:
72
76
 
73
77
  ### Cache
74
78
 
75
- Remote sources are cached under `~/.agentsmd/<owner>/<repo>/<ref>/`. Use `--refresh` to re-fetch or `--clear-cache` to remove cached rules.
79
+ Remote sources are cached under `~/.agentsmd/cache/<owner>/<repo>/<ref>/`. Use `--refresh` to re-fetch or `--clear-cache` to remove cached rules.
76
80
 
77
81
  ### Optional arguments
78
82
 
@@ -81,6 +85,8 @@ Remote sources are cached under `~/.agentsmd/<owner>/<repo>/<ref>/`. Use `--refr
81
85
  - `--ruleset-name <name>`: override the ruleset filename (default: `agent-ruleset.json`)
82
86
  - `--refresh`: refresh cached remote rules
83
87
  - `--clear-cache`: remove cached remote rules and exit
88
+ - `edit-rules`: prepare or locate a writable rules workspace
89
+ - `apply-rules`: push workspace changes (if GitHub source) and regenerate rules with refresh
84
90
 
85
91
  ## Development
86
92
 
@@ -6,14 +6,9 @@ import { execFileSync } from "node:child_process";
6
6
  import { Ajv } from "ajv";
7
7
  const DEFAULT_RULESET_NAME = "agent-ruleset.json";
8
8
  const DEFAULT_OUTPUT = "AGENTS.md";
9
- const DEFAULT_CACHE_ROOT = path.join(os.homedir(), ".agentsmd");
9
+ const DEFAULT_CACHE_ROOT = path.join(os.homedir(), ".agentsmd", "cache");
10
+ const DEFAULT_WORKSPACE_ROOT = path.join(os.homedir(), ".agentsmd", "workspace");
10
11
  const RULESET_SCHEMA_PATH = new URL("../agent-ruleset.schema.json", import.meta.url);
11
- const TOOL_RULES = [
12
- "# Tool Rules (compose-agentsmd)",
13
- "- Before starting any work, run `compose-agentsmd` from the project root.",
14
- "- To update rules, update the source rule modules or ruleset, then rerun `compose-agentsmd`.",
15
- "- Do not edit `AGENTS.md` directly; update the source rules and regenerate."
16
- ].join("\n");
17
12
  const DEFAULT_IGNORE_DIRS = new Set([
18
13
  ".git",
19
14
  "agent-rules",
@@ -29,25 +24,23 @@ const DEFAULT_IGNORE_DIRS = new Set([
29
24
  ".turbo",
30
25
  "coverage"
31
26
  ]);
32
- const usage = `Usage: compose-agentsmd [--root <path>] [--ruleset <path>] [--ruleset-name <name>] [--refresh] [--clear-cache]
33
-
34
- Options:
35
- --root <path> Project root directory (default: current working directory)
36
- --ruleset <path> Only compose a single ruleset file
37
- --ruleset-name <name> Ruleset filename to search for (default: agent-ruleset.json)
38
- --refresh Refresh cached remote rules
39
- --clear-cache Remove cached remote rules and exit
40
- `;
27
+ const TOOL_RULES_PATH = new URL("../tools/tool-rules.md", import.meta.url);
28
+ const USAGE_PATH = new URL("../tools/usage.txt", import.meta.url);
41
29
  const parseArgs = (argv) => {
42
30
  const args = {};
43
- for (let i = 0; i < argv.length; i += 1) {
44
- const arg = argv[i];
31
+ const knownCommands = new Set(["edit-rules", "apply-rules"]);
32
+ const remaining = [...argv];
33
+ if (remaining.length > 0 && knownCommands.has(remaining[0])) {
34
+ args.command = remaining.shift();
35
+ }
36
+ for (let i = 0; i < remaining.length; i += 1) {
37
+ const arg = remaining[i];
45
38
  if (arg === "--help" || arg === "-h") {
46
39
  args.help = true;
47
40
  continue;
48
41
  }
49
42
  if (arg === "--root") {
50
- const value = argv[i + 1];
43
+ const value = remaining[i + 1];
51
44
  if (!value) {
52
45
  throw new Error("Missing value for --root");
53
46
  }
@@ -56,7 +49,7 @@ const parseArgs = (argv) => {
56
49
  continue;
57
50
  }
58
51
  if (arg === "--ruleset") {
59
- const value = argv[i + 1];
52
+ const value = remaining[i + 1];
60
53
  if (!value) {
61
54
  throw new Error("Missing value for --ruleset");
62
55
  }
@@ -65,7 +58,7 @@ const parseArgs = (argv) => {
65
58
  continue;
66
59
  }
67
60
  if (arg === "--ruleset-name") {
68
- const value = argv[i + 1];
61
+ const value = remaining[i + 1];
69
62
  if (!value) {
70
63
  throw new Error("Missing value for --ruleset-name");
71
64
  }
@@ -88,7 +81,9 @@ const parseArgs = (argv) => {
88
81
  const normalizeTrailingWhitespace = (content) => content.replace(/\s+$/u, "");
89
82
  const normalizePath = (filePath) => filePath.replace(/\\/g, "/");
90
83
  const isNonEmptyString = (value) => typeof value === "string" && value.trim() !== "";
84
+ const usage = normalizeTrailingWhitespace(fs.readFileSync(USAGE_PATH, "utf8"));
91
85
  const rulesetSchema = JSON.parse(fs.readFileSync(RULESET_SCHEMA_PATH, "utf8"));
86
+ const TOOL_RULES = normalizeTrailingWhitespace(fs.readFileSync(TOOL_RULES_PATH, "utf8"));
92
87
  const ajv = new Ajv({ allErrors: true, strict: false });
93
88
  const validateRulesetSchema = ajv.compile(rulesetSchema);
94
89
  const formatSchemaErrors = (errors) => {
@@ -108,6 +103,9 @@ const resolveFrom = (baseDir, targetPath) => {
108
103
  }
109
104
  return path.resolve(baseDir, targetPath);
110
105
  };
106
+ const ensureDir = (dirPath) => {
107
+ fs.mkdirSync(dirPath, { recursive: true });
108
+ };
111
109
  const clearCache = () => {
112
110
  if (fs.existsSync(DEFAULT_CACHE_ROOT)) {
113
111
  fs.rmSync(DEFAULT_CACHE_ROOT, { recursive: true, force: true });
@@ -270,9 +268,6 @@ const resolveRefHash = (repoUrl, ref) => {
270
268
  const [hash] = raw.split(/\s+/u);
271
269
  return hash ?? null;
272
270
  };
273
- const ensureDir = (dirPath) => {
274
- fs.mkdirSync(dirPath, { recursive: true });
275
- };
276
271
  const cloneAtRef = (repoUrl, ref, destination) => {
277
272
  execGit(["clone", "--depth", "1", "--branch", ref, repoUrl, destination]);
278
273
  };
@@ -329,6 +324,41 @@ const resolveLocalRulesRoot = (rulesetDir, source) => {
329
324
  ensureDirectoryExists(candidate);
330
325
  return candidate;
331
326
  };
327
+ const resolveWorkspaceRoot = (rulesetDir, source) => {
328
+ if (source.startsWith("github:")) {
329
+ const parsed = parseGithubSource(source);
330
+ return path.join(DEFAULT_WORKSPACE_ROOT, parsed.owner, parsed.repo);
331
+ }
332
+ return resolveFrom(rulesetDir, source);
333
+ };
334
+ const ensureWorkspaceForGithubSource = (source) => {
335
+ const parsed = parseGithubSource(source);
336
+ const workspaceRoot = path.join(DEFAULT_WORKSPACE_ROOT, parsed.owner, parsed.repo);
337
+ if (!fs.existsSync(workspaceRoot)) {
338
+ ensureDir(path.dirname(workspaceRoot));
339
+ execGit(["clone", parsed.url, workspaceRoot]);
340
+ }
341
+ if (parsed.ref !== "latest") {
342
+ execGit(["fetch", "--all"], workspaceRoot);
343
+ execGit(["checkout", parsed.ref], workspaceRoot);
344
+ }
345
+ return workspaceRoot;
346
+ };
347
+ const applyRulesFromWorkspace = (rulesetDir, source) => {
348
+ if (!source.startsWith("github:")) {
349
+ return;
350
+ }
351
+ const workspaceRoot = ensureWorkspaceForGithubSource(source);
352
+ const status = execGit(["status", "--porcelain"], workspaceRoot);
353
+ if (status) {
354
+ throw new Error(`Workspace has uncommitted changes: ${workspaceRoot}`);
355
+ }
356
+ const branch = execGit(["rev-parse", "--abbrev-ref", "HEAD"], workspaceRoot);
357
+ if (branch === "HEAD") {
358
+ throw new Error(`Workspace is in detached HEAD state: ${workspaceRoot}`);
359
+ }
360
+ execGit(["push"], workspaceRoot);
361
+ };
332
362
  const resolveRulesRoot = (rulesetDir, source, refresh) => {
333
363
  if (source.startsWith("github:")) {
334
364
  return resolveGithubRulesRoot(source, refresh);
@@ -397,6 +427,16 @@ const getRulesetFiles = (rootDir, specificRuleset, rulesetName) => {
397
427
  }
398
428
  return findRulesetFiles(rootDir, rulesetName);
399
429
  };
430
+ const ensureSingleRuleset = (rulesetFiles, rootDir, rulesetName) => {
431
+ if (rulesetFiles.length === 0) {
432
+ throw new Error(`No ruleset files named ${rulesetName} found under ${rootDir}`);
433
+ }
434
+ if (rulesetFiles.length > 1) {
435
+ const list = rulesetFiles.map((file) => `- ${normalizePath(path.relative(rootDir, file))}`).join("\n");
436
+ throw new Error(`Multiple ruleset files found. Specify one with --ruleset:\n${list}`);
437
+ }
438
+ return rulesetFiles[0];
439
+ };
400
440
  const main = () => {
401
441
  const args = parseArgs(process.argv.slice(2));
402
442
  if (args.help) {
@@ -411,6 +451,27 @@ const main = () => {
411
451
  const rootDir = args.root ? path.resolve(args.root) : process.cwd();
412
452
  const rulesetName = args.rulesetName || DEFAULT_RULESET_NAME;
413
453
  const rulesetFiles = getRulesetFiles(rootDir, args.ruleset, rulesetName);
454
+ const command = args.command ?? "compose";
455
+ if (command === "edit-rules") {
456
+ const rulesetPath = ensureSingleRuleset(rulesetFiles, rootDir, rulesetName);
457
+ const rulesetDir = path.dirname(rulesetPath);
458
+ const ruleset = readProjectRuleset(rulesetPath);
459
+ let workspaceRoot = resolveWorkspaceRoot(rulesetDir, ruleset.source);
460
+ if (ruleset.source.startsWith("github:")) {
461
+ workspaceRoot = ensureWorkspaceForGithubSource(ruleset.source);
462
+ }
463
+ process.stdout.write(`Rules workspace: ${workspaceRoot}\n`);
464
+ return;
465
+ }
466
+ if (command === "apply-rules") {
467
+ const rulesetPath = ensureSingleRuleset(rulesetFiles, rootDir, rulesetName);
468
+ const rulesetDir = path.dirname(rulesetPath);
469
+ const ruleset = readProjectRuleset(rulesetPath);
470
+ applyRulesFromWorkspace(rulesetDir, ruleset.source);
471
+ const output = composeRuleset(rulesetPath, rootDir, { refresh: true });
472
+ process.stdout.write(`Composed AGENTS.md:\n- ${output}\n`);
473
+ return;
474
+ }
414
475
  if (rulesetFiles.length === 0) {
415
476
  throw new Error(`No ruleset files named ${rulesetName} found under ${rootDir}`);
416
477
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "compose-agentsmd",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "CLI tools for composing per-project AGENTS.md files from modular rule sets",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -0,0 +1,4 @@
1
+ # Tool Rules (compose-agentsmd)
2
+ - Before starting any work, run `compose-agentsmd` from the project root.
3
+ - To update shared rules, run `compose-agentsmd edit-rules`, edit the workspace rules, then run `compose-agentsmd apply-rules`.
4
+ - Do not edit `AGENTS.md` directly; update the source rules and regenerate.
@@ -0,0 +1,8 @@
1
+ Usage: compose-agentsmd [edit-rules|apply-rules] [--root <path>] [--ruleset <path>] [--ruleset-name <name>] [--refresh] [--clear-cache]
2
+
3
+ Options:
4
+ --root <path> Project root directory (default: current working directory)
5
+ --ruleset <path> Only compose a single ruleset file
6
+ --ruleset-name <name> Ruleset filename to search for (default: agent-ruleset.json)
7
+ --refresh Refresh cached remote rules
8
+ --clear-cache Remove cached remote rules and exit