@raindogs/contextmd 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +152 -0
- package/bin/contextmd.js +5 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1201 -0
- package/dist/index.js.map +1 -0
- package/package.json +33 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1201 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { program } from "commander";
|
|
5
|
+
import { writeFileSync, existsSync as existsSync5, mkdirSync } from "fs";
|
|
6
|
+
import { join as join5, dirname } from "path";
|
|
7
|
+
import { createInterface } from "readline";
|
|
8
|
+
|
|
9
|
+
// src/scanner/package.ts
|
|
10
|
+
import { readFileSync, existsSync } from "fs";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
function scanPackage(cwd) {
|
|
13
|
+
const pkgPath = join(cwd, "package.json");
|
|
14
|
+
if (!existsSync(pkgPath)) {
|
|
15
|
+
throw new Error("No package.json found. Run contextmd from the root of your project.");
|
|
16
|
+
}
|
|
17
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
18
|
+
const scripts = pkg.scripts ?? {};
|
|
19
|
+
const allDeps = {
|
|
20
|
+
...pkg.dependencies ?? {},
|
|
21
|
+
...pkg.devDependencies ?? {},
|
|
22
|
+
...pkg.peerDependencies ?? {}
|
|
23
|
+
};
|
|
24
|
+
const has = (name) => name in allDeps;
|
|
25
|
+
const hasPattern = (re) => Object.keys(allDeps).some((k) => re.test(k));
|
|
26
|
+
let packageManager = "npm";
|
|
27
|
+
if (existsSync(join(cwd, "bun.lockb"))) packageManager = "bun";
|
|
28
|
+
else if (existsSync(join(cwd, "pnpm-lock.yaml"))) packageManager = "pnpm";
|
|
29
|
+
else if (existsSync(join(cwd, "yarn.lock"))) packageManager = "yarn";
|
|
30
|
+
const runCmd = packageManager === "npm" ? "npm run" : packageManager;
|
|
31
|
+
let framework;
|
|
32
|
+
let isNextAppRouter = false;
|
|
33
|
+
if (has("next")) {
|
|
34
|
+
framework = "nextjs";
|
|
35
|
+
isNextAppRouter = existsSync(join(cwd, "app")) || existsSync(join(cwd, "src/app"));
|
|
36
|
+
} else if (has("react") || has("react-dom")) {
|
|
37
|
+
framework = "react";
|
|
38
|
+
} else if (has("vue") || has("@vue/core")) {
|
|
39
|
+
framework = "vue";
|
|
40
|
+
} else if (has("@sveltejs/kit") || has("svelte")) {
|
|
41
|
+
framework = "svelte";
|
|
42
|
+
} else if (has("express")) {
|
|
43
|
+
framework = "express";
|
|
44
|
+
} else if (has("fastify")) {
|
|
45
|
+
framework = "fastify";
|
|
46
|
+
} else if (has("hono")) {
|
|
47
|
+
framework = "hono";
|
|
48
|
+
}
|
|
49
|
+
let testRunner;
|
|
50
|
+
if (has("vitest")) testRunner = "vitest";
|
|
51
|
+
else if (has("jest") || has("@jest/core") || has("ts-jest")) testRunner = "jest";
|
|
52
|
+
else if (has("mocha")) testRunner = "mocha";
|
|
53
|
+
let database;
|
|
54
|
+
if (has("@prisma/client") || has("prisma")) database = "prisma";
|
|
55
|
+
else if (has("drizzle-orm")) database = "drizzle";
|
|
56
|
+
else if (has("@supabase/supabase-js") || has("@supabase/ssr")) database = "supabase";
|
|
57
|
+
else if (has("mongoose")) database = "mongoose";
|
|
58
|
+
else if (has("typeorm")) database = "typeorm";
|
|
59
|
+
else if (has("pg") || has("postgres")) database = "pg";
|
|
60
|
+
else if (has("better-sqlite3")) database = "better-sqlite3";
|
|
61
|
+
let stateLib;
|
|
62
|
+
if (has("zustand")) stateLib = "zustand";
|
|
63
|
+
else if (has("@reduxjs/toolkit") || has("redux")) stateLib = "redux";
|
|
64
|
+
else if (has("@tanstack/react-query") || has("@tanstack/query-core")) stateLib = "tanstack-query";
|
|
65
|
+
else if (has("jotai")) stateLib = "jotai";
|
|
66
|
+
else if (has("recoil")) stateLib = "recoil";
|
|
67
|
+
let cssApproach;
|
|
68
|
+
if (has("tailwindcss")) cssApproach = "tailwind";
|
|
69
|
+
else if (has("styled-components")) cssApproach = "styled-components";
|
|
70
|
+
else if (has("@emotion/styled") || has("@emotion/react")) cssApproach = "emotion";
|
|
71
|
+
else if (has("sass") || has("node-sass")) cssApproach = "sass";
|
|
72
|
+
let uiLib;
|
|
73
|
+
if (existsSync(join(cwd, "src/components/ui")) || existsSync(join(cwd, "components/ui"))) {
|
|
74
|
+
uiLib = "shadcn";
|
|
75
|
+
} else if (hasPattern(/@radix-ui\//)) {
|
|
76
|
+
uiLib = "radix";
|
|
77
|
+
} else if (has("@chakra-ui/react")) {
|
|
78
|
+
uiLib = "chakra";
|
|
79
|
+
} else if (has("@mantine/core")) {
|
|
80
|
+
uiLib = "mantine";
|
|
81
|
+
} else if (has("@mui/material")) {
|
|
82
|
+
uiLib = "mui";
|
|
83
|
+
}
|
|
84
|
+
const linter = has("eslint") ? "eslint" : has("biome") ? "biome" : void 0;
|
|
85
|
+
const formatter = has("prettier") ? "prettier" : has("biome") ? "biome" : void 0;
|
|
86
|
+
let commitConvention;
|
|
87
|
+
if (has("@commitlint/cli") || has("commitlint") || has("commitizen") || has("@commitlint/config-conventional")) {
|
|
88
|
+
commitConvention = "conventional-commits";
|
|
89
|
+
} else if (has("gitmoji-cli")) {
|
|
90
|
+
commitConvention = "gitmoji";
|
|
91
|
+
}
|
|
92
|
+
let deployTarget;
|
|
93
|
+
if (existsSync(join(cwd, "vercel.json")) || existsSync(join(cwd, ".vercel"))) deployTarget = "vercel";
|
|
94
|
+
else if (existsSync(join(cwd, "netlify.toml"))) deployTarget = "netlify";
|
|
95
|
+
else if (existsSync(join(cwd, "fly.toml"))) deployTarget = "fly";
|
|
96
|
+
else if (existsSync(join(cwd, "Dockerfile")) || existsSync(join(cwd, "docker-compose.yml"))) deployTarget = "docker";
|
|
97
|
+
let pathAlias;
|
|
98
|
+
const tsconfigPath = join(cwd, "tsconfig.json");
|
|
99
|
+
if (existsSync(tsconfigPath)) {
|
|
100
|
+
try {
|
|
101
|
+
const tsconfig = JSON.parse(readFileSync(tsconfigPath, "utf-8"));
|
|
102
|
+
const paths = tsconfig?.compilerOptions?.paths;
|
|
103
|
+
if (paths) {
|
|
104
|
+
const aliases = Object.keys(paths);
|
|
105
|
+
const atAlias = aliases.find((a) => a.startsWith("@/") || a === "@/*");
|
|
106
|
+
if (atAlias) pathAlias = "@/";
|
|
107
|
+
else if (aliases.length > 0) pathAlias = aliases[0].replace("/*", "/");
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const hasMonorepo = !!(pkg.workspaces || existsSync(join(cwd, "turbo.json")) || existsSync(join(cwd, "nx.json")) || existsSync(join(cwd, "lerna.json")) || existsSync(join(cwd, "pnpm-workspace.yaml")));
|
|
113
|
+
const hasSupabaseLocal = existsSync(join(cwd, "supabase"));
|
|
114
|
+
const findScript = (...names) => names.find((n) => n in scripts);
|
|
115
|
+
const devScript = findScript("dev", "start:dev", "develop");
|
|
116
|
+
const buildScript = findScript("build", "compile");
|
|
117
|
+
const testScript = findScript("test", "test:unit", "test:all");
|
|
118
|
+
const lintScript = findScript("lint", "eslint");
|
|
119
|
+
const typecheckScript = findScript("typecheck", "type-check", "tsc", "check:types");
|
|
120
|
+
const formatScript = findScript("format", "fmt", "prettier");
|
|
121
|
+
const dbMigrateScript = findScript("db:migrate", "db:push", "prisma:migrate", "drizzle-kit:push");
|
|
122
|
+
const dbGenerateScript = findScript("db:generate", "prisma:generate", "drizzle-kit:generate");
|
|
123
|
+
const huskyPreCommit = existsSync(join(cwd, ".husky/pre-commit"));
|
|
124
|
+
let testSingle;
|
|
125
|
+
if (testRunner === "vitest") testSingle = "vitest run <path/to/file.test.ts>";
|
|
126
|
+
else if (testRunner === "jest") testSingle = "jest <path/to/file.test.ts>";
|
|
127
|
+
let preCommit;
|
|
128
|
+
if (huskyPreCommit) {
|
|
129
|
+
preCommit = `${runCmd} lint && ${runCmd} typecheck`;
|
|
130
|
+
if (typecheckScript && lintScript) {
|
|
131
|
+
preCommit = `${runCmd} ${lintScript} && ${runCmd} ${typecheckScript}`;
|
|
132
|
+
}
|
|
133
|
+
} else if (typecheckScript && lintScript) {
|
|
134
|
+
preCommit = `${runCmd} ${lintScript} && ${runCmd} ${typecheckScript}`;
|
|
135
|
+
} else if (typecheckScript) {
|
|
136
|
+
preCommit = `${runCmd} ${typecheckScript}`;
|
|
137
|
+
}
|
|
138
|
+
const commands = {
|
|
139
|
+
dev: devScript ? `${runCmd} ${devScript}` : void 0,
|
|
140
|
+
build: buildScript ? `${runCmd} ${buildScript}` : void 0,
|
|
141
|
+
test: testScript ? `${runCmd} ${testScript}` : void 0,
|
|
142
|
+
testSingle,
|
|
143
|
+
lint: lintScript ? `${runCmd} ${lintScript}` : void 0,
|
|
144
|
+
typecheck: typecheckScript ? `${runCmd} ${typecheckScript}` : void 0,
|
|
145
|
+
format: formatScript ? `${runCmd} ${formatScript}` : void 0,
|
|
146
|
+
preCommit,
|
|
147
|
+
dbMigrate: dbMigrateScript ? `${runCmd} ${dbMigrateScript}` : void 0,
|
|
148
|
+
dbGenerate: dbGenerateScript ? `${runCmd} ${dbGenerateScript}` : void 0
|
|
149
|
+
};
|
|
150
|
+
const stack = {
|
|
151
|
+
framework,
|
|
152
|
+
language: has("typescript") || existsSync(tsconfigPath) ? "typescript" : "javascript",
|
|
153
|
+
testRunner,
|
|
154
|
+
database,
|
|
155
|
+
stateLib,
|
|
156
|
+
cssApproach,
|
|
157
|
+
uiLib,
|
|
158
|
+
packageManager,
|
|
159
|
+
linter,
|
|
160
|
+
formatter,
|
|
161
|
+
commitConvention,
|
|
162
|
+
deployTarget,
|
|
163
|
+
isNextAppRouter,
|
|
164
|
+
hasMonorepo,
|
|
165
|
+
hasSupabaseLocal,
|
|
166
|
+
pathAlias
|
|
167
|
+
};
|
|
168
|
+
return {
|
|
169
|
+
name: pkg.name ?? "untitled",
|
|
170
|
+
description: pkg.description,
|
|
171
|
+
version: pkg.version,
|
|
172
|
+
scripts,
|
|
173
|
+
allDeps,
|
|
174
|
+
stack,
|
|
175
|
+
commands
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// src/scanner/filesystem.ts
|
|
180
|
+
import { readdirSync, statSync, existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
181
|
+
import { join as join2, relative } from "path";
|
|
182
|
+
var SKIP_ALWAYS = /* @__PURE__ */ new Set([
|
|
183
|
+
"node_modules",
|
|
184
|
+
".git",
|
|
185
|
+
".next",
|
|
186
|
+
".nuxt",
|
|
187
|
+
".svelte-kit",
|
|
188
|
+
"dist",
|
|
189
|
+
"build",
|
|
190
|
+
"out",
|
|
191
|
+
".turbo",
|
|
192
|
+
".cache",
|
|
193
|
+
"coverage",
|
|
194
|
+
".vercel",
|
|
195
|
+
".netlify",
|
|
196
|
+
"__pycache__",
|
|
197
|
+
".pytest_cache"
|
|
198
|
+
]);
|
|
199
|
+
var PURPOSE_MAP = [
|
|
200
|
+
["src/app", "App Router pages, layouts, and server components"],
|
|
201
|
+
["src/pages", "Next.js pages and API routes"],
|
|
202
|
+
["app", "App Router pages, layouts, and server components"],
|
|
203
|
+
["pages", "Next.js pages and API routes"],
|
|
204
|
+
["src/components", "React components"],
|
|
205
|
+
["components", "React components"],
|
|
206
|
+
["src/components/ui", "shadcn/ui primitive components"],
|
|
207
|
+
["components/ui", "shadcn/ui primitive components"],
|
|
208
|
+
["src/lib", "Shared utilities and helper functions"],
|
|
209
|
+
["src/utils", "Shared utility functions"],
|
|
210
|
+
["lib", "Shared utilities"],
|
|
211
|
+
["utils", "Utility functions"],
|
|
212
|
+
["src/hooks", "Custom React hooks"],
|
|
213
|
+
["hooks", "Custom React hooks"],
|
|
214
|
+
["src/types", "TypeScript type definitions"],
|
|
215
|
+
["types", "TypeScript type definitions"],
|
|
216
|
+
["src/stores", "State management stores"],
|
|
217
|
+
["stores", "State management stores"],
|
|
218
|
+
["src/server", "Server-side code and API handlers"],
|
|
219
|
+
["src/api", "API route handlers"],
|
|
220
|
+
["server", "Server-side code"],
|
|
221
|
+
["api", "API endpoints"],
|
|
222
|
+
["src/db", "Database schema, queries, and migrations"],
|
|
223
|
+
["src/database", "Database schema and queries"],
|
|
224
|
+
["db", "Database schema and queries"],
|
|
225
|
+
["database", "Database layer"],
|
|
226
|
+
["supabase", "Supabase local config, migrations, and edge functions"],
|
|
227
|
+
["prisma", "Prisma schema and generated client"],
|
|
228
|
+
["drizzle", "Drizzle ORM schema and migrations"],
|
|
229
|
+
["migrations", "Database migration files"],
|
|
230
|
+
["public", "Static assets served as-is"],
|
|
231
|
+
["static", "Static assets"],
|
|
232
|
+
["assets", "Images, fonts, and other static assets"],
|
|
233
|
+
["styles", "Global styles and CSS variables"],
|
|
234
|
+
["src/styles", "Global styles"],
|
|
235
|
+
["src/config", "App configuration"],
|
|
236
|
+
["config", "Configuration files"],
|
|
237
|
+
["src/middleware", "Express/Next.js middleware"],
|
|
238
|
+
["middleware", "Request middleware"],
|
|
239
|
+
["src/services", "Business logic and external service clients"],
|
|
240
|
+
["services", "Service layer"],
|
|
241
|
+
["src/actions", "Server actions"],
|
|
242
|
+
["actions", "Server actions"],
|
|
243
|
+
["src/context", "React context providers"],
|
|
244
|
+
["context", "React context providers"],
|
|
245
|
+
["scripts", "Build and maintenance scripts"],
|
|
246
|
+
["tests", "Test files"],
|
|
247
|
+
["__tests__", "Test files"],
|
|
248
|
+
["e2e", "End-to-end tests"],
|
|
249
|
+
["docs", "Documentation"],
|
|
250
|
+
[".claude", "Claude Code configuration and rules"],
|
|
251
|
+
[".github", "GitHub Actions and workflows"]
|
|
252
|
+
];
|
|
253
|
+
function getPurpose(relPath) {
|
|
254
|
+
for (const [key, purpose] of PURPOSE_MAP) {
|
|
255
|
+
if (typeof key === "string") {
|
|
256
|
+
if (relPath === key) return purpose;
|
|
257
|
+
} else {
|
|
258
|
+
if (key.test(relPath)) return purpose;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return void 0;
|
|
262
|
+
}
|
|
263
|
+
function inferPurposeFromName(name) {
|
|
264
|
+
const lower = name.toLowerCase();
|
|
265
|
+
const map = {
|
|
266
|
+
scanner: "File and repo analysis modules",
|
|
267
|
+
generator: "Output generators",
|
|
268
|
+
generators: "Output generators",
|
|
269
|
+
parser: "Parsing utilities",
|
|
270
|
+
parsers: "Parsing utilities",
|
|
271
|
+
writer: "File writing utilities",
|
|
272
|
+
writers: "File writing utilities",
|
|
273
|
+
command: "CLI command handlers",
|
|
274
|
+
commands: "CLI command handlers",
|
|
275
|
+
handler: "Request or event handlers",
|
|
276
|
+
handlers: "Request or event handlers",
|
|
277
|
+
controller: "Route controllers",
|
|
278
|
+
controllers: "Route controllers",
|
|
279
|
+
model: "Data models",
|
|
280
|
+
models: "Data models",
|
|
281
|
+
schema: "Data schemas",
|
|
282
|
+
schemas: "Data schemas",
|
|
283
|
+
resolver: "GraphQL resolvers",
|
|
284
|
+
resolvers: "GraphQL resolvers",
|
|
285
|
+
route: "Route definitions",
|
|
286
|
+
routes: "Route definitions",
|
|
287
|
+
plugin: "Plugin modules",
|
|
288
|
+
plugins: "Plugin modules",
|
|
289
|
+
provider: "React context providers",
|
|
290
|
+
providers: "React context providers",
|
|
291
|
+
helper: "Helper utilities",
|
|
292
|
+
helpers: "Helper utilities",
|
|
293
|
+
test: "Test files",
|
|
294
|
+
tests: "Test files",
|
|
295
|
+
spec: "Test files",
|
|
296
|
+
specs: "Test files",
|
|
297
|
+
fixture: "Test fixtures",
|
|
298
|
+
fixtures: "Test fixtures",
|
|
299
|
+
mock: "Mock modules for testing",
|
|
300
|
+
mocks: "Mock modules for testing",
|
|
301
|
+
constant: "Constants and enums",
|
|
302
|
+
constants: "Constants and enums",
|
|
303
|
+
enum: "TypeScript enums",
|
|
304
|
+
enums: "TypeScript enums",
|
|
305
|
+
event: "Event definitions",
|
|
306
|
+
events: "Event definitions",
|
|
307
|
+
job: "Background jobs",
|
|
308
|
+
jobs: "Background jobs",
|
|
309
|
+
queue: "Job queue definitions",
|
|
310
|
+
queues: "Job queue definitions",
|
|
311
|
+
dto: "Data transfer objects",
|
|
312
|
+
dtos: "Data transfer objects",
|
|
313
|
+
entity: "Database entities",
|
|
314
|
+
entities: "Database entities",
|
|
315
|
+
repository: "Data access repositories",
|
|
316
|
+
repositories: "Data access repositories"
|
|
317
|
+
};
|
|
318
|
+
return map[lower];
|
|
319
|
+
}
|
|
320
|
+
function scanFilesystem(cwd, stack) {
|
|
321
|
+
const entries = [];
|
|
322
|
+
const seen = /* @__PURE__ */ new Set();
|
|
323
|
+
function walk(dir, depth) {
|
|
324
|
+
if (depth > 3) return;
|
|
325
|
+
let items;
|
|
326
|
+
try {
|
|
327
|
+
items = readdirSync(dir);
|
|
328
|
+
} catch {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
for (const item of items) {
|
|
332
|
+
if (item.startsWith(".") && item !== ".claude" && item !== ".github") continue;
|
|
333
|
+
if (SKIP_ALWAYS.has(item)) continue;
|
|
334
|
+
const full = join2(dir, item);
|
|
335
|
+
const rel = relative(cwd, full);
|
|
336
|
+
try {
|
|
337
|
+
const stat = statSync(full);
|
|
338
|
+
if (!stat.isDirectory()) continue;
|
|
339
|
+
} catch {
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (seen.has(rel)) continue;
|
|
343
|
+
seen.add(rel);
|
|
344
|
+
const purpose = getPurpose(rel);
|
|
345
|
+
if (purpose) {
|
|
346
|
+
entries.push({ path: rel, purpose, isKey: depth <= 2 });
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (item === "src" || item === "packages" || item === "apps" || item === "lib") {
|
|
350
|
+
walk(full, depth + 1);
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
if (depth >= 1) {
|
|
354
|
+
const inferred = inferPurposeFromName(item);
|
|
355
|
+
if (inferred) {
|
|
356
|
+
entries.push({ path: rel, purpose: inferred, isKey: true });
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
walk(cwd, 0);
|
|
362
|
+
entries.sort((a, b) => {
|
|
363
|
+
if (a.isKey !== b.isKey) return a.isKey ? -1 : 1;
|
|
364
|
+
const aDepth = a.path.split("/").length;
|
|
365
|
+
const bDepth = b.path.split("/").length;
|
|
366
|
+
if (aDepth !== bDepth) return aDepth - bDepth;
|
|
367
|
+
return a.path.localeCompare(b.path);
|
|
368
|
+
});
|
|
369
|
+
const deduped = [];
|
|
370
|
+
for (const entry of entries) {
|
|
371
|
+
const isSubpathOfExisting = deduped.some(
|
|
372
|
+
(e) => entry.path.startsWith(e.path + "/") && entry.path !== e.path
|
|
373
|
+
);
|
|
374
|
+
const isNotable = entry.path.includes("/ui") || entry.path === "supabase" || entry.path.startsWith(".claude");
|
|
375
|
+
if (!isSubpathOfExisting || isNotable) {
|
|
376
|
+
deduped.push(entry);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return deduped.filter((e) => e.isKey).slice(0, 12).concat(deduped.filter((e) => !e.isKey).slice(0, 3)).slice(0, 12);
|
|
380
|
+
}
|
|
381
|
+
function detectMcpServers(cwd) {
|
|
382
|
+
const servers = [];
|
|
383
|
+
const claudeDir = join2(cwd, ".claude");
|
|
384
|
+
if (!existsSync2(claudeDir)) return servers;
|
|
385
|
+
try {
|
|
386
|
+
const files = readdirSync(claudeDir);
|
|
387
|
+
for (const file of files) {
|
|
388
|
+
if (!file.endsWith(".json")) continue;
|
|
389
|
+
const content = readFileSync2(join2(claudeDir, file), "utf-8");
|
|
390
|
+
const parsed = JSON.parse(content);
|
|
391
|
+
if (parsed.mcpServers) {
|
|
392
|
+
servers.push(...Object.keys(parsed.mcpServers));
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
} catch {
|
|
396
|
+
}
|
|
397
|
+
const desktopConfig = join2(cwd, "claude_desktop_config.json");
|
|
398
|
+
if (existsSync2(desktopConfig)) {
|
|
399
|
+
try {
|
|
400
|
+
const parsed = JSON.parse(readFileSync2(desktopConfig, "utf-8"));
|
|
401
|
+
if (parsed.mcpServers) servers.push(...Object.keys(parsed.mcpServers));
|
|
402
|
+
} catch {
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return [...new Set(servers)];
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// src/scanner/git.ts
|
|
409
|
+
import { existsSync as existsSync3 } from "fs";
|
|
410
|
+
import { join as join3 } from "path";
|
|
411
|
+
async function getGit(cwd) {
|
|
412
|
+
const { simpleGit } = await import("simple-git");
|
|
413
|
+
return simpleGit(cwd);
|
|
414
|
+
}
|
|
415
|
+
async function scanGit(cwd) {
|
|
416
|
+
const hasHusky = existsSync3(join3(cwd, ".husky"));
|
|
417
|
+
if (!existsSync3(join3(cwd, ".git"))) {
|
|
418
|
+
return { recentExamples: [], hasHusky };
|
|
419
|
+
}
|
|
420
|
+
try {
|
|
421
|
+
const git = await getGit(cwd);
|
|
422
|
+
const log = await git.log({ maxCount: 25, "--no-merges": null });
|
|
423
|
+
const messages = log.all.map((c) => c.message.trim()).filter(Boolean);
|
|
424
|
+
const commitFormat = detectCommitFormat(messages);
|
|
425
|
+
let defaultBranch;
|
|
426
|
+
try {
|
|
427
|
+
const branches = await git.branch(["-r"]);
|
|
428
|
+
if (branches.all.includes("origin/main")) defaultBranch = "main";
|
|
429
|
+
else if (branches.all.includes("origin/master")) defaultBranch = "master";
|
|
430
|
+
} catch {
|
|
431
|
+
}
|
|
432
|
+
const recentExamples = pickExamples(messages, commitFormat);
|
|
433
|
+
return { commitFormat, recentExamples, defaultBranch, hasHusky };
|
|
434
|
+
} catch {
|
|
435
|
+
return { recentExamples: [], hasHusky };
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
function detectCommitFormat(messages) {
|
|
439
|
+
if (messages.length === 0) return void 0;
|
|
440
|
+
const conventionalRe = /^(feat|fix|chore|docs|style|refactor|perf|test|build|ci|revert)(\(.+\))?: .+/;
|
|
441
|
+
const conventionalCount = messages.filter((m) => conventionalRe.test(m)).length;
|
|
442
|
+
const gitmoji = /^[\u{1F300}-\u{1FAFF}]/u;
|
|
443
|
+
const gitmojiCount = messages.filter((m) => gitmoji.test(m)).length;
|
|
444
|
+
const threshold = Math.max(3, messages.length * 0.6);
|
|
445
|
+
if (conventionalCount >= threshold) return "type(scope): description \u2014 conventional commits";
|
|
446
|
+
if (gitmojiCount >= threshold) return ":emoji: description \u2014 gitmoji";
|
|
447
|
+
return void 0;
|
|
448
|
+
}
|
|
449
|
+
function pickExamples(messages, format) {
|
|
450
|
+
if (messages.length === 0) return [];
|
|
451
|
+
if (format?.includes("conventional")) {
|
|
452
|
+
const types = ["feat", "fix", "chore", "refactor", "docs"];
|
|
453
|
+
const picked = [];
|
|
454
|
+
for (const type of types) {
|
|
455
|
+
const match = messages.find((m) => m.startsWith(type));
|
|
456
|
+
if (match && picked.length < 3) picked.push(match);
|
|
457
|
+
}
|
|
458
|
+
if (picked.length > 0) return picked;
|
|
459
|
+
}
|
|
460
|
+
return messages.slice(0, 3);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// src/scanner/inference.ts
|
|
464
|
+
var RULES = [
|
|
465
|
+
// ── State management ────────────────────────────────────────────────────
|
|
466
|
+
{
|
|
467
|
+
condition: (s) => s.stateLib === "zustand",
|
|
468
|
+
doNot: "Do not introduce Redux or Redux Toolkit \u2014 state management is Zustand",
|
|
469
|
+
inferredFrom: "dep:zustand"
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
condition: (s) => s.stateLib === "tanstack-query",
|
|
473
|
+
doNot: "Do not store server state in useState or useReducer \u2014 use TanStack Query",
|
|
474
|
+
inferredFrom: "dep:@tanstack/react-query"
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
condition: (s) => s.stateLib === "jotai",
|
|
478
|
+
doNot: "Do not use Redux or Zustand \u2014 state management is Jotai",
|
|
479
|
+
inferredFrom: "dep:jotai"
|
|
480
|
+
},
|
|
481
|
+
// ── Database ────────────────────────────────────────────────────────────
|
|
482
|
+
{
|
|
483
|
+
condition: (s) => s.database === "prisma",
|
|
484
|
+
doNot: "Do not write raw SQL \u2014 use Prisma ORM for all database access",
|
|
485
|
+
inferredFrom: "dep:prisma"
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
condition: (s) => s.database === "drizzle",
|
|
489
|
+
doNot: "Do not write raw SQL \u2014 use Drizzle ORM for all database access",
|
|
490
|
+
inferredFrom: "dep:drizzle-orm"
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
condition: (s) => s.database === "supabase",
|
|
494
|
+
convention: "Use the Supabase client from @/lib/supabase (or equivalent) \u2014 do not instantiate directly",
|
|
495
|
+
inferredFrom: "dep:@supabase/supabase-js"
|
|
496
|
+
},
|
|
497
|
+
// ── CSS / styling ────────────────────────────────────────────────────────
|
|
498
|
+
{
|
|
499
|
+
condition: (s) => s.cssApproach === "tailwind",
|
|
500
|
+
doNot: "Do not use inline styles or CSS-in-JS \u2014 styling is Tailwind utility classes",
|
|
501
|
+
inferredFrom: "dep:tailwindcss"
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
condition: (s) => s.cssApproach === "css-modules",
|
|
505
|
+
doNot: "Do not use inline styles \u2014 use CSS Modules (*.module.css)",
|
|
506
|
+
inferredFrom: "css-modules detected"
|
|
507
|
+
},
|
|
508
|
+
{
|
|
509
|
+
condition: (s) => s.cssApproach === "styled-components",
|
|
510
|
+
convention: "Styling uses styled-components \u2014 keep component styles co-located",
|
|
511
|
+
inferredFrom: "dep:styled-components"
|
|
512
|
+
},
|
|
513
|
+
// ── UI lib ───────────────────────────────────────────────────────────────
|
|
514
|
+
{
|
|
515
|
+
condition: (s) => s.uiLib === "shadcn",
|
|
516
|
+
convention: "UI primitives live in @/components/ui \u2014 use them before creating new components",
|
|
517
|
+
inferredFrom: "src/components/ui detected"
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
condition: (s) => s.uiLib === "chakra",
|
|
521
|
+
convention: "UI components use Chakra UI \u2014 do not mix with other component libraries",
|
|
522
|
+
inferredFrom: "dep:@chakra-ui/react"
|
|
523
|
+
},
|
|
524
|
+
// ── Next.js ─────────────────────────────────────────────────────────────
|
|
525
|
+
{
|
|
526
|
+
condition: (s) => s.isNextAppRouter,
|
|
527
|
+
convention: 'This is a Next.js App Router project \u2014 use Server Components by default, add "use client" only when needed',
|
|
528
|
+
inferredFrom: "nextjs app/ directory detected"
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
condition: (s) => s.isNextAppRouter,
|
|
532
|
+
doNot: "Do not create files in a pages/ directory \u2014 this project uses App Router",
|
|
533
|
+
inferredFrom: "nextjs app/ directory detected"
|
|
534
|
+
},
|
|
535
|
+
// ── Path aliases ──────────────────────────────────────────────────────────
|
|
536
|
+
{
|
|
537
|
+
condition: (s) => !!s.pathAlias,
|
|
538
|
+
convention: (s) => `Use the ${s.pathAlias} path alias for imports \u2014 do not use relative paths like ../../`,
|
|
539
|
+
inferredFrom: "tsconfig paths"
|
|
540
|
+
},
|
|
541
|
+
// ── Monorepo ─────────────────────────────────────────────────────────────
|
|
542
|
+
{
|
|
543
|
+
condition: (s) => s.hasMonorepo,
|
|
544
|
+
convention: "Monorepo: run commands from the workspace root with --filter or --workspace flags",
|
|
545
|
+
inferredFrom: "monorepo config detected"
|
|
546
|
+
},
|
|
547
|
+
// ── Commit conventions ───────────────────────────────────────────────────
|
|
548
|
+
{
|
|
549
|
+
condition: (s) => s.commitConvention === "conventional-commits",
|
|
550
|
+
convention: "Commits follow Conventional Commits: type(scope): description (e.g. feat(auth): add OAuth login)",
|
|
551
|
+
inferredFrom: "dep:@commitlint"
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
condition: (s) => s.commitConvention === "gitmoji",
|
|
555
|
+
convention: "Commits use gitmoji prefix (e.g. \u2728 new feature, \u{1F41B} bug fix)",
|
|
556
|
+
inferredFrom: "dep:gitmoji-cli"
|
|
557
|
+
},
|
|
558
|
+
// ── TypeScript ───────────────────────────────────────────────────────────
|
|
559
|
+
{
|
|
560
|
+
condition: (s, deps) => s.language === "typescript" && "typescript" in deps,
|
|
561
|
+
doNot: "Do not use @ts-ignore or any \u2014 fix type errors properly",
|
|
562
|
+
inferredFrom: "dep:typescript"
|
|
563
|
+
},
|
|
564
|
+
// ── Testing ──────────────────────────────────────────────────────────────
|
|
565
|
+
{
|
|
566
|
+
condition: (s) => s.testRunner === "vitest",
|
|
567
|
+
convention: "Tests use Vitest \u2014 co-locate test files as *.test.ts beside the file they test",
|
|
568
|
+
inferredFrom: "dep:vitest"
|
|
569
|
+
}
|
|
570
|
+
];
|
|
571
|
+
function resolveConvention(c, stack) {
|
|
572
|
+
if (typeof c === "function") return c(stack);
|
|
573
|
+
return c;
|
|
574
|
+
}
|
|
575
|
+
function inferConventions(stack, allDeps) {
|
|
576
|
+
const conventions = [];
|
|
577
|
+
const doNots = [];
|
|
578
|
+
for (const rule of RULES) {
|
|
579
|
+
if (!rule.condition(stack, allDeps)) continue;
|
|
580
|
+
if (rule.convention) {
|
|
581
|
+
const text = resolveConvention(rule.convention, stack);
|
|
582
|
+
if (text) conventions.push({ rule: text, inferredFrom: rule.inferredFrom });
|
|
583
|
+
}
|
|
584
|
+
if (rule.doNot) {
|
|
585
|
+
doNots.push(rule.doNot);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return { conventions, doNots };
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// src/scanner/index.ts
|
|
592
|
+
async function scanRepo(cwd) {
|
|
593
|
+
const pkg = scanPackage(cwd);
|
|
594
|
+
const [directories, git] = await Promise.all([
|
|
595
|
+
Promise.resolve(scanFilesystem(cwd, pkg.stack)),
|
|
596
|
+
scanGit(cwd)
|
|
597
|
+
]);
|
|
598
|
+
const mcpServers = detectMcpServers(cwd);
|
|
599
|
+
const { conventions, doNots } = inferConventions(pkg.stack, pkg.allDeps);
|
|
600
|
+
return {
|
|
601
|
+
name: pkg.name,
|
|
602
|
+
description: pkg.description,
|
|
603
|
+
version: pkg.version,
|
|
604
|
+
stack: pkg.stack,
|
|
605
|
+
scripts: pkg.scripts,
|
|
606
|
+
commands: pkg.commands,
|
|
607
|
+
directories,
|
|
608
|
+
git,
|
|
609
|
+
conventions,
|
|
610
|
+
doNots,
|
|
611
|
+
mcpServers,
|
|
612
|
+
hasDatabaseNotes: !!(pkg.stack.database && (pkg.commands.dbMigrate || pkg.stack.hasSupabaseLocal)),
|
|
613
|
+
hasSupabaseLocal: pkg.stack.hasSupabaseLocal,
|
|
614
|
+
cwd,
|
|
615
|
+
allDeps: pkg.allDeps
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// src/generators/claude.ts
|
|
620
|
+
var MAX_LINES = 150;
|
|
621
|
+
function buildHeadline(ctx) {
|
|
622
|
+
const { stack, name, description } = ctx;
|
|
623
|
+
const parts = [];
|
|
624
|
+
if (stack.framework === "nextjs") {
|
|
625
|
+
parts.push(`Next.js${stack.isNextAppRouter ? " (App Router)" : ""}`);
|
|
626
|
+
} else if (stack.framework === "react") {
|
|
627
|
+
parts.push("React");
|
|
628
|
+
} else if (stack.framework === "vue") {
|
|
629
|
+
parts.push("Vue");
|
|
630
|
+
} else if (stack.framework === "svelte") {
|
|
631
|
+
parts.push("SvelteKit");
|
|
632
|
+
} else if (stack.framework === "express") {
|
|
633
|
+
parts.push("Express");
|
|
634
|
+
} else if (stack.framework === "fastify") {
|
|
635
|
+
parts.push("Fastify");
|
|
636
|
+
} else if (stack.framework === "hono") {
|
|
637
|
+
parts.push("Hono");
|
|
638
|
+
}
|
|
639
|
+
if (stack.language === "typescript") parts.push("TypeScript");
|
|
640
|
+
const keyDeps = [];
|
|
641
|
+
if (stack.database === "prisma") keyDeps.push("Prisma");
|
|
642
|
+
else if (stack.database === "drizzle") keyDeps.push("Drizzle");
|
|
643
|
+
else if (stack.database === "supabase") keyDeps.push("Supabase");
|
|
644
|
+
if (stack.cssApproach === "tailwind") keyDeps.push("Tailwind");
|
|
645
|
+
if (stack.uiLib === "shadcn") keyDeps.push("shadcn/ui");
|
|
646
|
+
if (stack.stateLib === "zustand") keyDeps.push("Zustand");
|
|
647
|
+
else if (stack.stateLib === "tanstack-query") keyDeps.push("TanStack Query");
|
|
648
|
+
if (stack.deployTarget === "vercel") keyDeps.push("Vercel");
|
|
649
|
+
const stackStr = parts.join(" + ");
|
|
650
|
+
const depsStr = keyDeps.length > 0 ? ` using ${keyDeps.join(", ")}.` : ".";
|
|
651
|
+
if (description && description.length < 80 && !description.toLowerCase().includes("undefined")) {
|
|
652
|
+
return `${stackStr}${depsStr} ${description}`;
|
|
653
|
+
}
|
|
654
|
+
return `${stackStr}${depsStr}`;
|
|
655
|
+
}
|
|
656
|
+
function buildCommandsSection(ctx) {
|
|
657
|
+
const lines = ["## Commands", ""];
|
|
658
|
+
const { commands } = ctx;
|
|
659
|
+
if (commands.dev) lines.push(`- \`${commands.dev}\` \u2014 start dev server`);
|
|
660
|
+
if (commands.build) lines.push(`- \`${commands.build}\` \u2014 production build`);
|
|
661
|
+
if (commands.test) lines.push(`- \`${commands.test}\` \u2014 run all tests`);
|
|
662
|
+
if (commands.testSingle) lines.push(`- \`${commands.testSingle}\` \u2014 run a single test file`);
|
|
663
|
+
if (commands.lint) lines.push(`- \`${commands.lint}\` \u2014 lint`);
|
|
664
|
+
if (commands.typecheck) lines.push(`- \`${commands.typecheck}\` \u2014 type check`);
|
|
665
|
+
if (commands.format) lines.push(`- \`${commands.format}\` \u2014 format`);
|
|
666
|
+
if (commands.dbMigrate) lines.push(`- \`${commands.dbMigrate}\` \u2014 run database migrations`);
|
|
667
|
+
if (commands.dbGenerate) lines.push(`- \`${commands.dbGenerate}\` \u2014 regenerate database types`);
|
|
668
|
+
if (commands.preCommit) {
|
|
669
|
+
lines.push("");
|
|
670
|
+
lines.push(`Always run \`${commands.preCommit}\` before committing.`);
|
|
671
|
+
}
|
|
672
|
+
return lines;
|
|
673
|
+
}
|
|
674
|
+
function buildArchSection(ctx) {
|
|
675
|
+
if (ctx.directories.length === 0) return [];
|
|
676
|
+
const lines = ["## Architecture", ""];
|
|
677
|
+
for (const dir of ctx.directories) {
|
|
678
|
+
lines.push(`- \`${dir.path}/\` \u2014 ${dir.purpose}`);
|
|
679
|
+
}
|
|
680
|
+
return lines;
|
|
681
|
+
}
|
|
682
|
+
function buildConventionsSection(ctx) {
|
|
683
|
+
if (ctx.conventions.length === 0) return [];
|
|
684
|
+
const lines = ["## Conventions", ""];
|
|
685
|
+
for (const c of ctx.conventions) {
|
|
686
|
+
lines.push(`- ${c.rule}`);
|
|
687
|
+
}
|
|
688
|
+
return lines;
|
|
689
|
+
}
|
|
690
|
+
function buildTestingSection(ctx) {
|
|
691
|
+
const { stack, commands } = ctx;
|
|
692
|
+
if (!stack.testRunner) return [];
|
|
693
|
+
const lines = ["## Testing", ""];
|
|
694
|
+
if (stack.testRunner === "vitest") {
|
|
695
|
+
lines.push(`Uses Vitest. Run all: \`${commands.test ?? "vitest"}\``);
|
|
696
|
+
if (commands.testSingle) lines.push(`Run one file: \`${commands.testSingle}\``);
|
|
697
|
+
} else if (stack.testRunner === "jest") {
|
|
698
|
+
lines.push(`Uses Jest. Run all: \`${commands.test ?? "jest"}\``);
|
|
699
|
+
if (commands.testSingle) lines.push(`Run one file: \`${commands.testSingle}\``);
|
|
700
|
+
} else if (stack.testRunner === "mocha") {
|
|
701
|
+
lines.push(`Uses Mocha. Run: \`${commands.test ?? "mocha"}\``);
|
|
702
|
+
}
|
|
703
|
+
return lines;
|
|
704
|
+
}
|
|
705
|
+
function buildGitSection(ctx) {
|
|
706
|
+
const { git } = ctx;
|
|
707
|
+
const lines = [];
|
|
708
|
+
if (!git.commitFormat && git.recentExamples.length === 0) return lines;
|
|
709
|
+
lines.push("## Git", "");
|
|
710
|
+
if (git.commitFormat) {
|
|
711
|
+
lines.push(`Commit format: \`${git.commitFormat}\``);
|
|
712
|
+
}
|
|
713
|
+
if (git.recentExamples.length > 0 && git.commitFormat) {
|
|
714
|
+
lines.push("");
|
|
715
|
+
lines.push("Examples:");
|
|
716
|
+
for (const ex of git.recentExamples.slice(0, 2)) {
|
|
717
|
+
lines.push(`- \`${ex.slice(0, 72)}\``);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return lines;
|
|
721
|
+
}
|
|
722
|
+
function buildDoNotSection(ctx) {
|
|
723
|
+
if (ctx.doNots.length === 0) return [];
|
|
724
|
+
const lines = ["## Do NOT", ""];
|
|
725
|
+
for (const doNot of ctx.doNots) {
|
|
726
|
+
lines.push(`- ${doNot}`);
|
|
727
|
+
}
|
|
728
|
+
return lines;
|
|
729
|
+
}
|
|
730
|
+
function buildMcpSection(ctx) {
|
|
731
|
+
if (ctx.mcpServers.length === 0) return [];
|
|
732
|
+
const lines = ["## MCP Servers", ""];
|
|
733
|
+
lines.push("Available in this project:");
|
|
734
|
+
for (const srv of ctx.mcpServers) {
|
|
735
|
+
lines.push(`- ${srv}`);
|
|
736
|
+
}
|
|
737
|
+
return lines;
|
|
738
|
+
}
|
|
739
|
+
function buildDatabaseSection(ctx) {
|
|
740
|
+
if (!ctx.hasDatabaseNotes) return [];
|
|
741
|
+
const lines = ["## Database", ""];
|
|
742
|
+
if (ctx.stack.database === "prisma") {
|
|
743
|
+
lines.push("Schema is in `prisma/schema.prisma`.");
|
|
744
|
+
if (ctx.commands.dbMigrate) lines.push(`Run migrations: \`${ctx.commands.dbMigrate}\``);
|
|
745
|
+
if (ctx.commands.dbGenerate) lines.push(`Regenerate client: \`${ctx.commands.dbGenerate}\``);
|
|
746
|
+
} else if (ctx.stack.database === "drizzle") {
|
|
747
|
+
lines.push("Schema is in `drizzle/` or `src/db/`.");
|
|
748
|
+
if (ctx.commands.dbMigrate) lines.push(`Apply migrations: \`${ctx.commands.dbMigrate}\``);
|
|
749
|
+
} else if (ctx.stack.hasSupabaseLocal) {
|
|
750
|
+
lines.push("Uses Supabase local development stack.");
|
|
751
|
+
lines.push("Start local: `supabase start`");
|
|
752
|
+
lines.push("Apply migrations: `supabase db push`");
|
|
753
|
+
if (ctx.commands.dbGenerate) lines.push(`Generate types: \`${ctx.commands.dbGenerate}\``);
|
|
754
|
+
}
|
|
755
|
+
return lines;
|
|
756
|
+
}
|
|
757
|
+
function generateClaude(ctx) {
|
|
758
|
+
const sections = [
|
|
759
|
+
[`# ${ctx.name}`, "", buildHeadline(ctx), ""],
|
|
760
|
+
[...buildCommandsSection(ctx), ""],
|
|
761
|
+
[...buildArchSection(ctx), ""],
|
|
762
|
+
[...buildConventionsSection(ctx), ""],
|
|
763
|
+
[...buildTestingSection(ctx), ""],
|
|
764
|
+
[...buildGitSection(ctx), ""],
|
|
765
|
+
[...buildDoNotSection(ctx), ""],
|
|
766
|
+
[...buildMcpSection(ctx), ""],
|
|
767
|
+
[...buildDatabaseSection(ctx), ""]
|
|
768
|
+
];
|
|
769
|
+
const allLines = sections.flat().join("\n").split("\n");
|
|
770
|
+
const trimmed = allLines.join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
771
|
+
const lines = trimmed.split("\n");
|
|
772
|
+
if (lines.length <= MAX_LINES) {
|
|
773
|
+
return trimmed + "\n";
|
|
774
|
+
}
|
|
775
|
+
const capped = lines.slice(0, MAX_LINES - 3).join("\n");
|
|
776
|
+
return capped + "\n\n<!-- contextmd: output capped at 150 lines. Run with --depth for extended output. -->\n";
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// src/generators/agents.ts
|
|
780
|
+
function generateAgents(ctx) {
|
|
781
|
+
const { name, stack, commands, directories, conventions, doNots, git } = ctx;
|
|
782
|
+
const lines = [];
|
|
783
|
+
lines.push(`# ${name} \u2014 Agent Instructions`);
|
|
784
|
+
lines.push("");
|
|
785
|
+
lines.push(buildHeadline2(ctx));
|
|
786
|
+
lines.push("");
|
|
787
|
+
lines.push("## Environment");
|
|
788
|
+
lines.push("");
|
|
789
|
+
if (commands.dev) lines.push(`Dev server: \`${commands.dev}\``);
|
|
790
|
+
if (commands.build) lines.push(`Build: \`${commands.build}\``);
|
|
791
|
+
if (commands.typecheck) lines.push(`Type check: \`${commands.typecheck}\``);
|
|
792
|
+
if (commands.lint) lines.push(`Lint: \`${commands.lint}\``);
|
|
793
|
+
if (commands.test) lines.push(`Tests: \`${commands.test}\``);
|
|
794
|
+
if (commands.testSingle) lines.push(`Single test: \`${commands.testSingle}\``);
|
|
795
|
+
lines.push("");
|
|
796
|
+
lines.push("## Required before completing any task");
|
|
797
|
+
lines.push("");
|
|
798
|
+
const checks = [];
|
|
799
|
+
if (commands.typecheck) checks.push(`Run \`${commands.typecheck}\` \u2014 zero type errors`);
|
|
800
|
+
if (commands.lint) checks.push(`Run \`${commands.lint}\` \u2014 zero lint errors`);
|
|
801
|
+
if (commands.test) checks.push(`Run \`${commands.test}\` \u2014 all tests pass`);
|
|
802
|
+
if (commands.build) checks.push(`Run \`${commands.build}\` \u2014 build succeeds`);
|
|
803
|
+
if (checks.length > 0) {
|
|
804
|
+
for (const check of checks) lines.push(`- ${check}`);
|
|
805
|
+
} else {
|
|
806
|
+
lines.push("- Ensure no TypeScript errors before submitting");
|
|
807
|
+
lines.push("- Ensure all existing tests pass");
|
|
808
|
+
}
|
|
809
|
+
lines.push("");
|
|
810
|
+
if (conventions.length > 0) {
|
|
811
|
+
lines.push("## Code style");
|
|
812
|
+
lines.push("");
|
|
813
|
+
for (const c of conventions) {
|
|
814
|
+
lines.push(`- ${c.rule}`);
|
|
815
|
+
}
|
|
816
|
+
lines.push("");
|
|
817
|
+
}
|
|
818
|
+
if (doNots.length > 0) {
|
|
819
|
+
lines.push("## Constraints");
|
|
820
|
+
lines.push("");
|
|
821
|
+
for (const doNot of doNots) {
|
|
822
|
+
lines.push(`- ${doNot}`);
|
|
823
|
+
}
|
|
824
|
+
lines.push("");
|
|
825
|
+
}
|
|
826
|
+
if (directories.length > 0) {
|
|
827
|
+
lines.push("## Repository structure");
|
|
828
|
+
lines.push("");
|
|
829
|
+
for (const dir of directories) {
|
|
830
|
+
lines.push(`- \`${dir.path}/\` \u2014 ${dir.purpose}`);
|
|
831
|
+
}
|
|
832
|
+
lines.push("");
|
|
833
|
+
}
|
|
834
|
+
if (git.commitFormat) {
|
|
835
|
+
lines.push("## Commit format");
|
|
836
|
+
lines.push("");
|
|
837
|
+
lines.push(git.commitFormat);
|
|
838
|
+
if (git.recentExamples.length > 0) {
|
|
839
|
+
lines.push("");
|
|
840
|
+
lines.push("Examples:");
|
|
841
|
+
for (const ex of git.recentExamples.slice(0, 2)) {
|
|
842
|
+
lines.push(`- \`${ex.slice(0, 72)}\``);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
lines.push("");
|
|
846
|
+
}
|
|
847
|
+
lines.push("## Reminders");
|
|
848
|
+
lines.push("");
|
|
849
|
+
if (stack.isNextAppRouter) {
|
|
850
|
+
lines.push("- This is a Next.js App Router project. Default to Server Components.");
|
|
851
|
+
lines.push('- Only add "use client" when the component needs browser APIs or event handlers.');
|
|
852
|
+
}
|
|
853
|
+
if (stack.database === "prisma") {
|
|
854
|
+
lines.push("- Never write raw SQL. All database access goes through Prisma.");
|
|
855
|
+
} else if (stack.database === "drizzle") {
|
|
856
|
+
lines.push("- Never write raw SQL. All database access goes through Drizzle ORM.");
|
|
857
|
+
}
|
|
858
|
+
if (stack.pathAlias) {
|
|
859
|
+
lines.push(`- Use \`${stack.pathAlias}\` path alias for imports, not relative paths.`);
|
|
860
|
+
}
|
|
861
|
+
lines.push("- Do not commit changes to auto-generated files (prisma client, supabase types, etc.).");
|
|
862
|
+
lines.push("- Prefer editing existing patterns over introducing new abstractions.");
|
|
863
|
+
return lines.join("\n") + "\n";
|
|
864
|
+
}
|
|
865
|
+
function buildHeadline2(ctx) {
|
|
866
|
+
const { stack, description } = ctx;
|
|
867
|
+
const parts = [];
|
|
868
|
+
if (stack.framework === "nextjs") parts.push(`Next.js${stack.isNextAppRouter ? " App Router" : ""}`);
|
|
869
|
+
else if (stack.framework === "react") parts.push("React");
|
|
870
|
+
else if (stack.framework === "vue") parts.push("Vue");
|
|
871
|
+
else if (stack.framework === "svelte") parts.push("SvelteKit");
|
|
872
|
+
else if (stack.framework) parts.push(stack.framework);
|
|
873
|
+
if (stack.language === "typescript") parts.push("TypeScript");
|
|
874
|
+
const extras = [];
|
|
875
|
+
if (stack.database === "prisma") extras.push("Prisma");
|
|
876
|
+
else if (stack.database === "drizzle") extras.push("Drizzle");
|
|
877
|
+
else if (stack.database === "supabase") extras.push("Supabase");
|
|
878
|
+
if (stack.cssApproach === "tailwind") extras.push("Tailwind CSS");
|
|
879
|
+
if (stack.uiLib === "shadcn") extras.push("shadcn/ui");
|
|
880
|
+
const base = parts.join(" + ") + (extras.length ? ` \xB7 ${extras.join(", ")}` : "");
|
|
881
|
+
return description ? `${base} \u2014 ${description}` : base;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// src/generators/ruler.ts
|
|
885
|
+
function generateRulerFiles(ctx) {
|
|
886
|
+
const files = {};
|
|
887
|
+
files[".ruler/rules.md"] = buildRulerRules(ctx);
|
|
888
|
+
files[".ruler/project.md"] = buildRulerProject(ctx);
|
|
889
|
+
return { files };
|
|
890
|
+
}
|
|
891
|
+
function buildRulerRules(ctx) {
|
|
892
|
+
const lines = [
|
|
893
|
+
"# Rules",
|
|
894
|
+
"",
|
|
895
|
+
"<!-- Generated by contextmd --ruler \u2014 edit here, ruler distributes to all agents -->",
|
|
896
|
+
""
|
|
897
|
+
];
|
|
898
|
+
if (ctx.conventions.length > 0) {
|
|
899
|
+
lines.push("## Conventions", "");
|
|
900
|
+
for (const c of ctx.conventions) lines.push(`- ${c.rule}`);
|
|
901
|
+
lines.push("");
|
|
902
|
+
}
|
|
903
|
+
if (ctx.doNots.length > 0) {
|
|
904
|
+
lines.push("## Constraints", "");
|
|
905
|
+
for (const doNot of ctx.doNots) lines.push(`- ${doNot}`);
|
|
906
|
+
lines.push("");
|
|
907
|
+
}
|
|
908
|
+
if (ctx.commands.preCommit) {
|
|
909
|
+
lines.push("## Before committing", "", `Always run: \`${ctx.commands.preCommit}\``, "");
|
|
910
|
+
}
|
|
911
|
+
return lines.join("\n");
|
|
912
|
+
}
|
|
913
|
+
function buildRulerProject(ctx) {
|
|
914
|
+
const { name, stack, directories } = ctx;
|
|
915
|
+
const lines = [`# ${name}`, ""];
|
|
916
|
+
const parts = [];
|
|
917
|
+
if (stack.framework) parts.push(stack.framework);
|
|
918
|
+
if (stack.language === "typescript") parts.push("TypeScript");
|
|
919
|
+
if (stack.database) parts.push(stack.database);
|
|
920
|
+
if (parts.length > 0) lines.push(parts.join(" + "), "");
|
|
921
|
+
if (directories.length > 0) {
|
|
922
|
+
lines.push("## Structure", "");
|
|
923
|
+
for (const dir of directories) lines.push(`- \`${dir.path}/\` \u2014 ${dir.purpose}`);
|
|
924
|
+
lines.push("");
|
|
925
|
+
}
|
|
926
|
+
return lines.join("\n");
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// src/generators/scorer.ts
|
|
930
|
+
import { readFileSync as readFileSync3, existsSync as existsSync4 } from "fs";
|
|
931
|
+
import { join as join4 } from "path";
|
|
932
|
+
function scoreClaudeFile(cwd) {
|
|
933
|
+
const filePath = join4(cwd, "CLAUDE.md");
|
|
934
|
+
if (!existsSync4(filePath)) {
|
|
935
|
+
return {
|
|
936
|
+
total: 0,
|
|
937
|
+
checks: [
|
|
938
|
+
{
|
|
939
|
+
name: "File exists",
|
|
940
|
+
passed: false,
|
|
941
|
+
score: 0,
|
|
942
|
+
feedback: "No CLAUDE.md found. Run `npx contextmd` to generate one."
|
|
943
|
+
}
|
|
944
|
+
]
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
948
|
+
const lines = content.split("\n");
|
|
949
|
+
const checks = [
|
|
950
|
+
checkCommands(content),
|
|
951
|
+
checkArchitecture(content, cwd),
|
|
952
|
+
checkConventions(content),
|
|
953
|
+
checkDoNot(content),
|
|
954
|
+
checkLength(lines)
|
|
955
|
+
];
|
|
956
|
+
const total = Math.round(checks.reduce((sum, c) => sum + c.score, 0));
|
|
957
|
+
return { total, checks };
|
|
958
|
+
}
|
|
959
|
+
function checkCommands(content) {
|
|
960
|
+
const hasCommandsSection = /^## Commands/m.test(content);
|
|
961
|
+
const commandMatches = content.match(/`([^`]+)`/g) ?? [];
|
|
962
|
+
const hasExecutableCommands = commandMatches.some((cmd) => {
|
|
963
|
+
const inner = cmd.replace(/`/g, "");
|
|
964
|
+
return /^(npm|pnpm|yarn|bun|node|npx|vitest|jest|tsc|next|vite|deno)/.test(inner);
|
|
965
|
+
});
|
|
966
|
+
const passed = hasCommandsSection && hasExecutableCommands;
|
|
967
|
+
return {
|
|
968
|
+
name: "Commands are executable",
|
|
969
|
+
passed,
|
|
970
|
+
score: passed ? 20 : hasCommandsSection ? 10 : 0,
|
|
971
|
+
feedback: passed ? "Commands section has executable commands." : !hasCommandsSection ? "Missing ## Commands section. Add dev, build, test commands." : "Commands section found but commands are not in backtick format."
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
function checkArchitecture(content, cwd) {
|
|
975
|
+
const hasArchSection = /^## Architecture/m.test(content);
|
|
976
|
+
if (!hasArchSection) {
|
|
977
|
+
return {
|
|
978
|
+
name: "Architecture map",
|
|
979
|
+
passed: false,
|
|
980
|
+
score: 0,
|
|
981
|
+
feedback: "Missing ## Architecture section with directory map."
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
const archSection = content.split(/^## /m).find((s) => s.startsWith("Architecture"));
|
|
985
|
+
const mentionedDirs = (archSection?.match(/`([^`/]+\/[^`]*)`/g) ?? []).map((m) => m.replace(/`/g, "").replace(/\/$/, ""));
|
|
986
|
+
const existCount = mentionedDirs.filter((d) => {
|
|
987
|
+
try {
|
|
988
|
+
return existsSync4(join4(cwd, d));
|
|
989
|
+
} catch {
|
|
990
|
+
return false;
|
|
991
|
+
}
|
|
992
|
+
}).length;
|
|
993
|
+
const accuracy = mentionedDirs.length > 0 ? existCount / mentionedDirs.length : 1;
|
|
994
|
+
const passed = accuracy >= 0.8 || mentionedDirs.length === 0;
|
|
995
|
+
return {
|
|
996
|
+
name: "Architecture matches actual structure",
|
|
997
|
+
passed,
|
|
998
|
+
score: passed ? 20 : 10,
|
|
999
|
+
feedback: passed ? "Architecture section looks accurate." : `${mentionedDirs.length - existCount} directories in Architecture section don't exist. Run \`contextmd sync\` to update.`
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
function checkConventions(content) {
|
|
1003
|
+
const hasSection = /^## Conventions/m.test(content);
|
|
1004
|
+
if (!hasSection) {
|
|
1005
|
+
return {
|
|
1006
|
+
name: "Conventions encode team decisions",
|
|
1007
|
+
passed: false,
|
|
1008
|
+
score: 0,
|
|
1009
|
+
feedback: "Missing ## Conventions section. Add at least one non-obvious team decision."
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
const section = content.split(/^## /m).find((s) => s.startsWith("Conventions"));
|
|
1013
|
+
const bulletCount = (section?.match(/^- /gm) ?? []).length;
|
|
1014
|
+
const passed = bulletCount >= 1;
|
|
1015
|
+
return {
|
|
1016
|
+
name: "Conventions encode team decisions",
|
|
1017
|
+
passed,
|
|
1018
|
+
score: passed ? 20 : 10,
|
|
1019
|
+
feedback: passed ? `${bulletCount} convention${bulletCount === 1 ? "" : "s"} documented.` : "Conventions section is empty. Add at least one non-obvious team decision."
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
function checkDoNot(content) {
|
|
1023
|
+
const hasSection = /^## Do NOT/m.test(content);
|
|
1024
|
+
if (!hasSection) {
|
|
1025
|
+
return {
|
|
1026
|
+
name: "Do NOT prevents architectural drift",
|
|
1027
|
+
passed: false,
|
|
1028
|
+
score: 0,
|
|
1029
|
+
feedback: "Missing ## Do NOT section. Add 2-3 explicit rejections of plausible alternatives."
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
const section = content.split(/^## /m).find((s) => s.startsWith("Do NOT"));
|
|
1033
|
+
const bulletCount = (section?.match(/^- /gm) ?? []).length;
|
|
1034
|
+
const passed = bulletCount >= 1;
|
|
1035
|
+
return {
|
|
1036
|
+
name: "Do NOT prevents architectural drift",
|
|
1037
|
+
passed,
|
|
1038
|
+
score: passed ? 20 : 10,
|
|
1039
|
+
feedback: passed ? `${bulletCount} explicit rejection${bulletCount === 1 ? "" : "s"} documented.` : "Do NOT section is empty. Add explicit rejections of alternatives the team has decided against."
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
function checkLength(lines) {
|
|
1043
|
+
const count = lines.length;
|
|
1044
|
+
const passed = count <= 200;
|
|
1045
|
+
let score;
|
|
1046
|
+
if (count <= 150) score = 20;
|
|
1047
|
+
else if (count <= 200) score = 15;
|
|
1048
|
+
else if (count <= 300) score = 10;
|
|
1049
|
+
else score = 0;
|
|
1050
|
+
return {
|
|
1051
|
+
name: "File under 200 lines",
|
|
1052
|
+
passed,
|
|
1053
|
+
score,
|
|
1054
|
+
feedback: passed ? `${count} lines \u2014 within the 200-line limit.` : `${count} lines \u2014 exceeds 200-line limit. Claude may treat it as low-signal. Trim or move detail to agent_docs/.`
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// src/index.ts
|
|
1059
|
+
var VERSION = "0.1.0";
|
|
1060
|
+
function write(filePath, content, overwrite = false) {
|
|
1061
|
+
if (!overwrite && existsSync5(filePath)) {
|
|
1062
|
+
console.log(` \u26A0 ${filePath} already exists \u2014 use --force to overwrite`);
|
|
1063
|
+
return false;
|
|
1064
|
+
}
|
|
1065
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
1066
|
+
writeFileSync(filePath, content, "utf-8");
|
|
1067
|
+
return true;
|
|
1068
|
+
}
|
|
1069
|
+
function lineCount(content) {
|
|
1070
|
+
return content.split("\n").length;
|
|
1071
|
+
}
|
|
1072
|
+
async function promptSubscribe() {
|
|
1073
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1074
|
+
return new Promise((resolve) => {
|
|
1075
|
+
rl.question(
|
|
1076
|
+
"\n\u{1F4EC} Get notified about updates? Enter email (or press Enter to skip): ",
|
|
1077
|
+
(email) => {
|
|
1078
|
+
rl.close();
|
|
1079
|
+
if (email && email.includes("@")) {
|
|
1080
|
+
console.log(` \u2713 Noted \u2014 ${email} will receive release notes`);
|
|
1081
|
+
}
|
|
1082
|
+
resolve();
|
|
1083
|
+
}
|
|
1084
|
+
);
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
program.name("contextmd").description("Generate and sync CLAUDE.md and AGENTS.md for your repo").version(VERSION);
|
|
1088
|
+
program.command("init", { isDefault: true }).description("Generate CLAUDE.md and AGENTS.md in the current directory").option("-f, --force", "overwrite existing files").option("--ruler", "also write .ruler/ files for ruler.js fan-out (27+ agent formats)").option("--no-agents", "skip AGENTS.md generation").option("--no-subscribe", "skip the email subscribe prompt").action(async (opts) => {
|
|
1089
|
+
const cwd = process.cwd();
|
|
1090
|
+
let spinner = null;
|
|
1091
|
+
try {
|
|
1092
|
+
const { default: ora } = await import("ora");
|
|
1093
|
+
spinner = ora("Scanning repository...").start();
|
|
1094
|
+
} catch {
|
|
1095
|
+
console.log("Scanning repository...");
|
|
1096
|
+
}
|
|
1097
|
+
let ctx;
|
|
1098
|
+
try {
|
|
1099
|
+
ctx = await scanRepo(cwd);
|
|
1100
|
+
if (spinner) spinner.stop();
|
|
1101
|
+
} catch (err) {
|
|
1102
|
+
if (spinner) spinner.stop();
|
|
1103
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1104
|
+
console.error(`
|
|
1105
|
+
\u2717 Scan failed: ${msg}`);
|
|
1106
|
+
process.exit(1);
|
|
1107
|
+
}
|
|
1108
|
+
const { default: chalk } = await import("chalk");
|
|
1109
|
+
console.log(`
|
|
1110
|
+
${chalk.bold(ctx.name)} \u2014 ${ctx.stack.language}${ctx.stack.framework ? ` / ${ctx.stack.framework}` : ""}`);
|
|
1111
|
+
console.log();
|
|
1112
|
+
const claudeContent = generateClaude(ctx);
|
|
1113
|
+
const claudePath = join5(cwd, "CLAUDE.md");
|
|
1114
|
+
const claudeWritten = write(claudePath, claudeContent, opts.force);
|
|
1115
|
+
if (claudeWritten) {
|
|
1116
|
+
console.log(` ${chalk.green("\u2713")} CLAUDE.md ${chalk.dim(`(${lineCount(claudeContent)} lines)`)}`);
|
|
1117
|
+
}
|
|
1118
|
+
if (opts.agents !== false) {
|
|
1119
|
+
const agentsContent = generateAgents(ctx);
|
|
1120
|
+
const agentsPath = join5(cwd, "AGENTS.md");
|
|
1121
|
+
const agentsWritten = write(agentsPath, agentsContent, opts.force);
|
|
1122
|
+
if (agentsWritten) {
|
|
1123
|
+
console.log(` ${chalk.green("\u2713")} AGENTS.md ${chalk.dim(`(${lineCount(agentsContent)} lines)`)}`);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
if (opts.ruler) {
|
|
1127
|
+
const rulerOutput = generateRulerFiles(ctx);
|
|
1128
|
+
for (const [relPath, content] of Object.entries(rulerOutput.files)) {
|
|
1129
|
+
const fullPath = join5(cwd, relPath);
|
|
1130
|
+
const written = write(fullPath, content, opts.force);
|
|
1131
|
+
if (written) {
|
|
1132
|
+
console.log(` ${chalk.green("\u2713")} ${relPath} ${chalk.dim("(ruler format)")}`);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
console.log();
|
|
1136
|
+
console.log(chalk.dim(" Run `ruler` to distribute rules to all your agent configs."));
|
|
1137
|
+
}
|
|
1138
|
+
console.log();
|
|
1139
|
+
console.log(chalk.dim(" Tip: commit these files \u2014 they improve every AI session on this repo."));
|
|
1140
|
+
console.log(chalk.dim(" Run `contextmd score` to audit the quality of your CLAUDE.md."));
|
|
1141
|
+
if (opts.subscribe !== false) {
|
|
1142
|
+
await promptSubscribe();
|
|
1143
|
+
}
|
|
1144
|
+
});
|
|
1145
|
+
program.command("sync").description("Regenerate CLAUDE.md and AGENTS.md from current repo state").option("--ruler", "also sync .ruler/ files").action(async (opts) => {
|
|
1146
|
+
const cwd = process.cwd();
|
|
1147
|
+
const hasClaudeFile = existsSync5(join5(cwd, "CLAUDE.md"));
|
|
1148
|
+
const hasAgentsFile = existsSync5(join5(cwd, "AGENTS.md"));
|
|
1149
|
+
if (!hasClaudeFile && !hasAgentsFile) {
|
|
1150
|
+
console.log("No CLAUDE.md or AGENTS.md found. Run `contextmd init` first.");
|
|
1151
|
+
process.exit(1);
|
|
1152
|
+
}
|
|
1153
|
+
console.log("Syncing...");
|
|
1154
|
+
const ctx = await scanRepo(cwd);
|
|
1155
|
+
const { default: chalk } = await import("chalk");
|
|
1156
|
+
if (hasClaudeFile) {
|
|
1157
|
+
const content = generateClaude(ctx);
|
|
1158
|
+
writeFileSync(join5(cwd, "CLAUDE.md"), content, "utf-8");
|
|
1159
|
+
console.log(` ${chalk.green("\u2713")} CLAUDE.md updated ${chalk.dim(`(${lineCount(content)} lines)`)}`);
|
|
1160
|
+
}
|
|
1161
|
+
if (hasAgentsFile) {
|
|
1162
|
+
const content = generateAgents(ctx);
|
|
1163
|
+
writeFileSync(join5(cwd, "AGENTS.md"), content, "utf-8");
|
|
1164
|
+
console.log(` ${chalk.green("\u2713")} AGENTS.md updated ${chalk.dim(`(${lineCount(content)} lines)`)}`);
|
|
1165
|
+
}
|
|
1166
|
+
if (opts.ruler) {
|
|
1167
|
+
const rulerOutput = generateRulerFiles(ctx);
|
|
1168
|
+
for (const [relPath, content] of Object.entries(rulerOutput.files)) {
|
|
1169
|
+
writeFileSync(join5(cwd, relPath), content, "utf-8");
|
|
1170
|
+
console.log(` ${chalk.green("\u2713")} ${relPath} updated`);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
});
|
|
1174
|
+
program.command("score").description("Audit the quality of your CLAUDE.md against the 5 commit-readiness tests").action(async () => {
|
|
1175
|
+
const cwd = process.cwd();
|
|
1176
|
+
const { default: chalk } = await import("chalk");
|
|
1177
|
+
const result = scoreClaudeFile(cwd);
|
|
1178
|
+
console.log(`
|
|
1179
|
+
CLAUDE.md quality score: ${chalk.bold(result.total)}/100
|
|
1180
|
+
`);
|
|
1181
|
+
for (const check of result.checks) {
|
|
1182
|
+
const icon = check.passed ? chalk.green("\u2713") : chalk.red("\u2717");
|
|
1183
|
+
const scoreStr = chalk.dim(`(${check.score} pts)`);
|
|
1184
|
+
console.log(` ${icon} ${check.name} ${scoreStr}`);
|
|
1185
|
+
if (!check.passed) {
|
|
1186
|
+
console.log(` ${chalk.dim(check.feedback)}`);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
console.log();
|
|
1190
|
+
if (result.total >= 80) {
|
|
1191
|
+
console.log(chalk.green(" Strong CLAUDE.md \u2014 this file is working hard for you."));
|
|
1192
|
+
} else if (result.total >= 60) {
|
|
1193
|
+
console.log(chalk.yellow(" Decent start \u2014 address the failing checks to improve agent accuracy."));
|
|
1194
|
+
} else {
|
|
1195
|
+
console.log(chalk.red(" High risk of Claude ignoring this file. Run `contextmd init --force` to regenerate."));
|
|
1196
|
+
}
|
|
1197
|
+
console.log();
|
|
1198
|
+
console.log(chalk.dim(` Share your score: "My CLAUDE.md scored ${result.total}/100 with contextmd"`));
|
|
1199
|
+
});
|
|
1200
|
+
program.parse();
|
|
1201
|
+
//# sourceMappingURL=index.js.map
|