dotclaudemd 0.1.1
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/LICENSE +21 -0
- package/README.md +108 -0
- package/dist/browse-7V4CRGTH.js +96 -0
- package/dist/browse-7V4CRGTH.js.map +1 -0
- package/dist/chunk-2D66CC54.js +17 -0
- package/dist/chunk-2D66CC54.js.map +1 -0
- package/dist/chunk-3ERFQLAD.js +145 -0
- package/dist/chunk-3ERFQLAD.js.map +1 -0
- package/dist/chunk-3R6PUA3E.js +79 -0
- package/dist/chunk-3R6PUA3E.js.map +1 -0
- package/dist/chunk-3WTPUEHL.js +42 -0
- package/dist/chunk-3WTPUEHL.js.map +1 -0
- package/dist/chunk-YHVOBZLV.js +28 -0
- package/dist/chunk-YHVOBZLV.js.map +1 -0
- package/dist/cli.js +40 -0
- package/dist/cli.js.map +1 -0
- package/dist/doctor-4B7J2EH3.js +318 -0
- package/dist/doctor-4B7J2EH3.js.map +1 -0
- package/dist/init-GLWLFVHN.js +287 -0
- package/dist/init-GLWLFVHN.js.map +1 -0
- package/dist/lint-W7ZIDPL7.js +281 -0
- package/dist/lint-W7ZIDPL7.js.map +1 -0
- package/package.json +50 -0
- package/templates/_global/default.md +52 -0
- package/templates/go/go-api.md +55 -0
- package/templates/javascript/express-mongodb.md +56 -0
- package/templates/javascript/mern-stack.md +58 -0
- package/templates/javascript/nextjs-prisma-tailwind.md +58 -0
- package/templates/javascript/nextjs-typescript.md +57 -0
- package/templates/javascript/node-cli-tool.md +55 -0
- package/templates/javascript/react-vite.md +55 -0
- package/templates/python/django-rest.md +55 -0
- package/templates/python/fastapi-sqlalchemy.md +54 -0
- package/templates/python/flask-basic.md +51 -0
- package/templates/rust/cargo-workspace.md +50 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
createSpinner
|
|
4
|
+
} from "./chunk-2D66CC54.js";
|
|
5
|
+
import {
|
|
6
|
+
defaultFsDeps,
|
|
7
|
+
findClaudeMd,
|
|
8
|
+
findProjectRoot
|
|
9
|
+
} from "./chunk-3R6PUA3E.js";
|
|
10
|
+
import {
|
|
11
|
+
error
|
|
12
|
+
} from "./chunk-YHVOBZLV.js";
|
|
13
|
+
|
|
14
|
+
// src/commands/doctor.ts
|
|
15
|
+
import chalk from "chalk";
|
|
16
|
+
|
|
17
|
+
// src/core/doctor-checks.ts
|
|
18
|
+
import { join } from "path";
|
|
19
|
+
var builtinChecks = [
|
|
20
|
+
{
|
|
21
|
+
name: "scripts-exist",
|
|
22
|
+
description: "Commands in CLAUDE.md exist in package.json scripts",
|
|
23
|
+
async check(claudeMd, projectRoot, deps = defaultFsDeps) {
|
|
24
|
+
const pkgPath = join(projectRoot, "package.json");
|
|
25
|
+
if (!await deps.fileExists(pkgPath)) {
|
|
26
|
+
return {
|
|
27
|
+
check: "scripts-exist",
|
|
28
|
+
status: "pass",
|
|
29
|
+
message: "No package.json found (skipped)"
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
let pkg;
|
|
33
|
+
try {
|
|
34
|
+
pkg = JSON.parse(await deps.readFile(pkgPath));
|
|
35
|
+
} catch {
|
|
36
|
+
return {
|
|
37
|
+
check: "scripts-exist",
|
|
38
|
+
status: "warn",
|
|
39
|
+
message: "Could not parse package.json"
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const scripts = Object.keys(
|
|
43
|
+
pkg.scripts ?? {}
|
|
44
|
+
);
|
|
45
|
+
const scriptRefs = [
|
|
46
|
+
...claudeMd.matchAll(/npm run (\w[\w-]*)/g)
|
|
47
|
+
].map((m) => m[1]);
|
|
48
|
+
const missing = scriptRefs.filter((s) => !scripts.includes(s));
|
|
49
|
+
if (missing.length > 0) {
|
|
50
|
+
return {
|
|
51
|
+
check: "scripts-exist",
|
|
52
|
+
status: "fail",
|
|
53
|
+
message: `Referenced scripts not found in package.json: ${missing.join(", ")}`,
|
|
54
|
+
details: missing.map((s) => `"npm run ${s}" not in package.json`)
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
check: "scripts-exist",
|
|
59
|
+
status: "pass",
|
|
60
|
+
message: `All ${scriptRefs.length} referenced scripts exist`
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: "deps-mentioned",
|
|
66
|
+
description: "Major dependencies are mentioned",
|
|
67
|
+
async check(claudeMd, projectRoot, deps = defaultFsDeps) {
|
|
68
|
+
const pkgPath = join(projectRoot, "package.json");
|
|
69
|
+
if (!await deps.fileExists(pkgPath)) {
|
|
70
|
+
return {
|
|
71
|
+
check: "deps-mentioned",
|
|
72
|
+
status: "pass",
|
|
73
|
+
message: "No package.json found (skipped)"
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
let pkg;
|
|
77
|
+
try {
|
|
78
|
+
pkg = JSON.parse(await deps.readFile(pkgPath));
|
|
79
|
+
} catch {
|
|
80
|
+
return {
|
|
81
|
+
check: "deps-mentioned",
|
|
82
|
+
status: "warn",
|
|
83
|
+
message: "Could not parse package.json"
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const prodDeps = Object.keys(
|
|
87
|
+
pkg.dependencies ?? {}
|
|
88
|
+
);
|
|
89
|
+
const lower = claudeMd.toLowerCase();
|
|
90
|
+
const unmentioned = prodDeps.filter(
|
|
91
|
+
(d) => !lower.includes(d.toLowerCase())
|
|
92
|
+
);
|
|
93
|
+
if (unmentioned.length > prodDeps.length * 0.5 && prodDeps.length > 3) {
|
|
94
|
+
return {
|
|
95
|
+
check: "deps-mentioned",
|
|
96
|
+
status: "warn",
|
|
97
|
+
message: `${unmentioned.length}/${prodDeps.length} major dependencies not mentioned in CLAUDE.md`,
|
|
98
|
+
details: unmentioned.slice(0, 10)
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
check: "deps-mentioned",
|
|
103
|
+
status: "pass",
|
|
104
|
+
message: "Key dependencies are referenced"
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: "file-refs-valid",
|
|
110
|
+
description: "File paths mentioned in CLAUDE.md exist",
|
|
111
|
+
async check(claudeMd, projectRoot, deps = defaultFsDeps) {
|
|
112
|
+
const pathRegex = /(?:^|\s)(?:\.\/)?([a-zA-Z][\w\-./]*\.\w{1,10})(?:\s|$|`|"|\))/gm;
|
|
113
|
+
let match;
|
|
114
|
+
const invalid = [];
|
|
115
|
+
while ((match = pathRegex.exec(claudeMd)) !== null) {
|
|
116
|
+
const filePath = match[1];
|
|
117
|
+
if (filePath.includes("example") || filePath.startsWith("http") || filePath.includes("*"))
|
|
118
|
+
continue;
|
|
119
|
+
const fullPath = join(projectRoot, filePath);
|
|
120
|
+
if (!await deps.fileExists(fullPath)) {
|
|
121
|
+
invalid.push(filePath);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (invalid.length > 0) {
|
|
125
|
+
return {
|
|
126
|
+
check: "file-refs-valid",
|
|
127
|
+
status: "warn",
|
|
128
|
+
message: `${invalid.length} referenced file(s) do not exist`,
|
|
129
|
+
details: invalid
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
check: "file-refs-valid",
|
|
134
|
+
status: "pass",
|
|
135
|
+
message: "All referenced files exist"
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: "node-version-match",
|
|
141
|
+
description: "Stated Node version matches engines/.nvmrc",
|
|
142
|
+
async check(claudeMd, projectRoot, deps = defaultFsDeps) {
|
|
143
|
+
const nodeVersionMatch = claudeMd.match(
|
|
144
|
+
/node\s*(?:version\s*)?:?\s*(\d+(?:\.\d+)*)/i
|
|
145
|
+
);
|
|
146
|
+
if (!nodeVersionMatch) {
|
|
147
|
+
return {
|
|
148
|
+
check: "node-version-match",
|
|
149
|
+
status: "pass",
|
|
150
|
+
message: "No Node version mentioned (skipped)"
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const mentioned = nodeVersionMatch[1];
|
|
154
|
+
const nvmrcPath = join(projectRoot, ".nvmrc");
|
|
155
|
+
if (await deps.fileExists(nvmrcPath)) {
|
|
156
|
+
const nvmrc = (await deps.readFile(nvmrcPath)).trim().replace("v", "");
|
|
157
|
+
if (!nvmrc.startsWith(mentioned.split(".")[0])) {
|
|
158
|
+
return {
|
|
159
|
+
check: "node-version-match",
|
|
160
|
+
status: "fail",
|
|
161
|
+
message: `CLAUDE.md says Node ${mentioned}, but .nvmrc says ${nvmrc}`
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
check: "node-version-match",
|
|
167
|
+
status: "pass",
|
|
168
|
+
message: `Node version ${mentioned} is consistent`
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
name: "test-framework-match",
|
|
174
|
+
description: "Mentioned test framework matches actual devDeps",
|
|
175
|
+
async check(claudeMd, projectRoot, deps = defaultFsDeps) {
|
|
176
|
+
const pkgPath = join(projectRoot, "package.json");
|
|
177
|
+
if (!await deps.fileExists(pkgPath)) {
|
|
178
|
+
return {
|
|
179
|
+
check: "test-framework-match",
|
|
180
|
+
status: "pass",
|
|
181
|
+
message: "No package.json found (skipped)"
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
let pkg;
|
|
185
|
+
try {
|
|
186
|
+
pkg = JSON.parse(await deps.readFile(pkgPath));
|
|
187
|
+
} catch {
|
|
188
|
+
return {
|
|
189
|
+
check: "test-framework-match",
|
|
190
|
+
status: "warn",
|
|
191
|
+
message: "Could not parse package.json"
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
const devDeps = Object.keys(
|
|
195
|
+
pkg.devDependencies ?? {}
|
|
196
|
+
);
|
|
197
|
+
const lower = claudeMd.toLowerCase();
|
|
198
|
+
const frameworks = [
|
|
199
|
+
"vitest",
|
|
200
|
+
"jest",
|
|
201
|
+
"mocha",
|
|
202
|
+
"ava",
|
|
203
|
+
"tap",
|
|
204
|
+
"pytest",
|
|
205
|
+
"unittest"
|
|
206
|
+
];
|
|
207
|
+
for (const fw of frameworks) {
|
|
208
|
+
if (lower.includes(fw) && !devDeps.includes(fw)) {
|
|
209
|
+
if (lower.includes(`not ${fw}`) || lower.includes(`don't use ${fw}`))
|
|
210
|
+
continue;
|
|
211
|
+
return {
|
|
212
|
+
check: "test-framework-match",
|
|
213
|
+
status: "warn",
|
|
214
|
+
message: `CLAUDE.md mentions "${fw}" but it's not in devDependencies`
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
check: "test-framework-match",
|
|
220
|
+
status: "pass",
|
|
221
|
+
message: "Test framework references are consistent"
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
name: "package-manager-match",
|
|
227
|
+
description: "Stated package manager matches lockfile",
|
|
228
|
+
async check(claudeMd, projectRoot, deps = defaultFsDeps) {
|
|
229
|
+
const lower = claudeMd.toLowerCase();
|
|
230
|
+
const lockfileMap = [
|
|
231
|
+
["pnpm-lock.yaml", "pnpm", "pnpm"],
|
|
232
|
+
["yarn.lock", "yarn", "yarn"],
|
|
233
|
+
["bun.lockb", "bun", "bun"],
|
|
234
|
+
["package-lock.json", "npm", "npm"]
|
|
235
|
+
];
|
|
236
|
+
for (const [lockfile, manager, keyword] of lockfileMap) {
|
|
237
|
+
if (await deps.fileExists(join(projectRoot, lockfile))) {
|
|
238
|
+
for (const [, otherManager, otherKeyword] of lockfileMap) {
|
|
239
|
+
if (otherManager === manager) continue;
|
|
240
|
+
if (lower.includes(`${otherKeyword} install`) || lower.includes(`${otherKeyword} run`)) {
|
|
241
|
+
return {
|
|
242
|
+
check: "package-manager-match",
|
|
243
|
+
status: "warn",
|
|
244
|
+
message: `CLAUDE.md references "${otherKeyword}" but lockfile is ${lockfile} (${manager})`
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
check: "package-manager-match",
|
|
253
|
+
status: "pass",
|
|
254
|
+
message: "Package manager references are consistent"
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
];
|
|
259
|
+
async function runDoctorChecks(claudeMdContent, projectRoot, checks = builtinChecks, deps = defaultFsDeps) {
|
|
260
|
+
const results = [];
|
|
261
|
+
for (const check of checks) {
|
|
262
|
+
results.push(await check.check(claudeMdContent, projectRoot, deps));
|
|
263
|
+
}
|
|
264
|
+
return results;
|
|
265
|
+
}
|
|
266
|
+
function computeFreshness(results) {
|
|
267
|
+
const total = results.length;
|
|
268
|
+
if (total === 0) return 100;
|
|
269
|
+
const passing = results.filter((r) => r.status === "pass").length;
|
|
270
|
+
const warnings = results.filter((r) => r.status === "warn").length;
|
|
271
|
+
return Math.round((passing + warnings * 0.5) / total * 100);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// src/commands/doctor.ts
|
|
275
|
+
async function doctorCommand(options = {}, deps = defaultFsDeps) {
|
|
276
|
+
const projectRoot = findProjectRoot();
|
|
277
|
+
const claudeMdPath = findClaudeMd(projectRoot);
|
|
278
|
+
if (!claudeMdPath) {
|
|
279
|
+
error(
|
|
280
|
+
"No CLAUDE.md found. Run `dotclaudemd init` to create one."
|
|
281
|
+
);
|
|
282
|
+
process.exitCode = 1;
|
|
283
|
+
return [];
|
|
284
|
+
}
|
|
285
|
+
const content = await deps.readFile(claudeMdPath);
|
|
286
|
+
const spinner = createSpinner("Running health checks...");
|
|
287
|
+
spinner.start();
|
|
288
|
+
const results = await runDoctorChecks(content, projectRoot, void 0, deps);
|
|
289
|
+
spinner.stop();
|
|
290
|
+
if (options.json) {
|
|
291
|
+
console.log(JSON.stringify({ results, freshness: computeFreshness(results) }, null, 2));
|
|
292
|
+
return results;
|
|
293
|
+
}
|
|
294
|
+
console.log();
|
|
295
|
+
console.log(chalk.bold("CLAUDE.md Health Check"));
|
|
296
|
+
console.log();
|
|
297
|
+
for (const result of results) {
|
|
298
|
+
const icon = result.status === "pass" ? chalk.green("\u2713") : result.status === "warn" ? chalk.yellow("\u26A0") : chalk.red("\u2717");
|
|
299
|
+
console.log(` ${icon} ${result.check}: ${result.message}`);
|
|
300
|
+
if (result.details && result.details.length > 0) {
|
|
301
|
+
for (const detail of result.details.slice(0, 5)) {
|
|
302
|
+
console.log(chalk.dim(` \u2192 ${detail}`));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const freshness = computeFreshness(results);
|
|
307
|
+
console.log();
|
|
308
|
+
const freshnessColor = freshness >= 80 ? chalk.green : freshness >= 50 ? chalk.yellow : chalk.red;
|
|
309
|
+
console.log(
|
|
310
|
+
` Your CLAUDE.md is ${freshnessColor(`${freshness}% fresh`)}`
|
|
311
|
+
);
|
|
312
|
+
console.log();
|
|
313
|
+
return results;
|
|
314
|
+
}
|
|
315
|
+
export {
|
|
316
|
+
doctorCommand
|
|
317
|
+
};
|
|
318
|
+
//# sourceMappingURL=doctor-4B7J2EH3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/doctor.ts","../src/core/doctor-checks.ts"],"sourcesContent":["import chalk from \"chalk\";\nimport { runDoctorChecks, computeFreshness } from \"../core/doctor-checks.js\";\nimport { findClaudeMd, findProjectRoot } from \"../utils/paths.js\";\nimport { defaultFsDeps } from \"../utils/fs.js\";\nimport * as logger from \"../utils/logger.js\";\nimport { createSpinner } from \"../utils/ui.js\";\nimport type { FsDeps, DoctorResult } from \"../types.js\";\n\nexport interface DoctorOptions {\n json?: boolean;\n}\n\nexport async function doctorCommand(\n options: DoctorOptions = {},\n deps: FsDeps = defaultFsDeps,\n): Promise<DoctorResult[]> {\n const projectRoot = findProjectRoot();\n const claudeMdPath = findClaudeMd(projectRoot);\n\n if (!claudeMdPath) {\n logger.error(\n \"No CLAUDE.md found. Run `dotclaudemd init` to create one.\",\n );\n process.exitCode = 1;\n return [];\n }\n\n const content = await deps.readFile(claudeMdPath);\n\n const spinner = createSpinner(\"Running health checks...\");\n spinner.start();\n const results = await runDoctorChecks(content, projectRoot, undefined, deps);\n spinner.stop();\n\n if (options.json) {\n console.log(JSON.stringify({ results, freshness: computeFreshness(results) }, null, 2));\n return results;\n }\n\n console.log();\n console.log(chalk.bold(\"CLAUDE.md Health Check\"));\n console.log();\n\n for (const result of results) {\n const icon =\n result.status === \"pass\"\n ? chalk.green(\"✓\")\n : result.status === \"warn\"\n ? chalk.yellow(\"⚠\")\n : chalk.red(\"✗\");\n\n console.log(` ${icon} ${result.check}: ${result.message}`);\n if (result.details && result.details.length > 0) {\n for (const detail of result.details.slice(0, 5)) {\n console.log(chalk.dim(` → ${detail}`));\n }\n }\n }\n\n const freshness = computeFreshness(results);\n console.log();\n const freshnessColor =\n freshness >= 80\n ? chalk.green\n : freshness >= 50\n ? chalk.yellow\n : chalk.red;\n console.log(\n ` Your CLAUDE.md is ${freshnessColor(`${freshness}% fresh`)}`,\n );\n console.log();\n\n return results;\n}\n","import { join } from \"node:path\";\nimport type { DoctorCheck, DoctorResult, FsDeps } from \"../types.js\";\nimport { defaultFsDeps } from \"../utils/fs.js\";\n\nexport const builtinChecks: DoctorCheck[] = [\n {\n name: \"scripts-exist\",\n description: \"Commands in CLAUDE.md exist in package.json scripts\",\n async check(\n claudeMd: string,\n projectRoot: string,\n deps: FsDeps = defaultFsDeps,\n ): Promise<DoctorResult> {\n const pkgPath = join(projectRoot, \"package.json\");\n if (!(await deps.fileExists(pkgPath))) {\n return {\n check: \"scripts-exist\",\n status: \"pass\",\n message: \"No package.json found (skipped)\",\n };\n }\n\n let pkg: Record<string, unknown>;\n try {\n pkg = JSON.parse(await deps.readFile(pkgPath));\n } catch {\n return {\n check: \"scripts-exist\",\n status: \"warn\",\n message: \"Could not parse package.json\",\n };\n }\n\n const scripts = Object.keys(\n (pkg.scripts ?? {}) as Record<string, string>,\n );\n // Find npm run <script> references in CLAUDE.md\n const scriptRefs = [\n ...claudeMd.matchAll(/npm run (\\w[\\w-]*)/g),\n ].map((m) => m[1]);\n const missing = scriptRefs.filter((s) => !scripts.includes(s));\n\n if (missing.length > 0) {\n return {\n check: \"scripts-exist\",\n status: \"fail\",\n message: `Referenced scripts not found in package.json: ${missing.join(\", \")}`,\n details: missing.map((s) => `\"npm run ${s}\" not in package.json`),\n };\n }\n\n return {\n check: \"scripts-exist\",\n status: \"pass\",\n message: `All ${scriptRefs.length} referenced scripts exist`,\n };\n },\n },\n {\n name: \"deps-mentioned\",\n description: \"Major dependencies are mentioned\",\n async check(\n claudeMd: string,\n projectRoot: string,\n deps: FsDeps = defaultFsDeps,\n ): Promise<DoctorResult> {\n const pkgPath = join(projectRoot, \"package.json\");\n if (!(await deps.fileExists(pkgPath))) {\n return {\n check: \"deps-mentioned\",\n status: \"pass\",\n message: \"No package.json found (skipped)\",\n };\n }\n\n let pkg: Record<string, unknown>;\n try {\n pkg = JSON.parse(await deps.readFile(pkgPath));\n } catch {\n return {\n check: \"deps-mentioned\",\n status: \"warn\",\n message: \"Could not parse package.json\",\n };\n }\n\n const prodDeps = Object.keys(\n (pkg.dependencies ?? {}) as Record<string, string>,\n );\n const lower = claudeMd.toLowerCase();\n const unmentioned = prodDeps.filter(\n (d) => !lower.includes(d.toLowerCase()),\n );\n\n if (unmentioned.length > prodDeps.length * 0.5 && prodDeps.length > 3) {\n return {\n check: \"deps-mentioned\",\n status: \"warn\",\n message: `${unmentioned.length}/${prodDeps.length} major dependencies not mentioned in CLAUDE.md`,\n details: unmentioned.slice(0, 10),\n };\n }\n\n return {\n check: \"deps-mentioned\",\n status: \"pass\",\n message: \"Key dependencies are referenced\",\n };\n },\n },\n {\n name: \"file-refs-valid\",\n description: \"File paths mentioned in CLAUDE.md exist\",\n async check(\n claudeMd: string,\n projectRoot: string,\n deps: FsDeps = defaultFsDeps,\n ): Promise<DoctorResult> {\n const pathRegex =\n /(?:^|\\s)(?:\\.\\/)?([a-zA-Z][\\w\\-./]*\\.\\w{1,10})(?:\\s|$|`|\"|\\))/gm;\n let match: RegExpExecArray | null;\n const invalid: string[] = [];\n\n while ((match = pathRegex.exec(claudeMd)) !== null) {\n const filePath = match[1];\n if (\n filePath.includes(\"example\") ||\n filePath.startsWith(\"http\") ||\n filePath.includes(\"*\")\n )\n continue;\n const fullPath = join(projectRoot, filePath);\n if (!(await deps.fileExists(fullPath))) {\n invalid.push(filePath);\n }\n }\n\n if (invalid.length > 0) {\n return {\n check: \"file-refs-valid\",\n status: \"warn\",\n message: `${invalid.length} referenced file(s) do not exist`,\n details: invalid,\n };\n }\n\n return {\n check: \"file-refs-valid\",\n status: \"pass\",\n message: \"All referenced files exist\",\n };\n },\n },\n {\n name: \"node-version-match\",\n description: \"Stated Node version matches engines/.nvmrc\",\n async check(\n claudeMd: string,\n projectRoot: string,\n deps: FsDeps = defaultFsDeps,\n ): Promise<DoctorResult> {\n // Extract Node version mentioned in CLAUDE.md\n const nodeVersionMatch = claudeMd.match(\n /node\\s*(?:version\\s*)?:?\\s*(\\d+(?:\\.\\d+)*)/i,\n );\n if (!nodeVersionMatch) {\n return {\n check: \"node-version-match\",\n status: \"pass\",\n message: \"No Node version mentioned (skipped)\",\n };\n }\n\n const mentioned = nodeVersionMatch[1];\n\n // Check .nvmrc\n const nvmrcPath = join(projectRoot, \".nvmrc\");\n if (await deps.fileExists(nvmrcPath)) {\n const nvmrc = (await deps.readFile(nvmrcPath)).trim().replace(\"v\", \"\");\n if (!nvmrc.startsWith(mentioned.split(\".\")[0])) {\n return {\n check: \"node-version-match\",\n status: \"fail\",\n message: `CLAUDE.md says Node ${mentioned}, but .nvmrc says ${nvmrc}`,\n };\n }\n }\n\n return {\n check: \"node-version-match\",\n status: \"pass\",\n message: `Node version ${mentioned} is consistent`,\n };\n },\n },\n {\n name: \"test-framework-match\",\n description: \"Mentioned test framework matches actual devDeps\",\n async check(\n claudeMd: string,\n projectRoot: string,\n deps: FsDeps = defaultFsDeps,\n ): Promise<DoctorResult> {\n const pkgPath = join(projectRoot, \"package.json\");\n if (!(await deps.fileExists(pkgPath))) {\n return {\n check: \"test-framework-match\",\n status: \"pass\",\n message: \"No package.json found (skipped)\",\n };\n }\n\n let pkg: Record<string, unknown>;\n try {\n pkg = JSON.parse(await deps.readFile(pkgPath));\n } catch {\n return {\n check: \"test-framework-match\",\n status: \"warn\",\n message: \"Could not parse package.json\",\n };\n }\n\n const devDeps = Object.keys(\n (pkg.devDependencies ?? {}) as Record<string, string>,\n );\n const lower = claudeMd.toLowerCase();\n\n const frameworks = [\n \"vitest\",\n \"jest\",\n \"mocha\",\n \"ava\",\n \"tap\",\n \"pytest\",\n \"unittest\",\n ];\n for (const fw of frameworks) {\n if (lower.includes(fw) && !devDeps.includes(fw)) {\n // Check if it's actually just mentioned as something NOT to use\n if (\n lower.includes(`not ${fw}`) ||\n lower.includes(`don't use ${fw}`)\n )\n continue;\n\n return {\n check: \"test-framework-match\",\n status: \"warn\",\n message: `CLAUDE.md mentions \"${fw}\" but it's not in devDependencies`,\n };\n }\n }\n\n return {\n check: \"test-framework-match\",\n status: \"pass\",\n message: \"Test framework references are consistent\",\n };\n },\n },\n {\n name: \"package-manager-match\",\n description: \"Stated package manager matches lockfile\",\n async check(\n claudeMd: string,\n projectRoot: string,\n deps: FsDeps = defaultFsDeps,\n ): Promise<DoctorResult> {\n const lower = claudeMd.toLowerCase();\n\n const lockfileMap: [string, string, string][] = [\n [\"pnpm-lock.yaml\", \"pnpm\", \"pnpm\"],\n [\"yarn.lock\", \"yarn\", \"yarn\"],\n [\"bun.lockb\", \"bun\", \"bun\"],\n [\"package-lock.json\", \"npm\", \"npm\"],\n ];\n\n for (const [lockfile, manager, keyword] of lockfileMap) {\n if (await deps.fileExists(join(projectRoot, lockfile))) {\n // Check if CLAUDE.md mentions a different package manager\n for (const [, otherManager, otherKeyword] of lockfileMap) {\n if (otherManager === manager) continue;\n if (\n lower.includes(`${otherKeyword} install`) ||\n lower.includes(`${otherKeyword} run`)\n ) {\n return {\n check: \"package-manager-match\",\n status: \"warn\",\n message: `CLAUDE.md references \"${otherKeyword}\" but lockfile is ${lockfile} (${manager})`,\n };\n }\n }\n break;\n }\n }\n\n return {\n check: \"package-manager-match\",\n status: \"pass\",\n message: \"Package manager references are consistent\",\n };\n },\n },\n];\n\nexport async function runDoctorChecks(\n claudeMdContent: string,\n projectRoot: string,\n checks: DoctorCheck[] = builtinChecks,\n deps: FsDeps = defaultFsDeps,\n): Promise<DoctorResult[]> {\n const results: DoctorResult[] = [];\n for (const check of checks) {\n results.push(await check.check(claudeMdContent, projectRoot, deps));\n }\n return results;\n}\n\nexport function computeFreshness(results: DoctorResult[]): number {\n const total = results.length;\n if (total === 0) return 100;\n\n const passing = results.filter((r) => r.status === \"pass\").length;\n const warnings = results.filter((r) => r.status === \"warn\").length;\n\n return Math.round(((passing + warnings * 0.5) / total) * 100);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
createSpinner
|
|
4
|
+
} from "./chunk-2D66CC54.js";
|
|
5
|
+
import {
|
|
6
|
+
listTemplates,
|
|
7
|
+
renderTemplate,
|
|
8
|
+
suggestTemplates
|
|
9
|
+
} from "./chunk-3ERFQLAD.js";
|
|
10
|
+
import "./chunk-3WTPUEHL.js";
|
|
11
|
+
import {
|
|
12
|
+
defaultFsDeps,
|
|
13
|
+
findClaudeMd,
|
|
14
|
+
findProjectRoot
|
|
15
|
+
} from "./chunk-3R6PUA3E.js";
|
|
16
|
+
import {
|
|
17
|
+
dim,
|
|
18
|
+
error,
|
|
19
|
+
info,
|
|
20
|
+
success
|
|
21
|
+
} from "./chunk-YHVOBZLV.js";
|
|
22
|
+
|
|
23
|
+
// src/commands/init.ts
|
|
24
|
+
import { join as join2, dirname } from "path";
|
|
25
|
+
import { mkdirSync } from "fs";
|
|
26
|
+
import { select, input, confirm } from "@inquirer/prompts";
|
|
27
|
+
|
|
28
|
+
// src/core/project-detector.ts
|
|
29
|
+
import { join } from "path";
|
|
30
|
+
async function detectStack(projectRoot, deps = defaultFsDeps) {
|
|
31
|
+
const detectors = [
|
|
32
|
+
detectNode,
|
|
33
|
+
detectPython,
|
|
34
|
+
detectRust,
|
|
35
|
+
detectGo
|
|
36
|
+
];
|
|
37
|
+
for (const detector of detectors) {
|
|
38
|
+
const result = await detector(projectRoot, deps);
|
|
39
|
+
if (result) return result;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
async function detectNode(projectRoot, deps) {
|
|
44
|
+
const pkgPath = join(projectRoot, "package.json");
|
|
45
|
+
if (!await deps.fileExists(pkgPath)) return null;
|
|
46
|
+
let pkg;
|
|
47
|
+
try {
|
|
48
|
+
pkg = JSON.parse(await deps.readFile(pkgPath));
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const allDeps = pkg.dependencies ?? {};
|
|
53
|
+
const allDevDeps = pkg.devDependencies ?? {};
|
|
54
|
+
const depNames = Object.keys(allDeps);
|
|
55
|
+
const devDepNames = Object.keys(allDevDeps);
|
|
56
|
+
const stack = {
|
|
57
|
+
language: "javascript",
|
|
58
|
+
dependencies: depNames,
|
|
59
|
+
devDependencies: devDepNames
|
|
60
|
+
};
|
|
61
|
+
if (devDepNames.includes("typescript") || await deps.fileExists(join(projectRoot, "tsconfig.json"))) {
|
|
62
|
+
stack.language = "javascript";
|
|
63
|
+
}
|
|
64
|
+
if (depNames.includes("next")) {
|
|
65
|
+
stack.framework = "Next.js";
|
|
66
|
+
} else if (depNames.includes("express") && depNames.includes("react")) {
|
|
67
|
+
stack.framework = "MERN";
|
|
68
|
+
} else if (depNames.includes("express")) {
|
|
69
|
+
stack.framework = "Express";
|
|
70
|
+
} else if (depNames.includes("react")) {
|
|
71
|
+
stack.framework = "React";
|
|
72
|
+
}
|
|
73
|
+
if (await deps.fileExists(join(projectRoot, "pnpm-lock.yaml"))) {
|
|
74
|
+
stack.packageManager = "pnpm";
|
|
75
|
+
} else if (await deps.fileExists(join(projectRoot, "yarn.lock"))) {
|
|
76
|
+
stack.packageManager = "yarn";
|
|
77
|
+
} else if (await deps.fileExists(join(projectRoot, "bun.lockb"))) {
|
|
78
|
+
stack.packageManager = "bun";
|
|
79
|
+
} else if (await deps.fileExists(join(projectRoot, "package-lock.json"))) {
|
|
80
|
+
stack.packageManager = "npm";
|
|
81
|
+
}
|
|
82
|
+
if (devDepNames.includes("vitest")) {
|
|
83
|
+
stack.testFramework = "vitest";
|
|
84
|
+
} else if (devDepNames.includes("jest")) {
|
|
85
|
+
stack.testFramework = "jest";
|
|
86
|
+
} else if (devDepNames.includes("mocha")) {
|
|
87
|
+
stack.testFramework = "mocha";
|
|
88
|
+
}
|
|
89
|
+
return stack;
|
|
90
|
+
}
|
|
91
|
+
async function detectPython(projectRoot, deps) {
|
|
92
|
+
const hasPyproject = await deps.fileExists(
|
|
93
|
+
join(projectRoot, "pyproject.toml")
|
|
94
|
+
);
|
|
95
|
+
const hasRequirements = await deps.fileExists(
|
|
96
|
+
join(projectRoot, "requirements.txt")
|
|
97
|
+
);
|
|
98
|
+
if (!hasPyproject && !hasRequirements) return null;
|
|
99
|
+
const stack = {
|
|
100
|
+
language: "python",
|
|
101
|
+
dependencies: [],
|
|
102
|
+
devDependencies: []
|
|
103
|
+
};
|
|
104
|
+
if (hasRequirements) {
|
|
105
|
+
try {
|
|
106
|
+
const content = await deps.readFile(
|
|
107
|
+
join(projectRoot, "requirements.txt")
|
|
108
|
+
);
|
|
109
|
+
const lines = content.split("\n").map((l) => l.trim().toLowerCase().split("==")[0].split(">=")[0]);
|
|
110
|
+
stack.dependencies = lines.filter((l) => l && !l.startsWith("#"));
|
|
111
|
+
if (lines.includes("fastapi")) stack.framework = "FastAPI";
|
|
112
|
+
else if (lines.includes("django")) stack.framework = "Django";
|
|
113
|
+
else if (lines.includes("flask")) stack.framework = "Flask";
|
|
114
|
+
} catch {
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (hasPyproject) {
|
|
118
|
+
try {
|
|
119
|
+
const content = await deps.readFile(
|
|
120
|
+
join(projectRoot, "pyproject.toml")
|
|
121
|
+
);
|
|
122
|
+
if (content.includes("fastapi")) stack.framework = "FastAPI";
|
|
123
|
+
else if (content.includes("django")) stack.framework = "Django";
|
|
124
|
+
else if (content.includes("flask")) stack.framework = "Flask";
|
|
125
|
+
if (content.includes("pytest")) stack.testFramework = "pytest";
|
|
126
|
+
} catch {
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (await deps.fileExists(join(projectRoot, "poetry.lock"))) {
|
|
130
|
+
stack.packageManager = "poetry";
|
|
131
|
+
} else if (await deps.fileExists(join(projectRoot, "Pipfile.lock"))) {
|
|
132
|
+
stack.packageManager = "pipenv";
|
|
133
|
+
} else if (await deps.fileExists(join(projectRoot, "uv.lock"))) {
|
|
134
|
+
stack.packageManager = "uv";
|
|
135
|
+
} else {
|
|
136
|
+
stack.packageManager = "pip";
|
|
137
|
+
}
|
|
138
|
+
return stack;
|
|
139
|
+
}
|
|
140
|
+
async function detectRust(projectRoot, deps) {
|
|
141
|
+
if (!await deps.fileExists(join(projectRoot, "Cargo.toml"))) return null;
|
|
142
|
+
const stack = {
|
|
143
|
+
language: "rust",
|
|
144
|
+
packageManager: "cargo",
|
|
145
|
+
dependencies: [],
|
|
146
|
+
devDependencies: []
|
|
147
|
+
};
|
|
148
|
+
try {
|
|
149
|
+
const content = await deps.readFile(join(projectRoot, "Cargo.toml"));
|
|
150
|
+
if (content.includes("actix-web")) stack.framework = "Actix";
|
|
151
|
+
else if (content.includes("axum")) stack.framework = "Axum";
|
|
152
|
+
else if (content.includes("rocket")) stack.framework = "Rocket";
|
|
153
|
+
} catch {
|
|
154
|
+
}
|
|
155
|
+
return stack;
|
|
156
|
+
}
|
|
157
|
+
async function detectGo(projectRoot, deps) {
|
|
158
|
+
if (!await deps.fileExists(join(projectRoot, "go.mod"))) return null;
|
|
159
|
+
const stack = {
|
|
160
|
+
language: "go",
|
|
161
|
+
packageManager: "go",
|
|
162
|
+
dependencies: [],
|
|
163
|
+
devDependencies: []
|
|
164
|
+
};
|
|
165
|
+
try {
|
|
166
|
+
const content = await deps.readFile(join(projectRoot, "go.mod"));
|
|
167
|
+
if (content.includes("gin-gonic/gin")) stack.framework = "Gin";
|
|
168
|
+
else if (content.includes("gofiber/fiber")) stack.framework = "Fiber";
|
|
169
|
+
else if (content.includes("go-chi/chi")) stack.framework = "Chi";
|
|
170
|
+
else if (content.includes("labstack/echo")) stack.framework = "Echo";
|
|
171
|
+
} catch {
|
|
172
|
+
}
|
|
173
|
+
return stack;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/commands/init.ts
|
|
177
|
+
async function initCommand(options = {}, deps = defaultFsDeps, promptFn) {
|
|
178
|
+
const projectRoot = options.global ? join2(process.env.HOME ?? "~", ".claude") : findProjectRoot();
|
|
179
|
+
const outputPath = options.global ? join2(process.env.HOME ?? "~", ".claude", "CLAUDE.md") : join2(projectRoot, "CLAUDE.md");
|
|
180
|
+
const existing = findClaudeMd(projectRoot);
|
|
181
|
+
if (existing && !options.force) {
|
|
182
|
+
if (options.noInteractive) {
|
|
183
|
+
error(
|
|
184
|
+
`CLAUDE.md already exists at ${existing}. Use --force to overwrite.`
|
|
185
|
+
);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const overwrite = await confirm({
|
|
189
|
+
message: `CLAUDE.md already exists at ${existing}. Overwrite?`,
|
|
190
|
+
default: false
|
|
191
|
+
});
|
|
192
|
+
if (!overwrite) {
|
|
193
|
+
info("Cancelled.");
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const spinner = createSpinner("Detecting project stack...");
|
|
198
|
+
spinner.start();
|
|
199
|
+
const detected = options.stack ? null : await detectStack(projectRoot, deps);
|
|
200
|
+
spinner.stop();
|
|
201
|
+
if (detected) {
|
|
202
|
+
info(
|
|
203
|
+
`Detected: ${detected.language}${detected.framework ? ` / ${detected.framework}` : ""}${detected.packageManager ? ` (${detected.packageManager})` : ""}`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
let template;
|
|
207
|
+
const allTemplates = await listTemplates();
|
|
208
|
+
if (options.stack) {
|
|
209
|
+
const match = allTemplates.find(
|
|
210
|
+
(t) => t.frontmatter.name === options.stack
|
|
211
|
+
);
|
|
212
|
+
if (!match) {
|
|
213
|
+
error(`No template found for stack: ${options.stack}`);
|
|
214
|
+
info(
|
|
215
|
+
`Available: ${allTemplates.map((t) => t.frontmatter.name).join(", ")}`
|
|
216
|
+
);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
template = match;
|
|
220
|
+
} else if (detected) {
|
|
221
|
+
const suggestions = await suggestTemplates(detected);
|
|
222
|
+
if (suggestions.length === 1 || options.noInteractive) {
|
|
223
|
+
template = suggestions[0] ?? allTemplates[0];
|
|
224
|
+
} else if (suggestions.length > 1) {
|
|
225
|
+
template = await selectTemplate(suggestions);
|
|
226
|
+
} else {
|
|
227
|
+
template = await selectTemplate(allTemplates);
|
|
228
|
+
}
|
|
229
|
+
} else {
|
|
230
|
+
if (options.noInteractive) {
|
|
231
|
+
template = allTemplates.find((t) => t.frontmatter.name === "default") ?? allTemplates[0];
|
|
232
|
+
} else {
|
|
233
|
+
template = await selectTemplate(allTemplates);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
info(`Using template: ${template.frontmatter.displayName}`);
|
|
237
|
+
const variables = {};
|
|
238
|
+
const doPrompt = promptFn ?? promptForVariables;
|
|
239
|
+
if (template.frontmatter.variables.length > 0 && !options.noInteractive) {
|
|
240
|
+
await doPrompt(template, variables, detected);
|
|
241
|
+
} else {
|
|
242
|
+
for (const v of template.frontmatter.variables) {
|
|
243
|
+
if (v.default) variables[v.name] = v.default;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const rendered = renderTemplate(template, variables);
|
|
247
|
+
mkdirSync(dirname(outputPath), { recursive: true });
|
|
248
|
+
await deps.writeFile(outputPath, rendered);
|
|
249
|
+
success(`Created ${outputPath}`);
|
|
250
|
+
info("Next steps:");
|
|
251
|
+
dim(" 1. Review and customize your CLAUDE.md");
|
|
252
|
+
dim(" 2. Run `dotclaudemd lint` to check for issues");
|
|
253
|
+
dim(" 3. Run `dotclaudemd doctor` to verify accuracy");
|
|
254
|
+
}
|
|
255
|
+
async function selectTemplate(templates) {
|
|
256
|
+
const answer = await select({
|
|
257
|
+
message: "Choose a template:",
|
|
258
|
+
choices: templates.map((t) => ({
|
|
259
|
+
name: `${t.frontmatter.displayName} \u2014 ${t.frontmatter.description}`,
|
|
260
|
+
value: t.frontmatter.name
|
|
261
|
+
}))
|
|
262
|
+
});
|
|
263
|
+
return templates.find((t) => t.frontmatter.name === answer);
|
|
264
|
+
}
|
|
265
|
+
async function promptForVariables(template, variables, detected) {
|
|
266
|
+
for (const v of template.frontmatter.variables) {
|
|
267
|
+
const defaultValue = v.default ?? (v.name === "package_manager" && detected?.packageManager ? detected.packageManager : void 0) ?? (v.name === "test_framework" && detected?.testFramework ? detected.testFramework : void 0);
|
|
268
|
+
if (v.options && v.options.length > 0) {
|
|
269
|
+
const answer = await select({
|
|
270
|
+
message: v.prompt,
|
|
271
|
+
choices: v.options.map((o) => ({ name: o, value: o })),
|
|
272
|
+
default: defaultValue
|
|
273
|
+
});
|
|
274
|
+
variables[v.name] = answer;
|
|
275
|
+
} else {
|
|
276
|
+
const answer = await input({
|
|
277
|
+
message: v.prompt,
|
|
278
|
+
default: defaultValue
|
|
279
|
+
});
|
|
280
|
+
variables[v.name] = answer;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
export {
|
|
285
|
+
initCommand
|
|
286
|
+
};
|
|
287
|
+
//# sourceMappingURL=init-GLWLFVHN.js.map
|