| 41 | /** Cheap filesystem + dependency sniff. Each check is fs.access on a fixed path — |
| 42 | * a handful of stat calls, no directory walks. Best-effort: errors mean "absent". */ |
| 43 | export async function gatherInfraSignals(cwd: string, signals: ProjectSignals): Promise<InfraSignals> { |
| 44 | const exists = async (p: string) => { |
| 45 | try { await fs.access(path.join(cwd, p)); return true; } catch { return false; } |
| 46 | }; |
| 47 | const deps = new Set((signals.deps ?? []).map(d => d.toLowerCase())); |
| 48 | |
| 49 | const [dockerfile, composeYml, composeYaml, ghWorkflows, gitlabCi, circleci, openapiY, openapiJ, swaggerY, awsDir] = |
| 50 | await Promise.all([ |
| 51 | exists('Dockerfile'), exists('docker-compose.yml'), exists('compose.yaml'), |
| 52 | exists('.github/workflows'), exists('.gitlab-ci.yml'), exists('.circleci'), |
| 53 | exists('openapi.yaml'), exists('openapi.json'), exists('swagger.yaml'), |
| 54 | exists('.aws'), |
| 55 | ]); |
| 56 | |
| 57 | return { |
| 58 | hasDocker: dockerfile || composeYml || composeYaml, |
| 59 | hasCi: ghWorkflows || gitlabCi || circleci, |
| 60 | hasOpenApi: openapiY || openapiJ || swaggerY, |
| 61 | hasMediaDeps: ['ffmpeg', 'fluent-ffmpeg', 'ffmpeg-static', 'sharp'].some(d => deps.has(d)), |
| 62 | hasBackendDeps: ['express', 'fastify', 'koa', '@nestjs/core', 'hono', 'h3'].some(d => deps.has(d)), |
| 63 | hasCloudConfig: awsDir, |
| 64 | }; |
| 65 | } |
| 66 | |
| 67 | /** Tool groups this layer manages, with their member-name prefixes/names and the |
| 68 | * prompt words that force-keep them. Browser/dev-server tools are deliberately |