From f8babd56dece4cab8106e8ad45ab2a6c7fb91fce Mon Sep 17 00:00:00 2001 From: Neil Chambers Date: Mon, 13 Apr 2026 18:17:59 +0100 Subject: [PATCH 1/6] feat: terraform infrastructure plugin + customSections plugin API (#1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add customSections to plugin API for first-class plugin output Plugins could previously only return routes, schemas, components, and middleware — they had no way to contribute new types of content to CODESIGHT.md. This meant plugin-generated insights were invisible to agents unless they knew to look for a separate file. Add customSections to PluginDetectorResult and ScanResult so plugins can return arbitrary markdown sections that get rendered into CODESIGHT.md alongside built-in sections, written as individual .md files, and referenced in AI config files (CLAUDE.md, .cursorrules, etc). Co-Authored-By: Claude Opus 4.6 (1M context) * feat: add terraform infrastructure plugin (AWS-focused) Add a plugin that scans Terraform/HCL files and generates infrastructure.md with deployment context for AI agents — where a service runs, what env vars and SSM secrets it receives, whether it's public-facing, what it depends on, and per-environment overrides. Supports two modes: in-project (terraform/ subdir alongside code) and external path (separate infrastructure repo, default ../infrastructure). Uses regex + brace-counting for zero-dependency HCL parsing, following the same approach as the Go extractor. The HCL parser and service matcher are provider-agnostic, but the infrastructure extractor currently targets AWS patterns (ECS, SSM Parameter Store, ALB, Route53, CloudWatch). Azure and GCP extraction would require additional provider-specific logic in the extractor — the parser and matcher layers would not need changes. Co-Authored-By: Claude Opus 4.6 (1M context) * fix: address PR review comments - formatter.ts: sanitise customSections names to safe basenames and reject collisions with built-in section names - hcl-parser.ts: skip comment stripping inside heredoc bodies to avoid corrupting literal content - extractor.ts: fix IAM statement extraction to read action/actions instead of falling back to effect - package.json: add dist/* wildcard export to preserve deep imports, update test script to run all test files Co-Authored-By: Claude Opus 4.6 (1M context) * fix: address review findings — reserved name bypass, name mismatch, regex safety - Fix reserved name "CODESIGHT" casing mismatch in formatter.ts (was uppercase, safeName is always lowercased so the guard never matched) - Sanitize section names in ai-config.ts to match filenames written by formatter.ts - Move BLOCK_HEADER regex inside function body to prevent shared mutable state under concurrent use - Add string-aware bracket counting in multi-line list parser - Remove dead TF_EXTENSIONS constant - Add informational TODOs for parallel file reads, parseTfvars multiline limitation, and custom section name collision Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- src/index.ts | 172 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) diff --git a/src/index.ts b/src/index.ts index bc5d2e2..b0fecd5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -76,6 +76,178 @@ async function fileExists(path: string): Promise { } } +async function scan(root: string, outputDirName: string, maxDepth: number, userConfig: CodesightConfig = {}): Promise { + const outputDir = join(root, outputDirName); + + console.log(`\n ${BRAND} v${VERSION}`); + console.log(` Scanning: ${root}\n`); + + const startTime = Date.now(); + + // Step 1: Detect project + process.stdout.write(" Detecting project..."); + const project = await detectProject(root); + console.log( + ` ${project.frameworks.length > 0 ? project.frameworks.join(", ") : "generic"} | ${project.orms.length > 0 ? project.orms.join(", ") : "no ORM"} | ${project.language}` + ); + + if (project.isMonorepo) { + console.log(` Monorepo: ${project.workspaces.map((w) => w.name).join(", ")}`); + } + + // Step 2: Collect files — merge .codesightignore + config ignorePatterns + process.stdout.write(" Collecting files..."); + const ignoreFromFile = await readCodesightIgnore(root); + const allIgnorePatterns = [...(userConfig.ignorePatterns ?? []), ...ignoreFromFile]; + const files = await collectFiles(root, maxDepth, allIgnorePatterns); + console.log(` ${files.length} files`); + + // Step 3: Run all detectors in parallel (respecting disableDetectors config) + process.stdout.write(" Analyzing..."); + + const disabled = new Set(userConfig.disableDetectors || []); + + const [rawHttpRoutes, schemas, components, libs, configResult, middleware, graph, + graphqlRoutes, grpcRoutes, wsRoutes, events, openapi] = + await Promise.all([ + disabled.has("routes") ? Promise.resolve([]) : detectRoutes(files, project, userConfig), + disabled.has("schema") ? Promise.resolve([]) : detectSchemas(files, project), + disabled.has("components") ? Promise.resolve([]) : detectComponents(files, project), + disabled.has("libs") ? Promise.resolve([]) : detectLibs(files, project), + disabled.has("config") ? Promise.resolve({ envVars: [], configFiles: [], dependencies: {}, devDependencies: {} }) : detectConfig(files, project), + disabled.has("middleware") ? Promise.resolve([]) : detectMiddleware(files, project), + disabled.has("graph") ? Promise.resolve({ edges: [], hotFiles: [] }) : detectDependencyGraph(files, project), + disabled.has("graphql") ? Promise.resolve([]) : detectGraphQLRoutes(files, project), + disabled.has("graphql") ? Promise.resolve([]) : detectGRPCRoutes(files, project), + disabled.has("graphql") ? Promise.resolve([]) : detectWebSocketRoutes(files, project), + disabled.has("events") ? Promise.resolve([]) : detectEvents(files, project), + detectOpenAPISpec(root, project), + ]); + + // Merge OpenAPI routes and schemas if spec found + const rawRoutes = [...rawHttpRoutes, ...graphqlRoutes, ...grpcRoutes, ...wsRoutes]; + if (openapi.routes.length > 0) { + // Only use OpenAPI routes if we got very few from code detection + if (rawRoutes.length === 0) { + rawRoutes.push(...openapi.routes); + } + // Add any OpenAPI schemas not already detected + const existingModelNames = new Set(schemas.map((m) => m.name.toLowerCase())); + for (const m of openapi.schemas) { + if (!existingModelNames.has(m.name.toLowerCase())) schemas.push(m); + } + } + + // Step 3b: Run plugin detectors + // NOTE: if two plugins emit sections with the same name, the last writer wins on disk + // but both appear in CODESIGHT.md. Guard with unique names per plugin for now. + const customSections: { name: string; content: string }[] = []; + if (userConfig.plugins) { + for (const plugin of userConfig.plugins) { + if (plugin.detector) { + try { + const pluginResult = await plugin.detector(files, project); + if (pluginResult.routes) rawRoutes.push(...pluginResult.routes); + if (pluginResult.schemas) schemas.push(...pluginResult.schemas); + if (pluginResult.components) components.push(...pluginResult.components); + if (pluginResult.middleware) middleware.push(...pluginResult.middleware); + if (pluginResult.customSections) customSections.push(...pluginResult.customSections); + } catch (err: any) { + console.warn(`\n Warning: plugin "${plugin.name}" failed: ${err.message}`); + } + } + } + } + + // Step 4: Enrich routes with contract info + const routes = await enrichRouteContracts(rawRoutes, project); + + // Step 4b: Test coverage detection + const testCoverage = await detectTestCoverage(files, routes, schemas, root); + + // Step 4c: Compute CRUD groups + const crudGroups = computeCrudGroups(routes); + + // Report AST vs regex detection + const astRoutes = routes.filter((r) => r.confidence === "ast").length; + const astSchemas = schemas.filter((s) => s.confidence === "ast").length; + const astComponents = components.filter((c) => c.confidence === "ast").length; + const totalAST = astRoutes + astSchemas + astComponents; + + const specialCounts: string[] = []; + const gqlCount = routes.filter((r) => ["QUERY", "MUTATION", "SUBSCRIPTION"].includes(r.method)).length; + const grpcCount = routes.filter((r) => r.method === "RPC").length; + const wsCount = routes.filter((r) => r.method === "WS" || r.method === "WS-ROOM").length; + if (gqlCount > 0) specialCounts.push(`${gqlCount} graphql`); + if (grpcCount > 0) specialCounts.push(`${grpcCount} rpc`); + if (wsCount > 0) specialCounts.push(`${wsCount} ws`); + if (events.length > 0) specialCounts.push(`${events.length} events`); + + const specialStr = specialCounts.length > 0 ? `, ${specialCounts.join(", ")}` : ""; + if (totalAST > 0) { + console.log(` done (AST: ${astRoutes} routes, ${astSchemas} models, ${astComponents} components${specialStr})`); + } else if (specialCounts.length > 0) { + console.log(` done (${specialCounts.join(", ")})`); + } else { + console.log(" done"); + } + + // Step 5: Write output + process.stdout.write(" Writing output..."); + + // Temporary result without token stats to generate output + const tempResult: ScanResult = { + project, + routes, + schemas, + components, + libs, + config: configResult, + middleware, + graph, + tokenStats: { outputTokens: 0, estimatedExplorationTokens: 0, saved: 0, fileCount: files.length }, + events: events.length > 0 ? events : undefined, + testCoverage: testCoverage.testFiles.length > 0 ? testCoverage : undefined, + crudGroups: crudGroups.length > 0 ? crudGroups : undefined, + customSections: customSections.length > 0 ? customSections : undefined, + }; + + const outputContent = await writeOutput(tempResult, outputDir); + + // Step 6: Calculate real token stats + const tokenStats = calculateTokenStats(tempResult, outputContent, files.length); + const result: ScanResult = { ...tempResult, tokenStats }; + + // Re-write with accurate token stats + await writeOutput(result, outputDir); + + console.log(` ${outputDirName}/`); + + const elapsed = Date.now() - startTime; + + // Stats + console.log(` + Results: + Routes: ${routes.length} + Models: ${schemas.length} + Components: ${components.length} + Libraries: ${libs.length} + Env vars: ${configResult.envVars.length} + Middleware: ${middleware.length} + Import links: ${graph.edges.length} + Hot files: ${graph.hotFiles.length} + + Tokens: + Output size: ~${tokenStats.outputTokens.toLocaleString()} tokens + Exploration cost: ~${tokenStats.estimatedExplorationTokens.toLocaleString()} tokens + Saved: ~${tokenStats.saved.toLocaleString()} tokens per conversation + + Done in ${elapsed}ms +`); + + return result; +} + async function installGitHook(root: string, outputDirName: string) { const hooksDir = join(root, ".git", "hooks"); const hookPath = join(hooksDir, "pre-commit"); From 1d0df15377877a04f4f6f92e6e636ff4d3c70976 Mon Sep 17 00:00:00 2001 From: Neil Chambers Date: Wed, 15 Apr 2026 09:00:45 +0100 Subject: [PATCH 2/6] chore: gitignore test fixtures generated by test suite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tests dynamically create fixtures via writeFixture() — no need to track them. Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/fixtures/config-app/.env.example | 3 - tests/fixtures/config-app/package.json | 1 - tests/fixtures/config-app/src/config.ts | 2 - tests/fixtures/django-app/requirements.txt | 1 - tests/fixtures/django-app/urls.py | 5 - tests/fixtures/drizzle-schema/package.json | 1 - tests/fixtures/drizzle-schema/src/schema.ts | 12 -- tests/fixtures/elysia-app/package.json | 1 - tests/fixtures/elysia-app/src/index.ts | 4 - tests/fixtures/elysia-detect/package.json | 1 - tests/fixtures/express-app/package.json | 1 - tests/fixtures/express-app/src/routes.ts | 6 - tests/fixtures/fastapi-app/main.py | 8 -- tests/fixtures/fastapi-app/requirements.txt | 2 - tests/fixtures/fastify-app/package.json | 1 - tests/fixtures/fastify-app/src/server.ts | 5 - tests/fixtures/graph-app/package.json | 1 - tests/fixtures/graph-app/src/auth.ts | 2 - tests/fixtures/graph-app/src/db.ts | 1 - tests/fixtures/graph-app/src/middleware.ts | 3 - tests/fixtures/graph-app/src/routes.ts | 3 - tests/fixtures/hono-app/package.json | 1 - tests/fixtures/hono-app/src/index.ts | 6 - tests/fixtures/js-imports/package.json | 1 - tests/fixtures/js-imports/src/main.ts | 2 - tests/fixtures/js-imports/src/utils.ts | 1 - tests/fixtures/middleware-app/package.json | 1 - .../middleware-app/src/middleware/auth.ts | 5 - .../src/middleware/rate-limit.ts | 4 - tests/fixtures/monorepo-detect/package.json | 1 - .../monorepo-detect/packages/api/package.json | 1 - .../monorepo-detect/packages/web/package.json | 1 - tests/fixtures/nestjs-app/package.json | 1 - .../nestjs-app/src/users.controller.ts | 12 -- tests/fixtures/nestjs-detect/package.json | 1 - tests/fixtures/next-app/package.json | 1 - .../next-app/src/app/api/users/route.ts | 6 - tests/fixtures/nuxt-app/package.json | 1 - .../fixtures/nuxt-app/server/api/users.get.ts | 1 - .../nuxt-app/server/api/users.post.ts | 1 - .../nuxt-app/server/api/users/[id].get.ts | 1 - tests/fixtures/nuxt-detect/package.json | 1 - tests/fixtures/prisma-schema/package.json | 1 - .../prisma-schema/prisma/schema.prisma | 12 -- tests/fixtures/raw-http-app/package.json | 1 - tests/fixtures/raw-http-app/src/server.ts | 7 -- tests/fixtures/react-app/package.json | 1 - tests/fixtures/react-app/src/ProjectCard.tsx | 3 - tests/fixtures/react-app/src/UserProfile.tsx | 3 - tests/fixtures/remix-app/app/routes/users.tsx | 6 - tests/fixtures/remix-app/package.json | 1 - tests/fixtures/remix-detect/package.json | 1 - tests/fixtures/sveltekit-app/package.json | 1 - .../src/routes/api/users/+server.ts | 6 - tests/fixtures/sveltekit-detect/package.json | 1 - .../fixtures/terraform/edge-cases/complex.tf | 109 ------------------ .../terraform/in-project/src/index.ts | 2 - .../terraform/in-project/terraform/main.tf | 17 --- .../fixtures/terraform/multi-service/auth.tf | 21 ---- .../terraform/multi-service/billing.tf | 26 ----- .../terraform/multi-service/notifications.tf | 17 --- .../simple-ecs-service/app-service.tf | 85 -------------- .../environments/production.tfvars | 5 - .../environments/staging.tfvars | 5 - tests/fixtures/trpc-app/package.json | 1 - tests/fixtures/trpc-app/src/router.ts | 6 - tests/fixtures/trpc-detect/package.json | 1 - 67 files changed, 453 deletions(-) delete mode 100644 tests/fixtures/config-app/.env.example delete mode 100644 tests/fixtures/config-app/package.json delete mode 100644 tests/fixtures/config-app/src/config.ts delete mode 100644 tests/fixtures/django-app/requirements.txt delete mode 100644 tests/fixtures/django-app/urls.py delete mode 100644 tests/fixtures/drizzle-schema/package.json delete mode 100644 tests/fixtures/drizzle-schema/src/schema.ts delete mode 100644 tests/fixtures/elysia-app/package.json delete mode 100644 tests/fixtures/elysia-app/src/index.ts delete mode 100644 tests/fixtures/elysia-detect/package.json delete mode 100644 tests/fixtures/express-app/package.json delete mode 100644 tests/fixtures/express-app/src/routes.ts delete mode 100644 tests/fixtures/fastapi-app/main.py delete mode 100644 tests/fixtures/fastapi-app/requirements.txt delete mode 100644 tests/fixtures/fastify-app/package.json delete mode 100644 tests/fixtures/fastify-app/src/server.ts delete mode 100644 tests/fixtures/graph-app/package.json delete mode 100644 tests/fixtures/graph-app/src/auth.ts delete mode 100644 tests/fixtures/graph-app/src/db.ts delete mode 100644 tests/fixtures/graph-app/src/middleware.ts delete mode 100644 tests/fixtures/graph-app/src/routes.ts delete mode 100644 tests/fixtures/hono-app/package.json delete mode 100644 tests/fixtures/hono-app/src/index.ts delete mode 100644 tests/fixtures/js-imports/package.json delete mode 100644 tests/fixtures/js-imports/src/main.ts delete mode 100644 tests/fixtures/js-imports/src/utils.ts delete mode 100644 tests/fixtures/middleware-app/package.json delete mode 100644 tests/fixtures/middleware-app/src/middleware/auth.ts delete mode 100644 tests/fixtures/middleware-app/src/middleware/rate-limit.ts delete mode 100644 tests/fixtures/monorepo-detect/package.json delete mode 100644 tests/fixtures/monorepo-detect/packages/api/package.json delete mode 100644 tests/fixtures/monorepo-detect/packages/web/package.json delete mode 100644 tests/fixtures/nestjs-app/package.json delete mode 100644 tests/fixtures/nestjs-app/src/users.controller.ts delete mode 100644 tests/fixtures/nestjs-detect/package.json delete mode 100644 tests/fixtures/next-app/package.json delete mode 100644 tests/fixtures/next-app/src/app/api/users/route.ts delete mode 100644 tests/fixtures/nuxt-app/package.json delete mode 100644 tests/fixtures/nuxt-app/server/api/users.get.ts delete mode 100644 tests/fixtures/nuxt-app/server/api/users.post.ts delete mode 100644 tests/fixtures/nuxt-app/server/api/users/[id].get.ts delete mode 100644 tests/fixtures/nuxt-detect/package.json delete mode 100644 tests/fixtures/prisma-schema/package.json delete mode 100644 tests/fixtures/prisma-schema/prisma/schema.prisma delete mode 100644 tests/fixtures/raw-http-app/package.json delete mode 100644 tests/fixtures/raw-http-app/src/server.ts delete mode 100644 tests/fixtures/react-app/package.json delete mode 100644 tests/fixtures/react-app/src/ProjectCard.tsx delete mode 100644 tests/fixtures/react-app/src/UserProfile.tsx delete mode 100644 tests/fixtures/remix-app/app/routes/users.tsx delete mode 100644 tests/fixtures/remix-app/package.json delete mode 100644 tests/fixtures/remix-detect/package.json delete mode 100644 tests/fixtures/sveltekit-app/package.json delete mode 100644 tests/fixtures/sveltekit-app/src/routes/api/users/+server.ts delete mode 100644 tests/fixtures/sveltekit-detect/package.json delete mode 100644 tests/fixtures/terraform/edge-cases/complex.tf delete mode 100644 tests/fixtures/terraform/in-project/src/index.ts delete mode 100644 tests/fixtures/terraform/in-project/terraform/main.tf delete mode 100644 tests/fixtures/terraform/multi-service/auth.tf delete mode 100644 tests/fixtures/terraform/multi-service/billing.tf delete mode 100644 tests/fixtures/terraform/multi-service/notifications.tf delete mode 100644 tests/fixtures/terraform/simple-ecs-service/app-service.tf delete mode 100644 tests/fixtures/terraform/simple-ecs-service/environments/production.tfvars delete mode 100644 tests/fixtures/terraform/simple-ecs-service/environments/staging.tfvars delete mode 100644 tests/fixtures/trpc-app/package.json delete mode 100644 tests/fixtures/trpc-app/src/router.ts delete mode 100644 tests/fixtures/trpc-detect/package.json diff --git a/tests/fixtures/config-app/.env.example b/tests/fixtures/config-app/.env.example deleted file mode 100644 index 7d4c260..0000000 --- a/tests/fixtures/config-app/.env.example +++ /dev/null @@ -1,3 +0,0 @@ -DATABASE_URL= -JWT_SECRET= -PORT=3000 \ No newline at end of file diff --git a/tests/fixtures/config-app/package.json b/tests/fixtures/config-app/package.json deleted file mode 100644 index 357e8e2..0000000 --- a/tests/fixtures/config-app/package.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"test"} \ No newline at end of file diff --git a/tests/fixtures/config-app/src/config.ts b/tests/fixtures/config-app/src/config.ts deleted file mode 100644 index b77bec6..0000000 --- a/tests/fixtures/config-app/src/config.ts +++ /dev/null @@ -1,2 +0,0 @@ -const db = process.env.DATABASE_URL; -const port = process.env.PORT || 3000; \ No newline at end of file diff --git a/tests/fixtures/django-app/requirements.txt b/tests/fixtures/django-app/requirements.txt deleted file mode 100644 index d3e4ba5..0000000 --- a/tests/fixtures/django-app/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -django diff --git a/tests/fixtures/django-app/urls.py b/tests/fixtures/django-app/urls.py deleted file mode 100644 index e0aa979..0000000 --- a/tests/fixtures/django-app/urls.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.urls import path -urlpatterns = [ - path("api/users/", views.UserList.as_view()), - path("api/users//", views.UserDetail.as_view()), -] \ No newline at end of file diff --git a/tests/fixtures/drizzle-schema/package.json b/tests/fixtures/drizzle-schema/package.json deleted file mode 100644 index 2ab8dea..0000000 --- a/tests/fixtures/drizzle-schema/package.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"test","dependencies":{"drizzle-orm":"^0.30.0"}} \ No newline at end of file diff --git a/tests/fixtures/drizzle-schema/src/schema.ts b/tests/fixtures/drizzle-schema/src/schema.ts deleted file mode 100644 index ee270d4..0000000 --- a/tests/fixtures/drizzle-schema/src/schema.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { pgTable, text, uuid, timestamp, boolean } from "drizzle-orm/pg-core"; -export const users = pgTable("users", { - id: uuid("id").primaryKey().defaultRandom(), - email: text("email").notNull().unique(), - name: text("name").notNull(), - active: boolean("active").default(true), -}); -export const posts = pgTable("posts", { - id: uuid("id").primaryKey().defaultRandom(), - title: text("title").notNull(), - userId: uuid("user_id").references(() => users.id), -}); \ No newline at end of file diff --git a/tests/fixtures/elysia-app/package.json b/tests/fixtures/elysia-app/package.json deleted file mode 100644 index dd7bf2e..0000000 --- a/tests/fixtures/elysia-app/package.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"test","dependencies":{"elysia":"^1.0.0"}} \ No newline at end of file diff --git a/tests/fixtures/elysia-app/src/index.ts b/tests/fixtures/elysia-app/src/index.ts deleted file mode 100644 index c3335fc..0000000 --- a/tests/fixtures/elysia-app/src/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Elysia } from "elysia"; -const app = new Elysia() - .get("/api/health", () => "ok") - .post("/api/items", () => ({ created: true })); \ No newline at end of file diff --git a/tests/fixtures/elysia-detect/package.json b/tests/fixtures/elysia-detect/package.json deleted file mode 100644 index dd7bf2e..0000000 --- a/tests/fixtures/elysia-detect/package.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"test","dependencies":{"elysia":"^1.0.0"}} \ No newline at end of file diff --git a/tests/fixtures/express-app/package.json b/tests/fixtures/express-app/package.json deleted file mode 100644 index 749d35e..0000000 --- a/tests/fixtures/express-app/package.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"test","dependencies":{"express":"^4.0.0"}} \ No newline at end of file diff --git a/tests/fixtures/express-app/src/routes.ts b/tests/fixtures/express-app/src/routes.ts deleted file mode 100644 index ef853b4..0000000 --- a/tests/fixtures/express-app/src/routes.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Router } from "express"; -const router = Router(); -router.get("/users", (req, res) => res.json([])); -router.post("/users", (req, res) => res.json({})); -router.delete("/users/:id", (req, res) => res.json({})); -export default router; \ No newline at end of file diff --git a/tests/fixtures/fastapi-app/main.py b/tests/fixtures/fastapi-app/main.py deleted file mode 100644 index 87fecc8..0000000 --- a/tests/fixtures/fastapi-app/main.py +++ /dev/null @@ -1,8 +0,0 @@ -from fastapi import FastAPI -app = FastAPI() -@app.get("/users") -def get_users(): - return [] -@app.post("/users") -def create_user(): - return {} \ No newline at end of file diff --git a/tests/fixtures/fastapi-app/requirements.txt b/tests/fixtures/fastapi-app/requirements.txt deleted file mode 100644 index 97dc7cd..0000000 --- a/tests/fixtures/fastapi-app/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -fastapi -uvicorn diff --git a/tests/fixtures/fastify-app/package.json b/tests/fixtures/fastify-app/package.json deleted file mode 100644 index 9de5a5c..0000000 --- a/tests/fixtures/fastify-app/package.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"test","dependencies":{"fastify":"^4.0.0"}} \ No newline at end of file diff --git a/tests/fixtures/fastify-app/src/server.ts b/tests/fixtures/fastify-app/src/server.ts deleted file mode 100644 index b4d96d8..0000000 --- a/tests/fixtures/fastify-app/src/server.ts +++ /dev/null @@ -1,5 +0,0 @@ -import fastify from "fastify"; -const app = fastify(); -app.get("/health", async () => ({ status: "ok" })); -app.post("/items", async (req) => ({ created: true })); -export default app; \ No newline at end of file diff --git a/tests/fixtures/graph-app/package.json b/tests/fixtures/graph-app/package.json deleted file mode 100644 index aff692f..0000000 --- a/tests/fixtures/graph-app/package.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"test","dependencies":{"hono":"^4.0.0"}} \ No newline at end of file diff --git a/tests/fixtures/graph-app/src/auth.ts b/tests/fixtures/graph-app/src/auth.ts deleted file mode 100644 index e1b5ffb..0000000 --- a/tests/fixtures/graph-app/src/auth.ts +++ /dev/null @@ -1,2 +0,0 @@ -import { db } from "./db.js"; -export const auth = {}; \ No newline at end of file diff --git a/tests/fixtures/graph-app/src/db.ts b/tests/fixtures/graph-app/src/db.ts deleted file mode 100644 index 04cb237..0000000 --- a/tests/fixtures/graph-app/src/db.ts +++ /dev/null @@ -1 +0,0 @@ -export const db = {}; \ No newline at end of file diff --git a/tests/fixtures/graph-app/src/middleware.ts b/tests/fixtures/graph-app/src/middleware.ts deleted file mode 100644 index 81996f7..0000000 --- a/tests/fixtures/graph-app/src/middleware.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { auth } from "./auth.js"; -import { db } from "./db.js"; -export const mw = {}; \ No newline at end of file diff --git a/tests/fixtures/graph-app/src/routes.ts b/tests/fixtures/graph-app/src/routes.ts deleted file mode 100644 index 2cf61c9..0000000 --- a/tests/fixtures/graph-app/src/routes.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { db } from "./db.js"; -import { auth } from "./auth.js"; -export const routes = {}; \ No newline at end of file diff --git a/tests/fixtures/hono-app/package.json b/tests/fixtures/hono-app/package.json deleted file mode 100644 index aff692f..0000000 --- a/tests/fixtures/hono-app/package.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"test","dependencies":{"hono":"^4.0.0"}} \ No newline at end of file diff --git a/tests/fixtures/hono-app/src/index.ts b/tests/fixtures/hono-app/src/index.ts deleted file mode 100644 index 9a2b542..0000000 --- a/tests/fixtures/hono-app/src/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Hono } from "hono"; -const app = new Hono(); -app.get("/api/users", (c) => c.json([])); -app.post("/api/users", (c) => c.json({})); -app.get("/api/users/:id", (c) => c.json({})); -export default app; \ No newline at end of file diff --git a/tests/fixtures/js-imports/package.json b/tests/fixtures/js-imports/package.json deleted file mode 100644 index 357e8e2..0000000 --- a/tests/fixtures/js-imports/package.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"test"} \ No newline at end of file diff --git a/tests/fixtures/js-imports/src/main.ts b/tests/fixtures/js-imports/src/main.ts deleted file mode 100644 index 33086b3..0000000 --- a/tests/fixtures/js-imports/src/main.ts +++ /dev/null @@ -1,2 +0,0 @@ -import { helper } from "./utils.js"; -console.log(helper); \ No newline at end of file diff --git a/tests/fixtures/js-imports/src/utils.ts b/tests/fixtures/js-imports/src/utils.ts deleted file mode 100644 index 94ec3e1..0000000 --- a/tests/fixtures/js-imports/src/utils.ts +++ /dev/null @@ -1 +0,0 @@ -export const helper = () => {}; \ No newline at end of file diff --git a/tests/fixtures/middleware-app/package.json b/tests/fixtures/middleware-app/package.json deleted file mode 100644 index 749d35e..0000000 --- a/tests/fixtures/middleware-app/package.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"test","dependencies":{"express":"^4.0.0"}} \ No newline at end of file diff --git a/tests/fixtures/middleware-app/src/middleware/auth.ts b/tests/fixtures/middleware-app/src/middleware/auth.ts deleted file mode 100644 index 700ee2c..0000000 --- a/tests/fixtures/middleware-app/src/middleware/auth.ts +++ /dev/null @@ -1,5 +0,0 @@ -export function authMiddleware(req, res, next) { - const token = req.headers.authorization; - if (!token) return res.status(401).json({ error: "unauthorized" }); - next(); -} \ No newline at end of file diff --git a/tests/fixtures/middleware-app/src/middleware/rate-limit.ts b/tests/fixtures/middleware-app/src/middleware/rate-limit.ts deleted file mode 100644 index 0874422..0000000 --- a/tests/fixtures/middleware-app/src/middleware/rate-limit.ts +++ /dev/null @@ -1,4 +0,0 @@ -export function rateLimiter(req, res, next) { - // rate limiting logic - next(); -} \ No newline at end of file diff --git a/tests/fixtures/monorepo-detect/package.json b/tests/fixtures/monorepo-detect/package.json deleted file mode 100644 index 950e109..0000000 --- a/tests/fixtures/monorepo-detect/package.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"test","workspaces":["packages/*"]} \ No newline at end of file diff --git a/tests/fixtures/monorepo-detect/packages/api/package.json b/tests/fixtures/monorepo-detect/packages/api/package.json deleted file mode 100644 index 018378c..0000000 --- a/tests/fixtures/monorepo-detect/packages/api/package.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"@test/api","dependencies":{"hono":"^4.0.0","drizzle-orm":"^0.30.0"}} \ No newline at end of file diff --git a/tests/fixtures/monorepo-detect/packages/web/package.json b/tests/fixtures/monorepo-detect/packages/web/package.json deleted file mode 100644 index 7224562..0000000 --- a/tests/fixtures/monorepo-detect/packages/web/package.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"@test/web","dependencies":{"react":"^18.0.0"}} \ No newline at end of file diff --git a/tests/fixtures/nestjs-app/package.json b/tests/fixtures/nestjs-app/package.json deleted file mode 100644 index 7672715..0000000 --- a/tests/fixtures/nestjs-app/package.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"test","dependencies":{"@nestjs/core":"^10.0.0","@nestjs/common":"^10.0.0"}} \ No newline at end of file diff --git a/tests/fixtures/nestjs-app/src/users.controller.ts b/tests/fixtures/nestjs-app/src/users.controller.ts deleted file mode 100644 index e3cd6dc..0000000 --- a/tests/fixtures/nestjs-app/src/users.controller.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Controller, Get, Post, Put, Delete, Param } from '@nestjs/common'; -@Controller('users') -export class UsersController { - @Get() - findAll() { return []; } - @Get(':id') - findOne(@Param('id') id: string) { return {}; } - @Post() - create() { return {}; } - @Delete(':id') - remove(@Param('id') id: string) { return {}; } -} \ No newline at end of file diff --git a/tests/fixtures/nestjs-detect/package.json b/tests/fixtures/nestjs-detect/package.json deleted file mode 100644 index 7672715..0000000 --- a/tests/fixtures/nestjs-detect/package.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"test","dependencies":{"@nestjs/core":"^10.0.0","@nestjs/common":"^10.0.0"}} \ No newline at end of file diff --git a/tests/fixtures/next-app/package.json b/tests/fixtures/next-app/package.json deleted file mode 100644 index afd7a24..0000000 --- a/tests/fixtures/next-app/package.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"test","dependencies":{"next":"^14.0.0"}} \ No newline at end of file diff --git a/tests/fixtures/next-app/src/app/api/users/route.ts b/tests/fixtures/next-app/src/app/api/users/route.ts deleted file mode 100644 index 0590ffa..0000000 --- a/tests/fixtures/next-app/src/app/api/users/route.ts +++ /dev/null @@ -1,6 +0,0 @@ -export async function GET() { - return Response.json([]); -} -export async function POST(request: Request) { - return Response.json({}); -} \ No newline at end of file diff --git a/tests/fixtures/nuxt-app/package.json b/tests/fixtures/nuxt-app/package.json deleted file mode 100644 index 8ed5126..0000000 --- a/tests/fixtures/nuxt-app/package.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"test","dependencies":{"nuxt":"^3.0.0"}} \ No newline at end of file diff --git a/tests/fixtures/nuxt-app/server/api/users.get.ts b/tests/fixtures/nuxt-app/server/api/users.get.ts deleted file mode 100644 index 355792e..0000000 --- a/tests/fixtures/nuxt-app/server/api/users.get.ts +++ /dev/null @@ -1 +0,0 @@ -export default defineEventHandler(() => []); \ No newline at end of file diff --git a/tests/fixtures/nuxt-app/server/api/users.post.ts b/tests/fixtures/nuxt-app/server/api/users.post.ts deleted file mode 100644 index 3a5224b..0000000 --- a/tests/fixtures/nuxt-app/server/api/users.post.ts +++ /dev/null @@ -1 +0,0 @@ -export default defineEventHandler(() => ({})); \ No newline at end of file diff --git a/tests/fixtures/nuxt-app/server/api/users/[id].get.ts b/tests/fixtures/nuxt-app/server/api/users/[id].get.ts deleted file mode 100644 index 3a5224b..0000000 --- a/tests/fixtures/nuxt-app/server/api/users/[id].get.ts +++ /dev/null @@ -1 +0,0 @@ -export default defineEventHandler(() => ({})); \ No newline at end of file diff --git a/tests/fixtures/nuxt-detect/package.json b/tests/fixtures/nuxt-detect/package.json deleted file mode 100644 index 8ed5126..0000000 --- a/tests/fixtures/nuxt-detect/package.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"test","dependencies":{"nuxt":"^3.0.0"}} \ No newline at end of file diff --git a/tests/fixtures/prisma-schema/package.json b/tests/fixtures/prisma-schema/package.json deleted file mode 100644 index dca7c60..0000000 --- a/tests/fixtures/prisma-schema/package.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"test","dependencies":{"prisma":"^5.0.0"}} \ No newline at end of file diff --git a/tests/fixtures/prisma-schema/prisma/schema.prisma b/tests/fixtures/prisma-schema/prisma/schema.prisma deleted file mode 100644 index 00ab729..0000000 --- a/tests/fixtures/prisma-schema/prisma/schema.prisma +++ /dev/null @@ -1,12 +0,0 @@ -model User { - id String @id @default(cuid()) - email String @unique - name String - posts Post[] -} -model Post { - id String @id @default(cuid()) - title String - userId String - user User @relation(fields: [userId], references: [id]) -} \ No newline at end of file diff --git a/tests/fixtures/raw-http-app/package.json b/tests/fixtures/raw-http-app/package.json deleted file mode 100644 index 357e8e2..0000000 --- a/tests/fixtures/raw-http-app/package.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"test"} \ No newline at end of file diff --git a/tests/fixtures/raw-http-app/src/server.ts b/tests/fixtures/raw-http-app/src/server.ts deleted file mode 100644 index 57e0722..0000000 --- a/tests/fixtures/raw-http-app/src/server.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createServer } from "http"; -const server = createServer((req, res) => { - const url = new URL(req.url!, "http://localhost").pathname; - if (url === "/health") { res.end("ok"); return; } - if (url === "/api/users" && req.method === "GET") { res.end("[]"); return; } - if (url === "/api/users" && req.method === "POST") { res.end("{}"); return; } -}); \ No newline at end of file diff --git a/tests/fixtures/react-app/package.json b/tests/fixtures/react-app/package.json deleted file mode 100644 index c8cacbf..0000000 --- a/tests/fixtures/react-app/package.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"test","dependencies":{"react":"^18.0.0"}} \ No newline at end of file diff --git a/tests/fixtures/react-app/src/ProjectCard.tsx b/tests/fixtures/react-app/src/ProjectCard.tsx deleted file mode 100644 index 8f1567f..0000000 --- a/tests/fixtures/react-app/src/ProjectCard.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export const ProjectCard = ({ title, description }: { title: string; description: string }) => { - return

{title}

{description}

; -}; \ No newline at end of file diff --git a/tests/fixtures/react-app/src/UserProfile.tsx b/tests/fixtures/react-app/src/UserProfile.tsx deleted file mode 100644 index e4e45bf..0000000 --- a/tests/fixtures/react-app/src/UserProfile.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function UserProfile({ name, email, avatar }: { name: string; email: string; avatar?: string }) { - return
{name} - {email}
; -} \ No newline at end of file diff --git a/tests/fixtures/remix-app/app/routes/users.tsx b/tests/fixtures/remix-app/app/routes/users.tsx deleted file mode 100644 index 510aae2..0000000 --- a/tests/fixtures/remix-app/app/routes/users.tsx +++ /dev/null @@ -1,6 +0,0 @@ -export async function loader({ request }) { - return json([]); -} -export async function action({ request }) { - return json({}); -} \ No newline at end of file diff --git a/tests/fixtures/remix-app/package.json b/tests/fixtures/remix-app/package.json deleted file mode 100644 index 44f75e3..0000000 --- a/tests/fixtures/remix-app/package.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"test","dependencies":{"@remix-run/node":"^2.0.0"}} \ No newline at end of file diff --git a/tests/fixtures/remix-detect/package.json b/tests/fixtures/remix-detect/package.json deleted file mode 100644 index 44f75e3..0000000 --- a/tests/fixtures/remix-detect/package.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"test","dependencies":{"@remix-run/node":"^2.0.0"}} \ No newline at end of file diff --git a/tests/fixtures/sveltekit-app/package.json b/tests/fixtures/sveltekit-app/package.json deleted file mode 100644 index f48cdce..0000000 --- a/tests/fixtures/sveltekit-app/package.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"test","dependencies":{"@sveltejs/kit":"^2.0.0"}} \ No newline at end of file diff --git a/tests/fixtures/sveltekit-app/src/routes/api/users/+server.ts b/tests/fixtures/sveltekit-app/src/routes/api/users/+server.ts deleted file mode 100644 index 834b1fe..0000000 --- a/tests/fixtures/sveltekit-app/src/routes/api/users/+server.ts +++ /dev/null @@ -1,6 +0,0 @@ -export async function GET() { - return new Response(JSON.stringify([]), { headers: { 'content-type': 'application/json' } }); -} -export async function POST({ request }) { - return new Response(JSON.stringify({})); -} \ No newline at end of file diff --git a/tests/fixtures/sveltekit-detect/package.json b/tests/fixtures/sveltekit-detect/package.json deleted file mode 100644 index f48cdce..0000000 --- a/tests/fixtures/sveltekit-detect/package.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"test","dependencies":{"@sveltejs/kit":"^2.0.0"}} \ No newline at end of file diff --git a/tests/fixtures/terraform/edge-cases/complex.tf b/tests/fixtures/terraform/edge-cases/complex.tf deleted file mode 100644 index ca2f29f..0000000 --- a/tests/fixtures/terraform/edge-cases/complex.tf +++ /dev/null @@ -1,109 +0,0 @@ -# This file tests edge cases for the HCL parser - -// C-style comment -variable "basic_var" { - type = string - default = "hello" -} - -/* Block comment - spanning multiple lines - with { braces } inside */ -resource "aws_ecs_task_definition" "with_heredoc" { - family = "test" - - container_definitions = < []), - create: publicProcedure.input(z.object({ name: z.string() })).mutation(async ({ input }) => ({})), - getById: publicProcedure.input(z.object({ id: z.string() })).query(async ({ input }) => ({})), -}); \ No newline at end of file diff --git a/tests/fixtures/trpc-detect/package.json b/tests/fixtures/trpc-detect/package.json deleted file mode 100644 index 1a434da..0000000 --- a/tests/fixtures/trpc-detect/package.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"test","dependencies":{"@trpc/server":"^10.0.0"}} \ No newline at end of file From 540226f4ae3f42883479ac3cd7638216db3f33d9 Mon Sep 17 00:00:00 2001 From: Neil Chambers Date: Thu, 23 Apr 2026 16:51:54 +0100 Subject: [PATCH 3/6] fix: resolve post-rebase TS errors and restore terraform fixtures - Remove duplicate scan() from index.ts (now lives in core.ts since upstream refactor) - Port customSections plugin API into core.ts scan() to preserve the feature - Narrow .gitignore pattern from tests/fixtures/ to only ignore generated .codesight/ output - Restore terraform fixture files deleted by the overly-broad gitignore commit Co-Authored-By: Claude Sonnet 4.6 --- src/index.ts | 172 ------------------ .../fixtures/terraform/edge-cases/complex.tf | 109 +++++++++++ .../terraform/in-project/src/index.ts | 2 + .../terraform/in-project/terraform/main.tf | 17 ++ .../fixtures/terraform/multi-service/auth.tf | 21 +++ .../terraform/multi-service/billing.tf | 26 +++ .../terraform/multi-service/notifications.tf | 17 ++ .../simple-ecs-service/app-service.tf | 85 +++++++++ .../environments/production.tfvars | 5 + .../environments/staging.tfvars | 5 + 10 files changed, 287 insertions(+), 172 deletions(-) create mode 100644 tests/fixtures/terraform/edge-cases/complex.tf create mode 100644 tests/fixtures/terraform/in-project/src/index.ts create mode 100644 tests/fixtures/terraform/in-project/terraform/main.tf create mode 100644 tests/fixtures/terraform/multi-service/auth.tf create mode 100644 tests/fixtures/terraform/multi-service/billing.tf create mode 100644 tests/fixtures/terraform/multi-service/notifications.tf create mode 100644 tests/fixtures/terraform/simple-ecs-service/app-service.tf create mode 100644 tests/fixtures/terraform/simple-ecs-service/environments/production.tfvars create mode 100644 tests/fixtures/terraform/simple-ecs-service/environments/staging.tfvars diff --git a/src/index.ts b/src/index.ts index b0fecd5..bc5d2e2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -76,178 +76,6 @@ async function fileExists(path: string): Promise { } } -async function scan(root: string, outputDirName: string, maxDepth: number, userConfig: CodesightConfig = {}): Promise { - const outputDir = join(root, outputDirName); - - console.log(`\n ${BRAND} v${VERSION}`); - console.log(` Scanning: ${root}\n`); - - const startTime = Date.now(); - - // Step 1: Detect project - process.stdout.write(" Detecting project..."); - const project = await detectProject(root); - console.log( - ` ${project.frameworks.length > 0 ? project.frameworks.join(", ") : "generic"} | ${project.orms.length > 0 ? project.orms.join(", ") : "no ORM"} | ${project.language}` - ); - - if (project.isMonorepo) { - console.log(` Monorepo: ${project.workspaces.map((w) => w.name).join(", ")}`); - } - - // Step 2: Collect files — merge .codesightignore + config ignorePatterns - process.stdout.write(" Collecting files..."); - const ignoreFromFile = await readCodesightIgnore(root); - const allIgnorePatterns = [...(userConfig.ignorePatterns ?? []), ...ignoreFromFile]; - const files = await collectFiles(root, maxDepth, allIgnorePatterns); - console.log(` ${files.length} files`); - - // Step 3: Run all detectors in parallel (respecting disableDetectors config) - process.stdout.write(" Analyzing..."); - - const disabled = new Set(userConfig.disableDetectors || []); - - const [rawHttpRoutes, schemas, components, libs, configResult, middleware, graph, - graphqlRoutes, grpcRoutes, wsRoutes, events, openapi] = - await Promise.all([ - disabled.has("routes") ? Promise.resolve([]) : detectRoutes(files, project, userConfig), - disabled.has("schema") ? Promise.resolve([]) : detectSchemas(files, project), - disabled.has("components") ? Promise.resolve([]) : detectComponents(files, project), - disabled.has("libs") ? Promise.resolve([]) : detectLibs(files, project), - disabled.has("config") ? Promise.resolve({ envVars: [], configFiles: [], dependencies: {}, devDependencies: {} }) : detectConfig(files, project), - disabled.has("middleware") ? Promise.resolve([]) : detectMiddleware(files, project), - disabled.has("graph") ? Promise.resolve({ edges: [], hotFiles: [] }) : detectDependencyGraph(files, project), - disabled.has("graphql") ? Promise.resolve([]) : detectGraphQLRoutes(files, project), - disabled.has("graphql") ? Promise.resolve([]) : detectGRPCRoutes(files, project), - disabled.has("graphql") ? Promise.resolve([]) : detectWebSocketRoutes(files, project), - disabled.has("events") ? Promise.resolve([]) : detectEvents(files, project), - detectOpenAPISpec(root, project), - ]); - - // Merge OpenAPI routes and schemas if spec found - const rawRoutes = [...rawHttpRoutes, ...graphqlRoutes, ...grpcRoutes, ...wsRoutes]; - if (openapi.routes.length > 0) { - // Only use OpenAPI routes if we got very few from code detection - if (rawRoutes.length === 0) { - rawRoutes.push(...openapi.routes); - } - // Add any OpenAPI schemas not already detected - const existingModelNames = new Set(schemas.map((m) => m.name.toLowerCase())); - for (const m of openapi.schemas) { - if (!existingModelNames.has(m.name.toLowerCase())) schemas.push(m); - } - } - - // Step 3b: Run plugin detectors - // NOTE: if two plugins emit sections with the same name, the last writer wins on disk - // but both appear in CODESIGHT.md. Guard with unique names per plugin for now. - const customSections: { name: string; content: string }[] = []; - if (userConfig.plugins) { - for (const plugin of userConfig.plugins) { - if (plugin.detector) { - try { - const pluginResult = await plugin.detector(files, project); - if (pluginResult.routes) rawRoutes.push(...pluginResult.routes); - if (pluginResult.schemas) schemas.push(...pluginResult.schemas); - if (pluginResult.components) components.push(...pluginResult.components); - if (pluginResult.middleware) middleware.push(...pluginResult.middleware); - if (pluginResult.customSections) customSections.push(...pluginResult.customSections); - } catch (err: any) { - console.warn(`\n Warning: plugin "${plugin.name}" failed: ${err.message}`); - } - } - } - } - - // Step 4: Enrich routes with contract info - const routes = await enrichRouteContracts(rawRoutes, project); - - // Step 4b: Test coverage detection - const testCoverage = await detectTestCoverage(files, routes, schemas, root); - - // Step 4c: Compute CRUD groups - const crudGroups = computeCrudGroups(routes); - - // Report AST vs regex detection - const astRoutes = routes.filter((r) => r.confidence === "ast").length; - const astSchemas = schemas.filter((s) => s.confidence === "ast").length; - const astComponents = components.filter((c) => c.confidence === "ast").length; - const totalAST = astRoutes + astSchemas + astComponents; - - const specialCounts: string[] = []; - const gqlCount = routes.filter((r) => ["QUERY", "MUTATION", "SUBSCRIPTION"].includes(r.method)).length; - const grpcCount = routes.filter((r) => r.method === "RPC").length; - const wsCount = routes.filter((r) => r.method === "WS" || r.method === "WS-ROOM").length; - if (gqlCount > 0) specialCounts.push(`${gqlCount} graphql`); - if (grpcCount > 0) specialCounts.push(`${grpcCount} rpc`); - if (wsCount > 0) specialCounts.push(`${wsCount} ws`); - if (events.length > 0) specialCounts.push(`${events.length} events`); - - const specialStr = specialCounts.length > 0 ? `, ${specialCounts.join(", ")}` : ""; - if (totalAST > 0) { - console.log(` done (AST: ${astRoutes} routes, ${astSchemas} models, ${astComponents} components${specialStr})`); - } else if (specialCounts.length > 0) { - console.log(` done (${specialCounts.join(", ")})`); - } else { - console.log(" done"); - } - - // Step 5: Write output - process.stdout.write(" Writing output..."); - - // Temporary result without token stats to generate output - const tempResult: ScanResult = { - project, - routes, - schemas, - components, - libs, - config: configResult, - middleware, - graph, - tokenStats: { outputTokens: 0, estimatedExplorationTokens: 0, saved: 0, fileCount: files.length }, - events: events.length > 0 ? events : undefined, - testCoverage: testCoverage.testFiles.length > 0 ? testCoverage : undefined, - crudGroups: crudGroups.length > 0 ? crudGroups : undefined, - customSections: customSections.length > 0 ? customSections : undefined, - }; - - const outputContent = await writeOutput(tempResult, outputDir); - - // Step 6: Calculate real token stats - const tokenStats = calculateTokenStats(tempResult, outputContent, files.length); - const result: ScanResult = { ...tempResult, tokenStats }; - - // Re-write with accurate token stats - await writeOutput(result, outputDir); - - console.log(` ${outputDirName}/`); - - const elapsed = Date.now() - startTime; - - // Stats - console.log(` - Results: - Routes: ${routes.length} - Models: ${schemas.length} - Components: ${components.length} - Libraries: ${libs.length} - Env vars: ${configResult.envVars.length} - Middleware: ${middleware.length} - Import links: ${graph.edges.length} - Hot files: ${graph.hotFiles.length} - - Tokens: - Output size: ~${tokenStats.outputTokens.toLocaleString()} tokens - Exploration cost: ~${tokenStats.estimatedExplorationTokens.toLocaleString()} tokens - Saved: ~${tokenStats.saved.toLocaleString()} tokens per conversation - - Done in ${elapsed}ms -`); - - return result; -} - async function installGitHook(root: string, outputDirName: string) { const hooksDir = join(root, ".git", "hooks"); const hookPath = join(hooksDir, "pre-commit"); diff --git a/tests/fixtures/terraform/edge-cases/complex.tf b/tests/fixtures/terraform/edge-cases/complex.tf new file mode 100644 index 0000000..ca2f29f --- /dev/null +++ b/tests/fixtures/terraform/edge-cases/complex.tf @@ -0,0 +1,109 @@ +# This file tests edge cases for the HCL parser + +// C-style comment +variable "basic_var" { + type = string + default = "hello" +} + +/* Block comment + spanning multiple lines + with { braces } inside */ +resource "aws_ecs_task_definition" "with_heredoc" { + family = "test" + + container_definitions = < Date: Sat, 25 Apr 2026 13:03:52 +0100 Subject: [PATCH 4/6] fix: remove sibling-repo discovery from terraform plugin The ../infrastructure sibling scan was specific to a particular monorepo layout. The general case is terraform co-located in the project; users with a separate infra repo should set infraPath explicitly. Co-Authored-By: Claude Sonnet 4.6 --- src/plugins/terraform/file-collector.ts | 14 +++----------- src/plugins/terraform/index.ts | 12 ++++++------ src/plugins/terraform/types.ts | 4 ++-- 3 files changed, 11 insertions(+), 19 deletions(-) diff --git a/src/plugins/terraform/file-collector.ts b/src/plugins/terraform/file-collector.ts index 4241c5a..4c12915 100644 --- a/src/plugins/terraform/file-collector.ts +++ b/src/plugins/terraform/file-collector.ts @@ -1,5 +1,5 @@ import { readdir, readFile, stat } from "node:fs/promises"; -import { join, dirname, resolve, extname } from "node:path"; +import { join, resolve, extname } from "node:path"; import type { TerraformPluginConfig } from "./types.js"; const SKIP_DIRS = new Set([".terraform", ".git", "node_modules", ".terragrunt-cache"]); @@ -12,7 +12,7 @@ export interface CollectedFiles { /** * Collect .tf and .tfvars files from the best-matching infrastructure location. - * Tries: explicit config path → in-project subdirs → sibling repos → project root. + * Tries: explicit config path → in-project subdirs (terraform/, infra/, etc.) → project root. */ export async function collectTfFiles( projectRoot: string, @@ -32,15 +32,7 @@ export async function collectTfFiles( if (files.tfFiles.length > 0) return files; } - // 3. Sibling infrastructure repo - const parent = dirname(projectRoot); - for (const sibling of ["infrastructure", "infra", "terraform", "deploy"]) { - const candidate = join(parent, sibling); - const files = await scanDirForTf(candidate); - if (files.tfFiles.length > 0) return files; - } - - // 4. .tf files at project root + // 3. .tf files at project root const rootFiles = await scanDirForTf(projectRoot, 1); if (rootFiles.tfFiles.length > 0) return rootFiles; diff --git a/src/plugins/terraform/index.ts b/src/plugins/terraform/index.ts index ce26eec..db1cf93 100644 --- a/src/plugins/terraform/index.ts +++ b/src/plugins/terraform/index.ts @@ -12,19 +12,19 @@ export type { TerraformPluginConfig } from "./types.js"; /** * Create a Terraform infrastructure plugin for codesight. * - * Scans .tf files — either co-located in the project or in a separate - * infrastructure repo — and generates an infrastructure section with - * deployment context for AI agents. + * Scans .tf files co-located in the project (terraform/, infra/, etc.) and generates + * an infrastructure section with deployment context for AI agents. + * Use infraPath to point at a separate infrastructure repository. * * @example - * // Auto-discover infrastructure + * // Auto-discover co-located terraform * createTerraformPlugin() * * @example - * // Explicit centralised infra repo + * // Explicit separate infra repo * createTerraformPlugin({ * infraPath: '../infrastructure', - * serviceName: 'query-service', + * serviceName: 'my-service', * }) */ export function createTerraformPlugin(config: TerraformPluginConfig = {}): CodesightPlugin { diff --git a/src/plugins/terraform/types.ts b/src/plugins/terraform/types.ts index f8efffa..99bbb4a 100644 --- a/src/plugins/terraform/types.ts +++ b/src/plugins/terraform/types.ts @@ -1,7 +1,7 @@ /** User-facing configuration for the Terraform infrastructure plugin */ export interface TerraformPluginConfig { - /** Path to infra repo — absolute or relative to project root. - * Default: auto-discovers ../infrastructure, ./terraform, ./infra, ./deploy */ + /** Path to a separate infrastructure repo — absolute or relative to project root. + * Default: auto-discovers ./terraform, ./infra, ./infrastructure, ./deploy, ./iac */ infraPath?: string; /** Override service name matching (default: project.name from package.json etc.) */ serviceName?: string; From 6c0cd8c217c852a42f29a75d15e4fc37b2c5d4c2 Mon Sep 17 00:00:00 2001 From: Neil Chambers Date: Tue, 26 May 2026 09:25:29 +0100 Subject: [PATCH 5/6] chore: bump version to 1.14.0-emerge.0 First release of the emerge fork on top of upstream Houseofmvps/codesight@v1.14.0. Adds customSections plugin API, terraform plugin (with sibling-repo discovery removed), and CI/CD plugin deltas not yet upstreamed in this exact form. Co-Authored-By: Claude Opus 4.7 (1M context) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 018ba29..da87442 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codesight", - "version": "1.14.0", + "version": "1.14.0-emerge.0", "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.", "main": "dist/index.js", "bin": { From 49e7acf3fcd34bdd2decefa1962b9b0063f3cd46 Mon Sep 17 00:00:00 2001 From: Neil Chambers Date: Tue, 26 May 2026 10:17:35 +0100 Subject: [PATCH 6/6] fix(components): do not filter custom components in workspaces named ui/ `isUIPrimitive` had a `filePath.includes("/ui/")` check intended to filter shadcn primitives. That check is too broad: it matches every file under a monorepo workspace literally named `ui/` (e.g. `ui/src/components/*.tsx`), so every custom component in such a repo was wrongly dropped. The real shadcn case (`components/ui/.tsx`) is still caught by the more specific `/components/ui/` check, and lowercase primitive file names are still caught by the `UI_PRIMITIVES` set. Vendor path checks (`@radix-ui`, `@shadcn`) are unchanged. Repro: morgan monorepo (`packages: shared, server, ui`) reported 0 components. After the fix, 130 components are detected with prop signatures extracted via AST. Includes a regression test fixture that asserts a workspace-level `ui/src/components/AppShell.tsx` is detected while a sibling `ui/src/components/ui/button.tsx` is still filtered. Co-Authored-By: Claude Opus 4.7 --- src/detectors/components.ts | 6 +++++- tests/detectors.test.ts | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/detectors/components.ts b/src/detectors/components.ts index a47e770..94978fd 100644 --- a/src/detectors/components.ts +++ b/src/detectors/components.ts @@ -63,9 +63,13 @@ const UI_PRIMITIVES = new Set([ function isUIPrimitive(filePath: string): boolean { const name = basename(filePath, extname(filePath)).toLowerCase(); + // Note: do NOT match a bare `/ui/` segment here. That collides with monorepo + // workspaces literally named `ui/` (e.g. `ui/src/components/*.tsx`), where + // every custom component would be wrongly filtered. The real shadcn case is + // `components/ui/` (caught below) or lowercase primitive filenames (caught + // via UI_PRIMITIVES). Vendor paths (`@radix-ui`, `@shadcn`) are kept. return ( UI_PRIMITIVES.has(name) || - filePath.includes("/ui/") || filePath.includes("/components/ui/") || filePath.includes("@radix-ui") || filePath.includes("@shadcn") diff --git a/tests/detectors.test.ts b/tests/detectors.test.ts index 857828f..593fec2 100644 --- a/tests/detectors.test.ts +++ b/tests/detectors.test.ts @@ -371,6 +371,43 @@ describe("Component Detection", async () => { assert.ok(components.length >= 2); assert.ok(components.some((c: any) => c.name === "UserProfile" && c.props.includes("name"))); }); + + it("detects components in a workspace named `ui/` without filtering them as shadcn primitives", async () => { + // Regression: a bare `/ui/` segment in the file path used to trigger the + // shadcn-primitive filter, which wrongly dropped every custom component + // in monorepos whose UI package is literally named `ui/` (e.g. morgan). + // The real shadcn case lives at `components/ui/.tsx` and is + // still filtered here. + const dir = await writeFixture("ui-workspace-app", { + "package.json": JSON.stringify({ name: "root", private: true }), + "pnpm-workspace.yaml": "packages:\n - ui\n", + "ui/package.json": JSON.stringify({ + name: "@app/ui", + dependencies: { react: "^19.0.0" }, + }), + // Custom app component inside the `ui` workspace. PascalCase name, so + // the UI_PRIMITIVES filename filter must not catch it either. + "ui/src/components/AppShell.tsx": `interface AppShellProps { children: React.ReactNode; width?: "default" | "narrow" } +const AppShell = ({ children, width = "default" }: AppShellProps) => { + return
{children}
; +}; +export default AppShell;`, + // shadcn primitive at the canonical path. Must still be filtered. + "ui/src/components/ui/button.tsx": `export const Button = ({ label }: { label: string }) => ;`, + }); + const project = await mods.detectProject(dir); + assert.equal(project.componentFramework, "react", "react workspace dep should be aggregated"); + const files = await mods.collectFiles(dir); + const components = await mods.detectComponents(files, project); + assert.ok( + components.some((c: any) => c.name === "AppShell"), + `expected AppShell to be detected, got: ${components.map((c: any) => c.name).join(", ") || ""}`, + ); + assert.ok( + !components.some((c: any) => c.name === "Button"), + "shadcn primitive at components/ui/button.tsx should still be filtered", + ); + }); }); // =================== DEPENDENCY GRAPH TESTS ===================