dev-diagnosis-cli 0.1.8 → 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/CHANGELOG.md +89 -0
- package/README.md +120 -14
- package/bin/dev-diagnosis.js +234 -188
- package/lib/checks.mjs +280 -0
- package/lib/context.mjs +49 -9
- package/lib/mcp.mjs +271 -0
- package/lib/report.mjs +12 -0
- package/lib/setup.mjs +249 -0
- package/lib/skill.mjs +81 -0
- package/package.json +2 -2
- /package/examples/runs/{run-1 → run-01}/results.xml +0 -0
- /package/examples/runs/{run-2 → run-02}/results.xml +0 -0
package/lib/setup.mjs
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-command install: `dev-diagnosis setup`.
|
|
3
|
+
*
|
|
4
|
+
* Detects which agents are installed on this machine and writes the MCP
|
|
5
|
+
* config that wires dev-diagnosis into each of them — Claude Code,
|
|
6
|
+
* Codex CLI, Cursor and VS Code. The command always shows a preview of
|
|
7
|
+
* exactly what would be written before it writes anything, and it
|
|
8
|
+
* never silently overwrites:
|
|
9
|
+
*
|
|
10
|
+
* - a config that already mentions dev-diagnosis is reported as
|
|
11
|
+
* already wired and left alone;
|
|
12
|
+
* - a config that exists without it is merged (other entries kept) and
|
|
13
|
+
* the change is reported;
|
|
14
|
+
* - a missing config is created with just the dev-diagnosis entry.
|
|
15
|
+
*
|
|
16
|
+
* A config file that cannot be parsed is left alone and reported as an
|
|
17
|
+
* error for that agent — the tool never rewrites a file it cannot read.
|
|
18
|
+
* TOML (Codex) is appended to, never rewritten: only the
|
|
19
|
+
* `[mcp_servers.dev-diagnosis]` section is added when it is absent.
|
|
20
|
+
*
|
|
21
|
+
* Detection is by config file or by the agent's CLI being on PATH, so
|
|
22
|
+
* an agent with no config yet still gets one. The MCP entry itself is
|
|
23
|
+
* the same shape the README documents: command `dev-diagnosis`, args
|
|
24
|
+
* `["--mcp"]`. If `dev-diagnosis` is not on PATH, setup warns that the
|
|
25
|
+
* agents will not be able to run it until it is installed globally.
|
|
26
|
+
*/
|
|
27
|
+
import fs from 'node:fs';
|
|
28
|
+
import os from 'node:os';
|
|
29
|
+
import path from 'node:path';
|
|
30
|
+
import { lookupCommand } from './pathcheck.mjs';
|
|
31
|
+
|
|
32
|
+
export const MCP_COMMAND = 'dev-diagnosis';
|
|
33
|
+
export const MCP_ARGS = ['--mcp'];
|
|
34
|
+
export const MCP_ENTRY = { command: MCP_COMMAND, args: MCP_ARGS };
|
|
35
|
+
|
|
36
|
+
export function mcpEntryJson() {
|
|
37
|
+
return { mcpServers: { 'dev-diagnosis': MCP_ENTRY } };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function vscodeSettingsPath(home) {
|
|
41
|
+
if (process.platform === 'darwin') {
|
|
42
|
+
return path.join(home, 'Library', 'Application Support', 'Code', 'User', 'settings.json');
|
|
43
|
+
}
|
|
44
|
+
if (process.platform === 'win32') {
|
|
45
|
+
const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
|
|
46
|
+
return path.join(appData, 'Code', 'User', 'settings.json');
|
|
47
|
+
}
|
|
48
|
+
return path.join(home, '.config', 'Code', 'User', 'settings.json');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The agents setup knows about. Each entry describes where its MCP
|
|
53
|
+
* config lives and how it is stored. `configPath` is a function of
|
|
54
|
+
* the home directory so tests can point it at a temp dir.
|
|
55
|
+
*/
|
|
56
|
+
export function agentDefs(home) {
|
|
57
|
+
return [
|
|
58
|
+
{
|
|
59
|
+
id: 'claude-code',
|
|
60
|
+
name: 'Claude Code',
|
|
61
|
+
binaries: ['claude'],
|
|
62
|
+
format: 'json',
|
|
63
|
+
configPath: path.join(home, '.claude.json'),
|
|
64
|
+
mergeKey: ['mcpServers'],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: 'codex',
|
|
68
|
+
name: 'Codex CLI',
|
|
69
|
+
binaries: ['codex'],
|
|
70
|
+
format: 'toml',
|
|
71
|
+
configPath: path.join(home, '.codex', 'config.toml'),
|
|
72
|
+
mergeKey: null,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: 'cursor',
|
|
76
|
+
name: 'Cursor',
|
|
77
|
+
binaries: ['cursor'],
|
|
78
|
+
format: 'json',
|
|
79
|
+
configPath: path.join(home, '.cursor', 'mcp.json'),
|
|
80
|
+
mergeKey: ['mcpServers'],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: 'vscode',
|
|
84
|
+
name: 'VS Code',
|
|
85
|
+
binaries: ['code'],
|
|
86
|
+
format: 'json',
|
|
87
|
+
configPath: vscodeSettingsPath(home),
|
|
88
|
+
mergeKey: ['mcp', 'servers'],
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function binaryOnPath(name, pathEnv) {
|
|
94
|
+
return lookupCommand(name, pathEnv || process.env.PATH || '').resolved;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function readJsonFile(file) {
|
|
98
|
+
try {
|
|
99
|
+
return { data: JSON.parse(fs.readFileSync(file, 'utf8')), error: null };
|
|
100
|
+
} catch (error) {
|
|
101
|
+
return { data: null, error: error.message };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function nestedGet(obj, keys) {
|
|
106
|
+
let current = obj;
|
|
107
|
+
for (const key of keys) {
|
|
108
|
+
if (current == null || typeof current !== 'object') return undefined;
|
|
109
|
+
current = current[key];
|
|
110
|
+
}
|
|
111
|
+
return current;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function nestedSet(obj, keys, value) {
|
|
115
|
+
let current = obj;
|
|
116
|
+
for (let i = 0; i < keys.length - 1; i += 1) {
|
|
117
|
+
if (current[keys[i]] == null || typeof current[keys[i]] !== 'object') current[keys[i]] = {};
|
|
118
|
+
current = current[keys[i]];
|
|
119
|
+
}
|
|
120
|
+
current[keys[keys.length - 1]] = value;
|
|
121
|
+
return obj;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function tomlSection() {
|
|
125
|
+
return '[mcp_servers.dev-diagnosis]\ncommand = "dev-diagnosis"\nargs = ["--mcp"]\n';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function hasTomlEntry(content) {
|
|
129
|
+
return /^\s*\[mcp_servers\.dev-diagnosis\]/m.test(content);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Detect installed agents. `options` takes { home, pathEnv } so tests
|
|
134
|
+
* can point detection at a temp HOME without touching the real one.
|
|
135
|
+
* An agent counts as installed when its config file exists or its CLI
|
|
136
|
+
* is on PATH. Returns one row per agent: { id, name, format,
|
|
137
|
+
* configPath, detected, via } where via is 'config', 'binary' or
|
|
138
|
+
* 'config+binary'.
|
|
139
|
+
*/
|
|
140
|
+
export function detectAgents(options = {}) {
|
|
141
|
+
const home = options.home || os.homedir();
|
|
142
|
+
const pathEnv = options.pathEnv !== undefined ? options.pathEnv : process.env.PATH || '';
|
|
143
|
+
return agentDefs(home).map((def) => {
|
|
144
|
+
const hasConfig = fs.existsSync(def.configPath);
|
|
145
|
+
const hasBinary = def.binaries.some((name) => binaryOnPath(name, pathEnv));
|
|
146
|
+
const via = hasConfig && hasBinary ? 'config+binary' : hasConfig ? 'config' : hasBinary ? 'binary' : null;
|
|
147
|
+
return { ...def, detected: hasConfig || hasBinary, via };
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Work out what setup would do for each detected agent, without
|
|
153
|
+
* writing anything. Returns { agents, commandOnPath } where each agent
|
|
154
|
+
* row carries:
|
|
155
|
+
* status: 'already-wired' | 'merge' | 'create' | 'unreadable'
|
|
156
|
+
* preview: the exact text that would be written or added (for
|
|
157
|
+
* 'merge' on JSON, the added entry; for 'create', the full file)
|
|
158
|
+
* note: human-readable explanation for unreadable configs.
|
|
159
|
+
*/
|
|
160
|
+
export function planSetup(options = {}) {
|
|
161
|
+
const home = options.home || os.homedir();
|
|
162
|
+
const agents = detectAgents({ home, pathEnv: options.pathEnv }).filter((agent) => agent.detected);
|
|
163
|
+
const commandOnPath = binaryOnPath(MCP_COMMAND, options.pathEnv !== undefined ? options.pathEnv : process.env.PATH || '');
|
|
164
|
+
const planned = agents.map((agent) => {
|
|
165
|
+
const row = { id: agent.id, name: agent.name, format: agent.format, configPath: agent.configPath, mergeKey: agent.mergeKey };
|
|
166
|
+
if (agent.format === 'toml') {
|
|
167
|
+
if (!fs.existsSync(agent.configPath)) {
|
|
168
|
+
return { ...row, status: 'create', preview: tomlSection() };
|
|
169
|
+
}
|
|
170
|
+
const content = fs.readFileSync(agent.configPath, 'utf8');
|
|
171
|
+
if (hasTomlEntry(content)) return { ...row, status: 'already-wired', preview: null };
|
|
172
|
+
return { ...row, status: 'merge', preview: tomlSection() };
|
|
173
|
+
}
|
|
174
|
+
// JSON formats: merge under mergeKey, never touch other entries.
|
|
175
|
+
if (!fs.existsSync(agent.configPath)) {
|
|
176
|
+
const data = nestedSet({}, agent.mergeKey, { 'dev-diagnosis': MCP_ENTRY });
|
|
177
|
+
return { ...row, status: 'create', preview: `${JSON.stringify(data, null, 2)}\n` };
|
|
178
|
+
}
|
|
179
|
+
const { data, error } = readJsonFile(agent.configPath);
|
|
180
|
+
if (error) {
|
|
181
|
+
return { ...row, status: 'unreadable', preview: null, note: `could not parse ${agent.configPath}: ${error} — left alone` };
|
|
182
|
+
}
|
|
183
|
+
const existing = nestedGet(data, agent.mergeKey);
|
|
184
|
+
if (existing && typeof existing === 'object' && existing['dev-diagnosis'] !== undefined) {
|
|
185
|
+
return { ...row, status: 'already-wired', preview: null };
|
|
186
|
+
}
|
|
187
|
+
if (existing !== undefined && (existing === null || typeof existing !== 'object')) {
|
|
188
|
+
return { ...row, status: 'unreadable', preview: null, note: `${agent.mergeKey.join('.')} in ${agent.configPath} is not an object — left alone` };
|
|
189
|
+
}
|
|
190
|
+
return { ...row, status: 'merge', preview: JSON.stringify({ 'dev-diagnosis': MCP_ENTRY }, null, 2) };
|
|
191
|
+
});
|
|
192
|
+
return { agents: planned, commandOnPath };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Carry out a plan from planSetup(). Writes only 'merge' and 'create'
|
|
197
|
+
* rows; 'already-wired' and 'unreadable' rows are reported and left
|
|
198
|
+
* alone. Returns one result per agent: { id, name, configPath,
|
|
199
|
+
* action: 'already-wired' | 'merged' | 'created' | 'unreadable',
|
|
200
|
+
* detail }.
|
|
201
|
+
*/
|
|
202
|
+
export function applySetup(plan) {
|
|
203
|
+
return plan.agents.map((row) => {
|
|
204
|
+
const base = { id: row.id, name: row.name, configPath: row.configPath };
|
|
205
|
+
if (row.status === 'already-wired') return { ...base, action: 'already-wired', detail: 'dev-diagnosis is already in this config' };
|
|
206
|
+
if (row.status === 'unreadable') return { ...base, action: 'unreadable', detail: row.note };
|
|
207
|
+
fs.mkdirSync(path.dirname(row.configPath), { recursive: true });
|
|
208
|
+
if (row.format === 'toml') {
|
|
209
|
+
if (row.status === 'create') {
|
|
210
|
+
fs.writeFileSync(row.configPath, row.preview);
|
|
211
|
+
return { ...base, action: 'created', detail: 'created with the dev-diagnosis MCP server entry' };
|
|
212
|
+
}
|
|
213
|
+
const existing = fs.readFileSync(row.configPath, 'utf8');
|
|
214
|
+
const joined = existing.endsWith('\n') || existing === '' ? `${existing}${row.preview}` : `${existing}\n${row.preview}`;
|
|
215
|
+
fs.writeFileSync(row.configPath, joined);
|
|
216
|
+
return { ...base, action: 'merged', detail: 'appended [mcp_servers.dev-diagnosis] to the existing config' };
|
|
217
|
+
}
|
|
218
|
+
if (row.status === 'create') {
|
|
219
|
+
fs.writeFileSync(row.configPath, row.preview);
|
|
220
|
+
return { ...base, action: 'created', detail: 'created with the dev-diagnosis MCP server entry' };
|
|
221
|
+
}
|
|
222
|
+
const { data } = readJsonFile(row.configPath);
|
|
223
|
+
nestedSet(data, [...row.mergeKey, 'dev-diagnosis'], MCP_ENTRY);
|
|
224
|
+
fs.writeFileSync(row.configPath, `${JSON.stringify(data, null, 2)}\n`);
|
|
225
|
+
return { ...base, action: 'merged', detail: 'added dev-diagnosis under the existing MCP servers, other entries kept' };
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Report which detected agents are wired and which are not. Returns
|
|
231
|
+
* { rows, detected, allWired } where each row is { id, name,
|
|
232
|
+
* configPath, wired }. An agent with an unreadable config counts as
|
|
233
|
+
* not wired.
|
|
234
|
+
*/
|
|
235
|
+
export function checkSetup(options = {}) {
|
|
236
|
+
const plan = planSetup(options);
|
|
237
|
+
const rows = plan.agents.map((row) => ({
|
|
238
|
+
id: row.id,
|
|
239
|
+
name: row.name,
|
|
240
|
+
configPath: row.configPath,
|
|
241
|
+
wired: row.status === 'already-wired',
|
|
242
|
+
status: row.status,
|
|
243
|
+
}));
|
|
244
|
+
return {
|
|
245
|
+
rows,
|
|
246
|
+
detected: rows.length,
|
|
247
|
+
allWired: rows.length > 0 && rows.every((row) => row.wired),
|
|
248
|
+
};
|
|
249
|
+
}
|
package/lib/skill.mjs
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Skills generation: `dev-diagnosis --skill-generate`.
|
|
3
|
+
*
|
|
4
|
+
* Writes a Claude Agent Skills SKILL.md that tells an agent to run
|
|
5
|
+
* dev-diagnosis when it starts work in a repo, read the verdicts, fix
|
|
6
|
+
* what fails, and re-run to confirm. Default scope is --project
|
|
7
|
+
* (./.claude/skills/dev-diagnosis/SKILL.md); --global writes
|
|
8
|
+
* ~/.claude/skills/dev-diagnosis/SKILL.md instead.
|
|
9
|
+
*
|
|
10
|
+
* Never overwrites: if the SKILL.md already exists, generation stops
|
|
11
|
+
* with a plain message and the caller exits 1 — the file may hold the
|
|
12
|
+
* user's own edits.
|
|
13
|
+
*/
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import os from 'node:os';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
|
|
18
|
+
export const SKILL_DIR_NAME = 'dev-diagnosis';
|
|
19
|
+
|
|
20
|
+
export const SKILL_MD = `---
|
|
21
|
+
name: dev-diagnosis
|
|
22
|
+
description: Diagnose a repo before working in it — context noise that wastes tokens, missing commands, build-cache value, flaky tests. Run when starting work in a repository, after a build or test failure, or when an agent session feels slow.
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
# dev-diagnosis
|
|
26
|
+
|
|
27
|
+
Run this before reading a repo, and again after fixing what it finds.
|
|
28
|
+
|
|
29
|
+
## When to use it
|
|
30
|
+
|
|
31
|
+
- Starting work in a repository you have not diagnosed yet.
|
|
32
|
+
- An agent session feels slow, or the context looks full of files it should not be reading.
|
|
33
|
+
- A build is slow, a command is missing, or tests keep failing on retry.
|
|
34
|
+
|
|
35
|
+
## Run it
|
|
36
|
+
|
|
37
|
+
\`\`\`sh
|
|
38
|
+
dev-diagnosis all
|
|
39
|
+
\`\`\`
|
|
40
|
+
|
|
41
|
+
If the command is not installed, use \`npx dev-diagnosis-cli\` instead. Run it in the repo directory; pass a path to diagnose another one (\`dev-diagnosis all ./other-repo\`).
|
|
42
|
+
|
|
43
|
+
## Read the verdicts
|
|
44
|
+
|
|
45
|
+
- **context FAIL** — the repo has noise an AI agent should not read (build output, lockfiles, logs, oversized files). The report lists the files and the token estimate. Fix it with \`dev-diagnosis context --fix\`, which writes the suggestions into \`.aiignore\`, then re-run \`dev-diagnosis all\` to confirm the score is 100/100.
|
|
46
|
+
- **path FAIL** — a command is missing in the current shell. The report says which shell contexts have it and prints a copy-paste fix; apply the fix, then re-run.
|
|
47
|
+
- **flakes FAIL** — the report ranks the flaky tests. Look at the top-ranked test first; a test marked \`new\` broke recently, \`chronic\` has been unstable for a while.
|
|
48
|
+
- **cache FAIL** — the verdict says whether the build cache pays. If it costs more than it saves, the cache setup is the problem, not the build.
|
|
49
|
+
|
|
50
|
+
Skipped checks are not failures: they just need their input
|
|
51
|
+
(\`--command <name>\` for path, \`--flakes <runs-dir>\` for flakes,
|
|
52
|
+
\`-- <build-command>\` for cache).
|
|
53
|
+
|
|
54
|
+
## Rules
|
|
55
|
+
|
|
56
|
+
- Fix what FAILs before starting the real work; a 75/100 repo costs real tokens on every turn.
|
|
57
|
+
- Always re-run \`dev-diagnosis all\` after a fix and confirm it passes.
|
|
58
|
+
- Never edit \`.aiignore\` by hand to hide a failure you have not read — the file is the record of what the agent agreed not to read.
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
export function skillPath(scope = 'project', base = process.cwd()) {
|
|
62
|
+
if (scope === 'global') {
|
|
63
|
+
return path.join(os.homedir(), '.claude', 'skills', SKILL_DIR_NAME, 'SKILL.md');
|
|
64
|
+
}
|
|
65
|
+
return path.join(path.resolve(base), '.claude', 'skills', SKILL_DIR_NAME, 'SKILL.md');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Write the SKILL.md. Returns `{ path, created: true }` on success,
|
|
70
|
+
* or `{ path, created: false, reason }` when the file already exists —
|
|
71
|
+
* the caller prints the reason and exits 1.
|
|
72
|
+
*/
|
|
73
|
+
export function generateSkill(scope = 'project', base = process.cwd()) {
|
|
74
|
+
const target = skillPath(scope, base);
|
|
75
|
+
if (fs.existsSync(target)) {
|
|
76
|
+
return { path: target, created: false, reason: `already exists: ${target}` };
|
|
77
|
+
}
|
|
78
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
79
|
+
fs.writeFileSync(target, SKILL_MD);
|
|
80
|
+
return { path: target, created: true };
|
|
81
|
+
}
|
package/package.json
CHANGED
|
File without changes
|
|
File without changes
|