@jazpiper/rules-doctor 0.1.0 → 0.3.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/CHANGELOG.md +18 -0
- package/README.md +141 -39
- package/dist/adapters/claude.js +31 -0
- package/dist/adapters/codex.js +14 -0
- package/dist/adapters/common.js +54 -0
- package/dist/adapters/copilot.js +15 -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 +1146 -242
- package/package.json +13 -5
package/dist/index.js
CHANGED
|
@@ -1,32 +1,52 @@
|
|
|
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
|
|
7
|
-
const
|
|
8
|
-
const MARKER_BEGIN = "<!-- RULES_DOCTOR:BEGIN -->";
|
|
9
|
-
const MARKER_END = "<!-- RULES_DOCTOR:END -->";
|
|
6
|
+
const RULES_RELATIVE_PATH = ".agentrules/rules.yaml";
|
|
7
|
+
const IMPORT_REPORT_RELATIVE_PATH = ".agentrules/import-report.md";
|
|
8
|
+
const PRESET_NAMES = ["all", "core", "copilot"];
|
|
10
9
|
|
|
11
10
|
function usage() {
|
|
11
|
+
const targets = ADAPTERS.map((adapter) => adapter.id).join("|");
|
|
12
|
+
const presets = PRESET_NAMES.join("|");
|
|
12
13
|
return [
|
|
13
14
|
"rules-doctor",
|
|
14
15
|
"",
|
|
15
16
|
"Usage:",
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
` rules-doctor init [--import] [--preset ${presets}]`,
|
|
18
|
+
` rules-doctor preset apply <${presets}> [--diff] [--write]`,
|
|
19
|
+
` rules-doctor sync [--target all|${targets}|<comma-separated-targets>] [--diff] [--write] [--backup]`,
|
|
20
|
+
` rules-doctor check [--target all|${targets}|<comma-separated-targets>] [--diff]`,
|
|
21
|
+
" rules-doctor analyze [--strict]",
|
|
22
|
+
" rules-doctor doctor [--strict]",
|
|
23
|
+
" rules-doctor targets list",
|
|
24
|
+
"",
|
|
25
|
+
"Notes:",
|
|
26
|
+
" - sync defaults to dry-run. Add --write to apply changes.",
|
|
19
27
|
].join("\n");
|
|
20
28
|
}
|
|
21
29
|
|
|
30
|
+
function createLogger(options) {
|
|
31
|
+
return {
|
|
32
|
+
log:
|
|
33
|
+
options && typeof options.stdout === "function"
|
|
34
|
+
? options.stdout
|
|
35
|
+
: (message) => console.log(message),
|
|
36
|
+
error:
|
|
37
|
+
options && typeof options.stderr === "function"
|
|
38
|
+
? options.stderr
|
|
39
|
+
: (message) => console.error(message),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
22
43
|
function readJsonFile(filePath) {
|
|
23
44
|
if (!existsSync(filePath)) {
|
|
24
45
|
return null;
|
|
25
46
|
}
|
|
26
47
|
|
|
27
48
|
try {
|
|
28
|
-
|
|
29
|
-
return JSON.parse(raw);
|
|
49
|
+
return JSON.parse(readFileSync(filePath, "utf8"));
|
|
30
50
|
} catch {
|
|
31
51
|
return null;
|
|
32
52
|
}
|
|
@@ -49,6 +69,12 @@ function stripQuotes(value) {
|
|
|
49
69
|
|
|
50
70
|
function parseScalar(value) {
|
|
51
71
|
const cleaned = stripQuotes(value);
|
|
72
|
+
if (/^(true|false)$/i.test(cleaned)) {
|
|
73
|
+
return cleaned.toLowerCase() === "true";
|
|
74
|
+
}
|
|
75
|
+
if (cleaned === "null" || cleaned === "~") {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
52
78
|
if (/^-?\d+$/.test(cleaned)) {
|
|
53
79
|
return Number(cleaned);
|
|
54
80
|
}
|
|
@@ -70,6 +96,7 @@ function parseRulesText(text) {
|
|
|
70
96
|
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
71
97
|
let section = null;
|
|
72
98
|
let nested = null;
|
|
99
|
+
let currentTarget = null;
|
|
73
100
|
|
|
74
101
|
for (const rawLine of lines) {
|
|
75
102
|
if (!rawLine.trim() || rawLine.trim().startsWith("#")) {
|
|
@@ -81,6 +108,8 @@ function parseRulesText(text) {
|
|
|
81
108
|
|
|
82
109
|
if (indent === 0) {
|
|
83
110
|
nested = null;
|
|
111
|
+
currentTarget = null;
|
|
112
|
+
|
|
84
113
|
const top = line.match(/^([a-zA-Z0-9_-]+):(.*)$/);
|
|
85
114
|
if (!top) {
|
|
86
115
|
continue;
|
|
@@ -88,11 +117,12 @@ function parseRulesText(text) {
|
|
|
88
117
|
|
|
89
118
|
const key = top[1];
|
|
90
119
|
const value = top[2].trim();
|
|
120
|
+
|
|
91
121
|
if (!value) {
|
|
92
122
|
section = key;
|
|
93
123
|
if (key === "workflow" || key === "done") {
|
|
94
124
|
data[key] = [];
|
|
95
|
-
} else if (key === "commands" || key === "approvals") {
|
|
125
|
+
} else if (key === "commands" || key === "approvals" || key === "targets") {
|
|
96
126
|
data[key] = {};
|
|
97
127
|
}
|
|
98
128
|
} else {
|
|
@@ -132,6 +162,35 @@ function parseRulesText(text) {
|
|
|
132
162
|
if (nested === "notes" && line.startsWith("- ")) {
|
|
133
163
|
data.approvals.notes.push(parseScalar(line.slice(2)));
|
|
134
164
|
}
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (section === "targets") {
|
|
169
|
+
if (indent === 2) {
|
|
170
|
+
const target = line.match(/^([a-zA-Z0-9_-]+):(.*)$/);
|
|
171
|
+
if (!target) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
currentTarget = target[1];
|
|
176
|
+
const maybeValue = target[2].trim();
|
|
177
|
+
if (!maybeValue) {
|
|
178
|
+
data.targets[currentTarget] = {};
|
|
179
|
+
} else {
|
|
180
|
+
data.targets[currentTarget] = { path: parseScalar(maybeValue), enabled: true };
|
|
181
|
+
}
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (indent >= 4 && currentTarget) {
|
|
186
|
+
const pair = line.match(/^([a-zA-Z0-9_-]+):(.*)$/);
|
|
187
|
+
if (pair) {
|
|
188
|
+
if (!data.targets[currentTarget] || typeof data.targets[currentTarget] !== "object") {
|
|
189
|
+
data.targets[currentTarget] = {};
|
|
190
|
+
}
|
|
191
|
+
data.targets[currentTarget][pair[1]] = parseScalar(pair[2].trim());
|
|
192
|
+
}
|
|
193
|
+
}
|
|
135
194
|
}
|
|
136
195
|
}
|
|
137
196
|
|
|
@@ -139,31 +198,15 @@ function parseRulesText(text) {
|
|
|
139
198
|
}
|
|
140
199
|
|
|
141
200
|
function quoteYaml(value) {
|
|
201
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
202
|
+
return String(value);
|
|
203
|
+
}
|
|
204
|
+
if (value === null || typeof value === "undefined") {
|
|
205
|
+
return "null";
|
|
206
|
+
}
|
|
142
207
|
return JSON.stringify(String(value));
|
|
143
208
|
}
|
|
144
209
|
|
|
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
210
|
function inferCommandFromScripts(scripts, scriptName) {
|
|
168
211
|
if (scripts && typeof scripts[scriptName] === "string") {
|
|
169
212
|
return `npm run ${scriptName}`;
|
|
@@ -171,12 +214,17 @@ function inferCommandFromScripts(scripts, scriptName) {
|
|
|
171
214
|
return `echo "TODO: define ${scriptName} command"`;
|
|
172
215
|
}
|
|
173
216
|
|
|
174
|
-
function createDefaultRules() {
|
|
175
|
-
const
|
|
176
|
-
const
|
|
217
|
+
function createDefaultRules(scripts) {
|
|
218
|
+
const targets = {};
|
|
219
|
+
for (const adapter of ADAPTERS) {
|
|
220
|
+
targets[adapter.id] = {
|
|
221
|
+
enabled: true,
|
|
222
|
+
path: adapter.defaultPath,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
177
225
|
|
|
178
226
|
return {
|
|
179
|
-
version:
|
|
227
|
+
version: 2,
|
|
180
228
|
mission: "Ship safe changes quickly while keeping agent instructions consistent.",
|
|
181
229
|
workflow: [
|
|
182
230
|
"Read relevant files before editing.",
|
|
@@ -196,322 +244,1178 @@ function createDefaultRules() {
|
|
|
196
244
|
mode: "ask-before-destructive",
|
|
197
245
|
notes: ["Ask before destructive actions or privileged operations."],
|
|
198
246
|
},
|
|
247
|
+
targets,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function getPresetTargetIds(presetName) {
|
|
252
|
+
if (presetName === "all") {
|
|
253
|
+
return ADAPTERS.map((adapter) => adapter.id);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (presetName === "core") {
|
|
257
|
+
return ["claude", "codex", "opencode", "cursor", "gemini"];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (presetName === "copilot") {
|
|
261
|
+
return ["copilot"];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
throw new Error(`Unknown preset "${presetName}". Use one of: ${PRESET_NAMES.join(", ")}`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function applyTargetPreset(rules, presetName) {
|
|
268
|
+
const enabled = new Set(getPresetTargetIds(presetName));
|
|
269
|
+
const targets = rules.targets && typeof rules.targets === "object" ? rules.targets : {};
|
|
270
|
+
|
|
271
|
+
for (const adapter of ADAPTERS) {
|
|
272
|
+
const current = normalizeTargetConfig(targets[adapter.id], adapter.defaultPath);
|
|
273
|
+
targets[adapter.id] = {
|
|
274
|
+
enabled: enabled.has(adapter.id),
|
|
275
|
+
path: current.path,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
...rules,
|
|
281
|
+
targets,
|
|
199
282
|
};
|
|
200
283
|
}
|
|
201
284
|
|
|
202
|
-
function
|
|
285
|
+
function normalizeTargetConfig(source, fallbackPath) {
|
|
286
|
+
if (typeof source === "string" && source.trim()) {
|
|
287
|
+
return { enabled: true, path: source.trim() };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (!source || typeof source !== "object") {
|
|
291
|
+
return { enabled: true, path: fallbackPath };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
enabled: typeof source.enabled === "boolean" ? source.enabled : true,
|
|
296
|
+
path: typeof source.path === "string" && source.path.trim() ? source.path.trim() : fallbackPath,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function normalizeRules(input, defaults) {
|
|
203
301
|
const source = input && typeof input === "object" ? input : {};
|
|
204
302
|
const commands = source.commands && typeof source.commands === "object" ? source.commands : {};
|
|
205
303
|
const approvals =
|
|
206
304
|
source.approvals && typeof source.approvals === "object" ? source.approvals : {};
|
|
305
|
+
const sourceTargets = source.targets && typeof source.targets === "object" ? source.targets : {};
|
|
207
306
|
|
|
208
307
|
const workflow = Array.isArray(source.workflow)
|
|
209
308
|
? source.workflow.filter((item) => typeof item === "string")
|
|
210
|
-
:
|
|
309
|
+
: defaults.workflow;
|
|
211
310
|
|
|
212
311
|
const done = Array.isArray(source.done)
|
|
213
312
|
? source.done.filter((item) => typeof item === "string")
|
|
214
|
-
:
|
|
313
|
+
: defaults.done;
|
|
215
314
|
|
|
216
315
|
const notes = Array.isArray(approvals.notes)
|
|
217
316
|
? approvals.notes.filter((item) => typeof item === "string")
|
|
218
|
-
:
|
|
317
|
+
: defaults.approvals.notes;
|
|
318
|
+
|
|
319
|
+
const targets = {};
|
|
320
|
+
for (const adapter of ADAPTERS) {
|
|
321
|
+
const fallback = defaults.targets[adapter.id]
|
|
322
|
+
? defaults.targets[adapter.id].path
|
|
323
|
+
: adapter.defaultPath;
|
|
324
|
+
const config = normalizeTargetConfig(sourceTargets[adapter.id], fallback);
|
|
325
|
+
targets[adapter.id] = {
|
|
326
|
+
enabled: typeof config.enabled === "boolean" ? config.enabled : true,
|
|
327
|
+
path: config.path,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
for (const customId of Object.keys(sourceTargets)) {
|
|
332
|
+
if (targets[customId]) {
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
targets[customId] = normalizeTargetConfig(sourceTargets[customId], `${customId.toUpperCase()}.md`);
|
|
336
|
+
}
|
|
219
337
|
|
|
220
338
|
return {
|
|
221
|
-
version: typeof source.version === "number" ? source.version :
|
|
339
|
+
version: typeof source.version === "number" ? source.version : defaults.version,
|
|
222
340
|
mission:
|
|
223
|
-
typeof source.mission === "string" && source.mission.trim()
|
|
224
|
-
|
|
225
|
-
: "Define your project mission.",
|
|
226
|
-
workflow,
|
|
341
|
+
typeof source.mission === "string" && source.mission.trim() ? source.mission : defaults.mission,
|
|
342
|
+
workflow: workflow.length > 0 ? workflow : defaults.workflow,
|
|
227
343
|
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"',
|
|
344
|
+
lint: typeof commands.lint === "string" ? commands.lint : defaults.commands.lint,
|
|
345
|
+
test: typeof commands.test === "string" ? commands.test : defaults.commands.test,
|
|
346
|
+
build: typeof commands.build === "string" ? commands.build : defaults.commands.build,
|
|
240
347
|
},
|
|
241
|
-
done,
|
|
348
|
+
done: done.length > 0 ? done : defaults.done,
|
|
242
349
|
approvals: {
|
|
243
|
-
mode:
|
|
244
|
-
typeof approvals.mode === "string" ? approvals.mode : "ask-before-destructive",
|
|
350
|
+
mode: typeof approvals.mode === "string" ? approvals.mode : defaults.approvals.mode,
|
|
245
351
|
notes,
|
|
246
352
|
},
|
|
353
|
+
targets,
|
|
247
354
|
};
|
|
248
355
|
}
|
|
249
356
|
|
|
250
|
-
function
|
|
251
|
-
|
|
252
|
-
|
|
357
|
+
function stringifyRules(rules) {
|
|
358
|
+
const knownTargetIds = ADAPTERS.map((adapter) => adapter.id);
|
|
359
|
+
const allTargetIds = [
|
|
360
|
+
...knownTargetIds.filter((id) => Object.prototype.hasOwnProperty.call(rules.targets || {}, id)),
|
|
361
|
+
...Object.keys(rules.targets || {})
|
|
362
|
+
.filter((id) => !knownTargetIds.includes(id))
|
|
363
|
+
.sort(),
|
|
364
|
+
];
|
|
365
|
+
|
|
366
|
+
const lines = [
|
|
367
|
+
`version: ${quoteYaml(Number.isFinite(rules.version) ? rules.version : 2)}`,
|
|
368
|
+
`mission: ${quoteYaml(rules.mission)}`,
|
|
369
|
+
"workflow:",
|
|
370
|
+
...rules.workflow.map((step) => ` - ${quoteYaml(step)}`),
|
|
371
|
+
"commands:",
|
|
372
|
+
...Object.keys(rules.commands).map((name) => ` ${name}: ${quoteYaml(rules.commands[name])}`),
|
|
373
|
+
"done:",
|
|
374
|
+
...rules.done.map((item) => ` - ${quoteYaml(item)}`),
|
|
375
|
+
"approvals:",
|
|
376
|
+
` mode: ${quoteYaml(rules.approvals.mode)}`,
|
|
377
|
+
" notes:",
|
|
378
|
+
...rules.approvals.notes.map((note) => ` - ${quoteYaml(note)}`),
|
|
379
|
+
"targets:",
|
|
380
|
+
];
|
|
381
|
+
|
|
382
|
+
for (const id of allTargetIds) {
|
|
383
|
+
const config = normalizeTargetConfig(rules.targets[id], `${id.toUpperCase()}.md`);
|
|
384
|
+
lines.push(` ${id}:`);
|
|
385
|
+
lines.push(` enabled: ${quoteYaml(config.enabled)}`);
|
|
386
|
+
lines.push(` path: ${quoteYaml(config.path)}`);
|
|
253
387
|
}
|
|
254
388
|
|
|
255
|
-
|
|
256
|
-
return
|
|
389
|
+
lines.push("");
|
|
390
|
+
return lines.join("\n");
|
|
257
391
|
}
|
|
258
392
|
|
|
259
|
-
function
|
|
260
|
-
|
|
261
|
-
return "- (none)";
|
|
262
|
-
}
|
|
263
|
-
return items.map((item) => `- ${item}`).join("\n");
|
|
393
|
+
function hasVerifyCommand(text) {
|
|
394
|
+
return /\b(npm run|pnpm|yarn|bun)\s+(lint|test|build)\b/i.test(text);
|
|
264
395
|
}
|
|
265
396
|
|
|
266
|
-
function
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
397
|
+
function hasNoApprovalLanguage(text) {
|
|
398
|
+
return /never ask (for )?approval|no approvals|without approval|do not ask for approval/i.test(
|
|
399
|
+
text,
|
|
400
|
+
);
|
|
401
|
+
}
|
|
270
402
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
...Object.keys(commands).filter((name) => !preferredOrder.includes(name)),
|
|
275
|
-
];
|
|
403
|
+
function hasAskApprovalLanguage(text) {
|
|
404
|
+
return /ask for approval|request approval|require approval|needs approval/i.test(text);
|
|
405
|
+
}
|
|
276
406
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
407
|
+
function hasRequireTestsLanguage(text) {
|
|
408
|
+
return /must run tests|always run tests|run tests before done/i.test(text);
|
|
409
|
+
}
|
|
280
410
|
|
|
281
|
-
|
|
411
|
+
function hasSkipTestsLanguage(text) {
|
|
412
|
+
return /skip tests|tests optional|do not run tests/i.test(text);
|
|
282
413
|
}
|
|
283
414
|
|
|
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");
|
|
415
|
+
function resolveInRoot(rootDir, filePath) {
|
|
416
|
+
if (isAbsolute(filePath)) {
|
|
417
|
+
return filePath;
|
|
418
|
+
}
|
|
419
|
+
return resolve(rootDir, filePath);
|
|
305
420
|
}
|
|
306
421
|
|
|
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");
|
|
422
|
+
function findProjectRoot(startDir) {
|
|
423
|
+
let current = resolve(startDir);
|
|
424
|
+
while (true) {
|
|
425
|
+
const gitPath = resolve(current, ".git");
|
|
426
|
+
const rulesPath = resolve(current, RULES_RELATIVE_PATH);
|
|
427
|
+
if (existsSync(rulesPath) || existsSync(gitPath)) {
|
|
428
|
+
return current;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const parent = dirname(current);
|
|
432
|
+
if (parent === current) {
|
|
433
|
+
return resolve(startDir);
|
|
434
|
+
}
|
|
435
|
+
current = parent;
|
|
436
|
+
}
|
|
338
437
|
}
|
|
339
438
|
|
|
340
|
-
function
|
|
341
|
-
|
|
342
|
-
|
|
439
|
+
function ensureParentDirectory(filePath) {
|
|
440
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function upsertManagedSection(existing, content, beginMarker, endMarker) {
|
|
444
|
+
const start = existing.indexOf(beginMarker);
|
|
445
|
+
const end = start >= 0 ? existing.indexOf(endMarker, start) : -1;
|
|
343
446
|
|
|
344
447
|
if (start >= 0 && end > start) {
|
|
345
|
-
const before = existing.slice(0, start +
|
|
448
|
+
const before = existing.slice(0, start + beginMarker.length);
|
|
346
449
|
const after = existing.slice(end);
|
|
347
450
|
return `${before}\n${content.trim()}\n${after}`.replace(/\n{3,}/g, "\n\n");
|
|
348
451
|
}
|
|
349
452
|
|
|
350
453
|
const base = existing.trimEnd();
|
|
351
454
|
const prefix = base ? `${base}\n\n` : "";
|
|
352
|
-
return `${prefix}${
|
|
455
|
+
return `${prefix}${beginMarker}\n${content.trim()}\n${endMarker}\n`;
|
|
353
456
|
}
|
|
354
457
|
|
|
355
|
-
function
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
return;
|
|
458
|
+
function loadPackageScripts(rootDir) {
|
|
459
|
+
const pkg = readJsonFile(resolve(rootDir, "package.json"));
|
|
460
|
+
if (!pkg || typeof pkg !== "object" || !pkg.scripts || typeof pkg.scripts !== "object") {
|
|
461
|
+
return {};
|
|
359
462
|
}
|
|
463
|
+
return pkg.scripts;
|
|
464
|
+
}
|
|
360
465
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
466
|
+
function loadRules(rootDir, options) {
|
|
467
|
+
const rulesFile = resolve(rootDir, RULES_RELATIVE_PATH);
|
|
468
|
+
const defaults = createDefaultRules(loadPackageScripts(rootDir));
|
|
469
|
+
|
|
470
|
+
if (!existsSync(rulesFile)) {
|
|
471
|
+
if (options && options.allowMissing) {
|
|
472
|
+
return {
|
|
473
|
+
rules: defaults,
|
|
474
|
+
rulesFile,
|
|
475
|
+
rulesExists: false,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
throw new Error(`Missing ${rulesFile}. Run "rules-doctor init" to create it first.`);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const parsed = parseRulesText(readFileSync(rulesFile, "utf8"));
|
|
482
|
+
return {
|
|
483
|
+
rules: normalizeRules(parsed, defaults),
|
|
484
|
+
rulesFile,
|
|
485
|
+
rulesExists: true,
|
|
486
|
+
};
|
|
364
487
|
}
|
|
365
488
|
|
|
366
|
-
function
|
|
367
|
-
|
|
489
|
+
function getTargetsFromSpec(spec) {
|
|
490
|
+
if (spec === "all") {
|
|
491
|
+
return ADAPTERS.map((adapter) => adapter.id);
|
|
492
|
+
}
|
|
368
493
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
494
|
+
const unique = [];
|
|
495
|
+
for (const raw of spec.split(",")) {
|
|
496
|
+
const id = raw.trim();
|
|
497
|
+
if (!id) {
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
if (!ADAPTERS_BY_ID[id]) {
|
|
501
|
+
throw new Error(
|
|
502
|
+
`Unknown target "${id}". Use one of: all, ${ADAPTERS.map((adapter) => adapter.id).join(", ")}`,
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
if (!unique.includes(id)) {
|
|
506
|
+
unique.push(id);
|
|
507
|
+
}
|
|
372
508
|
}
|
|
373
509
|
|
|
374
|
-
if (
|
|
375
|
-
|
|
376
|
-
const updated = upsertManagedSection(existing, renderCodexManagedSection(rules));
|
|
377
|
-
writeFileSync(AGENTS_FILE, updated, "utf8");
|
|
378
|
-
console.log(`Updated ${AGENTS_FILE}`);
|
|
510
|
+
if (unique.length === 0) {
|
|
511
|
+
throw new Error("No targets selected.");
|
|
379
512
|
}
|
|
513
|
+
|
|
514
|
+
return unique;
|
|
380
515
|
}
|
|
381
516
|
|
|
382
|
-
function
|
|
383
|
-
|
|
517
|
+
function parseInitArgs(args) {
|
|
518
|
+
const options = {
|
|
519
|
+
importExisting: false,
|
|
520
|
+
preset: "all",
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
for (let index = 0; index < (args || []).length; index += 1) {
|
|
524
|
+
const arg = args[index];
|
|
525
|
+
if (arg === "--import") {
|
|
526
|
+
options.importExisting = true;
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (arg === "--preset") {
|
|
531
|
+
const value = args[index + 1];
|
|
532
|
+
if (!value) {
|
|
533
|
+
throw new Error("Missing value for --preset.");
|
|
534
|
+
}
|
|
535
|
+
if (!PRESET_NAMES.includes(value)) {
|
|
536
|
+
throw new Error(`Unknown preset "${value}". Use one of: ${PRESET_NAMES.join(", ")}`);
|
|
537
|
+
}
|
|
538
|
+
options.preset = value;
|
|
539
|
+
index += 1;
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
throw new Error(`Unknown option for init: ${arg}`);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return options;
|
|
384
547
|
}
|
|
385
548
|
|
|
386
|
-
function
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
549
|
+
function parsePresetArgs(args) {
|
|
550
|
+
if (!args || args.length === 0) {
|
|
551
|
+
throw new Error(`Missing preset subcommand. Use "rules-doctor preset apply <${PRESET_NAMES.join("|")}>".`);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const [subcommand, presetName, ...rest] = args;
|
|
555
|
+
if (subcommand !== "apply") {
|
|
556
|
+
throw new Error(`Unknown preset subcommand: ${subcommand}. Use "rules-doctor preset apply <preset>".`);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (!presetName) {
|
|
560
|
+
throw new Error(`Missing preset name. Use one of: ${PRESET_NAMES.join(", ")}`);
|
|
561
|
+
}
|
|
562
|
+
if (!PRESET_NAMES.includes(presetName)) {
|
|
563
|
+
throw new Error(`Unknown preset "${presetName}". Use one of: ${PRESET_NAMES.join(", ")}`);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const options = {
|
|
567
|
+
presetName,
|
|
568
|
+
diff: false,
|
|
569
|
+
write: false,
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
for (const arg of rest) {
|
|
573
|
+
if (arg === "--diff") {
|
|
574
|
+
options.diff = true;
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
if (arg === "--write") {
|
|
578
|
+
options.write = true;
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
throw new Error(`Unknown option for preset apply: ${arg}`);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return options;
|
|
390
585
|
}
|
|
391
586
|
|
|
392
|
-
function
|
|
393
|
-
|
|
587
|
+
function parseTargetedArgs(commandName, args, extra) {
|
|
588
|
+
const options = {
|
|
589
|
+
targetSpec: "all",
|
|
590
|
+
diff: false,
|
|
591
|
+
write: false,
|
|
592
|
+
backup: false,
|
|
593
|
+
};
|
|
594
|
+
const allowed = extra || {};
|
|
595
|
+
|
|
596
|
+
for (let index = 0; index < (args || []).length; index += 1) {
|
|
597
|
+
const arg = args[index];
|
|
598
|
+
if (arg === "--target") {
|
|
599
|
+
const value = args[index + 1];
|
|
600
|
+
if (!value) {
|
|
601
|
+
throw new Error(`Missing value for --target (${commandName})`);
|
|
602
|
+
}
|
|
603
|
+
options.targetSpec = value;
|
|
604
|
+
index += 1;
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
if (arg === "--diff") {
|
|
608
|
+
options.diff = true;
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
if (arg === "--write" && allowed.write) {
|
|
612
|
+
options.write = true;
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
if (arg === "--backup" && allowed.backup) {
|
|
616
|
+
options.backup = true;
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
throw new Error(`Unknown option for ${commandName}: ${arg}`);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (options.backup && !options.write) {
|
|
623
|
+
throw new Error("--backup requires --write.");
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return {
|
|
627
|
+
targetIds: getTargetsFromSpec(options.targetSpec),
|
|
628
|
+
diff: options.diff,
|
|
629
|
+
write: options.write,
|
|
630
|
+
backup: options.backup,
|
|
631
|
+
};
|
|
394
632
|
}
|
|
395
633
|
|
|
396
|
-
function
|
|
397
|
-
|
|
634
|
+
function parseAnalyzeArgs(args) {
|
|
635
|
+
const options = {
|
|
636
|
+
strict: false,
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
for (const arg of args || []) {
|
|
640
|
+
if (arg === "--strict") {
|
|
641
|
+
options.strict = true;
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
throw new Error(`Unknown option for analyze: ${arg}`);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
return options;
|
|
398
648
|
}
|
|
399
649
|
|
|
400
|
-
function
|
|
401
|
-
|
|
650
|
+
function parseDoctorArgs(args) {
|
|
651
|
+
const options = {
|
|
652
|
+
strict: false,
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
for (const arg of args || []) {
|
|
656
|
+
if (arg === "--strict") {
|
|
657
|
+
options.strict = true;
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
throw new Error(`Unknown option for doctor: ${arg}`);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
return options;
|
|
402
664
|
}
|
|
403
665
|
|
|
404
|
-
function
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
666
|
+
function getTargetConfig(rules, adapter) {
|
|
667
|
+
const source = rules.targets && typeof rules.targets === "object" ? rules.targets[adapter.id] : null;
|
|
668
|
+
return normalizeTargetConfig(source, adapter.defaultPath);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function normalizeHeading(value) {
|
|
672
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
673
|
+
}
|
|
410
674
|
|
|
411
|
-
|
|
412
|
-
|
|
675
|
+
function parseMarkdownSections(text) {
|
|
676
|
+
const sections = {};
|
|
677
|
+
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
678
|
+
let currentHeading = null;
|
|
679
|
+
let currentLines = [];
|
|
680
|
+
|
|
681
|
+
function flush() {
|
|
682
|
+
if (!currentHeading) {
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
const normalized = normalizeHeading(currentHeading);
|
|
686
|
+
if (!sections[normalized]) {
|
|
687
|
+
sections[normalized] = currentLines.join("\n").trim();
|
|
688
|
+
}
|
|
413
689
|
}
|
|
414
|
-
|
|
415
|
-
|
|
690
|
+
|
|
691
|
+
for (const line of lines) {
|
|
692
|
+
const heading = line.match(/^#{1,6}\s+(.+?)\s*$/);
|
|
693
|
+
if (heading) {
|
|
694
|
+
flush();
|
|
695
|
+
currentHeading = heading[1];
|
|
696
|
+
currentLines = [];
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
if (currentHeading) {
|
|
701
|
+
currentLines.push(line);
|
|
702
|
+
}
|
|
416
703
|
}
|
|
417
704
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
705
|
+
flush();
|
|
706
|
+
return sections;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function pickFirstNonEmptyLine(text) {
|
|
710
|
+
for (const line of text.split("\n")) {
|
|
711
|
+
const trimmed = line.trim();
|
|
712
|
+
if (trimmed) {
|
|
713
|
+
return trimmed;
|
|
423
714
|
}
|
|
424
715
|
}
|
|
716
|
+
return "";
|
|
717
|
+
}
|
|
425
718
|
|
|
426
|
-
|
|
427
|
-
|
|
719
|
+
function parseListItems(text) {
|
|
720
|
+
const items = [];
|
|
721
|
+
for (const line of text.split("\n")) {
|
|
722
|
+
const bullet = line.match(/^\s*[-*]\s+(.+?)\s*$/);
|
|
723
|
+
if (bullet) {
|
|
724
|
+
items.push(bullet[1].trim());
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
const numbered = line.match(/^\s*\d+\.\s+(.+?)\s*$/);
|
|
728
|
+
if (numbered) {
|
|
729
|
+
items.push(numbered[1].trim());
|
|
730
|
+
}
|
|
428
731
|
}
|
|
732
|
+
return items;
|
|
733
|
+
}
|
|
429
734
|
|
|
430
|
-
|
|
431
|
-
|
|
735
|
+
function unquoteValue(value) {
|
|
736
|
+
let current = value.trim();
|
|
737
|
+
if (current.startsWith("`") && current.endsWith("`")) {
|
|
738
|
+
current = current.slice(1, -1);
|
|
432
739
|
}
|
|
740
|
+
current = stripQuotes(current);
|
|
741
|
+
return current.trim();
|
|
742
|
+
}
|
|
433
743
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
)
|
|
438
|
-
|
|
744
|
+
function importCommandsFromText(text, commands) {
|
|
745
|
+
const merged = { ...commands };
|
|
746
|
+
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
747
|
+
const commandNames = Object.keys(merged);
|
|
748
|
+
|
|
749
|
+
for (const raw of lines) {
|
|
750
|
+
const line = raw.trim();
|
|
751
|
+
for (const name of commandNames) {
|
|
752
|
+
const direct = line.match(new RegExp(`^(?:[-*]\\s*)?${name}\\s*:\\s*(.+)$`, "i"));
|
|
753
|
+
if (direct && direct[1]) {
|
|
754
|
+
merged[name] = unquoteValue(direct[1]);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
439
757
|
}
|
|
440
758
|
|
|
441
|
-
|
|
442
|
-
(
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
759
|
+
for (const name of commandNames) {
|
|
760
|
+
if (!merged[name] || merged[name].includes("TODO")) {
|
|
761
|
+
const found = text.match(new RegExp(`\\b(?:npm run|pnpm|yarn|bun)\\s+${name}\\b`, "i"));
|
|
762
|
+
if (found) {
|
|
763
|
+
merged[name] = found[0];
|
|
764
|
+
}
|
|
765
|
+
}
|
|
446
766
|
}
|
|
447
767
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
console.log(`- AGENTS.md: ${agentsExists ? "found" : "missing"}`);
|
|
451
|
-
console.log("- Findings:");
|
|
768
|
+
return merged;
|
|
769
|
+
}
|
|
452
770
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
771
|
+
function getSectionText(sections, aliases) {
|
|
772
|
+
for (const alias of aliases) {
|
|
773
|
+
const key = normalizeHeading(alias);
|
|
774
|
+
if (sections[key] && sections[key].trim()) {
|
|
775
|
+
return sections[key].trim();
|
|
776
|
+
}
|
|
456
777
|
}
|
|
778
|
+
return "";
|
|
779
|
+
}
|
|
457
780
|
|
|
458
|
-
|
|
459
|
-
|
|
781
|
+
function collectImportSources(rootDir) {
|
|
782
|
+
const candidates = [
|
|
783
|
+
{ id: "claude", path: "CLAUDE.md" },
|
|
784
|
+
{ id: "claude-local", path: ".claude/CLAUDE.md" },
|
|
785
|
+
{ id: "codex", path: "AGENTS.md" },
|
|
786
|
+
{ id: "copilot", path: ".github/copilot-instructions.md" },
|
|
787
|
+
{ id: "gemini", path: "GEMINI.md" },
|
|
788
|
+
{ id: "cursor", path: ".cursor/rules/rules-doctor.mdc" },
|
|
789
|
+
];
|
|
790
|
+
|
|
791
|
+
const uniquePaths = new Set();
|
|
792
|
+
const sources = [];
|
|
793
|
+
|
|
794
|
+
for (const item of candidates) {
|
|
795
|
+
if (uniquePaths.has(item.path)) {
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
uniquePaths.add(item.path);
|
|
799
|
+
|
|
800
|
+
const absolutePath = resolveInRoot(rootDir, item.path);
|
|
801
|
+
if (!existsSync(absolutePath)) {
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
sources.push({
|
|
805
|
+
id: item.id,
|
|
806
|
+
path: item.path,
|
|
807
|
+
absolutePath,
|
|
808
|
+
text: readFileSync(absolutePath, "utf8"),
|
|
809
|
+
});
|
|
460
810
|
}
|
|
811
|
+
|
|
812
|
+
return sources;
|
|
461
813
|
}
|
|
462
814
|
|
|
463
|
-
function
|
|
464
|
-
|
|
815
|
+
function importRulesFromDocs(rootDir, defaults) {
|
|
816
|
+
const imported = JSON.parse(JSON.stringify(defaults));
|
|
817
|
+
const sources = collectImportSources(rootDir);
|
|
818
|
+
const notes = [];
|
|
819
|
+
|
|
820
|
+
if (sources.length === 0) {
|
|
821
|
+
return {
|
|
822
|
+
rules: imported,
|
|
823
|
+
report: "No existing docs were found. Created default rules.",
|
|
824
|
+
sources,
|
|
825
|
+
};
|
|
826
|
+
}
|
|
465
827
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
828
|
+
notes.push(`Found ${sources.length} source file(s):`);
|
|
829
|
+
for (const source of sources) {
|
|
830
|
+
notes.push(`- ${source.path}`);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
for (const source of sources) {
|
|
834
|
+
const sections = parseMarkdownSections(source.text);
|
|
835
|
+
|
|
836
|
+
const missionSection = getSectionText(sections, ["mission"]);
|
|
837
|
+
if (missionSection) {
|
|
838
|
+
const mission = pickFirstNonEmptyLine(missionSection);
|
|
839
|
+
if (mission) {
|
|
840
|
+
imported.mission = mission;
|
|
472
841
|
}
|
|
473
|
-
|
|
474
|
-
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
const workflowSection = getSectionText(sections, ["workflow", "operational loop"]);
|
|
845
|
+
const workflow = parseListItems(workflowSection);
|
|
846
|
+
if (workflow.length > 0) {
|
|
847
|
+
imported.workflow = workflow;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
const doneSection = getSectionText(sections, ["done", "done criteria"]);
|
|
851
|
+
const done = parseListItems(doneSection);
|
|
852
|
+
if (done.length > 0) {
|
|
853
|
+
imported.done = done;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
const approvalsSection = getSectionText(sections, ["approvals", "approval"]);
|
|
857
|
+
if (approvalsSection) {
|
|
858
|
+
const mode = approvalsSection.match(/(?:mode|policy)\s*:\s*`?([a-z0-9_-]+)`?/i);
|
|
859
|
+
if (mode && mode[1]) {
|
|
860
|
+
imported.approvals.mode = mode[1].trim();
|
|
861
|
+
}
|
|
862
|
+
const approvalNotes = parseListItems(approvalsSection);
|
|
863
|
+
if (approvalNotes.length > 0) {
|
|
864
|
+
imported.approvals.notes = approvalNotes;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
imported.commands = importCommandsFromText(source.text, imported.commands);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
for (const adapter of ADAPTERS) {
|
|
872
|
+
const config = getTargetConfig(imported, adapter);
|
|
873
|
+
const absolutePath = resolveInRoot(rootDir, config.path);
|
|
874
|
+
if (existsSync(absolutePath)) {
|
|
875
|
+
imported.targets[adapter.id].enabled = true;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
notes.push("Imported mission/workflow/commands/done/approvals where detected.");
|
|
880
|
+
return {
|
|
881
|
+
rules: imported,
|
|
882
|
+
report: notes.join("\n"),
|
|
883
|
+
sources,
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function initCommand(rootDir, logger, args) {
|
|
888
|
+
const options = parseInitArgs(args);
|
|
889
|
+
const rulesFile = resolve(rootDir, RULES_RELATIVE_PATH);
|
|
890
|
+
if (existsSync(rulesFile)) {
|
|
891
|
+
logger.log(`rules.yaml already exists: ${rulesFile}`);
|
|
892
|
+
return 0;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
const defaults = createDefaultRules(loadPackageScripts(rootDir));
|
|
896
|
+
let rules = defaults;
|
|
897
|
+
let importReport = "";
|
|
898
|
+
|
|
899
|
+
if (options.importExisting) {
|
|
900
|
+
const imported = importRulesFromDocs(rootDir, defaults);
|
|
901
|
+
rules = imported.rules;
|
|
902
|
+
importReport = imported.report;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
rules = applyTargetPreset(rules, options.preset);
|
|
906
|
+
|
|
907
|
+
ensureParentDirectory(rulesFile);
|
|
908
|
+
writeFileSync(rulesFile, stringifyRules(rules), "utf8");
|
|
909
|
+
logger.log(`Created ${rulesFile}`);
|
|
910
|
+
logger.log(`Applied target preset: ${options.preset}`);
|
|
911
|
+
|
|
912
|
+
if (options.importExisting) {
|
|
913
|
+
const reportPath = resolve(rootDir, IMPORT_REPORT_RELATIVE_PATH);
|
|
914
|
+
const reportContent =
|
|
915
|
+
options.preset === "all"
|
|
916
|
+
? `${importReport}\n`
|
|
917
|
+
: `${importReport}\n\nApplied target preset: ${options.preset}\n`;
|
|
918
|
+
writeFileSync(reportPath, reportContent, "utf8");
|
|
919
|
+
logger.log(`Import report: ${reportPath}`);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
return 0;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function presetApplyCommand(rootDir, logger, args) {
|
|
926
|
+
const options = parsePresetArgs(args);
|
|
927
|
+
const { rules, rulesFile } = loadRules(rootDir);
|
|
928
|
+
const nextRules = applyTargetPreset(
|
|
929
|
+
{
|
|
930
|
+
...rules,
|
|
931
|
+
targets: { ...(rules.targets || {}) },
|
|
932
|
+
},
|
|
933
|
+
options.presetName,
|
|
934
|
+
);
|
|
935
|
+
|
|
936
|
+
const currentText = stringifyRules(rules);
|
|
937
|
+
const nextText = stringifyRules(nextRules);
|
|
938
|
+
const changed = currentText !== nextText;
|
|
939
|
+
|
|
940
|
+
logger.log("rules-doctor preset apply");
|
|
941
|
+
logger.log(`- root: ${rootDir}`);
|
|
942
|
+
logger.log(`- preset: ${options.presetName}`);
|
|
943
|
+
logger.log(`- mode: ${options.write ? "write" : "dry-run"}`);
|
|
944
|
+
|
|
945
|
+
if (options.diff && changed) {
|
|
946
|
+
logger.log("\n# diff: .agentrules/rules.yaml");
|
|
947
|
+
logger.log(renderSimpleDiff(currentText, nextText));
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
if (!changed) {
|
|
951
|
+
logger.log("No changes required: preset already applied.");
|
|
952
|
+
return 0;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
if (!options.write) {
|
|
956
|
+
logger.log("Dry-run complete: rules.yaml would be updated. Re-run with --write.");
|
|
957
|
+
return 0;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
ensureParentDirectory(rulesFile);
|
|
961
|
+
writeFileSync(rulesFile, nextText, "utf8");
|
|
962
|
+
logger.log(`Updated ${rulesFile}`);
|
|
963
|
+
return 0;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
function buildTargetPlans(rootDir, rules, targetIds) {
|
|
967
|
+
const plans = [];
|
|
968
|
+
|
|
969
|
+
for (const targetId of targetIds) {
|
|
970
|
+
const adapter = ADAPTERS_BY_ID[targetId];
|
|
971
|
+
const target = getTargetConfig(rules, adapter);
|
|
972
|
+
const targetPath = resolveInRoot(rootDir, target.path);
|
|
973
|
+
const fileExists = existsSync(targetPath);
|
|
974
|
+
const currentText = fileExists ? readFileSync(targetPath, "utf8") : "";
|
|
975
|
+
|
|
976
|
+
if (!target.enabled) {
|
|
977
|
+
plans.push({
|
|
978
|
+
targetId,
|
|
979
|
+
adapter,
|
|
980
|
+
enabled: false,
|
|
981
|
+
targetPath,
|
|
982
|
+
targetPathDisplay: target.path,
|
|
983
|
+
exists: fileExists,
|
|
984
|
+
currentText,
|
|
985
|
+
desiredText: currentText,
|
|
986
|
+
changed: false,
|
|
987
|
+
});
|
|
475
988
|
continue;
|
|
476
989
|
}
|
|
477
990
|
|
|
478
|
-
|
|
991
|
+
const rendered = adapter.render(rules).trim();
|
|
992
|
+
const desiredText =
|
|
993
|
+
adapter.management === "marker"
|
|
994
|
+
? upsertManagedSection(currentText, rendered, adapter.markerBegin, adapter.markerEnd)
|
|
995
|
+
: `${rendered}\n`;
|
|
996
|
+
|
|
997
|
+
plans.push({
|
|
998
|
+
targetId,
|
|
999
|
+
adapter,
|
|
1000
|
+
enabled: true,
|
|
1001
|
+
targetPath,
|
|
1002
|
+
targetPathDisplay: target.path,
|
|
1003
|
+
exists: fileExists,
|
|
1004
|
+
currentText,
|
|
1005
|
+
desiredText,
|
|
1006
|
+
changed: desiredText !== currentText,
|
|
1007
|
+
});
|
|
479
1008
|
}
|
|
480
1009
|
|
|
481
|
-
return
|
|
1010
|
+
return plans;
|
|
482
1011
|
}
|
|
483
1012
|
|
|
484
|
-
function
|
|
485
|
-
|
|
1013
|
+
function renderSimpleDiff(currentText, desiredText) {
|
|
1014
|
+
if (currentText === desiredText) {
|
|
1015
|
+
return "";
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
const oldLines = currentText.replace(/\r\n/g, "\n").split("\n");
|
|
1019
|
+
const newLines = desiredText.replace(/\r\n/g, "\n").split("\n");
|
|
1020
|
+
const output = ["--- current", "+++ desired"];
|
|
1021
|
+
const maxLines = Math.max(oldLines.length, newLines.length);
|
|
1022
|
+
const hardLimit = 120;
|
|
1023
|
+
let emitted = 0;
|
|
1024
|
+
|
|
1025
|
+
for (let index = 0; index < maxLines; index += 1) {
|
|
1026
|
+
const oldLine = oldLines[index];
|
|
1027
|
+
const newLine = newLines[index];
|
|
1028
|
+
|
|
1029
|
+
if (oldLine === newLine) {
|
|
1030
|
+
continue;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
if (typeof oldLine !== "undefined") {
|
|
1034
|
+
output.push(`-${oldLine}`);
|
|
1035
|
+
emitted += 1;
|
|
1036
|
+
}
|
|
1037
|
+
if (typeof newLine !== "undefined") {
|
|
1038
|
+
output.push(`+${newLine}`);
|
|
1039
|
+
emitted += 1;
|
|
1040
|
+
}
|
|
486
1041
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
1042
|
+
if (emitted >= hardLimit) {
|
|
1043
|
+
output.push("... diff truncated ...");
|
|
1044
|
+
break;
|
|
1045
|
+
}
|
|
490
1046
|
}
|
|
491
1047
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
1048
|
+
return output.join("\n");
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function formatPlanSummary(plans) {
|
|
1052
|
+
const changed = plans.filter((plan) => plan.changed).length;
|
|
1053
|
+
const changedFiles = new Set(plans.filter((plan) => plan.changed).map((plan) => plan.targetPath)).size;
|
|
1054
|
+
const enabled = plans.filter((plan) => plan.enabled).length;
|
|
1055
|
+
const disabled = plans.length - enabled;
|
|
1056
|
+
return { changed, changedFiles, enabled, disabled };
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function getUniqueWritePlans(plans) {
|
|
1060
|
+
const byPath = new Map();
|
|
1061
|
+
const duplicates = [];
|
|
1062
|
+
|
|
1063
|
+
for (const plan of plans) {
|
|
1064
|
+
if (!plan.enabled || !plan.changed) {
|
|
1065
|
+
continue;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
const existing = byPath.get(plan.targetPath);
|
|
1069
|
+
if (!existing) {
|
|
1070
|
+
byPath.set(plan.targetPath, plan);
|
|
1071
|
+
continue;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
if (existing.desiredText !== plan.desiredText) {
|
|
1075
|
+
throw new Error(
|
|
1076
|
+
`Conflicting outputs for ${plan.targetPathDisplay}: ${existing.targetId} and ${plan.targetId} produce different content.`,
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
duplicates.push({
|
|
1081
|
+
targetPathDisplay: plan.targetPathDisplay,
|
|
1082
|
+
winner: existing.targetId,
|
|
1083
|
+
duplicate: plan.targetId,
|
|
1084
|
+
});
|
|
495
1085
|
}
|
|
496
1086
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
1087
|
+
return {
|
|
1088
|
+
uniquePlans: Array.from(byPath.values()),
|
|
1089
|
+
duplicates,
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
function syncCommand(rootDir, logger, args) {
|
|
1094
|
+
const options = parseTargetedArgs("sync", args, { write: true, backup: true });
|
|
1095
|
+
const { rules } = loadRules(rootDir);
|
|
1096
|
+
const plans = buildTargetPlans(rootDir, rules, options.targetIds);
|
|
1097
|
+
const summary = formatPlanSummary(plans);
|
|
1098
|
+
|
|
1099
|
+
logger.log("rules-doctor sync");
|
|
1100
|
+
logger.log(`- root: ${rootDir}`);
|
|
1101
|
+
logger.log(`- selected targets: ${options.targetIds.join(", ")}`);
|
|
1102
|
+
logger.log(`- mode: ${options.write ? "write" : "dry-run"}`);
|
|
1103
|
+
|
|
1104
|
+
for (const plan of plans) {
|
|
1105
|
+
if (!plan.enabled) {
|
|
1106
|
+
logger.log(`- ${plan.targetId}: disabled (${plan.targetPathDisplay})`);
|
|
1107
|
+
continue;
|
|
1108
|
+
}
|
|
1109
|
+
if (!plan.changed) {
|
|
1110
|
+
logger.log(`- ${plan.targetId}: up-to-date (${plan.targetPathDisplay})`);
|
|
1111
|
+
continue;
|
|
1112
|
+
}
|
|
1113
|
+
logger.log(`- ${plan.targetId}: would update (${plan.targetPathDisplay})`);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
if (options.diff) {
|
|
1117
|
+
for (const plan of plans) {
|
|
1118
|
+
if (!plan.enabled || !plan.changed) {
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
logger.log(`\n# diff: ${plan.targetId} (${plan.targetPathDisplay})`);
|
|
1122
|
+
logger.log(renderSimpleDiff(plan.currentText, plan.desiredText));
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
if (!options.write) {
|
|
1127
|
+
if (summary.changed === 0) {
|
|
1128
|
+
logger.log("Dry-run complete: no changes.");
|
|
1129
|
+
} else {
|
|
1130
|
+
logger.log(
|
|
1131
|
+
`Dry-run complete: ${summary.changedFiles} file(s) would change (${summary.changed} target mappings). Re-run with --write.`,
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
return 0;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
const { uniquePlans, duplicates } = getUniqueWritePlans(plans);
|
|
1138
|
+
for (const duplicate of duplicates) {
|
|
1139
|
+
logger.log(
|
|
1140
|
+
` note: ${duplicate.duplicate} shares output with ${duplicate.winner} at ${duplicate.targetPathDisplay}`,
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
1145
|
+
for (const plan of uniquePlans) {
|
|
1146
|
+
ensureParentDirectory(plan.targetPath);
|
|
1147
|
+
if (options.backup && plan.exists) {
|
|
1148
|
+
const backupPath = `${plan.targetPath}.rules-doctor.bak.${timestamp}`;
|
|
1149
|
+
writeFileSync(backupPath, plan.currentText, "utf8");
|
|
1150
|
+
logger.log(` backup: ${backupPath}`);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
writeFileSync(plan.targetPath, plan.desiredText, "utf8");
|
|
1154
|
+
logger.log(` updated: ${plan.targetPath}`);
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
logger.log(
|
|
1158
|
+
`Write complete: ${uniquePlans.length} file(s) updated (${summary.changed} target mappings changed).`,
|
|
1159
|
+
);
|
|
1160
|
+
return 0;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
function checkCommand(rootDir, logger, args) {
|
|
1164
|
+
const options = parseTargetedArgs("check", args, { write: false, backup: false });
|
|
1165
|
+
const { rules } = loadRules(rootDir);
|
|
1166
|
+
const plans = buildTargetPlans(rootDir, rules, options.targetIds);
|
|
1167
|
+
const summary = formatPlanSummary(plans);
|
|
1168
|
+
|
|
1169
|
+
logger.log("rules-doctor check");
|
|
1170
|
+
logger.log(`- root: ${rootDir}`);
|
|
1171
|
+
logger.log(`- selected targets: ${options.targetIds.join(", ")}`);
|
|
1172
|
+
|
|
1173
|
+
for (const plan of plans) {
|
|
1174
|
+
if (!plan.enabled) {
|
|
1175
|
+
logger.log(`- ${plan.targetId}: disabled (${plan.targetPathDisplay})`);
|
|
1176
|
+
continue;
|
|
1177
|
+
}
|
|
1178
|
+
logger.log(
|
|
1179
|
+
`- ${plan.targetId}: ${plan.changed ? "drift detected" : "in sync"} (${plan.targetPathDisplay})`,
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
if (options.diff) {
|
|
1184
|
+
for (const plan of plans) {
|
|
1185
|
+
if (!plan.enabled || !plan.changed) {
|
|
1186
|
+
continue;
|
|
1187
|
+
}
|
|
1188
|
+
logger.log(`\n# diff: ${plan.targetId} (${plan.targetPathDisplay})`);
|
|
1189
|
+
logger.log(renderSimpleDiff(plan.currentText, plan.desiredText));
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
if (summary.changed === 0) {
|
|
1194
|
+
logger.log("Check complete: all selected targets are in sync.");
|
|
1195
|
+
return 0;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
logger.log(`Check failed: ${summary.changed} target file(s) need sync.`);
|
|
1199
|
+
return 1;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
function analyzeCommand(rootDir, logger, args) {
|
|
1203
|
+
const options = parseAnalyzeArgs(args);
|
|
1204
|
+
const { rules, rulesExists, rulesFile } = loadRules(rootDir, { allowMissing: true });
|
|
1205
|
+
const issues = [];
|
|
1206
|
+
const targetSnapshots = [];
|
|
1207
|
+
|
|
1208
|
+
logger.log("rules-doctor analyze");
|
|
1209
|
+
logger.log(`- rules.yaml: ${rulesExists ? "found" : "missing (using defaults)"}`);
|
|
1210
|
+
logger.log(`- rules path: ${rulesFile}`);
|
|
1211
|
+
|
|
1212
|
+
for (const adapter of ADAPTERS) {
|
|
1213
|
+
const target = getTargetConfig(rules, adapter);
|
|
1214
|
+
const absolutePath = resolveInRoot(rootDir, target.path);
|
|
1215
|
+
const fileExists = existsSync(absolutePath);
|
|
1216
|
+
const content = fileExists ? readFileSync(absolutePath, "utf8") : "";
|
|
1217
|
+
|
|
1218
|
+
logger.log(
|
|
1219
|
+
`- target ${adapter.id}: ${target.enabled ? "enabled" : "disabled"}, ${
|
|
1220
|
+
fileExists ? "found" : "missing"
|
|
1221
|
+
} (${target.path})`,
|
|
1222
|
+
);
|
|
1223
|
+
|
|
1224
|
+
if (!target.enabled) {
|
|
1225
|
+
continue;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
if (!fileExists) {
|
|
1229
|
+
issues.push(`${adapter.id}: expected file is missing (${target.path}).`);
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
if (adapter.management === "marker") {
|
|
1234
|
+
const hasBegin = content.includes(adapter.markerBegin);
|
|
1235
|
+
const hasEnd = content.includes(adapter.markerEnd);
|
|
1236
|
+
if (!hasBegin || !hasEnd) {
|
|
1237
|
+
issues.push(`${adapter.id}: marker block is missing.`);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
if (!hasVerifyCommand(content)) {
|
|
1242
|
+
issues.push(`${adapter.id}: verify commands (lint/test/build) not detected.`);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
targetSnapshots.push({
|
|
1246
|
+
id: adapter.id,
|
|
1247
|
+
content,
|
|
1248
|
+
asksApproval: hasAskApprovalLanguage(content),
|
|
1249
|
+
noApproval: hasNoApprovalLanguage(content),
|
|
1250
|
+
requiresTests: hasRequireTestsLanguage(content),
|
|
1251
|
+
skipsTests: hasSkipTestsLanguage(content),
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
const askApprovalTargets = targetSnapshots.filter((item) => item.asksApproval).map((item) => item.id);
|
|
1256
|
+
const noApprovalTargets = targetSnapshots.filter((item) => item.noApproval).map((item) => item.id);
|
|
1257
|
+
if (askApprovalTargets.length > 0 && noApprovalTargets.length > 0) {
|
|
1258
|
+
issues.push(
|
|
1259
|
+
`Potential contradiction: approval guidance differs (${askApprovalTargets.join(
|
|
1260
|
+
", ",
|
|
1261
|
+
)} vs ${noApprovalTargets.join(", ")}).`,
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
const requireTestsTargets = targetSnapshots
|
|
1266
|
+
.filter((item) => item.requiresTests)
|
|
1267
|
+
.map((item) => item.id);
|
|
1268
|
+
const skipTestsTargets = targetSnapshots.filter((item) => item.skipsTests).map((item) => item.id);
|
|
1269
|
+
if (requireTestsTargets.length > 0 && skipTestsTargets.length > 0) {
|
|
1270
|
+
issues.push(
|
|
1271
|
+
`Potential contradiction: test guidance differs (${requireTestsTargets.join(
|
|
1272
|
+
", ",
|
|
1273
|
+
)} vs ${skipTestsTargets.join(", ")}).`,
|
|
1274
|
+
);
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
logger.log("- Findings:");
|
|
1278
|
+
if (issues.length === 0) {
|
|
1279
|
+
logger.log("- No obvious issues found.");
|
|
1280
|
+
return 0;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
for (const issue of issues) {
|
|
1284
|
+
logger.log(`- ${issue}`);
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
if (options.strict) {
|
|
1288
|
+
throw new Error(`Analyze failed in strict mode with ${issues.length} issue(s).`);
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
return 0;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
function doctorCommand(rootDir, logger, args) {
|
|
1295
|
+
const options = parseDoctorArgs(args);
|
|
1296
|
+
const { rules, rulesExists, rulesFile } = loadRules(rootDir, { allowMissing: true });
|
|
1297
|
+
const issues = [];
|
|
1298
|
+
|
|
1299
|
+
logger.log("rules-doctor doctor");
|
|
1300
|
+
logger.log(`- root: ${rootDir}`);
|
|
1301
|
+
logger.log(`- rules file: ${rulesFile}`);
|
|
1302
|
+
logger.log(`- rules exists: ${rulesExists ? "yes" : "no (defaults assumed)"}`);
|
|
1303
|
+
|
|
1304
|
+
const pathToTargets = {};
|
|
1305
|
+
for (const adapter of ADAPTERS) {
|
|
1306
|
+
const target = getTargetConfig(rules, adapter);
|
|
1307
|
+
const absolutePath = resolveInRoot(rootDir, target.path);
|
|
1308
|
+
const exists = existsSync(absolutePath);
|
|
1309
|
+
const enabledText = target.enabled ? "enabled" : "disabled";
|
|
1310
|
+
logger.log(`- ${adapter.id}: ${enabledText}, path=${target.path}, file=${exists ? "found" : "missing"}`);
|
|
1311
|
+
|
|
1312
|
+
if (!target.enabled) {
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
const key = absolutePath;
|
|
1317
|
+
if (!pathToTargets[key]) {
|
|
1318
|
+
pathToTargets[key] = [];
|
|
1319
|
+
}
|
|
1320
|
+
pathToTargets[key].push(adapter.id);
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
for (const path of Object.keys(pathToTargets)) {
|
|
1324
|
+
const ids = pathToTargets[path];
|
|
1325
|
+
if (ids.length > 1) {
|
|
1326
|
+
issues.push(`Multiple enabled targets map to the same file: ${ids.join(", ")} -> ${path}`);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
logger.log("- Findings:");
|
|
1331
|
+
if (issues.length === 0) {
|
|
1332
|
+
logger.log("- No structural issues found.");
|
|
1333
|
+
return 0;
|
|
1334
|
+
}
|
|
1335
|
+
for (const issue of issues) {
|
|
1336
|
+
logger.log(`- ${issue}`);
|
|
500
1337
|
}
|
|
501
1338
|
|
|
502
|
-
if (
|
|
503
|
-
|
|
504
|
-
return;
|
|
1339
|
+
if (options.strict) {
|
|
1340
|
+
return 1;
|
|
505
1341
|
}
|
|
1342
|
+
return 0;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
function targetsListCommand(logger) {
|
|
1346
|
+
logger.log("Supported targets:");
|
|
1347
|
+
for (const adapter of ADAPTERS) {
|
|
1348
|
+
const mode = adapter.management === "marker" ? "marker-managed" : "full-managed";
|
|
1349
|
+
logger.log(`- ${adapter.id}: ${adapter.name}`);
|
|
1350
|
+
logger.log(` default path: ${adapter.defaultPath}`);
|
|
1351
|
+
logger.log(` mode: ${mode}`);
|
|
1352
|
+
logger.log(` ${adapter.description}`);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
function runCli(argv, options) {
|
|
1357
|
+
const args = Array.isArray(argv) ? argv : [];
|
|
1358
|
+
const logger = createLogger(options || {});
|
|
1359
|
+
const cwd = resolve(options && options.cwd ? options.cwd : process.cwd());
|
|
1360
|
+
const rootDir = findProjectRoot(cwd);
|
|
1361
|
+
|
|
1362
|
+
try {
|
|
1363
|
+
const [command, ...rest] = args;
|
|
1364
|
+
|
|
1365
|
+
if (!command || command === "--help" || command === "-h") {
|
|
1366
|
+
logger.log(usage());
|
|
1367
|
+
return 0;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
if (command === "init") {
|
|
1371
|
+
return initCommand(rootDir, logger, rest);
|
|
1372
|
+
}
|
|
506
1373
|
|
|
507
|
-
|
|
1374
|
+
if (command === "preset") {
|
|
1375
|
+
return presetApplyCommand(rootDir, logger, rest);
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
if (command === "sync") {
|
|
1379
|
+
return syncCommand(rootDir, logger, rest);
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
if (command === "check") {
|
|
1383
|
+
return checkCommand(rootDir, logger, rest);
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
if (command === "analyze") {
|
|
1387
|
+
return analyzeCommand(rootDir, logger, rest);
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
if (command === "doctor") {
|
|
1391
|
+
return doctorCommand(rootDir, logger, rest);
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
if (command === "targets") {
|
|
1395
|
+
if (rest.length === 1 && rest[0] === "list") {
|
|
1396
|
+
targetsListCommand(logger);
|
|
1397
|
+
return 0;
|
|
1398
|
+
}
|
|
1399
|
+
throw new Error('Unknown targets command. Use "rules-doctor targets list".');
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
throw new Error(`Unknown command: ${command}\n\n${usage()}`);
|
|
1403
|
+
} catch (error) {
|
|
1404
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1405
|
+
logger.error(`Error: ${message}`);
|
|
1406
|
+
return 1;
|
|
1407
|
+
}
|
|
508
1408
|
}
|
|
509
1409
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
} catch (error) {
|
|
513
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
514
|
-
console.error(`Error: ${message}`);
|
|
515
|
-
process.exitCode = 1;
|
|
1410
|
+
if (require.main === module) {
|
|
1411
|
+
process.exitCode = runCli(process.argv.slice(2));
|
|
516
1412
|
}
|
|
517
1413
|
|
|
1414
|
+
module.exports = {
|
|
1415
|
+
ADAPTERS,
|
|
1416
|
+
createDefaultRules,
|
|
1417
|
+
normalizeRules,
|
|
1418
|
+
parseRulesText,
|
|
1419
|
+
runCli,
|
|
1420
|
+
stringifyRules,
|
|
1421
|
+
};
|