codesight 1.4.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Go structured parser for routes and models.
3
+ * Uses brace-tracking + regex for near-AST accuracy on Go's regular syntax.
4
+ *
5
+ * Go's syntax is regular enough that structured parsing (tracking braces,
6
+ * extracting struct bodies, parsing field tags) achieves AST-level accuracy
7
+ * without needing the Go compiler.
8
+ *
9
+ * Extracts:
10
+ * - Gin/Fiber/Echo/Chi/net-http routes with group prefixes
11
+ * - GORM model structs with field types, tags (primaryKey, unique, etc.)
12
+ */
13
+ import type { RouteInfo, SchemaModel, Framework } from "../types.js";
14
+ /**
15
+ * Extract routes from a Go file with group/prefix tracking.
16
+ * Works for Gin, Fiber, Echo, Chi, and net/http.
17
+ */
18
+ export declare function extractGoRoutesStructured(filePath: string, content: string, framework: Framework, tags: string[]): RouteInfo[];
19
+ /**
20
+ * Extract GORM model structs from a Go file.
21
+ * Parses struct bodies, field types, and gorm tags.
22
+ */
23
+ export declare function extractGORMModelsStructured(_filePath: string, content: string): SchemaModel[];
@@ -0,0 +1,331 @@
1
+ /**
2
+ * Go structured parser for routes and models.
3
+ * Uses brace-tracking + regex for near-AST accuracy on Go's regular syntax.
4
+ *
5
+ * Go's syntax is regular enough that structured parsing (tracking braces,
6
+ * extracting struct bodies, parsing field tags) achieves AST-level accuracy
7
+ * without needing the Go compiler.
8
+ *
9
+ * Extracts:
10
+ * - Gin/Fiber/Echo/Chi/net-http routes with group prefixes
11
+ * - GORM model structs with field types, tags (primaryKey, unique, etc.)
12
+ */
13
+ /**
14
+ * Extract routes from a Go file with group/prefix tracking.
15
+ * Works for Gin, Fiber, Echo, Chi, and net/http.
16
+ */
17
+ export function extractGoRoutesStructured(filePath, content, framework, tags) {
18
+ const routes = [];
19
+ // Step 1: Find route groups with prefixes
20
+ // Gin: r.Group("/api") / g := r.Group("/v1")
21
+ // Echo: g := e.Group("/api")
22
+ // Fiber: api := app.Group("/api")
23
+ // Chi: r.Route("/api", func(r chi.Router) { ... })
24
+ const groups = extractRouteGroups(content, framework);
25
+ // Collect group variable names to exclude from top-level scan
26
+ const groupVarNames = new Set();
27
+ // Step 2: Extract routes from each group with prefix resolution
28
+ for (const group of groups) {
29
+ // Track which variable this group belongs to
30
+ if (group.varName)
31
+ groupVarNames.add(group.varName);
32
+ const groupRoutes = extractRoutesFromBlock(group.body, framework, filePath, tags);
33
+ for (const route of groupRoutes) {
34
+ route.path = normalizePath(group.prefix + "/" + route.path);
35
+ routes.push(route);
36
+ }
37
+ }
38
+ // Step 3: Extract top-level routes (only from lines NOT belonging to group vars)
39
+ // Filter out lines that reference group variables to avoid duplicates
40
+ if (groupVarNames.size > 0) {
41
+ const lines = content.split("\n");
42
+ const topLines = lines.filter((line) => {
43
+ for (const v of groupVarNames) {
44
+ if (line.includes(v + "."))
45
+ return false;
46
+ }
47
+ return true;
48
+ });
49
+ const topContent = topLines.join("\n");
50
+ const topLevelRoutes = extractRoutesFromBlock(topContent, framework, filePath, tags);
51
+ routes.push(...topLevelRoutes);
52
+ }
53
+ else {
54
+ const topLevelRoutes = extractRoutesFromBlock(content, framework, filePath, tags);
55
+ routes.push(...topLevelRoutes);
56
+ }
57
+ // Deduplicate
58
+ const seen = new Set();
59
+ return routes.filter((r) => {
60
+ const key = `${r.method}:${r.path}`;
61
+ if (seen.has(key))
62
+ return false;
63
+ seen.add(key);
64
+ return true;
65
+ });
66
+ }
67
+ function extractRouteGroups(content, framework) {
68
+ const groups = [];
69
+ if (framework === "chi") {
70
+ // Chi: r.Route("/prefix", func(r chi.Router) { ... })
71
+ const chiRoutePattern = /\.Route\s*\(\s*"([^"]+)"\s*,\s*func\s*\([^)]*\)\s*\{/g;
72
+ let match;
73
+ while ((match = chiRoutePattern.exec(content)) !== null) {
74
+ const prefix = match[1];
75
+ const bodyStart = match.index + match[0].length;
76
+ const body = extractBraceBlock(content, bodyStart);
77
+ if (body)
78
+ groups.push({ prefix, body });
79
+ }
80
+ }
81
+ else {
82
+ // Gin/Echo/Fiber: varName := receiver.Group("/prefix")
83
+ // Build a prefix map to resolve chained groups: api := r.Group("/api"), users := api.Group("/users")
84
+ const prefixMap = new Map(); // varName -> resolved full prefix
85
+ const groupPattern = /(\w+)\s*:?=\s*(\w+)\.Group\s*\(\s*"([^"]*)"/g;
86
+ let match;
87
+ // First pass: build prefix chain
88
+ while ((match = groupPattern.exec(content)) !== null) {
89
+ const varName = match[1];
90
+ const receiver = match[2];
91
+ const prefix = match[3];
92
+ // Resolve receiver prefix (if receiver is itself a group)
93
+ const receiverPrefix = prefixMap.get(receiver) || "";
94
+ const fullPrefix = normalizePath(receiverPrefix + "/" + prefix);
95
+ prefixMap.set(varName, fullPrefix);
96
+ }
97
+ // Second pass: extract routes for each group variable
98
+ for (const [varName, fullPrefix] of prefixMap) {
99
+ const varRoutes = [];
100
+ // Match routes — allow empty path strings with ([^"]*)
101
+ const varPattern = new RegExp(`${escapeRegex(varName)}\\.\\s*(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD|Get|Post|Put|Patch|Delete|Options|Head)\\s*\\(\\s*"([^"]*)"`, "g");
102
+ let routeMatch;
103
+ while ((routeMatch = varPattern.exec(content)) !== null) {
104
+ varRoutes.push(`${routeMatch[1]}:${routeMatch[2]}`);
105
+ }
106
+ // Also handle HandleFunc for mixed patterns
107
+ const handlePattern = new RegExp(`${escapeRegex(varName)}\\.\\s*HandleFunc\\s*\\(\\s*"([^"]*)"`, "g");
108
+ while ((routeMatch = handlePattern.exec(content)) !== null) {
109
+ varRoutes.push(`ALL:${routeMatch[1]}`);
110
+ }
111
+ if (varRoutes.length > 0) {
112
+ groups.push({
113
+ prefix: fullPrefix,
114
+ varName,
115
+ body: varRoutes
116
+ .map((r) => {
117
+ const colonIdx = r.indexOf(":");
118
+ const m = r.slice(0, colonIdx);
119
+ const p = r.slice(colonIdx + 1);
120
+ return `FAKE.${m}("${p}")`;
121
+ })
122
+ .join("\n"),
123
+ });
124
+ }
125
+ }
126
+ }
127
+ return groups;
128
+ }
129
+ function extractRoutesFromBlock(block, framework, filePath, tags) {
130
+ const routes = [];
131
+ if (framework === "gin" || framework === "echo") {
132
+ // .GET("/path", handler) — uppercase methods (allow empty path)
133
+ const pattern = /\.(?:FAKE\.)?(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s*\(\s*"([^"]*)"/g;
134
+ let match;
135
+ while ((match = pattern.exec(block)) !== null) {
136
+ routes.push({
137
+ method: match[1],
138
+ path: match[2],
139
+ file: filePath,
140
+ tags,
141
+ framework,
142
+ params: extractPathParams(match[2]),
143
+ confidence: "ast",
144
+ });
145
+ }
146
+ }
147
+ else if (framework === "fiber" || framework === "chi") {
148
+ // .Get("/path", handler) — PascalCase methods (allow empty path)
149
+ const pattern = /\.(?:FAKE\.)?(Get|Post|Put|Patch|Delete|Options|Head)\s*\(\s*"([^"]*)"/g;
150
+ let match;
151
+ while ((match = pattern.exec(block)) !== null) {
152
+ routes.push({
153
+ method: match[1].toUpperCase(),
154
+ path: match[2],
155
+ file: filePath,
156
+ tags,
157
+ framework,
158
+ params: extractPathParams(match[2]),
159
+ confidence: "ast",
160
+ });
161
+ }
162
+ }
163
+ // net/http: HandleFunc or Handle
164
+ if (framework === "go-net-http" || framework === "chi") {
165
+ const pattern = /\.(?:HandleFunc|Handle)\s*\(\s*"([^"]+)"/g;
166
+ let match;
167
+ while ((match = pattern.exec(block)) !== null) {
168
+ // Go 1.22+: "GET /path" patterns
169
+ const pathStr = match[1];
170
+ let method = "ALL";
171
+ let path = pathStr;
172
+ const methodMatch = pathStr.match(/^(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s+(\/.*)/);
173
+ if (methodMatch) {
174
+ method = methodMatch[1];
175
+ path = methodMatch[2];
176
+ }
177
+ routes.push({
178
+ method,
179
+ path,
180
+ file: filePath,
181
+ tags,
182
+ framework,
183
+ params: extractPathParams(path),
184
+ confidence: "ast",
185
+ });
186
+ }
187
+ }
188
+ // Chi: r.Get, r.Post etc. (also catch Method pattern)
189
+ if (framework === "chi") {
190
+ const methodPattern = /\.Method\s*\(\s*"(\w+)"\s*,\s*"([^"]+)"/g;
191
+ let match;
192
+ while ((match = methodPattern.exec(block)) !== null) {
193
+ routes.push({
194
+ method: match[1].toUpperCase(),
195
+ path: match[2],
196
+ file: filePath,
197
+ tags,
198
+ framework,
199
+ params: extractPathParams(match[2]),
200
+ confidence: "ast",
201
+ });
202
+ }
203
+ }
204
+ return routes;
205
+ }
206
+ // ─── GORM Model Extraction ───
207
+ /**
208
+ * Extract GORM model structs from a Go file.
209
+ * Parses struct bodies, field types, and gorm tags.
210
+ */
211
+ export function extractGORMModelsStructured(_filePath, content) {
212
+ const models = [];
213
+ // Find structs that embed gorm.Model or have gorm tags
214
+ const structPattern = /type\s+(\w+)\s+struct\s*\{/g;
215
+ let match;
216
+ while ((match = structPattern.exec(content)) !== null) {
217
+ const name = match[1];
218
+ const bodyStart = match.index + match[0].length;
219
+ const body = extractBraceBlock(content, bodyStart);
220
+ if (!body)
221
+ continue;
222
+ // Check if this is a GORM model
223
+ const isGormModel = body.includes("gorm.Model") ||
224
+ body.includes("gorm.DeletedAt") ||
225
+ body.includes("`gorm:") ||
226
+ body.includes("`json:");
227
+ if (!isGormModel)
228
+ continue;
229
+ const fields = [];
230
+ const relations = [];
231
+ const lines = body.split("\n");
232
+ for (const line of lines) {
233
+ const trimmed = line.trim();
234
+ if (!trimmed || trimmed.startsWith("//") || trimmed === "}" || trimmed === "{")
235
+ continue;
236
+ // Embedded model: gorm.Model
237
+ if (trimmed === "gorm.Model") {
238
+ fields.push({ name: "ID", type: "uint", flags: ["pk"] });
239
+ fields.push({ name: "CreatedAt", type: "time.Time", flags: [] });
240
+ fields.push({ name: "UpdatedAt", type: "time.Time", flags: [] });
241
+ fields.push({ name: "DeletedAt", type: "gorm.DeletedAt", flags: ["nullable"] });
242
+ continue;
243
+ }
244
+ // Parse field: Name Type `gorm:"..." json:"..."`
245
+ const fieldMatch = trimmed.match(/^(\w+)\s+([\w.*\[\]]+)\s*(?:`(.+)`)?/);
246
+ if (!fieldMatch)
247
+ continue;
248
+ const fieldName = fieldMatch[1];
249
+ const fieldType = fieldMatch[2];
250
+ const tagStr = fieldMatch[3] || "";
251
+ // Skip embedded structs that aren't fields
252
+ if (fieldType.startsWith("*") || fieldType.includes(".")) {
253
+ // Check if it's a relation
254
+ if (trimmed.includes("gorm:\"foreignKey") || trimmed.includes("gorm:\"many2many")) {
255
+ relations.push(`${fieldName}: ${fieldType.replace("*", "").replace("[]", "")}`);
256
+ continue;
257
+ }
258
+ // Check for belongs_to / has_many by type
259
+ if (fieldType.startsWith("[]") || fieldType.startsWith("*")) {
260
+ const relType = fieldType.replace("*", "").replace("[]", "");
261
+ if (relType[0] === relType[0]?.toUpperCase() && !relType.includes(".")) {
262
+ relations.push(`${fieldName}: ${relType}`);
263
+ continue;
264
+ }
265
+ }
266
+ }
267
+ const flags = [];
268
+ // Parse gorm tag
269
+ const gormTag = tagStr.match(/gorm:"([^"]+)"/)?.[1] || "";
270
+ if (gormTag) {
271
+ if (gormTag.includes("primaryKey") || gormTag.includes("primarykey"))
272
+ flags.push("pk");
273
+ if (gormTag.includes("unique"))
274
+ flags.push("unique");
275
+ if (gormTag.includes("not null"))
276
+ flags.push("required");
277
+ if (gormTag.includes("default:"))
278
+ flags.push("default");
279
+ if (gormTag.includes("index"))
280
+ flags.push("index");
281
+ if (gormTag.includes("foreignKey") || gormTag.includes("foreignkey"))
282
+ flags.push("fk");
283
+ }
284
+ fields.push({ name: fieldName, type: fieldType, flags });
285
+ }
286
+ if (fields.length > 0) {
287
+ models.push({
288
+ name,
289
+ fields,
290
+ relations,
291
+ orm: "gorm",
292
+ confidence: "ast",
293
+ });
294
+ }
295
+ }
296
+ return models;
297
+ }
298
+ // ─── Helpers ───
299
+ /**
300
+ * Extract the content between matched braces starting at position.
301
+ * Returns the content inside the braces (not including the opening brace).
302
+ */
303
+ function extractBraceBlock(content, startAfterOpenBrace) {
304
+ let depth = 1;
305
+ let i = startAfterOpenBrace;
306
+ while (i < content.length && depth > 0) {
307
+ if (content[i] === "{")
308
+ depth++;
309
+ else if (content[i] === "}")
310
+ depth--;
311
+ i++;
312
+ }
313
+ if (depth !== 0)
314
+ return null;
315
+ return content.slice(startAfterOpenBrace, i - 1);
316
+ }
317
+ function extractPathParams(path) {
318
+ const params = [];
319
+ // Gin/Echo: :param, Chi: {param}, Go 1.22: {param}
320
+ const regex = /[:{}](\w+)/g;
321
+ let m;
322
+ while ((m = regex.exec(path)) !== null)
323
+ params.push(m[1]);
324
+ return params;
325
+ }
326
+ function normalizePath(path) {
327
+ return path.replace(/\/+/g, "/").replace(/\/$/, "") || "/";
328
+ }
329
+ function escapeRegex(str) {
330
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
331
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Python AST extraction via subprocess.
3
+ * Spawns python3 with an inline script using stdlib `ast` module.
4
+ * Zero dependencies — if the project uses Python, the interpreter is there.
5
+ *
6
+ * Extracts:
7
+ * - FastAPI/Flask route decorators with precise path + method
8
+ * - Django urlpatterns with path() calls
9
+ * - SQLAlchemy model classes with Column types, flags, and relationships
10
+ */
11
+ import type { RouteInfo, SchemaModel, Framework } from "../types.js";
12
+ /**
13
+ * Extract routes from a Python file using AST.
14
+ * Returns routes with confidence: "ast", or null if Python is unavailable.
15
+ */
16
+ export declare function extractPythonRoutesAST(filePath: string, content: string, framework: Framework, tags: string[]): Promise<RouteInfo[] | null>;
17
+ /**
18
+ * Extract SQLAlchemy models from a Python file using AST.
19
+ * Returns models with confidence: "ast", or null if Python is unavailable.
20
+ */
21
+ export declare function extractSQLAlchemyAST(filePath: string, content: string): Promise<SchemaModel[] | null>;
22
+ /**
23
+ * Check if Python 3 is available on this system.
24
+ */
25
+ export declare function isPythonAvailable(): Promise<boolean>;
@@ -0,0 +1,388 @@
1
+ /**
2
+ * Python AST extraction via subprocess.
3
+ * Spawns python3 with an inline script using stdlib `ast` module.
4
+ * Zero dependencies — if the project uses Python, the interpreter is there.
5
+ *
6
+ * Extracts:
7
+ * - FastAPI/Flask route decorators with precise path + method
8
+ * - Django urlpatterns with path() calls
9
+ * - SQLAlchemy model classes with Column types, flags, and relationships
10
+ */
11
+ import { execFile } from "node:child_process";
12
+ import { promisify } from "node:util";
13
+ const execFileP = promisify(execFile);
14
+ // The Python script that does AST parsing via stdlib
15
+ // Outputs JSON to stdout
16
+ const PYTHON_ROUTE_SCRIPT = `
17
+ import ast, json, sys
18
+
19
+ def extract_routes(source, filename):
20
+ try:
21
+ tree = ast.parse(source, filename)
22
+ except SyntaxError:
23
+ return []
24
+
25
+ routes = []
26
+
27
+ for node in ast.walk(tree):
28
+ # FastAPI/Flask style: @router.get("/path") or @app.route("/path", methods=["GET","POST"])
29
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
30
+ for dec in node.decorator_list:
31
+ route = parse_route_decorator(dec)
32
+ if route:
33
+ for r in route:
34
+ routes.append(r)
35
+
36
+ # Django: path("url", view) in urlpatterns list
37
+ if isinstance(node, ast.Assign):
38
+ for target in node.targets:
39
+ if isinstance(target, ast.Name) and target.id == 'urlpatterns':
40
+ if isinstance(node.value, (ast.List, ast.BinOp)):
41
+ routes.extend(parse_urlpatterns(node.value))
42
+
43
+ return routes
44
+
45
+ def parse_route_decorator(dec):
46
+ results = []
47
+
48
+ # @router.get("/path") / @app.post("/path") — Attribute call
49
+ if isinstance(dec, ast.Call) and isinstance(dec.func, ast.Attribute):
50
+ method_name = dec.func.attr.upper()
51
+ methods_map = {'GET':'GET','POST':'POST','PUT':'PUT','PATCH':'PATCH',
52
+ 'DELETE':'DELETE','OPTIONS':'OPTIONS','HEAD':'HEAD'}
53
+
54
+ if method_name in methods_map and dec.args:
55
+ path = get_str(dec.args[0])
56
+ if path is not None:
57
+ results.append({'method': methods_map[method_name], 'path': path})
58
+
59
+ # @app.route("/path", methods=["GET","POST"])
60
+ if dec.func.attr == 'route' and dec.args:
61
+ path = get_str(dec.args[0])
62
+ if path is not None:
63
+ methods = ['GET']
64
+ for kw in dec.keywords:
65
+ if kw.arg == 'methods' and isinstance(kw.value, ast.List):
66
+ methods = [get_str(e) for e in kw.value.elts if get_str(e)]
67
+ for m in methods:
68
+ results.append({'method': m.upper(), 'path': path})
69
+
70
+ # @app.api_route("/path", methods=["GET","POST"]) — FastAPI api_route
71
+ if isinstance(dec, ast.Call) and isinstance(dec.func, ast.Attribute):
72
+ if dec.func.attr == 'api_route' and dec.args:
73
+ path = get_str(dec.args[0])
74
+ if path is not None:
75
+ methods = ['GET']
76
+ for kw in dec.keywords:
77
+ if kw.arg == 'methods' and isinstance(kw.value, ast.List):
78
+ methods = [get_str(e) for e in kw.value.elts if get_str(e)]
79
+ for m in methods:
80
+ results.append({'method': m.upper(), 'path': path})
81
+
82
+ return results if results else None
83
+
84
+ def parse_urlpatterns(node):
85
+ routes = []
86
+ elements = []
87
+ if isinstance(node, ast.List):
88
+ elements = node.elts
89
+ elif isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add):
90
+ # urlpatterns = [...] + [...]
91
+ if isinstance(node.left, ast.List):
92
+ elements.extend(node.left.elts)
93
+ if isinstance(node.right, ast.List):
94
+ elements.extend(node.right.elts)
95
+
96
+ for elt in elements:
97
+ if isinstance(elt, ast.Call):
98
+ func_name = ''
99
+ if isinstance(elt.func, ast.Name):
100
+ func_name = elt.func.id
101
+ elif isinstance(elt.func, ast.Attribute):
102
+ func_name = elt.func.attr
103
+
104
+ if func_name in ('path', 're_path', 'url') and elt.args:
105
+ path_str = get_str(elt.args[0])
106
+ if path_str is not None:
107
+ routes.append({'method': 'ALL', 'path': '/' + path_str.lstrip('/')})
108
+
109
+ return routes
110
+
111
+ def get_str(node):
112
+ if isinstance(node, ast.Constant) and isinstance(node.value, str):
113
+ return node.value
114
+ if isinstance(node, ast.Str): # Python 3.7
115
+ return node.s
116
+ return None
117
+
118
+ source = sys.stdin.read()
119
+ filename = sys.argv[1] if len(sys.argv) > 1 else '<stdin>'
120
+ result = extract_routes(source, filename)
121
+ print(json.dumps(result))
122
+ `;
123
+ const PYTHON_SCHEMA_SCRIPT = `
124
+ import ast, json, sys
125
+
126
+ def extract_models(source, filename):
127
+ try:
128
+ tree = ast.parse(source, filename)
129
+ except SyntaxError:
130
+ return []
131
+
132
+ models = []
133
+
134
+ for node in ast.walk(tree):
135
+ if not isinstance(node, ast.ClassDef):
136
+ continue
137
+
138
+ # Check if inherits from Base, Model, DeclarativeBase, db.Model
139
+ is_model = False
140
+ for base in node.bases:
141
+ name = ''
142
+ if isinstance(base, ast.Name):
143
+ name = base.id
144
+ elif isinstance(base, ast.Attribute):
145
+ name = base.attr
146
+ if name in ('Base', 'Model', 'DeclarativeBase', 'AbstractBase'):
147
+ is_model = True
148
+ break
149
+ if not is_model:
150
+ continue
151
+
152
+ fields = []
153
+ relations = []
154
+
155
+ for item in node.body:
156
+ # Column assignments: field = Column(Type, ...) or field: Mapped[Type] = mapped_column(...)
157
+ if isinstance(item, ast.Assign):
158
+ for target in item.targets:
159
+ if isinstance(target, ast.Name):
160
+ field_info = parse_column_or_rel(target.id, item.value)
161
+ if field_info:
162
+ if field_info['kind'] == 'field':
163
+ fields.append(field_info)
164
+ else:
165
+ relations.append(field_info)
166
+
167
+ if isinstance(item, ast.AnnAssign) and isinstance(item.target, ast.Name):
168
+ field_name = item.target.id
169
+ if field_name.startswith('__'):
170
+ continue
171
+ if item.value:
172
+ field_info = parse_column_or_rel(field_name, item.value)
173
+ if field_info:
174
+ if field_info['kind'] == 'field':
175
+ # Try to get type from annotation
176
+ ann_type = extract_mapped_type(item.annotation)
177
+ if ann_type and field_info['type'] == 'unknown':
178
+ field_info['type'] = ann_type
179
+ fields.append(field_info)
180
+ else:
181
+ relations.append(field_info)
182
+
183
+ if fields:
184
+ models.append({
185
+ 'name': node.name,
186
+ 'fields': fields,
187
+ 'relations': relations,
188
+ })
189
+
190
+ return models
191
+
192
+ def parse_column_or_rel(name, value):
193
+ if name.startswith('__'):
194
+ return None
195
+
196
+ # relationship("Model")
197
+ if isinstance(value, ast.Call):
198
+ func_name = get_func_name(value)
199
+
200
+ if func_name in ('relationship', 'db.relationship'):
201
+ target = ''
202
+ if value.args:
203
+ target = get_str(value.args[0]) or ''
204
+ return {'kind': 'relation', 'name': name, 'target': target}
205
+
206
+ if func_name in ('Column', 'db.Column', 'mapped_column'):
207
+ col_type = 'unknown'
208
+ flags = []
209
+
210
+ for arg in value.args:
211
+ t = get_type_name(arg)
212
+ if t:
213
+ col_type = t
214
+ if isinstance(arg, ast.Call) and get_func_name(arg) == 'ForeignKey':
215
+ flags.append('fk')
216
+
217
+ for kw in value.keywords:
218
+ if kw.arg == 'primary_key' and is_true(kw.value):
219
+ flags.append('pk')
220
+ elif kw.arg == 'unique' and is_true(kw.value):
221
+ flags.append('unique')
222
+ elif kw.arg == 'nullable' and is_true(kw.value):
223
+ flags.append('nullable')
224
+ elif kw.arg == 'default':
225
+ flags.append('default')
226
+ elif kw.arg == 'index' and is_true(kw.value):
227
+ flags.append('index')
228
+
229
+ return {'kind': 'field', 'name': name, 'type': col_type, 'flags': flags}
230
+
231
+ return None
232
+
233
+ def extract_mapped_type(annotation):
234
+ # Mapped[int] or Mapped[Optional[str]]
235
+ if isinstance(annotation, ast.Subscript):
236
+ if isinstance(annotation.value, ast.Name) and annotation.value.id == 'Mapped':
237
+ slice_node = annotation.slice
238
+ if isinstance(slice_node, ast.Name):
239
+ return slice_node.id
240
+ if isinstance(slice_node, ast.Subscript) and isinstance(slice_node.value, ast.Name):
241
+ if slice_node.value.id == 'Optional':
242
+ inner = slice_node.slice
243
+ if isinstance(inner, ast.Name):
244
+ return inner.id
245
+ return None
246
+
247
+ def get_func_name(node):
248
+ if isinstance(node.func, ast.Name):
249
+ return node.func.id
250
+ if isinstance(node.func, ast.Attribute):
251
+ if isinstance(node.func.value, ast.Name):
252
+ return node.func.value.id + '.' + node.func.attr
253
+ return node.func.attr
254
+ return ''
255
+
256
+ def get_type_name(node):
257
+ if isinstance(node, ast.Name):
258
+ known = {'String','Integer','Boolean','Float','Text','DateTime','JSON',
259
+ 'UUID','BigInteger','SmallInteger','Numeric','Date','Time',
260
+ 'LargeBinary','Enum','ARRAY','JSONB'}
261
+ if node.id in known:
262
+ return node.id
263
+ if isinstance(node, ast.Call):
264
+ return get_type_name(node.func)
265
+ if isinstance(node, ast.Attribute):
266
+ return node.attr
267
+ return None
268
+
269
+ def get_str(node):
270
+ if isinstance(node, ast.Constant) and isinstance(node.value, str):
271
+ return node.value
272
+ return None
273
+
274
+ def is_true(node):
275
+ if isinstance(node, ast.Constant):
276
+ return node.value is True
277
+ if isinstance(node, ast.NameConstant): # Python 3.7
278
+ return node.value is True
279
+ return False
280
+
281
+ source = sys.stdin.read()
282
+ filename = sys.argv[1] if len(sys.argv) > 1 else '<stdin>'
283
+ result = extract_models(source, filename)
284
+ print(json.dumps(result))
285
+ `;
286
+ let pythonAvailable = null;
287
+ let pythonCmd = "python3";
288
+ async function findPython() {
289
+ if (pythonAvailable !== null)
290
+ return pythonAvailable;
291
+ for (const cmd of ["python3", "python"]) {
292
+ try {
293
+ const { stdout } = await execFileP(cmd, ["--version"], { timeout: 3000 });
294
+ if (stdout.includes("Python 3")) {
295
+ pythonCmd = cmd;
296
+ pythonAvailable = true;
297
+ return true;
298
+ }
299
+ }
300
+ catch { }
301
+ }
302
+ pythonAvailable = false;
303
+ return false;
304
+ }
305
+ import { spawn } from "node:child_process";
306
+ async function runPythonWithStdin(script, source, filename) {
307
+ if (!(await findPython()))
308
+ return null;
309
+ return new Promise((resolve) => {
310
+ const proc = spawn(pythonCmd, ["-c", script, filename], {
311
+ timeout: 10000,
312
+ env: { ...process.env, PYTHONDONTWRITEBYTECODE: "1" },
313
+ stdio: ["pipe", "pipe", "pipe"],
314
+ });
315
+ let stdout = "";
316
+ let stderr = "";
317
+ proc.stdout.on("data", (data) => { stdout += data.toString(); });
318
+ proc.stderr.on("data", (data) => { stderr += data.toString(); });
319
+ proc.on("close", (code) => {
320
+ if (code !== 0 || !stdout.trim()) {
321
+ resolve(null);
322
+ return;
323
+ }
324
+ try {
325
+ resolve(JSON.parse(stdout.trim()));
326
+ }
327
+ catch {
328
+ resolve(null);
329
+ }
330
+ });
331
+ proc.on("error", () => resolve(null));
332
+ proc.stdin.write(source);
333
+ proc.stdin.end();
334
+ });
335
+ }
336
+ /**
337
+ * Extract routes from a Python file using AST.
338
+ * Returns routes with confidence: "ast", or null if Python is unavailable.
339
+ */
340
+ export async function extractPythonRoutesAST(filePath, content, framework, tags) {
341
+ const result = await runPythonWithStdin(PYTHON_ROUTE_SCRIPT, content, filePath);
342
+ if (!result || !Array.isArray(result) || result.length === 0)
343
+ return null;
344
+ return result.map((r) => ({
345
+ method: r.method,
346
+ path: r.path,
347
+ file: filePath,
348
+ tags,
349
+ framework,
350
+ params: extractPathParams(r.path),
351
+ confidence: "ast",
352
+ }));
353
+ }
354
+ /**
355
+ * Extract SQLAlchemy models from a Python file using AST.
356
+ * Returns models with confidence: "ast", or null if Python is unavailable.
357
+ */
358
+ export async function extractSQLAlchemyAST(filePath, content) {
359
+ const result = await runPythonWithStdin(PYTHON_SCHEMA_SCRIPT, content, filePath);
360
+ if (!result || !Array.isArray(result) || result.length === 0)
361
+ return null;
362
+ return result.map((m) => ({
363
+ name: m.name,
364
+ fields: (m.fields || []).map((f) => ({
365
+ name: f.name,
366
+ type: f.type || "unknown",
367
+ flags: f.flags || [],
368
+ })),
369
+ relations: (m.relations || []).map((r) => `${r.name}: ${r.target}`),
370
+ orm: "sqlalchemy",
371
+ confidence: "ast",
372
+ }));
373
+ }
374
+ /**
375
+ * Check if Python 3 is available on this system.
376
+ */
377
+ export async function isPythonAvailable() {
378
+ return findPython();
379
+ }
380
+ function extractPathParams(path) {
381
+ const params = [];
382
+ // FastAPI: {param} / Flask: <param> / Django: <type:param>
383
+ const regex = /[{<](?:\w+:)?(\w+)[}>]/g;
384
+ let m;
385
+ while ((m = regex.exec(path)) !== null)
386
+ params.push(m[1]);
387
+ return params;
388
+ }
@@ -2,6 +2,8 @@ import { relative, basename } from "node:path";
2
2
  import { readFileSafe } from "../scanner.js";
3
3
  import { loadTypeScript } from "../ast/loader.js";
4
4
  import { extractRoutesAST } from "../ast/extract-routes.js";
5
+ import { extractPythonRoutesAST } from "../ast/extract-python.js";
6
+ import { extractGoRoutesStructured } from "../ast/extract-go.js";
5
7
  const HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"];
6
8
  const TAG_PATTERNS = [
7
9
  ["auth", [/auth/i, /jwt/i, /token/i, /session/i, /bearer/i, /passport/i, /clerk/i, /betterAuth/i, /better-auth/i]],
@@ -581,6 +583,14 @@ async function detectFastAPIRoutes(files, project) {
581
583
  if (!content.includes("fastapi") && !content.includes("FastAPI") && !content.includes("APIRouter"))
582
584
  continue;
583
585
  const rel = relative(project.root, file);
586
+ const tags = detectTags(content);
587
+ // Try Python AST first
588
+ const astRoutes = await extractPythonRoutesAST(rel, content, "fastapi", tags);
589
+ if (astRoutes && astRoutes.length > 0) {
590
+ routes.push(...astRoutes);
591
+ continue;
592
+ }
593
+ // Fallback to regex
584
594
  const routePattern = /@\w+\s*\.\s*(get|post|put|patch|delete|options)\s*\(\s*['"]([^'"]+)['"]/gi;
585
595
  let match;
586
596
  while ((match = routePattern.exec(content)) !== null) {
@@ -588,7 +598,7 @@ async function detectFastAPIRoutes(files, project) {
588
598
  method: match[1].toUpperCase(),
589
599
  path: match[2],
590
600
  file: rel,
591
- tags: detectTags(content),
601
+ tags,
592
602
  framework: "fastapi",
593
603
  });
594
604
  }
@@ -604,6 +614,14 @@ async function detectFlaskRoutes(files, project) {
604
614
  if (!content.includes("flask") && !content.includes("Flask") && !content.includes("Blueprint"))
605
615
  continue;
606
616
  const rel = relative(project.root, file);
617
+ const tags = detectTags(content);
618
+ // Try Python AST first
619
+ const astRoutes = await extractPythonRoutesAST(rel, content, "flask", tags);
620
+ if (astRoutes && astRoutes.length > 0) {
621
+ routes.push(...astRoutes);
622
+ continue;
623
+ }
624
+ // Fallback to regex
607
625
  const routePattern = /@(?:app|bp|blueprint|\w+)\s*\.\s*route\s*\(\s*['"]([^'"]+)['"](?:\s*,\s*methods\s*=\s*\[([^\]]+)\])?\s*\)/gi;
608
626
  let match;
609
627
  while ((match = routePattern.exec(content)) !== null) {
@@ -616,7 +634,7 @@ async function detectFlaskRoutes(files, project) {
616
634
  method,
617
635
  path,
618
636
  file: rel,
619
- tags: detectTags(content),
637
+ tags,
620
638
  framework: "flask",
621
639
  });
622
640
  }
@@ -631,7 +649,14 @@ async function detectDjangoRoutes(files, project) {
631
649
  for (const file of pyFiles) {
632
650
  const content = await readFileSafe(file);
633
651
  const rel = relative(project.root, file);
634
- // path("api/v1/users/", views.UserView.as_view())
652
+ const tags = detectTags(content);
653
+ // Try Python AST first
654
+ const astRoutes = await extractPythonRoutesAST(rel, content, "django", tags);
655
+ if (astRoutes && astRoutes.length > 0) {
656
+ routes.push(...astRoutes);
657
+ continue;
658
+ }
659
+ // Fallback to regex
635
660
  const pathPattern = /path\s*\(\s*['"]([^'"]*)['"]\s*,/g;
636
661
  let match;
637
662
  while ((match = pathPattern.exec(content)) !== null) {
@@ -639,7 +664,7 @@ async function detectDjangoRoutes(files, project) {
639
664
  method: "ALL",
640
665
  path: "/" + match[1],
641
666
  file: rel,
642
- tags: detectTags(content),
667
+ tags,
643
668
  framework: "django",
644
669
  });
645
670
  }
@@ -653,72 +678,43 @@ async function detectGoRoutes(files, project, fw) {
653
678
  for (const file of goFiles) {
654
679
  const content = await readFileSafe(file);
655
680
  const rel = relative(project.root, file);
656
- if (fw === "gin") {
657
- const pattern = /\.\s*(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s*\(\s*["']([^"']+)["']/g;
658
- let match;
659
- while ((match = pattern.exec(content)) !== null) {
660
- routes.push({
661
- method: match[1],
662
- path: match[2],
663
- file: rel,
664
- tags: detectTags(content),
665
- framework: fw,
666
- });
667
- }
668
- }
669
- else if (fw === "fiber") {
670
- const pattern = /\.\s*(Get|Post|Put|Patch|Delete|Options|Head)\s*\(\s*["']([^"']+)["']/g;
671
- let match;
672
- while ((match = pattern.exec(content)) !== null) {
673
- routes.push({
674
- method: match[1].toUpperCase(),
675
- path: match[2],
676
- file: rel,
677
- tags: detectTags(content),
678
- framework: fw,
679
- });
680
- }
681
+ const tags = detectTags(content);
682
+ // Use structured parser (brace-tracking + group prefix resolution)
683
+ const structuredRoutes = extractGoRoutesStructured(rel, content, fw, tags);
684
+ if (structuredRoutes.length > 0) {
685
+ routes.push(...structuredRoutes);
686
+ continue;
681
687
  }
682
- else if (fw === "echo") {
683
- // e.GET("/path", handler) or g.POST("/path", handler)
688
+ // Fallback to simple regex for files where structured parser found nothing
689
+ if (fw === "gin" || fw === "echo") {
684
690
  const pattern = /\.\s*(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s*\(\s*["']([^"']+)["']/g;
685
691
  let match;
686
692
  while ((match = pattern.exec(content)) !== null) {
687
- routes.push({
688
- method: match[1],
689
- path: match[2],
690
- file: rel,
691
- tags: detectTags(content),
692
- framework: fw,
693
- });
693
+ routes.push({ method: match[1], path: match[2], file: rel, tags, framework: fw });
694
694
  }
695
695
  }
696
- else if (fw === "chi") {
697
- // r.Get("/path", handler), r.Post("/path", handler)
696
+ else if (fw === "fiber" || fw === "chi") {
698
697
  const pattern = /\.\s*(Get|Post|Put|Patch|Delete|Options|Head)\s*\(\s*["']([^"']+)["']/g;
699
698
  let match;
700
699
  while ((match = pattern.exec(content)) !== null) {
701
- routes.push({
702
- method: match[1].toUpperCase(),
703
- path: match[2],
704
- file: rel,
705
- tags: detectTags(content),
706
- framework: fw,
707
- });
700
+ routes.push({ method: match[1].toUpperCase(), path: match[2], file: rel, tags, framework: fw });
708
701
  }
709
702
  }
710
703
  else {
711
- // net/http: http.HandleFunc("/path", handler) or mux.HandleFunc("/path", handler)
704
+ // net/http
712
705
  const pattern = /(?:HandleFunc|Handle)\s*\(\s*["']([^"']+)["']/g;
713
706
  let match;
714
707
  while ((match = pattern.exec(content)) !== null) {
715
- routes.push({
716
- method: "ALL",
717
- path: match[1],
718
- file: rel,
719
- tags: detectTags(content),
720
- framework: fw,
721
- });
708
+ // Go 1.22+: "GET /path" patterns
709
+ const pathStr = match[1];
710
+ let method = "ALL";
711
+ let path = pathStr;
712
+ const methodMatch = pathStr.match(/^(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s+(\/.*)/);
713
+ if (methodMatch) {
714
+ method = methodMatch[1];
715
+ path = methodMatch[2];
716
+ }
717
+ routes.push({ method, path, file: rel, tags, framework: fw });
722
718
  }
723
719
  }
724
720
  }
@@ -1,7 +1,9 @@
1
- import { join } from "node:path";
1
+ import { join, relative } from "node:path";
2
2
  import { readFileSafe } from "../scanner.js";
3
3
  import { loadTypeScript } from "../ast/loader.js";
4
4
  import { extractDrizzleSchemaAST, extractTypeORMSchemaAST } from "../ast/extract-schema.js";
5
+ import { extractSQLAlchemyAST } from "../ast/extract-python.js";
6
+ import { extractGORMModelsStructured } from "../ast/extract-go.js";
5
7
  const AUDIT_FIELDS = new Set([
6
8
  "createdAt",
7
9
  "updatedAt",
@@ -26,6 +28,9 @@ export async function detectSchemas(files, project) {
26
28
  case "sqlalchemy":
27
29
  models.push(...(await detectSQLAlchemySchemas(files, project)));
28
30
  break;
31
+ case "gorm":
32
+ models.push(...(await detectGORMSchemas(files, project)));
33
+ break;
29
34
  }
30
35
  }
31
36
  return models;
@@ -256,7 +261,14 @@ async function detectSQLAlchemySchemas(files, project) {
256
261
  continue;
257
262
  if (!content.includes("Base") && !content.includes("DeclarativeBase") && !content.includes("Model"))
258
263
  continue;
259
- // Match class definitions
264
+ const rel = relative(project.root, file);
265
+ // Try Python AST first
266
+ const astModels = await extractSQLAlchemyAST(rel, content);
267
+ if (astModels && astModels.length > 0) {
268
+ models.push(...astModels);
269
+ continue;
270
+ }
271
+ // Fallback to regex
260
272
  const classPattern = /class\s+(\w+)\s*\([^)]*(?:Base|Model|DeclarativeBase)[^)]*\)\s*:([\s\S]*?)(?=\nclass\s|\n[^\s]|$)/g;
261
273
  let match;
262
274
  while ((match = classPattern.exec(content)) !== null) {
@@ -301,3 +313,17 @@ async function detectSQLAlchemySchemas(files, project) {
301
313
  }
302
314
  return models;
303
315
  }
316
+ // --- GORM ---
317
+ async function detectGORMSchemas(files, _project) {
318
+ const goFiles = files.filter((f) => f.endsWith(".go"));
319
+ const models = [];
320
+ for (const file of goFiles) {
321
+ const content = await readFileSafe(file);
322
+ if (!content.includes("gorm") && !content.includes("Model") && !content.includes("`json:"))
323
+ continue;
324
+ const rel = relative(_project.root, file);
325
+ const structModels = extractGORMModelsStructured(rel, content);
326
+ models.push(...structModels);
327
+ }
328
+ return models;
329
+ }
package/dist/formatter.js CHANGED
@@ -239,7 +239,9 @@ function formatCombined(result, sections) {
239
239
  // Token stats
240
240
  const ts = result.tokenStats;
241
241
  lines.push(`> ${result.routes.length} routes | ${result.schemas.length} models | ${result.components.length} components | ${result.libs.length} lib files | ${result.config.envVars.length} env vars | ${result.middleware.length} middleware | ${result.graph.edges.length} import links`);
242
- lines.push(`> **Token savings:** this file is ~${ts.outputTokens.toLocaleString()} tokens. Without it, AI exploration would cost ~${ts.estimatedExplorationTokens.toLocaleString()} tokens. **Saves ~${ts.saved.toLocaleString()} tokens per conversation.**`);
242
+ // Round to nearest 100 to keep output deterministic across runs (avoids git conflicts in worktrees)
243
+ const roundTo100 = (n) => Math.round(n / 100) * 100;
244
+ lines.push(`> **Token savings:** this file is ~${roundTo100(ts.outputTokens).toLocaleString()} tokens. Without it, AI exploration would cost ~${roundTo100(ts.estimatedExplorationTokens).toLocaleString()} tokens. **Saves ~${roundTo100(ts.saved).toLocaleString()} tokens per conversation.**`);
243
245
  lines.push("");
244
246
  lines.push("---");
245
247
  lines.push("");
package/dist/index.js CHANGED
@@ -15,7 +15,7 @@ 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
+ const VERSION = "1.5.1";
19
19
  const BRAND = "codesight";
20
20
  function printHelp() {
21
21
  console.log(`
@@ -396,9 +396,9 @@ async function main() {
396
396
  const reportPath = await generateHtmlReport(result, outputDir);
397
397
  console.log(` ${outputDirName}/report.html`);
398
398
  if (doOpen) {
399
- const { exec } = await import("node:child_process");
399
+ const { execFile } = await import("node:child_process");
400
400
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
401
- exec(`${cmd} "${reportPath}"`);
401
+ execFile(cmd, [reportPath]);
402
402
  console.log(" Opening in browser...");
403
403
  }
404
404
  }
package/dist/scanner.js CHANGED
@@ -90,7 +90,7 @@ export async function detectProject(root) {
90
90
  pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
91
91
  }
92
92
  catch { }
93
- const name = pkg.name || basename(root);
93
+ const name = pkg.name || await resolveRepoName(root);
94
94
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
95
95
  // Detect monorepo
96
96
  const isMonorepo = !!(pkg.workspaces || await fileExists(join(root, "pnpm-workspace.yaml")));
@@ -212,11 +212,11 @@ async function detectFrameworks(root, pkg) {
212
212
  frameworks.push("django");
213
213
  // Go frameworks - check go.mod
214
214
  const goDeps = await getGoDeps(root);
215
- if (goDeps.includes("net/http"))
215
+ if (goDeps.some((d) => d.includes("net/http")))
216
216
  frameworks.push("go-net-http");
217
- if (goDeps.includes("gin-gonic/gin"))
217
+ if (goDeps.some((d) => d.includes("gin-gonic/gin")))
218
218
  frameworks.push("gin");
219
- if (goDeps.includes("gofiber/fiber"))
219
+ if (goDeps.some((d) => d.includes("gofiber/fiber")))
220
220
  frameworks.push("fiber");
221
221
  if (goDeps.some((d) => d.includes("labstack/echo")))
222
222
  frameworks.push("echo");
@@ -429,6 +429,33 @@ async function getGoDeps(root) {
429
429
  catch { }
430
430
  return deps;
431
431
  }
432
+ /**
433
+ * Resolve the repo name, handling git worktrees.
434
+ * In a worktree, basename(root) is a random name — resolve the actual repo instead.
435
+ */
436
+ async function resolveRepoName(root) {
437
+ try {
438
+ // Check if .git is a file (worktree) vs directory (normal repo)
439
+ const gitPath = join(root, ".git");
440
+ const gitStat = await stat(gitPath);
441
+ if (gitStat.isFile()) {
442
+ // Worktree: .git is a file containing "gitdir: /path/to/main/.git/worktrees/name"
443
+ const gitContent = await readFile(gitPath, "utf-8");
444
+ const gitdirMatch = gitContent.match(/gitdir:\s*(.+)/);
445
+ if (gitdirMatch) {
446
+ // Resolve back to main repo: /repo/.git/worktrees/name -> /repo
447
+ const worktreeGitDir = gitdirMatch[1].trim();
448
+ // Go up from .git/worktrees/name to the repo root
449
+ const mainGitDir = join(worktreeGitDir, "..", "..");
450
+ const mainRepoRoot = join(mainGitDir, "..");
451
+ return basename(mainRepoRoot);
452
+ }
453
+ }
454
+ }
455
+ catch { }
456
+ // Fallback: use directory name
457
+ return basename(root);
458
+ }
432
459
  async function fileExists(path) {
433
460
  try {
434
461
  await stat(path);
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codesight",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
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": {