@jazpiper/rules-doctor 0.1.0 → 0.2.0
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 +77 -5
- package/dist/adapters/antigravity.js +12 -0
- package/dist/adapters/claude.js +31 -0
- package/dist/adapters/codex.js +14 -0
- package/dist/adapters/common.js +54 -0
- package/dist/adapters/cursor.js +19 -0
- package/dist/adapters/gemini.js +12 -0
- package/dist/adapters/index.js +14 -0
- package/dist/adapters/opencode.js +14 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +456 -251
- package/package.json +8 -4
package/README.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# @jazpiper/rules-doctor
|
|
2
2
|
|
|
3
|
-
`rules-doctor` is a Node.js CLI
|
|
3
|
+
`rules-doctor` is a Node.js CLI that keeps agent instruction files in sync from one source of truth: `.agentrules/rules.yaml`.
|
|
4
|
+
|
|
5
|
+
It is designed for multi-agent workflows where each coding CLI reads a different file format.
|
|
6
|
+
|
|
7
|
+
By default, commands resolve paths relative to the project root (`.git` ancestor), not the current subdirectory.
|
|
4
8
|
|
|
5
9
|
## Install
|
|
6
10
|
|
|
@@ -36,11 +40,15 @@ Creates `.agentrules/rules.yaml` if it does not exist.
|
|
|
36
40
|
rules-doctor sync
|
|
37
41
|
rules-doctor sync --target claude
|
|
38
42
|
rules-doctor sync --target codex
|
|
43
|
+
rules-doctor sync --target cursor,gemini,opencode,antigravity
|
|
39
44
|
```
|
|
40
45
|
|
|
41
46
|
Generates/updates:
|
|
42
|
-
- `CLAUDE.md` (fully managed)
|
|
43
|
-
- `AGENTS.md` (
|
|
47
|
+
- `CLAUDE.md` (`claude`, fully managed)
|
|
48
|
+
- `AGENTS.md` (`codex`, `opencode`, marker-managed)
|
|
49
|
+
- `.cursor/rules/rules-doctor.mdc` (`cursor`, fully managed)
|
|
50
|
+
- `GEMINI.md` (`gemini`, fully managed)
|
|
51
|
+
- `GEMINI.md` (`antigravity`, inferred-compatible mapping)
|
|
44
52
|
|
|
45
53
|
Managed marker block in `AGENTS.md`:
|
|
46
54
|
|
|
@@ -58,10 +66,74 @@ If markers are missing, a new managed block is appended to the end of `AGENTS.md
|
|
|
58
66
|
rules-doctor analyze
|
|
59
67
|
```
|
|
60
68
|
|
|
61
|
-
Reads
|
|
69
|
+
Reads enabled target files from `rules.yaml`, then prints a concise report about:
|
|
62
70
|
- missing markers
|
|
63
71
|
- missing verify commands (`lint`/`test`/`build`)
|
|
64
|
-
- obvious contradictions (simple heuristics)
|
|
72
|
+
- obvious contradictions (simple heuristics across generated targets)
|
|
73
|
+
|
|
74
|
+
`--strict` makes `analyze` fail with non-zero exit code when findings exist.
|
|
75
|
+
|
|
76
|
+
### 4) List supported adapters
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
rules-doctor targets list
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Shows built-in adapters and default file paths.
|
|
83
|
+
|
|
84
|
+
### 5) Directory/Path Notes
|
|
85
|
+
|
|
86
|
+
- `claude`: searches `CLAUDE.md` from current directory upward; also supports `.claude/CLAUDE.md` and `.claude/rules/*.md`.
|
|
87
|
+
- `codex`: looks for `AGENTS.override.md` or `AGENTS.md` from project root down to current directory.
|
|
88
|
+
- `opencode`: reads project `AGENTS.md`, plus user global `~/.config/opencode/AGENTS.md`.
|
|
89
|
+
- `cursor`: project rules live in `.cursor/rules/*.mdc` (legacy `.cursorrules` still exists).
|
|
90
|
+
- `gemini`: uses `GEMINI.md` in workspace/ancestor directories and `~/.gemini/GEMINI.md`.
|
|
91
|
+
- `antigravity`: currently mapped to `GEMINI.md` as an inferred default; verify in your environment.
|
|
92
|
+
|
|
93
|
+
## Rules Schema (v2 Draft)
|
|
94
|
+
|
|
95
|
+
`init` now creates a v2-compatible draft with `targets` configuration:
|
|
96
|
+
|
|
97
|
+
```yaml
|
|
98
|
+
version: 2
|
|
99
|
+
mission: "Ship safe changes quickly while keeping agent instructions consistent."
|
|
100
|
+
workflow:
|
|
101
|
+
- "Read relevant files before editing."
|
|
102
|
+
- "Make the smallest correct change."
|
|
103
|
+
- "Run verification commands before finalizing."
|
|
104
|
+
commands:
|
|
105
|
+
lint: "npm run lint"
|
|
106
|
+
test: "npm run test"
|
|
107
|
+
build: "npm run build"
|
|
108
|
+
done:
|
|
109
|
+
- "Commands pass or blockers are documented."
|
|
110
|
+
- "Changed behavior is reflected in docs where needed."
|
|
111
|
+
approvals:
|
|
112
|
+
mode: "ask-before-destructive"
|
|
113
|
+
notes:
|
|
114
|
+
- "Ask before destructive actions or privileged operations."
|
|
115
|
+
targets:
|
|
116
|
+
claude:
|
|
117
|
+
enabled: true
|
|
118
|
+
path: "CLAUDE.md"
|
|
119
|
+
codex:
|
|
120
|
+
enabled: true
|
|
121
|
+
path: "AGENTS.md"
|
|
122
|
+
cursor:
|
|
123
|
+
enabled: true
|
|
124
|
+
path: ".cursor/rules/rules-doctor.mdc"
|
|
125
|
+
gemini:
|
|
126
|
+
enabled: true
|
|
127
|
+
path: "GEMINI.md"
|
|
128
|
+
opencode:
|
|
129
|
+
enabled: true
|
|
130
|
+
path: "AGENTS.md"
|
|
131
|
+
antigravity:
|
|
132
|
+
enabled: true
|
|
133
|
+
path: "GEMINI.md"
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
You can disable or relocate any target by editing `targets.<id>.enabled/path`.
|
|
65
137
|
|
|
66
138
|
## Development
|
|
67
139
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const { renderManagedRulesBody } = require("./common");
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
id: "antigravity",
|
|
5
|
+
name: "Antigravity CLI",
|
|
6
|
+
description: "Generate GEMINI.md-compatible managed instruction file (inferred mapping).",
|
|
7
|
+
defaultPath: "GEMINI.md",
|
|
8
|
+
management: "full",
|
|
9
|
+
render(rules) {
|
|
10
|
+
return ["# GEMINI.md", "", renderManagedRulesBody(rules)].join("\n");
|
|
11
|
+
},
|
|
12
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const { formatCommands, formatList } = require("./common");
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
id: "claude",
|
|
5
|
+
name: "Claude Code",
|
|
6
|
+
description: "Generate CLAUDE.md from rules.yaml.",
|
|
7
|
+
defaultPath: "CLAUDE.md",
|
|
8
|
+
management: "full",
|
|
9
|
+
render(rules) {
|
|
10
|
+
return [
|
|
11
|
+
"# CLAUDE.md",
|
|
12
|
+
"",
|
|
13
|
+
"## Mission",
|
|
14
|
+
rules.mission,
|
|
15
|
+
"",
|
|
16
|
+
"## Workflow",
|
|
17
|
+
formatList(rules.workflow),
|
|
18
|
+
"",
|
|
19
|
+
"## Commands",
|
|
20
|
+
formatCommands(rules.commands),
|
|
21
|
+
"",
|
|
22
|
+
"## Done",
|
|
23
|
+
formatList(rules.done),
|
|
24
|
+
"",
|
|
25
|
+
"## Approvals",
|
|
26
|
+
`- Mode: \`${rules.approvals.mode}\``,
|
|
27
|
+
...rules.approvals.notes.map((note) => `- ${note}`),
|
|
28
|
+
"",
|
|
29
|
+
].join("\n");
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const { renderManagedRulesBody } = require("./common");
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
id: "codex",
|
|
5
|
+
name: "Codex CLI",
|
|
6
|
+
description: "Manage AGENTS.md via marker-managed section.",
|
|
7
|
+
defaultPath: "AGENTS.md",
|
|
8
|
+
management: "marker",
|
|
9
|
+
markerBegin: "<!-- RULES_DOCTOR:BEGIN -->",
|
|
10
|
+
markerEnd: "<!-- RULES_DOCTOR:END -->",
|
|
11
|
+
render(rules) {
|
|
12
|
+
return renderManagedRulesBody(rules);
|
|
13
|
+
},
|
|
14
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
function formatList(items) {
|
|
2
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
3
|
+
return "- (none)";
|
|
4
|
+
}
|
|
5
|
+
return items.map((item) => `- ${item}`).join("\n");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function formatCommands(commands) {
|
|
9
|
+
if (!commands || typeof commands !== "object") {
|
|
10
|
+
return "- (none)";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const preferredOrder = ["lint", "test", "build"];
|
|
14
|
+
const names = [
|
|
15
|
+
...preferredOrder.filter((name) => Object.prototype.hasOwnProperty.call(commands, name)),
|
|
16
|
+
...Object.keys(commands).filter((name) => !preferredOrder.includes(name)),
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
if (names.length === 0) {
|
|
20
|
+
return "- (none)";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return names.map((name) => `- ${name}: \`${commands[name]}\``).join("\n");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function renderManagedRulesBody(rules) {
|
|
27
|
+
return [
|
|
28
|
+
"## rules-doctor Managed Rules",
|
|
29
|
+
"Generated from `.agentrules/rules.yaml`. Edit that file, then run `rules-doctor sync`.",
|
|
30
|
+
"",
|
|
31
|
+
"### Mission",
|
|
32
|
+
rules.mission,
|
|
33
|
+
"",
|
|
34
|
+
"### Workflow",
|
|
35
|
+
formatList(rules.workflow),
|
|
36
|
+
"",
|
|
37
|
+
"### Commands",
|
|
38
|
+
formatCommands(rules.commands),
|
|
39
|
+
"",
|
|
40
|
+
"### Done",
|
|
41
|
+
formatList(rules.done),
|
|
42
|
+
"",
|
|
43
|
+
"### Approvals",
|
|
44
|
+
`- Policy: \`${rules.approvals.mode}\``,
|
|
45
|
+
...rules.approvals.notes.map((note) => `- ${note}`),
|
|
46
|
+
"",
|
|
47
|
+
].join("\n");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = {
|
|
51
|
+
formatCommands,
|
|
52
|
+
formatList,
|
|
53
|
+
renderManagedRulesBody,
|
|
54
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const { renderManagedRulesBody } = require("./common");
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
id: "cursor",
|
|
5
|
+
name: "Cursor",
|
|
6
|
+
description: "Manage .cursor/rules/rules-doctor.mdc as an always-applied project rule.",
|
|
7
|
+
defaultPath: ".cursor/rules/rules-doctor.mdc",
|
|
8
|
+
management: "full",
|
|
9
|
+
render(rules) {
|
|
10
|
+
return [
|
|
11
|
+
"---",
|
|
12
|
+
"description: rules-doctor managed coding rules",
|
|
13
|
+
"alwaysApply: true",
|
|
14
|
+
"---",
|
|
15
|
+
"",
|
|
16
|
+
renderManagedRulesBody(rules),
|
|
17
|
+
].join("\n");
|
|
18
|
+
},
|
|
19
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const { renderManagedRulesBody } = require("./common");
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
id: "gemini",
|
|
5
|
+
name: "Gemini CLI",
|
|
6
|
+
description: "Generate GEMINI.md managed instruction file.",
|
|
7
|
+
defaultPath: "GEMINI.md",
|
|
8
|
+
management: "full",
|
|
9
|
+
render(rules) {
|
|
10
|
+
return ["# GEMINI.md", "", renderManagedRulesBody(rules)].join("\n");
|
|
11
|
+
},
|
|
12
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const claude = require("./claude");
|
|
2
|
+
const codex = require("./codex");
|
|
3
|
+
const cursor = require("./cursor");
|
|
4
|
+
const gemini = require("./gemini");
|
|
5
|
+
const opencode = require("./opencode");
|
|
6
|
+
const antigravity = require("./antigravity");
|
|
7
|
+
|
|
8
|
+
const ADAPTERS = [claude, codex, cursor, gemini, opencode, antigravity];
|
|
9
|
+
const ADAPTERS_BY_ID = Object.fromEntries(ADAPTERS.map((adapter) => [adapter.id, adapter]));
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
ADAPTERS,
|
|
13
|
+
ADAPTERS_BY_ID,
|
|
14
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const { renderManagedRulesBody } = require("./common");
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
id: "opencode",
|
|
5
|
+
name: "OpenCode CLI",
|
|
6
|
+
description: "Manage AGENTS.md via marker-managed section (OpenCode rules).",
|
|
7
|
+
defaultPath: "AGENTS.md",
|
|
8
|
+
management: "marker",
|
|
9
|
+
markerBegin: "<!-- RULES_DOCTOR:BEGIN -->",
|
|
10
|
+
markerEnd: "<!-- RULES_DOCTOR:END -->",
|
|
11
|
+
render(rules) {
|
|
12
|
+
return renderManagedRulesBody(rules);
|
|
13
|
+
},
|
|
14
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.js
CHANGED
|
@@ -1,32 +1,43 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
const { dirname, isAbsolute, resolve } = require("node:path");
|
|
2
3
|
const { existsSync, mkdirSync, readFileSync, writeFileSync } = require("node:fs");
|
|
3
|
-
const {
|
|
4
|
+
const { ADAPTERS, ADAPTERS_BY_ID } = require("./adapters");
|
|
4
5
|
|
|
5
|
-
const
|
|
6
|
-
const CLAUDE_FILE = resolve("CLAUDE.md");
|
|
7
|
-
const AGENTS_FILE = resolve("AGENTS.md");
|
|
8
|
-
const MARKER_BEGIN = "<!-- RULES_DOCTOR:BEGIN -->";
|
|
9
|
-
const MARKER_END = "<!-- RULES_DOCTOR:END -->";
|
|
6
|
+
const RULES_RELATIVE_PATH = ".agentrules/rules.yaml";
|
|
10
7
|
|
|
11
8
|
function usage() {
|
|
9
|
+
const targets = ADAPTERS.map((adapter) => adapter.id).join("|");
|
|
12
10
|
return [
|
|
13
11
|
"rules-doctor",
|
|
14
12
|
"",
|
|
15
13
|
"Usage:",
|
|
16
14
|
" rules-doctor init",
|
|
17
|
-
|
|
15
|
+
` rules-doctor sync [--target all|${targets}|<comma-separated-targets>]`,
|
|
18
16
|
" rules-doctor analyze",
|
|
17
|
+
" rules-doctor targets list",
|
|
19
18
|
].join("\n");
|
|
20
19
|
}
|
|
21
20
|
|
|
21
|
+
function createLogger(options) {
|
|
22
|
+
return {
|
|
23
|
+
log:
|
|
24
|
+
options && typeof options.stdout === "function"
|
|
25
|
+
? options.stdout
|
|
26
|
+
: (message) => console.log(message),
|
|
27
|
+
error:
|
|
28
|
+
options && typeof options.stderr === "function"
|
|
29
|
+
? options.stderr
|
|
30
|
+
: (message) => console.error(message),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
22
34
|
function readJsonFile(filePath) {
|
|
23
35
|
if (!existsSync(filePath)) {
|
|
24
36
|
return null;
|
|
25
37
|
}
|
|
26
38
|
|
|
27
39
|
try {
|
|
28
|
-
|
|
29
|
-
return JSON.parse(raw);
|
|
40
|
+
return JSON.parse(readFileSync(filePath, "utf8"));
|
|
30
41
|
} catch {
|
|
31
42
|
return null;
|
|
32
43
|
}
|
|
@@ -49,6 +60,12 @@ function stripQuotes(value) {
|
|
|
49
60
|
|
|
50
61
|
function parseScalar(value) {
|
|
51
62
|
const cleaned = stripQuotes(value);
|
|
63
|
+
if (/^(true|false)$/i.test(cleaned)) {
|
|
64
|
+
return cleaned.toLowerCase() === "true";
|
|
65
|
+
}
|
|
66
|
+
if (cleaned === "null" || cleaned === "~") {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
52
69
|
if (/^-?\d+$/.test(cleaned)) {
|
|
53
70
|
return Number(cleaned);
|
|
54
71
|
}
|
|
@@ -70,6 +87,7 @@ function parseRulesText(text) {
|
|
|
70
87
|
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
71
88
|
let section = null;
|
|
72
89
|
let nested = null;
|
|
90
|
+
let currentTarget = null;
|
|
73
91
|
|
|
74
92
|
for (const rawLine of lines) {
|
|
75
93
|
if (!rawLine.trim() || rawLine.trim().startsWith("#")) {
|
|
@@ -81,6 +99,8 @@ function parseRulesText(text) {
|
|
|
81
99
|
|
|
82
100
|
if (indent === 0) {
|
|
83
101
|
nested = null;
|
|
102
|
+
currentTarget = null;
|
|
103
|
+
|
|
84
104
|
const top = line.match(/^([a-zA-Z0-9_-]+):(.*)$/);
|
|
85
105
|
if (!top) {
|
|
86
106
|
continue;
|
|
@@ -88,11 +108,12 @@ function parseRulesText(text) {
|
|
|
88
108
|
|
|
89
109
|
const key = top[1];
|
|
90
110
|
const value = top[2].trim();
|
|
111
|
+
|
|
91
112
|
if (!value) {
|
|
92
113
|
section = key;
|
|
93
114
|
if (key === "workflow" || key === "done") {
|
|
94
115
|
data[key] = [];
|
|
95
|
-
} else if (key === "commands" || key === "approvals") {
|
|
116
|
+
} else if (key === "commands" || key === "approvals" || key === "targets") {
|
|
96
117
|
data[key] = {};
|
|
97
118
|
}
|
|
98
119
|
} else {
|
|
@@ -132,6 +153,35 @@ function parseRulesText(text) {
|
|
|
132
153
|
if (nested === "notes" && line.startsWith("- ")) {
|
|
133
154
|
data.approvals.notes.push(parseScalar(line.slice(2)));
|
|
134
155
|
}
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (section === "targets") {
|
|
160
|
+
if (indent === 2) {
|
|
161
|
+
const target = line.match(/^([a-zA-Z0-9_-]+):(.*)$/);
|
|
162
|
+
if (!target) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
currentTarget = target[1];
|
|
167
|
+
const maybeValue = target[2].trim();
|
|
168
|
+
if (!maybeValue) {
|
|
169
|
+
data.targets[currentTarget] = {};
|
|
170
|
+
} else {
|
|
171
|
+
data.targets[currentTarget] = { path: parseScalar(maybeValue), enabled: true };
|
|
172
|
+
}
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (indent >= 4 && currentTarget) {
|
|
177
|
+
const pair = line.match(/^([a-zA-Z0-9_-]+):(.*)$/);
|
|
178
|
+
if (pair) {
|
|
179
|
+
if (!data.targets[currentTarget] || typeof data.targets[currentTarget] !== "object") {
|
|
180
|
+
data.targets[currentTarget] = {};
|
|
181
|
+
}
|
|
182
|
+
data.targets[currentTarget][pair[1]] = parseScalar(pair[2].trim());
|
|
183
|
+
}
|
|
184
|
+
}
|
|
135
185
|
}
|
|
136
186
|
}
|
|
137
187
|
|
|
@@ -139,31 +189,15 @@ function parseRulesText(text) {
|
|
|
139
189
|
}
|
|
140
190
|
|
|
141
191
|
function quoteYaml(value) {
|
|
192
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
193
|
+
return String(value);
|
|
194
|
+
}
|
|
195
|
+
if (value === null || typeof value === "undefined") {
|
|
196
|
+
return "null";
|
|
197
|
+
}
|
|
142
198
|
return JSON.stringify(String(value));
|
|
143
199
|
}
|
|
144
200
|
|
|
145
|
-
function stringifyRules(rules) {
|
|
146
|
-
const lines = [
|
|
147
|
-
`version: ${Number.isFinite(rules.version) ? rules.version : 1}`,
|
|
148
|
-
`mission: ${quoteYaml(rules.mission)}`,
|
|
149
|
-
"workflow:",
|
|
150
|
-
...rules.workflow.map((step) => ` - ${quoteYaml(step)}`),
|
|
151
|
-
"commands:",
|
|
152
|
-
...Object.keys(rules.commands).map(
|
|
153
|
-
(name) => ` ${name}: ${quoteYaml(rules.commands[name])}`,
|
|
154
|
-
),
|
|
155
|
-
"done:",
|
|
156
|
-
...rules.done.map((item) => ` - ${quoteYaml(item)}`),
|
|
157
|
-
"approvals:",
|
|
158
|
-
` mode: ${quoteYaml(rules.approvals.mode)}`,
|
|
159
|
-
" notes:",
|
|
160
|
-
...rules.approvals.notes.map((note) => ` - ${quoteYaml(note)}`),
|
|
161
|
-
"",
|
|
162
|
-
];
|
|
163
|
-
|
|
164
|
-
return lines.join("\n");
|
|
165
|
-
}
|
|
166
|
-
|
|
167
201
|
function inferCommandFromScripts(scripts, scriptName) {
|
|
168
202
|
if (scripts && typeof scripts[scriptName] === "string") {
|
|
169
203
|
return `npm run ${scriptName}`;
|
|
@@ -171,12 +205,17 @@ function inferCommandFromScripts(scripts, scriptName) {
|
|
|
171
205
|
return `echo "TODO: define ${scriptName} command"`;
|
|
172
206
|
}
|
|
173
207
|
|
|
174
|
-
function createDefaultRules() {
|
|
175
|
-
const
|
|
176
|
-
const
|
|
208
|
+
function createDefaultRules(scripts) {
|
|
209
|
+
const targets = {};
|
|
210
|
+
for (const adapter of ADAPTERS) {
|
|
211
|
+
targets[adapter.id] = {
|
|
212
|
+
enabled: true,
|
|
213
|
+
path: adapter.defaultPath,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
177
216
|
|
|
178
217
|
return {
|
|
179
|
-
version:
|
|
218
|
+
version: 2,
|
|
180
219
|
mission: "Ship safe changes quickly while keeping agent instructions consistent.",
|
|
181
220
|
workflow: [
|
|
182
221
|
"Read relevant files before editing.",
|
|
@@ -196,322 +235,488 @@ function createDefaultRules() {
|
|
|
196
235
|
mode: "ask-before-destructive",
|
|
197
236
|
notes: ["Ask before destructive actions or privileged operations."],
|
|
198
237
|
},
|
|
238
|
+
targets,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function normalizeTargetConfig(source, fallbackPath) {
|
|
243
|
+
if (typeof source === "string" && source.trim()) {
|
|
244
|
+
return { enabled: true, path: source.trim() };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (!source || typeof source !== "object") {
|
|
248
|
+
return { enabled: true, path: fallbackPath };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
enabled: typeof source.enabled === "boolean" ? source.enabled : true,
|
|
253
|
+
path: typeof source.path === "string" && source.path.trim() ? source.path.trim() : fallbackPath,
|
|
199
254
|
};
|
|
200
255
|
}
|
|
201
256
|
|
|
202
|
-
function normalizeRules(input) {
|
|
257
|
+
function normalizeRules(input, defaults) {
|
|
203
258
|
const source = input && typeof input === "object" ? input : {};
|
|
204
259
|
const commands = source.commands && typeof source.commands === "object" ? source.commands : {};
|
|
205
260
|
const approvals =
|
|
206
261
|
source.approvals && typeof source.approvals === "object" ? source.approvals : {};
|
|
262
|
+
const sourceTargets = source.targets && typeof source.targets === "object" ? source.targets : {};
|
|
207
263
|
|
|
208
264
|
const workflow = Array.isArray(source.workflow)
|
|
209
265
|
? source.workflow.filter((item) => typeof item === "string")
|
|
210
|
-
:
|
|
266
|
+
: defaults.workflow;
|
|
211
267
|
|
|
212
268
|
const done = Array.isArray(source.done)
|
|
213
269
|
? source.done.filter((item) => typeof item === "string")
|
|
214
|
-
:
|
|
270
|
+
: defaults.done;
|
|
215
271
|
|
|
216
272
|
const notes = Array.isArray(approvals.notes)
|
|
217
273
|
? approvals.notes.filter((item) => typeof item === "string")
|
|
218
|
-
:
|
|
274
|
+
: defaults.approvals.notes;
|
|
275
|
+
|
|
276
|
+
const targets = {};
|
|
277
|
+
for (const adapter of ADAPTERS) {
|
|
278
|
+
targets[adapter.id] = normalizeTargetConfig(sourceTargets[adapter.id], defaults.targets[adapter.id].path);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
for (const customId of Object.keys(sourceTargets)) {
|
|
282
|
+
if (targets[customId]) {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
targets[customId] = normalizeTargetConfig(sourceTargets[customId], `${customId.toUpperCase()}.md`);
|
|
286
|
+
}
|
|
219
287
|
|
|
220
288
|
return {
|
|
221
|
-
version: typeof source.version === "number" ? source.version :
|
|
289
|
+
version: typeof source.version === "number" ? source.version : defaults.version,
|
|
222
290
|
mission:
|
|
223
|
-
typeof source.mission === "string" && source.mission.trim()
|
|
224
|
-
|
|
225
|
-
: "Define your project mission.",
|
|
226
|
-
workflow,
|
|
291
|
+
typeof source.mission === "string" && source.mission.trim() ? source.mission : defaults.mission,
|
|
292
|
+
workflow: workflow.length > 0 ? workflow : defaults.workflow,
|
|
227
293
|
commands: {
|
|
228
|
-
lint:
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
: 'echo "TODO: define lint command"',
|
|
232
|
-
test:
|
|
233
|
-
typeof commands.test === "string"
|
|
234
|
-
? commands.test
|
|
235
|
-
: 'echo "TODO: define test command"',
|
|
236
|
-
build:
|
|
237
|
-
typeof commands.build === "string"
|
|
238
|
-
? commands.build
|
|
239
|
-
: 'echo "TODO: define build command"',
|
|
294
|
+
lint: typeof commands.lint === "string" ? commands.lint : defaults.commands.lint,
|
|
295
|
+
test: typeof commands.test === "string" ? commands.test : defaults.commands.test,
|
|
296
|
+
build: typeof commands.build === "string" ? commands.build : defaults.commands.build,
|
|
240
297
|
},
|
|
241
|
-
done,
|
|
298
|
+
done: done.length > 0 ? done : defaults.done,
|
|
242
299
|
approvals: {
|
|
243
|
-
mode:
|
|
244
|
-
typeof approvals.mode === "string" ? approvals.mode : "ask-before-destructive",
|
|
300
|
+
mode: typeof approvals.mode === "string" ? approvals.mode : defaults.approvals.mode,
|
|
245
301
|
notes,
|
|
246
302
|
},
|
|
303
|
+
targets,
|
|
247
304
|
};
|
|
248
305
|
}
|
|
249
306
|
|
|
250
|
-
function
|
|
251
|
-
|
|
252
|
-
|
|
307
|
+
function stringifyRules(rules) {
|
|
308
|
+
const knownTargetIds = ADAPTERS.map((adapter) => adapter.id);
|
|
309
|
+
const allTargetIds = [
|
|
310
|
+
...knownTargetIds.filter((id) => Object.prototype.hasOwnProperty.call(rules.targets || {}, id)),
|
|
311
|
+
...Object.keys(rules.targets || {})
|
|
312
|
+
.filter((id) => !knownTargetIds.includes(id))
|
|
313
|
+
.sort(),
|
|
314
|
+
];
|
|
315
|
+
|
|
316
|
+
const lines = [
|
|
317
|
+
`version: ${quoteYaml(Number.isFinite(rules.version) ? rules.version : 2)}`,
|
|
318
|
+
`mission: ${quoteYaml(rules.mission)}`,
|
|
319
|
+
"workflow:",
|
|
320
|
+
...rules.workflow.map((step) => ` - ${quoteYaml(step)}`),
|
|
321
|
+
"commands:",
|
|
322
|
+
...Object.keys(rules.commands).map((name) => ` ${name}: ${quoteYaml(rules.commands[name])}`),
|
|
323
|
+
"done:",
|
|
324
|
+
...rules.done.map((item) => ` - ${quoteYaml(item)}`),
|
|
325
|
+
"approvals:",
|
|
326
|
+
` mode: ${quoteYaml(rules.approvals.mode)}`,
|
|
327
|
+
" notes:",
|
|
328
|
+
...rules.approvals.notes.map((note) => ` - ${quoteYaml(note)}`),
|
|
329
|
+
"targets:",
|
|
330
|
+
];
|
|
331
|
+
|
|
332
|
+
for (const id of allTargetIds) {
|
|
333
|
+
const config = normalizeTargetConfig(rules.targets[id], `${id.toUpperCase()}.md`);
|
|
334
|
+
lines.push(` ${id}:`);
|
|
335
|
+
lines.push(` enabled: ${quoteYaml(config.enabled)}`);
|
|
336
|
+
lines.push(` path: ${quoteYaml(config.path)}`);
|
|
253
337
|
}
|
|
254
338
|
|
|
255
|
-
|
|
256
|
-
return
|
|
339
|
+
lines.push("");
|
|
340
|
+
return lines.join("\n");
|
|
257
341
|
}
|
|
258
342
|
|
|
259
|
-
function
|
|
260
|
-
|
|
261
|
-
return "- (none)";
|
|
262
|
-
}
|
|
263
|
-
return items.map((item) => `- ${item}`).join("\n");
|
|
343
|
+
function hasVerifyCommand(text) {
|
|
344
|
+
return /\b(npm run|pnpm|yarn|bun)\s+(lint|test|build)\b/i.test(text);
|
|
264
345
|
}
|
|
265
346
|
|
|
266
|
-
function
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
347
|
+
function hasNoApprovalLanguage(text) {
|
|
348
|
+
return /never ask (for )?approval|no approvals|without approval|do not ask for approval/i.test(
|
|
349
|
+
text,
|
|
350
|
+
);
|
|
351
|
+
}
|
|
270
352
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
...Object.keys(commands).filter((name) => !preferredOrder.includes(name)),
|
|
275
|
-
];
|
|
353
|
+
function hasAskApprovalLanguage(text) {
|
|
354
|
+
return /ask for approval|request approval|require approval|needs approval/i.test(text);
|
|
355
|
+
}
|
|
276
356
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
357
|
+
function hasRequireTestsLanguage(text) {
|
|
358
|
+
return /must run tests|always run tests|run tests before done/i.test(text);
|
|
359
|
+
}
|
|
280
360
|
|
|
281
|
-
|
|
361
|
+
function hasSkipTestsLanguage(text) {
|
|
362
|
+
return /skip tests|tests optional|do not run tests/i.test(text);
|
|
282
363
|
}
|
|
283
364
|
|
|
284
|
-
function
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
rules.mission,
|
|
290
|
-
"",
|
|
291
|
-
"## Workflow",
|
|
292
|
-
formatList(rules.workflow),
|
|
293
|
-
"",
|
|
294
|
-
"## Commands",
|
|
295
|
-
formatCommands(rules.commands),
|
|
296
|
-
"",
|
|
297
|
-
"## Done",
|
|
298
|
-
formatList(rules.done),
|
|
299
|
-
"",
|
|
300
|
-
"## Approvals",
|
|
301
|
-
`- Mode: \`${rules.approvals.mode}\``,
|
|
302
|
-
...rules.approvals.notes.map((note) => `- ${note}`),
|
|
303
|
-
"",
|
|
304
|
-
].join("\n");
|
|
365
|
+
function resolveInRoot(rootDir, filePath) {
|
|
366
|
+
if (isAbsolute(filePath)) {
|
|
367
|
+
return filePath;
|
|
368
|
+
}
|
|
369
|
+
return resolve(rootDir, filePath);
|
|
305
370
|
}
|
|
306
371
|
|
|
307
|
-
function
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
formatCommands(rules.commands),
|
|
323
|
-
"",
|
|
324
|
-
"### Failure Loop",
|
|
325
|
-
"1. Capture the exact failing command and error output.",
|
|
326
|
-
"2. Form one concrete hypothesis for the failure.",
|
|
327
|
-
"3. Apply one fix and rerun the same command.",
|
|
328
|
-
"4. Repeat until green or blocked, then report blocker and next action.",
|
|
329
|
-
"",
|
|
330
|
-
"### Done",
|
|
331
|
-
formatList(rules.done),
|
|
332
|
-
"",
|
|
333
|
-
"### Approvals",
|
|
334
|
-
`- Policy: \`${rules.approvals.mode}\``,
|
|
335
|
-
...rules.approvals.notes.map((note) => `- ${note}`),
|
|
336
|
-
"",
|
|
337
|
-
].join("\n");
|
|
372
|
+
function findProjectRoot(startDir) {
|
|
373
|
+
let current = resolve(startDir);
|
|
374
|
+
while (true) {
|
|
375
|
+
const gitPath = resolve(current, ".git");
|
|
376
|
+
const rulesPath = resolve(current, RULES_RELATIVE_PATH);
|
|
377
|
+
if (existsSync(rulesPath) || existsSync(gitPath)) {
|
|
378
|
+
return current;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const parent = dirname(current);
|
|
382
|
+
if (parent === current) {
|
|
383
|
+
return resolve(startDir);
|
|
384
|
+
}
|
|
385
|
+
current = parent;
|
|
386
|
+
}
|
|
338
387
|
}
|
|
339
388
|
|
|
340
|
-
function
|
|
341
|
-
|
|
342
|
-
|
|
389
|
+
function ensureParentDirectory(filePath) {
|
|
390
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function upsertManagedSection(existing, content, beginMarker, endMarker) {
|
|
394
|
+
const start = existing.indexOf(beginMarker);
|
|
395
|
+
const end = start >= 0 ? existing.indexOf(endMarker, start) : -1;
|
|
343
396
|
|
|
344
397
|
if (start >= 0 && end > start) {
|
|
345
|
-
const before = existing.slice(0, start +
|
|
398
|
+
const before = existing.slice(0, start + beginMarker.length);
|
|
346
399
|
const after = existing.slice(end);
|
|
347
400
|
return `${before}\n${content.trim()}\n${after}`.replace(/\n{3,}/g, "\n\n");
|
|
348
401
|
}
|
|
349
402
|
|
|
350
403
|
const base = existing.trimEnd();
|
|
351
404
|
const prefix = base ? `${base}\n\n` : "";
|
|
352
|
-
return `${prefix}${
|
|
405
|
+
return `${prefix}${beginMarker}\n${content.trim()}\n${endMarker}\n`;
|
|
353
406
|
}
|
|
354
407
|
|
|
355
|
-
function
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
return;
|
|
408
|
+
function loadPackageScripts(rootDir) {
|
|
409
|
+
const pkg = readJsonFile(resolve(rootDir, "package.json"));
|
|
410
|
+
if (!pkg || typeof pkg !== "object" || !pkg.scripts || typeof pkg.scripts !== "object") {
|
|
411
|
+
return {};
|
|
412
|
+
}
|
|
413
|
+
return pkg.scripts;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function loadRules(rootDir, options) {
|
|
417
|
+
const rulesFile = resolve(rootDir, RULES_RELATIVE_PATH);
|
|
418
|
+
const defaults = createDefaultRules(loadPackageScripts(rootDir));
|
|
419
|
+
|
|
420
|
+
if (!existsSync(rulesFile)) {
|
|
421
|
+
if (options && options.allowMissing) {
|
|
422
|
+
return {
|
|
423
|
+
rules: defaults,
|
|
424
|
+
rulesFile,
|
|
425
|
+
rulesExists: false,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
throw new Error(`Missing ${rulesFile}. Run "rules-doctor init" to create it first.`);
|
|
359
429
|
}
|
|
360
430
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
431
|
+
const parsed = parseRulesText(readFileSync(rulesFile, "utf8"));
|
|
432
|
+
return {
|
|
433
|
+
rules: normalizeRules(parsed, defaults),
|
|
434
|
+
rulesFile,
|
|
435
|
+
rulesExists: true,
|
|
436
|
+
};
|
|
364
437
|
}
|
|
365
438
|
|
|
366
|
-
function
|
|
367
|
-
|
|
439
|
+
function getTargetsFromSpec(spec) {
|
|
440
|
+
if (spec === "all") {
|
|
441
|
+
return ADAPTERS.map((adapter) => adapter.id);
|
|
442
|
+
}
|
|
368
443
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
444
|
+
const unique = [];
|
|
445
|
+
for (const raw of spec.split(",")) {
|
|
446
|
+
const id = raw.trim();
|
|
447
|
+
if (!id) {
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
if (!ADAPTERS_BY_ID[id]) {
|
|
451
|
+
throw new Error(
|
|
452
|
+
`Unknown target "${id}". Use one of: all, ${ADAPTERS.map((adapter) => adapter.id).join(", ")}`,
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
if (!unique.includes(id)) {
|
|
456
|
+
unique.push(id);
|
|
457
|
+
}
|
|
372
458
|
}
|
|
373
459
|
|
|
374
|
-
if (
|
|
375
|
-
|
|
376
|
-
const updated = upsertManagedSection(existing, renderCodexManagedSection(rules));
|
|
377
|
-
writeFileSync(AGENTS_FILE, updated, "utf8");
|
|
378
|
-
console.log(`Updated ${AGENTS_FILE}`);
|
|
460
|
+
if (unique.length === 0) {
|
|
461
|
+
throw new Error("No targets selected.");
|
|
379
462
|
}
|
|
380
|
-
}
|
|
381
463
|
|
|
382
|
-
|
|
383
|
-
return /\b(npm run|pnpm|yarn)\s+(lint|test|build)\b/i.test(text);
|
|
464
|
+
return unique;
|
|
384
465
|
}
|
|
385
466
|
|
|
386
|
-
function
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
}
|
|
467
|
+
function parseSyncTargets(args) {
|
|
468
|
+
if (!args || args.length === 0) {
|
|
469
|
+
return ADAPTERS.map((adapter) => adapter.id);
|
|
470
|
+
}
|
|
391
471
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
472
|
+
let targetSpec = "all";
|
|
473
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
474
|
+
const arg = args[index];
|
|
475
|
+
if (arg === "--target") {
|
|
476
|
+
const value = args[index + 1];
|
|
477
|
+
if (!value) {
|
|
478
|
+
throw new Error("Missing value for --target");
|
|
479
|
+
}
|
|
480
|
+
targetSpec = value;
|
|
481
|
+
index += 1;
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
395
484
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
}
|
|
485
|
+
throw new Error(`Unknown option for sync: ${arg}`);
|
|
486
|
+
}
|
|
399
487
|
|
|
400
|
-
|
|
401
|
-
return /skip tests|tests optional|do not run tests/i.test(text);
|
|
488
|
+
return getTargetsFromSpec(targetSpec);
|
|
402
489
|
}
|
|
403
490
|
|
|
404
|
-
function
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
const claude = claudeExists ? readFileSync(CLAUDE_FILE, "utf8") : "";
|
|
408
|
-
const agents = agentsExists ? readFileSync(AGENTS_FILE, "utf8") : "";
|
|
409
|
-
const issues = [];
|
|
410
|
-
|
|
411
|
-
if (!claudeExists) {
|
|
412
|
-
issues.push("CLAUDE.md missing.");
|
|
413
|
-
}
|
|
414
|
-
if (!agentsExists) {
|
|
415
|
-
issues.push("AGENTS.md missing.");
|
|
491
|
+
function parseAnalyzeArgs(args) {
|
|
492
|
+
if (!args || args.length === 0) {
|
|
493
|
+
return {};
|
|
416
494
|
}
|
|
417
495
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
if (!hasBegin || !hasEnd) {
|
|
422
|
-
issues.push("AGENTS.md missing rules-doctor markers.");
|
|
496
|
+
for (const arg of args) {
|
|
497
|
+
if (arg === "--strict") {
|
|
498
|
+
return { strict: true };
|
|
423
499
|
}
|
|
500
|
+
throw new Error(`Unknown option for analyze: ${arg}`);
|
|
424
501
|
}
|
|
425
502
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
}
|
|
503
|
+
return {};
|
|
504
|
+
}
|
|
429
505
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
506
|
+
function getTargetConfig(rules, adapter) {
|
|
507
|
+
const source = rules.targets && typeof rules.targets === "object" ? rules.targets[adapter.id] : null;
|
|
508
|
+
return normalizeTargetConfig(source, adapter.defaultPath);
|
|
509
|
+
}
|
|
433
510
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
511
|
+
function initCommand(rootDir, logger) {
|
|
512
|
+
const rulesFile = resolve(rootDir, RULES_RELATIVE_PATH);
|
|
513
|
+
if (existsSync(rulesFile)) {
|
|
514
|
+
logger.log(`rules.yaml already exists: ${rulesFile}`);
|
|
515
|
+
return;
|
|
439
516
|
}
|
|
440
517
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
}
|
|
518
|
+
const defaults = createDefaultRules(loadPackageScripts(rootDir));
|
|
519
|
+
ensureParentDirectory(rulesFile);
|
|
520
|
+
writeFileSync(rulesFile, stringifyRules(defaults), "utf8");
|
|
521
|
+
logger.log(`Created ${rulesFile}`);
|
|
522
|
+
}
|
|
447
523
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
console.log("- Findings:");
|
|
524
|
+
function syncCommand(rootDir, logger, args) {
|
|
525
|
+
const { rules } = loadRules(rootDir);
|
|
526
|
+
const selectedTargetIds = parseSyncTargets(args);
|
|
452
527
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
528
|
+
let updated = 0;
|
|
529
|
+
for (const targetId of selectedTargetIds) {
|
|
530
|
+
const adapter = ADAPTERS_BY_ID[targetId];
|
|
531
|
+
const target = getTargetConfig(rules, adapter);
|
|
532
|
+
|
|
533
|
+
if (!target.enabled) {
|
|
534
|
+
logger.log(`Skipped ${targetId} (disabled in rules.yaml).`);
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const targetPath = resolveInRoot(rootDir, target.path);
|
|
539
|
+
const rendered = adapter.render(rules).trim();
|
|
540
|
+
ensureParentDirectory(targetPath);
|
|
541
|
+
|
|
542
|
+
if (adapter.management === "marker") {
|
|
543
|
+
const existing = existsSync(targetPath) ? readFileSync(targetPath, "utf8") : "";
|
|
544
|
+
const updatedText = upsertManagedSection(
|
|
545
|
+
existing,
|
|
546
|
+
rendered,
|
|
547
|
+
adapter.markerBegin,
|
|
548
|
+
adapter.markerEnd,
|
|
549
|
+
);
|
|
550
|
+
writeFileSync(targetPath, updatedText, "utf8");
|
|
551
|
+
} else {
|
|
552
|
+
writeFileSync(targetPath, `${rendered}\n`, "utf8");
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
logger.log(`Updated ${targetPath} (${targetId})`);
|
|
556
|
+
updated += 1;
|
|
456
557
|
}
|
|
457
558
|
|
|
458
|
-
|
|
459
|
-
|
|
559
|
+
if (updated === 0) {
|
|
560
|
+
logger.log("No files updated.");
|
|
460
561
|
}
|
|
461
562
|
}
|
|
462
563
|
|
|
463
|
-
function
|
|
464
|
-
|
|
564
|
+
function analyzeCommand(rootDir, logger, args) {
|
|
565
|
+
const options = parseAnalyzeArgs(args);
|
|
566
|
+
const { rules, rulesExists, rulesFile } = loadRules(rootDir, { allowMissing: true });
|
|
567
|
+
const issues = [];
|
|
568
|
+
const targetSnapshots = [];
|
|
465
569
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
570
|
+
logger.log("rules-doctor analyze");
|
|
571
|
+
logger.log(`- rules.yaml: ${rulesExists ? "found" : "missing (using defaults)"}`);
|
|
572
|
+
logger.log(`- rules path: ${rulesFile}`);
|
|
573
|
+
|
|
574
|
+
for (const adapter of ADAPTERS) {
|
|
575
|
+
const target = getTargetConfig(rules, adapter);
|
|
576
|
+
const absolutePath = resolveInRoot(rootDir, target.path);
|
|
577
|
+
const fileExists = existsSync(absolutePath);
|
|
578
|
+
const content = fileExists ? readFileSync(absolutePath, "utf8") : "";
|
|
579
|
+
|
|
580
|
+
logger.log(
|
|
581
|
+
`- target ${adapter.id}: ${target.enabled ? "enabled" : "disabled"}, ${
|
|
582
|
+
fileExists ? "found" : "missing"
|
|
583
|
+
} (${target.path})`,
|
|
584
|
+
);
|
|
585
|
+
|
|
586
|
+
if (!target.enabled) {
|
|
475
587
|
continue;
|
|
476
588
|
}
|
|
477
589
|
|
|
478
|
-
|
|
590
|
+
if (!fileExists) {
|
|
591
|
+
issues.push(`${adapter.id}: expected file is missing (${target.path}).`);
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (adapter.management === "marker") {
|
|
596
|
+
const hasBegin = content.includes(adapter.markerBegin);
|
|
597
|
+
const hasEnd = content.includes(adapter.markerEnd);
|
|
598
|
+
if (!hasBegin || !hasEnd) {
|
|
599
|
+
issues.push(`${adapter.id}: marker block is missing.`);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (!hasVerifyCommand(content)) {
|
|
604
|
+
issues.push(`${adapter.id}: verify commands (lint/test/build) not detected.`);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
targetSnapshots.push({
|
|
608
|
+
id: adapter.id,
|
|
609
|
+
content,
|
|
610
|
+
asksApproval: hasAskApprovalLanguage(content),
|
|
611
|
+
noApproval: hasNoApprovalLanguage(content),
|
|
612
|
+
requiresTests: hasRequireTestsLanguage(content),
|
|
613
|
+
skipsTests: hasSkipTestsLanguage(content),
|
|
614
|
+
});
|
|
479
615
|
}
|
|
480
616
|
|
|
481
|
-
|
|
482
|
-
|
|
617
|
+
const askApprovalTargets = targetSnapshots.filter((item) => item.asksApproval).map((item) => item.id);
|
|
618
|
+
const noApprovalTargets = targetSnapshots.filter((item) => item.noApproval).map((item) => item.id);
|
|
619
|
+
if (askApprovalTargets.length > 0 && noApprovalTargets.length > 0) {
|
|
620
|
+
issues.push(
|
|
621
|
+
`Potential contradiction: approval guidance differs (${askApprovalTargets.join(
|
|
622
|
+
", ",
|
|
623
|
+
)} vs ${noApprovalTargets.join(", ")}).`,
|
|
624
|
+
);
|
|
625
|
+
}
|
|
483
626
|
|
|
484
|
-
|
|
485
|
-
|
|
627
|
+
const requireTestsTargets = targetSnapshots
|
|
628
|
+
.filter((item) => item.requiresTests)
|
|
629
|
+
.map((item) => item.id);
|
|
630
|
+
const skipTestsTargets = targetSnapshots.filter((item) => item.skipsTests).map((item) => item.id);
|
|
631
|
+
if (requireTestsTargets.length > 0 && skipTestsTargets.length > 0) {
|
|
632
|
+
issues.push(
|
|
633
|
+
`Potential contradiction: test guidance differs (${requireTestsTargets.join(
|
|
634
|
+
", ",
|
|
635
|
+
)} vs ${skipTestsTargets.join(", ")}).`,
|
|
636
|
+
);
|
|
637
|
+
}
|
|
486
638
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
639
|
+
logger.log("- Findings:");
|
|
640
|
+
if (issues.length === 0) {
|
|
641
|
+
logger.log("- No obvious issues found.");
|
|
642
|
+
return 0;
|
|
490
643
|
}
|
|
491
644
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
return;
|
|
645
|
+
for (const issue of issues) {
|
|
646
|
+
logger.log(`- ${issue}`);
|
|
495
647
|
}
|
|
496
648
|
|
|
497
|
-
if (
|
|
498
|
-
|
|
499
|
-
return;
|
|
649
|
+
if (options.strict) {
|
|
650
|
+
throw new Error(`Analyze failed in strict mode with ${issues.length} issue(s).`);
|
|
500
651
|
}
|
|
501
652
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
653
|
+
return 0;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function targetsListCommand(logger) {
|
|
657
|
+
logger.log("Supported targets:");
|
|
658
|
+
for (const adapter of ADAPTERS) {
|
|
659
|
+
const mode = adapter.management === "marker" ? "marker-managed" : "full-managed";
|
|
660
|
+
logger.log(`- ${adapter.id}: ${adapter.name}`);
|
|
661
|
+
logger.log(` default path: ${adapter.defaultPath}`);
|
|
662
|
+
logger.log(` mode: ${mode}`);
|
|
663
|
+
logger.log(` ${adapter.description}`);
|
|
505
664
|
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function runCli(argv, options) {
|
|
668
|
+
const args = Array.isArray(argv) ? argv : [];
|
|
669
|
+
const logger = createLogger(options || {});
|
|
670
|
+
const cwd = resolve(options && options.cwd ? options.cwd : process.cwd());
|
|
671
|
+
const rootDir = findProjectRoot(cwd);
|
|
506
672
|
|
|
507
|
-
|
|
673
|
+
try {
|
|
674
|
+
const [command, ...rest] = args;
|
|
675
|
+
|
|
676
|
+
if (!command || command === "--help" || command === "-h") {
|
|
677
|
+
logger.log(usage());
|
|
678
|
+
return 0;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (command === "init") {
|
|
682
|
+
initCommand(rootDir, logger);
|
|
683
|
+
return 0;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (command === "sync") {
|
|
687
|
+
syncCommand(rootDir, logger, rest);
|
|
688
|
+
return 0;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (command === "analyze") {
|
|
692
|
+
return analyzeCommand(rootDir, logger, rest);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (command === "targets") {
|
|
696
|
+
if (rest.length === 1 && rest[0] === "list") {
|
|
697
|
+
targetsListCommand(logger);
|
|
698
|
+
return 0;
|
|
699
|
+
}
|
|
700
|
+
throw new Error('Unknown targets command. Use "rules-doctor targets list".');
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
throw new Error(`Unknown command: ${command}\n\n${usage()}`);
|
|
704
|
+
} catch (error) {
|
|
705
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
706
|
+
logger.error(`Error: ${message}`);
|
|
707
|
+
return 1;
|
|
708
|
+
}
|
|
508
709
|
}
|
|
509
710
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
} catch (error) {
|
|
513
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
514
|
-
console.error(`Error: ${message}`);
|
|
515
|
-
process.exitCode = 1;
|
|
711
|
+
if (require.main === module) {
|
|
712
|
+
process.exitCode = runCli(process.argv.slice(2));
|
|
516
713
|
}
|
|
517
714
|
|
|
715
|
+
module.exports = {
|
|
716
|
+
ADAPTERS,
|
|
717
|
+
createDefaultRules,
|
|
718
|
+
normalizeRules,
|
|
719
|
+
parseRulesText,
|
|
720
|
+
runCli,
|
|
721
|
+
stringifyRules,
|
|
722
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jazpiper/rules-doctor",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Node.js CLI to
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Node.js CLI to sync shared coding rules across multiple agent/CLI targets",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
7
7
|
"rules-doctor": "dist/index.js"
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"LICENSE"
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
|
-
"build": "node scripts/tsup.js src/index.js --clean",
|
|
17
|
+
"build": "node scripts/tsup.js src/index.js --clean --dts",
|
|
18
18
|
"test": "npm run build && node --test"
|
|
19
19
|
},
|
|
20
20
|
"keywords": [
|
|
@@ -22,7 +22,11 @@
|
|
|
22
22
|
"agents",
|
|
23
23
|
"rules",
|
|
24
24
|
"claude",
|
|
25
|
-
"codex"
|
|
25
|
+
"codex",
|
|
26
|
+
"cursor",
|
|
27
|
+
"gemini",
|
|
28
|
+
"opencode",
|
|
29
|
+
"antigravity"
|
|
26
30
|
],
|
|
27
31
|
"engines": {
|
|
28
32
|
"node": ">=18"
|