codesight 1.3.2 → 1.5.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.
@@ -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,25 @@
1
+ {
2
+ "routes": [
3
+ { "method": "GET", "path": "/health" },
4
+ { "method": "GET", "path": "/api/users" },
5
+ { "method": "GET", "path": "/api/users/:id" },
6
+ { "method": "POST", "path": "/api/users" },
7
+ { "method": "PUT", "path": "/api/users/:id" },
8
+ { "method": "DELETE", "path": "/api/users/:id" },
9
+ { "method": "GET", "path": "/api/projects" },
10
+ { "method": "GET", "path": "/api/projects/:id" },
11
+ { "method": "POST", "path": "/api/projects" }
12
+ ],
13
+ "models": [
14
+ {
15
+ "name": "User",
16
+ "fields": ["ID", "CreatedAt", "UpdatedAt", "DeletedAt", "Email", "Password", "Name", "Role"]
17
+ },
18
+ {
19
+ "name": "Project",
20
+ "fields": ["ID", "CreatedAt", "UpdatedAt", "DeletedAt", "Name", "Description", "IsPublic", "OwnerID"]
21
+ }
22
+ ],
23
+ "envVars": ["DATABASE_URL", "JWT_SECRET", "PORT", "GIN_MODE"],
24
+ "middleware": ["auth"]
25
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "gin-gorm-api",
3
+ "description": "Gin REST API with GORM models and route groups",
4
+ "files": {
5
+ "go.mod": "module github.com/example/api\n\ngo 1.22\n\nrequire (\n\tgithub.com/gin-gonic/gin v1.9.1\n\tgorm.io/gorm v1.25.5\n\tgorm.io/driver/postgres v1.5.4\n)",
6
+ ".env.example": "DATABASE_URL=postgres://localhost:5432/ginapi\nJWT_SECRET=changeme\nPORT=8080\nGIN_MODE=release",
7
+ "main.go": "package main\n\nimport (\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/example/api/handlers\"\n\t\"github.com/example/api/middleware\"\n)\n\nfunc main() {\n\tr := gin.Default()\n\n\tr.GET(\"/health\", handlers.Health)\n\n\tapi := r.Group(\"/api\")\n\tapi.Use(middleware.Auth())\n\n\tusers := api.Group(\"/users\")\n\tusers.GET(\"\", handlers.ListUsers)\n\tusers.GET(\"/:id\", handlers.GetUser)\n\tusers.POST(\"\", handlers.CreateUser)\n\tusers.PUT(\"/:id\", handlers.UpdateUser)\n\tusers.DELETE(\"/:id\", handlers.DeleteUser)\n\n\tprojects := api.Group(\"/projects\")\n\tprojects.GET(\"\", handlers.ListProjects)\n\tprojects.GET(\"/:id\", handlers.GetProject)\n\tprojects.POST(\"\", handlers.CreateProject)\n\n\tr.Run(\":8080\")\n}",
8
+ "models/user.go": "package models\n\nimport (\n\t\"gorm.io/gorm\"\n\t\"time\"\n)\n\ntype User struct {\n\tgorm.Model\n\tEmail string `gorm:\"uniqueIndex;not null\" json:\"email\"`\n\tPassword string `gorm:\"not null\" json:\"-\"`\n\tName string `json:\"name\"`\n\tRole string `gorm:\"default:user\" json:\"role\"`\n\tProjects []Project `gorm:\"foreignKey:OwnerID\" json:\"projects,omitempty\"`\n}",
9
+ "models/project.go": "package models\n\nimport (\n\t\"gorm.io/gorm\"\n)\n\ntype Project struct {\n\tgorm.Model\n\tName string `gorm:\"not null\" json:\"name\"`\n\tDescription string `json:\"description\"`\n\tIsPublic bool `gorm:\"default:true\" json:\"is_public\"`\n\tOwnerID uint `gorm:\"not null;index\" json:\"owner_id\"`\n\tOwner *User `gorm:\"foreignKey:OwnerID\" json:\"owner,omitempty\"`\n}",
10
+ "handlers/health.go": "package handlers\n\nimport \"github.com/gin-gonic/gin\"\n\nfunc Health(c *gin.Context) {\n\tc.JSON(200, gin.H{\"status\": \"ok\"})\n}",
11
+ "handlers/users.go": "package handlers\n\nimport (\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/example/api/models\"\n)\n\nfunc ListUsers(c *gin.Context) {\n\tvar users []models.User\n\tc.JSON(200, users)\n}\n\nfunc GetUser(c *gin.Context) {\n\tc.JSON(200, gin.H{})\n}\n\nfunc CreateUser(c *gin.Context) {\n\tc.JSON(201, gin.H{})\n}\n\nfunc UpdateUser(c *gin.Context) {\n\tc.JSON(200, gin.H{})\n}\n\nfunc DeleteUser(c *gin.Context) {\n\tc.JSON(200, gin.H{\"deleted\": true})\n}",
12
+ "handlers/projects.go": "package handlers\n\nimport (\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc ListProjects(c *gin.Context) {\n\tc.JSON(200, gin.H{})\n}\n\nfunc GetProject(c *gin.Context) {\n\tc.JSON(200, gin.H{})\n}\n\nfunc CreateProject(c *gin.Context) {\n\tc.JSON(201, gin.H{})\n}",
13
+ "middleware/auth.go": "package middleware\n\nimport \"github.com/gin-gonic/gin\"\n\nfunc Auth() gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\ttoken := c.GetHeader(\"Authorization\")\n\t\tif token == \"\" {\n\t\t\tc.AbortWithStatusJSON(401, gin.H{\"error\": \"unauthorized\"})\n\t\t\treturn\n\t\t}\n\t\tc.Next()\n\t}\n}",
14
+ "db/db.go": "package db\n\nimport (\n\t\"gorm.io/driver/postgres\"\n\t\"gorm.io/gorm\"\n\t\"os\"\n)\n\nvar DB *gorm.DB\n\nfunc Init() {\n\tdsn := os.Getenv(\"DATABASE_URL\")\n\tdb, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tDB = db\n}"
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
+ }