@sayansr26/agent-os 0.5.0 → 0.5.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/CHANGELOG.md +28 -0
- package/README.md +14 -0
- package/package.json +2 -2
- package/src/adopt.mjs +80 -0
- package/src/cli.mjs +81 -7
- package/src/plugin.mjs +74 -0
- package/src/selftest.mjs +56 -1
- package/src/targets.mjs +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,34 @@ All notable changes to this project are documented here.
|
|
|
5
5
|
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
6
6
|
this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.5.1] — 2026-09-15
|
|
9
|
+
|
|
10
|
+
Found by running 0.5.0 against a real repository that already had six
|
|
11
|
+
path-scoped rules and a hand-written `AGENTS.md`. It overwrote the
|
|
12
|
+
`AGENTS.md`. 0.5.0 was never published.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- **`init` installs the Claude Code plugin.** When Claude Code is detected it
|
|
16
|
+
runs `claude plugin marketplace add` and `claude plugin install --yes` at
|
|
17
|
+
project scope, so the plugin travels with the repository rather than living
|
|
18
|
+
on one machine. If the `claude` CLI is not on PATH it prints the two slash
|
|
19
|
+
commands instead. `--no-plugin` skips it, and the rules are written either
|
|
20
|
+
way — the compiler never depends on the plugin step succeeding.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- **`init` scaffolded over projects that already had rules.** It wrote a
|
|
24
|
+
placeholder `AGENTS.md` and an `example.md` rule regardless of what was
|
|
25
|
+
there, and the next `sync` compiled the placeholder on top of the project's
|
|
26
|
+
real `AGENTS.md`. `init` now adopts what it finds — the existing `AGENTS.md`
|
|
27
|
+
and the first rules directory it recognises, `.cursor/rules/*.mdc` converted
|
|
28
|
+
back to `paths:` — and seeds `example.md` only when there was nothing to
|
|
29
|
+
adopt. A tool for stopping rule drift must not cause it.
|
|
30
|
+
- **`sync` now refuses to overwrite a file it did not generate.** Generated
|
|
31
|
+
files carry a banner; anything else at a generated path is the user's own
|
|
32
|
+
work. `sync` names those files, leaves them alone and exits non-zero, with
|
|
33
|
+
`--force` as the explicit opt-out. `AGENTS.md` carries the banner too — it
|
|
34
|
+
previously did not, which is why nothing could tell it apart.
|
|
35
|
+
|
|
8
36
|
## [0.5.0] — 2026-09-15
|
|
9
37
|
|
|
10
38
|
`agent-os` becomes a cross-tool CLI. The Claude Code plugin is now one target
|
package/README.md
CHANGED
|
@@ -76,6 +76,20 @@ Every command below is `npx @sayansr26/agent-os <command>`. Install it once —
|
|
|
76
76
|
|
|
77
77
|
`--root <dir>` to target another directory, `--dry-run` to preview.
|
|
78
78
|
|
|
79
|
+
**`init` sets up Claude Code completely.** When it detects Claude Code it also
|
|
80
|
+
installs the plugin — the agents, skills, per-agent memory and session hook —
|
|
81
|
+
with `claude plugin marketplace add` and `claude plugin install --yes` at
|
|
82
|
+
project scope, so it travels with the repo. `--no-plugin` skips it. Every other
|
|
83
|
+
detected tool gets its rules in its own schema; the plugin layer is Claude Code
|
|
84
|
+
only because no other tool has anywhere to put it.
|
|
85
|
+
|
|
86
|
+
**On an existing project, `init` adopts rather than scaffolds.** It takes your
|
|
87
|
+
current `AGENTS.md` and the first rules directory it recognises as the source,
|
|
88
|
+
so the first `sync` regenerates what you already had. **`sync` never overwrites
|
|
89
|
+
a file it did not generate** — generated files carry a banner, anything else at
|
|
90
|
+
that path is yours. It names those files, leaves them alone and exits 1; pass
|
|
91
|
+
`--force` if you really mean to replace them.
|
|
92
|
+
|
|
79
93
|
Generated files carry a banner. Edit `.agent-os/`, run `sync`, never edit the output.
|
|
80
94
|
|
|
81
95
|
---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sayansr26/agent-os",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "One source of truth for AI coding agent config. Write your rules once; compile them to Claude Code, Cursor, Cline, Windsurf, Antigravity, Gemini CLI, OpenCode and Kilo.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -46,4 +46,4 @@
|
|
|
46
46
|
"test": "node scripts/validate-plugin.mjs && node src/selftest.mjs",
|
|
47
47
|
"prepublishOnly": "npm test"
|
|
48
48
|
}
|
|
49
|
-
}
|
|
49
|
+
}
|
package/src/adopt.mjs
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adopt what a project already has, instead of scaffolding over it.
|
|
3
|
+
*
|
|
4
|
+
* `init` used to write a placeholder `AGENTS.md` and an `example.md` rule into
|
|
5
|
+
* every project, including projects that already had a real AGENTS.md and a
|
|
6
|
+
* directory of real rules. The next `sync` then compiled the placeholder over
|
|
7
|
+
* the real file. A tool whose whole purpose is to stop rule files drifting must
|
|
8
|
+
* not destroy the rules it finds, so `init` now imports them as the source.
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
11
|
+
import { join, basename } from "node:path";
|
|
12
|
+
import { BANNER } from "./source.mjs";
|
|
13
|
+
|
|
14
|
+
const isGenerated = (text) => text.includes(BANNER);
|
|
15
|
+
|
|
16
|
+
const readDirMd = (dir, ext) => {
|
|
17
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory()) return [];
|
|
18
|
+
return readdirSync(dir)
|
|
19
|
+
.filter((f) => f.endsWith(ext))
|
|
20
|
+
.map((f) => ({ name: basename(f, ext), text: readFileSync(join(dir, f), "utf8") }))
|
|
21
|
+
.filter((r) => !isGenerated(r.text));
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** `.cursor/rules/*.mdc` back into the canonical `paths:` / `always:` shape. */
|
|
25
|
+
function fromCursor({ name, text }) {
|
|
26
|
+
const m = text.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
27
|
+
if (!m) return { name, text };
|
|
28
|
+
const fields = {};
|
|
29
|
+
for (const line of m[1].split("\n")) {
|
|
30
|
+
const kv = line.match(/^([a-zA-Z_][\w-]*):\s*(.*)$/);
|
|
31
|
+
if (kv) fields[kv[1]] = kv[2].trim();
|
|
32
|
+
}
|
|
33
|
+
const globs = (fields.globs || "").split(",").map((g) => g.trim()).filter(Boolean);
|
|
34
|
+
const always = String(fields.alwaysApply) === "true";
|
|
35
|
+
const fm = ["---"];
|
|
36
|
+
if (fields.description) fm.push(`description: ${fields.description}`);
|
|
37
|
+
if (always || globs.length === 0) fm.push("always: true");
|
|
38
|
+
else { fm.push("paths:"); for (const g of globs) fm.push(` - "${g}"`); }
|
|
39
|
+
fm.push("---", "");
|
|
40
|
+
return { name, text: `${fm.join("\n")}\n${m[2].trim()}\n` };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* What this project already has that should become the source.
|
|
45
|
+
* Returns `{ agents, rules, from, paths }`; `from` is what to tell the user,
|
|
46
|
+
* `paths` the files it consumed — safe for `init` to regenerate in place.
|
|
47
|
+
*/
|
|
48
|
+
export function adopt(root) {
|
|
49
|
+
const from = [];
|
|
50
|
+
const paths = new Set();
|
|
51
|
+
let agents = null;
|
|
52
|
+
|
|
53
|
+
const agentsPath = join(root, "AGENTS.md");
|
|
54
|
+
if (existsSync(agentsPath)) {
|
|
55
|
+
const text = readFileSync(agentsPath, "utf8");
|
|
56
|
+
if (!isGenerated(text)) { agents = text; from.push("AGENTS.md"); paths.add("AGENTS.md"); }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// In precedence order — the first store that has anything wins, because these
|
|
60
|
+
// are copies of each other in a project that was keeping them in sync by hand.
|
|
61
|
+
const sources = [
|
|
62
|
+
[".claude/rules", ".md", (r) => r],
|
|
63
|
+
[".clinerules", ".md", (r) => r],
|
|
64
|
+
[".agents/rules", ".md", (r) => r],
|
|
65
|
+
[".cursor/rules", ".mdc", fromCursor],
|
|
66
|
+
[".windsurf/rules", ".md", (r) => r],
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
let rules = [];
|
|
70
|
+
for (const [dir, ext, convert] of sources) {
|
|
71
|
+
const found = readDirMd(join(root, dir), ext);
|
|
72
|
+
if (!found.length) continue;
|
|
73
|
+
rules = found.map(convert);
|
|
74
|
+
from.push(`${dir}/ (${rules.length} rule${rules.length === 1 ? "" : "s"})`);
|
|
75
|
+
for (const r of found) paths.add(`${dir}/${r.name}${ext}`);
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { agents, rules, from, paths };
|
|
80
|
+
}
|
package/src/cli.mjs
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { readFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { readFileSync, existsSync, mkdirSync, writeFileSync, readdirSync } from "node:fs";
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { detect, summarise, TOOLS } from "./detect.mjs";
|
|
6
|
-
import { load, write, readIfExists, matches, DIR } from "./source.mjs";
|
|
6
|
+
import { load, write, readIfExists, matches, BANNER, DIR } from "./source.mjs";
|
|
7
|
+
import { adopt } from "./adopt.mjs";
|
|
8
|
+
import { installPlugin, MARKETPLACE, MARKETPLACE_NAME, PLUGIN } from "./plugin.mjs";
|
|
7
9
|
import { compile, TARGETS } from "./targets.mjs";
|
|
8
10
|
|
|
9
11
|
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
@@ -25,6 +27,8 @@ ${bold("agent-os")} — one source of truth for AI coding agent config
|
|
|
25
27
|
Options
|
|
26
28
|
--root <dir> project directory (default: cwd)
|
|
27
29
|
--dry-run print what would change, write nothing
|
|
30
|
+
--force overwrite files agent-os did not generate (it refuses by default)
|
|
31
|
+
--no-plugin skip installing the Claude Code plugin during init
|
|
28
32
|
`;
|
|
29
33
|
|
|
30
34
|
function scaffold(root, found) {
|
|
@@ -32,6 +36,17 @@ function scaffold(root, found) {
|
|
|
32
36
|
mkdirSync(join(root, DIR, "rules"), { recursive: true });
|
|
33
37
|
if (!existsSync(join(root, DIR, "config.json")))
|
|
34
38
|
write(root, `${DIR}/config.json`, JSON.stringify({ targets: present.length ? present : ["claude-code"] }, null, 2) + "\n");
|
|
39
|
+
|
|
40
|
+
// Take what the project already has as the source. Writing a placeholder over
|
|
41
|
+
// a real AGENTS.md, then compiling the placeholder back on top of it, is the
|
|
42
|
+
// exact drift this tool exists to prevent.
|
|
43
|
+
const taken = adopt(root);
|
|
44
|
+
if (taken.agents && !existsSync(join(root, DIR, "AGENTS.md")))
|
|
45
|
+
write(root, `${DIR}/AGENTS.md`, taken.agents);
|
|
46
|
+
for (const r of taken.rules)
|
|
47
|
+
if (!existsSync(join(root, DIR, "rules", `${r.name}.md`)))
|
|
48
|
+
write(root, `${DIR}/rules/${r.name}.md`, r.text);
|
|
49
|
+
|
|
35
50
|
if (!existsSync(join(root, DIR, "AGENTS.md")))
|
|
36
51
|
write(root, `${DIR}/AGENTS.md`, `# Project instructions
|
|
37
52
|
|
|
@@ -42,7 +57,11 @@ Keep it short. Anything that only matters for part of the tree belongs in
|
|
|
42
57
|
\`.agent-os/rules/\` instead, where it can be scoped to the files it applies to.
|
|
43
58
|
`);
|
|
44
59
|
mkdirSync(join(root, DIR, "skills"), { recursive: true });
|
|
45
|
-
|
|
60
|
+
|
|
61
|
+
// Only seed the placeholder when there was nothing to adopt. A project with
|
|
62
|
+
// real rules does not need an `example.md` compiled into every tool it uses.
|
|
63
|
+
const anyRule = readdirSync(join(root, DIR, "rules")).some((f) => f.endsWith(".md"));
|
|
64
|
+
if (!anyRule)
|
|
46
65
|
write(root, `${DIR}/rules/example.md`, `---
|
|
47
66
|
description: Conventions for the API layer
|
|
48
67
|
paths:
|
|
@@ -55,7 +74,7 @@ A rule with \`paths:\` is loaded only when the agent touches a matching file, in
|
|
|
55
74
|
every tool that supports conditional loading. Tools that do not support it get
|
|
56
75
|
these as a referenced list instead of always-on text.
|
|
57
76
|
`);
|
|
58
|
-
return present;
|
|
77
|
+
return { present, taken };
|
|
59
78
|
}
|
|
60
79
|
|
|
61
80
|
export async function main(argv) {
|
|
@@ -101,11 +120,19 @@ export async function main(argv) {
|
|
|
101
120
|
return;
|
|
102
121
|
}
|
|
103
122
|
|
|
123
|
+
let adopted = new Set();
|
|
124
|
+
|
|
104
125
|
if (cmd === "init") {
|
|
105
126
|
console.log(`\n${bold("agent-os init")} ${root}\n`);
|
|
106
127
|
console.log(summarise(found));
|
|
107
|
-
const present = scaffold(root, found);
|
|
108
|
-
|
|
128
|
+
const { present, taken } = scaffold(root, found);
|
|
129
|
+
adopted = taken.paths;
|
|
130
|
+
if (taken.from.length) {
|
|
131
|
+
console.log(`\n adopted into ${DIR}/ ${dim("— your existing files are now the source")}`);
|
|
132
|
+
for (const f of taken.from) console.log(` ${f}`);
|
|
133
|
+
} else {
|
|
134
|
+
console.log(`\n created ${DIR}/ ${dim("(config.json, AGENTS.md, rules/example.md)")}`);
|
|
135
|
+
}
|
|
109
136
|
if (!present.length) console.log(` ${dim("no tools detected — defaulting to claude-code; edit .agent-os/config.json")}`);
|
|
110
137
|
}
|
|
111
138
|
|
|
@@ -142,14 +169,61 @@ export async function main(argv) {
|
|
|
142
169
|
}
|
|
143
170
|
return TARGETS[t]?.label || t;
|
|
144
171
|
};
|
|
172
|
+
// Never overwrite a file this tool did not write. A generated file carries
|
|
173
|
+
// the banner; anything else at that path is the user's own work, and
|
|
174
|
+
// silently compiling over it is worse than doing nothing.
|
|
175
|
+
const force = argv.includes("--force");
|
|
176
|
+
const isOurs = (f) => {
|
|
177
|
+
const cur = readIfExists(root, f.path);
|
|
178
|
+
if (cur === null) return true; // nothing there yet
|
|
179
|
+
if (cur.includes(BANNER)) return true; // we wrote it
|
|
180
|
+
if (TARGETS[f.target]?.merge) return true; // merged in place, nothing lost
|
|
181
|
+
if (adopted.has(f.path)) return true; // init just took this as the source
|
|
182
|
+
if (matches(root, f.path, f.content) === true) return true; // already identical
|
|
183
|
+
return false;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const blocked = [];
|
|
145
187
|
for (const [t, fs_] of Object.entries(byTarget)) {
|
|
146
188
|
console.log(` ${labelFor(t).padEnd(38)} ${fs_.length} file(s)`);
|
|
147
189
|
for (const f of fs_) {
|
|
190
|
+
if (!force && !isOurs(f)) { blocked.push(f.path); console.log(` ${f.path} ${dim("SKIPPED — not generated by agent-os")}`); continue; }
|
|
148
191
|
if (!dry) write(root, f.path, f.content);
|
|
149
192
|
console.log(` ${f.path}`);
|
|
150
193
|
}
|
|
151
194
|
}
|
|
152
|
-
|
|
195
|
+
|
|
196
|
+
if (blocked.length) {
|
|
197
|
+
console.log(`\n${bold(`${blocked.length} file(s) left alone`)} because agent-os did not write them:\n`);
|
|
198
|
+
for (const b of blocked) console.log(` ${b}`);
|
|
199
|
+
console.log(`
|
|
200
|
+
Pick one:
|
|
201
|
+
· move the content into ${DIR}/ so it becomes the source, then re-run
|
|
202
|
+
· ${dim("--force")} to overwrite (the current content is lost — commit first)
|
|
203
|
+
`);
|
|
204
|
+
process.exitCode = 1;
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
console.log(`\n${dim("Generated files carry a banner. Edit .agent-os/ and re-run sync; never edit them directly.")}`);
|
|
209
|
+
|
|
210
|
+
// The rules are done and safe at this point. The Claude Code plugin is the
|
|
211
|
+
// other half — agents, skills, per-agent memory, the session hook — and it
|
|
212
|
+
// installs from the same repo. Only on `init`, never on `sync`.
|
|
213
|
+
if (cmd === "init" && targets.includes("claude-code") && !argv.includes("--no-plugin")) {
|
|
214
|
+
console.log(`\n${bold("Claude Code plugin")} ${dim(`${PLUGIN}@${MARKETPLACE_NAME}`)}\n`);
|
|
215
|
+
const r = installPlugin({ dry });
|
|
216
|
+
for (const line of r.done) console.log(` ${dim(line)}`);
|
|
217
|
+
if (r.ok && r.reason === "installed") {
|
|
218
|
+
console.log(`\n installed at project scope ${dim("— .claude/settings.json, so it travels with the repo")}`);
|
|
219
|
+
console.log(` ${dim("restart Claude Code, or /reload-plugins, then run /agent-os:init")}`);
|
|
220
|
+
} else if (!r.ok) {
|
|
221
|
+
console.log("");
|
|
222
|
+
for (const line of r.hint) console.log(` ${line}`);
|
|
223
|
+
}
|
|
224
|
+
console.log(`\n ${dim("--no-plugin skips this")}`);
|
|
225
|
+
}
|
|
226
|
+
console.log("");
|
|
153
227
|
return;
|
|
154
228
|
}
|
|
155
229
|
|
package/src/plugin.mjs
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install the Claude Code half of agent-os during `init`.
|
|
3
|
+
*
|
|
4
|
+
* The CLI compiles rules for every tool. Claude Code additionally gets the
|
|
5
|
+
* agents, skills, per-agent memory and session hook, which ship as a plugin —
|
|
6
|
+
* so `init` should install it rather than printing two slash commands and
|
|
7
|
+
* hoping. `claude plugin ...` with `--yes` is the documented automation path.
|
|
8
|
+
*
|
|
9
|
+
* Everything here is best effort and reversible: if the `claude` CLI is not on
|
|
10
|
+
* PATH, or a command fails, `init` reports it and carries on. The rules are the
|
|
11
|
+
* part that must not depend on this.
|
|
12
|
+
*/
|
|
13
|
+
import { spawnSync } from "node:child_process";
|
|
14
|
+
|
|
15
|
+
export const MARKETPLACE = "sayansr26/agent-os";
|
|
16
|
+
export const MARKETPLACE_NAME = "sayan-plugins";
|
|
17
|
+
export const PLUGIN = "agent-os";
|
|
18
|
+
|
|
19
|
+
const run = (args) =>
|
|
20
|
+
spawnSync("claude", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
21
|
+
|
|
22
|
+
/** Is the Claude Code CLI usable from here? */
|
|
23
|
+
export function claudeAvailable() {
|
|
24
|
+
const r = spawnSync("claude", ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
25
|
+
return !r.error && r.status === 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Add the marketplace and install the plugin at project scope, so the setup
|
|
30
|
+
* travels with the repository instead of living on one machine.
|
|
31
|
+
*/
|
|
32
|
+
export function installPlugin({ dry = false, scope = "project" } = {}) {
|
|
33
|
+
const steps = [
|
|
34
|
+
["marketplace", ["plugin", "marketplace", "add", MARKETPLACE, "--scope", scope]],
|
|
35
|
+
["plugin", ["plugin", "install", `${PLUGIN}@${MARKETPLACE_NAME}`, "--scope", scope, "--yes"]],
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
if (!claudeAvailable())
|
|
39
|
+
return {
|
|
40
|
+
ok: false,
|
|
41
|
+
reason: "no-cli",
|
|
42
|
+
done: [],
|
|
43
|
+
hint: [
|
|
44
|
+
"The `claude` CLI is not on PATH, so the plugin was not installed.",
|
|
45
|
+
"Inside Claude Code, run:",
|
|
46
|
+
` /plugin marketplace add ${MARKETPLACE}`,
|
|
47
|
+
` /plugin install ${PLUGIN}@${MARKETPLACE_NAME}`,
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
if (dry)
|
|
52
|
+
return { ok: true, reason: "dry-run", done: steps.map(([, a]) => `claude ${a.join(" ")}`), hint: [] };
|
|
53
|
+
|
|
54
|
+
const done = [];
|
|
55
|
+
for (const [what, args] of steps) {
|
|
56
|
+
const r = run(args);
|
|
57
|
+
if (r.status !== 0) {
|
|
58
|
+
const err = (r.stderr || r.stdout || "").trim().split("\n").slice(-3).join("\n");
|
|
59
|
+
return {
|
|
60
|
+
ok: false,
|
|
61
|
+
reason: what,
|
|
62
|
+
done,
|
|
63
|
+
hint: [
|
|
64
|
+
`\`claude ${args.join(" ")}\` failed:`,
|
|
65
|
+
...err.split("\n").map((l) => ` ${l}`),
|
|
66
|
+
"The rules above were still written. Install the plugin by hand inside",
|
|
67
|
+
`Claude Code: /plugin install ${PLUGIN}@${MARKETPLACE_NAME}`,
|
|
68
|
+
],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
done.push(`claude ${args.join(" ")}`);
|
|
72
|
+
}
|
|
73
|
+
return { ok: true, reason: "installed", done, hint: [] };
|
|
74
|
+
}
|
package/src/selftest.mjs
CHANGED
|
@@ -53,11 +53,66 @@ try {
|
|
|
53
53
|
|
|
54
54
|
console.log("\n drift");
|
|
55
55
|
ok(cli(["check"], root).status === 0, "check passes when in sync");
|
|
56
|
-
|
|
56
|
+
// Realistic drift: someone edits the body of a generated file and leaves the
|
|
57
|
+
// banner in place. sync owns that file and must put it back.
|
|
58
|
+
writeFileSync(join(root, ".cursor/rules/api.mdc"),
|
|
59
|
+
read(".cursor/rules/api.mdc").replace("Validate at the boundary.", "hand edited"));
|
|
57
60
|
const d = cli(["check"], root);
|
|
58
61
|
ok(d.status === 1, "check exits 1 on drift");
|
|
59
62
|
ok(d.stdout.includes("DRIFTED"), "check names the drifted file");
|
|
60
63
|
ok(cli(["sync"], root).status === 0 && cli(["check"], root).status === 0, "sync repairs drift");
|
|
64
|
+
// Banner stripped: now indistinguishable from a hand-written file, so sync
|
|
65
|
+
// refuses rather than guessing.
|
|
66
|
+
writeFileSync(join(root, ".cursor/rules/api.mdc"), "mine now\n");
|
|
67
|
+
ok(cli(["sync"], root).status === 1, "sync refuses once the banner is gone");
|
|
68
|
+
ok(read(".cursor/rules/api.mdc") === "mine now\n", "and leaves that file untouched");
|
|
69
|
+
cli(["sync", "--force"], root);
|
|
70
|
+
|
|
71
|
+
console.log("\n never clobbers what it did not write");
|
|
72
|
+
{
|
|
73
|
+
const r2 = mkdtempSync(join(tmpdir(), "agent-os-adopt-"));
|
|
74
|
+
mkdirSync(join(r2, ".claude/rules"), { recursive: true });
|
|
75
|
+
const realAgents = "# Real AGENTS.md\n\nCLAUDE.md is the source of truth.\n";
|
|
76
|
+
writeFileSync(join(r2, "AGENTS.md"), realAgents);
|
|
77
|
+
writeFileSync(join(r2, ".claude/rules/theming.md"),
|
|
78
|
+
'---\ndescription: Theming\npaths:\n - "src/**/*.tsx"\n---\n\nUse the token set.\n');
|
|
79
|
+
|
|
80
|
+
const i = cli(["init", "--no-plugin"], r2);
|
|
81
|
+
ok(i.status === 0, "init exits 0 on a project that already has rules");
|
|
82
|
+
ok(readFileSync(join(r2, "AGENTS.md"), "utf8") === realAgents ||
|
|
83
|
+
readFileSync(join(r2, ".agent-os/AGENTS.md"), "utf8") === realAgents,
|
|
84
|
+
"existing AGENTS.md becomes the source rather than being replaced");
|
|
85
|
+
ok(existsSync(join(r2, ".agent-os/rules/theming.md")), "existing .claude/rules/ are adopted");
|
|
86
|
+
ok(!existsSync(join(r2, ".agent-os/rules/example.md")), "no example.md when real rules were adopted");
|
|
87
|
+
ok(!existsSync(join(r2, ".claude/rules/example.md")), "no example.md compiled into the project");
|
|
88
|
+
ok(!i.stdout.includes("Claude Code plugin"), "--no-plugin skips the plugin install");
|
|
89
|
+
|
|
90
|
+
// init offers the plugin when not told otherwise; --dry-run proves the
|
|
91
|
+
// commands without running them against the machine's real config.
|
|
92
|
+
const r4 = mkdtempSync(join(tmpdir(), "agent-os-plug-"));
|
|
93
|
+
const pi = cli(["init", "--dry-run"], r4);
|
|
94
|
+
ok(pi.stdout.includes("Claude Code plugin"), "init sets up the Claude Code plugin by default");
|
|
95
|
+
ok(/marketplace add sayansr26\/agent-os|plugin marketplace add|not on PATH/.test(pi.stdout),
|
|
96
|
+
"init names the marketplace step or says why it could not run it");
|
|
97
|
+
rmSync(r4, { recursive: true, force: true });
|
|
98
|
+
|
|
99
|
+
// A hand-written file at a generated path must survive a sync.
|
|
100
|
+
const r3 = mkdtempSync(join(tmpdir(), "agent-os-guard-"));
|
|
101
|
+
mkdirSync(join(r3, ".agent-os/rules"), { recursive: true });
|
|
102
|
+
writeFileSync(join(r3, ".agent-os/config.json"), JSON.stringify({ targets: ["claude-code"] }));
|
|
103
|
+
writeFileSync(join(r3, ".agent-os/AGENTS.md"), "# Compiled\n");
|
|
104
|
+
writeFileSync(join(r3, ".agent-os/rules/x.md"), '---\npaths:\n - "a/**"\n---\n\nBody.\n');
|
|
105
|
+
const mine = "# Mine, by hand\n";
|
|
106
|
+
writeFileSync(join(r3, "AGENTS.md"), mine);
|
|
107
|
+
const g = cli(["sync"], r3);
|
|
108
|
+
ok(g.status === 1, "sync exits 1 rather than clobbering");
|
|
109
|
+
ok(readFileSync(join(r3, "AGENTS.md"), "utf8") === mine, "hand-written AGENTS.md survives sync");
|
|
110
|
+
ok(g.stdout.includes("SKIPPED"), "sync says which file it left alone");
|
|
111
|
+
const fg = cli(["sync", "--force"], r3);
|
|
112
|
+
ok(fg.status === 0 && readFileSync(join(r3, "AGENTS.md"), "utf8") !== mine, "--force overwrites when asked");
|
|
113
|
+
rmSync(r2, { recursive: true, force: true });
|
|
114
|
+
rmSync(r3, { recursive: true, force: true });
|
|
115
|
+
}
|
|
61
116
|
|
|
62
117
|
console.log("\n merge, not overwrite");
|
|
63
118
|
writeFileSync(join(root, "opencode.json"), JSON.stringify({ model: "anthropic/x", instructions: ["KEEP.md"] }, null, 2));
|
package/src/targets.mjs
CHANGED
|
@@ -28,7 +28,9 @@ function agentsMd(src) {
|
|
|
28
28
|
s.map((r) => `- \`${r.paths.join("`, `")}\` — ${r.description || r.name}`).join("\n")
|
|
29
29
|
);
|
|
30
30
|
}
|
|
31
|
-
|
|
31
|
+
// The banner is what lets `sync` tell its own output from the user's file.
|
|
32
|
+
// Without it AGENTS.md is indistinguishable from hand-written work.
|
|
33
|
+
return mdHeader + parts.filter(Boolean).join("\n\n") + "\n";
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
export const TARGETS = {
|