codesight 1.3.2 → 1.4.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/dist/config.d.ts +16 -0
- package/dist/config.js +96 -0
- package/dist/detectors/schema.js +1 -1
- package/dist/eval.d.ts +5 -0
- package/dist/eval.js +184 -0
- package/dist/index.js +99 -15
- package/dist/telemetry.d.ts +38 -0
- package/dist/telemetry.js +257 -0
- package/dist/types.d.ts +35 -0
- package/eval/README.md +36 -0
- package/eval/fixtures/express-prisma/ground-truth.json +31 -0
- package/eval/fixtures/express-prisma/repo.json +19 -0
- package/eval/fixtures/fastapi-sqlalchemy/ground-truth.json +29 -0
- package/eval/fixtures/fastapi-sqlalchemy/repo.json +16 -0
- package/eval/fixtures/hono-monorepo/ground-truth.json +33 -0
- package/eval/fixtures/hono-monorepo/repo.json +20 -0
- package/eval/fixtures/nextjs-drizzle/ground-truth.json +37 -0
- package/eval/fixtures/nextjs-drizzle/repo.json +21 -0
- package/package.json +3 -2
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration loader: reads codesight.config.(ts|js|json) from project root.
|
|
3
|
+
*/
|
|
4
|
+
import type { CodesightConfig } from "./types.js";
|
|
5
|
+
/**
|
|
6
|
+
* Load config from project root. Returns empty config if no config file found.
|
|
7
|
+
*/
|
|
8
|
+
export declare function loadConfig(root: string): Promise<CodesightConfig>;
|
|
9
|
+
/**
|
|
10
|
+
* Merges CLI args with config file values (CLI takes precedence).
|
|
11
|
+
*/
|
|
12
|
+
export declare function mergeCliConfig(config: CodesightConfig, cli: {
|
|
13
|
+
maxDepth?: number;
|
|
14
|
+
outputDir?: string;
|
|
15
|
+
profile?: string;
|
|
16
|
+
}): CodesightConfig;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration loader: reads codesight.config.(ts|js|json) from project root.
|
|
3
|
+
*/
|
|
4
|
+
import { readFile, stat } from "node:fs/promises";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
const CONFIG_FILES = [
|
|
8
|
+
"codesight.config.ts",
|
|
9
|
+
"codesight.config.js",
|
|
10
|
+
"codesight.config.mjs",
|
|
11
|
+
"codesight.config.json",
|
|
12
|
+
];
|
|
13
|
+
async function fileExists(path) {
|
|
14
|
+
try {
|
|
15
|
+
await stat(path);
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Load config from project root. Returns empty config if no config file found.
|
|
24
|
+
*/
|
|
25
|
+
export async function loadConfig(root) {
|
|
26
|
+
for (const filename of CONFIG_FILES) {
|
|
27
|
+
const configPath = join(root, filename);
|
|
28
|
+
if (!(await fileExists(configPath)))
|
|
29
|
+
continue;
|
|
30
|
+
try {
|
|
31
|
+
if (filename.endsWith(".json")) {
|
|
32
|
+
const content = await readFile(configPath, "utf-8");
|
|
33
|
+
return JSON.parse(content);
|
|
34
|
+
}
|
|
35
|
+
if (filename.endsWith(".ts")) {
|
|
36
|
+
// Try loading with tsx or ts-node if available
|
|
37
|
+
return await loadTsConfig(configPath, root);
|
|
38
|
+
}
|
|
39
|
+
// JS/MJS — dynamic import
|
|
40
|
+
const module = await import(pathToFileURL(configPath).href);
|
|
41
|
+
return (module.default || module);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
console.warn(` Warning: failed to load ${filename}: ${err.message}`);
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Also check package.json "codesight" field
|
|
49
|
+
try {
|
|
50
|
+
const pkgPath = join(root, "package.json");
|
|
51
|
+
if (await fileExists(pkgPath)) {
|
|
52
|
+
const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
|
|
53
|
+
if (pkg.codesight && typeof pkg.codesight === "object") {
|
|
54
|
+
return pkg.codesight;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch { }
|
|
59
|
+
return {};
|
|
60
|
+
}
|
|
61
|
+
async function loadTsConfig(configPath, _root) {
|
|
62
|
+
// Strategy 1: try tsx via dynamic import of the .ts file directly
|
|
63
|
+
// (works if tsx or ts-node is installed)
|
|
64
|
+
try {
|
|
65
|
+
const module = await import(pathToFileURL(configPath).href);
|
|
66
|
+
return (module.default || module);
|
|
67
|
+
}
|
|
68
|
+
catch { }
|
|
69
|
+
// Strategy 2: read as text and extract JSON-like config
|
|
70
|
+
// (fallback for when no TS loader is available)
|
|
71
|
+
const content = await readFile(configPath, "utf-8");
|
|
72
|
+
// Try to extract the config object from simple export default { ... }
|
|
73
|
+
const match = content.match(/export\s+default\s+({[\s\S]*})\s*;?\s*$/m);
|
|
74
|
+
if (match) {
|
|
75
|
+
try {
|
|
76
|
+
// Use Function constructor to evaluate the object literal
|
|
77
|
+
// Safe here since this is user's own config file in their project
|
|
78
|
+
const fn = new Function(`return (${match[1]})`);
|
|
79
|
+
return fn();
|
|
80
|
+
}
|
|
81
|
+
catch { }
|
|
82
|
+
}
|
|
83
|
+
console.warn(` Warning: cannot load codesight.config.ts (install tsx for TS config support)`);
|
|
84
|
+
return {};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Merges CLI args with config file values (CLI takes precedence).
|
|
88
|
+
*/
|
|
89
|
+
export function mergeCliConfig(config, cli) {
|
|
90
|
+
return {
|
|
91
|
+
...config,
|
|
92
|
+
maxDepth: cli.maxDepth ?? config.maxDepth,
|
|
93
|
+
outputDir: cli.outputDir ?? config.outputDir,
|
|
94
|
+
profile: cli.profile ?? config.profile,
|
|
95
|
+
};
|
|
96
|
+
}
|
package/dist/detectors/schema.js
CHANGED
|
@@ -257,7 +257,7 @@ async function detectSQLAlchemySchemas(files, project) {
|
|
|
257
257
|
if (!content.includes("Base") && !content.includes("DeclarativeBase") && !content.includes("Model"))
|
|
258
258
|
continue;
|
|
259
259
|
// Match class definitions
|
|
260
|
-
const classPattern = /class\s+(\w+)\s*\([^)]*(?:Base|Model|DeclarativeBase)[^)]*\)\s*:([\s\S]*?)(?=\nclass\s|\n[^\s]
|
|
260
|
+
const classPattern = /class\s+(\w+)\s*\([^)]*(?:Base|Model|DeclarativeBase)[^)]*\)\s*:([\s\S]*?)(?=\nclass\s|\n[^\s]|$)/g;
|
|
261
261
|
let match;
|
|
262
262
|
while ((match = classPattern.exec(content)) !== null) {
|
|
263
263
|
const name = match[1];
|
package/dist/eval.d.ts
ADDED
package/dist/eval.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evaluation suite: runs codesight on fixture repos and measures
|
|
3
|
+
* precision, recall, and F1 against ground truth.
|
|
4
|
+
*/
|
|
5
|
+
import { readFile, writeFile, mkdir, rm } from "node:fs/promises";
|
|
6
|
+
import { join, dirname } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { collectFiles, detectProject } from "./scanner.js";
|
|
9
|
+
import { detectRoutes } from "./detectors/routes.js";
|
|
10
|
+
import { detectSchemas } from "./detectors/schema.js";
|
|
11
|
+
import { detectComponents } from "./detectors/components.js";
|
|
12
|
+
import { detectConfig } from "./detectors/config.js";
|
|
13
|
+
import { detectMiddleware } from "./detectors/middleware.js";
|
|
14
|
+
function calcMetrics(detected, expected) {
|
|
15
|
+
let tp = 0;
|
|
16
|
+
let fp = 0;
|
|
17
|
+
let fn = 0;
|
|
18
|
+
for (const item of detected) {
|
|
19
|
+
if (expected.has(item))
|
|
20
|
+
tp++;
|
|
21
|
+
else
|
|
22
|
+
fp++;
|
|
23
|
+
}
|
|
24
|
+
for (const item of expected) {
|
|
25
|
+
if (!detected.has(item))
|
|
26
|
+
fn++;
|
|
27
|
+
}
|
|
28
|
+
const precision = tp + fp > 0 ? tp / (tp + fp) : 1;
|
|
29
|
+
const recall = tp + fn > 0 ? tp / (tp + fn) : 1;
|
|
30
|
+
const f1 = precision + recall > 0 ? (2 * precision * recall) / (precision + recall) : 0;
|
|
31
|
+
return {
|
|
32
|
+
precision: Math.round(precision * 1000) / 1000,
|
|
33
|
+
recall: Math.round(recall * 1000) / 1000,
|
|
34
|
+
f1: Math.round(f1 * 1000) / 1000,
|
|
35
|
+
truePositives: tp,
|
|
36
|
+
falsePositives: fp,
|
|
37
|
+
falseNegatives: fn,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
async function createTempRepo(fixture) {
|
|
41
|
+
const tmpDir = join((await import("node:os")).tmpdir(), `codesight-eval-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
|
|
42
|
+
for (const [filePath, content] of Object.entries(fixture.files)) {
|
|
43
|
+
const fullPath = join(tmpDir, filePath);
|
|
44
|
+
await mkdir(dirname(fullPath), { recursive: true });
|
|
45
|
+
await writeFile(fullPath, content);
|
|
46
|
+
}
|
|
47
|
+
return tmpDir;
|
|
48
|
+
}
|
|
49
|
+
async function evalFixture(fixturePath) {
|
|
50
|
+
const repoJson = JSON.parse(await readFile(join(fixturePath, "repo.json"), "utf-8"));
|
|
51
|
+
const groundTruth = JSON.parse(await readFile(join(fixturePath, "ground-truth.json"), "utf-8"));
|
|
52
|
+
// Create temp repo from fixture
|
|
53
|
+
const tmpDir = await createTempRepo(repoJson);
|
|
54
|
+
const startTime = Date.now();
|
|
55
|
+
try {
|
|
56
|
+
// Run codesight detectors
|
|
57
|
+
const project = await detectProject(tmpDir);
|
|
58
|
+
const files = await collectFiles(tmpDir, 10);
|
|
59
|
+
const [routes, schemas, components, config, middleware] = await Promise.all([
|
|
60
|
+
detectRoutes(files, project),
|
|
61
|
+
detectSchemas(files, project),
|
|
62
|
+
detectComponents(files, project),
|
|
63
|
+
detectConfig(files, project),
|
|
64
|
+
detectMiddleware(files, project),
|
|
65
|
+
]);
|
|
66
|
+
const runtime = Date.now() - startTime;
|
|
67
|
+
// Compare routes: method:path
|
|
68
|
+
const detectedRoutes = new Set(routes.map((r) => `${r.method}:${r.path}`));
|
|
69
|
+
const expectedRoutes = new Set((groundTruth.routes || []).map((r) => `${r.method}:${r.path}`));
|
|
70
|
+
// Compare models: name
|
|
71
|
+
const detectedModels = new Set(schemas.map((s) => s.name.toLowerCase()));
|
|
72
|
+
const expectedModels = new Set((groundTruth.models || []).map((m) => m.name.toLowerCase()));
|
|
73
|
+
// Compare env vars
|
|
74
|
+
const detectedEnvVars = new Set(config.envVars.map((e) => e.name));
|
|
75
|
+
const expectedEnvVars = new Set(groundTruth.envVars || []);
|
|
76
|
+
const result = {
|
|
77
|
+
name: repoJson.name,
|
|
78
|
+
routes: calcMetrics(detectedRoutes, expectedRoutes),
|
|
79
|
+
models: calcMetrics(detectedModels, expectedModels),
|
|
80
|
+
envVars: calcMetrics(detectedEnvVars, expectedEnvVars),
|
|
81
|
+
runtime,
|
|
82
|
+
};
|
|
83
|
+
// Components (if ground truth has them)
|
|
84
|
+
if (groundTruth.components && groundTruth.components.length > 0) {
|
|
85
|
+
const detectedComps = new Set(components.map((c) => c.name));
|
|
86
|
+
const expectedComps = new Set(groundTruth.components.map((c) => c.name));
|
|
87
|
+
result.components = calcMetrics(detectedComps, expectedComps);
|
|
88
|
+
}
|
|
89
|
+
// Middleware
|
|
90
|
+
if (groundTruth.middleware && groundTruth.middleware.length > 0) {
|
|
91
|
+
const detectedMw = new Set(middleware.map((m) => m.name));
|
|
92
|
+
const expectedMw = new Set(groundTruth.middleware);
|
|
93
|
+
result.middleware = calcMetrics(detectedMw, expectedMw);
|
|
94
|
+
}
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
// Cleanup temp dir
|
|
99
|
+
await rm(tmpDir, { recursive: true, force: true }).catch(() => { });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function formatPercent(n) {
|
|
103
|
+
return `${(n * 100).toFixed(1)}%`;
|
|
104
|
+
}
|
|
105
|
+
function printMetrics(label, m) {
|
|
106
|
+
console.log(` ${label.padEnd(14)} P: ${formatPercent(m.precision).padStart(6)} R: ${formatPercent(m.recall).padStart(6)} F1: ${formatPercent(m.f1).padStart(6)} (TP:${m.truePositives} FP:${m.falsePositives} FN:${m.falseNegatives})`);
|
|
107
|
+
}
|
|
108
|
+
export async function runEval() {
|
|
109
|
+
// Find eval fixtures
|
|
110
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
111
|
+
const evalDir = join(__dirname, "..", "eval", "fixtures");
|
|
112
|
+
let fixtureNames;
|
|
113
|
+
try {
|
|
114
|
+
const { readdir } = await import("node:fs/promises");
|
|
115
|
+
fixtureNames = await readdir(evalDir);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// Try from dist path
|
|
119
|
+
const altDir = join(__dirname, "..", "..", "eval", "fixtures");
|
|
120
|
+
const { readdir } = await import("node:fs/promises");
|
|
121
|
+
fixtureNames = await readdir(altDir);
|
|
122
|
+
// Override evalDir for the loop below
|
|
123
|
+
return runEvalFromDir(altDir, fixtureNames);
|
|
124
|
+
}
|
|
125
|
+
return runEvalFromDir(evalDir, fixtureNames);
|
|
126
|
+
}
|
|
127
|
+
async function runEvalFromDir(evalDir, fixtureNames) {
|
|
128
|
+
console.log(`\n codesight eval — precision/recall benchmarks\n`);
|
|
129
|
+
const results = [];
|
|
130
|
+
let totalPrecision = 0;
|
|
131
|
+
let totalRecall = 0;
|
|
132
|
+
let totalF1 = 0;
|
|
133
|
+
let metricCount = 0;
|
|
134
|
+
for (const name of fixtureNames) {
|
|
135
|
+
const fixturePath = join(evalDir, name);
|
|
136
|
+
// Check if it has repo.json
|
|
137
|
+
try {
|
|
138
|
+
await import("node:fs/promises").then((fs) => fs.stat(join(fixturePath, "repo.json")));
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
process.stdout.write(` ${name}...`);
|
|
144
|
+
const result = await evalFixture(fixturePath);
|
|
145
|
+
results.push(result);
|
|
146
|
+
console.log(` ${result.runtime}ms`);
|
|
147
|
+
printMetrics("Routes", result.routes);
|
|
148
|
+
printMetrics("Models", result.models);
|
|
149
|
+
printMetrics("Env vars", result.envVars);
|
|
150
|
+
if (result.components)
|
|
151
|
+
printMetrics("Components", result.components);
|
|
152
|
+
if (result.middleware)
|
|
153
|
+
printMetrics("Middleware", result.middleware);
|
|
154
|
+
console.log("");
|
|
155
|
+
// Accumulate for averages
|
|
156
|
+
const metrics = [result.routes, result.models, result.envVars];
|
|
157
|
+
if (result.components)
|
|
158
|
+
metrics.push(result.components);
|
|
159
|
+
if (result.middleware)
|
|
160
|
+
metrics.push(result.middleware);
|
|
161
|
+
for (const m of metrics) {
|
|
162
|
+
totalPrecision += m.precision;
|
|
163
|
+
totalRecall += m.recall;
|
|
164
|
+
totalF1 += m.f1;
|
|
165
|
+
metricCount++;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (results.length === 0) {
|
|
169
|
+
console.log(" No fixtures found. Add fixtures to eval/fixtures/");
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
// Summary
|
|
173
|
+
const avgP = totalPrecision / metricCount;
|
|
174
|
+
const avgR = totalRecall / metricCount;
|
|
175
|
+
const avgF1 = totalF1 / metricCount;
|
|
176
|
+
const totalRuntime = results.reduce((s, r) => s + r.runtime, 0);
|
|
177
|
+
console.log(" ──────────────────────────────────────────");
|
|
178
|
+
console.log(` Fixtures: ${results.length}`);
|
|
179
|
+
console.log(` Avg precision: ${formatPercent(avgP)}`);
|
|
180
|
+
console.log(` Avg recall: ${formatPercent(avgR)}`);
|
|
181
|
+
console.log(` Avg F1: ${formatPercent(avgF1)}`);
|
|
182
|
+
console.log(` Total runtime: ${totalRuntime}ms`);
|
|
183
|
+
console.log("");
|
|
184
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,8 @@ import { calculateTokenStats } from "./detectors/tokens.js";
|
|
|
14
14
|
import { writeOutput } from "./formatter.js";
|
|
15
15
|
import { generateAIConfigs } from "./generators/ai-config.js";
|
|
16
16
|
import { generateHtmlReport } from "./generators/html-report.js";
|
|
17
|
-
|
|
17
|
+
import { loadConfig, mergeCliConfig } from "./config.js";
|
|
18
|
+
const VERSION = "1.4.0";
|
|
18
19
|
const BRAND = "codesight";
|
|
19
20
|
function printHelp() {
|
|
20
21
|
console.log(`
|
|
@@ -35,9 +36,15 @@ function printHelp() {
|
|
|
35
36
|
--benchmark Show detailed token savings breakdown
|
|
36
37
|
--profile <tool> Generate optimized config (claude-code|cursor|codex|copilot|windsurf)
|
|
37
38
|
--blast <file> Show blast radius for a file
|
|
39
|
+
--telemetry Run token telemetry (real before/after measurement)
|
|
40
|
+
--eval Run precision/recall benchmarks on eval fixtures
|
|
38
41
|
-v, --version Show version
|
|
39
42
|
-h, --help Show this help
|
|
40
43
|
|
|
44
|
+
Config:
|
|
45
|
+
Reads codesight.config.(ts|js|json) or package.json "codesight" field.
|
|
46
|
+
See docs for disableDetectors, customRoutePatterns, plugins, and more.
|
|
47
|
+
|
|
41
48
|
Examples:
|
|
42
49
|
npx ${BRAND} # Scan current directory
|
|
43
50
|
npx ${BRAND} --init # Scan + generate AI config files
|
|
@@ -45,6 +52,8 @@ function printHelp() {
|
|
|
45
52
|
npx ${BRAND} --watch # Watch mode, re-scan on changes
|
|
46
53
|
npx ${BRAND} --mcp # Start MCP server
|
|
47
54
|
npx ${BRAND} --hook # Install git pre-commit hook
|
|
55
|
+
npx ${BRAND} --telemetry # Measure real token savings
|
|
56
|
+
npx ${BRAND} --eval # Run accuracy benchmarks
|
|
48
57
|
npx ${BRAND} ./my-project # Scan specific directory
|
|
49
58
|
`);
|
|
50
59
|
}
|
|
@@ -57,7 +66,7 @@ async function fileExists(path) {
|
|
|
57
66
|
return false;
|
|
58
67
|
}
|
|
59
68
|
}
|
|
60
|
-
async function scan(root, outputDirName, maxDepth) {
|
|
69
|
+
async function scan(root, outputDirName, maxDepth, userConfig = {}) {
|
|
61
70
|
const outputDir = join(root, outputDirName);
|
|
62
71
|
console.log(`\n ${BRAND} v${VERSION}`);
|
|
63
72
|
console.log(` Scanning: ${root}\n`);
|
|
@@ -73,17 +82,39 @@ async function scan(root, outputDirName, maxDepth) {
|
|
|
73
82
|
process.stdout.write(" Collecting files...");
|
|
74
83
|
const files = await collectFiles(root, maxDepth);
|
|
75
84
|
console.log(` ${files.length} files`);
|
|
76
|
-
// Step 3: Run all detectors in parallel
|
|
85
|
+
// Step 3: Run all detectors in parallel (respecting disableDetectors config)
|
|
77
86
|
process.stdout.write(" Analyzing...");
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
87
|
+
const disabled = new Set(userConfig.disableDetectors || []);
|
|
88
|
+
const [rawRoutes, schemas, components, libs, configResult, middleware, graph] = await Promise.all([
|
|
89
|
+
disabled.has("routes") ? Promise.resolve([]) : detectRoutes(files, project),
|
|
90
|
+
disabled.has("schema") ? Promise.resolve([]) : detectSchemas(files, project),
|
|
91
|
+
disabled.has("components") ? Promise.resolve([]) : detectComponents(files, project),
|
|
92
|
+
disabled.has("libs") ? Promise.resolve([]) : detectLibs(files, project),
|
|
93
|
+
disabled.has("config") ? Promise.resolve({ envVars: [], configFiles: [], dependencies: {}, devDependencies: {} }) : detectConfig(files, project),
|
|
94
|
+
disabled.has("middleware") ? Promise.resolve([]) : detectMiddleware(files, project),
|
|
95
|
+
disabled.has("graph") ? Promise.resolve({ edges: [], hotFiles: [] }) : detectDependencyGraph(files, project),
|
|
86
96
|
]);
|
|
97
|
+
// Step 3b: Run plugin detectors
|
|
98
|
+
if (userConfig.plugins) {
|
|
99
|
+
for (const plugin of userConfig.plugins) {
|
|
100
|
+
if (plugin.detector) {
|
|
101
|
+
try {
|
|
102
|
+
const pluginResult = await plugin.detector(files, project);
|
|
103
|
+
if (pluginResult.routes)
|
|
104
|
+
rawRoutes.push(...pluginResult.routes);
|
|
105
|
+
if (pluginResult.schemas)
|
|
106
|
+
schemas.push(...pluginResult.schemas);
|
|
107
|
+
if (pluginResult.components)
|
|
108
|
+
components.push(...pluginResult.components);
|
|
109
|
+
if (pluginResult.middleware)
|
|
110
|
+
middleware.push(...pluginResult.middleware);
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
console.warn(`\n Warning: plugin "${plugin.name}" failed: ${err.message}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
87
118
|
// Step 4: Enrich routes with contract info
|
|
88
119
|
const routes = await enrichRouteContracts(rawRoutes, project);
|
|
89
120
|
// Report AST vs regex detection
|
|
@@ -106,7 +137,7 @@ async function scan(root, outputDirName, maxDepth) {
|
|
|
106
137
|
schemas,
|
|
107
138
|
components,
|
|
108
139
|
libs,
|
|
109
|
-
config,
|
|
140
|
+
config: configResult,
|
|
110
141
|
middleware,
|
|
111
142
|
graph,
|
|
112
143
|
tokenStats: { outputTokens: 0, estimatedExplorationTokens: 0, saved: 0, fileCount: files.length },
|
|
@@ -126,7 +157,7 @@ async function scan(root, outputDirName, maxDepth) {
|
|
|
126
157
|
Models: ${schemas.length}
|
|
127
158
|
Components: ${components.length}
|
|
128
159
|
Libraries: ${libs.length}
|
|
129
|
-
Env vars: ${
|
|
160
|
+
Env vars: ${configResult.envVars.length}
|
|
130
161
|
Middleware: ${middleware.length}
|
|
131
162
|
Import links: ${graph.edges.length}
|
|
132
163
|
Hot files: ${graph.hotFiles.length}
|
|
@@ -234,6 +265,8 @@ async function main() {
|
|
|
234
265
|
let doBenchmark = false;
|
|
235
266
|
let doProfile = "";
|
|
236
267
|
let doBlast = "";
|
|
268
|
+
let doTelemetry = false;
|
|
269
|
+
let doEval = false;
|
|
237
270
|
for (let i = 0; i < args.length; i++) {
|
|
238
271
|
const arg = args[i];
|
|
239
272
|
if ((arg === "-o" || arg === "--output") && args[i + 1]) {
|
|
@@ -273,6 +306,12 @@ async function main() {
|
|
|
273
306
|
else if (arg === "--blast" && args[i + 1]) {
|
|
274
307
|
doBlast = args[++i];
|
|
275
308
|
}
|
|
309
|
+
else if (arg === "--telemetry") {
|
|
310
|
+
doTelemetry = true;
|
|
311
|
+
}
|
|
312
|
+
else if (arg === "--eval") {
|
|
313
|
+
doEval = true;
|
|
314
|
+
}
|
|
276
315
|
else if (!arg.startsWith("-")) {
|
|
277
316
|
targetDir = resolve(arg);
|
|
278
317
|
}
|
|
@@ -283,13 +322,58 @@ async function main() {
|
|
|
283
322
|
await startMCPServer();
|
|
284
323
|
return;
|
|
285
324
|
}
|
|
325
|
+
// Eval mode (standalone, no scan needed)
|
|
326
|
+
if (doEval) {
|
|
327
|
+
const { runEval } = await import("./eval.js");
|
|
328
|
+
await runEval();
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
286
331
|
const root = resolve(targetDir);
|
|
332
|
+
// Load config file
|
|
333
|
+
const fileConfig = await loadConfig(root);
|
|
334
|
+
const config = mergeCliConfig(fileConfig, {
|
|
335
|
+
maxDepth: maxDepth !== 10 ? maxDepth : undefined,
|
|
336
|
+
outputDir: outputDirName !== ".codesight" ? outputDirName : undefined,
|
|
337
|
+
profile: doProfile || undefined,
|
|
338
|
+
});
|
|
339
|
+
// Apply config overrides
|
|
340
|
+
if (config.maxDepth)
|
|
341
|
+
maxDepth = config.maxDepth;
|
|
342
|
+
if (config.outputDir)
|
|
343
|
+
outputDirName = config.outputDir;
|
|
287
344
|
// Install git hook
|
|
288
345
|
if (doHook) {
|
|
289
346
|
await installGitHook(root, outputDirName);
|
|
290
347
|
}
|
|
291
|
-
// Run scan
|
|
292
|
-
|
|
348
|
+
// Run scan (passes config for disabled detectors + plugins)
|
|
349
|
+
let result = await scan(root, outputDirName, maxDepth, config);
|
|
350
|
+
// Run plugin post-processors
|
|
351
|
+
if (config.plugins) {
|
|
352
|
+
for (const plugin of config.plugins) {
|
|
353
|
+
if (plugin.postProcessor) {
|
|
354
|
+
try {
|
|
355
|
+
result = await plugin.postProcessor(result);
|
|
356
|
+
}
|
|
357
|
+
catch (err) {
|
|
358
|
+
console.warn(` Warning: plugin "${plugin.name}" post-processor failed: ${err.message}`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
// Token telemetry
|
|
364
|
+
if (doTelemetry) {
|
|
365
|
+
const { runTelemetry } = await import("./telemetry.js");
|
|
366
|
+
const outputDir = join(root, outputDirName);
|
|
367
|
+
process.stdout.write(" Running telemetry...");
|
|
368
|
+
const report = await runTelemetry(root, result, outputDir);
|
|
369
|
+
console.log(` ${outputDirName}/telemetry.md`);
|
|
370
|
+
console.log(`\n Telemetry Results:`);
|
|
371
|
+
for (const task of report.tasks) {
|
|
372
|
+
console.log(` ${task.name}: ${task.reduction}x reduction (${task.tokensWithout.toLocaleString()} → ${task.tokensWith.toLocaleString()} tokens)`);
|
|
373
|
+
}
|
|
374
|
+
console.log(` Average: ${report.summary.averageReduction}x | Tool calls saved: ${report.summary.totalToolCallsSaved}`);
|
|
375
|
+
console.log("");
|
|
376
|
+
}
|
|
293
377
|
// JSON output
|
|
294
378
|
if (jsonOutput) {
|
|
295
379
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token telemetry: measures real before/after token usage by simulating
|
|
3
|
+
* what an AI agent would do with and without codesight context.
|
|
4
|
+
*
|
|
5
|
+
* Approach: for each standard task (explain architecture, add route, review diff),
|
|
6
|
+
* measure the actual bytes of context that would be consumed.
|
|
7
|
+
*
|
|
8
|
+
* "Without codesight": count tokens from the files an AI would need to read
|
|
9
|
+
* to discover routes, schema, components, config, etc.
|
|
10
|
+
*
|
|
11
|
+
* "With codesight": count tokens from the CODESIGHT.md output.
|
|
12
|
+
*/
|
|
13
|
+
import type { ScanResult } from "./types.js";
|
|
14
|
+
export interface TelemetryTask {
|
|
15
|
+
name: string;
|
|
16
|
+
description: string;
|
|
17
|
+
/** Files the AI would need to read without codesight */
|
|
18
|
+
filesRead: string[];
|
|
19
|
+
/** Tool calls the AI would make (glob, grep, read) */
|
|
20
|
+
toolCalls: number;
|
|
21
|
+
/** Tokens consumed reading those files */
|
|
22
|
+
tokensWithout: number;
|
|
23
|
+
/** Tokens consumed from codesight output */
|
|
24
|
+
tokensWith: number;
|
|
25
|
+
/** Reduction factor */
|
|
26
|
+
reduction: number;
|
|
27
|
+
}
|
|
28
|
+
export interface TelemetryReport {
|
|
29
|
+
project: string;
|
|
30
|
+
tasks: TelemetryTask[];
|
|
31
|
+
summary: {
|
|
32
|
+
totalTokensWithout: number;
|
|
33
|
+
totalTokensWith: number;
|
|
34
|
+
averageReduction: number;
|
|
35
|
+
totalToolCallsSaved: number;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export declare function runTelemetry(root: string, result: ScanResult, outputDir: string): Promise<TelemetryReport>;
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token telemetry: measures real before/after token usage by simulating
|
|
3
|
+
* what an AI agent would do with and without codesight context.
|
|
4
|
+
*
|
|
5
|
+
* Approach: for each standard task (explain architecture, add route, review diff),
|
|
6
|
+
* measure the actual bytes of context that would be consumed.
|
|
7
|
+
*
|
|
8
|
+
* "Without codesight": count tokens from the files an AI would need to read
|
|
9
|
+
* to discover routes, schema, components, config, etc.
|
|
10
|
+
*
|
|
11
|
+
* "With codesight": count tokens from the CODESIGHT.md output.
|
|
12
|
+
*/
|
|
13
|
+
import { readFile } from "node:fs/promises";
|
|
14
|
+
import { join, relative } from "node:path";
|
|
15
|
+
function countTokens(text) {
|
|
16
|
+
return Math.ceil(text.length / 4);
|
|
17
|
+
}
|
|
18
|
+
async function readFileSafe(path) {
|
|
19
|
+
try {
|
|
20
|
+
return await readFile(path, "utf-8");
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Task 1: "Explain the architecture"
|
|
28
|
+
* Without codesight: AI reads package.json, scans dirs, reads route files,
|
|
29
|
+
* schema files, config files, middleware files — typically 15-25 file reads.
|
|
30
|
+
*/
|
|
31
|
+
async function measureExplainArchitecture(root, result, codesightTokens) {
|
|
32
|
+
const filesToRead = new Set();
|
|
33
|
+
// AI would read package.json first
|
|
34
|
+
filesToRead.add(join(root, "package.json"));
|
|
35
|
+
// Then scan for route files
|
|
36
|
+
for (const route of result.routes) {
|
|
37
|
+
filesToRead.add(join(root, route.file));
|
|
38
|
+
}
|
|
39
|
+
// Schema files
|
|
40
|
+
for (const _schema of result.schemas) {
|
|
41
|
+
// Find the file containing this schema from routes or libs
|
|
42
|
+
for (const lib of result.libs) {
|
|
43
|
+
if (lib.file.includes("schema") || lib.file.includes("model") || lib.file.includes("db")) {
|
|
44
|
+
filesToRead.add(join(root, lib.file));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Config files
|
|
49
|
+
for (const cf of result.config.configFiles) {
|
|
50
|
+
filesToRead.add(join(root, cf));
|
|
51
|
+
}
|
|
52
|
+
// Middleware files
|
|
53
|
+
for (const mw of result.middleware) {
|
|
54
|
+
filesToRead.add(join(root, mw.file));
|
|
55
|
+
}
|
|
56
|
+
// Hot files (AI would discover these during exploration)
|
|
57
|
+
for (const hf of result.graph.hotFiles.slice(0, 10)) {
|
|
58
|
+
filesToRead.add(join(root, hf.file));
|
|
59
|
+
}
|
|
60
|
+
// Read all files and count tokens
|
|
61
|
+
let totalTokens = 0;
|
|
62
|
+
const readFiles = [];
|
|
63
|
+
for (const f of filesToRead) {
|
|
64
|
+
const content = await readFileSafe(f);
|
|
65
|
+
if (content) {
|
|
66
|
+
totalTokens += countTokens(content);
|
|
67
|
+
readFiles.push(relative(root, f));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Add overhead for glob/grep tool calls (each costs ~50-100 tokens for command + results)
|
|
71
|
+
const toolCalls = Math.max(10, Math.ceil(filesToRead.size * 0.8));
|
|
72
|
+
totalTokens += toolCalls * 75; // average 75 tokens per tool call overhead
|
|
73
|
+
const reduction = totalTokens > 0 ? Math.round((totalTokens / codesightTokens) * 10) / 10 : 1;
|
|
74
|
+
return {
|
|
75
|
+
name: "Explain architecture",
|
|
76
|
+
description: "Understand project stack, routes, schema, and dependencies",
|
|
77
|
+
filesRead: readFiles,
|
|
78
|
+
toolCalls,
|
|
79
|
+
tokensWithout: totalTokens,
|
|
80
|
+
tokensWith: codesightTokens,
|
|
81
|
+
reduction,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Task 2: "Add a new API route"
|
|
86
|
+
* Without codesight: AI needs to find existing routes to match patterns,
|
|
87
|
+
* read schema for related models, check middleware, check config.
|
|
88
|
+
*/
|
|
89
|
+
async function measureAddRoute(root, result, codesightTokens) {
|
|
90
|
+
const filesToRead = new Set();
|
|
91
|
+
// AI would grep for existing route patterns — reads 3-5 route files
|
|
92
|
+
const routeFiles = [...new Set(result.routes.map((r) => r.file))];
|
|
93
|
+
for (const f of routeFiles.slice(0, 5)) {
|
|
94
|
+
filesToRead.add(join(root, f));
|
|
95
|
+
}
|
|
96
|
+
// Read schema to understand models
|
|
97
|
+
for (const lib of result.libs) {
|
|
98
|
+
if (lib.file.includes("schema") || lib.file.includes("model") || lib.file.includes("db")) {
|
|
99
|
+
filesToRead.add(join(root, lib.file));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Check middleware to know what to apply
|
|
103
|
+
for (const mw of result.middleware) {
|
|
104
|
+
filesToRead.add(join(root, mw.file));
|
|
105
|
+
}
|
|
106
|
+
let totalTokens = 0;
|
|
107
|
+
const readFiles = [];
|
|
108
|
+
for (const f of filesToRead) {
|
|
109
|
+
const content = await readFileSafe(f);
|
|
110
|
+
if (content) {
|
|
111
|
+
totalTokens += countTokens(content);
|
|
112
|
+
readFiles.push(relative(root, f));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const toolCalls = Math.max(6, Math.ceil(filesToRead.size * 0.7));
|
|
116
|
+
totalTokens += toolCalls * 75;
|
|
117
|
+
// With codesight, AI only reads the routes + schema sections (~40% of output)
|
|
118
|
+
const withTokens = Math.ceil(codesightTokens * 0.4);
|
|
119
|
+
const reduction = totalTokens > 0 ? Math.round((totalTokens / withTokens) * 10) / 10 : 1;
|
|
120
|
+
return {
|
|
121
|
+
name: "Add new API route",
|
|
122
|
+
description: "Find route patterns, check schema, apply middleware",
|
|
123
|
+
filesRead: readFiles,
|
|
124
|
+
toolCalls,
|
|
125
|
+
tokensWithout: totalTokens,
|
|
126
|
+
tokensWith: withTokens,
|
|
127
|
+
reduction,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Task 3: "Review a diff / understand blast radius"
|
|
132
|
+
* Without codesight: AI needs to trace imports, find dependents, check what routes
|
|
133
|
+
* and models are affected by a file change.
|
|
134
|
+
*/
|
|
135
|
+
async function measureReviewDiff(root, result, codesightTokens) {
|
|
136
|
+
const filesToRead = new Set();
|
|
137
|
+
// AI would read the changed file + all its importers
|
|
138
|
+
// Simulate: pick the hottest file and trace its dependents
|
|
139
|
+
if (result.graph.hotFiles.length > 0) {
|
|
140
|
+
const hotFile = result.graph.hotFiles[0];
|
|
141
|
+
filesToRead.add(join(root, hotFile.file));
|
|
142
|
+
// Read files that import it
|
|
143
|
+
for (const edge of result.graph.edges) {
|
|
144
|
+
if (edge.to === hotFile.file) {
|
|
145
|
+
filesToRead.add(join(root, edge.from));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Also read some route files to check impact
|
|
150
|
+
const routeFiles = [...new Set(result.routes.map((r) => r.file))];
|
|
151
|
+
for (const f of routeFiles.slice(0, 3)) {
|
|
152
|
+
filesToRead.add(join(root, f));
|
|
153
|
+
}
|
|
154
|
+
let totalTokens = 0;
|
|
155
|
+
const readFiles = [];
|
|
156
|
+
for (const f of filesToRead) {
|
|
157
|
+
const content = await readFileSafe(f);
|
|
158
|
+
if (content) {
|
|
159
|
+
totalTokens += countTokens(content);
|
|
160
|
+
readFiles.push(relative(root, f));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const toolCalls = Math.max(8, Math.ceil(filesToRead.size * 0.6));
|
|
164
|
+
totalTokens += toolCalls * 75;
|
|
165
|
+
// With codesight, AI reads graph section + routes (~50% of output)
|
|
166
|
+
const withTokens = Math.ceil(codesightTokens * 0.5);
|
|
167
|
+
const reduction = totalTokens > 0 ? Math.round((totalTokens / withTokens) * 10) / 10 : 1;
|
|
168
|
+
return {
|
|
169
|
+
name: "Review diff / blast radius",
|
|
170
|
+
description: "Trace imports, find affected routes and models",
|
|
171
|
+
filesRead: readFiles,
|
|
172
|
+
toolCalls,
|
|
173
|
+
tokensWithout: totalTokens,
|
|
174
|
+
tokensWith: withTokens,
|
|
175
|
+
reduction,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
export async function runTelemetry(root, result, outputDir) {
|
|
179
|
+
// Read the codesight output to get real token count
|
|
180
|
+
const codesightContent = await readFileSafe(join(outputDir, "CODESIGHT.md"));
|
|
181
|
+
const codesightTokens = countTokens(codesightContent);
|
|
182
|
+
const tasks = await Promise.all([
|
|
183
|
+
measureExplainArchitecture(root, result, codesightTokens),
|
|
184
|
+
measureAddRoute(root, result, codesightTokens),
|
|
185
|
+
measureReviewDiff(root, result, codesightTokens),
|
|
186
|
+
]);
|
|
187
|
+
const totalWithout = tasks.reduce((s, t) => s + t.tokensWithout, 0);
|
|
188
|
+
const totalWith = tasks.reduce((s, t) => s + t.tokensWith, 0);
|
|
189
|
+
const totalToolCalls = tasks.reduce((s, t) => s + t.toolCalls, 0);
|
|
190
|
+
const report = {
|
|
191
|
+
project: result.project.name,
|
|
192
|
+
tasks,
|
|
193
|
+
summary: {
|
|
194
|
+
totalTokensWithout: totalWithout,
|
|
195
|
+
totalTokensWith: totalWith,
|
|
196
|
+
averageReduction: totalWith > 0 ? Math.round((totalWithout / totalWith) * 10) / 10 : 1,
|
|
197
|
+
totalToolCallsSaved: totalToolCalls,
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
// Write telemetry report
|
|
201
|
+
const reportLines = [
|
|
202
|
+
`# Token Telemetry: ${result.project.name}`,
|
|
203
|
+
"",
|
|
204
|
+
`> Measured by reading the actual files an AI agent would need for each task,`,
|
|
205
|
+
`> then comparing against the codesight output (~${codesightTokens.toLocaleString()} tokens).`,
|
|
206
|
+
"",
|
|
207
|
+
"## Tasks",
|
|
208
|
+
"",
|
|
209
|
+
];
|
|
210
|
+
for (const task of tasks) {
|
|
211
|
+
reportLines.push(`### ${task.name}`);
|
|
212
|
+
reportLines.push(`_${task.description}_`);
|
|
213
|
+
reportLines.push("");
|
|
214
|
+
reportLines.push(`| Metric | Value |`);
|
|
215
|
+
reportLines.push(`|---|---|`);
|
|
216
|
+
reportLines.push(`| Files AI would read | ${task.filesRead.length} |`);
|
|
217
|
+
reportLines.push(`| Tool calls (glob/grep/read) | ${task.toolCalls} |`);
|
|
218
|
+
reportLines.push(`| Tokens without codesight | ~${task.tokensWithout.toLocaleString()} |`);
|
|
219
|
+
reportLines.push(`| Tokens with codesight | ~${task.tokensWith.toLocaleString()} |`);
|
|
220
|
+
reportLines.push(`| **Reduction** | **${task.reduction}x** |`);
|
|
221
|
+
reportLines.push("");
|
|
222
|
+
if (task.filesRead.length > 0) {
|
|
223
|
+
reportLines.push("<details>");
|
|
224
|
+
reportLines.push(`<summary>Files read (${task.filesRead.length})</summary>`);
|
|
225
|
+
reportLines.push("");
|
|
226
|
+
for (const f of task.filesRead) {
|
|
227
|
+
reportLines.push(`- \`${f}\``);
|
|
228
|
+
}
|
|
229
|
+
reportLines.push("");
|
|
230
|
+
reportLines.push("</details>");
|
|
231
|
+
reportLines.push("");
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
reportLines.push("## Summary");
|
|
235
|
+
reportLines.push("");
|
|
236
|
+
reportLines.push(`| Metric | Value |`);
|
|
237
|
+
reportLines.push(`|---|---|`);
|
|
238
|
+
reportLines.push(`| Total tokens without codesight | ~${report.summary.totalTokensWithout.toLocaleString()} |`);
|
|
239
|
+
reportLines.push(`| Total tokens with codesight | ~${report.summary.totalTokensWith.toLocaleString()} |`);
|
|
240
|
+
reportLines.push(`| **Average reduction** | **${report.summary.averageReduction}x** |`);
|
|
241
|
+
reportLines.push(`| Tool calls saved | ${report.summary.totalToolCallsSaved} |`);
|
|
242
|
+
reportLines.push("");
|
|
243
|
+
reportLines.push("## Methodology");
|
|
244
|
+
reportLines.push("");
|
|
245
|
+
reportLines.push("Token counts are calculated by reading the actual source files an AI agent would");
|
|
246
|
+
reportLines.push("need to explore for each task, using the ~4 chars/token heuristic (standard for");
|
|
247
|
+
reportLines.push("GPT/Claude tokenizers). Tool call overhead is estimated at ~75 tokens per call");
|
|
248
|
+
reportLines.push("(command text + result formatting). The \"with codesight\" count uses the real");
|
|
249
|
+
reportLines.push("CODESIGHT.md output size, proportioned to the sections relevant to each task.");
|
|
250
|
+
reportLines.push("");
|
|
251
|
+
reportLines.push(`_Generated by codesight --telemetry_`);
|
|
252
|
+
const { writeFile: wf } = await import("node:fs/promises");
|
|
253
|
+
const { mkdir } = await import("node:fs/promises");
|
|
254
|
+
await mkdir(outputDir, { recursive: true });
|
|
255
|
+
await wf(join(outputDir, "telemetry.md"), reportLines.join("\n"));
|
|
256
|
+
return report;
|
|
257
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -95,12 +95,47 @@ export interface BlastRadiusResult {
|
|
|
95
95
|
depth: number;
|
|
96
96
|
}
|
|
97
97
|
export interface CodesightConfig {
|
|
98
|
+
/** Disable specific detectors: "routes", "schema", "components", "libs", "config", "middleware", "graph" */
|
|
98
99
|
disableDetectors?: string[];
|
|
100
|
+
/** Custom route tags: { "billing": ["stripe", "payment"] } */
|
|
99
101
|
customTags?: Record<string, string[]>;
|
|
102
|
+
/** Max directory depth (default: 10) */
|
|
100
103
|
maxDepth?: number;
|
|
104
|
+
/** Output directory name (default: ".codesight") */
|
|
101
105
|
outputDir?: string;
|
|
106
|
+
/** AI tool profile */
|
|
102
107
|
profile?: "claude-code" | "cursor" | "codex" | "copilot" | "windsurf" | "generic";
|
|
108
|
+
/** Additional ignore patterns (glob-style) */
|
|
103
109
|
ignorePatterns?: string[];
|
|
110
|
+
/** Custom route patterns: [{ pattern: "router\\.handle\\(", method: "ALL" }] */
|
|
111
|
+
customRoutePatterns?: {
|
|
112
|
+
pattern: string;
|
|
113
|
+
method?: string;
|
|
114
|
+
}[];
|
|
115
|
+
/** Blast radius max BFS depth (default: 5) */
|
|
116
|
+
blastRadiusDepth?: number;
|
|
117
|
+
/** Hot file threshold: min imports to be "hot" (default: 3) */
|
|
118
|
+
hotFileThreshold?: number;
|
|
119
|
+
/** Plugin hooks */
|
|
120
|
+
plugins?: CodesightPlugin[];
|
|
121
|
+
}
|
|
122
|
+
export interface CodesightPlugin {
|
|
123
|
+
/** Plugin name for identification */
|
|
124
|
+
name: string;
|
|
125
|
+
/** Custom detector: runs after built-in detectors */
|
|
126
|
+
detector?: (files: string[], project: ProjectInfo) => Promise<PluginDetectorResult>;
|
|
127
|
+
/** Post-processor: transforms the final ScanResult */
|
|
128
|
+
postProcessor?: (result: ScanResult) => Promise<ScanResult>;
|
|
129
|
+
}
|
|
130
|
+
export interface PluginDetectorResult {
|
|
131
|
+
/** Additional routes to merge */
|
|
132
|
+
routes?: RouteInfo[];
|
|
133
|
+
/** Additional schema models to merge */
|
|
134
|
+
schemas?: SchemaModel[];
|
|
135
|
+
/** Additional components to merge */
|
|
136
|
+
components?: ComponentInfo[];
|
|
137
|
+
/** Additional middleware to merge */
|
|
138
|
+
middleware?: MiddlewareInfo[];
|
|
104
139
|
}
|
|
105
140
|
export interface ScanResult {
|
|
106
141
|
project: ProjectInfo;
|
package/eval/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# codesight Evaluation Suite
|
|
2
|
+
|
|
3
|
+
Reproducible accuracy benchmarks for codesight detectors.
|
|
4
|
+
|
|
5
|
+
## How It Works
|
|
6
|
+
|
|
7
|
+
Each fixture in `fixtures/` contains:
|
|
8
|
+
- `repo.json` — describes the repo structure (files with inline content)
|
|
9
|
+
- `ground-truth.json` — expected detection results (routes, models, env vars, blast radius)
|
|
10
|
+
|
|
11
|
+
Running `npx codesight --eval` will:
|
|
12
|
+
1. Create temporary directories from each fixture
|
|
13
|
+
2. Run codesight detectors on them
|
|
14
|
+
3. Compare results against ground truth
|
|
15
|
+
4. Print precision, recall, F1 score, and runtime per fixture
|
|
16
|
+
|
|
17
|
+
## Fixtures
|
|
18
|
+
|
|
19
|
+
| Fixture | Stack | What it tests |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| `nextjs-drizzle` | Next.js App Router + Drizzle ORM | Routes, schema, components, env vars |
|
|
22
|
+
| `express-prisma` | Express + Prisma | Route detection, schema parsing, middleware |
|
|
23
|
+
| `fastapi-sqlalchemy` | FastAPI + SQLAlchemy | Python routes, Python ORM, config |
|
|
24
|
+
| `hono-monorepo` | Hono + Drizzle (pnpm monorepo) | Monorepo detection, workspace routes, schema |
|
|
25
|
+
|
|
26
|
+
## Adding a Fixture
|
|
27
|
+
|
|
28
|
+
1. Create a folder in `fixtures/` with `repo.json` and `ground-truth.json`
|
|
29
|
+
2. Follow the JSON schema used by existing fixtures
|
|
30
|
+
3. Run `npx codesight --eval` to verify
|
|
31
|
+
|
|
32
|
+
## Metrics
|
|
33
|
+
|
|
34
|
+
- **Precision**: of all items codesight detected, how many are correct?
|
|
35
|
+
- **Recall**: of all items that exist, how many did codesight find?
|
|
36
|
+
- **F1**: harmonic mean of precision and recall
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"routes": [
|
|
3
|
+
{ "method": "POST", "path": "/login" },
|
|
4
|
+
{ "method": "POST", "path": "/register" },
|
|
5
|
+
{ "method": "GET", "path": "/" },
|
|
6
|
+
{ "method": "GET", "path": "/:id" },
|
|
7
|
+
{ "method": "PUT", "path": "/:id" },
|
|
8
|
+
{ "method": "DELETE", "path": "/:id" },
|
|
9
|
+
{ "method": "POST", "path": "/" }
|
|
10
|
+
],
|
|
11
|
+
"models": [
|
|
12
|
+
{
|
|
13
|
+
"name": "User",
|
|
14
|
+
"fields": ["id", "email", "password", "name", "role", "posts", "createdAt"]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"name": "Post",
|
|
18
|
+
"fields": ["id", "title", "content", "published", "author", "authorId", "tags", "createdAt"]
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "Tag",
|
|
22
|
+
"fields": ["id", "name", "posts"]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"name": "enum:Role",
|
|
26
|
+
"fields": ["USER", "ADMIN"]
|
|
27
|
+
}
|
|
28
|
+
],
|
|
29
|
+
"envVars": ["DATABASE_URL", "JWT_SECRET", "PORT", "REDIS_URL", "CORS_ORIGIN"],
|
|
30
|
+
"middleware": ["auth", "error", "rate-limit", "cors"]
|
|
31
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "express-prisma-api",
|
|
3
|
+
"description": "Express.js REST API with Prisma ORM and middleware",
|
|
4
|
+
"files": {
|
|
5
|
+
"package.json": "{\"name\":\"api-server\",\"dependencies\":{\"express\":\"4.18.0\",\"@prisma/client\":\"5.0.0\",\"cors\":\"2.8.5\",\"helmet\":\"7.0.0\",\"express-rate-limit\":\"7.0.0\",\"jsonwebtoken\":\"9.0.0\"},\"devDependencies\":{\"typescript\":\"5.3.0\",\"@types/express\":\"4.17.0\",\"@types/node\":\"20.0.0\",\"prisma\":\"5.0.0\"}}",
|
|
6
|
+
"tsconfig.json": "{\"compilerOptions\":{\"target\":\"es2017\",\"module\":\"commonjs\",\"outDir\":\"dist\"}}",
|
|
7
|
+
".env.example": "DATABASE_URL=postgresql://localhost:5432/api\nJWT_SECRET=changeme\nPORT=3000\nREDIS_URL=redis://localhost:6379\nCORS_ORIGIN=http://localhost:3001",
|
|
8
|
+
"prisma/schema.prisma": "datasource db {\n provider = \"postgresql\"\n url = env(\"DATABASE_URL\")\n}\n\ngenerator client {\n provider = \"prisma-client-js\"\n}\n\nmodel User {\n id Int @id @default(autoincrement())\n email String @unique\n password String\n name String?\n role Role @default(USER)\n posts Post[]\n createdAt DateTime @default(now())\n}\n\nmodel Post {\n id Int @id @default(autoincrement())\n title String\n content String?\n published Boolean @default(false)\n author User @relation(fields: [authorId], references: [id])\n authorId Int\n tags Tag[]\n createdAt DateTime @default(now())\n}\n\nmodel Tag {\n id Int @id @default(autoincrement())\n name String @unique\n posts Post[]\n}\n\nenum Role {\n USER\n ADMIN\n}",
|
|
9
|
+
"src/index.ts": "import express from 'express';\nimport cors from 'cors';\nimport helmet from 'helmet';\nimport { authRouter } from './routes/auth';\nimport { usersRouter } from './routes/users';\nimport { postsRouter } from './routes/posts';\nimport { errorHandler } from './middleware/error';\nimport { rateLimiter } from './middleware/rate-limit';\n\nconst app = express();\n\napp.use(cors());\napp.use(helmet());\napp.use(express.json());\napp.use(rateLimiter);\n\napp.use('/api/auth', authRouter);\napp.use('/api/users', usersRouter);\napp.use('/api/posts', postsRouter);\n\napp.use(errorHandler);\n\napp.listen(process.env.PORT || 3000);",
|
|
10
|
+
"src/routes/auth.ts": "import { Router } from 'express';\nimport { prisma } from '../lib/prisma';\nimport { generateToken } from '../lib/jwt';\n\nexport const authRouter = Router();\n\nauthRouter.post('/login', async (req, res) => {\n const { email, password } = req.body;\n const user = await prisma.user.findUnique({ where: { email } });\n if (!user) return res.status(401).json({ error: 'Invalid credentials' });\n const token = generateToken(user.id);\n res.json({ token });\n});\n\nauthRouter.post('/register', async (req, res) => {\n const { email, password, name } = req.body;\n const user = await prisma.user.create({ data: { email, password, name } });\n const token = generateToken(user.id);\n res.status(201).json({ token });\n});",
|
|
11
|
+
"src/routes/users.ts": "import { Router } from 'express';\nimport { prisma } from '../lib/prisma';\nimport { authenticate } from '../middleware/auth';\n\nexport const usersRouter = Router();\n\nusersRouter.get('/', authenticate, async (req, res) => {\n const users = await prisma.user.findMany();\n res.json(users);\n});\n\nusersRouter.get('/:id', authenticate, async (req, res) => {\n const user = await prisma.user.findUnique({ where: { id: parseInt(req.params.id) } });\n res.json(user);\n});\n\nusersRouter.put('/:id', authenticate, async (req, res) => {\n const user = await prisma.user.update({ where: { id: parseInt(req.params.id) }, data: req.body });\n res.json(user);\n});\n\nusersRouter.delete('/:id', authenticate, async (req, res) => {\n await prisma.user.delete({ where: { id: parseInt(req.params.id) } });\n res.json({ deleted: true });\n});",
|
|
12
|
+
"src/routes/posts.ts": "import { Router } from 'express';\nimport { prisma } from '../lib/prisma';\nimport { authenticate } from '../middleware/auth';\n\nexport const postsRouter = Router();\n\npostsRouter.get('/', async (req, res) => {\n const posts = await prisma.post.findMany({ include: { author: true, tags: true } });\n res.json(posts);\n});\n\npostsRouter.get('/:id', async (req, res) => {\n const post = await prisma.post.findUnique({ where: { id: parseInt(req.params.id) }, include: { author: true, tags: true } });\n res.json(post);\n});\n\npostsRouter.post('/', authenticate, async (req, res) => {\n const post = await prisma.post.create({ data: { ...req.body, authorId: req.userId } });\n res.status(201).json(post);\n});\n\npostsRouter.put('/:id', authenticate, async (req, res) => {\n const post = await prisma.post.update({ where: { id: parseInt(req.params.id) }, data: req.body });\n res.json(post);\n});\n\npostsRouter.delete('/:id', authenticate, async (req, res) => {\n await prisma.post.delete({ where: { id: parseInt(req.params.id) } });\n res.json({ deleted: true });\n});",
|
|
13
|
+
"src/middleware/auth.ts": "import { Request, Response, NextFunction } from 'express';\nimport { verifyToken } from '../lib/jwt';\n\nexport function authenticate(req: Request, res: Response, next: NextFunction) {\n const token = req.headers.authorization?.replace('Bearer ', '');\n if (!token) return res.status(401).json({ error: 'No token' });\n try {\n const payload = verifyToken(token);\n (req as any).userId = payload.userId;\n next();\n } catch {\n res.status(401).json({ error: 'Invalid token' });\n }\n}",
|
|
14
|
+
"src/middleware/error.ts": "import { Request, Response, NextFunction } from 'express';\n\nexport function errorHandler(err: Error, req: Request, res: Response, next: NextFunction) {\n console.error(err.stack);\n res.status(500).json({ error: 'Internal server error' });\n}",
|
|
15
|
+
"src/middleware/rate-limit.ts": "import rateLimit from 'express-rate-limit';\n\nexport const rateLimiter = rateLimit({\n windowMs: 15 * 60 * 1000,\n max: 100,\n message: { error: 'Too many requests' }\n});",
|
|
16
|
+
"src/lib/prisma.ts": "import { PrismaClient } from '@prisma/client';\n\nexport const prisma = new PrismaClient();",
|
|
17
|
+
"src/lib/jwt.ts": "import jwt from 'jsonwebtoken';\n\nconst SECRET = process.env.JWT_SECRET || 'dev-secret';\n\nexport function generateToken(userId: number): string {\n return jwt.sign({ userId }, SECRET, { expiresIn: '24h' });\n}\n\nexport function verifyToken(token: string): { userId: number } {\n return jwt.verify(token, SECRET) as { userId: number };\n}"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"routes": [
|
|
3
|
+
{ "method": "GET", "path": "/health" },
|
|
4
|
+
{ "method": "GET", "path": "/" },
|
|
5
|
+
{ "method": "GET", "path": "/{user_id}" },
|
|
6
|
+
{ "method": "POST", "path": "/" },
|
|
7
|
+
{ "method": "DELETE", "path": "/{user_id}" },
|
|
8
|
+
{ "method": "GET", "path": "/{item_id}" },
|
|
9
|
+
{ "method": "POST", "path": "/" },
|
|
10
|
+
{ "method": "PUT", "path": "/{item_id}" },
|
|
11
|
+
{ "method": "POST", "path": "/login" },
|
|
12
|
+
{ "method": "POST", "path": "/register" }
|
|
13
|
+
],
|
|
14
|
+
"models": [
|
|
15
|
+
{
|
|
16
|
+
"name": "User",
|
|
17
|
+
"fields": ["id", "email", "password", "name", "is_active", "items", "created_at"]
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"name": "Item",
|
|
21
|
+
"fields": ["id", "title", "description", "price", "owner_id", "owner", "created_at"]
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"name": "Category",
|
|
25
|
+
"fields": ["id", "name", "description"]
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
"envVars": ["DATABASE_URL", "SECRET_KEY", "DEBUG", "ALLOWED_ORIGINS"]
|
|
29
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fastapi-sqlalchemy-api",
|
|
3
|
+
"description": "FastAPI with SQLAlchemy models",
|
|
4
|
+
"files": {
|
|
5
|
+
"requirements.txt": "fastapi==0.104.0\nuvicorn==0.24.0\nsqlalchemy==2.0.23\nalembic==1.12.0\npydantic==2.5.0\npython-dotenv==1.0.0",
|
|
6
|
+
".env.example": "DATABASE_URL=postgresql://localhost:5432/fastapi_db\nSECRET_KEY=changeme\nDEBUG=true\nALLOWED_ORIGINS=http://localhost:3000",
|
|
7
|
+
"app/__init__.py": "",
|
|
8
|
+
"app/main.py": "from fastapi import FastAPI\nfrom fastapi.middleware.cors import CORSMiddleware\nfrom app.routes import users, items, auth\n\napp = FastAPI(title=\"My API\")\n\napp.add_middleware(CORSMiddleware, allow_origins=[\"*\"])\n\napp.include_router(users.router, prefix=\"/api/users\", tags=[\"users\"])\napp.include_router(items.router, prefix=\"/api/items\", tags=[\"items\"])\napp.include_router(auth.router, prefix=\"/api/auth\", tags=[\"auth\"])\n\n@app.get(\"/health\")\ndef health():\n return {\"status\": \"ok\"}",
|
|
9
|
+
"app/routes/__init__.py": "",
|
|
10
|
+
"app/routes/users.py": "from fastapi import APIRouter, Depends, HTTPException\nfrom sqlalchemy.orm import Session\nfrom app.db import get_db\nfrom app.models import User\n\nrouter = APIRouter()\n\n@router.get(\"/\")\ndef list_users(db: Session = Depends(get_db)):\n return db.query(User).all()\n\n@router.get(\"/{user_id}\")\ndef get_user(user_id: int, db: Session = Depends(get_db)):\n user = db.query(User).filter(User.id == user_id).first()\n if not user:\n raise HTTPException(status_code=404)\n return user\n\n@router.post(\"/\")\ndef create_user(data: dict, db: Session = Depends(get_db)):\n user = User(**data)\n db.add(user)\n db.commit()\n return user\n\n@router.delete(\"/{user_id}\")\ndef delete_user(user_id: int, db: Session = Depends(get_db)):\n db.query(User).filter(User.id == user_id).delete()\n db.commit()\n return {\"deleted\": True}",
|
|
11
|
+
"app/routes/items.py": "from fastapi import APIRouter, Depends\nfrom sqlalchemy.orm import Session\nfrom app.db import get_db\nfrom app.models import Item\n\nrouter = APIRouter()\n\n@router.get(\"/\")\ndef list_items(db: Session = Depends(get_db)):\n return db.query(Item).all()\n\n@router.get(\"/{item_id}\")\ndef get_item(item_id: int, db: Session = Depends(get_db)):\n return db.query(Item).filter(Item.id == item_id).first()\n\n@router.post(\"/\")\ndef create_item(data: dict, db: Session = Depends(get_db)):\n item = Item(**data)\n db.add(item)\n db.commit()\n return item\n\n@router.put(\"/{item_id}\")\ndef update_item(item_id: int, data: dict, db: Session = Depends(get_db)):\n item = db.query(Item).filter(Item.id == item_id).first()\n for k, v in data.items():\n setattr(item, k, v)\n db.commit()\n return item",
|
|
12
|
+
"app/routes/auth.py": "from fastapi import APIRouter\n\nrouter = APIRouter()\n\n@router.post(\"/login\")\ndef login(data: dict):\n return {\"token\": \"fake-token\"}\n\n@router.post(\"/register\")\ndef register(data: dict):\n return {\"user\": data}",
|
|
13
|
+
"app/models.py": "from sqlalchemy import Column, Integer, String, Boolean, ForeignKey, DateTime, Float\nfrom sqlalchemy.orm import relationship\nfrom datetime import datetime\nfrom app.db import Base\n\nclass User(Base):\n __tablename__ = 'users'\n id = Column(Integer, primary_key=True)\n email = Column(String, unique=True, nullable=False)\n password = Column(String, nullable=False)\n name = Column(String)\n is_active = Column(Boolean, default=True)\n items = relationship('Item', back_populates='owner')\n created_at = Column(DateTime, default=datetime.utcnow)\n\nclass Item(Base):\n __tablename__ = 'items'\n id = Column(Integer, primary_key=True)\n title = Column(String, nullable=False)\n description = Column(String)\n price = Column(Float)\n owner_id = Column(Integer, ForeignKey('users.id'))\n owner = relationship('User', back_populates='items')\n created_at = Column(DateTime, default=datetime.utcnow)\n\nclass Category(Base):\n __tablename__ = 'categories'\n id = Column(Integer, primary_key=True)\n name = Column(String, unique=True, nullable=False)\n description = Column(String)",
|
|
14
|
+
"app/db.py": "from sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker, declarative_base\nimport os\n\nDATABASE_URL = os.getenv('DATABASE_URL', 'sqlite:///./test.db')\nengine = create_engine(DATABASE_URL)\nSessionLocal = sessionmaker(bind=engine)\nBase = declarative_base()\n\ndef get_db():\n db = SessionLocal()\n try:\n yield db\n finally:\n db.close()"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"routes": [
|
|
3
|
+
{ "method": "GET", "path": "/health" },
|
|
4
|
+
{ "method": "POST", "path": "/login" },
|
|
5
|
+
{ "method": "POST", "path": "/register" },
|
|
6
|
+
{ "method": "POST", "path": "/refresh" },
|
|
7
|
+
{ "method": "GET", "path": "/" },
|
|
8
|
+
{ "method": "GET", "path": "/:id" },
|
|
9
|
+
{ "method": "PUT", "path": "/:id" },
|
|
10
|
+
{ "method": "DELETE", "path": "/:id" },
|
|
11
|
+
{ "method": "GET", "path": "/" },
|
|
12
|
+
{ "method": "GET", "path": "/:id" },
|
|
13
|
+
{ "method": "POST", "path": "/" }
|
|
14
|
+
],
|
|
15
|
+
"models": [
|
|
16
|
+
{
|
|
17
|
+
"name": "users",
|
|
18
|
+
"fields": ["id", "email", "name", "role", "createdAt"],
|
|
19
|
+
"relations": ["projects"]
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"name": "projects",
|
|
23
|
+
"fields": ["id", "name", "description", "ownerId", "isPublic", "createdAt"],
|
|
24
|
+
"relations": ["owner"]
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"components": [
|
|
28
|
+
{ "name": "ProjectCard", "props": ["name", "description", "isPublic"] },
|
|
29
|
+
{ "name": "UserAvatar", "props": ["name", "size"] }
|
|
30
|
+
],
|
|
31
|
+
"envVars": ["DATABASE_URL", "JWT_SECRET", "PORT"],
|
|
32
|
+
"middleware": ["auth"]
|
|
33
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hono-monorepo",
|
|
3
|
+
"description": "Hono API + React frontend in pnpm monorepo with Drizzle",
|
|
4
|
+
"files": {
|
|
5
|
+
"package.json": "{\"name\":\"my-monorepo\",\"private\":true}",
|
|
6
|
+
"pnpm-workspace.yaml": "packages:\n - 'apps/*'\n - 'packages/*'",
|
|
7
|
+
"apps/api/package.json": "{\"name\":\"@mono/api\",\"dependencies\":{\"hono\":\"4.0.0\",\"drizzle-orm\":\"0.30.0\",\"pg\":\"8.11.0\",\"zod\":\"3.22.0\"},\"devDependencies\":{\"typescript\":\"5.3.0\"}}",
|
|
8
|
+
"apps/api/.env.example": "DATABASE_URL=postgres://localhost:5432/mono\nJWT_SECRET=secret\nPORT=4000",
|
|
9
|
+
"apps/api/src/index.ts": "import { Hono } from 'hono';\nimport { cors } from 'hono/cors';\nimport { logger } from 'hono/logger';\nimport { authRoutes } from './routes/auth';\nimport { userRoutes } from './routes/users';\nimport { projectRoutes } from './routes/projects';\n\nconst app = new Hono();\n\napp.use('*', cors());\napp.use('*', logger());\n\napp.get('/health', (c) => c.json({ ok: true }));\n\napp.route('/api/auth', authRoutes);\napp.route('/api/users', userRoutes);\napp.route('/api/projects', projectRoutes);\n\nexport default app;",
|
|
10
|
+
"apps/api/src/routes/auth.ts": "import { Hono } from 'hono';\nimport { zValidator } from '@hono/zod-validator';\nimport { z } from 'zod';\n\nexport const authRoutes = new Hono();\n\nconst loginSchema = z.object({ email: z.string().email(), password: z.string() });\n\nauthRoutes.post('/login', zValidator('json', loginSchema), async (c) => {\n const { email, password } = c.req.valid('json');\n return c.json({ token: 'jwt-token' });\n});\n\nauthRoutes.post('/register', async (c) => {\n const body = await c.req.json();\n return c.json({ user: body }, 201);\n});\n\nauthRoutes.post('/refresh', async (c) => {\n return c.json({ token: 'new-token' });\n});",
|
|
11
|
+
"apps/api/src/routes/users.ts": "import { Hono } from 'hono';\nimport { db } from '../db';\nimport { users } from '../db/schema';\nimport { eq } from 'drizzle-orm';\n\nexport const userRoutes = new Hono();\n\nuserRoutes.get('/', async (c) => {\n const all = await db.select().from(users);\n return c.json(all);\n});\n\nuserRoutes.get('/:id', async (c) => {\n const id = parseInt(c.req.param('id'));\n const user = await db.select().from(users).where(eq(users.id, id));\n return c.json(user[0]);\n});\n\nuserRoutes.put('/:id', async (c) => {\n const id = parseInt(c.req.param('id'));\n const body = await c.req.json();\n const updated = await db.update(users).set(body).where(eq(users.id, id)).returning();\n return c.json(updated[0]);\n});\n\nuserRoutes.delete('/:id', async (c) => {\n const id = parseInt(c.req.param('id'));\n await db.delete(users).where(eq(users.id, id));\n return c.json({ deleted: true });\n});",
|
|
12
|
+
"apps/api/src/routes/projects.ts": "import { Hono } from 'hono';\nimport { db } from '../db';\nimport { projects } from '../db/schema';\nimport { eq } from 'drizzle-orm';\n\nexport const projectRoutes = new Hono();\n\nprojectRoutes.get('/', async (c) => {\n const all = await db.select().from(projects);\n return c.json(all);\n});\n\nprojectRoutes.get('/:id', async (c) => {\n const id = parseInt(c.req.param('id'));\n const project = await db.select().from(projects).where(eq(projects.id, id));\n return c.json(project[0]);\n});\n\nprojectRoutes.post('/', async (c) => {\n const body = await c.req.json();\n const created = await db.insert(projects).values(body).returning();\n return c.json(created[0], 201);\n});",
|
|
13
|
+
"apps/api/src/db/index.ts": "import { drizzle } from 'drizzle-orm/node-postgres';\nimport { Pool } from 'pg';\n\nconst pool = new Pool({ connectionString: process.env.DATABASE_URL });\nexport const db = drizzle(pool);",
|
|
14
|
+
"apps/api/src/db/schema.ts": "import { pgTable, serial, text, timestamp, integer, boolean } from 'drizzle-orm/pg-core';\nimport { relations } from 'drizzle-orm';\n\nexport const users = pgTable('users', {\n id: serial('id').primaryKey(),\n email: text('email').notNull().unique(),\n name: text('name'),\n role: text('role').default('user'),\n createdAt: timestamp('created_at').defaultNow()\n});\n\nexport const projects = pgTable('projects', {\n id: serial('id').primaryKey(),\n name: text('name').notNull(),\n description: text('description'),\n ownerId: integer('owner_id').notNull().references(() => users.id),\n isPublic: boolean('is_public').default(true),\n createdAt: timestamp('created_at').defaultNow()\n});\n\nexport const usersRelations = relations(users, ({ many }) => ({\n projects: many(projects)\n}));\n\nexport const projectsRelations = relations(projects, ({ one }) => ({\n owner: one(users, { fields: [projects.ownerId], references: [users.id] })\n}));",
|
|
15
|
+
"apps/api/src/middleware/auth.ts": "import { Context, Next } from 'hono';\n\nexport async function authMiddleware(c: Context, next: Next) {\n const token = c.req.header('Authorization');\n if (!token) return c.json({ error: 'Unauthorized' }, 401);\n await next();\n}",
|
|
16
|
+
"apps/web/package.json": "{\"name\":\"@mono/web\",\"dependencies\":{\"react\":\"18.0.0\",\"react-dom\":\"18.0.0\"},\"devDependencies\":{\"typescript\":\"5.3.0\",\"vite\":\"5.0.0\"}}",
|
|
17
|
+
"apps/web/src/components/ProjectCard.tsx": "import React from 'react';\n\ninterface ProjectCardProps {\n name: string;\n description: string;\n isPublic: boolean;\n}\n\nexport function ProjectCard({ name, description, isPublic }: ProjectCardProps) {\n return <div><h3>{name}</h3><p>{description}</p>{isPublic && <span>Public</span>}</div>;\n}",
|
|
18
|
+
"apps/web/src/components/UserAvatar.tsx": "'use client';\nimport React from 'react';\n\ninterface UserAvatarProps {\n name: string;\n size?: number;\n}\n\nexport function UserAvatar({ name, size = 40 }: UserAvatarProps) {\n return <div style={{ width: size, height: size }}>{name[0]}</div>;\n}"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"routes": [
|
|
3
|
+
{ "method": "GET", "path": "/api/users" },
|
|
4
|
+
{ "method": "POST", "path": "/api/users" },
|
|
5
|
+
{ "method": "GET", "path": "/api/users/[id]" },
|
|
6
|
+
{ "method": "PUT", "path": "/api/users/[id]" },
|
|
7
|
+
{ "method": "DELETE", "path": "/api/users/[id]" },
|
|
8
|
+
{ "method": "GET", "path": "/api/posts" },
|
|
9
|
+
{ "method": "POST", "path": "/api/posts" },
|
|
10
|
+
{ "method": "GET", "path": "/api/posts/[id]/comments" },
|
|
11
|
+
{ "method": "POST", "path": "/api/posts/[id]/comments" }
|
|
12
|
+
],
|
|
13
|
+
"models": [
|
|
14
|
+
{
|
|
15
|
+
"name": "users",
|
|
16
|
+
"fields": ["id", "email", "name", "createdAt"],
|
|
17
|
+
"relations": ["posts", "comments"]
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"name": "posts",
|
|
21
|
+
"fields": ["id", "title", "content", "published", "authorId", "createdAt"],
|
|
22
|
+
"relations": ["author", "comments"]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"name": "comments",
|
|
26
|
+
"fields": ["id", "body", "postId", "authorId", "createdAt"],
|
|
27
|
+
"relations": []
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"components": [
|
|
31
|
+
{ "name": "UserCard", "props": ["name", "email", "avatar"] },
|
|
32
|
+
{ "name": "PostList", "props": ["posts", "onSelect"] },
|
|
33
|
+
{ "name": "CommentForm", "props": ["postId", "onSubmit"] }
|
|
34
|
+
],
|
|
35
|
+
"envVars": ["DATABASE_URL", "NEXTAUTH_SECRET", "NEXT_PUBLIC_API_URL"],
|
|
36
|
+
"middleware": ["middleware", "auth"]
|
|
37
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nextjs-drizzle-app",
|
|
3
|
+
"description": "Next.js App Router with Drizzle ORM and React components",
|
|
4
|
+
"files": {
|
|
5
|
+
"package.json": "{\"name\":\"my-app\",\"dependencies\":{\"next\":\"14.0.0\",\"react\":\"18.0.0\",\"drizzle-orm\":\"0.30.0\",\"pg\":\"8.11.0\",\"zod\":\"3.22.0\"},\"devDependencies\":{\"typescript\":\"5.3.0\",\"@types/node\":\"20.0.0\"}}",
|
|
6
|
+
"tsconfig.json": "{\"compilerOptions\":{\"target\":\"es2017\",\"module\":\"esnext\",\"jsx\":\"react-jsx\"}}",
|
|
7
|
+
".env.example": "DATABASE_URL=postgres://localhost:5432/mydb\nNEXTAUTH_SECRET=\nNEXT_PUBLIC_API_URL=http://localhost:3000",
|
|
8
|
+
"drizzle.config.ts": "import { defineConfig } from 'drizzle-kit';\nexport default defineConfig({ schema: './src/db/schema.ts', driver: 'pg' });",
|
|
9
|
+
"src/db/schema.ts": "import { pgTable, serial, text, timestamp, integer, boolean } from 'drizzle-orm/pg-core';\nimport { relations } from 'drizzle-orm';\n\nexport const users = pgTable('users', {\n id: serial('id').primaryKey(),\n email: text('email').notNull().unique(),\n name: text('name'),\n createdAt: timestamp('created_at').defaultNow()\n});\n\nexport const posts = pgTable('posts', {\n id: serial('id').primaryKey(),\n title: text('title').notNull(),\n content: text('content'),\n published: boolean('published').default(false),\n authorId: integer('author_id').notNull().references(() => users.id),\n createdAt: timestamp('created_at').defaultNow()\n});\n\nexport const comments = pgTable('comments', {\n id: serial('id').primaryKey(),\n body: text('body').notNull(),\n postId: integer('post_id').notNull().references(() => posts.id),\n authorId: integer('author_id').notNull().references(() => users.id),\n createdAt: timestamp('created_at').defaultNow()\n});\n\nexport const usersRelations = relations(users, ({ many }) => ({\n posts: many(posts),\n comments: many(comments)\n}));\n\nexport const postsRelations = relations(posts, ({ one, many }) => ({\n author: one(users, { fields: [posts.authorId], references: [users.id] }),\n comments: many(comments)\n}));",
|
|
10
|
+
"src/app/api/users/route.ts": "import { NextResponse } from 'next/server';\nimport { db } from '@/db';\nimport { users } from '@/db/schema';\n\nexport async function GET() {\n const all = await db.select().from(users);\n return NextResponse.json(all);\n}\n\nexport async function POST(req: Request) {\n const body = await req.json();\n const user = await db.insert(users).values(body).returning();\n return NextResponse.json(user, { status: 201 });\n}",
|
|
11
|
+
"src/app/api/users/[id]/route.ts": "import { NextResponse } from 'next/server';\nimport { db } from '@/db';\nimport { users } from '@/db/schema';\nimport { eq } from 'drizzle-orm';\n\nexport async function GET(req: Request, { params }: { params: { id: string } }) {\n const user = await db.select().from(users).where(eq(users.id, parseInt(params.id)));\n return NextResponse.json(user[0]);\n}\n\nexport async function PUT(req: Request, { params }: { params: { id: string } }) {\n const body = await req.json();\n const updated = await db.update(users).set(body).where(eq(users.id, parseInt(params.id))).returning();\n return NextResponse.json(updated[0]);\n}\n\nexport async function DELETE(req: Request, { params }: { params: { id: string } }) {\n await db.delete(users).where(eq(users.id, parseInt(params.id)));\n return NextResponse.json({ deleted: true });\n}",
|
|
12
|
+
"src/app/api/posts/route.ts": "import { NextResponse } from 'next/server';\nimport { db } from '@/db';\nimport { posts } from '@/db/schema';\n\nexport async function GET() {\n const all = await db.select().from(posts);\n return NextResponse.json(all);\n}\n\nexport async function POST(req: Request) {\n const body = await req.json();\n const post = await db.insert(posts).values(body).returning();\n return NextResponse.json(post, { status: 201 });\n}",
|
|
13
|
+
"src/app/api/posts/[id]/comments/route.ts": "import { NextResponse } from 'next/server';\nimport { db } from '@/db';\nimport { comments } from '@/db/schema';\nimport { eq } from 'drizzle-orm';\n\nexport async function GET(req: Request, { params }: { params: { id: string } }) {\n const all = await db.select().from(comments).where(eq(comments.postId, parseInt(params.id)));\n return NextResponse.json(all);\n}\n\nexport async function POST(req: Request, { params }: { params: { id: string } }) {\n const body = await req.json();\n const comment = await db.insert(comments).values({ ...body, postId: parseInt(params.id) }).returning();\n return NextResponse.json(comment, { status: 201 });\n}",
|
|
14
|
+
"src/components/UserCard.tsx": "'use client';\nimport React from 'react';\n\ninterface UserCardProps {\n name: string;\n email: string;\n avatar?: string;\n}\n\nexport function UserCard({ name, email, avatar }: UserCardProps) {\n return <div className=\"card\"><h3>{name}</h3><p>{email}</p></div>;\n}",
|
|
15
|
+
"src/components/PostList.tsx": "import React from 'react';\n\ninterface PostListProps {\n posts: Array<{ id: number; title: string; published: boolean }>;\n onSelect: (id: number) => void;\n}\n\nexport function PostList({ posts, onSelect }: PostListProps) {\n return <ul>{posts.map(p => <li key={p.id} onClick={() => onSelect(p.id)}>{p.title}</li>)}</ul>;\n}",
|
|
16
|
+
"src/components/CommentForm.tsx": "'use client';\nimport React, { useState } from 'react';\n\ninterface CommentFormProps {\n postId: number;\n onSubmit: (body: string) => void;\n}\n\nexport function CommentForm({ postId, onSubmit }: CommentFormProps) {\n const [body, setBody] = useState('');\n return <form onSubmit={() => onSubmit(body)}><textarea value={body} onChange={e => setBody(e.target.value)} /><button type=\"submit\">Submit</button></form>;\n}",
|
|
17
|
+
"src/middleware.ts": "import { NextResponse } from 'next/server';\nimport type { NextRequest } from 'next/server';\n\nexport function middleware(request: NextRequest) {\n const token = request.headers.get('authorization');\n if (!token && request.nextUrl.pathname.startsWith('/api')) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });\n }\n return NextResponse.next();\n}\n\nexport const config = { matcher: '/api/:path*' };",
|
|
18
|
+
"src/lib/auth.ts": "export function verifyToken(token: string): boolean {\n return token.startsWith('Bearer ');\n}\n\nexport function createToken(userId: number): string {\n return `Bearer ${userId}-${Date.now()}`;\n}",
|
|
19
|
+
"src/lib/validate.ts": "import { z } from 'zod';\n\nexport const createUserSchema = z.object({\n email: z.string().email(),\n name: z.string().min(1)\n});\n\nexport const createPostSchema = z.object({\n title: z.string().min(1),\n content: z.string().optional(),\n published: z.boolean().optional()\n});"
|
|
20
|
+
}
|
|
21
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codesight",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "See your codebase clearly. Universal AI context generator that maps routes, schema, components, dependencies, and more for Claude Code, Cursor, Copilot, Codex, and any AI coding tool.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"node": ">=18.0.0"
|
|
51
51
|
},
|
|
52
52
|
"files": [
|
|
53
|
-
"dist"
|
|
53
|
+
"dist",
|
|
54
|
+
"eval"
|
|
54
55
|
]
|
|
55
56
|
}
|