@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 +67 -0
- package/VERSION +1 -1
- package/bin/agents.mjs +46 -22
- package/bin/chew.mjs +53 -9
- package/package.json +1 -1
- package/scripts/bootstrap.mjs +101 -3
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.
|
|
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
|
-
|
|
9
|
-
import {
|
|
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] [
|
|
14
|
+
const HELP = `Usage: agents <command> [dir] [flags]
|
|
12
15
|
|
|
13
16
|
Commands:
|
|
14
|
-
init
|
|
15
|
-
update
|
|
16
|
-
check
|
|
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
|
|
20
|
-
|
|
21
|
-
--dry-run
|
|
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(
|
|
27
|
-
|
|
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
|
-
|
|
31
|
-
if (
|
|
32
|
-
if (!
|
|
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
|
|
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
|
|
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
|
-
|
|
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] [
|
|
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
|
|
33
|
-
|
|
34
|
-
--dry-run
|
|
35
|
-
--local
|
|
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
|
|
104
|
-
const [ns, command, dir] =
|
|
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
|
-
|
|
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.
|
|
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": {
|
package/scripts/bootstrap.mjs
CHANGED
|
@@ -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
|
-
|
|
159
|
-
|
|
160
|
-
else if (r.
|
|
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).
|