@evertonferreira96/agents 0.1.2 → 0.1.3

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
@@ -21,6 +21,7 @@
21
21
  - [The big picture](#the-big-picture)
22
22
  - [What `setup:pull` does, step by step](#what-setuppull-does-step-by-step)
23
23
  - [How each file is decided](#how-each-file-is-decided)
24
+ - [Projects that already have an AGENTS.md](#projects-that-already-have-an-agentsmd)
24
25
  - [What lands in your project](#what-lands-in-your-project)
25
26
  - [Working in a configured project](#working-in-a-configured-project)
26
27
  - [Command reference](#command-reference)
@@ -188,6 +189,60 @@ flowchart TD
188
189
  | Configured project, nothing edited | New files added, changed files updated, removed files deleted |
189
190
  | You edited a shipped file | Kept and listed; `--force` replaces it with the config version |
190
191
  | Project configured before the manifest existed | Differing files count as edited; review them, then run `--force` once |
192
+ | The project has its own `AGENTS.md` | You choose: keep, overwrite, append or print ([next section](#projects-that-already-have-an-agentsmd)) |
193
+
194
+ ### Projects that already have an AGENTS.md
195
+
196
+ Many projects already carry an `AGENTS.md` with their own rules. When the project copy differs from the config, `setup:pull` asks what to do. Press Enter to keep yours; outside a terminal (CI, scripts) it keeps yours and lists the options.
197
+
198
+ ```text
199
+ AGENTS.md already exists in this project and differs from the config. What should happen to it?
200
+ [k] keep yours as it is (default)
201
+ [o] overwrite it with the config version
202
+ [a] append the config version at the end, in a managed block that later pulls keep current
203
+ [p] print the config version here, to concatenate it yourself
204
+ Choice [k/o/a/p]:
205
+ ```
206
+
207
+ ```mermaid
208
+ flowchart TD
209
+ A["Project AGENTS.md differs<br/>from the config"] --> B{"Has a managed block?"}
210
+ B -- yes --> RB["Refresh only the block<br/>no question asked"]
211
+ B -- no --> Q{"Your choice<br/>prompt or --agents-md"}
212
+ Q -- keep --> K["Yours stays as it is"]
213
+ Q -- overwrite --> O["Replaced by the config version<br/>later pulls keep it current"]
214
+ Q -- append --> AP["Config version added at the end<br/>inside the managed block"]
215
+ Q -- print --> P["Config version printed<br/>you paste it where you want"]
216
+ AP -. "next pull" .-> RB
217
+ P -. "pasted with the markers" .-> RB
218
+
219
+ classDef keep fill:#f3f4f6,stroke:#9ca3af,color:#374151
220
+ classDef upd fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
221
+ classDef add fill:#dcfce7,stroke:#16a34a,color:#14532d
222
+ classDef out fill:#fef3c7,stroke:#d97706,color:#78350f
223
+ class K keep
224
+ class O,RB upd
225
+ class AP add
226
+ class P out
227
+ ```
228
+
229
+ The managed block is plain Markdown between two comments. Everything outside the markers is yours; later pulls replace only what is inside them:
230
+
231
+ ```markdown
232
+ # My project
233
+ Rules that belong to this project.
234
+
235
+ <!-- agents-config:start -->
236
+ <!-- Managed by @evertonferreira96/agents: `chew agents setup:pull` refreshes this block. Edit outside the markers. -->
237
+ ...the config AGENTS.md...
238
+ <!-- agents-config:end -->
239
+ ```
240
+
241
+ | Want | Command |
242
+ |---|---|
243
+ | Decide without the question | `chew agents setup:pull --agents-md=keep` (or `overwrite`, `append`, `print`) |
244
+ | Just see the config version | `chew agents setup:print` (in the managed block) or `chew agents setup:print --raw` (file alone) |
245
+ | Concatenate from the shell | `chew agents setup:print >> AGENTS.md` (bash, zsh, cmd; in Windows PowerShell 5, `>>` writes UTF-16, so use `--agents-md=append`) |
191
246
 
192
247
  ### What lands in your project
193
248
 
@@ -291,6 +346,7 @@ The full flow, with every section of the playbook, is in [`.claude/skills/orches
291
346
  |---|---|
292
347
  | `chew agents setup:pull [dir]` | Download the latest version and install or refresh it in `dir` (default: current folder) |
293
348
  | `chew agents setup:check [dir]` | Compare the project with the latest version; exit code 1 on drift |
349
+ | `chew agents setup:print` | Print the latest config `AGENTS.md` in its managed block, to concatenate into yours (`--raw`: the file alone) |
294
350
  | `chew --version` | Version of the installed `chew` |
295
351
 
296
352
  | Flag | Effect |
@@ -298,6 +354,7 @@ The full flow, with every section of the playbook, is in [`.claude/skills/orches
298
354
  | `--force` | Also overwrite files edited in the project (protected files excepted) |
299
355
  | `--dry-run` | Show what would change, write nothing |
300
356
  | `--local` | Use the version installed with `chew` instead of downloading |
357
+ | `--agents-md=<choice>` | When the project has its own `AGENTS.md`: `keep` (default), `overwrite`, `append` or `print`; skips the question |
301
358
 
302
359
  ### `agents` (same package, via `npx` or global)
303
360
 
@@ -306,6 +363,9 @@ The full flow, with every section of the playbook, is in [`.claude/skills/orches
306
363
  | `agents init [dir]` | Copy what is missing; never overwrites |
307
364
  | `agents update [dir]` | Same rules as `setup:pull`, using this installed version |
308
365
  | `agents check [dir]` | Report missing, outdated, edited and obsolete files; exit 1 on drift |
366
+ | `agents print-agents-md` | Same as `chew agents setup:print`, for this installed version |
367
+
368
+ `init` and `update` accept `--force`, `--dry-run` and `--agents-md=<choice>`, like `chew`.
309
369
 
310
370
  ### Legacy, from a local clone
311
371
 
@@ -409,6 +469,13 @@ By design: files edited in the project are kept and listed as `edited`. `chew ag
409
469
 
410
470
  </details>
411
471
 
472
+ <details>
473
+ <summary><b>My project's AGENTS.md was not touched</b></summary>
474
+
475
+ Keeping it is the default when no choice is given, including every run outside a terminal. Run `chew agents setup:pull --agents-md=append` to add the config in a managed block below your content, or `--agents-md=overwrite` to replace it. See [Projects that already have an AGENTS.md](#projects-that-already-have-an-agentsmd).
476
+
477
+ </details>
478
+
412
479
  <details>
413
480
  <summary><b>"no manifest from a previous install"</b></summary>
414
481
 
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.2
1
+ 0.1.3
package/bin/agents.mjs CHANGED
@@ -3,43 +3,67 @@
3
3
  // agents init [dir] copy the config into the project (default: current directory); never overwrites
4
4
  // agents update [dir] refresh files the project did not edit, add new ones, delete untouched ones that left the config
5
5
  // agents check [dir] report missing, outdated, edited and obsolete files; exit 1 on drift
6
+ // agents print-agents-md print this version's AGENTS.md in its managed block (--raw: the file alone)
6
7
  // Flags: --force (update also overwrites files edited in the project, except CLAUDE.md, settings.local.json and agent memory),
7
- // --dry-run (init/update: show what would change, write nothing).
8
- import { resolve } from "node:path";
9
- import { DEFAULT_SRC, drift, install, readPackage, report } from "../scripts/bootstrap.mjs";
8
+ // --dry-run (init/update: show what would change, write nothing),
9
+ // --agents-md=keep|overwrite|append|print (a project AGENTS.md that differs; asked in a terminal, default keep).
10
+ import { readFileSync } from "node:fs";
11
+ import { join, resolve } from "node:path";
12
+ import { AGENTS_MD_CHOICES, DEFAULT_SRC, agentsBlock, drift, install, readPackage, report, settleAgentsMd } from "../scripts/bootstrap.mjs";
10
13
 
11
- const HELP = `Usage: agents <command> [dir] [--force] [--dry-run]
14
+ const HELP = `Usage: agents <command> [dir] [flags]
12
15
 
13
16
  Commands:
14
- init copy the config into the project (never overwrites existing files)
15
- update refresh untouched files, add new ones, delete untouched files that left the config
16
- check report missing, outdated, edited and obsolete files (exit 1 on drift)
17
+ init copy the config into the project (never overwrites existing files)
18
+ update refresh untouched files, add new ones, delete untouched files that left the config
19
+ check report missing, outdated, edited and obsolete files (exit 1 on drift)
20
+ print-agents-md print the config AGENTS.md inside its managed block (--raw: the file alone)
17
21
 
18
22
  Flags:
19
- --force update also overwrites files edited in the project
20
- (CLAUDE.md, .claude/settings.local.json and .claude/agent-memory/** are never touched)
21
- --dry-run show what init/update would change without writing
23
+ --force update also overwrites files edited in the project
24
+ (CLAUDE.md, .claude/settings.local.json and .claude/agent-memory/** are never touched)
25
+ --dry-run show what init/update would change without writing
26
+ --agents-md=<choice>
27
+ when the project already has its own AGENTS.md (asked in a terminal; default keep):
28
+ keep, overwrite, append (managed block refreshed by later updates) or print
22
29
 
23
30
  dir defaults to the current directory.`;
24
31
 
25
32
  const args = process.argv.slice(2);
26
- const flags = new Set(args.filter((a) => a.startsWith("-")));
27
- const [command, dir] = args.filter((a) => !a.startsWith("-"));
33
+ const flags = new Set(), opts = {}, pos = [];
34
+ for (let i = 0; i < args.length; i++) {
35
+ const a = args[i];
36
+ if (a === "--agents-md") opts["agents-md"] = args[++i] ?? "";
37
+ else if (a.startsWith("--") && a.includes("=")) opts[a.slice(2, a.indexOf("="))] = a.slice(a.indexOf("=") + 1);
38
+ else if (a.startsWith("-")) flags.add(a);
39
+ else pos.push(a);
40
+ }
41
+ const [command, dir] = pos;
28
42
  const pkg = readPackage(DEFAULT_SRC);
29
43
 
30
- if (flags.has("--version") || flags.has("-v")) { console.log(pkg.version); process.exit(0); }
31
- if (!command || command === "help" || flags.has("--help") || flags.has("-h")) { console.log(`${pkg.name} ${pkg.version}\n\n${HELP}`); process.exit(command ? 0 : 2); }
32
- if (!["init", "update", "check"].includes(command)) { console.error(`unknown command: ${command}\n\n${HELP}`); process.exit(2); }
44
+ async function main() {
45
+ if (flags.has("--version") || flags.has("-v")) { console.log(pkg.version); return 0; }
46
+ if (!command || command === "help" || flags.has("--help") || flags.has("-h")) { console.log(`${pkg.name} ${pkg.version}\n\n${HELP}`); return command ? 0 : 2; }
47
+ if (!["init", "update", "check", "print-agents-md"].includes(command)) { console.error(`unknown command: ${command}\n\n${HELP}`); return 2; }
48
+ const agentsMd = opts["agents-md"];
49
+ if (agentsMd !== undefined && !AGENTS_MD_CHOICES.includes(agentsMd)) { console.error(`--agents-md must be one of: ${AGENTS_MD_CHOICES.join(", ")}`); return 2; }
50
+
51
+ if (command === "print-agents-md") {
52
+ process.stdout.write(flags.has("--raw") ? readFileSync(join(DEFAULT_SRC, "AGENTS.md"), "utf8") : agentsBlock(DEFAULT_SRC));
53
+ return 0;
54
+ }
33
55
 
34
- try {
35
56
  const target = resolve(dir ?? process.cwd());
36
- const r = install({ target, mode: command, force: flags.has("--force"), dryRun: flags.has("--dry-run") });
57
+ const dryRun = flags.has("--dry-run");
58
+ const r = install({ target, mode: command, force: flags.has("--force"), dryRun });
37
59
  report(r);
38
- if (command === "init" && !flags.has("--dry-run")) {
60
+ if (command !== "check") {
61
+ await settleAgentsMd({ target, r, choice: agentsMd, dryRun, interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY) });
62
+ }
63
+ if (command === "init" && !dryRun) {
39
64
  console.log("\nnext: fill in purpose and commands in CLAUDE.md; optional: `npx impeccable install` (for /ui) and `/graphify .`");
40
65
  }
41
- process.exit(command === "check" && drift(r) ? 1 : 0);
42
- } catch (e) {
43
- console.error(e.message);
44
- process.exit(2);
66
+ return command === "check" && drift(r) ? 1 : 0;
45
67
  }
68
+
69
+ main().then((code) => { process.exitCode = code; }, (e) => { console.error(e.message); process.exitCode = 2; });
package/bin/chew.mjs CHANGED
@@ -3,8 +3,10 @@
3
3
  // chew agents setup:pull [dir] download the latest published config and install it (fresh project: everything;
4
4
  // configured project: refresh what was not edited there, add new files, drop obsolete ones)
5
5
  // chew agents setup:check [dir] compare the project with the latest published config; exit 1 on drift
6
+ // chew agents setup:print print the latest config AGENTS.md in its managed block (--raw: the file alone)
6
7
  // Flags: --force (also overwrite files edited in the project), --dry-run (show, write nothing),
7
- // --local (use the version installed with chew instead of downloading the latest).
8
+ // --local (use the version installed with chew instead of downloading the latest),
9
+ // --agents-md=keep|overwrite|append|print (a project AGENTS.md that differs; asked in a terminal, default keep).
8
10
  // The install runs with the engine of the downloaded version, so an old global chew still applies the newest setup.
9
11
  // Test hook: CHEW_TARBALL=<path to a .tgz from `npm pack`> replaces the registry download.
10
12
  import { spawnSync } from "node:child_process";
@@ -21,18 +23,26 @@ const SCOPE = SELF.name.startsWith("@") ? SELF.name.split("/")[0] : null;
21
23
 
22
24
  const HELP = `chew ${SELF.version}
23
25
 
24
- Usage: chew agents <command> [dir] [--force] [--dry-run] [--local]
26
+ Usage: chew agents <command> [dir] [flags]
25
27
 
26
28
  Commands:
27
29
  setup:pull download the latest ${SELF.name} and install it into the project
28
30
  (fresh project: everything; configured project: refresh what was not edited there)
29
31
  setup:check compare the project with the latest published config (exit 1 on drift)
32
+ setup:print print the latest config AGENTS.md inside its managed block, to concatenate into yours
33
+ (--raw: the file alone, without the markers)
30
34
 
31
35
  Flags:
32
- --force also overwrite files edited in the project
33
- (CLAUDE.md, .claude/settings.local.json and .claude/agent-memory/** are never touched)
34
- --dry-run show what would change without writing
35
- --local use the version installed with chew (${SELF.version}) instead of downloading
36
+ --force also overwrite files edited in the project
37
+ (CLAUDE.md, .claude/settings.local.json and .claude/agent-memory/** are never touched)
38
+ --dry-run show what would change without writing
39
+ --local use the version installed with chew (${SELF.version}) instead of downloading
40
+ --agents-md=<choice>
41
+ when the project already has its own AGENTS.md (asked in a terminal; default keep):
42
+ keep leave yours as it is
43
+ overwrite replace it with the config version
44
+ append add the config version at the end, in a managed block that later pulls refresh
45
+ print print the config version to concatenate yourself
36
46
 
37
47
  dir defaults to the current directory.`;
38
48
 
@@ -99,16 +109,46 @@ function fetchConfig(local) {
99
109
 
100
110
  const newer = (a, b) => { const x = a.split(".").map(Number), y = b.split(".").map(Number); for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] > y[i]; return false; };
101
111
 
112
+ const COMMANDS = ["setup:pull", "setup:check", "setup:print"];
113
+ const AGENTS_MD = ["keep", "overwrite", "append", "print"];
114
+
115
+ // Flags, `--name=value` / `--name value` options and positionals.
116
+ function parseArgs(argv) {
117
+ const flags = new Set(), opts = {}, pos = [];
118
+ for (let i = 0; i < argv.length; i++) {
119
+ const a = argv[i];
120
+ if (a === "--agents-md") opts["agents-md"] = argv[++i] ?? "";
121
+ else if (a.startsWith("--") && a.includes("=")) { const k = a.slice(2, a.indexOf("=")); opts[k] = a.slice(a.indexOf("=") + 1); }
122
+ else if (a.startsWith("-")) flags.add(a);
123
+ else pos.push(a);
124
+ }
125
+ return { flags, opts, pos };
126
+ }
127
+
102
128
  async function main(argv) {
103
- const flags = new Set(argv.filter((a) => a.startsWith("-")));
104
- const [ns, command, dir] = argv.filter((a) => !a.startsWith("-"));
129
+ const { flags, opts, pos } = parseArgs(argv);
130
+ const [ns, command, dir] = pos;
105
131
  if (flags.has("--version") || flags.has("-v")) { console.log(SELF.version); return 0; }
106
132
  if (!ns || ns === "help" || flags.has("--help") || flags.has("-h")) { console.log(HELP); return ns ? 0 : 2; }
107
- if (ns !== "agents" || !["setup:pull", "setup:check"].includes(command)) {
133
+ const agentsMd = opts["agents-md"];
134
+ if (agentsMd !== undefined && !AGENTS_MD.includes(agentsMd)) { console.error(`--agents-md must be one of: ${AGENTS_MD.join(", ")}`); return 2; }
135
+ if (ns !== "agents" || !COMMANDS.includes(command)) {
108
136
  console.error(`unknown command: chew ${[ns, command].filter(Boolean).join(" ")}\n\n${HELP}`);
109
137
  return 2;
110
138
  }
111
139
 
140
+ if (command === "setup:print") { // stdout carries only the content, so it can be redirected or piped
141
+ const config = fetchConfig(flags.has("--local"));
142
+ try {
143
+ const engine = await import(pathToFileURL(join(config.dir, "scripts", "bootstrap.mjs")).href);
144
+ const raw = readFileSync(join(config.dir, "AGENTS.md"), "utf8");
145
+ process.stdout.write(flags.has("--raw") || !engine.agentsBlock ? raw : engine.agentsBlock(config.dir));
146
+ return 0;
147
+ } finally {
148
+ config.cleanup();
149
+ }
150
+ }
151
+
112
152
  const target = resolve(dir ?? process.cwd());
113
153
  if (!existsSync(target)) throw new Error(`target is not a directory: ${target}`);
114
154
  try { if (JSON.parse(readFileSync(join(target, "package.json"), "utf8")).name === SELF.name) throw new Error("this is the config repo itself; run chew inside a project"); }
@@ -120,6 +160,10 @@ async function main(argv) {
120
160
  const mode = command === "setup:check" ? "check" : "update";
121
161
  const r = engine.install({ src: config.dir, target, mode, force: flags.has("--force"), dryRun: flags.has("--dry-run") });
122
162
  engine.report(r);
163
+ if (mode === "update" && engine.settleAgentsMd) {
164
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
165
+ await engine.settleAgentsMd({ src: config.dir, target, r, choice: agentsMd, dryRun: flags.has("--dry-run"), interactive });
166
+ }
123
167
  if (mode === "update" && !flags.has("--dry-run") && r.written.some((w) => w.startsWith("CLAUDE.md"))) {
124
168
  console.log("\nnext: fill in purpose and commands in CLAUDE.md; optional: `npx impeccable install` (for /ui) and `/graphify .`");
125
169
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evertonferreira96/agents",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Personal agents, rules, hooks and skills for Claude Code (portable to Cursor and Codex through AGENTS.md), installable into any project.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,6 +25,12 @@ export const GITIGNORE = [".claude/worktrees/", ".claude/handoffs/", ".claude/.c
25
25
  export const MANIFEST = ".claude/config-manifest.json";
26
26
  export const SOURCE_FILE = ".claude/config-source.json";
27
27
  export const DEFAULT_SRC = fileURLToPath(new URL("..", import.meta.url));
28
+ // AGENTS.md of a project that already had its own: the config version can live inside it as a managed block,
29
+ // which `update` refreshes in place without touching the rest of the file.
30
+ export const AGENTS_FILE = "AGENTS.md";
31
+ export const BLOCK_START = "<!-- agents-config:start -->";
32
+ export const BLOCK_END = "<!-- agents-config:end -->";
33
+ export const AGENTS_MD_CHOICES = ["keep", "overwrite", "append", "print"];
28
34
 
29
35
  const sha = (p) => createHash("sha256").update(readFileSync(p)).digest("hex");
30
36
  const readJson = (p) => { try { return JSON.parse(readFileSync(p, "utf8")); } catch { return null; } };
@@ -61,6 +67,82 @@ function removeEmptyParents(target, rel) {
61
67
  }
62
68
  }
63
69
 
70
+ // The config's AGENTS.md wrapped in the managed-block markers (LF line endings, trailing newline).
71
+ export function agentsBlock(src = DEFAULT_SRC) {
72
+ const body = readFileSync(join(src, AGENTS_FILE), "utf8").replace(/\r\n/g, "\n").trimEnd();
73
+ const note = `<!-- Managed by ${readPackage(src).name}: \`chew agents setup:pull\` refreshes this block. Edit outside the markers. -->`;
74
+ return `${BLOCK_START}\n${note}\n${body}\n${BLOCK_END}\n`;
75
+ }
76
+
77
+ // Position of the managed block in `text` (normalized to LF), or null when the file has none.
78
+ function findBlock(text) {
79
+ const t = text.replace(/\r\n/g, "\n");
80
+ const start = t.indexOf(BLOCK_START);
81
+ const end = start < 0 ? -1 : t.indexOf(BLOCK_END, start + BLOCK_START.length);
82
+ return end < 0 ? null : { text: t, start, end: end + BLOCK_END.length, eol: text.includes("\r\n") ? "\r\n" : "\n" };
83
+ }
84
+
85
+ function recordHash(target, rel, hash) {
86
+ const p = join(target, MANIFEST);
87
+ const m = readJson(p);
88
+ if (!m?.files) return;
89
+ m.files[rel] = hash;
90
+ writeFileSync(p, JSON.stringify(m, null, 2) + "\n");
91
+ }
92
+
93
+ // What to do with a project AGENTS.md that differs from the config and has no managed block:
94
+ // keep (nothing), overwrite (config version, refreshed by later updates), append (managed block at the end, refreshed by
95
+ // later updates), print (returns the managed block for the user to paste). The manifest records the config hash, so a
96
+ // file with a block never counts as untouched and is never overwritten whole.
97
+ export function applyAgentsMd({ src = DEFAULT_SRC, target, choice, dryRun = false }) {
98
+ if (!AGENTS_MD_CHOICES.includes(choice)) throw new Error(`unknown AGENTS.md choice: ${choice} (${AGENTS_MD_CHOICES.join(", ")})`);
99
+ const abs = join(src, AGENTS_FILE);
100
+ const dst = join(target, AGENTS_FILE);
101
+ if (choice === "print") return { choice, text: agentsBlock(src) };
102
+ if (choice === "keep" || dryRun) return { choice };
103
+ if (choice === "overwrite") copyFileSync(abs, dst);
104
+ if (choice === "append") {
105
+ const cur = existsSync(dst) ? readFileSync(dst, "utf8") : "";
106
+ const eol = cur.includes("\r\n") ? "\r\n" : "\n";
107
+ const block = agentsBlock(src).replace(/\n/g, eol);
108
+ const found = findBlock(cur);
109
+ if (found) writeFileSync(dst, (found.text.slice(0, found.start) + agentsBlock(src).trimEnd() + found.text.slice(found.end)).replace(/\n/g, eol));
110
+ else writeFileSync(dst, cur + (cur === "" || cur.endsWith(eol + eol) ? "" : cur.endsWith(eol) ? eol : eol + eol) + block);
111
+ }
112
+ recordHash(target, AGENTS_FILE, sha(abs));
113
+ return { choice };
114
+ }
115
+
116
+ // Ask once in a terminal (default keep).
117
+ export async function askAgentsMd({ input = process.stdin, output = process.stdout } = {}) {
118
+ const { createInterface } = await import("node:readline/promises");
119
+ output.write(`\nAGENTS.md already exists in this project and differs from the config. What should happen to it?
120
+ [k] keep yours as it is (default)
121
+ [o] overwrite it with the config version
122
+ [a] append the config version at the end, in a managed block that later pulls keep current
123
+ [p] print the config version here, to concatenate it yourself\n`);
124
+ const rl = createInterface({ input, output });
125
+ try {
126
+ const answer = (await rl.question("Choice [k/o/a/p]: ")).trim().toLowerCase();
127
+ return { o: "overwrite", a: "append", p: "print" }[answer[0]] ?? "keep";
128
+ } finally {
129
+ rl.close();
130
+ }
131
+ }
132
+
133
+ // After init/update: settle an AGENTS.md conflict with `choice`, or ask when interactive, or keep and say how to choose.
134
+ export async function settleAgentsMd({ src = DEFAULT_SRC, target, r, choice, dryRun = false, interactive = false, log = console.log }) {
135
+ if (!r.agentsMdConflict) return null;
136
+ const picked = choice || (interactive ? await askAgentsMd() : "keep");
137
+ const out = applyAgentsMd({ src, target, choice: picked, dryRun });
138
+ const dry = dryRun ? " (dry run: nothing written)" : "";
139
+ if (picked === "keep") log(`\nAGENTS.md: kept yours. To change that: --agents-md=overwrite, --agents-md=append or --agents-md=print`);
140
+ if (picked === "overwrite") log(`\nAGENTS.md: overwritten with the config version${dry}`);
141
+ if (picked === "append") log(`\nAGENTS.md: config version appended at the end in a managed block${dry}; later pulls refresh only that block`);
142
+ if (picked === "print") log(`\nAGENTS.md: the config version is below. Paste it into yours with the markers, so later pulls refresh it:\n\n${out.text}`);
143
+ return picked;
144
+ }
145
+
64
146
  // mode: "init" (copy what is missing), "update" (also refresh untouched files and drop obsolete ones), "check" (report only).
65
147
  export function install({ src = DEFAULT_SRC, target, mode = "init", force = false, dryRun = false }) {
66
148
  if (!["init", "update", "check"].includes(mode)) throw new Error(`unknown mode: ${mode}`);
@@ -86,6 +168,20 @@ export function install({ src = DEFAULT_SRC, target, mode = "init", force = fals
86
168
  }
87
169
  const have = sha(dst);
88
170
  if (have === want) { r.same++; next[rel] = want; continue; }
171
+ if (rel === AGENTS_FILE) {
172
+ const cur = readFileSync(dst, "utf8");
173
+ const found = findBlock(cur);
174
+ if (found) { // the project's own AGENTS.md carrying the config in a managed block: only the block is ours
175
+ next[rel] = want; // never the project file's hash, so no engine treats the file as untouched and replaces it whole
176
+ const block = agentsBlock(src).trimEnd();
177
+ if (found.text.slice(found.start, found.end) === block) { r.same++; continue; }
178
+ if (mode === "check") { r.outdated.push(`${rel} (managed block)`); continue; }
179
+ if (mode === "init") { r.kept.push(`${rel} (managed block outdated; run update)`); continue; }
180
+ if (write) writeFileSync(dst, (found.text.slice(0, found.start) + block + found.text.slice(found.end)).replace(/\n/g, found.eol));
181
+ r.updated.push(`${rel} (managed block)`);
182
+ continue;
183
+ }
184
+ }
89
185
  const untouched = installed?.[rel] === have;
90
186
  if (mode === "check") { (untouched ? r.outdated : r.edited).push(rel); continue; }
91
187
  if (mode === "update" && (untouched || force) && !PROTECTED.test(rel)) {
@@ -94,6 +190,7 @@ export function install({ src = DEFAULT_SRC, target, mode = "init", force = fals
94
190
  continue;
95
191
  }
96
192
  r.kept.push(rel);
193
+ if (rel === AGENTS_FILE) r.agentsMdConflict = true; // the CLI offers keep / overwrite / append / print
97
194
  if (installed?.[rel]) next[rel] = installed[rel];
98
195
  }
99
196
 
@@ -155,9 +252,10 @@ export function report(r, log = console.log) {
155
252
  list("kept", r.kept, r.mode === "init" ? " (differs; init never overwrites)" : " (edited here)");
156
253
  list("kept", r.obsoleteKept, " (left the config, but edited here)");
157
254
  if (r.written.length) { log("written:"); for (const w of r.written) log(` ${w}`); }
158
- if (r.mode === "update" && !r.hadManifest && r.kept.length) log("no manifest from a previous install, so differing files count as edited; review them and run `update --force` once to adopt the config versions");
159
- else if (r.kept.length && r.mode === "update") log("`update --force` overwrites the files kept above (protected files excepted)");
160
- else if (r.kept.length) log("run `update` to refresh differing files");
255
+ const others = r.kept.filter((k) => !(r.agentsMdConflict && k === AGENTS_FILE)); // AGENTS.md gets its own choice (settleAgentsMd)
256
+ if (r.mode === "update" && !r.hadManifest && others.length) log("no manifest from a previous install, so differing files count as edited; review them and run `update --force` once to adopt the config versions");
257
+ else if (others.length && r.mode === "update") log("`update --force` overwrites the files kept above (protected files excepted)");
258
+ else if (others.length) log("run `update` to refresh differing files");
161
259
  }
162
260
 
163
261
  // Legacy CLI (scripts/bootstrap.mjs <target> [--check|--force] | --sync).